深刻學習 JavaScript —— 原型

前言

這一篇鴿了兩個周,實在不能再拖下去了。之因此會拖這麼久,除了一方面這一塊不像以前那樣只是一個小知識點,另外一方面是總想着能寫出點什麼新東西。如今發現,我還只是一名技術領域的追隨者,可以勉強跟得上技術的潮流就已經值得慶幸了;我所學習的東西,大可能是五六年前的標準,七八年前的框架,十幾年前的思想。因此,看清本身的位置,立足當下,把它看成本身的學習總結。javascript

正文開始

JavaScript 是一門動態語言,動態語言的哲學決定了咱們很難用一些靜態語言類的思想去操縱 JavaScript 的對象。雖然 es6 給出了不少新特性,這些新特性能夠幫助咱們在必定程度上模擬類,但不能忽視的是,它們是創建在原型的基礎之上的。深刻地瞭解原型,而不是一味地迴避,可讓咱們更好地使用 JavaScript。java

接下來我會按照本身的理解,整理關於原型的線索,內容以下:es6

  • 原型
    • 原型對象
      • 原型對象是什麼
      • 原型的複製機制
    • 原型鏈
      • 認識原型鏈的工具
      • 原型鏈是什麼

原型

先來了解下原型。之因此把原型放在對象前面,是由於我在學習 JavaScript 對象的相關知識時發現,它徹底繞不開原型。先對原型創建個大概印象,再以它爲工具,能夠更好地發現隱藏在JS語法背後的奧祕。瀏覽器

因此,原型是什麼?大概你已經在不一樣的場合見識過原型的介紹了,這裏請看MDN官方文檔關於對象原型的介紹:bash

JavaScript 常被描述爲一種基於原型的語言 (prototype-based language)——每一個對象擁有一個原型對象,對象以其原型爲模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關係常被稱爲原型鏈 (prototype chain),它解釋了爲什麼一個對象會擁有定義在其餘對象中的屬性和方法。markdown

準確地說,這些屬性和方法定義在Object的構造器函數(constructor functions)之上的 prototype 屬性上,而非對象實例自己。框架

在傳統的 OOP 中,首先定義「類」,此後建立對象實例時,類中定義的全部屬性和方法都被複制到實例中。在 JavaScript 中並不如此複製——而是在對象實例和它的構造器之間創建一個連接(它是 __proto__ 屬性,是從構造函數的 prototype 屬性派生的),以後經過上溯原型鏈,在構造器中找到這些屬性和方法。函數

注意:理解對象的原型(能夠經過 Object.getPrototypeOf(obj) 或者已被棄用的 __proto__ 屬性得到)與構造函數的 prototype 屬性之間的區別是很重要的。前者是每一個實例上都有的屬性,後者是構造函數的屬性。也就是說,Object.getPrototypeOf(new Foobar())Foobar.prototype 指向着同一個對象。工具

初學者看這段介紹確定是懵的,不要緊接下來一一做介紹。學習

原型對象

原型對象是什麼

這裏仍是先沿用官方的說明和例子。

在javascript中,函數能夠有屬性(注:能夠認爲JS中函數是特殊的對象)。每一個函數都有一個特殊的屬性叫做 prototype(注,mdn中文文檔這裏翻譯爲【原型】,但我認爲不翻譯比較好,後面也是如此),正以下面所展現的。

function Foo(){}
console.log( Foo.prototype );
// 你如何聲明函數並不重要,
// 在javascript中函數都會有一個默認的
// prototype 屬性。
var Foo = function(){}; 
console.log( Foo.prototype );
複製代碼

它們都會返回同一個對象。是的沒錯,這些函數的 prototype 指向了同一個特殊的對象,通常稱爲【原型對象】:

