发布于 

全面解析JavaScript中的原型

JavaScript 是一门面向对象的语言,但与其他面向对象语言 Java、python (基于类继承) 不同,它的继承方式是基于原型实现的。

原型

几乎每个JavaScript对象都有另一个与之关联的对象,这个对象被称为原型 ( prototype ),第一个对象从这个原型继承属性。

通过对象字面量创建的所有对象都有相同的原型对象,在JavaScript代码中可以通过 Object.prototype 引用这个原型对象。使用 new 关键字和构造函数调用创建的对象,使用构造函数 prototype 属性的值作为它们的原型。换句话说,使用 new Object () 创建的对象继承来自 Object.prototype 属性的值作为它们的原型。换句话说,使用 new Object () 创建的对象继承来自 Object.prototype ,与通过 {} 创建的对象一样。类似地,通过 new Array () 创建的对象以 Array.prototype 为原型,通过 new Date () 创建的对象以Date.prototype 为原型。

注意:几乎所有对象都有原型,但只有少数对象有 prototype 属性。正是这些有 prototype 属性的对象为所有其他对象定义了原型。

原型链

Object.prototype 是为数不多的没有原型的对象,因为它不继承任何属性。其他原型对象都是常规对象,都有自己的原型。多数内置构造函数 ( 和多数用户定义的构造函数 ) 的原型都继承自 Object.prototype 。例如,Date.prototype 从 Object.prototype 继承属性,因此通过 new Date () 创建的日期对象从 Date.prototype 和 Object.prototype 继承属性。这种原型对象链接起来的序列被称为原型链。

创建一个指定原型的对象

Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const person = {
isHuman: false,
name:'who',
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};

//me对象的原型为person
const me = Object.create(person);

me.name = 'Matthew';
me.isHuman = true;
me.printIntroduction();

console.log(me.__proto__ == person) //true
//在me对象添加name属性不会影响到原始对象
console.log(me.name)//Matthew
console.log(person.name)//who

Object.create() 的一个用途是防止对象被某个第三方库函数意外修改。这种情况下,不要直接把对象传给库函数,而要传入一个继承自它的对象。如果函数读取一个对象的属性,可以读到继承的值。而如果它设置这个对象的属性,则修改不会影响原始对象。

1
2
let o = { x: "do not change this value" };
library.function(Object.create(o))

原型与继承

JavaScript 对象有一组 “自有属性” ,同时也从它们的原型对象继承一组属性。要理解这一点,必须更详细地分析属性存取。下面我将使用 Object.create() 函数以指定原型来创建对象。

假设要从对象 o 中查询属性 x 。如果 o 没有叫这个名字的自有属性,则会从 o 的原型对象查询属性 x 。如果原型对象也没有叫这个名字的自有属性,但它有自己的原型,则会继续查询这个原型的原型。这个过程一直持续,直至找到属性 x 或者查询到一个原型为 null 的对象。可见,对象通过其 prototype 属性创建了一个用于继承属性的链条或链表:

1
2
3
4
5
6
7
8
9
let o = {}              // o从Object.prototype继承对象方法
o.x = 1; //现在它有了自有属性x
let p = Object.create(o);// p从o和Object.prototype继承属性
p.y = 2; // 而且有一个自有属性y
let q = Object.create(p);// q从p、o和Object.prototype继承属性
q.z = 3; // 且有一个自有属性z
let f = q.toString() //toString继承自Object.prototype
console.log(f); // => [object Object]
console.log(q.x + q.y) // => 3; x和y分别继承自o和p

现在假设你为对象 o 的 x 属性赋值。 如果 o 有一个名为 x 的自有 (非继承) 属性,这次赋值就会修改已有 x 属性的值。否则,这次赋值会在对象 o 创建一个名为 x 的新属性。如果 o 之前继承了属性 x ,那么现在这个继承的属性会被新创建的同名属性隐藏。

属性赋值查询原型链只为确定是否允许赋值。如果 o 继承了一个名为 x 的只读属性,则不允许赋值。

如:

1
2
3
4
5
6
7
8
9
const me = Object.create(person,{
gender:{
writable:false,
value:'male'
}
});

me.gender = 'female' //无效
console.log('性别',me.gender)// male

不过,如果允许赋值,则只会在原始对象上创建或设置属性,而不会修改原型链中的对象。查询属性时会用到原型链,而设置属性时不影响原型链是一个重要的JavaScript特性,利用这一点,可以选择性地覆盖继承的属性:

1
2
3
4
5
let unitcircle = { r: 1 };           //c继承自的对象
let c = Object.create(unitcircle); //c继承了属性r
c.x = 1; c.y = 1; //c定义了两个自有属性
c.r = 2; //c覆盖了它继承的属性s
unitcircle.r // => 1; 原型不受影响

如果 o 继承了属性 x ,而该属性是通过一个设置方法定义的访问器属性 (get/set) ,那么就会调用该设置方法而不会在 o 上创建新属性 x 。要注意,此时会在对象 o 上而不是在定义该属性的原型对象上调用设置方法。因此如果这个设置方法定义了别的属性,那也会在 o 上定义同样的属性,但仍然不会修改原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const person = {
name:'who',
get getName(){
return 'I am '+this.name
},

};

const me = Object.create(person,{
gender:{
writable:false,
value:'male'
}
});
me.name = 'Matthew';

console.log(me.getName) //Matthew
console.log(person.getName) //who