【前端词典】继承(二) - 回的八种写法

7,455

前言

上一篇我讲了下继承的基础知识-原型和原型链。看到有人读完我的技术分享后而有所得,我很开心;看到有人提意见我也虚心接受。

在讲继承的几种方式前我打算先说一下——《孔乙己》。

《孔乙己》一文中我印象最深的是孔己乙的一个动作和一句对白一个提问。

一个动作:排出九文大钱
一句对白:窃书不能算偷……读书人的事,能算偷么
一个提问:回香豆的回字,怎样写的

孔乙己这种深受科举教育毒害的读书人,常会注意一些没有用的字,而且把这看成学问和本领。会‘回’的几种写法就是有本领吗?

我正思考这个问题时。好像有一个面试官在回答: 会‘回’的几种写法是不是本领我不清楚,不过我想知道你会几种继承的写法。

So 正月初七开工大吉,了解继承的几种方式,不失为一种有趣的迎新方式。

入题

JavaScript继承是非常重要的一个概念。我们有必要去了解,请大家多指教。

目的:简化代码逻辑和结构,实现代码重用

接下来我们一起学习下 8 种 JavaScript 实现继承的方法。

继承的实现

推荐组合继承(四)、寄生组合式继承(七)、ES6 继承(八)

一、原型链法(使用原型)

基本思想是利用原型让一个引用类型继承另一个引用类型的方法和实例。

代码如下

function staff(){ 
  this.company = 'ABC';
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  this.employeeName = name;
  this.profession = profession;
}
// 继承 staff
employee.prototype = new staff();
// 将这个对象的 constructor 手动改成 employee,否则还会是 staff
employee.prototype.constructor = employee;
// 不使用对象字面量方式创建原型方法,会重写原型链
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}
let instance = new employee('Andy','front-end');

// 测试 
console.log(instance.companyName()); // ABC
console.log(instance.showInfo());    // "Andy's profession is front-end"
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instance.hasOwnProperty('employeeName'))     // true
console.log(instance.hasOwnProperty('company'))          // false
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(employee.prototype.isPrototypeOf(instance)); // true
console.log(staff.prototype.isPrototypeOf(instance));    // true
console.log(Object.prototype.isPrototypeOf(instance));   // true

存在的问题

原型链实现继承最大的问题是:

当原型中存在引用类型值时,实例可以修改其值。

function staff(){ 
  this.test = [1,2,3,4];
}
function employee(name,profession){
  this.employeeName = name;
  this.profession = profession;
}
employee.prototype = new staff();
let instanceOne = new employee();
let instanceTwo = new employee();
instanceOne.test.push(5);
console.log(instanceTwo.test); // [1, 2, 3, 4, 5]

鉴于此问题:所以我们在实践中会少单独使用原型链实现继承。

小结

  1. 基于构造函数和原型链
  2. 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
  3. 通过 isPrototypeOf() 方法来确定原型和实例的关系
  4. 在实例中可以修改原型中引用类型的值

二.仅继承父构造函数的原型对象

此方法和方法一区别就是将:

employee.prototype = new staff();

改成:

Employee.prototype = Person.prototype;

优点

  1. 构建继承关系时不需要新建对象实例
  2. 由于公用一个原型对象,所以在访问对象的时候不需要遍历原型链,效率自然就高

缺点

  1. 和方法一相同,子对象的修改会影响父对象。

小结

  1. 基于构造函数,没有使用原型链
  2. 子对象和父对象公用一个原型对象

三、借用构造函数法

此方法可以解决原型中引用类型值被修改的问题

function staff(){ 
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  staff.call(this);	
  this.employeeName = name;
  this.profession = profession;
}
// 不使用对象字面量方式创建原型方法,会重写原型链
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}
let instanceOne = new employee('Andy','front-end');
let instanceTwo = new employee('Mick','after-end');
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
// console.log(instanceOne.companyName()); // 报错
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // false

从上面的结果可以看出:

  1. 借用构造函数法可以解决原型中引用类型值被修改的问题
  2. 可是 instanceOnestaff 已经没有原型链的关系了

缺点

  1. 只能继承父对象的实例属性和方法,不能继承父对象原型属性和方法
  2. 无法实现函数复用,每个子对象都有父对象实例的副本,性能欠优

四、组合继承(推荐)

指的是将原型链技术和借用构造函数技术结合起来,二者皆取其长处的一种经典继承方式。

