【第772期】你不懂JS:原型(Prototype)

圖片

前言前端

你所不懂js連載中斷幾天以後,今天它又來了。相信這又是讓大家一篇稍後閱讀的文章了。今天繼續由前端早讀課專欄做者@HetfieldJoe帶來連載《你不懂JS》的分享。ps:基礎原理老是苦澀的。算法


正文從這開始~編程


你不懂JS:this與對象原型 第五章:原型(Prototype)設計模式


【第767期】你不懂JS:混合(淆)「類」的對象 【第766期】你不懂JS:對象中,咱們幾回提到了[[Prototype]]鏈,但咱們沒有討論它究竟是什麼。如今咱們就詳細講解一下原型(prototype)。瀏覽器


注意: 全部模擬類拷貝行爲的企圖,也就是咱們在前面第四章描述的內容,稱爲各類種類的「mixin」,和咱們要在本章中講解的[[Prototype]]鏈機制徹底不一樣。安全


[[Prototype]]框架

JavaScript中的對象有一個內部屬性,在語言規範中稱爲[[Prototype]],它只是一個其餘對象的引用。幾乎全部的對象在被建立時,它的這個屬性都被賦予了一個非null值。ide


注意: 咱們立刻就會看到,一個對象擁有一個空的[[Prototype]]連接是 可能 的,雖然這有些不尋常。函數


考慮下面的代碼:工具

圖片


[[Prototype]]引用有什麼用?在【第766期】你不懂JS:對象中,咱們講解了[[Get]]操做,它會在你引用一個對象上的屬性時被調用,好比myObject.a。對於默認的[[Get]]操做來講,第一步就是檢查對象自己是否擁有一個a屬性,若是有,就使用它。


注意: ES6的代理(Proxy)超出了咱們要在本書內討論的範圍(將會在本系列的後續書目中涵蓋!),可是若是加入Proxy,咱們在這裏討論的關於普通[[Get]]和[[Put]]的行爲都是不被採用的。


可是若是myObject上 不 存在a屬性時,咱們就將注意力轉向對象的[[Prototype]]鏈。


若是默認的[[Get]]操做不能直接在對象上找到被請求的屬性,那麼會沿着對象的[[Prototype]]鏈 繼續處理。

image.png


注意: 咱們立刻就會解釋Object.create(..)是作什麼,如何作的。眼下先假設,它建立了一個對象,這個對象帶有一個鏈到指定的對象的[[Prototype]]連接,這個連接就是咱們要講解的。


那麼,咱們如今讓myObject``[[Prototype]]鏈到了anotherObject。雖然很明顯myObject.a實際上不存在,可是不管如何屬性訪問成功了(在anotherObject中找到了),並且確實找到了值2。


可是,若是在anotherObject上也沒有找到a,並且若是它的[[Prototype]]鏈不爲空,就沿着它繼續查找。


這個處理持續進行,直到找到名稱匹配的屬性,或者[[Prototype]]鏈終結。若是在鏈條的末尾都沒有找到匹配的屬性,那麼[[Get]]操做的返回結果爲undefined。


和這種[[Prototype]]鏈查詢處理類似,若是你使用for..in循環迭代一個對象,全部在它的鏈條上能夠到達的(而且是enumerable——見第三章)屬性都會被枚舉。若是你使用in操做符來測試一個屬性在一個對象上的存在性,in將會檢查對象的整個鏈條(無論 可枚舉性)。

image.png


因此,當你以各類方式進行屬性查詢時,[[Prototype]]鏈就會一個連接一個連接地被查詢。一旦找到屬性或者鏈條終結,這種查詢會就會中止。


Object.prototype

可是[[Prototype]]鏈到底在 哪裏 「終結」?


每一個 普通 的[[Prototype]]鏈的最頂端,是內建的Object.prototype。這個對象包含各類在整個JS中被使用的共通工具,由於JavaScript中全部普通(內建,而非被宿主環境擴展的)的對象都「衍生自」(也就是,使它們的[[Prototype]]頂端爲)Object.prototype對象。


你會在這裏發現一些你可能很熟悉的工具,好比.toString()和.valueOf()。在第三章中,咱們介紹了另外一個:.hasOwnProperty(..)。還有另一個你可能不太熟悉,但咱們將在這一章裏討論的Object.prototype上的函數是.isPrototypeOf(..)。


設置與遮蔽屬性

回到第三章,咱們提到過在對象上設置屬性要比僅僅在對象上添加新屬性或改變既存屬性的值更加微妙。如今咱們將更完整地重溫這個話題。

image.png


若是myObject對象已直接經擁有了普通的名爲foo的數據訪問器屬性,那麼這個賦值就和改變既存屬性的值同樣簡單。


若是foo尚未直接存在於myObject,[[Prototype]]就會被遍歷,就像[[Get]]操做那樣。若是在鏈條的任何地方都沒有找到foo,那麼就會像咱們指望的那樣,屬性foo就以指定的值被直接添加到myObject上。


然而,若是foo已經存在於鏈條更高層的某處,myObject.foo = "bar"賦值就可能會發生微妙的(也許使人詫異的)行爲。咱們一下子就詳細講解。


