⛰ JavaScript 原型继承
当已经存在一个 user 对象以及其属性与方法,想要将 admin 和 guest 作为基于 user 稍加修改的变体,在 user 基础之上构建一个新的对象。
JavaScript 的 原型继承(Prototype inheritance) 特性帮助实现这个需求。
# [[Prototype]] 属性
在 JavaScript 中,对象有一个特殊的 隐藏属性 [[Prototype]] :
要么为
null;要么就是对另一个对象的引用。该对象被称为「原型」。
当尝试从一个对象 object 中获取一个 缺失的属性 时, JavaScript 会自动从 原型 中获取该属性。这个过程为「原型继承」。
对象的 [[Prototype]] 属性是 在内部的 并且是 隐藏的。
🌰 例子:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
2
3
4
5
6
7
8
当从 rabbit 中读取一个它不存在的属性,JavaScript 会从 animal 中获取:
console.log(rabbit.eats) // true
这个例子中的
rabbit__proto__ = animal将animal设置为rabbit的 原型。原来的rabbit对象并不存在eats属性,JavaScript 会顺着[[Prototype]]引用 在animal中查找(自下而上)。这个例子可以称 「
animal是rabbit的原型」 或者 「rabbit的原型是从animal继承而来的 」。当animal中有的属性和方法,都会自动变为在rabbit中可以使用(继承属性和方法)。
当 animal 中有方法:
let animal = {
eats: true,
walk() {
console.log('animal walk')
}
}
2
3
4
5
6
rabbit.walk() // animal walk
# 原型链
🌰 例子:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现在,如果从
longEar中读取一些它不存在的内容,JavaScript 会先在rabbit中查找,然后在animal中查找。但是要遵循以下规则:
- 引用不能形成闭环。试图在闭环内分配
__prototype__会报错。__proto__的值可以是 对象,也可以是null。而其他的类型都会被忽略。- 显然,一个对象只能有一个
[[prototype]]。一个对象不能从其他两个对象中即成。
__proto__是[[prototype]]的访问器(getter/setter)(__proto__与[[prototype]]要区别开):
__proto__属性有点过时了,它的存在是出于历史的原因。现代编程语言建议应该使用函数Object.getPrototypeOf/Object.setPrototypeOf来取代__proto__去 get/set 原型。
# 原型仅用于读取属性
🌰 例子:
let animal = {
eats: true,
walk() {
// ...
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk() // Rabbit! Bounce-bounce!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这个例子中,
rabbit中分配了自己的walk方法。尽管
rabbit的原型是animal。但是因为rabbit本身就有walk方法,所以可以在对象中找到该方法无需使用原型。
尽管设置了原型在先,但是后来的 添加方法 操作,是在对象子身上进行的,不会再原型上进行(对于删除属性同理)。
可以看出,原型仅用于读取属性。
🌰 例子 / 访问器属性是 例外。因为分配操作是通过 setter 函数处理的,所以写入此属性的操作相当于 读取该属性。
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
};
console.log(admin.fullName) // John Smith
admin.fullName = "Alice Cooper"
console.log(admin.fullName) // Alice Cooper
console.log(user.fullName) // John Smith
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
admin.fullName = "Alice Cooper"可以正常运行,因为它的原型对象中有fullName的setter函数。并且修改的是
admin原型对象的内容,而不是 原来的对象(被保护)。
# this 的值
this 不受原型的影响:无论在哪里找到该方法(在一个对象或者在原型中),在方法调用时, this 始终指向的是 . 前面的对象(调用的该方法的对象)。
从上面的例子中可以知道,在 setter 被调用时, admin.fullName = '...' 是 admin 调用的方法,所以 this 指向的是 admin 。
提示
清楚 this 的值。有可能有一个带有很多方法的大对象,并且还有从其继承的对象。当继承的对象运行方法时,它们应该只修改自身的状态而不修改大对象的状态。
🌰 例子 / 有一个方法是存储属性:
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
rabbit.sleep();
console.log(rabbit.isSleeping) // true
console.log(animal.isSleeping) // undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
原型方法共享,但是对象状态不是。
# 循环
🌰 例子:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// 对于 Object.keys
console.log(Object.keys(rabbit)) // jumps
// 对于 for...in
for(let prop in rabbit) console.log(prop) // jumps, eats
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看出
for ... in循环会迭代 继承的属性。
如果想 排除继承的属性 或者利用它们进行其他操作, 可以使用 obj.hasOwnProperty(key) 判断:如果 obj 具有自己的(非继承的) key 属性则返回 true 。
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop)
if(isOwn) {
console.log(`our: ${prop}`)
} else {
console.log(`inherited: ${prop}`)
}
}
2
3
4
5
6
7
8
9
hasOwnProperty方法是rabbit从Object.prototype中继承的(该方法是Object.prototype.hasOwnProperty提供的)。并且该方法是不可枚举的,所以for ... in没有列出。
提示
几乎所有的 其他键 / 值获取方法 都忽略继承的属性(例如 Object.keys 和 Object.values 等)。它们只会对对象 自身 进行操作。不考虑 继承自原型的属性。
# 总结
[[prototype]]是 对象 的隐藏属性,要么是另一个对象,要么为null。可以通过obj.__proto__访问。- 通过
[[Prototype]]引用的对象被称为「原型」。 - 当读取
obj的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。- 写 / 删除操作直接在 对象自身 上进行,不使用原型。
this的值是 谁调用该方法,就是哪个对象。
for ... in循环时,除了自身的属性,继承的属性也会迭代列出。所有其他的键 / 值获取方法仅对对象本身起作用。
# 实例
# 搜索链
let head = { glasses: 1 }; let table = { pen: 3 }; let bed = { sheet: 1, pillow: 2 }; let pockets = { money: 2000 };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 使用
__proto__来分配原型,以使得任何属性的查找都遵循以下路径:pockets→bed→table→head。例如,pockets.pen应该是3(在table中找到),bed.glasses应该是1(在head中找到)。- 通过
pockets.glasses或head.glasses获取glasses,哪个更快?必要时需要进行基准测试。
点击查看
- 在现代引擎中,从性能的角度来看,是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。
- 对于
pockets.glasses来说,它们(引擎)会记得在哪里找到的glasses(在head中),这样下次就会 直接在这个位置进行搜索。并且引擎足够聪明,一旦有内容更改,它们就会自动更新内部缓存,因此,该优化是安全的。
# 仓鼠例子
有两只仓鼠:
speedy和lazy都继承自普通的hamster对象。当我们喂其中一只的时候,另一只也吃饱了。为什么?如何修复它?
let hamster = { stomach: [], eat(food) { this.stomach.push(food); } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; speedy.eat("apple"); console.log(speedy.stomach) // apple console.log(lazy.stomach) // apple1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
点击查看
由于每只仓鼠没有自身的 stomach ,在 push 操作时,只能顺着原型链找到 hamster 的 stomach 。
push操作的是在原型链找到的stomach。但是 简单赋值this.stomach时不会出现这个种情况:// ... eat(food) { this.stomach = [food] } // ...1
2
3
4
5因为
this.stomach =。不会执行对stomach的查找。该值会被直接写入this对象。
可以通过确保每只仓鼠都有自己的 stomach 解决:
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster,
stomach: []
};
let lazy = {
__proto__: hamster,
stomach: []
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
所有描述特定对象状态的属性,例如上面的 stomach ,都应该被写入该对象中。这样可以避免此类问题。
