Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入js之分析继承的多种方式 #13

Open
FE-Sadhu opened this issue Apr 26, 2019 · 0 comments
Open

深入js之分析继承的多种方式 #13

FE-Sadhu opened this issue Apr 26, 2019 · 0 comments
Labels
深入js笔记 about javascript

Comments

@FE-Sadhu
Copy link
Owner

学习继承的重要性,不用多说。下面直接开始啦。

原型链继承

原型链继承首先得知道原型链吧。忘记了可以去看这篇文章复习下:深入js之原型与原型链

function Person(name) {
  this.name = name;
  this.body = ['head', 'arm', 'leg'];
}

Person.prototype.act = function() {
  console.log('run');
}

function Wife(hobby) {
  this.hobby = hobby;
  this.friend = [1, 2, 3];
}

Wife.prototype.job = function() {
  console.log('FE');
}

// 将父构造函数的实例赋给子构造函数的原型。
Wife.prototype = new Person(); // Wife.prototype.__proto__ === Person.prototype

const kk = new Wife('clothes');

这方式的缺点:

  1. 多个实例对引用类型的操作会被篡改

  2. 子类型的原型上的 constructor 属性被重写了

  3. 给子类型原型添加属性和方法必须在替换原型之后

  4. 创建子类型实例时无法向父类型的构造函数传参

多个实例对引用类型的操作会被篡改

因为Person构造函数的实例里有引用类型的值,并且Person构造函数的实例是Wife构造函数的实例的原型对象,所以Wife的实例的原型链上存在了引用类型的值,自然多个Wife实例对该引用类型的操作会被篡改。举个例子,在上例基础上:

const kk = new Wife('clothes');

const ww = new Wife('cook');

kk.body.push('eye');

console.log(ww.body);
// [ 'head', 'arm', 'leg', 'eye' ]

子类型的原型上的 constructor 属性被重写

我们知道每个原型对象本身都有一个 constructor 属性指向与之对应的构造函数。但是在原型链继承方式中,我们直接执行了这个操作:Wife.prototype = new Person(),这个操作直接把Person的实例传给Wife的原型,覆盖了原本Wife对应的原型对象。又因为Person里面没有定义constructor属性,所以其实例也不会有,即现在Wife的原型对象也没有constructor属性了。

此时如果调用实例.constructor的话,会其在原型链上找到这个属性,这个属性指向的是Person构造函数:

console.log(ww.constructor);
// [Function: Person]

给子类型原型添加属性和方法必须在替换原型之后

创建子类型实例时无法向父类型的构造函数传参

这张图就说明了。

借用构造函数继承(经典继承)

function Person(name) {
  this.name = name;
  this.body = ['head', 'arm', 'leg'];
}

Person.prototype.act = function() {
  console.log('run');
}

function Wife(hobby) {
  Person.call(this, 'Linda')
  this.hobby = hobby;
  this.friend = [1, 2, 3];
}

Wife.prototype.job = function() {
  console.log('FE');
}

const kk = new Wife('clothes');

可以看出,此时可以给父类传参,并且该方法不会重写子类的原型,故也不会损坏子类的原型方法。此外,由于每个实例都会将父类中的属性复制一份,所以也不会发生多个实例篡改引用类型的问题(因为父类的实例属性不在原型中了)。

缺点也很明显,就是父构造函数原型对象上的属性和方法不晓得跑到哪里去了,因为该继承方式只能继承父构造函数实例的属性和方法而不能继承父构造函数原型上的属性和方法。

组合继承

该继承方式吸收上两种继承方式的优点:借用构造函数实现对构造函数实例上的属性和方法的继承,借用原型链实现对构造函数原型上的属性和方法的继承。

function Person(name) {
  this.name = name;
  this.body = ['head', 'arm', 'leg'];
}

Person.prototype.act = function() {
  console.log('run');
}