若是屬性名foo同時存在於myObject自己和從myObject開始的[[Prototype]]鏈的更高層,這樣的狀況稱爲 遮蔽。直接存在於myObject上的foo屬性會 遮蔽 任何出如今鏈條高層的foo屬性,由於myObject.foo查詢老是在尋找鏈條最底層的foo屬性。


正如咱們被暗示的那樣,在myObject上的foo遮蔽沒有看起來那麼簡單。咱們如今來考察myObject.foo = "bar"賦值的三種場景,當foo 不直接存在 於myObject,但 存在 於myObject的[[Prototype]]鏈的更高層:


若是一個普通的名爲foo的數據訪問屬性在[[Prototype]]鏈的高層某處被找到,並且沒有被標記爲只讀(writable:false),那麼一個名爲foo的新屬性就直接添加到myObject上,造成一個 遮蔽屬性。


若是一個foo在[[Prototype]]鏈的高層某處被找到,可是它被標記爲 只讀(writable:false) ,那麼設置既存屬性和在myObject上建立遮蔽屬性都是 不容許 的。若是代碼運行在strict mode下,一個錯誤會被拋出。不然,這個設置屬性值的操做會被無聲地忽略。不論怎樣,沒有發生遮蔽。


若是一個foo在[[Prototype]]鏈的高層某處被找到,並且它是一個setter(見第三章),那麼這個setter老是被調用。沒有foo會被添加到(也就是遮蔽在)myObject上,這個foosetter也不會被重定義。


大多數開發者認爲,若是一個屬性已經存在於[[Prototype]]鏈的高層,那麼對它的賦值([[Put]])將老是形成遮蔽。但如你所見,這僅在剛纔描述的三中場景中的一種(第一種)中是對的。


若是你想在第二和第三種狀況中遮蔽foo,那你就不能使用=賦值,而必須使用Object.defineProperty(..)(見第三章)將foo添加到myObject。


注意: 第二種狀況多是三種狀況中最讓人詫異的了。只讀 屬性的存在會阻止同名屬性在[[Prototype]]鏈的低層被建立(遮蔽)。這個限制的主要緣由是爲了加強類繼承屬性的幻覺。若是你想象位於鏈條高層的foo被繼承(拷貝)至myObject, 那麼在myObject上強制foo屬性不可寫就有道理。但若是你將幻覺和現實分開,並且認識到 實際上 沒有這樣的繼承拷貝發生(見第四,五章),那麼僅由於某些其餘的對象上擁有不可寫的foo,而致使myObject不能擁有foo屬性就有些不天然。並且更奇怪的是,這個限制僅限於=賦值,當使用Object.defineProperty(..)時不被強制。


若是你須要在方法間進行委託,方法 的遮蔽會致使難看的 顯式假想多態(見第四章)。通常來講,遮蔽與它帶來的好處相比太過複雜和微妙了,因此你應當儘可能避免它。第六章介紹另外一種設計模式,它提倡乾淨並且不鼓勵遮蔽。


遮蔽甚至會以微妙的方式隱含地發生,因此要想避免它必須當心。考慮這段代碼:

image.png


雖然看起來myObject.a++應當(經過委託)查詢並 原地 遞增anotherObject.a屬性,可是++操做符至關於myObject.a = myObject.a + 1。結果就是在[[Prototype]]上進行a的[[Get]]查詢,從anotherObject.a獲得當前的值2,將這個值遞增1,而後將值3用[[Put]]賦值到myObject上的新遮蔽屬性a上。噢!


修改你的委託屬性時要很是當心。若是你想遞增anotherObject.a, 那麼惟一正確的方法是anotherObject.a++。


「類」

如今你可能會想知道:「爲何 一個對象須要鏈到另外一個對象?」真正的好處是什麼?這是一個很恰當的問題,但在咱們可以徹底理解和體味它是什麼和如何有用以前,咱們必須首先理解[[Prototype]] 不是 什麼。


正如咱們在第四章講解的,在JavaScript中,對於對象來講沒有抽象模式/藍圖,即沒有面向類的語言中那樣的稱爲類的東西。JavaScript 只有 對象。


實際上,在全部語言中,JavaScript 幾乎是獨一無二的,也許是惟一的能夠被稱爲「面向對象」的語言,由於能夠根本沒有類而直接建立對象的語言不多,而JavaScript就是其中之一。


在JavaScript中,類不能(由於根本不存在)描述對象能夠作什麼。對象直接定義它本身的行爲。這裏 僅有 對象。


「類」函數

在JavaScript中有一種奇異的行爲被無恥地濫用了許多年來 山寨 成某些 看起來 像「類」的東西。咱們來仔細看看這種方式。


「某種程度的類」這種奇特的行爲取決於函數的一個奇怪的性質:全部的函數默認都會獲得一個公有的,不可枚舉的屬性,稱爲prototype,它能夠指向任意的對象。

image.png


