前端积累 | 基于原型链的各种继承方式

86 阅读4分钟

众所周知,在ES6正式引入class之前,JavaScript都只能使用基于原型的面向对象来实现复杂的编程。下面介绍一些继承方式。

原型链继承

如果我们不使用任何额外的继承方法的话,那么使用的便是JavaScript原生的原型链继承。每一个对象都有自己的prototype也就是原型(在chromium内部是用_proto_实现),代表的便是对象所处的类。如果要实现继承的话,只需要让一个对象的原型等于一个超类对象即可。

但是这也会带来一些问题,因为我们让对象的原型等于一个超类对象,那么所有的新对象的原型就都是当时的那个新建的超类,这就导致我们如果修改了超类的数据的话,所有的子类数据也会被同步修改。

function SuperType(){
    this.superProperty = 1;
    this.arr = [];
}
function SubType(){
    this.subProperty = 2;
}
SubType.prototype = new SuperType();
let obj1 = new SubType();
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.subProperty;// 2
obj1.arr;// [1]
let obj2 = new SubType();
obj2.arr;//[1]

盗用构造函数

为了解决上文中说的原型链继承中的问题,我们可以使用盗用构造函数来规避这个问题,也就是在子类的构造函数中调用一下父类的构造函数,这样的话就可以保证每一个子类的父类对象都是唯一的了。

但是这种方法并不能继承原型属性和方法,只能继承实例属性和方法。因为构造函数是不能提供原型方法的。

function SuperType(){
    this.superProperty = 1;
    this.arr = [];
}
function SubType(){
    SuperType.call(this);
    this.subProperty = 2;
}
let obj1 = new SubType();
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.subProperty;// 2
obj1.arr;// [1]
let obj2 = new SubType();
obj2.arr;//[]

组合式继承

那么有没有一种方法可以结合上述的两种继承呢?有的,这便是组合式继承。不光使用call,还要把子对象的原型指向一个超类对象,然后让超类的构造函数为子类。前者能够让子类继承超类的所有方法和属性,后者能够继承超类的实例属性和方法,把前者的实例属性覆盖掉(属性依然会挂载但是不会被使用),这样就能实现全部继承了。

function SuperType(){
    this.superProperty = 1;
}
SuperType.prototype.arr=[];
function SubType(){
    SuperType.call(this);
    this.subProperty = 2;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
let obj1 = new SubType();
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.subProperty;// 2
obj1.arr;// [1]
let obj2 = new SubType();
obj2.arr;// [1]

但是这样依旧有新的问题产生。让子对象的原型指向一个超类对象虽然是用来继承原型方法的,但是也会同时继承从来不会被用到的实例属性。这对内存的开销也是会有影响的。

原型式继承

原型式继承一般要传一个父类对象作为参数。通常是创建一个空的过渡对象,将过渡对象的原型绑定到父类对象中并返回。也由于这个原因父类对象通常不使用函数来表示。

其缺点也显而易见,因为遵循和原型链继承类似的逻辑,因此修改超类数据的话所有子类数据也会被修改。而且我们现在并没有给子类对象添加任何新的属性和方法。

let superType={
    superProperty: 1,
    arr: [],
}
function subType(superObj){
    function F(){};
    F.prototype = superObj;
    return new F();
}
let obj = superType;
let obj1 = subType(obj);
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.arr;// [1]
let obj2 = subType(obj);
obj2.arr;// [1]

寄生式继承

寄生式继承是原型式继承的改进版本,通过一个包装函数来给子类添加属性。不过依旧没有解决原型式继承最主要的问题。

let superType={
    superProperty: 1,
    arr: [],
}
function subType(superObj){
    function F(){};
    F.prototype = superObj;
    return new F();
}
function createSubObj(superObj){
    let subObj = subType(superObj);
    subObj.subProperty =2;
    return subObj;
}
let obj = superType;
let obj1 = createSubObj(obj);
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.subProperty;// 2
obj1.arr;// [1]
let obj2 = createSubObj(obj);
obj2.arr;// [1]

寄生组合式继承

JavaScript类型继承的最终答案便是寄生组合式继承,这也是ES6中的解决方法。通过将寄生式继承与组合式继承相结合,我们就可以规避所有已知的问题。其中组合式继承用来继承超类的实例方法,而寄生式继承用来继承超类的静态方法(通过typeExtend函数)。

function SuperType(){
    this.superProperty = 1;
    this.arr = [];
}
function SubType(){
    SuperType.call(this);
    this.subProperty = 2;
}
function typeExtend(typeSuper,typeSub){
    let prototype = Object.create(SuperType.prototype);
    prototype.constructor = SubType;
    SubType.prototype = prototype;

}
typeExtend(SuperType, SubType);
let obj1 = new SubType();
obj1.arr.push(1);
obj1.superProperty;// 1
obj1.subProperty;// 2
obj1.arr;// [1]
let obj2 = new SubType();
obj2.arr;// []

总结

JavaScript的继承也蕴含了各种设计模式的思想在里面,通常来说要继承实例方法就可以使用组合式继承,要继承静态方法就可以使用寄生式继承。虽然ES6的语法已经普及很长时间了,但是我们依然要掌握这些思想。