帮你理清js的继承

关于继承网上有很多文章,但是能讲解的大多都不让我满意,所以自己写一篇。本文适合已经知道js继承的各种或部分方式,但是尚未形成系统脉络的读者。

接下来为方便描述,约定:A是父类,a是A类的实例,B是子类,b是B类的实例。

原型链继承

B.prototype = a;

这种继承的潜在问题是,将A的实例属性变成了原型上的属性。如果某一个属性是引用类型,那么所有的b 都将共用一个引用,改变一个b,所有的b都跟着变。

问题示例:

function A(){
    this.objFromA = { name:'a' }
}
b1.objFromA.name='b1'
console.log( b2.objFromA.name )  // 变成了b1

至于网上说的原型式继承、寄生式继承,其实都是这种方式的另一种写法。

// Object.create 原型链继承
let b1 = Object.create(a);
let b2 = Object.create(a);

// 寄生式继承  
// 我实在看不出来寄生式继承和原型继承有啥差别,玩概念而已,解决不了任何问题。这种继承就不说了。

当然你还可以用 Object.setPrototypeOf 实现原型继承:

let b={}
Object.setPrototypeOf(b,a)
// 比Object.create多写一行,较麻烦。

这些所有的实现方式,本质都是原型链继承,其潜在问题都是一样的:实例属性变成了原型上的属性,如果是引用类型属性的话,多个b会共享。

构造继承

function B(){
    A.call(this);     // 每次创建b时,都重新调用A,从而产生的属性是互相独立的,巧妙的避免了属性被共享的问题。
}

这种继承问题很明显:无法继承A的原型上的方法。

两者组合

function B() {    
    A.call(this);   
}
B.prototype = a;
B.prototype.constructor = B;

这种方式接近完美:属性不会被共享,同时原型上的方法也继承过来了。

美中不足的是  B.prototype = a;  这一行,a的实例属性会污染B的原型,成为永远不能被b访问到的 却额外占用了内存的垃圾。

组合的升级 :

function B() {
    A.call(this);   
}
B.prototype = Object.create(A.prototype);   // 不产生实例a,从而避免了产生垃圾
B.prototype.constructor = B;

仍然存在的问题

大多数文章都是到此为止了,但这个方案还是由缺陷。

潜在问题一:A的静态方法无法被继承

静态方法是直接写在A上的,所以也要将A上的成员(注意是A不是a)继承到B上(注意是B不是b)。

// 方法一:
Object.assign(B,A)  // 这种方法存在问题:如果A的静态成员是变化的,B将无法跟随变化

// 方法二:
Object.setPrototypeOf(B, A);  // 这是完美的方法

潜在问题二:有时构造函数会返回一个对象,而不是通过操作this。所以 A.call(this); 这一句也许并不能继承A的实例成员。稍加改造:

let _this = A.call(this) || this;    
return _this;

最终完美的继承方法

function B() {
    let _this = A.call(this) || this;    
    return _this; 
}
Object.setPrototypeOf(B, A);
B.prototype = Object.create(A.prototype); 
B.prototype.constructor = B;

当你尝试用es6的class继承时 class B extends A 你会发现它最终会被babel翻译成上面这个完美的方案。

 

posted @ 2021-03-05 11:22  enne5w4  阅读(89)  评论(0编辑  收藏  举报