這個對象常常被稱爲「Foo的原型」,由於咱們經過一個不幸地被命名爲Foo.prototype的屬性引用來訪問它。然而,咱們立刻會看到,這個術語命中註定地將咱們搞糊塗。爲了取代它,我將它稱爲「之前被認爲是Foo的原型的對象」。只是開個玩笑。「一個被隨意標記爲‘Foo點兒原型’的對象」,怎麼樣?


無論咱們怎麼稱呼它,這個對象究竟是什麼?


解釋它的最直接的方法是,每一個由調用new Foo()(見第二章)而建立的對象將最終(有些隨意地)被[[Prototype]]連接到這個「Foo點兒原型」對象。


讓咱們描繪一下:

image.png


當經過調用new Foo()建立a時,會發生的事情之一(見第二章瞭解全部 四個 步驟)是,a獲得一個內部[[Prototype]]連接,此連接鏈到Foo.prototype所指向的對象。


停一會來思考一下這句話的含義。


在面向類的語言中,能夠製造一個類的多個 拷貝(即「實例」),就像從模具中衝壓出某些東西同樣。咱們在第四章中看到,這是由於初始化(或者繼承)類的處理意味着,「將行爲計劃從這個類拷貝到物理對象中」,對於每一個新實例這都會發生。


可是在JavaScript中,沒有這樣的拷貝處理髮生。你不會建立類的多個實例。你能夠建立多個對象,它們的[[Prototype]]鏈接至一個共通對象。但默認地,沒有拷貝發生,如此這些對象彼此間最終不會徹底分離和切斷關係,而是 連接在一塊兒。


new Foo()獲得一個新對象(咱們叫他a),這個新對象a內部地被[[Prototype]]連接至Foo.prototype對象。


結果咱們獲得兩個對象,彼此連接。 如是而已。咱們沒有初始化一個對象。固然咱們也沒有作任何從一個「類」到一個實體對象拷貝。咱們只是讓兩個對象互相連接在一塊兒。


事實上,這個使大多數JS開發者沒法理解的祕密,是由於new Foo()函數調用實際上幾乎和創建連接的處理沒有任何 直接 關係。它是某種偶然的反作用。new Foo()是一個間接的,迂迴的方法來獲得咱們想要的:一個被連接到另外一個對象的對象。


咱們能用更直接的方法獲得咱們想要的嗎?能夠! 這位英雄就是Object.create(..)。咱們過會兒就談到它。


名稱的意義何在?

在JavaScript中,咱們不從一個對象(「類」)向另外一個對象(「實例」) 拷貝。咱們在對象之間製造 連接。對於[[Prototype]]機制,視覺上,箭頭的移動方向是從右至左,由下至上。

image.png


這種機制常被稱爲「原型繼承(prototypal inheritance)」(咱們很快就用代碼說明),它常常被說成是動態語言版的「類繼承」。這種說法試圖創建在面向類世界中對「繼承」含義的共識上。可是 弄擰(意思是:抹平) 了被理解語義,來適應動態腳本。


先入爲主,「繼承」這個詞有很強烈的含義(見第四章)。僅僅在它前面加入「原型」來區別於JavaScript中 實際上幾乎相反 的行爲,使真相在泥濘般的困惑中沉睡了近二十年。


我想說,將「原型」貼在「繼承」以前很大程度上搞反了它的實際意義,就像一隻手拿着一個桔子,另外一手拿着一個蘋果,而堅持說蘋果是一個「紅色的桔子」。不管我在它前面放什麼使人困惑的標籤,那都不會改變一個水果是蘋果而另外一個是桔子的 事實。


更好的方法是直白地將蘋果稱爲蘋果——使用最準確和最直接的術語。這樣能更容易地理解它們的類似之處和 許多不一樣之處,由於咱們都對「蘋果」的意義有一個簡單的,共享的理解。


因爲用語的模糊和歧義,我相信,對於解釋JavaScript機制真正如何工做來講,「原型繼承」這個標籤(以及試圖錯誤地應用全部面向類的術語,好比「類」,「構造器」,「實例」,「多態」等)自己帶來的 危害比好處多。


「繼承」意味着 拷貝 操做,而JavaScript不拷貝對象屬性(原生上,默認地)。相反,JS在兩個對象間創建連接,一個對象實質上能夠將對屬性/函數的訪問 委託 到另外一個對象上。對於描述JavaScript對象連接機制來講,「委託」是一個準確得多的術語。


另外一個有時被扔到JavaScript旁邊的術語是「差分繼承」。它的想法是,咱們能夠用一個對象與一個更泛化的對象的 不一樣 來描述一個它的行爲。好比,你要解釋汽車是一種載具,與其從新描述組成一個通常載具的全部特色,不如只說它有4個輪子。


若是你試着想象,在JS中任何給定的對象都是經過委託可用的全部行爲的總和,並且 在你思惟中你扁平化 全部的行爲到一個有形的 東西 中,那麼你就能夠(八九不離十地)看到「差分繼承」是如何自圓其說的。


