淺談JavaScript中的繼承

近期,公司的業務處於上升期,對人才的需求彷佛比以往任什麼時候候都多。做爲公司的前端,有幸窺探到了公司的前端面試題目,其中有一題大概是這樣的(別激動,題目已經改了)前端

請用你本身的方式來實現JavaScript的繼承。node

這不是當年讓我一籌莫展的題目嗎?然而我卻痛苦地發現,在一年多之後的今天,即使我已經累計了很多的前端工做經驗,但再次面對這道題目的時候我依然一籌莫展。react

JavaScript

若是當時稍微懂一點ES6的話可能想都不想就能回答出來面試

class A extends B {

}
複製代碼

ES6總算給咱們帶來了class關鍵字,這使得JavaScript用起來更有面向對象編程語言的味道。然而JavaScript基於原型這一本質並無變,今天就來談談語法糖衣背後的東西,在ES6尚未盛行以前咱們如何作繼承?瞭解了底層原理以後,上面的面試題就再也不是問題了。編程

1. 基於原型

下面作一個簡單的類比,可能描述得並非十分準確。ruby

若是我把類想象成一個模子,對象則猶如是往模子澆灌材料所鑄形成的的器具。而這個模子,它規定了咱們指望的器具的樣式,規格,形狀等屬性,就如同編程語言裏面類預先定製了它所可以產生的對象的屬性,方法那樣。babel

而原型,則能夠理解成一個經過模子製做的器具,它擁有模子所預設的各類屬性,它自身就是樣式,規格,形狀這些屬性的集合,咱們能夠根據這個已有的器具去仿製更多一樣的器具。app

一樣是用於生產,只不過從行爲上來看他們屬性的源頭稍微有點不同。一種就如同設計師給了你一份文檔,告訴你我要這樣的產品,而後你把它量產。另外一種就是給你一個成型的產品,告訴你我要如出一轍的產品,而後你根據已有的產品去量產更多的類似產品。 我的以爲JS基於原型的行爲更像是後者。編程語言

2. JavaScript中的類定義

在ES5裏面咱們沒有class關鍵字,這使得它面向對象的特性沒有常規的面嚮對象語言那麼直觀,咱們只可以經過方法來模擬類。咱們來定義一個名爲Person的類,並設置一個實例方法printInformation,代碼以下函數

// 定義Person類
function Person(name, age) {
  this.name = name
  this.age = age
}

// 在原型上定義方法
Person.prototype.printInformation = function() {
  console.log("Name:" + this.name + " Age:" + this.age)
}

複製代碼

可見上面的代碼有點反人類,起碼不如通常的面向對象的編程語言直觀。爲了方便,我採用node環境來執行上面代碼,並查看它的效果。

> me = new Person("Lan", 26)
Person { name: 'Lan', age: 26 }

> me.printInformation()
Name:Lan Age:26

> me.name
'Lan'

> me.age
26

> console.log(Person.prototype)
Person { printInformation: [Function] }
複製代碼

對象中的nameage兩個屬性就像是咱們平時接觸得比較多的實例變量,而放在原型中的printInformation方法就能夠當作是實例方法。不過如今看來在JavaScript裏面他們之間的界限彷佛有點模糊,由於他們均可以經過對象來直接訪問,他們以前的區別在後面講繼承的時候可能會愈來愈清晰。

3. ES5中的繼承

若是用ES6的語法來實現繼承的話,彷佛沒有什麼難度,ES6提供了class語法,以及extends語法,使得咱們很容易就可以實現類與類之間的繼承,如下是React組件的官方推薦寫法。

import React from 'react'

class Button extends React.Component {
  constructor(props) {
    super(props)

    ....
  }

  ...
}
複製代碼

語法十分簡練,然而,在ES5的時候咱們彷佛並無這麼幸運,爲了實現子類繼承父類的行爲,咱們彷佛須要作不少工做。下面就採用以前定義的Person來做爲父類,另外再建立一個子類Student來繼承它。

1) 繼承父類實例變量

實例變量的初始化通常是放在構造函數中,而在ES5中咱們能夠直接把上面的Person這類函數看作是構造函數,另外Student構造函數也應該可以初始化nameage這兩個實例變量。那麼如何讓Student所產生的對象也擁有兩個字段呢?咱們能夠實例化的時候用當前上下文this去調用Person方法,那麼當前的上下文就可以包含nameage這兩個屬性了。

function Student(name, age, school) {
  Person.call(this, name, age)
  this.school = school || ''
}
複製代碼

簡單測試一下效果

> var student = new Student('Lan', 26)
undefined

> student
Student { name: 'Lan', age: 26, school: '' }
複製代碼

咱們所建立的事例已經具有了nameageschool這三個字段了,然而實例方法呢?

> student.printInformation
undefined
複製代碼

彷佛Student並沒能繼承父類Person的相關實例方法,接下來咱們看看如何從原型鏈中得到父類的實例方法。

2) 從原型中獲取方法

JavaScript是一門基於原型的面向對象編程語言。這裏咱們能夠簡單地把原型理解爲一個對象,它包含了一些方法或者屬性,只要爲咱們的設定了這個原型,它所初始化的對象就可以擁有該原型中所包含的方法了。

> Person.prototype
Person { printInformation: [Function] }

> Student.prototype
Student {}
複製代碼

