有关于JavaScript的七种继承方式

189 阅读9分钟
JavaScript的语言继承机制:

我一直很难理解Javascript语言的继承机制。

它没有"子类""父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。

————阮一峰的网络日志

继承概念

什么是继承?

通俗来说,就是子拥有父的属性和方法。

继承方法

(一)原型链继承

原理:子类原型为父类的实例对象。

我们造一个父类函数。

function Dad(name){}

给父类函数设置一些属性name,age,cars

方法addCars添加车子,原型上的方法getCar获取车子

function Dad(name,name){
    this.name = name
    this.age = age
    this.cars = ['baoma','liebao']
    this.addCars = function(car){
        this.cars.push(car)
    }
}
Dad.prototype.getCar = function(id){
    return this.cars[id]
}

再造一个子类函数,不设置任何方法和属性。

function Son(){}

我们知道,原型就是object.prototype

原型链继承,就是,把子类的原型,指向父类的实例。

let dad_ex1 = new Dad()// 父函数的实例
Son.prototype = dad_ex1
  • 只能继承一个父类

Son.prototype只能指向一个实例,所以,只能继承一个父类,不能继承多个。

  • 子类实例无法向父类构造函数传参

来 new 一个子类Son的实例son_ex1

let son_ex1 = new Son()

实例son_ex1__proto__属性指向构造函数的prototype属性。即原型:

son_ex1.__proto__ === Son.prototype
dad_ex1.__proto__ === Dad.prototype

而子类Son构造函数的原型 是 父类的一个实例 : Son.prototype = dad_ex1

son_ex1.__proto__.__proto__ === Dad.prototype

一个原型继承链就形成了。

将子类实例son_ex1打印出来:

console.log(son_ex1)

可以看到,当我们new Son()时,子类Son函数并没办法向父类传参,导致name,age属性都为undefined。

  • 来自父类的所有属性被所有实例共享

原型链继承,子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法

再来造一个子类实例son_ex2,我们向实例son_ex2添加一辆车

let son_ex2 = new Son()
son_ex2.addCars('mashaladi')
console.log(son_ex1,son_ex2)

结果,实例son_ex2的属性cars发生变化,实例son_ex1的属性cars也同样发生变化。

(二)借用构造函数继承

原理:在子类构造函数中调用父类构造函数

父类:

function Dad(name,age){
    this.name = name
    this.age = age
    this.cars = ['baoma','liebao']
    this.addCars = function(car){
        this.cars.push(car)
    }
}
Dad.prototype.getCar = function(id){
    return this.cars[id]
}

子类构造函数中调用父类构造函数:

function Son(name,age){
    Dad.call(this,name,age)// 调用Dad函数,改变this指向,并执行函数。
}

再来new 两个子类实例,传入name,age等属性,并添加son_ex2的车子。

let son_ex1 = new Son('son1',10)
let son_ex2 = new Son('son2',20)
son_ex2.addCars('mashaladi')
console.log(son_ex1,son_ex2)

  • 子类实例继承了父类构造函数中的属性和方法。

父类的属性name,age,方法addCars,子类实例都拥有。

  • 父类的cars属性没有被所有实例共享,子类实例之间不会影响。

添加实例son_ex2的车类型,实例son_ex1没有被影响。

每个子类实例都有父类实例函数的副本,没有实现函数复用。

  • 可以向父类函数传参

name,age属性也可以被传递进去。

  • 无法继承父类原型上的方法和属性

但同时,我们也发现,父类原型上的方法Dad.prototype.getCar却没有了。

son_ex1.getCar(0) //son_ex1.getCar is not a function
  • 可以实现多个父类继承
function Dad1(price){
    this.price = price
}
function Dad2(phone){
    this.phone = phone
}
function Son(name,age,price,phone){
    Dad.call(this,name,age)// 调用Dad函数,改变this指向,并执行函数。
    Dad1.call(this,price)
    Dad2.call(this,phone)
}

(三)组合继承 -> 原型链+借用构造函数