但正如「原型繼承」,「差分繼承」假意使你的思惟模型比在語言中物理髮生的事情更重要。它忽視了這樣一個事實:對象B實際上不是一個差別結構,而是由一些定義好的特定性質,與一些沒有任何定義的「漏洞」組成的。正是經過這些「漏洞」(缺乏定義),委託能夠接管而且動態地用委託行爲「填補」它們。


對象不是像「差分繼承」的思惟模型所暗示的那樣,原生默認地,經過拷貝 扁平化到一個單獨的差別對象中。如此,對於描述JavaScript的[[Prototype]]機制如何工做來講,「差分繼承」就不是天然合理。


你 能夠選擇 偏向「差分繼承」這個術語和思惟模型,這是我的口味的問題,可是不可否認這個事實:它 僅僅 符合你思惟中的主觀過程,不是引擎的物理行爲。


"構造器"(Constructors)

讓咱們回到早先的代碼:

image.png


究竟是什麼致使咱們認爲Foo是一個「類」?


其一,咱們看到了new關鍵字的使用,就像面向類語言中人們構建類的對象那樣。另外,它看起來咱們事實上執行了一個類的 構造器 方法,由於Foo()其實是個被調用的方法,就像當你初始化一個真實的類時這個類的構造器被調用的那樣。


爲了使「構造器」的語義更令人糊塗,被隨意貼上標籤的Foo.prototype對象還有另一招。考慮這段代碼:

image.png


Foo.prototype對象默認地(就在代碼段中第一行中聲明的地方!)獲得一個公有的,稱爲.constructor的不可枚舉(見第三章)屬性,並且這個屬性回頭指向這個對象關聯的函數(這裏是Foo)。另外,咱們看到被「構造器」調用new Foo()建立的對象a 看起來 也擁有一個稱爲.constructor的屬性,也類似地指向「建立它的函數」。


注意: 這實際上不是真的。a上沒有.constructor屬性,而a.constructor確實解析成了Foo函數,「constructor」並不像它看起來的那樣實際意味着「被XX建立」。咱們很快就會解釋這個奇怪的地方。


哦,是的,另外……根據JavaScript世界中的慣例,「類」都以大寫字母開頭的單詞命名,因此使用Foo而不是foo強烈地意味着咱們打算讓它成爲一個「類」。這對你來講太明顯了,對吧!?


注意: 這個慣例是如此強大,以致於若是你在一個小寫字母名稱的方法上使用new調用,或並無在一個大寫字母開頭的函數上使用new,許多JS語法檢查器將會報告錯誤。這是由於咱們如此努力地想要在JavaScript中將(假的)「面向類」 搞對,因此咱們創建了這些語法規則來確保咱們使用了大寫字母,即使對JS引擎來說,大寫字母根本沒有 任何意義。


構造器仍是調用?

上面的代碼的段中,咱們試圖認爲Foo是一個「構造器」,是由於咱們用new調用它,並且咱們觀察到它「構建」了一個對象。


在現實中,Foo不會比你的程序中的其餘任何函數「更像構造器」。函數自身 不是 構造器。可是,當你在普通函數調用前面放一個new關鍵字時,這就將函數調用變成了「構造器調用」。事實上,new在某種意義上劫持了普通函數並將它以另外一種方式調用:構建一個對象,外加這個函數要作的其餘任何事。


舉個例子:

image.png


NothingSpecial僅僅是一個普通的函數,但當用new調用時,幾乎是一種反作用,它會 構建 一個對象,並被咱們賦值到a。這個 調用 是一個 構造器調用,可是NothingSpecial自己並非一個 構造器。


換句話說,在JavaScript中,更合適的說法是,「構造器」是在前面 用new關鍵字調用的任何函數。


函數不是構造器,可是當且僅當new被使用時,函數調用是一個「構造器調用」。


機制

僅僅是這些緣由使得JavaScript中關於「類」的討論變得命運多舛嗎?


不全是。 JS開發者們努力地儘量的模擬面向類:

image.png


這段代碼展現了另外兩種「面向類」的花招:


this.name = name:在每一個對象(分別在a和b上;參照第二章關於this綁定的內容)上添加了.name屬性,和類的實例包裝數據值很類似。


Foo.prototype.myName = ...:這也許是更有趣的技術,它在Foo.prototype對象上添加了一個屬性(函數)。如今,也許讓人驚奇,a.myName()能夠工做。可是是如何工做的?


在上面的代碼段中,有很強的傾向認爲當a和b被建立時,Foo.prototype上的屬性/函數被 拷貝 到了a與b倆個對象上。可是,這沒有發生。


在本章開頭,咱們解釋了[[Prototype]]鏈,和它做爲默認的[[Get]]算法的一部分,如何在不能直接在對象上找到屬性引用時提供後備的查詢步驟。


因而,得益於他們被建立的方式,a和b都最終擁有一個內部的[[Prototype]]連接鏈到Foo.prototype。當沒法分別在a和b中找到myName時,就會在Foo.prototype上找到(經過委託,見第六章)。


復活"構造器"

回想咱們剛纔對.constructor屬性的討論,怎麼看起來a.constructor === Foo爲true意味着a上實際擁有一個.constructor屬性,指向Foo?不對。


