深刻理解Javascript原型關係

前言

在軟件工程中,代碼重用的模式極爲重要,由於他們能夠顯著地減小軟件開發的成本。在那些主流的基於類的語言(好比Java,C++)中都是經過繼承(extend)來實現代碼複用,同時類繼承引入了一套類型規範。而JavaScript是一門弱類型的語言,歷來不須要類型裝換,在JavaScript中變量能夠指向任何類型的value(ES6規範中的類也只是語法糖,基於類的繼承本質上也是經過原型實現)。而基於原型的繼承模式能夠說提供了更加豐富的代碼重用模式(後面再詳細講解JavaScript中的經常使用繼承模式,本文只專一於JavaScript中的原型),一個對象能夠直接繼承另一個對象,從而得到新的方法和屬性。javascript

適合人羣

  • 對JavaScript原型有必定了解,但願深刻了解原型。
  • 具備JavaScript相關開發經驗
  • 不適合剛接觸JavaScript人員

對象

要理解JavaScript中的原型關係,首先必須弄清楚對象的基本概念。ECMAScript 5.1規範中描述的對象java

An object is a collection of properties and has a single prototype object. The prototype may be the null value.

直譯就是:對象是屬性的集合而且擁有一個原型對象。原型多是null(除非故意設置一個對象的原型爲null,不然只有Object.prototype的原型爲null)。咱們能夠簡單把對象想象成hash表。有一種說法是JavaScript中一切都是對象,這種說法並不許確。How is almost everything in Javascript an object?app

原型

JavaScript的原型存在着諸多矛盾。它的某些複雜的語法看起來就像那些基於類的語言,這些語法的問題掩蓋了它的原型機制。它不直接讓對象從其餘對象繼承,反而插入一個多餘的間接層:經過構建器函數產生對象。——JavaScript語言精粹第5章節(繼承)

雖然能夠直接設置一個對象的原型爲另一個對象,從而得到新的方法和屬性。以下所示:ecmascript

// Generic prototype for all letters.
let letter = {
  getNumber() {
    return this.number
  }
}

// 在ES6規範中,已經正式把__proto__屬性添加到規範中
// 也能夠經過Object.setPrototypeOf(obj, prototype) Object.getPrototypeOf(obj)
// 設置和獲取對象的原型對象

let a = { number: 1, __proto__: letter }
let b = { number: 2, __proto__: letter }
// ...
let z = { number: 26, __proto__: letter }

console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber() // 26
)

對象之間的關係能夠用下圖來表示函數

原型關係圖

但規範主要介紹瞭如何利用構造函數去構建原型關係。因此JavaScript語言精粹的做者Douglas Crockford纔會認爲:不讓對象直接繼承另一個對象,而經過中間層(構造函數)去實現顯得有些複雜並且存在一些弊端。調用構造器函數忘記new關鍵字,this將不會綁定到一個新對象上。悲劇的是,this將會綁定到全局對象上。詳情能夠閱讀JavaScript語言精粹繼承章節。ui

下面利用構造函數來實現上述一樣功能this

function Letter(number) {
  this.number = number
}

Letter.prototype.getNumber = function() {
  return this.number
}

let a = new Letter(1)
let b = new Letter(2)
let z = new Letter(26)

console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber() // 26
)

其中原型關係能夠下圖表示spa

原型關係

prototype__proto__屬性

咱們看下規範中有關原型介紹的核心,更多詳情請閱讀ECMAScript 5.1 4.2.1章節 prototype

...Each constructor is a function that has a property named 「prototype」 that is used to implement prototype-based inheritance and shared properties...

Every object created by a constructor has an implicit reference (called the object’s prototype) to the value of its constructor’s 「prototype」 property. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain...設計

經過上面的描述咱們能夠得出如下結論

  1. 構造函數就是一個函數,函數中包含prototype屬性用來實現基於原型的繼承和共享屬性。經過Function.prototype.bind方法構造出來的函數是個例外,它沒有prototype屬性。
  2. 經過被構造函數建立的對象都有一個隱式的引用指向構造函數的prototype屬性。
  3. 構造函數的prototype屬性值一樣也是普通一個對象,它也有一個隱式的引用(non-null)指向它的原型對象。這樣才造成了原型鏈,因此經過原型鏈去查找屬性值時候,並不會訪問prototype屬性,而是obj.__proto__.__proto__...這樣一層一層去尋找。

構造函數說到底本質上也是一個普通函數,只是該函數專門經過new關鍵字來生成對象。因此JavaScript語言沒法肯定哪一個函數是打算用來作構造函數的。因此每一個函數都會獲得一個prototype屬性,該屬性值是一個包含constructor屬性且constructor屬性值爲該函數的對象,以下所示。
構造函數

只有函數才擁有prototype屬性用來實現原型的繼承,其餘對象並無。對象擁有__proto__指向其原型對象,JavaScript引擎可經過內部屬性[[prptotype]]獲取對象的原型對象。

關於這兩個屬性聯繫能夠用一句話歸納:__proto__ is the actual object that is used in the prototype chain to resolve field,methods, etc. prototype is the object that is used to build __proto__ when you create an object with new.

爲何要設計構造函數

若是你已經瞭解JavaScript原型,那咱們能夠來說講JavaScript語法爲何要設計構造函數。

首先來加深一遍概念:JavaScript是一門基於原型繼承的語言,這意味着對象能夠直接從其餘對象繼承屬性,該語言是無類型的。
然而這種設計是偏離主流方向的,當時主流語言JavaScript,C++都是經過 new Class 的語法來建立對象。JavaScript顯然對它的原型本質缺少信心,因此它提供了一套和class語法相似的對象構建語法——也就是構造函數。經過instanceof操做符來判斷對象是否屬於某一類型。

instanceof 操做符

MDN介紹了其內部原理

The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.

instanceof操做符的語法

object instanceof constructor

簡單來講:instanceof 操做符就是判斷構造函數的prototype屬性值是否能在object對象的原型鏈中被找到,對就是這麼簡單。這樣經過構造函數語法JavaScript引入了類的概念(僞類)。

最後的彩蛋

知乎用戶wang z在其專欄中發佈一張有關JavaScript原型鏈圖,能夠說看懂了圖片也就清楚了JavaScript中的原型關係,感興趣的用戶能夠直接瀏覽詳情。以下圖所示
原型鏈圖

筆者就可能讀者遇到的問題備註以下:

  1. Object.__proto__=== Function.prototype。Object本質上是一個built-in的全局構造函數,也是Function構造函數的實例。因此Object.__proto__ === Function.prototype.
  2. Number,Date,Array等built-in構造函數都和Object構造函數同樣。
  3. Function.prototype本質也是對象,因此其__proto__指向Object.prototype

最後

若是你最後仍是沒有弄清楚JavaScript中的原型關係,能夠在評論中進行描述我將盡我所能幫你答疑解惑。
或許你也能夠看看參考文獻中的引用連接。

參考文獻

  1. JavaScript. The Core: 2nd Edition
  2. ecma-262
  3. JavaScript Prototype in Plain Language
  4. proto VS. prototype in JavaScript
  5. Javascript語言精粹第五章節
相關文章
相關標籤/搜索