继承相关知识还是有点乱,来总结一下。
本质上,JS里面的继承都是通过原型链实现的(除了实例属性之外),原型链继承的关键就是对象的__proto__
属性,它应该指向另外一个对象的prototype
构造函数的继承
ES5里面最常用的继承方法是以下两种:
- 在子类内部执行父类(
call
/apply
改变this
),继承实例属性 - 子类的
prototype
等于父类的一个实例(new 父类
),继承实例属性和原型属性
ES6里面使用class
和extends
实现的继承
下面具体来看(下面提到的属性
,除非特殊说明,都是泛指属性和方法)
继承实例属性
在子类的内部让父类调用call
或者apply
的方法,这样只能继承父类的实例属性,不能继承原型上的属性。
function Child() {
Father.call(this)
}
继承原型属性
(1)直接让子类的原型等于父类的原型:
Child.prototype = Father.prototype;
Child.prototype.constructor = Child;
(2)利用Object.setPrototypeOf
方法
使用Object.setPrototypeOf
方法来让指定Child.prototype.__proto__
等于Father.prototype
,实现了原型属性的继承(相当于new
过程的一部分)
Object.setPrototypeOf(Child.prototype, Father.prototype);
(3)利用中间函数
第三种种方法是对第一种方法的改进,避免子类的原型更改影响到父类的原型,利用了一个中间函数:
const Temp = function(){};
Temp.prototype = Father.prototype;
Child.prototype = new Temp()
Child.prototype.constructor = Child;
(4)通过父类的原型对象进行遍历、拷贝,从而实现继承:
for (const i in Parent.prototype) {
Child.prototype[i] = Parent.prototype[i]
}
同时继承实例属性和原型属性
(1)让子对象的原型成为父对象的实例:
Child.prototype = new Father();
Child.prototype.constructor = Child;
(2)通过ES6中的方法class
和extends
实现继承:
class Father {
}
class Child extends Father {
constructor() {
super();
}
}
要注意的是,class
只能在原型链上定义方法,所以也只能继承原型方法,而不能继承原型属性。
非构造函数的继承
让一个对象去继承一个不相关对象,由于这两个对象都是普通对象,不是构造函数,所以无法使用构造函数方法实现继承。
通过Object.create
实现
Object.create
方法是直接通过原型,而非模拟类,来实现普通对象(非构造函数)之间的继承
const child = Object.create(father, [childDescriptors])
实际上实现的是child.__proto__ === father
通过Object.setPrototypeOf
实现
Object.setPrototypeOf
和Object.create
都是更改原型链(设置__proto__
属性)的手段,Object.getPrototypeOf(a)
是用来获取对象的__proto__
属性
Object.setPrototypeOf(child, father)
这个方法实现的也是child.__proto__ === father
通过object
函数
可以自己编写一个函数,实现上面Object.setPrototypeOf()
和Object.create()
的(部分)功能
function object(parent) {
function Child() {}
Child.prototype = parent;
return new Child();
}
实际上,可以将这个方法看做Object.create()
的简易的polyfill。
通过拷贝实现
可以父对象进行遍历、拷贝实现
浅拷贝:
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
深拷贝:
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
const type = Object.prototype.toString.call(p[i]).match(/\s(.+)\]/)[1].toLowerCase();
switch (type) {
case 'array': {
c[i] = [];
deepCopy(p[i], c[i]);
break;
}
case 'object': {
c[i] = {};
deepCopy(p[i], c[i]);
break;
}
default: {
c[i] = p[i];
break;
}
}
}
return c;
}
关于深拷贝、浅拷贝可以参考以前写过的笔记《JS05 JS中的拷贝》
ES6中的继承
ES6里面使用class
和extends
实现的继承,实际上是ES5中的继承方法的语法糖,它可以实现实例属性、原型方法、静态属性的继承
- 类的实例属性是通过
class
内部的constructor
里的super
实现的 - 类的原型方法是通过
Object.setPrototypeOf(Child.prototype, Father.prototype)
实现的( - 类的静态属性是通过
Object.setPrototypeOf(Child, Father)
实现的
使用ES6的继承方法,与ES5中的继承方法相比,不同点主要有以下下几点:
(1)实现的顺序
ES5的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。
ES6的继承机制完全不同,子类一开始并没有自己的this
,而是需要先创造父类的实例对象this
(所以必须先调用super
方法),然后再用子类的构造函数修改this。
(2)静态方法和静态属性
ES6的继承方法可以继承父类的静态方法和静态属性,这一点是ES5的继承方法做不到的。
(3)无法继承原型属性,只能继承原型方法
由于在class
内部定义在constructor
之外的方法实际上都是原型方法,并不能定义原型属性,所以继承的自然也只能是原型方法,而无法继承原型属性
(4)原型方法的可枚举性
由于class
内部定义的方法都是不可枚举的,这一点与ES5的行为是不一致的。
new
的过程
在通过构造函数实例化一个对象(new
)的过程中
let p = new Person()
发生了以下的过程:
// 1 新建一个对象
let p = {};
// 2 修改p的__proto__属性,实现原型链的继承
p.__proto__ === Person.protype
// 3 将Person的作用域改为p,也就是说让Person中的this指向p,为p添加实例属性
Person.call(p)
// 4 返回p
return p
new
运算的返回值默认返回this
,但显式的返回值时,如果返回值的是基本类型,则忽略返回值,仍然返回this
,如果返回值是引用类型,则直接返回该返回值作为对象的结果