原理:借用以上的两种方式,构造函数+原型链继承:保留传参,实现复用

父类:

function Dad(name,age){
    this.name = name
    this.age = age
    this.cars = ['baoma','liebao']
    this.addCars = function(car){
        this.cars.push(car)
    }
}
Dad.prototype.getCar = function(id){
    return this.cars[id]
}

子类中,需要把原型指向父类实例,同时把原型的构造函数指向自己。

function Son(name,age){
    Dad.call(this,name,age)// 调用Dad函数,改变this指向,并执行函数
}

Son.prototype = new Dad()
Son.prototype.constructor = Son

来创建两个子类的实例,加入相应的参数,并向其中一个实例添加车子类型

let son_ex1 = new Son('son1',10)
let son_ex2 = new Son('son2',20)
son_ex2.addCars('mashaladi')
console.log(son_ex1,son_ex2)

  • 可传参,可复用,可以继承父类原型上的属性和方法

  • 父类被调用了两次,生成了两份实例

Dad.call(this,name,age)调用了一次,new Dad()调用了一次。

(四)原型式继承

原型式继承版本1

原理:复制一个对象,把这个对象做为原型,返回一个临时的实例

通俗的来说,是模拟复制一个对象。

先来定义一个对象,简称父对象

let Dad = {
    name:'',
    age:'',
    cars:['baoma','laoteleisi'],
    addCar:function(car){
        this.cars.push(car)
    }
}

定义一个函数,这个函数中有一个函数,把函数的原型指向定义的父对象,返回一个新的实例

function create(obj){
    function Dad(){}
    Dad.prototype = obj
    return new Dad()
}

来创建两个子类实例,同时修改子类实例的名字,给实例son_ex2添加一辆车子。

let son_ex1 = create(Dad)
let son_ex2 = create(Dad)
son_ex1.name = 'son_ex1'
son_ex2.name = 'son_ex2'
son_ex2.addCar('falali')
console.log(son_ex1,son_ex2)

跟原型链继承有着同样的缺点。

  • 父对象引用类型的属性,被子类实例共享

cars是引用类型,实例son_ex2改变了值,实例son_ex1也同样改变了

  • 无法实现复用

实例son_ex1 son_ex2 新增的name属性,是后面添加上去的。

原型式继承版本2

ES5新增了一个Object.create(),规范了原型式继承

Object.create(prototype,desc)
  • prototype 用来做原型的对象,可为null,必需。
  • desc可选参数,包含一个或多个属性描述符的 JavaScript 对象,简单来说,是为新对象定义额外的属性,指定的任何属性都会覆盖原型上的同名属性。

先来定义一父对象

let Dad = {
    name:'',
    age:'',
    cars:['baoma','laoteleisi'],
    addCar:function(car){
        this.cars.push(car)
    }
}

实例化子类,同时修改两个子类实例的名字,给实例son_ex2添加一辆车子

let son_ex1 = Object.create(Dad)
let son_ex2 = Object.create(Dad)
son_ex1.name = 'son_ex1'
son_ex2.name = 'son_ex2'
son_ex2.addCar('falali')
console.log(son_ex1,son_ex2)

同上个版本的原型式继承一样。

我们再来利用Object.create的第二个参数,它可以新增属性,以及覆盖原来的属性。

let son_ex1 = Object.create(Dad,{
    name:{
      value:'son_ex1'
    },
    cars:{
       value:['baoma']
    }
})
let son_ex2 = Object.create(Dad,{
    name:{
      value:'son_ex2'
    },
    cars:{
       value:['mashaladi']
    }
})
son_ex2.addCar('falali')
console.log(son_ex1,son_ex2)

因为是为子类实例新增的属性,所以,各实例之间不会相互影响。

缺点也还是同上。……

(五)寄生式继承

寄生式继承和原型式继承是相似的一种思路

原理:创建一个用于封装继承过程的函数,然后函数内部做一些增强,返回新对象

先来定义一父对象

let Dad = {
    name:'',
    age:'',
    cars:['baoma','laoteleisi'],
    addCar:function(car){
        this.cars.push(car)
    }
}

