继承是面向对象编程的特征之一(另外两个是 封装、多肽)
继承的方式有: 接口继承 和 类继承;接口继承的是方法签名,类继承的是实际的方法。ESCMAScript 中 只支持类继承,而 js 中的类继承是通过原型链来实现的。其基本思想就是通过原型继承多个引用类型的属性和方法
原型链
每个构造函数都有一个原型对象,原型有一个属性指向构造函数,而实例有一个内部指针指向原型。
如果原型是另一个类型的实例呢,那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这样就在实例和原型之间构建了一条原型链,这就是原型链的基本构思。
原型链的顶端
默认情况下,所有引用类型都继承 Object,这也是通过原型链实现的,任何函数的默认原型都是一个 Object 实例,这意味着这个实例有一个内部指针指向 Object.prototype, 这也是为什么自定义类型能够继承包括 toSting(), valueOf() 等默认方法的原因
实例的检查
原型与实例的关系可以通过 instanceof 操作符 和 isPrototypeOf 来检查
- 如果一个实例的原型链中出现过相应的构造函数,则 instanceOf 返回true
- xxx.isPrototypeOf(instance) 只要 instance 的原型链中 包含了 xxx 这个原型,则返回 true
const { log } = console;
function Super() {
this.attr = "super";
}
Super.staticSuperFn = function () {
log("super_static_fn");
};
Super.prototype.getSuperValue = function () {
return this.attr;
};
function Sub() {
// 通过 this.xxx = yyy 是挂在到实例上的属性或方法
this.subAttr = "sub";
}
// 直接挂在到 构造函数上的 是静态方法,属于构造函数的
Sub.staticSubFn = function () {
log("sub_static_fn");
};
// Sub 继承 Super,这个赋值重写了 Sub 最初的原型,将其替换为 Super 的实例
// 意味这 Super 实例可以访问的所有属性和方法也会存在于 Sub.prototype
Sub.prototype = new Super();
// Sub.prototype 已经指向了 new Super(),这样实现继承之后,相当于给 Super 的实例 添加了一个新方法
Sub.prototype.getSubValue = function () {
return this.subAttr;
};
const instance = new Sub();
log(instance.getSubValue()); // log: sub, getSubValue 挂在到 new Super() 产生的实例上的
log(instance.getSuperValue()); // log: super, getSuperValue 是挂在到 Super.prototype 上的
log(instance.constructor); // [Function: Super] { staticSuperFn: [Function (anonymous)] }
// instance 原型链中包含了 Sub,Super, Object 构造函数,所以都是 true
log(instance instanceof Sub); // log: true
log(instance instanceof Super); // log: true
log(instance instanceof Object); // log: true
// instance 原型链中都包含了 Sub/Super/Object d的 prototype ,所以都返回 true
log(Sub.prototype.isPrototypeOf(instance));
log(Super.prototype.isPrototypeOf(instance));
log(Object.prototype.isPrototypeOf(instance));
如上代码之间的原型关系如下:
图解: 因为 Sub的 prototype 被重写为了 new Super(), 这里暂时叫做 superInstance, superInstance 即为 Super 的实例,实例拥有 [[Prototype]] 指向其 构造函数 Super的 prototype,因此 根据原型链的查找(看箭头)可以看出, superInstance 的 constructor 为 Super,同理 instance 的指向 Sub prototype 及 superInstance, 所以 instance 也能获取其原型链上的属性和方法
原型搜索机制,在读取实例上的属性时,首先会在实例上搜索这个属性,如果没有找到,则会继续所搜实例的 [[Prototype]] 指向,在原型链上一层一层搜索,直到搜索到或者找到最顶层为止。
原型链继承的注意事项
- 通过原型链实现继承,需要在原型赋值之后再添加到重写的原型上,不然会有方法的丢失
- 原型中包含的属性或方法在实例间共享,如果原型中的属性是引用值时,要注意某个实例修改后其他实例共享的值也跟着意外变化的情况
function Parent() {
this.pAttr = "parent";
/**
* 如果 Son 中没有自己的 friends 属性,而某个 Son 实例对 friends 进行更改了,
* 这都将会 造成其他 Son 实例 造成意外的 friends 突变;
* 这是 原型链继承的 弊端
*/
this.friends = ["p_jake1", "p_jake2"];
}
Parent.prototype.protoFn = function () {
log("parent_prototype_fn");
};
function Son() {
this.attr = "son";
}
// invalidBeforeRewriteSonPrototype 在重写 prototype 之前挂在是无效的
Son.prototype.invalidBeforeRewriteSonPrototype = function () {
log("invalid");
};
// 原型链继承, 重写 Son.prototype
Son.prototype = new Parent(); // 在这一行之前为 Son.prototype 上挂在属性或方法将会无效
Son.prototype.validAfterRewriteSonPrototype = function () {
log("It's ok!");
};
const son1 = new Son();
son1.validAfterRewriteSonPrototype(); // log: It's ok! son1.protoFn(); // log: parent_prototype_fn
try {
son1.invalidBeforeRewriteSonPrototype();
} catch (err) {
log("err-> ", err); // log: err-> TypeError: son1.invalidBeforeRewriteSonPrototype is not a function
}
log("pop before>>", son1.friends); // log: pop before>> [ 'p_jake1', 'p_jake2' ]
son1.friends.pop(); // 将最后一个 friend 元素删掉,造成其他实例共享 friends 突变了
log("pop after>>", son1.friends); // log: pop after>> [ 'p_jake1' ]
const son2 = new Son();
// OMG son2 懵逼为啥自己为啥丢失了从 Parent 继承的朋友(是因为 son1 改变了 继承的 friends)
log("son2.friends", son2.friends); // log: son2.friends [ 'p_jake1' ];