function Wife(hobby) {
  this.hobby = hobby;
  this.friend = [1, 2, 3];

  Person.call(this, 'Linda'); // 第二次调用父构造函数
}

Wife.prototype = new Person(); // 第一次调用父构造函数

Wife.prototype.constructor = Wife; // 修正constructor指针

Wife.prototype.job = function () {
  console.log('FE');
}

const kk = new Wife('clothes'); 

缺点也很明显。调用了两次父构造函数,造成Wife实例里和原型里都存在父构造函数的属性name月body。根据原型链的规则,实例上的这两个属性会屏蔽原型链上的两个同名属性。

原型式继承

该方式通过借助原型基于已有对象创建新的对象。

首先创建一个名为 object 的函数,然后在里面中创建一个空的函数 F,并将该函数的 prototype 指向传入的对象,最后返回该函数的实例。本质来讲,object() 对传入的对象做了一次 浅拷贝

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

测试:

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

const kk = {
  name: 'Linda',
  friends: [1, 2, 3],
  job: function() {
    console.log('FE');
  }
}

const exp = object(kk);

原型式继承相当于浅拷贝,通过原型链来访问属性,所以会导致引用类型被多个实例篡改。

在上面例子中加上这段代码输出什么?:

const exp2 = object(kk);
exp.name = 'sadhu';
console.log(exp2.name); // Linda

exp.name = 'sadhu'实际是在exp自身添加了属性name而非修改了原型上的name值。

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function creatObj(o) {
  const clone = Object.create(o);

  clone.act = 'coding'
  clone.job = function() {
    console.log('FE');
  };

  return clone;
}

const kk = {
  name: 'Linda',
  friends: [1, 2, 3],
  job: function() {
    console.log('FE');
  }
}

const exp = creatObj(kk);

缺点:

  1. 每次创建对象都会创建一遍方法。
  2. 引用类型 会被多个实例篡改。

注:Object.create()API的仅针对第一个参数的polyfill就是 :

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

寄生组合式继承

上面我们谈到了 组合继承,它的缺点是会调用两次父类,因此父类的实例属性会在子类的实例和其原型上各自创建一份,这会导致实例属性屏蔽原型链上的同名属性。

好在我们有 寄生组合式继承,它本质上是通过 寄生式继承 来继承父类的原型,然后再将结果指定给子类的原型。这可以说是在 ES6 之前最好的继承方式了。

function creatObj(child, parent) {
  const prototype = Object.create(parent.prototype);
  prototype.constructor = child
  child.prototype = prototype;
}

举个例子:

// function object(o) {
//   function F() {};
//   F.prototype = o;
//   return new F();
// }

function creatObj(child, parent) {
  const prototype = Object.create(parent.prototype); // 或者const prototype = object(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype;
}

function Person(name) {
  this.name = name;
  this.body = ['head', 'arm', 'leg'];
}

Person.prototype.act = function() {
  console.log('run');
}

function Wife(hobby) {
  this.hobby = hobby;
  this.friend = [1, 2, 3];

  Person.call(this, 'Linda');
}

// Wife.prototype = new Person(); 

// Wife.prototype.constructor = Wife; // 修正constructor指针
creatObj(Wife, Person);

Wife.prototype.job = function() {
  console.log('FE');
}

const kk = new Wife('clothes');

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

其实也有个缺点:要在子构造函数的原型上添加属性和方法只能在实现寄生组合式继承之后。

本文完。

希望看到各位技术人对这篇文章有不同的有“证据”,符合逻辑的分析、看法~

文章会第一时间更新在GitHub,觉得写得还不错的,可以点个star支持下作者🍪


参考:

  1. JavaScript深入之继承的多种方式和优缺点
  2. JavaScript 七大继承全解析
@FE-Sadhu FE-Sadhu added the 深入js笔记 about javascript label Apr 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
深入js笔记 about javascript
Projects
None yet
Development

No branches or pull requests

1 participant