可見Person的原型中包含了一個方法,Student的原型中啥都沒有。然而咱們卻不可以直接把Person的原型直接賦值給Student的原型,否則當Student往自身原型中添加方法的時候也會影響到Person的原型。怎麼破?把原型拷貝一份唄。

一開始說過JS裏面建立對象的過程有點像器具的仿造,因此咱們能夠用Person建立一個新的對象,而後把這個對象做爲Student的原型。不過這樣的話會有一個問題,若是用傳統的new方法來建立對象的話,它還會包含一些雜質

> new Person()
Person { name: undefined, age: undefined }
複製代碼

這些屬性應該在構造器中初始化的,咱們不該該把他們放在原型中。這個時候咱們能夠藉助ES5提供方法,建立一個稍微純淨點的對象。

> Object.create(Person.prototype)
Person {}
複製代碼

如今建立出來的對象沒有包含構造函數中的實例變量了,咱們能夠用它來做爲原型。稍微深刻窺探一下Object.create的原理,其實咱們能夠用JS代碼來簡單模擬它(注意只是簡單模擬),更詳細的內容能夠參考MDN文檔,粗略的模擬代碼以下

function createByPrototype(proto) {
  var F = function() {}
  F.prototype = proto
  return new F()
}
複製代碼

它接收一個原型做爲參數,而後在內部建立一個沒有實例變量的潔淨函數,並把傳入的參數設置爲它的原型。最後使用這個函數來建立一個對象,所獲得的對象就不會有實例變量了,可是它卻可以訪問原型中的方法,咱們能夠把它理解成一個新的原型

> var createObject = createByPrototype(Person.prototype)
undefined
> createObject.printInformation
[Function]
複製代碼

OK,理解了原理以後,咱們依舊用Object.create來建立新的原型

Student.prototype = Object.create(Person.prototype)
複製代碼

簡單演示一下

> var student = new Student('Lan', 26, 'GD')
undefined

> student
Person { name: 'Lan', age: 26, school: 'GD' }

> student.printInformation()
Name:Lan Age:26

// 小問題
> student.constructor
[Function: Person]
複製代碼

上面的結果代表咱們的繼承關係已經比較完善了,不過我遺留了一個小問題。咱們從student實例去尋找它的構造器,卻找到了Person這個構造函數,這顯然是有問題的,緣由我接下來說。

3) 構造器

經過student對象獲取構造器而時候沒法獲得Student這個構造函數,就至關於你問某我的的父親叫啥名字,他告訴了你他爺爺的名字同樣。咱大Ruby就沒有這種問題

[1] pry(main)> class A < String
[1] pry(main)* end
=> nil
[2] pry(main)> a = A.new
=> ""
[3] pry(main)> a.class
=> A
複製代碼

在JavaScript中,對象在當前類的原型中找不到對應的屬性,就會沿着原型鏈繼續往上查找。回到上面的例子,由於student實例在Student類的原型中找不到constructor這個屬性,因此它只能去更高層的Person的原型中去查找,因此纔會獲得這種結果

> Person.prototype.constructor
[Function: Person]

> Student.prototype.constructor
[Function: Person]
複製代碼

解決的辦法很簡單,就如同一個從小由爺爺扶養長大的孩子,很容易就把爺爺當成是父親,你要作的只是告訴他他的父親是誰,一句代碼就能夠了

Student.prototype.constructor = Student
....

> student.constructor
[Function: Student]
複製代碼

4. 代碼彙總

對前面所講的東西作個簡單的代碼彙總

// 定義一個簡單的`類`,幷包含實例變量
function Person(name, age) {
  this.name = name
  this.age = age
}

// 在原型鏈中定義`printInformation`方法
Person.prototype.printInformation = function() {
  console.log("Name:" + this.name + " Age:" + this.age)
}


// 定義一個Student子類,它會收集Person中的實例變量,而且本身會有一個新的實例變量 school
function Student(name, age, school) {
  Person.call(this, name, age)
  this.school = school || ''
}

// 繼承Person原型中的方法,並在原型鏈中添加構造器屬性
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
複製代碼

可見在ES5的時代連類的概念都不清晰,實現繼承都一大堆的麻煩,如今都ES6/7的時代了,通常人應該不會這樣寫代碼了。

另外,我上面所作的ES5實現的繼承方式,跟現在Babel的作法並不徹底同樣,Babel細節方面處理得會稍微多一些,這篇文章我只是闡述了大體的繼承原理。想要了解更多ES6轉換到ES5的細節,能夠在Babel的網站上嘗試。

5. 尾聲

今天這篇文章主要闡述了JavaScript基於原型的面向對象特性,以及在JavaScript裏面要實現繼承的注意事項。咱們須要經過手動調用父類構造函數來繼承父類的實例變量,還要經過設置原型來獲取父類原型中的方法或者屬性,最後要手動在原型鏈中設置constructor屬性來指向自身構造器。

Happy Coding

雖然在ES6的時代咱們再也不須要手動地作這些事情了,Babel這些現代編譯工具給咱們提供了不少的語法糖衣。可是我的以爲有些時候掌握這些老掉牙的知識或許可以讓你更加深入地理解這門語言的內涵,而不至於在工具盛行的今天,被各類工具,語法糖衣搞得暈頭轉向。面向工具編程是個高效的事情,然而當沒有了工具就不會編程了,可就不是什麼好事情了。

Happy Coding and Writing!!

相關文章
相關標籤/搜索