function staff(){ 
  this.company = "ABC";	
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  // 继承属性
  staff.call(this);	
  this.employeeName = name;
  this.profession = profession;
}
// 继承方法
employee.prototype = new staff();
employee.prototype.constructor = employee;
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}

let instanceOne = new employee('Andy','front-end');
let instanceTwo = new employee('Mick','after-end');
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

优点

  1. 可以复用原型上定义的方法
  2. 可以保证每个函数有自己的属性,可以解决原型中引用类型值被修改的问题

缺点

  1. staff 会被调用 2 次:第 1 次是employee.prototype = new staff();,第 2 次是调用 staff.call(this)

五、原型式继承 - Object.create()

利用一个临时性的构造函数(空对象)作为中介,将某个对象直接赋值给构造函数的原型。

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

本质上 object() 对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

var employee = {
  test: [1,2,3]
}

let instanceOne = object(employee);
let instanceTwo = object(employee);
// 测试 
instanceOne.test.push(4);
console.log(instanceTwo.test); // [1, 2, 3, 4]

缺点

  1. 原型中引用类型值会被修改
  2. 无法传递参数

另,ES5 中存在 Object.create() 的方法规范化了原型式继承,能够代替 object 方法。

六、寄生式继承

要点:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象

function createAnother(original){
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}

createAnother 函数的主要作用是为构造函数新增属性和方法,以增强函数。

缺点(同原型式继承):

  1. 原型中引用类型值会被修改
  2. 无法传递参数

七、寄生组合式继承(推荐)

该方法主要是解决组合继承调用两次超类构造函数的问题。

function inheritPrototype(sub, super){
  var prototype = Object.create(super.prototype); // 创建对象,父原型的副本
  prototype.constructor = sub;                    // 增强对象
  sub.prototype = prototype;                      // 指定对象,赋给子的原型
}

function staff(){ 
  this.company = "ABC";	
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  staff.call(this, name);
  this.employeeName = name;
  this.profession = profession;
}

// 将父类原型指向子类
inheritPrototype(employee,staff)
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);            // [1,2,3]
console.log(instanceOne.companyName());   // ABC
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))           // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));  // true

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,

八、Class 的继承(推荐)

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class staff { 
  constructor(){
    this.company = "ABC";	
    this.test = [1,2,3];
  }
  companyName(){
    return this.company; 
  }
}
class employee extends staff {
  constructor(name,profession){
    super();
    this.employeeName = name;
    this.profession = profession;
  }
}

// 将父类原型指向子类
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类
console.log(Object.getPrototypeOf(employee) === staff)
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

  1. 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的this 对象,而是继承父类的 this 对象,然后对其进行加工。
  2. 只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。

`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于 A.prototype.constructor.call(this)

ES5 和 ES6 实现继承的区别

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super() 方法),然后再用子类的构造函数修改 this

extends 继承核心代码(寄生组合式继承)

function _inherits(subType, superType) {
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superType) {
    Object.setPrototypeOf 
    ? Object.setPrototypeOf(subType, superType) 
    : subType.__proto__ = superType;
  }
}

由此可以看出:

  1. 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  2. 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。

另:ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。

以上八种继承方式是比较常见的继承方式,倘若了解了这些方式的机制,在以后的面试中原型链与继承的问题也就不在话下了。

参考

  1. 《JavaScript 高级程序设计》
  2. es6.ruanyifeng.com/#docs/class…

后记

前后写了两个多星期,最主要的原因宝宝刚进入我的生活,无休的照顾宝宝,换尿布、喂奶、换衣之类花费了大量精力和时间。这篇文章也是在宝宝睡觉的间隙写成的,文章的内容如果觉得简陋,也请大家多包涵,提出宝贵的意见,日后有时间一定修改。

新年伊始,不忘初心

前端词典系列

《前端词典》这个系列会持续更新,每一期我都会讲一个出现频率较高的知识点。希望大家在阅读的过程当中可以斧正文中出现不严谨或是错误的地方,本人将不胜感激;若通过本系列而有所得,本人亦将不胜欣喜。

如果你觉得我的文章写的还不错,可以关注我的微信公众号,公众号里会提前剧透呦。

你也可以添加我的微信 wqhhsd, 欢迎交流。

下期预告

【前端词典】前端需要理解的网络基础

传送门

  1. 【前端词典】和媳妇讲代理后的意外收获
  2. 【前端词典】滚动穿透问题的解决方案
  3. 继承(一) - 原型链你真的懂吗?
  4. 【前端词典】继承(二) - 回的八种写法·面试必问