关于继承网上有很多文章,但是能讲解的大多都不让我满意,所以自己写一篇。本文适合已经知道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翻译成上面这个完美的方案。