在编程中,我们经常希望获取某些内容并对其进行扩展。
例如,我们有一个具有其属性和方法的 user
对象,并希望将 admin
和 guest
作为其经过轻微修改的变体。我们希望重复使用我们在 user
中的内容,而不是复制/重新实现其方法,只需在其之上构建一个新对象。
原型继承 是一个有助于实现此目的的语言特性。
[[Prototype]]
在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]]
(如规范中所述),该属性要么为 null
,要么引用另一个对象。该对象称为“原型”
当我们从 object
读取一个属性,并且该属性不存在时,JavaScript 会自动从原型中获取它。在编程中,这称为“原型继承”。我们很快将学习此类继承的许多示例,以及建立在其之上的更酷的语言特性。
属性 [[Prototype]]
是内部的并且是隐藏的,但是有很多方法可以设置它。
其中一种方法是使用特殊名称 __proto__
,如下所示
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal
现在,如果我们从 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
的原型。
然后,当 alert
尝试读取属性 rabbit.eats
(**)
时,它不在 rabbit
中,因此 JavaScript 遵循 [[Prototype]]
引用并在 animal
中找到它(从下往上看)
在这里我们可以说“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)
现在,如果我们从 longEar
中读取一些内容,并且它不存在,JavaScript 将在 rabbit
中查找它,然后在 animal
中查找它。
只有两个限制
- 引用不能循环。如果我们尝试以循环方式分配
__proto__
,JavaScript 将抛出错误。 __proto__
的值可以是对象或null
。其他类型将被忽略。
它可能很明显,但仍然:只能有一个 [[Prototype]]
。一个对象不能从两个其他对象中继承。
__proto__
是 [[Prototype]]
的历史 getter/setter初学者开发者不知道这两个之间的区别是一个常见的错误。
请注意,__proto__
与 内部 [[Prototype]]
属性不同。它是 [[Prototype]]
的 getter/setter。稍后我们将看到它重要的场景,现在让我们记住它,因为我们在构建对 JavaScript 语言的理解。
__proto__
属性有点过时。它存在于历史原因,现代 JavaScript 建议我们使用 Object.getPrototypeOf/Object.setPrototypeOf
函数来获取/设置原型。我们稍后也会介绍这些函数。
根据规范,__proto__
只能由浏览器支持。但事实上,包括服务器端在内的所有环境都支持 __proto__
,因此我们使用它非常安全。
由于 __proto__
表示法更直观,我们在示例中使用它。
写入不使用原型
原型仅用于读取属性。
写入/删除操作直接作用于对象。
在下面的示例中,我们将自己的 walk
方法分配给 rabbit
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()
调用会在对象中立即找到该方法并执行它,而无需使用原型
访问器属性是一个例外,因为赋值由 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"; // (**)
alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected
在行 (*)
中,属性 admin.fullName
在原型 user
中有一个 getter,因此会调用它。而在行 (**)
中,该属性在原型中有一个 setter,因此会调用它。
“this” 的值
在上面的示例中可能会出现一个有趣的问题:set fullName(value)
中 this
的值是什么?属性 this.name
和 this.surname
写入到哪里:user
还是 admin
?
答案很简单:this
完全不受原型影响。
无论在何处找到该方法:在对象还是其原型中。在方法调用中,this
始终是点之前的对象。
因此,setter 调用 admin.fullName=
使用 admin
作为 this
,而不是 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
时,它将存储到这些对象中。
因此,方法是共享的,但对象状态不是。
for…in 循环
for..in
循环也会迭代继承的属性。
例如
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps
// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats
如果这不是我们想要的,并且我们希望排除继承的属性,那么有一个内置方法 obj.hasOwnProperty(key):如果 obj
有自己的(未继承的)名为 key
的属性,则返回 true
。
因此,我们可以过滤掉继承的属性(或对它们执行其他操作)
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
这里我们有以下继承链:rabbit
继承自 animal
,后者继承自 Object.prototype
(因为 animal
是一个字面量对象 {...}
,所以它默认如此),然后是它上面的 null
请注意,有一件有趣的事情。rabbit.hasOwnProperty
方法来自哪里?我们没有定义它。查看链,我们可以看到该方法由 Object.prototype.hasOwnProperty
提供。换句话说,它是继承的。
…但是,如果 for..in
列出继承的属性,为什么 hasOwnProperty
不像 eats
和 jumps
那样出现在 for..in
循环中?
答案很简单:它不可枚举。就像 Object.prototype
的所有其他属性一样,它有 enumerable:false
标志。而 for..in
只列出可枚举的属性。这就是为什么它和 Object.prototype
的其他属性没有列出的原因。
几乎所有其他键/值获取方法,例如 Object.keys
、Object.values
等都忽略继承的属性。
它们只对对象本身进行操作。不考虑原型中的属性。
总结
- 在 JavaScript 中,所有对象都有一个隐藏的
[[Prototype]]
属性,它可能是另一个对象或null
。 - 我们可以使用
obj.__proto__
访问它(一个历史悠久的 getter/setter,还有其他方法,很快就会介绍)。 - 由
[[Prototype]]
引用的对象称为“原型”。 - 如果我们想读取
obj
的属性或调用方法,但它不存在,那么 JavaScript 会尝试在原型中找到它。 - 写/删操作直接作用于对象,它们不使用原型(假设它是一个数据属性,而不是一个 setter)。
- 如果我们调用
obj.method()
,并且method
是从原型中获取的,则this
仍然引用obj
。因此,即使方法是继承的,它们也始终与当前对象一起工作。 for..in
循环会遍历它自己的和它继承的属性。所有其他键/值获取方法仅对对象本身进行操作。
评论
<code>
标签,对于多行 - 将其包装在<pre>
标签中,对于超过 10 行 - 使用沙盒(plnkr,jsbin,codepen…)