這只是一種不幸的混淆。實際上,.constructor引用也 委託 到了Foo.prototype,它 剛好 有一個指向Foo的默認屬性。


這 看起來 方便得可怕,一個被Foo構建的對象能夠訪問指向Foo的.constructor屬性。但這只不過是安全感上的錯覺。它是一個歡樂的巧合,幾乎是誤打誤撞,經過默認的[[Prototype]]委託a.constructor 剛好 指向Foo。實際上.construcor意味着「被XX構建」這種註定失敗的臆測會以幾種方式來咬到你。


第一,在Foo.prototype上的.constructor屬性僅當Foo函數被聲明時纔出如今對象上。若是你建立一個新對象,並用它替換函數默認的.prototype對象引用,這個新對象上將不會魔法般地獲得.contructor。


考慮這段代碼:

image.png


Object(..)沒有「構建」a1,是吧?看起來確實是Foo()「構建了」它。許多開發者認爲Foo()在執行構建,但當你認爲「構造器」意味着「被XX構建」時,一切就都崩塌了,由於若是那樣的話,a1.construcor應當是Foo,但它不是!


發生了什麼?a1沒有.constructor屬性,因此它沿者[[Prototype]]鏈向上委託到了Foo.prototype。可是這個對象也沒有.constructor(默認的Foo.prototype對象就會有!),因此它繼續委託,此次輪到了Object.prototype,委託鏈的最頂端。那個 對象上確實擁有.constructor,它指向內建的Object(..)函數。


誤解,消除。


固然,你能夠把.constructor加回到Foo.prototype對象上,可是要作一些手動工做,特別是若是你想要它與原生的行爲吻合,並不可枚舉時(見第三章)。


舉例來講:

image.png


要修復.constructor要花很多功夫。並且,咱們作的一切是爲了延續「構造器」意味着「被XX構建」的誤解。這是一種昂貴的假象。


事實上,一個對象上的.construcor默認地隨意指向一個函數,而這個函數反過來擁有一個指向被這個對象稱爲.prototype的對象。「構造器」和「原型」這兩個詞僅有鬆散的默認含義,多是真的也可能不是真的。最佳方案是提醒你本身,「構造器不是意味着被XX構建」。


.constructor不是一個魔法般不可變的屬性。它是不可枚舉的(見上面的代碼段),可是它的值是可寫的(能夠改變),並且,你能夠在[[Prototype]]鏈上的任何對象上添加或覆蓋(有意或無心地)名爲constructor的屬性,用你感受合適的任何值。


根據[[Get]]算法如何遍歷[[Prototype]]鏈,在任何地方找到的一個.constructor屬性引用解析的結果可能與你指望的十分不一樣。


看到它的實際意義有多隨便了嗎?


結果?某些像a1.constructor這樣隨意的對象屬性引用實際上不能被認爲是默認的函數引用。還有,咱們立刻就會看到,經過一個簡單的省略,a1.constructor能夠最終指向某些使人詫異,沒道理的地方。


a1.constructor是極其不可靠的,在你的代碼中不該依賴的不安全引用。通常來講,這樣的引用應當儘可能避免。


「(原型)繼承」

咱們已經看到了一些近似的「類」機制駭進JavaScript程序。可是若是咱們沒有一種近似的「繼承」,JavaScript的「類」將會更空洞。


實際上,咱們已經看到了一個常被稱爲「原型繼承」的機制如何工做:a能夠「繼承自」Foo.prototype,並所以能夠訪問myName()函數。可是咱們傳統的想法認爲「繼承」是兩個「類」間的關係,而非「類」與「實例」的關係。

image.png


回想以前這幅圖,它不只展現了從對象(也就是「實例」)a1到對象Foo.prototype的委託,並且從Bar.prototype到Foo.prototype,這酷似類繼承的親自概念。酷似,除了方向,箭頭表示的是委託連接,而不是拷貝操做。


這裏是一段典型的建立這樣的連接的「原型風格」代碼:

image.png


注意: 要想知道爲何上面代碼中的this指向a,參見第二章。


重要的部分是Bar.prototype = Object.create( Foo.prototype )。Object.create(..)憑空 建立 了一個「新」對象,並將這個新對象內部的[[Prototype]]連接到你指定的對象上(在這裏是Foo.prototype)。


換句話說,這一行的意思是:「作一個 新的 連接到‘Foo點兒prototype’的‘Bar點兒prototype’對象」。


當function Bar() { .. }被聲明時,就像其餘函數同樣,擁有一個鏈到默認對象的.prototype連接。可是 那個 對象沒有鏈到咱們但願的Foo.prototype。因此,咱們建立了一個 新 對象,鏈到咱們但願的地方,並將原來的錯誤連接的對象扔掉。


注意: 這裏一個常見的誤解/困惑是,下面兩種方法 也 能工做,可是他們不會如你指望的那樣工做:

image.png


