JavaScript的6中继承方式

492 阅读4分钟

基于原型链的继承

基于原型链的继承的问题,原型上的引用属性会被多个实例所共享,修改一个实例的该属性会导致其他实例的该属性一起被修改.

function Super(){
    this.name = 'super'
    this.colors = ['red','green','blue']
}
function Sub(){
​
}
Sub.prototype = new Super()
​
let sub1 = new Sub()
let sub2 = new Sub()
console.log(sub1.name)//super
​
sub1.colors.push('black')
console.log(sub1.colors,sub2.colors)//[ 'red', 'green', 'blue', 'black' ] [ 'red', 'green', 'blue', 'black' ]

借用构造函数进行继承

原理:在子类的构造函数中调用父类构造函数,使用 apply()和 call()方法以新创建的对象为上下文执行构造函数.

优点:可以传递参数

function Super(name){
    this.name = name
    this.colors = ['red','green','blue']
}
function Sub(name){
    Super.call(this,name)
}
let sub1 = new Sub('sub')
console.log(sub1.name)//sub

问题:必须在构造函数中定义方法,因此函数不能重用.此外,子实例也不能访问父实例原型上定义的方法.

组合继承

组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来.

function Super(name){
    this.name = name
    this.sayMyName = function(){
        console.log(this.name)
    }
}
//继承了属性
function Sub(name){
    Super.call(this,name)
}
//继承了方法
Sub.prototype = new Super()
let sub1 = new Sub('sub')
console.log(sub1.name)//sub
sub1.sayMyName()//sub

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript中使用最多的继承模式.而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力. 但是也存在缺点,父级的构造函数会被调用两次,增加了内存开销.

原型式继承

适用情况:你有一个对象,想在它的基础上再创建一个新对象,新的对象会以已有对象为原型.

let originO = {
    name:'lhc',
    age:20,
    cars:['benz']
}
function object(o){
    function F(){}
    F.prototype = o
    return new F()
}
let sub1 = object(originO)
sub1.name = 'james'
sub1.age = 34;
sub1.cars.push('bmw')
​
​
let sub2 = object(originO)
sub2.name = 'wade'
sub2.age = 37
sub2.cars.push('linken')
console.log(sub1,sub2,originO.cars)//{ name: 'james', age: 34 } { name: 'wade', age: 37 } [ 'benz', 'bmw', 'linken' ]

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合.但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了.

Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述.以这种方式添加的属性会遮蔽原型对象上的同名属性。

let originO = {
    name:'lhc',
    age:20,
    cars:['benz']
}
​
let newO = Object.create(originO,{
    name:{
        value:'wade'
    }
})
console.log(newO.name)//wade

寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象.

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}
​
//工厂函数对对象进行增强,最后返回这个对象
function createAnotherObject(obj){
    let clone = object(obj)
    clone.say = function(){
        console.log('hi')
    }
    return clone
}
​
let person = {
    name:'lhc',
    age:20
}
let clone = createAnotherObject(person)
clone.say()//hi

这个例子基于 person 对象返回了一个新对象.新返回的 anotherPerson对象具有 person 的所有属性和方法,还有一个新方法叫 sayHi().寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。

通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生式组合继承

组合继承其实也存在效率问题.最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法.基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本.

该继承方式是目前不使用ES6类的情况下的最佳继承方式,避免了父级构造函数调用两次的问题,可以实现原型上函数的复用,并且可以传递参数.

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}
​
function inheritPrototype(subType,superType){
    let prototype = object(superType.prototype)//创建父对象原型的副本
    prototype.constructor = subType//对该父对象原型的副本进行增强,避免重写原型后导致的constructor丢失
    subType.prototype = prototype//赋值对象
}
​
function SuperType(name){
    this.name = name
    
}
SuperType.prototype.say = function(){
    console.log(this.name)
}
​
function Subtype(age,name){
    this.age = age
    SuperType.call(this,name)
}
inheritPrototype(Subtype,SuperType)
​
Subtype.prototype.sayAge = function(){
    console.log(this.age)
}
let sub = new Subtype(20,'lhc')
sub.say()//lhc
sub.sayAge()//20
console.log(sub.__proto__.constructor === Subtype)//true