Javascript 的继承总结

151 阅读5分钟

写惯了 TypeScript 的人很容易了解继承(extends),比如类的继承和接口的继承等,传送门:www.tslang.cn/docs/handbo… ,但是对于ES2015出现之前,JavaScript如何实现继承的呢?毫无疑问,只能通过原型链的方式实现继承,本篇主要是在读书时遇到了原型继承的问题,回顾以下原型继承的集中方式并整理成笔记方便日后查阅。

类式继承(原型链继承)

类式继承非常简单,总结来说就是:子类的原型的 prototype 被赋予父类的实例,从而继承父类的属性和方法

// 定义 Person 类
function Person() {
    this.name = 'zhang san'
}
// 添加成员函数
Person.prototype.getName = function() {
    console.log(this.name)
}
// 定义 Employee 类
function Employee() {
    
}
// 类式继承
Employee.prototype = new Person()
let employee1 = new Employee()
console.log(employee1.getName()) // zhang san

并且,通过 instanceof 关键字可以判断该子类实例为父类原型:

let employee1 = new Employee()
console.log(employee1 instanceof Employee) // true
console.log(employee1 instanceof Person) // true
console.log(employee1 instanceof Object) // true, 顶层原型为Object

但是,类式继承往往会出现许多问题:

  1. 引用类型的属性被所有实例共享
// 定义 Person 类
function Person() {
    this.name = 'zhang san'
    this.skill = ['吃饭''睡觉']
}
// 添加成员函数
Person.prototype.getSkill = function() {
    console.log(this.skill)
}
// 定义 Employee 类
function Employee() {
    
}
// 类式继承
Employee.prototype = new Person()

let employee1 = new Employee()
employee1.skill.push('写代码')

let employee2 = new Employee()
employee2.skill.push('做决策')

console.log(employee1.getSkill()) // ['吃饭', '睡觉', '写代码', '做决策']
console.log(employee2.getSkill()) // ['吃饭', '睡觉', '写代码', '做决策']
  1. 在创建 Employee 实例时候,无法向 Person 传递参数

构造函数继承

类式继承虽然简单,但是缺点非常明显,我们是否可以通过别的继承方式避免这些缺点呢?实际上,构造函数继承就可以解决这一点,总结来说构造函数继承就是:子类构造函数作用环境执行一次父类构造函数

// 定义 Person 类
function Person() {
    this.name = 'zhang san'
    this.skill = ['吃饭''睡觉']
}
// 添加成员函数
Person.prototype.getSkill = function() {
    console.log(this.skill)
}
// 定义 Employee 类,并继承 Person 类
function Employee() {
    Person.call(this)
}

let employee1 = new Employee()
employee1.skill.push('写代码')

let employee2 = new Employee()
employee2.skill.push('做决策')

console.log(employee1.skill) // ['吃饭', '睡觉', '写代码']
console.log(employee2.skill) // ['吃饭', '睡觉', '做决策']

// 定义 Manager 类,并继承 Person 类
function Manager(skills) {
    Person.call(this, skills)
}

let manager1 = new Manager(['吃饭', '睡觉', '做决策'])
console.log(manager1.skill) // ['吃饭', '睡觉', '做决策']

console.log(manager1.getSkill()) // 报错,没有该方法

可以看出,构造函数继承已经完美解决了类式继承的两个问题,但是出现了一个新的问题,即:构造函数继承只继承父类属性,父类中方法不会被子类继承

组合继承

一看上面两个方式都或多或少有些问题,这就需要用到组合继承,也是JavaScript中最常见的继承方式。总结来说就是一句话,类式继承 + 构造函数继承

// 定义 Person 类
function Person() {
    this.name = 'zhang san'
    this.skill = ['吃饭''睡觉']
}
// 添加成员函数
Person.prototype.getSkill = function() {
    console.log(this.skill)
}
Person.prototype.getName = function() {
    console.log(this.name)
}
// 定义 Employee 类,并继承 Person 类
function Employee(name, skill) {
    Person.call(this, name, skill)
}
Employee.prototype = new Person()

let employee1 = new Employee('zhang san', ['吃饭', '睡觉', '写代码'])
console.log(employee1.getName()) // zhang san
console.log(employee1.getSkill()) // ['吃饭', '睡觉', '写代码']

let employee2 = new Employee('li si', ['吃饭', '睡觉', '做决策'])
console.log(employee2.getName()) // li si
console.log(employee2.getSkill()) // ['吃饭', '睡觉', '做决策']

组合继承综合两种继承方式,解决了之前的两种方式产生的问题。但是实际上,在一次继承中,父类构造函数被调用了两次,这看起来令人十分费解:

// 设置子类型实例的原型,调用父类构造函数
Employee.prototype = new Person()

// 创建子类实例,调用父类构造函数
Person.call(this, name, skill)

原型式继承

有人提出了一种新的继承方式:原型式继承,总结来说就是:根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。简单来说原型继承就是封装了下面这样一个 create 函数:

function create(o) {
    function F(){}
    f.prototype = o
    return new F()
}

熟悉ES规范的朋友可以看出,这就是 Object.create()的模拟实现。缺点还是很明显,值类型属性被复制,引用类型属性被共用,跟原型链继承的问题差不多:

// 定义 Person 类
function Person() {
    this.name = 'zhang san'
    this.skill = ['吃饭', '睡觉']
}

let person1 = create(Person)
let person2 = create(Person)

person1.name = 'li si'
console.log(person1.name) // li si

person1.skill = ['吃饭', '睡觉', '写代码']
console.log(person2.skill) // ['吃饭', '睡觉', '写代码']

这是不是说明原型式赋值并没有什么用?可以往下继续看。

寄生式继承

寄生式继承方式就跟名字一样,总结来说就是:创建一个仅用于封嘴昂继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象

function create(o) {
    let clone = Object.create(o)
    clone.getName = function() {
        console.log('get name')
    }
    return clone
}

当然,跟构造函数继承一样,不会继承父类方法,每次创建对象时都需要创建一次方法。

寄生组合式继承

寄生组合式继承,实际上就是寄生继承 + 类式继承,目的就是为了解决组合继承创建对象时调用两次父类构造方法的问题,如果不使用 Employee.prototype = new Person(),而是间接让 Employee.prototype 访问到 Person.prototype 呢?

function create(o) {
    function F() {}
    F。prototype = o
    return new F ()
}
function prototypr(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}

prototypr(Employee, Person)

寄生组合方式解决了调用两次父类构造函数的问题,可以算是JavaScript终极解决方式了。

总结

本文总结了类式继承、构造函数继承、组合式继承、原型式继承、寄生式继承、寄生组合式继承,建议对原型链和继承不是了解很多的同学仔细看看,ES6 class 继承请移步《ES6入门教程》es6.ruanyifeng.com/#docs/class…

参考

  1. 《JavaScript设计模式》
  2. 《JavaScript高级程序设计(第二版)》