实际编程中,我们经常需要一些东西并扩展之。例如,我们有
user
对象,带有属性和方法,现在想要admin
和guest
,和其稍微有些变化,我们最好重用user
对象,但不是复制/重新实现它的方法,而是在其基础上构建。原型继承是Javascript重要特性,可以实现之。
[[Prototype]]
在Javascript中,对象有个特殊的隐藏属性[[Prototype]]
(规范中的名称),其可以为null或引用其他对象,该对象称为原型:
这个[[Prototype]]
有个魔力的意思,当我们从对象中读取属性,如果没有找到,Javascript自动从其原型中查找。在编程中,这样机制被称为“原型继承”。很多酷的语言和编程技术是基于该机制。
属性[[Prototype]]
是内在的且隐藏的,但是有很多方式去设置它。
其一是使用__proto__
方式,代码如下:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
请注意,__proto__
和 [[Prototype]]
不同,后者是前者的getter/setter访问器,后面我们讨论其他方式,现在使用__proto__
够用。
如果我们在rabbit
中查找属性,没有发现,Javascript自动从animal
中查找。示例:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
这里,我们说animal
是rabbit
的原型,或rabbit
原型继承自animal
对象。
所以如果animal
有许多有用的属性和方法,那么自动成为rabbit
对象的属性和方法,这些是继承的。
如果animal
有一个方法,可以在rabbit
中调用:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk is taken from the prototype
rabbit.walk(); // Animal walk
原型链可以更长:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
}
// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)
实际上有两个限制:
1、不能循环引用。Javascript抛出错误,如果__proto__
循环引用。
2、__proto__
的值,只能赋值为对象或null,所有其他值(原始值)被忽略。
另外显而易见,只有有一个[[Prototype]]
,不支持多继承。
读/写规则
原型仅用于reading属性。
对于数据属性(不是getter/setter访问器),写/删除操作直接通过对象实现。下面的例子,我们给rabbit
自己的walk
方法赋值:
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
}
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
现在,rabbit.walk()
在自己内部查找方法并立刻调用,没有使用原型方法。
对于getter/setter访问器,如果我们读写属性,他们在原型中查找并执行。示例,留意代码中的admin.fullName
属性。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
星号()行属性admin.fullName
,在原型user
中有getter访问器,所以他可以调用,(*)行属性在原型中有setter访问器,所以也可以调用。
上面的示例可能提出有趣的问题,在setfullName(value)
内部this的值是什么? this.name
和this.surname
是那个对象的属性,user
或admin
?
答案是简单的:this根本不受原型影响。
无论方法出现在哪里,对象或原型。调用方法时,this总是“.”号前面的那个对象。
所以,setter是有admin调用,this是admin,不是user。
这实际是超级重要的事情,因为我们可能有一个大对象,带有很多方法,从它继承。那么我们能调用它的方法在子对象上,并修改子对象,而不是那个大对象。举例,这里animal
代表方法库,rabbit
使用他们。
调用rabbit.sleep()
在rabbit对象上,通过设置了 this.isSleeping
:
// animal has methods
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// modifies rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)
如果我们有其他对象bird
,snake
等继承自animal
,他们也获得animal的方法。但this在每个方法中和调用其对象一致,是运行时确定(.前面的对象),不是animal。所以当我们写数据至this
,它实际存在在那些调用的子对象中。
结论是:方法是共享的,但对象状态不是。
[[Prototype]]
属性,其值只能是其他对象或null.obj
的属性或调用方法,它不存在,那么JavaScript尝试去原型中查找. Write/delete 属性直接在对象上运行, 他们不使用原型 (除非属性确实是setter访问器).