JS的六种继承方式

1,904 阅读7分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

继承

什么是继承?

JS里的继承就是子类继承父类的属性和方法

目的可以让子类的实例能够使用父类的属性和方法

抽象的表达就是:一个人有车,有房,那么他的儿子也可以去使用他的车子,住他的房子。

方法一:原型链继承

话不多说,先上一个例子:

function SuperType(){
    this.property = true
}

function Type(){
    this.typeproperty = false
}

var instance = new Type()

console.log(instance.property)

这里我们有两个构造函数,分别是:SuperType 和 Type

这里我们对 console.log(instance.property) 进行了打印,那么结果是显而易见的

构造函数 Type 的实例对象 instance 身上根本就不存在 property 属性,这个打印自然是

—— undefined

那么如果我想要在 Type 的实例对象 instance 身上拿到 SuperType 身上的属性该怎么去做呢?

很简单,这里我们就要用到我们的第一个方法——原型链继承

我们只需要让 SuperType 的实例对象变成 Type 构造函数的原型;

即改成如下:

function SuperType(){
    this.property = true
}

Type.prototype = new SuperType()

function Type(){
    this.typeproperty = false
}

var instance = new Type()

console.log(instance.property)

那么我们打印出来的结果就是:

image.png

这说明我们是可以拿到构造函数 SuperType 身上的 property 属性

当然,这种方法有几个的缺陷

  • 会在子类实例上共享父类所有的引用类型的实例属性
  • 不能给父类构造函数传参

那么什么叫“会在子类实例上共享父类所有的引用类型的实例属性”呢?

抽象来说,就是一个人有一套房子和30万的欠债,同时他还有一个儿子,那么他的儿子肯定是只想要继承拿一套房子,而不想继承那30万的欠债对吧(当然,这里只是举个栗子);可是如果用了原型链继承那么它就会不管三七二十一,把所有的用的到的,用不到的全部继承过来。

那么什么又叫不能给父类构造函数传参呢?

我们都知道,在 new 一个实例对象时,我们是可以朝构造函数里传参来修改一些属性值,但是什么叫不能给父类构造函数传值呢?来,我们一起看看下面代码你就懂了。

function SuperType(){
    this.property = true
}

Type.prototype = new SuperType()

function Type(str){
    this.typeproperty = str
}

var instance = new Type('我是参数')

console.log(instance)

我们看看打印结果:

image.png

这里我们成功通过传参的方式,改变了实例对象里的初始属性值;

而我们是没有任何办法去通过传参的方式修改它的父类 SuperType 里的属性值;

这就是它的第二个缺陷——不能给父类构造函数传参

方法二:经典继承(伪造对象)

同样是这个例子

function SuperType(){
    this.property = true
}

function Type(){
    this.typeproperty = false
}

var instance = new Type()

console.log(instance.property)

经典继承的原理是:在子类的身体里将父类的所有属性方法解构在其中,让儿子具备父亲所有的属性方法。

用笨一点的话来说,就是在子构造函数里面,用遍历把父类所有的属性方法遍历一遍拿出来,然后放到子构造函数里面,达到人为改造继承的样子;不过这里是改变 this 的指向达到这样的效果。

代码实现如下:

function SuperType(){
    this.color = 'red'
}


function Type(){
    SuperType.call(this);
}

var instance = new Type()

console.log(instance.color)

那么结果也是显而易见的:

image.png

这里要注意一点,需要用call这个方法把 SuperType 的 this 绑定到 Type 身上,保证 this 不会出现隐式丢失。

值得一提的是,经典继承这个方法不会导致它的实例对象会共享到其它实例对象的属性,且能传参。 但是缺点还是有的,那就是:1. 方法不能复用;2. 子类继承不到父类原型上的属性;

方法三:组合继承(伪经典继承)

组合继承是一种由原型链继承及经典继承的组合起来的继承方式,既解决了原型链继承里的不能传参的弊端,又解决了经典继承里不能继承到原型链上的属性方法的弊端。

代码如下:

function SuperType(age){
    this.color = 'red';
    this.age = age;
}
SuperType.prototype.name = '阿轩';