Bar.prototype = Foo.prototype不會建立新對象讓Bar.prototype連接。它只是讓Bar.prototype成爲Foo.prototype的另外一個引用,將Bar直接鏈到Foo鏈着的 同一個對象:Foo.prototype。這意味着當你開始賦值時,好比Bar.prototype.myLabel = ...,你修改的 不是一個分離的對象 而是那個被分享的Foo.prototype對象自己,它將影響到全部連接到Foo.prototype的對象。這幾乎能夠肯定不是你想要的。若是這正是你想要的,那麼你根本就不須要Bar,你應當僅使用Foo來使你的代碼更簡單。


Bar.prototype = new Foo()確實 建立了一個新的對象,這個新對象也的確連接到了咱們但願的Foo.prototype。可是,它是用Foo(..)「構造器調用」來這樣作的。若是這個函數有任何反作用(好比logging,改變狀態,註冊其餘對象,向this添加數據屬性,等等),這些反作用就會在連接時發生(並且極可能是對錯誤的對象!),而不是像可能但願的那樣,僅最終在Bar()的「後裔」被建立時發生。


因而,咱們剩下的選擇就是使用Object.create(..)來製造一個新對象,這個對象被正確地連接,並且沒有調用Foo(..)時所產生的反作用。一個輕微的缺點是,咱們不得不建立新對象,並把舊的扔掉,而不是修改提供給咱們的默認既存對象。


若是有一種標準且可靠地方法來修改既存對象的連接就行了。ES6以前,有一個非標準的,並且不是徹底對全部瀏覽器通用的方法:經過能夠設置的.__proto__屬性。ES6中增長了Object.setPrototypeOf(..)輔助工具,它提供了標準且可預見的方法。


讓咱們一對一地比較ES6以前和ES6標準的技術如何處理將Bar.prototype連接至Foo.prototype:

image.png


若是忽略Object.create(..)方式在性能上的輕微劣勢(扔掉一個對象,而後被回收),其實它相對短一些並且可能比ES6+的方式更易讀。但兩種方式可能都只是語法表面現象。


考察「類」關係

若是你有一個對象a而且但願找到它委託至哪一個對象呢(若是有的話)?考察一個實例(一個JS對象)的繼承血統(在JS中是委託連接),在傳統的面向類環境中稱爲 自省(introspection)(或 反射(reflection))。


考慮下面的代碼:

image.png


那麼咱們如何自省a來找到它的「祖先」(委託鏈)呢?一種方式是接受「類」的困惑:

image.png


instanceof操做符的左邊操做數接收一個普通對象,右邊操做數接收一個 函數。instanceof回答的問題是:在a的整個[[Prototype]]鏈中,有沒有出現被那個被Foo.prototype所隨便指向的對象?


不幸的是,這意味着若是你擁有能夠用於測試的 函數(Foo,和它帶有的.prototype引用),你只能查詢某些對象(a)的「祖先」。若是你有兩個任意的對象,好比a和b,並且你想調查是否 這些對象 經過[[Prototype]]鏈相互關聯,單靠instanceof幫不上什麼忙。


注意: 若是你使用內建的.bind(..)工具來製造一個硬綁定的函數(見第二章),這個被建立的函數將不會擁有.prototype屬性。將instanceof與這樣的函數一塊兒使用時,將會透明地替換爲建立這個硬綁定函數的 目標函數 的.prototype。


將硬綁定函數用於「構造器調用」十分罕見,但若是你這麼作,它會表現得好像是 目標函數 被調用了,這意味着將instanceof與硬綁定函數一塊兒使用也會參照原版函數。


下面這段代碼展現了試圖經過「類」的語義和instanceof來推導 兩個對象 間的關係是多麼荒謬:

image.png


在isRelatedTo(..)內部,咱們借用一個一次性的函數F,從新對它的.prototype賦值,使他隨意地指向某個對象o2,以後問是否o1是F的「一個實例」。很明顯,o1實際上不是繼承或遺傳自F,甚至不是由F構建的,因此顯而易見這種實踐是愚蠢且讓人困惑的。這個問題歸根結底是將類的語義強加於JavaScript的尷尬,在這個例子中是由instanceof的間接語義揭露的。


第二種,也是更乾淨的方式,[[Prototype]]反射:

image.png

注意在這種狀況下,咱們並不真正關心(甚至 不須要)Foo,咱們僅須要一個 對象(在咱們的例子中就是隨意標誌爲Foo.prototype)來與另外一個 對象 測試。isPrototypeOf(..)回答的問題是:在a的整個[[Prototype]]鏈中,Foo.prototype出現過嗎?


一樣的問題,和徹底一樣的答案。可是在第二種方式中,咱們實際上不須要間接地引用一個.prototype屬性將被自動查詢的 函數(Foo)。


咱們 只須要 兩個 對象 來考察它們之間的關係。好比:

image.png


注意,這種方法根本不要求有一個函數(「類」)。它僅僅使用對象的直接引用b和c,來查詢他們的關係。換句話說,咱們上面的isRelatedTo(..)工具是內建在語言中的,它的名字叫isPrototypeOf(..)。


咱們也能夠直接取得一個對象的[[Prototype]]。在ES5中,這麼作的標準方法是:

image.png


並且你將注意到對象引用是咱們指望的:

