JavaScript 的继承

294 阅读5分钟

前言

首先来看看继承在维基百科上的定义。

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在运行期扩展。

在 ES6 之前,JavaScript 中是没有类(class)这个概念的,那也就是说 JavaScript 根本无所谓继承这个概念。所谓的继承都是模拟出来的,父类的各种属性和方法是通过原型链提供给子类使用的。

JavaScript 中的继承

在 JavaScript 中,一切对象都是由其构造函数构造出来的,子类对象由子类函数构造,父类对象由父类函数构造,JavaScript 中的继承是指子类对象拥有父类对象的属性和方法。

来看一段代码

let arr = new Array()
console.log(arr)

来看看由 Array 构造出来的 arr 有些什么属性和方法。

在来看看 arr 有没有一些在这里没显示的属性或方法。

咦,这个 .toString() 的方法是哪里来的?

熟悉对象的朋友会知道,Array 其实也是 Object 的一种,那么会不会是从 Object 那里弄过来的呢?

来看看 Object 的实例对象由哪些属性?

obj 上果然有 .toString() 的方法,再往下深究以下 arr 的属性。

哇,Object 构造对象的属性或方法 Array 构造的对象全都有,也就是说 Array 有一些属性和方法继承自 Object,arr 其实是通过原型链拥有了Object 的一些属性和方法,也就是平常说的“继承”。

JavaScript 继承的实现

看代码,先自己写一个构造函数。

function Human(name) {
  this.name = name;
}
Human.prototype.run = function() {
  console.log('run');
};

let person = new Human('yyzcl');
console.log(person);

来看看由这个构造函数构造出来的对象有些什么属性。

可以看到这个 person 对象拥有 Human 的属性和方法,还拥有了 Object 的属性和方法,这是因为所有的对象都源自 Object,自动发生了继承。

那我想自己再定义一个构造函数 Man,让它继承 Human 的属性和方法该怎么做?要实现这个继承有两个点,一是要继承 name 这个属性,二是要继承 run 这个方法。

来看看 Man 这个构造函数。

function Man(name) {
  this.gender = '男';
}
Man.prototype.fight = function() {
  console.log('fight');
};

那么如何让其拥有名字呢?反过来看 Human 这个构造函数。

this.name = name 这句代码不就是给一个对象赋予一个 name 的属性么,那么要是能调用一下 Human,再将 Man 中的对象传递进去,那不就是给 Man 赋予 name 属性了么。代码如下

function Man(name) {
  Human.call(this, name);
  this.gender = '男';
}

在看看构造出来的对象。

这下真的有 name 这个属性了,接下来就要考虑 Huamn 的方法了。其实这里就是要实现一个效果,就是将 person -> Man.prototype -> Object.prototype 变成 person -> Man.prototype -> Human.prototype -> Object.prototype,将 Human 的方法给插进去,要实现这一步,也就是将 Man.prototype.__proto__ = Human.prototype。看代码

function Man(name) {
  Human.call(this, name);
  this.gender = '男';
}

Man.prototype.__proto__ = Human.prototype

Man.prototype.fight = function() {
  console.log('fight');
};

let person = new Man('yyzcl');
console.log(person);

哈,这下 Human 的属性和方法 Man 全都有了,这就实现了继承。

这样子直接操作 Man.prototype.__proto__ 有一个问题,就是有些浏览器并不支持直接操作__proto__,这就麻烦了,得另外想个法子。

来看看 new

我们来看看使用 new 时,发生了什么事?

当写这么一句代码时,let obj = new Fn(),会发生什么事?

  1. 产生一个空对象.
  2. this = 这个空对象
  3. this.__proto__ = Fn.prototype
  4. Fn.call(this)
  5. return 4 的结果

我们可以发现第3步帮我们操作了 __proto__ ,那么这么写Man.prototype = new Human()不就完成了 Man.prototype.__proto__ = Human.prototype 这个操作吗,但是有个问题,就是第4步,它执行了一遍 Human.call(this) 这下就可能出问题了。我们来试一试,看会产生什么影响。

从图中可以看到,person.__proto__ 多了一个 name 属性,按照我们的想法,这个属性是多余的,这就是执行第4步产生的后果,那么要怎么解决呢?

既然要执行 Human.call(this),那我把函数里的代码删掉,就算执行一遍 Human.call(this),也不影响嘛。但是 Human 我们是无法改的,那么重新定义一个函数,复制 Human 的原型链,不要 Human 的执行代码不就得了。

于是就有了下面这几句。

let f = function(){}
f.prototype = Human.prototype
Man.prototype = new f()

放进去执行一下,看产生个什么对象。

哈,这就是我们要的结果,bingo。

继承的原型链图

我们来看看继承的过程中,他们的原型链是怎么链接的。

从图中可以看到,当利用 Man 构造出来一个对象 person 时,这个对象本身拥有 namegender 属性,同时它的 __proto__ 指向 Man.prototypeMan.prototype.__proto__ 指向 Human.prototypeHuman.prototype.__proto__ 指向 Object.prototype,这样 person 除了拥有本身的属性和方法之外,还拥有 ManHumanObject 的属性和方法,完成了所谓的继承。

ES6 的继承

ES6 引入了 class 这个关键字,实现继承的语法变得简单了一些。

class Human {
  constructor(name) {
    //自身属性写到 constructor 里面
    this.name = name;
  }
  run() {
    //共有属性,也就是 prototype 与 constructor 并列
    console.log('run');
  }
}

class Man extends Human {
  //将 Man.prototype.__proto__ 与 Human.prototype 连接起来
  constructor(name) {
    super(name);
    //也就是 Human.call(this, name)
    this.gender = '男';
  }
  fight() {
    console.log('fight');
  }
}

ES6 的语法固然简洁,但是不利于我们理解 JavaScript 继承的实质,毕竟JavaScript 只是用原型链模拟了继承的功能。

这种写法还有一个弊端,就是无法在 .prototype 上申明一个非函数,也即是 runfight 的位置只能放函数,无法放一个非函数。

结尾

以上就是我对 JavaScript 继承的一点点理解。