1. 程式人生 > >前端面試基礎回顧之深入JS繼承

前端面試基礎回顧之深入JS繼承

前言

對於靈活的js而言,繼承相比於java等語言,繼承實現方式可謂百花齊放。方式的多樣就意味著知識點繁多,當然也是面試時繞不開的點。撇開ES6 class不談,傳統的繼承方式你知道幾種?每種實現原理是什麼,優劣點能談談嗎。這裡就結合具體例子,按照漸進式的思路來看看繼承的發展。

準備

談到js繼承之前先回顧下js 例項化物件的實現方式。

建構函式是指可以通過new 來例項化物件的函式,目的就是為了複用,避免每次都手動宣告物件例項。

new 簡單實現如下:

function my_new(func){
    var obj = {}
    obj._proto_ = func.prototype // 修改原型鏈指向,拼接至func原型鏈
    func.call(obj) // 例項屬性賦值
    return obj
}

由上可以看出,通過建構函式呼叫,可以將例項屬性賦值到目標物件上。
如此可以推想,子類中呼叫父類建構函式同樣可以達到繼承的目的。
這就提供了js繼承的一種思路,即通過建構函式呼叫。

至於原型屬性,就是通過修改原型指向,來實現原型屬性的共享。
那麼繼承時同樣也可以通過該方式進行。

總結

基於建構函式和原型鏈兩種特性,結合js語言的靈活性。
繼承的實現方式雖然繁多萬變也不離其宗

繼承的n種方式

原型式繼承

定義:這種繼承藉助原型並基於已有的物件建立新物件,同時還不用建立自定義型別的方式稱為原型式繼承。

直接看程式碼更清晰:

function createObj(o) {
  function F() { }
  F.prototype = o;
  return new F();
}
var parent = {
  name: 'trigkit4',
  arr: ['brother', 'sister', 'baba']
};
var child1 = createObj(parent);

該方式表面上看基於物件建立,不需要建構函式(當然實際建構函式被封裝起來罷了)。只借助了原型物件,所以名稱為原型式繼承。

缺點
比較明顯優良者

  • 無法複用該繼承,每個子類的例項,都要走完整的createObj流程。
  • 對於子類物件
    因為建構函式封裝createObj中,對其而言,沒有建構函式。由此造成無法初始化時傳參。
    補充:其中 createObj 就是我們ES6中常用的Object.create(),不過Object.create進行了完善,允許額外引數來完善了。

解決思路
既然提到沒有建構函式導致了問題,那麼大膽猜測,更進一步就是涉及了建構函式的原型鏈繼承了。

原型鏈式繼承

定義:為了讓子類繼承父類的屬性(也包括方法),首先需要定義一個建構函式。然後,將父類的新例項賦值給建構函式的原型。

function Parent() {
  this.name = 'mike';
}
function Child() {
  this.age = 12;
}
Child.prototype = new Parent();
child.prototype.contructor = child // 原型屬性被覆蓋,所以要修正回來。
var child1 = new Child();

也就是直接修改子類的原型物件指父建構函式的例項,這樣把父類的例項屬性和原型屬性都掛到自己原型鏈上。

缺點

  • Child.prototype = new Parent() ,那麼子函式自身的原型屬性就被覆蓋了,如果需要就要在後面補充。
  • 子物件例項化時,無法向父類建構函式傳遞引數。
    例如在new Child()執行的時候,想要去覆蓋name,只能在Child.prototype = new Parent()時。 是我們在new Child()的時候統一傳參初始化是更常規需求。

解決思路
如何在子類初始化時,呼叫父類建構函式。結合前面的基礎,答案也呼之欲出。

借用建構函式(類式繼承)

類式繼承:是在子型別建構函式的內部呼叫超型別的建構函式。

思路比較清晰,由問題驅動。
既然原型鏈式子類不能向父類傳參的問題,那麼在子類初始化是呼叫父類不就滿足目的了。

示例如下:

function Parent(age) {
  this.name = ['mike', 'jack', 'smith'];
  this.age = age;
}
Parent.prototype.run = function () {
  return this.name + ' are both' + this.age;
};
function Child(age) {
  // 呼叫父類
  Parent.call(this, age);
}
var child1 = new Child(21);

這樣滿足了初始化時傳參的需求,但是問題也比較明顯。

child1.run //undefined

問題

  • 父類原型屬性丟失
    父類初始化只繼承了示例屬性,原型屬性在子類的原型鏈上丟失

解決思路
丟失的原因在於原型鏈沒有修改指向,那麼修改下指向不就完了。

組合繼承

定義:使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對例項屬性的繼承

示例:

function Parent(age) {
  this.name = ['mike', 'jack', 'smith'];
  this.age = age;
}
Parent.prototype.run = function () {
  return this.name + ' are both' + this.age;
};
function Child(age) {
  // 呼叫父類建構函式
  Parent.call(this, age);
}
Child.prototype = new Parent();//原型屬性繼承
Child.prototype.contructor = Child
var child1 = new Child(21);

這樣問題就避免了:

child1.run() // "mike,jack,smith are both21"

問題
功能滿足之後,就該關注效能了。這種繼承方式問題在於父類建構函式執行了兩次。
分別是:

function Child(age) {
  // 呼叫父類建構函式,第二次
  Parent.call(this, age);
}
Child.prototype = new Parent();//修改原型鏈指向,第一次

解決思路
解決自然是取消一次建構函式呼叫,要取消自然要分析這兩次執行,功能上是否有重複。
第一次同樣繼承了例項和原型屬性,第二次執行同樣繼承了父類的例項屬性。
因此第二次滿足對父類傳參的不可獲取性,因此只能思考能否第一次不呼叫父類建構函式,只繼承原型屬性。
答案自然是能,前面原型式繼承就是這個思路。

寄生組合式繼承

顧名思義,寄生指的是將繼承原型屬性的方法封裝在特定方法中,組合的是將建構函式繼承組合起來,補充原型式繼承的不足。

饒了點,直接看:

function createObj(o) {
  function F() { }
  F.prototype = o;
  return new F();
}
//繼承原型屬性 即原型式繼承
function create(parent, child) { 
  var f = createObj(parent.prototype);//獲取原型物件
  child.prototype = f
  child.prototype.constructor = child;//增強物件原型,即保持原有constructor指向
}

function Parent(name) {
  this.name = name;
  this.arr = ['brother', 'sister', 'parents'];
}
Parent.prototype.run = function () {
  return this.name;
};
function Child(name, age) {
  // 示例屬性
  Parent.call(this, name);
  this.age = age;
}
// 原型屬性繼承寄生於該方法中
create(Parent.prototype,Child);
var child1 = new Child('trigkit4', 21);

這樣沿著發現問題解決問題的思路直到相對完善的繼承方式。至於ES的方式本篇就不涉及了。

結束語

唯有厚積,才能薄發,想要心儀的offer,就得準備充裕,夯實基礎,切忌似是而非,道理我都懂就是答得不完全,這樣跟不懂差別也不太大。不算新的日子裡立個flag,每週三個知識點回顧。要去相信,你若盛開蝴蝶自來