{
    constructor: ƒ Foo(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}
複製代碼

上面那些奇奇怪怪的函數,有些等會會說起到,有些本文沒法顧及。咱們先重點來看看該原型對象的兩個屬性名:constructor__proto__

前者 constructor,能夠翻譯成【構造器】,看起來它又指回原來的函數了。它們的關係彷佛是這樣的:

因此,是否是就意味着一個函數的原型對象constructor 必定指向該函數呢?確定不是,既然它是一個可訪問屬性,那麼它的對象確定就能夠修改。具體怎麼修改,這裏暫且不提,若是有讀者感興趣能夠留言,或者自行閱讀《你不知道的 JavaScript(上)》第二部分第5、六章。

後者 __proto__,看起來是一個很奇怪的屬性名,它有另外一個稱呼你可能見過,[[Prototype]]。嗯?這個怎麼看起來和以前的 prototype 屬性那麼像啊。二者有什麼關聯嗎?

這裏我暫時找不到關於 [[Prototype]] 的官方定義,ECMAScript 可能有但我懶得找了。不過,不管是 MDN 仍是《你所不知道的 JavaScript》都提到它是一個內部屬性。什麼意思呢?雖然JS中沒有私有屬性的概念,可是每一個對象都有一些內部屬性,其中就有 [[Prototype]]。在 ES 標準中,該屬性你是沒法經過常規的訪問方式訪問和設置的——包括點訪問法和括號訪問法(.__proto__ 不是標準實現,它只是個別瀏覽器廠商的內部實現)。甚至在 ES5 以前,除了 new 操做外沒法經過其它途徑操做該屬性(後文會介紹 ES5 支持的新方法)。

上面說到,__proto__ ,即 [[Prototype]] 是一個屬性,也指向了一個對象。該對象也有一個 construcotr 屬性,難道它也是原型對象?沒錯,而且JS 還有不少內置函數,包括 Function__proto__ 都指向這個原型對象。後文會提到,它其實就是 Object原型對象

你可能看得雲裏霧裏,不要緊看看下面代碼你就清楚了:

console.log(Foo.prototype.__proto__ === Object.prototype) // 谷歌瀏覽器下
// true
複製代碼

原型的複製機制

再回到 MDN 官方文檔的介紹,裏面有一句話提到JS原型的複製機制:

在對象實例和它的構造器之間創建一個連接(它是 __proto__ 屬性,是從構造函數的 prototype 屬性派生的)

這句話怎麼理解呢?請看下面這個代碼示例,環境是谷歌開發者工具:

var Foo = function(){}
// undefined
var foo = new Foo  // 無參數時可省略括號
// undefined
Foo.prototype === foo.__proto__
// true
複製代碼

看起來它們之間的關係是這樣的:

須要注意的是,foo 沒有屬性 prototype。這裏官方文檔也有提到:**[[Prototype]] 是每一個實例上都有的屬性,prototype 是構造函數的屬性。**這裏,每一個實例應是指對象(包括函數),也就是說JS中每一個對象都有 [[Prototype]] 屬性(並且是內部屬性)。你可能會好奇,那 Foo[[Prototype]] 屬性指向什麼呢?

這裏其實就涉及到原型鏈的知識了。

原型鏈

那麼,Foo[[Prototype]] 屬性指向什麼呢?前文說到,Foo原型對象[[Prototype]] 屬性指向 Object原型對象。那麼若是不是該函數自己是否也指向 Object原型對象呢?嘗試下看看:

console.log(Foo.__proto__ === Object.prototype) // 谷歌瀏覽器下
// false
複製代碼

看起來不是。這裏直接給出答案吧,它其實指向 Function原型對象,你能夠用一樣的方法檢測一下:

console.log(Foo.__proto__ === Function.prototype) // 谷歌瀏覽器下
// true
複製代碼

估計有些人會滿腦子疑問,那 Function[[Prototype]],及該函數原型對象[[Prototype]] 都指向什麼呢(包括 Objectfoo 等等)?有兩個方法,一個是你本身一個一個找,另外一個是請你看下面這幅圖,而且試着作下驗證:

你能夠着重看下 FunctionObjectFoo 這幾個函數的原型對象之間的關係。看的時候確定有不少疑問,**爲何光線條的樣式就有三種呢?**不要緊先放下繼續看下文。

不知道看完上文,特別是上圖,你對【原型鏈】是否有必定的認識?也許你腦中會反應過來剛剛說起的函數的 prototype ,及全部實例即對象的 [[Prototype]](請記住,二者不一樣一回事)。沒錯,當咱們談及原型鏈時,確定繞不開這兩個屬性。

認識原型鏈的工具

工欲善其事,必先利其器。咱們先了解下操縱這兩個屬性的工具。前者其實不用說了,它是一個可以直接修改的屬性;後者前文說過了,它是一個內部屬性,ES5 後有三個標準實現能夠操縱它:

  • Object.getPrototypeOf
  • Object.setPrototypeOf(注意,只有它是es6的新方法,其他兩個都是es5的)
  • Object.isPrototypeOf

除此以外,在 ES6 以前沒有 setPrototypeOf 方法時,有兩個替代性的方法:

  • Object.create (es5方法)
  • new 構造(es5 以前惟一修改 [[Prototype]] 的方法)

前文說過,__proto__ 是個別瀏覽器廠商的內部實現,還不是標準。綜上,這些方法,基本上就是 es6 後可以瞭解 [[Prototype]] 的工具了。它們的做用應該很好猜,若是不肯定的話還請自行搜索一下吧。

原型鏈是什麼

那麼,咱們如今再回到最開始的問題,原型鏈是什麼?

這裏用一些很容易搞錯的問題,做爲引子。請看下面代碼(後面不特殊說明,環境都是谷歌開發者工具):

var Foo = function(){}
console.log(Foo.constructor === Function)
// true
console.log(Object.constructor === Function)
// true
console.log(Function.constructor === Function)
// true
複製代碼

看起來很奇怪,尤爲是最後一個。嗯,前文的原型對象彷佛有提到這個 constructor,像 Foo原型對象constructor 正是它自己。前文也提到,該屬性是可直接訪問和修改的。看起來,該屬性應該只有原型對象纔有的,這些構造函數應該不可能有。嗯,這話說對了一半,請看下例:

Foo.hasOwnProperty("constructor")
// false
Object.hasOwnProperty("constructor")
// false
Function.hasOwnProperty("constructor")
// false
複製代碼

問題就來了,既然這三個函數都沒有該屬性,爲何以前的例子又是那樣輸出的呢?瀏覽器確定沒犯毛病,問題的根源就在原型鏈上。

回到原型鏈,咱們先找找 MDN 官方文檔中對它的描述:

每一個對象擁有一個原型對象,對象以其原型爲模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關係常被稱爲原型鏈 (prototype chain),它解釋了爲什麼一個對象會擁有定義在其餘對象中的屬性和方法。

簡單點說,原型鏈可讓一個對象訪問定義在其它對象中的屬性和方法。具體的訪問過程是怎樣的,這裏不細講,下一篇博客【對象部分】會討論這個問題。你這裏就能夠先按照你的喜愛簡單理解。

那麼,咱們用這個剛認識的原型鏈來剖析一下上面的問題吧。咱們都知道了,那三個函數確定是沒有 constructor 屬性的。那麼根據原型鏈的定義,它們確定是訪問了其它對象的該屬性,並且極可能仍是同一個對象。那是哪一個對象呢?我不妨再放一次圖(但是辛辛苦苦作了兩個小時的),這一副重點描了一個紅框,它其實就同時是這三個函數所訪問的那個「受害人」,以及三個紅圈,它們所在的三條線其實就是形成上述問題的「罪魁禍首」。

嗯,原型鏈簡直是魔鬼。這裏我爲了方便你們理解,將製做的這幅圖一部分線條用紅色和藍色描出來。紅線是關於 Object 的原型鏈,藍線是關於 Function 的原型鏈。

當你理解這幅圖時,你也就理解了,爲何函數能夠訪問一些特殊方法,對象又能夠訪問一些特殊方法。其實都是原型鏈的功勞。至於都有哪些特殊方法,我貼一副《你不知道的 JavaScript》書中的插圖:

其中左邊的紅色橢圓圈住 Function 的原型對象,右邊的紅色方框圈住 Object 的原型對象,省略號部分可自行搜索。另外,上面紅色方框圈住的 construct 是我認爲有問題之處。根據以前的代碼示例,Object 沒有 constructor,這裏應該指的是原型委託,但委託的對象又出了差錯。總之,還請讀者自行辨認。

後記

短短一篇博客還分前言後記是挺搞笑的。只不過我這篇確實寫了兩三天,工做量挺大的,光是例圖可能就花了三個多小時,因此也想請看了此篇後,以爲有幫助的讀者給我點個贊吧~

另外作一個預告,下一篇應該是關於 JS 的對象了,它與原型本緊密結合,應放一塊兒寫纔對;只不過此篇就寫了七千多字(markdown格式的統計,包括字母),再寫下去太長了。嗯,就這樣吧,感謝慧鑑。

相關文章
相關標籤/搜索