image.png


大多數瀏覽器(不是所有!)還一種長期支持的,非標準方法能夠訪問內部的[[Prototype]]:

image.png


這個奇怪的.__proto__(直到ES6才標準化!)屬性「魔法般地」取得一個對象內部的[[Prototype]]做爲引用,若是你想要直接考察(甚至遍歷:.__proto__.__proto__...)[[Prototype]]鏈,這個引用十分有用。


和咱們早先看到的.constructor同樣,.__proto__實際上不存在於你考察的對象上(在咱們的例子中是a)。事實上,它存在於(不可枚舉地;見第二章)內建的Object.prototype上,和其餘的共通工具在一塊兒(.toString(), .isPrototypeOf(..), 等等)。


並且,.__proto__看起來像一個屬性,但實際上將它看作是一個getter/setter(見第三章)更合適。


大體地,咱們能夠這樣描述.__proto__實現(見第三章,對象屬性的定義):

image.png


因此,當咱們訪問a.__proto__(取得它的值)時,就好像調用a.__proto__()(調用getter函數)。雖然getter函數存在於Object.prototype上(參照第二章,this綁定規則),但這個函數調用將a用做它的this,因此它至關於在說Object.getPrototypeOf( a )。


.__proto__仍是一個可設置的屬性,就像早先展現過的ES6Object.setPrototypeOf(..)。然而,通常來講你 不該該改變一個既存對象的[[Prototype]]。


在某些容許對Array定義「子類」的框架中,深度地使用了一些很是複雜,高級的技術,可是在通常的編程實踐中常常是讓人皺眉頭的,由於這一般致使很是難理解/維護的代碼。


注意: 在ES6中,關鍵字class將容許某些近似方法,對像Array這樣的內建類型「定義子類」。參見附錄A中關於ES6中加入的class的討論。


僅有一小部分例外(就像前面提到過的),會設置一個默認函數.prototype對象的[[Prototype]],使它引用其餘的對象(Object.prototype以外的對象)。它們會避免將這個默認對象徹底替換爲一個新的連接對象。不然,爲了在之後更容易地閱讀你的代碼 最好將對象的[[Prototype]]連接做爲只讀性質對待。


注意: 針對雙下劃線,特別是在像__proto__這樣的屬性中開頭的部分,JavaScript社區非官方地創造了一個術語:「dunder」。因此,那些JavaScript的「酷小子」們一般將__proto__讀做「dunder proto」。


對象連接

正如咱們看到的,[[Prototype]]機制是一個內部連接,它存在於一個對象上,這個對象引用一些其餘的對象。


這種連接(主要)在對第一個對象進行屬性/方法引用,但這樣的屬性/方法不存在時實施。在這種狀況下,[[Prototype]]連接告訴引擎在那個被連接的對象上查找這個屬性/方法。接下來,若是這個對象不能知足查詢,它的[[Prototype]]又會被查找,如此繼續。這個在對象間的一系列連接構成了所謂的「原形鏈」。


建立連接

咱們已經完全揭露了爲何JavaScript的[[Prototype]]機制和 類 不 同樣,並且咱們也看到了如何在正確的對象間建立 連接。


[[Prototype]]機制的意義是什麼?爲何老是見到JS開發者們費那麼大力氣(模擬類)在他們的代碼中搞亂這些連接?


記得咱們在本章很靠前的地方說過Object.create(..)是英雄嗎?如今,咱們準備好看看爲何了。

image.png


Object.create(..)建立了一個連接到咱們指定的對象(foo)上的新對象(bar),這給了咱們[[Prototype]]機制的全部力量(委託),並且沒有new函數做爲類和構造器調用產生的任何不必的複雜性,搞亂.prototype和.constructor 引用,或任何其餘的多餘的東西。


注意: Object.create(null)建立一個擁有空(也就是null)[[Prototype]]連接的對象,如此這個對象不能委託到任何地方。由於這樣的對象沒有原形鏈,instancof操做符(前面解釋過)沒有東西可檢查,因此它總返回false。因爲他們典型的用途是在屬性中存儲數據,這種特殊的空[[Prototype]]對象常常被稱爲「dictionaries(字典)」,這主要是由於它們沒有可能受到在[[Prototype]]鏈上任何委託屬性/函數的影響,因此它們是純粹的扁平數據存儲。


咱們不 須要 類來在兩個對象間建立有意義的關係。咱們須要 真正關心 的惟一問題是對象爲了委託而連接在一塊兒,而Object.create(..)給咱們這種連接而且沒有一切關於類的爛設計。


填補Object.create()

Object.create(..)在ES5中被加入。你可能須要支持ES5以前的環境(好比老版本的IE),因此讓咱們來看一個Object.create(..)的簡單 部分 填補工具,它甚至能在更老的JS環境中給咱們所需的能力:

image.png


這個填補工具經過一個一次性的F函數並覆蓋它的.prototype屬性來指向咱們想鏈接到的對象。以後咱們用new F()構造器調用來製造一個將會鏈到咱們指定對象上的新對象。


