【前端詞典】繼承(二) - 回的八種寫法·面試必問

小生方勤發表於2019-02-11

前言

上一篇我講了下繼承的基礎知識-原型和原型鏈。看到有人讀完我的技術分享後而有所得,我很開心;看到有人提意見我也虛心接受。

在講繼承的幾種方式前我打算先說一下——《孔乙己》。

《孔乙己》一文中我印象最深的是孔己乙的一個動作和一句對白一個提問。

一個動作:排出九文大錢
一句對白:竊書不能算偷……讀書人的事,能算偷麼
一個提問:回香豆的回字,怎樣寫的

孔乙己這種深受科舉教育毒害的讀書人,常會注意一些沒有用的字,而且把這看成學問和本領。會‘回’的幾種寫法就是有本領嗎?

我正思考這個問題時。好像有一個面試官在回答: 會‘回’的幾種寫法是不是本領我不清楚,不過我想知道你會幾種繼承的寫法。

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. 【前端詞典】繼承(二) - 回的八種寫法·面試必問

相關文章