手把手教你如何实现继承

本文将从最简单的例子开始,从零讲解在 JavaScript 中如何实现继承。bash


小例子

如今有个需求,须要实现 Cat 继承 Animal ,构造函数以下:函数

function Animal(name){
    this.name = name
}

function Cat(name){
    this.name = name
}
复制代码

注:如对继承相关的 prototype、constructor、__proto__、new 等内容不太熟悉,能够先查看这篇文章:理性分析 JavaScript 中的原型post


继承

在实现这个需求以前,咱们先谈谈继承的意义。继承本质上为了提升代码的复用性。性能

对于 JavaScript 来讲,继承有两个要点:ui

  1. 复用父构造函数中的代码
  2. 复用父原型中的代码

下面的内容将围绕这两个要点展开。this

初版代码

复用父构造函数中的代码,咱们能够考虑调用父构造函数并将 this 绑定到子构造函数。spa

复用父原型中的代码,咱们只需改变原型链便可。将子构造函数的原型对象的 __proto__ 属性指向父构造函数的原型对象。prototype

初版代码以下:设计

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype
复制代码

检验一下是否继承成功:咱们在 Animal 的原型对象上添加 eat 函数。使用 Cat 构造函数生成一个名为 'Tom' 的实例对象 cat 。代码以下:code

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype

// 添加 eat 函数
Animal.prototype.eat = function(){
    console.log('eat')
}

var cat = new Cat('Tom')
// 查看 name 属性是否成功挂载到 cat 对象上
console.log(cat.name) // Tom
// 查看是否能访问到 eat 函数
cat.eat() // eat 
// 查看 Animal.prototype 是否位于原型链上
console.log(cat instanceof Animal) // true
// 查看 Cat.prototype 是否位于原型链上
console.log(cat instanceof Cat) //true
复制代码

经检验,成功复用父构造函数中的代码,并复用父原型对象中的代码,原型链正常。

图示

弊端

__proto__ 属性虽然能够很方便地改变原型链,可是 __proto__ 直到 ES6 才添加到规范中,存在兼容性问题,而且直接使用 __proto__ 来改变原型链很是消耗性能。因此 __proto__ 属性来实现继承并不可取。

第二版代码

针对 __proto__ 属性的弊端,咱们考虑使用 new 操做符来替代直接使用 __proto__ 属性来改变原型链。

咱们知道实例对象中的 __proto__ 属性指向构造函数的 prototype 属性的。这样咱们 Animal 的实例对象赋值给 Cat.prototype 。不就也实现了Cat.prototype.__proto__ = Animal.prototype 语句的功能了吗?

代码以下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
复制代码

使用这套方案有个问题,就是在将实例对象赋值给 Cat.prototype 的时候,将 Cat.prototype 原有的 constructor 属性覆盖了。实例对象的 constructor 属性向上查询获得的是构造函数 Animal 。因此咱们须要矫正一下 Cat.prototype 的 constructor 属性,将其设置为构造函数 Cat 。

图示

优势

兼容性比较好,而且实现较为简单。

弊端

使用 new 操做符带来的弊端是,执行 new 操做符的时候,会执行一次构造函数将构造函数中的属性绑定到这个实例对象。这样就多执行了一次构造函数,将本来属于 Animal 实例对象的属性混到 prototype 中了。

第三版代码

考虑到第二版的弊端,咱们使用一个空构造函数来做为中介函数,这样就不会将构造函数中的属性混到 prototype 中,而且减小了多执行一次构造函数带来的性能损耗。

代码以下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}
function Func(){}
Func.prototype = Animal.prototype

Cat.prototype = new Func()
Cat.prototype.constructor = Cat
复制代码

图示

ES6

使用 ES6 就方便多了。可使用 extends 关键字实现继承, 复用父原型中的代码。使用 super 关键字来复用父构造函数中的代码。

代码以下:

class Animal {
    constructor(name){
        this.name = name
    }
    eat(){
        console.log('eat')
    }
}
class Cat extends Animal{
    constructor(name){
        super(name)
    }
}

let cat = new Cat('Tom')
console.log(cat.name) // Tom
cat.eat() // eat
复制代码

相关知识点


参考书籍

  • 《JavaScript高级程序设计(第3版)》
  • 《Java核心技术 卷Ⅰ(第9版)》
相关文章
相关标签/搜索