JS

JS继承

Posted by Youzi on January 15, 2021

JS 继承

本文结合原先写过的JS 面向对象,接着写 JS 中实现继承的几种方式;

原型链继承

JS 是一种基于原型的语言,链式继承是这个语言很常见的继承方式;涉及到三种对象,实例对象、构造函数、原型对象,其中构造函数有一个原型对象,原型对象有一个指针指向构造函数,实例对象有一个指针指向原型对象。

用代码来表示就是:

1
2
3
4
5
6
7
8
// 实例
instance.__proto__ === prototype;

// 构造函数
constructor.prototype === prototype;

// 原型对象
prototype.constructor === constructor;

看个具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
function A() {
  this.name = 'A';
  this.arr = [1, 2, 3];
}
function B() {
  this.name = 'B';
}
/*
 * 使用原型链实现继承
 */
B.prototype = new A();
let b = new B();

通俗易懂,直接让子类的prototype指针指向父类的实例对象,实现继承;

注意这里是父类的实例对象,为什么不直接指向父类呢,这是因为父类是个构造函数,如果直接指向父类,那继承的其实只是这个构造函数,但构造函数对象上本身没有任何值得继承的属性或者方法,因为构造函数内部用的this指的是实例对象;

基于原型的继承会产生一些问题,由于继承的是父类的实例对象,所以如果某个属性是引用类型,那子类的所有实例的该属性都会指向同一个引用地址,像上面的例子;

1
2
3
let b1 = new B(),
  b2 = new B();
b1.arr === b2.arr; // true

基于构造函数的继承(call)

1
2
3
4
5
6
7
8
9
10
11
12
13
function A() {
  this.name = 'A';
  this.arr = [1, 2, 3];
}
A.prototype.method = function () {};
function B() {
  // 使用call调用父类A,使B的实例也有A实例的属性,并且每个B的实例属性都是不一样的引用地址
  A.call(this);
  this.name = 'B';
}
let b = new B();
// 但由于B没有真正的继承A,所以实例b访问不到A原型链上的方法method
b.method(); // 报错

基于构造函数的继承,核心是在子类内部,通过call方法,执行父类的构造函数,但是和new操作不一样咯,只是在子类的实例上添加属性,并没有继承父类;所以其实会导致实例访问不到父类原型链上的属性。

组合式继承(原型链+构造函数)

结合前两种方式,实现的继承;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function A() {
  this.name = 'A';
  this.arr = [1, 2, 3];
}
A.prototype.method = function () {};
function B() {
  // 使用call调用父类A,使B的实例也有A实例的属性,并且每个B的实例属性都是不一样的引用地址
  A.call(this);
  this.name = 'B';
}
/*
 * 使用原型链实现继承
 */
B.prototype = new A();
// 手动改变指针指向
B.prototype.constructor = B;
let b = new B();
b.method();

我们前面说过,原型对象上有一个指针指向构造函数,在例子中B是构造函数,所以我们让B.prototype原型对象的construtor指针指向构造函数B本身。这样就实现了一个双向的指向问题,可以通过原型对象访问到构造函数,也可以通过构造函数来访问原型对象了。

继承普通对象(原型链方式)

前面讲了有关构造函数的继承方式,都涉及到了prototype这个属性,这是函数特有的一个属性,如果想对普通对象继承,就需要引入Object.create这个函数;该函数接受一个原型对象,一个属性类型对象(会添加到要返回的新对象上),返回一个新对象,新对象的__proto__属性指向原型对象。

但是要注意使用这个方法得到的新对象,是原对象属性的浅拷贝,因为只实现了原型链继承,说白了就是把新对象的原型指向了原型对象,并不会像构造函数那样在对象上添加属性。

1
2
3
4
5
6
7
8
let parent = {
  a: 1,
  b: [1, 2, 3],
  method() {}
};
let c1 = Object.create(parent);
let c2 = Object.create(parent);
c1.b === c2.b; // true

寄生继承

这种继承方式只是在使用Object.create的原型继承方式上,手动添加了额外的方法;看个例子就明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
let parent = {
  a: 1,
  b: [1, 2, 3],
  method() {}
};
const clone = parent => {
  let child = Object.create(parent);
  child.method2 = function () {
    // do something
  };
  return child;
};
let child = clone(parent);

像这样创建了一个函数,在函数内部对要返回的对象进行一些操作,就能达到这种继承方式了。不过这种方式看起来不是很好用,使用场景也不是很多吧。

寄生组合式继承(终极方案)

前几种方案或多或少都有些问题,寄生组合式方案算是最优解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function parent() {
  this.name = 'a';
  this.arr = [1, 2, 3];
}
parent.prototype.method = () => {};

function child() {
  // 构造函数式继承,借助call方法
  parent.call(this);
  this.attr = 123;
}

function clone(child, parent) {
  // 使用原型链继承时原本要用new关键词,
  // 会使得parent构造函数多执行一次,
  // 这里用Object.create方法避免了
  child.prototype = Object.create(parent);
  child.prototype.constructor = child;
}

clone(child, parent);
child.prototype.method = () => {};
let c = new child();

寄生组合式继承减少了调用父类的次数,在性能上更有优势。总的来说可以归纳为:使用call(或者apply)实现构造函数式继承,再写克隆函数,在其内部调用Object.create方法,实现原型链式继承;

class extends分析

ES6 中实现了class, extends语法糖,来看看它编译为 ES5 之后的结果;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ES6
class Person {
  constructor(name) {
    this.name = name;
  }

  // 原型方法

  // 即 Person.prototype.getName = function() { }

  // 下面可以简写为 getName() {...}

  getName = function () {
    console.log('Person:', this.name);
  };
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。

    super(name);

    this.age = age;
  }
}

const asuna = new Gamer('Asuna', 20);

asuna.getName(); // 成功访问到父类的方法

// ES5
function _possibleConstructorReturn(self, call) {
  // ...

  return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}

// 继承函数,相当于clone
function _inherits(subClass, superClass) {
  // 这里可以看到

  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,

      enumerable: false,

      writable: true,

      configurable: true
    }
  });

  if (superClass)
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : (subClass.__proto__ = superClass);
}

var Parent = function Parent() {
  // 验证是否是 Parent 构造出来的 this

  _classCallCheck(this, Parent);
};

var Child = (function (_Parent) {
  _inherits(Child, _Parent);

  function Child() {
    _classCallCheck(this, Child);

    return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
  }

  return Child;
})(Parent);

可以看到语法糖编译后使用的也是组合寄生的继承方式;

总结