function Type(age){
    SuperType.call(this,age);    //绑定this
}
Type.prototype = new SuperType();    //将父类实例对象绑定到子类原型上
Type.prototype.constructor = Type    //弥补因为重写原型而没有构造器属性的缺陷
var instance = new Type(18);

console.log(instance.color)
console.log(instance.name)
console.log(instance.age)

喏,运行结果如下:

image.png

这种组合式继承方式解决了原型链继承还有经典继承的弊端,可以说是比较全方面一点的继承方式,但是它也是有一个缺点,那就是无论在什么情况下,都需要重复调用两次父类构造函数,算是性能方面的缺陷吧。

方法四:原型式继承

这是一种跟原型链继承相似的继承方式,通过名字也是能看出来它们是有关联的。

讲到原型式继承,那么我们就要讲到一个创建对象的方法了,它就是: Object.create() 方法。

可能大家都知道 Object.create() 方法可以创建对象,可你们知道吗,它还可以让创建出来的对象继承到其它对象的属性方法。

那么,让我们看到如下代码吧:

var person = {
    name:'阿轩',
    age:'20',
}

var obj = Object.create(person);
console.log(obj);

如果只是拿到 Node环境 执行的话,我们只能看到一个空对象,这里,我们拿到浏览器环境下去执行给大家看:

image.png

大家可以看到,它确实只是一个空对象,但是在它的原型上,却继承到 person 对象的所有属性,这就是原型式继承;

而 Object.create() 方法的原理则是如下代码演示:

function object(obj){
    function F(){}        //创建一个构造函数
    F.prototype = obj;    //将传进来的参数对象重写构造函数的原型
    return new F();       //返回出去一个F构造函数的实例对象
}
var person = {
    name:'阿轩',
    age:'20',
}
var obj = Object(person);

它的实现原理很简单,其实也就是原型链继承差不多。缺点:引用类型的属性始终会被继承所共享。 缺点“引用类型的属性始终会被继承所共享”解释:像这个地方有一个 person 作为引用类型传递进去,如果 person 里还有嵌套对象如下:

function object(obj){
    function F(){}        //创建一个构造函数
    F.prototype = obj;    //将传进来的参数对象重写构造函数的原型
    return new F();       //返回出去一个F构造函数的实例对象
}
var person = {
    name:'阿轩',
    age:'20',
    like:{
        sport:'singing'
    }
}
var obj = object(person);
obj.like.sport = 'runing'
var obj1 = object(person);
console.log(obj1)

那么在这里的Obj1原型上的 sport 属性也会因为 obj 更改了 person 里面的引用类型里的 sport 属性的更改而更改,因为整个 person 都是一个引用地址,相当于儿子可以改动父亲的基因了,属于一个很大的弊端了。

方法五:寄生式继承

寄生式继承是原型式继承的功能添加版,它给原型式继承方法添加了一个功能,就是可以往对象上额外添加属性方法;

但是说到底,其实只是在 object.create() 方法身上嵌套一层函数,然后在函数里添加方法罢了。

如代码:

function js(obj){
    let clone = Object.create(obj);
    clone.say = () =>{
        console.log('新增添的可调用函数')
    }
}
var person = {
    name:'阿轩',
    age:'20',
    like:{
        sport:'singing'
    }
}
var obj2 = js(person);

所以这种寄生式继承只是原型式继承的功能补充,而不是原型式继承的缺陷弥补,它仍然具有原型式继承的缺陷:引用类型的属性始终会被继承所共享。

寄生组合式继承

寄生组合式继承是一种原型式继承和经典继承的组合继承方式,它变相的解决了原型式继承的弊端。

function SuperType(age){
    this.color = 'red';
    this.age = age;
}
SuperType.prototype.name = '阿轩';


function Type(age){
    SuperType.call(this,age);    //绑定this
}
Type.prototype = Object.assign(Type.prototype,SuperType.prototype) 
Type.prototype.constructor = Type
var instance = new Type(18);

console.log(instance.color)
console.log(instance.name)
console.log(instance.age)

这里用 Object.assign 去代替了 Object.create ,把父类的原型融入到子类的原型上,且以对象的形式放到 Type 的原型上,且这些原型一般是不会发生改变的,故即引用类型的属性始终会被继承所共享也是没有关系的,变相的解决了这些问题。