Object.create(..)的這種用法是目前最多見的用法,由於他的這一部分是 能夠 填補的。ES5標準的內建Object.create(..)還提供了一個附加的功能,它是 不能 被ES5以前的版本填補的。如此,這個功能的使用遠沒有那麼常見。爲了完整性,讓我麼看看這個附加功能:

image.png


Object.create(..)的第二個參數指定了要添加在新對象上的屬性名,經過聲明每一個新屬性的 屬性描述符(見第三章)。由於在ES5以前的環境中填補屬性描述符是不可能的,因此Object.create(..)的這個附加功能沒法填補。


由於Object.create(..)的絕大多數用途都是使用填補安全的功能子集,因此大多數開發者在ES5以前的環境中使用這種 部分填補 也沒有問題。


有些開發者採起嚴格得多的觀點,也就是除非可以被 徹底 填補,不然沒有函數應該被填補。由於Object.create(..)能夠部分填補的工具之一,這種較狹窄的觀點會說,若是你須要在ES5以前的環境中使用Object.create(..)的任何功能,你應當使用自定義的工具,而不是填充,並且應當完全遠離使用Object.create這個名字。你能夠定義本身的工具,好比:

image.png


我不會分享這種嚴格的觀點。我徹底擁護如上面展現的Object.create(..)的常見部分填補,甚至在ES5以前的環境下在你的代碼中使用它。我將選擇權留給你。


連接做爲候補?

也許這麼想很吸引人:這些對象間的連接 主要 是爲了給「缺失」的屬性和方法提供某種候補。雖然這是一個可觀察到的結果,可是我不認爲這是考慮[[Prototype]]的正確方法。


考慮下面的代碼:

image.png


得益於[[Prototype]],這段代碼能夠工做,但若是你這樣寫是爲了 萬一 myObject不能處理某些開發者可能會調用的屬性/方法,而讓anotherObject做爲一個候補,你的軟件大概會變得有點兒「魔法」而且更難於理解和維護。


這不是說候補在任何狀況下都不是一個合適的設計模式,但它不是一個在JS中很常見的用法,因此若是你發現本身在這麼作,那麼你可能想要退一步並從新考慮它是否真的是合適且合理的設計。


注意: 在ES6中,引入了一個稱爲Proxy(代理)的高級功能,它能夠提供某種「方法未找到」類型的行爲。Proxy超出了本書的範圍,但會在之後的 「你不懂JS」 系列圖書中詳細講解。


這裏不要錯過一個重要的細節。


例如,你打算爲一個開發者設計軟件,若是即便在myObject上沒有cool()方法時調用myObject.cool()也能工做,會在你的API設計上引入一些「魔法」氣息,這可能會使將來維護你的軟件的開發者很吃驚。


然而你能夠在你的API設計上少用些「魔法」,而仍然利用[[Prototype]]連接的力量。

image.png


這裏,咱們調用myObject.doCool(),它是一個 實際存在於 myObject上的方法,這使咱們的API設計更清晰(沒那麼「魔法」)。在它內部,咱們的實現依照 委託設計模式(見第六章),利用[[Prototype]]委託到anotherObject.cool()。


換句話說,若是委託是一個內部實現細節,而非在你的API結構設計中簡單地暴露出來,它傾向於減小意外/困惑。咱們會在下一章中詳細解釋 委託。


複習

當試圖在一個對象上進行屬性訪問,而對象沒有該屬性時,對象內部的[[Prototype]]連接定義了[[Get]]操做(見第三章)下一步應當到哪裏尋找它。這種對象到對象的串行連接定義了對象的「原形鏈」(和嵌套的做用域鏈有些類似),在解析屬性時發揮做用。


全部普通的對象用內建的Object.prototype做爲原形鏈的頂端(就像做用域查詢的頂端是全局做用域),若是屬性沒能在鏈條的前面任何地方找到,屬性解析就會在這裏中止。toString(),valueOf(),和其餘幾種共同工具都存在於這個Object.prototype對象上,這解釋了語言中全部的對象是如何可以訪問他們的。


使兩個對象相互連接在一塊兒的最多見的方法是將new關鍵字與函數調用一塊兒使用,在它的四個步驟中(見第二章),就會創建一個新對象連接到另外一個對象。


那個用new調用的函數有一個被隨便地命名爲.prototype的屬性,這個屬性所引用的對象剛好就是這個新對象連接到的「另外一個對象」。帶有new的函數調用一般被稱爲「構造器」,儘管實際上它們並無像傳統的面相類語言那樣初始化一個類。


雖然這些JavaScript機制看起來和傳統面向類語言的「初始化類」和「類繼承」相似,而在JavaScript中的關鍵區別是,沒有拷貝發生。取而代之的是對象最終經過[[Prototype]]鏈連接在一塊兒。


因爲各類緣由,不光是前面提到的術語,「繼承」(和「原型繼承」)與全部其餘的OO用語,在考慮JavaScript實際如何工做時都沒有道理。


相反,「委託」是一個更確切的術語,由於這些關係不是 拷貝 而是委託 連接。

相關文章
相關標籤/搜索