定义一个函数,这个函数中有一个函数,把函数的原型指向定义的父对象,返回一个新的实例

function object(obj){
    function Dad(){}
    Dad.prototype = obj
    return new Dad()
}

再来定义一个创建对象的函数

function createSon(obj){
    let son = object(obj)
    son.addAge = function(age){
        this.age = age
    }
    return son
}

我们来创建两个子类实例,修改名字,年龄,添加车子。

let son_ex1 = createSon(Dad)
let son_ex2 = createSon(Dad)
son_ex1.name = 'son_ex1'
son_ex2.name = 'son_ex2'
son_ex1.addAge(10)
son_ex2.addAge(20)
son_ex2.addCar('falali')
console.log(son_ex1,son_ex2)

  • 父对象属性被所有实例共享

  • 无法复用

寄生式继承和原型式不同的是,寄生式把原型式继承封装了一下,原理缺点优点也都是一样的。

(六)寄生组合式继承 -> 寄生式 + 组合式

原理:将寄生式继承和组合继承相结合,解决了组合式继承中会调用两次父类构造函数的缺点。

定义一个函数,这个函数中有一个函数,把函数的原型指向定义的父对象,返回一个新的实例

function object(obj){
    function F(){}
    F.prototype = obj
    return new F()
}

父类:

function Dad(name,age){
    this.name = name
    this.age = age
    this.cars = ['baoma','liebao']
    this.addCars = function(car){
        this.cars.push(car)
    }
}
Dad.prototype.getCar = function(id){
    return this.cars[id]
}

组合式继承:通过构造函数继承父类中的方法和属性,通过原型继承父类原型上的方法和属性。

寄生组合式:通过构造函数继承父类中的方法和属性,通过原型式继承父类原型上的方法和属性。

简单来说,我们不需要再把子类原型指向父类实例,因此,我们也不再需要去new 一个父类实例。我们要的仅仅只是父类原型的一个复制对象而已。

我们定义一个继承的模型。

function inheritPrototype(Son,Dad){
    let prototype = object(Dad.prototype) // 创建对象
    prototype.constructor = Son
    Son.prototype = prototype
}

定义一个子类,并且让子类继承父类原型

function Son(name,age){
    Dad.call(this,name,age)
}
inheritPrototype(Son,Dad)

测试一下,创建两个子类实例,并修改他们的name属性,添加车子。

let son_ex1 = new Son('son_ex1',10)
let son_ex2 = new Son('son_ex2',20)
son_ex1.addCars('mashaladi')
son_ex2.addCars('laoteleisi')
console.log(son_ex1,son_ex2)

  • 只调用了一次父类的构造函数
  • 方法属性可复用,可传参
  • 父类属性不会被所有子类实例共享
  • 父类构造函数中的属性和方法,以及原型上的属性和方法都可继承
  • 可继承多个父类

(七)ES6 class继承

在ES6中,calss作为对象的模板被引入,可以通过class关键字来定义一个类。

但是,class的本质,仍然是function

class关键字只是原型的语法糖,底层也是采用了寄生组合式继承

了解更多class,可查看我另一篇文章[ES6的新特性]

我们来定义一个父类

class Dad{
    // 构造函数
    constructor(name,age){
        this.name = name
        this.age = age
        this.cars = ['baoma','liebao']
    }
    // 一般方法 
    addCars(car){
        this.cars.push(car)
    }
}

定义一个子类,通过关键字extends继承父类

class Son extends Dad{
    constructor(name,age){
        super(name,age)//通过super调用父类的构造函数
    }
}

再来创建两个子类实例,修改实例2的车子

let son_ex1 = new Son('son_ex1',10)
let son_ex2 = new Son('son_ex2',20)
son_ex2.addCars('mashaladi')
console.log(son_ex1,son_ex2)

但要注意的是,并不是所有的浏览器都支持class关键字。

总结

继承方式主要有以下七种:

  • 原型链继承
  • 借用构造函数继承
  • 组合式继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6class继承