原型继承定义:每个对象都有一个特殊的隐藏属性 [[Prototype]]
,其中存储其它对象(称为原型)的引用。当访问一个对象的属性缺失时,JavaScript引擎会沿着一个由对象通过[[Prototype]]
属性链接起来的原型链向上查找,直到找到该属性!或到达链的末端(null)。
对象直接继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
let animal = {
eats: true
}
let rabbit = {
jumps: true
}
rabbit.__proto__ = animal; // 设置 rabbit 的原型为 animal
console.log(rabbit.eats); // true, 通过原型链找到animal的eats
// 修改 animal.eats,rabbit.eats 也会随之改变
animal.eats = 'yummy';
console.log(rabbit.eats); // 'yummy'
// 反之,如果在 rabbit 上直接赋值,会创建自身的属性,从而屏蔽原型上的属性
rabbit.eats = false;
console.log(rabbit.eats); // false (现在访问的是 rabbit 自己的属性)
console.log(animal.eats); // 'yummy' (原型的属性未被修改)
|
等价方法
直接修改对象的原型(使用 proto 或 Object.setPrototypeOf)会干扰 JavaScript 引擎的优化
优先使用 Object.create() 来建立原型继承链。
Object.create(proto)
:在创建新对象的那一刻就为其设置原型。
Object.setPrototypeOf(obj, proto)
:在对象已经被创建之后,再去修改它的原型。
Object.getPrototypeOf(obj)
:获取对象的原型
现在要创建具有指定原型对象的新对象,使用 Object.create(proto)
它不是拷贝,也没有复制 animal 的任何属性到 rabbit 上。它是创建了一个全新的空对象 rabbit,然后将 rabbit 的内部属性 [[Prototype]]
链接到 animal 对象上
因为属性没有被复制,所有通过 Object.create(animal) 创建的对象都共享同一个 animal 对象作为原型。修改 animal.eats,所有这些对象的 eats 属性值都会改变
1
2
3
4
5
6
7
8
9
10
|
let animal = {
eats: true
};
// 创建一个新对象 rabbit,并将其原型设置为 animal
let rabbit = Object.create(animal);
rabbit.jumps = true; // 初始化 rabbit 自己的属性
console.log(rabbit.eats); // true (来自原型)
console.log(rabbit.jumps); // true (来自自身)
|
如果只关心“最终对象的状态”,这些继承方法是等价的
使用构造函数继承
通过构造函数创建实例
还可以使用 function 和 new 来创建一个新对象,这里的 function 就是构造函数。
其底层实现依然是靠原型链来让实例 rabbit 找到 sleep 方法的。
看起来更麻烦了,但是适合大规模复用,是 class 出现之前社区摸索出的能较好模拟类式继承的方式。
折腾一晚上,我觉得困扰我其实是类似于先鸡后蛋的问题: 构造函数的原型 prototype 是哪里来的?函数和原型谁先出现?内容是什么?
每一个构造函数在创建时,JavaScript 都会自动为它创建一个对应的 prototype 属性(一个新对象)。
这个自动创建的 prototype 对象并不是空的,它自带一个属性:constructor 指向这个构造函数本身。
这意味着在不做任何操作的情况下,Animal.prototype.constructor === Animal
已经是成立的。
这个原型Animal.prototype不来自原本存在的对象,不会造成“其他”继承被修改,但它会精确地影响所有通过 new Animal()
创建的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 构造函数
function Animal() {
this.eats = true;
}
// 在构造函数的 prototype 上添加方法
Animal.prototype.sleep = function() { console.log('Zzz'); };
// 创建实例 dog 和 cat
let dog = new Animal();
let cat = new Animal();
console.log(dog.sleep); // undefined, 此时还没有 sleep 方法
// 现在给 Animal 的 prototype 添加方法
Animal.prototype.sleep = function() { console.log('Zzz'); };
dog.sleep(); // 'Zzz'
cat.sleep(); // 'Zzz'
|
new 操作符
对执行 new Animal() 创建实例时发生的事情进行模拟:
1
2
3
4
5
6
7
8
9
10
11
12
|
function New(fn, ...args) {
if (typeof fn !== 'function') {
throw new TypeError('First argument must be a function');
}
// 实例在这一步后能够访问fn.prototype的属性
const obj = Object.create(fn.prototype);
// 执行构造函数,实例在这一步获得fn的属性
const ret = fn.apply(obj, args);
//
return ret instanceof Object ? ret : obj;
}
|
图示
红色卡片为构造函数,黑色为对象(原型or实例)
Object.prototype 是所有普通对象的最终原型
通过构造函数实现的继承
因为比直接使用原型对象继承多了两个属性 prototype 和 constructor ,在写好子类的构建函数后,像刚才说的默认设置 Rabbit.prototype 指向的原型是个除了包含 constructor = Rabbit 之外一无所有的新对象,并不认识Animal的原型
想实现对象之间的原型链,所以需要手动给它们牵线搭桥…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function Animal() {
this.eats = true;
}
Animal.prototype.sleep = function() { console.log('Zzz'); };
// 子"类"构造函数
function Rabbit() {
// 调用父类构造函数来初始化,只有父类实例属性没有原型属性
Animal.call(this); // 相当于 `this.eats = true
// 添加“子类实例属性”
this.jumps = true;
}
// 设置原型链前
let rabbit1 = new Rabbit();
console.log(rabbit1.sleep); // undefined (无法访问)
// 建立原型链以实现继承 将默认对象替换成指向Animal.prototype的对象
Rabbit.prototype = Object.create(Animal.prototype) // 这句之后rabbit才能找到.sleep方法
Rabbit.prototype.constructor = Rabbit // 原本的constructor被替换没了,手动添
// 设置原型链后
let rabbit2 = new Rabbit();
rabbit2.sleep(); // "Zzz" (可以访问)
|
图示
建立原型链过程,顺序从左向右
绿色线条是原型链,黄色蓝色分别对应 prototype 和 constructor
使用class的继承
ES6用来实现原型继承的新方法,虽然教程说 class 不仅仅是语法糖,我心急,是非对错已无心分辨…至少不用手动设置原型链了,就当它是语法糖吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Animal { // class实际上是一个function
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
speak() {
console.log(`${this.name} makes a sound.`);
}
// 静态方法
static info() {
console.log('Animals are living organisms.');
}
}
const dog = new Animal('Rex', 3);
dog.speak(); // "Rex makes a sound."
Animal.info(); // "Animals are living organisms."
|
使用 class 语法时生成的原型对象仍然具有 constructor 属性。
使用 extends
关键字时,自动正确处理原型链,包括 constructor 属性的设置,无需手动。
至于 new function 用法的构造函数,差不多变成了class本身的 constructor 方法,这下终于实至名归了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age); // 调用父类的constructor
this.breed = breed;
}
// 重写父类方法
speak() {
console.log(`${this.name} barks!`);
}
// 新增
fetch() {
console.log(`${this.name} fetches the ball.`);
}
}
const myDog = new Dog('Buddy', 5, 'Golden Retriever');
myDog.speak(); // "Buddy barks!"
myDog.fetch(); // "Buddy fetches the ball."
|