摘要:首先,需要來理清一些基礎的計算機編程概念編程哲學與設計模式計算機編程理念源自于對現實抽象的哲學思考,面向對象編程是其一種思維方式,與它并駕齊驅的是另外兩種思路過程式和函數式編程。
JavaScript 中的原型機制一直以來都被眾多開發者(包括本人)低估甚至忽視了,這是因為絕大多數人沒有想要深刻理解這個機制的內涵,以及越來越多的開發者缺乏計算機編程相關的基礎知識。對于這樣的開發者來說 JavaScript 的原型機制是一個尚待發掘的大寶藏,深入了解下去會讓大家在編程這條路上走得更長遠,當然你不能妄想任何一種機制、模式或范式是完美無缺的。
首先,需要來理清一些基礎的計算機編程概念:
編程哲學與設計模式:Programming Philosophy and Design Pattern計算機編程理念源自于對現實抽象的哲學思考,面向對象編程(OOP)是其一種思維方式,與它并駕齊驅的是另外兩種思路:過程式和函數式編程。這三種方式對應于解決計算機架構問題的三種不同思路。它們也分別代表了不同的編程哲學。
具體實現編程架構的代碼方案可以稱為設計模式。設計模式是解決具體問題的一種最佳實踐,可以用在設計語言本身,也可以用在具體業務場景中。
三種思路在語言本身的設計和應用業務中是可能混用的,靈活的語言正如 JavaScript ,內部雖然是基于面向對象編程而實現,但在開發過程中也可以運用過程式編程或函數式編程的思路進行具體業務的設計。正因為這容易造成開發者的混亂,所以特別指出,下面一段討論的是針對語言內部的實現方式而不是應用業務。
面向對象編程語言的核心是對象,針對如何設計出一套語言的對象模型編程大師們又提出了三種不同的模式:類、原型、元類(元類是基于類模型產生的新模型)。三種模型造就了許多不同的編程語言,JavaScript 恰好是原型模式的典型代表,正如 JAVA 是基于類模式的典范,請謹記這一語言本身在設計模式上的區別。
很多語言由于自身的實現而限制了在其中可能應用到業務中的設計模式。但對于 JavaScript 這樣的語言來說,選擇是開放性的,因為我們經常在應用業務上聽到大家討論類繼承或原型繼承這樣的實現方案,這便是它非常靈活的一個表現。但對于類模式和原型模式,有一些本質上的概念區別和使用混淆是很多人沒有注意到的,下面對這兩種設計模式做一個詳細的討論。
作為一種設計模式的類:"Class" Design Pattern基于類的應用或業務架構實現可以稱為類設計模式,我們在業務開發中不可避免地會使用到繼承的概念便是出自于類的范疇。類不專屬于 JavaScript 語言范疇,JavaScript 中實質上也沒有實現真正的基于類設計模式的接口。JavaScript 中一切關于“類”的說法實際上都是一種有名無實的冒充和混淆。
我們通常以為在 JavaScript 中“類”是必選的,使用它來實現業務架構不僅天經地義而且是唯一的——這是對 JavaScript 的最大誤解。JavaScript 雖然是面向對象的編程語言,但以類作為對象模型來實現業務需求的方式只能說是一種設計模式:面向對象絕不等同于類。
類是一份產品制造說明書,指導生產機器生產符合其定義參數、具有相應功能的產品。它的用途在于規定而不在于實際使用,使用的是通過類制造出來的產品,在 JavaScript 中即對象。我們基于復用、繼承等工業化生產需求而使用類這套設計模式:規定 -> 制造 -> 使用。但我們千萬不能忘記,在工業化時代出現之前,通過手工的方式一樣可以制造產品,如果你需要批量生產模樣一樣的東西才需要這份產品制造說明說。就手段來說要澄清的一個誤區是,類并不是實現功能復用、廣義上的繼承等業務目標的唯一模式。
類:What"s Class類,是面向對象編程中一種通用對象模型,它是基于一種對現實中事物進行分類的抽象,天生帶有類別層級的觀念,如生物是一級類、動物是一個具有所有生物特性而派生出自己獨有特性的二級類,依照這樣的邏輯還可以繼續推及到其下更多細別的子類,這是一種將所有對象進行樹狀類別組織關聯的思維方式:
通過這張圖可以得出一個顯而易見卻容易被忽視的事實:永遠沒有一只具體的哺乳動物(比如說一只獅子)等同于哺乳動物這個類別,就像你不等于人類一樣。類是一個并不具有實體的概念,是人為的發明,為了將具有類似特性的事物分門別類以適應人腦簡化處理信息的方式,盡管自然并不是出于這樣的目的而生成各種事物的。
JavaScript 中類的概念也是人為的設計,為的是更靠近本身以類模式設計而成的語言,盡管它本身是以原型模式設計而成的。因此我們有了 new 一個對象這種操作,為的是更符合采用類這一設計模式來實踐面向對象編程。所以在此處埋下了第一個令人迷惑的種子:JavaScript 原生基于原型關聯起來的對象與基于類創建的與類關聯起來的對象兩種概念的混淆。對于發現了這一對使人迷惑的概念的開發者來說,便有了第一個疑問:
為什么基于原型模式設計而成的 JavaScript 不繼續在業務場景中使用原型設計模式,而是轉而求向類設計模式?
之前有過說明,實踐面向對象編程的方式有三種的,并且沒有任何一種是完美無缺的。所以請把類模式是最好的這種想法拋到九霄云外吧。暫且將這個問題移到潛意識中去,繼續了解一下類范疇的的其他相關概念。
實例:What"s Instance實例的概念基于類之上。正如自然界中單一的個體即是它所屬類別中的一個實例,面向對象語言中的一個對象就是它所屬類中的一個實例。語言通過類的規定,生成了具有內存實體的對象。在這樣的語言中,實例和對象的指代物是一致的,我們通常在類設計模式中采用實例來描述一個內存實體,而在編程實踐中使用對象來描述一個內存實體,其實是在不同層面上的語言轉換。理解這種詞語的轉換,對于我們在閱讀各種技術書籍時了解作者所選擇的表述視角是有幫助的。
創建實例操作的結果是將類的屬性和方法分別復制到不同的實例對象中,它們持有各自獨立的版本,這也意味著每一個由同一個類創建出的實例都是各自獨立互不影響的個體。
而在 JavaScript 中,事情就變得沒那么簡單了。不管在它的設計者設計出模擬類模式的原生 API 之前還是之后(當然官方一直有關于類的語法糖的支持),JavaScript 的世界實際上都是由且只由對象組成。當你創建了一個構造器函數或使用 ES6 的類定義語法時,其實質根本沒有真的定義了類,它是由對象偽裝而成的。
在這一事實的基礎上,就能發現既然“類”也是對象,那么我們本以為應用類模式建立的類與實例之間的純粹關系就被基于對象的模擬打破了。使用上面那個大自然的歸類例子再來解釋下這是什么意思:當哺乳動物這一類別是一只獅子時,它既是具體又是抽象的,作為一個類這只獅子囊括了所有的哺乳動物,它是凌駕于其他具體生物之上的;作為一個具體生物它又是被包含進它本身的...這似乎變成了一個邏輯問題。
人類在采用類這一概念時就已經將這個概念進行了抽象,它不指代任何具體的個體,即便它是一份具有實體的藍圖,也是與遵循它創造出來的物品不相同的東西。而在 JavaScript 里所發生的正是與之相矛盾的,它對于類模式的模擬實現其實是對類模式的顛覆。
繼承:What"s Inheritance繼承是類范疇里的重要概念,也是我們之所以要使用類的重要理由。繼承的目的是為了實現屬性或功能復用,順便減少編寫代碼的機械操作。類模式的繼承操作使子類擁有已經在父類里定義的屬性或方法,繼承而來的屬性或方法是子類所有的獨立版本,子類可以在此基礎上繼續修改已繼承的屬性或方法,并且擴展屬于自己的屬性或方法。
繼承即是基于現實中類別的多級抽象。前面圖示中所列出的樹狀結構就是對繼承很好的說明。在自然過程中,我們從祖先那里繼承而來的基因是屬于復制而來的獨立版本,現實中當然不存在繼承而來的一模一樣的基因,但即便是一模一樣的基因序列,也是各自獨立的版本,你身體中的基因再也不是祖先身體中的那個基因了。
尤其強調獨立這個詞,是因為類模式如實地實現了對自然界這一復制過程的模擬,而在 JavaScript 這一基于原型模式設計的語言中,我們又一次被它的表面類模式糊弄了。
在真正的類模式中,不管是父類還是子類都是獨立封裝好的一份規格,如果一個子類沒有繼承到父類的某一屬性或方法它自身也沒有進行擴展時,它的實例是不可能使用這個屬性或方法的。很明顯 JavaScript 中的繼承“完美解決了這個問題”,即便一個“類”自己沒有繼承也沒有擴展某個屬性或方法,它創造出的實例還可以從祖先那里借用。
結合實例一節所述,于是第二個問題呼之欲出:除了寫法相似之外,JavaScript 中幾乎所有與類相關的概念和行為都同慣常的類模式不那么相符,這真的可以被稱為是類模式的實現么?
基于以上兩個問題對自己進行了靈魂拷問,終于決定要來仔細瞧瞧 JavaScript 中一直被當做類的影子的那個親骨肉——原型。
作為一種機制的原型:"Prototype" Mechanism在詞匯語義上,原型的概念就與類所區別:原型是一個最初的對象。類的邏輯在于將已存在事物劃分層次,達到概括事物或分類的目的;原型的邏輯中沒有抽象的層級,它是根據已存在事物尋找能代表它最初的最本源的那一個,層層溯源,途徑的都是具象的。恐怕原型的概念對于熟稔哲學的人來說比類更為親切。它在編程上的思想是:新的物體藉由復制原型產生。
JavaScript 的原型機制就遵循了一定程度原型哲學的思路。而原型機制是 JavaScript 所特有的。原型機制的實現是,對象有一個內部屬性指向另一個對象,將二者聯結起來的屬性的變量名就是我們熟悉的 __proto__,它暴露了內部實現的原型,被指向的對象被稱為前者的原型,通常用 obj.__proto__ 來指代 obj 這個對象的原型。除此之外別忘記,這只是那個真實的原型對象的別稱。例如 origin 是另一個對象,以下這條語句就建立了這兩個對象的原型關聯關系:
let obj = {} let origin = {} obj.__proto__ = origin
你可以使用 origin 引用它指向的那個對象,其實質是一個內存地址,也可以使用 obj.__proto__ 來引用同樣的內存地址。作為一個多帶帶個體的對象和一個作為別的對象的原型的對象是合而為一的。(實際開發中不要直接使用 __proto__ ,此處只是為了簡便。應該用 Object.getPrototypeOf() 方法獲取原型對象)
原型機制用一句話概括就是:將單個對象建立起原型關聯關系的過程。
原型:What"s Prototype原型的語義概念上面已經介紹了,現在專門講講 JavaScript 中的原型。在 JavaScript 中,一切都是對象,那么這個世界總要有一個本源性的對象,就像上圖中的原核生物一樣,從它一生二而生成萬物。的確,這樣的一個被稱為最初的原型的對象是存在的,它就是 Object.prototype,原因是它再也無法向上追溯到任何對象了:
Object.prototype.__proto__ === null
這里我們要知道 null 代表的是“沒有”的意思。因此 JavaScript 的世界是從 Object.prototype 開始的。使用過 JavaScript 的開發者必定對這個對象印象深刻,但可能很多人從來沒有從這個視角看待它。
從它衍生出的一個重要的對象是一個函數 Object,它被稱為構造函數,盡管由 Object 構造函數創建出來的對象的原型都是指向 Object.prototype 的,但它自己的原型對象卻并不是 Object.prototype,而是 Function.prototype, Function.prototype 的原型才指向的是 Object.prototype,從這里我們可以隱隱窺見原型繼承的精髓。
再次強調一下,Object 是一個名字叫做“對象”的函數,Object.prototype 是一個叫做“對象構造器原型”的對象,與其他的原生構造器原型對象一樣,這些對象都是沒有自己獨立名稱的對象。在學習 JavaScript 時,必須好好區分這些基礎概念。
原型鏈:Prototype Chain原型鏈是原型繼承得以實現的基礎,但其實在原型中使用“繼承”這個詞是不那么準確的。原型鏈是內部機制通過私有的“原型”屬性實現對象之間的關聯而形成的一條鏈式屬性查找規則。它是單向度的,只能向上回溯,作為原型的對象無法查找它的繼承者們的任何屬性和方法。
原型鏈機制為 JavaScript 提供了實現強大功能的基礎,但可以想象,每次查找都是要花費額外開銷的,鏈條越長,開銷越大。它具有一個奇特的特點,即便某個對象上并未定義變量它也不會導致程序報錯,而是得到 undefined,這正是原型鏈機制自動查找屬性的一個后果。在沒有必要的情況下,應該避免編寫造成無謂的原型鏈查找的代碼。
我們時常需要通過判斷一個對象的屬性存在與否實現一些分支判斷,現在假設一條原型鏈是這樣的,
obj5 -> obj4 -> obj3 -> obj2 -> obj1
它們都不具有一個叫做 prop 的屬性,接著實現了如下簡化了過程的判斷場景:
let condition = action() ... if (condition) obj5.prop = true ... if (obj5.prop) { ... }
沒有任何問題的代碼對不對?當然,在條件為true時一切都很完美,但是如果 condition 為 false 呢,最后那條判斷語句就要查找5次最后才能回到判斷,如果鏈條更長呢?
// 解決方案1:不需要中間變量時 obj5.prop = action() // 解決方案2:需要中間變量時(可能二次改變) obj5.prop = condition // 當然還有更多變種...
或許有人覺得不太可能出現這樣的錯誤,但當代碼復雜到一定程度、中間過程非常繁瑣,工期非常緊迫時,一切都是有可能的,大問題都是因為那些小步驟中一個又一個的將就累積出來的。更何況作為一個有追求的開發者,即便瀏覽器為我們的代碼實現了最大程度的性能優化,不應該多一些對自我的要求么。
原型的作用:Why Prototype既然類設計模式已經如此流行并深入一代又一代開發者的腦海,那么為什么還會有原型設計模式的立足之地呢?毫無疑問是因為 JavaScript 的存在。作為網頁開發腳本的 JavaScript 一直唯我獨尊地統御著這片疆域,至少目前開來還沒有哪一種新的腳本語言能夠取代它的位置。但試想一下假如有一天一種以類模式設計而成的語言可以徹底取代它,原型機制將要消亡的那天大概就要來臨了,沒有哪一種語言能夠像 JavaScript 這樣能夠徹底地實踐原型機制了。
除了上面這個從語言層面來說的使用原型模式的前提,在 JavaScript 編程中使用原型模式而不是類模式實現業務功能也有一個讓人較為信服的原因。眾所周知使用類和原型的目的都是為了實現繼承,或者從更本質上來說是功能復用。
而在 JavaScript 中選擇原型模式的理由就在《You Don"t Know JS》這本書的章節中。作者敘述地那么明了,也不需要做額外的解析了。在此我只引用兩張圖作為最直觀的證據:
使用類模式實現繼承的邏輯圖 使用原型模式實現繼承的邏輯圖很多最為有效的問題處理方式通常都是最簡潔的方式,那些需要通過制造一個問題而去解決另一個問題的方法只會讓人頭腦暈眩,通常如果我們不能三言兩語就點出問題的核心,只能反思自己可能對問題理解得不夠透徹。如果能用一個非常簡單有效的方法實現同樣的結果,我實在是找不出什么原因非要去采用一個更加復雜的方法。
如上鋪墊了一大堆概念,到底能從中得出什么結論?——你為什么想在 JavaScript 的業務開發中使用類模式而不是原型模式?
原型模式作為 JavaScript 原生的設計模式卻沒有得到開發者足夠的理解,這與官方挖空心思強行模擬類模式的引導不無關系。
一位國外開發者 Eric Elliott 作了一個尖銳的比喻:
Using class inheritance in JavaScript is like driving your new Tesla Model S to the dealer and trading it in for a rusted out 1973 Ford Pinto.
翻譯:在 JavaScript 中使用類繼承就像把你嶄新的特斯拉Model S開到交易商那換了一輛生銹的1973年的福特平托。
這種比喻何以見得恐怕通過上面那兩張圖的比較已經有了一個大致的理解,即便是不打算放棄類模式的開發方式,深入理解這種爭議的緣由更助于提高我們的開發能力。我們需要時不時停下來多問問幾個為什么。
模式之爭:The War of Pattern一直以來在 JavaScript 中使用類繼承還是原型繼承似乎不是什么值得爭論的事情。但目前越來越多的國外開發者開始意識到原型模式在 JavaScript 中的自然性與邏輯簡潔性。類模式與原型模式開始升級為不同陣營實現功能復用的爭論點。
原型與類:Prototype vs. Class如果我說在 JavaScript 中使用類模式實現繼承是不符合目前人類大腦思維模式的復雜度的,我相信深入理解其中緣由的大多數人是會認可的,證據還是上面那張圖,有多少人能夠清晰地把上面的邏輯復演出來呢?恐怕大多數人都會在來來往往的直線曲線中迷失了方向,畢竟這樣的方式要求你不僅要對類、子類和實例的關系把握精準,還要時刻銘記著它們暗中的原型關聯關系,對于初學者來說這種雙重性關系一定是會在未來學習的道路上橫梗多年的坎。所以才需要在此尤為強調類與原型的種種區別。
但如果只是將注意力集中在對象之間的原型關聯關系上,事情就簡單多了。要清楚的是只要 JavaScript 語言本身的實現不改變,對象的原型關聯關系是我們無法擺脫的。
不過原型與類的爭論已經屬于“舊時代”的爭論,在隨后開發者們對原型模式更加深入的理解基礎上,形成了更深刻的認識和結論,“現代爭論”不再是原型與類的沖突,而是原型更新、更本質的行為委托。
原型與委托:Prototype vs. Delegation前面有提到過在原型里說“繼承”是不準確的,原因是名副其實的類繼承的行為本質上是復制,而 JavaScript 里無論是用何種方式實現“繼承”,它的本質行為都不是復制。
這里要澄清一個可能的誤會,JavaScript 當然是支持復制的,然而成熟的開發者都知道復制與引用原型上的方法可是完全不一樣的內存消耗,也正是由于 JavaScript 的原型機制才得以通過不增加副本的方式實現“繼承”,所以就此排除了這種使用復制實現“繼承”的方式。
那么在 JavaScript 里“繼承”的本質又是什么呢?許多開發者共同倡導了一種新的概念——委托。這種機制可以這樣簡單地理解:所謂的“繼承”其實是對象委托其原型們代勞辦事,繼承者借助原型上的方法實現功能。這個新的說法確實是比較生動地描述了原型繼承機制的本質的。
以后或許開發者們會達成共識,把使用原型模式實現繼承的方式稱為原型委托,如此更符合它的實際情況。但究竟想使用哪種模式進行開發最終還是在于個人的選擇,官方對類模式的不懈支持當然無法讓眾多開發者立即摒棄類語法糖,要從類轉換到純粹的原型上,是需要耗費思路轉換和習慣改變的成本的,希望對這個核心知識點的剖析能夠使學習者們更好地理解 JavaScript 的本質語言特性,啟發來者們更多的深入思考。
參考文獻:Reference
You Don"t Know JS: this & object prototypes
Chapter 4: Mixing (Up) "Class" Objects
Chapter 5: Prototypes
Chapter 6: Behavior Delegation
MDN web docs: 繼承與原型鏈
The JavaScript language:Prototypal inheritance
JavaScript Inheritance and the Prototype Chain
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance?
Common Misconceptions About Inheritance in JavaScript
面向對象編程(Object Oriented Programming,OOP,面向對象程序設計)
Understanding JavaScript: Prototype and Inheritance
JavaScript Prototypal Inheritance
The JavaScript Prototypal Inheritance Pattern
Prototypal Inheritance in JavaScript
面向過程,面向對象,函數式對同一個問題的思考方式
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105442.html
摘要:作用域鏈的作用就是做標示符解析。事件循環還有個明顯的特點單線程。早期都是用作開發,單線程可以比較好當規避同步問題,降低了開發門檻。單線程需要解決的是效率問題,里的解決思想是異步非阻塞。 0、前言 本人在大學時非常癡迷java,認為java就是世界上最好的語言,偶爾在項目中會用到一些javascript,但基本沒放在眼里。較全面的接觸javascript是在實習的時候,通過這次的了解發現...
摘要:前言原型這個概念在這門語言中是一個核心關鍵的知識點,但是你是否真的已經完全理解透徹了呢可能我個人的理解能力較差,因此經過多次翻閱書籍和實踐我才真正了解原型,所以記錄下來以加深理解,也以便日后深入探討。 前言 原型這個概念在JavaScript這門語言中是一個核心關鍵的知識點,但是你是否真的已經完全理解透徹了呢?可能我個人的理解能力較差,因此經過多次翻閱書籍和實踐我才真正了解原型,所以記...
摘要:同理,原型鏈也是實現繼承的主要方式的只是語法糖。原型對象也可能擁有原型,并從中繼承方法和屬性,一層一層以此類推。利用構造函數小明張三張三小明缺點每次實例化都需要復制一遍函數到實例里面。寄生構造函數模式只有被類出來的才能用。 showImg(https://segmentfault.com/img/bVbo4hv?w=1800&h=1000); 引言 最近又攀登了一下JS三座大山中的第二...
摘要:目錄導語理解對象和面向對象的程序設計創建對象的方式的繼承機制原型對象原型鏈與原型對象相關的方法小結導語前面的系列文章,基本把的核心知識點的基本語法標準庫等章節講解完本章開始進入核心知識點的高級部分面向對象的程序設計,這一部分的內容將會對對象 目錄 導語 1.理解對象和面向對象的程序設計 2.創建對象的方式 3.JavaScript的繼承機制 3.1 原型對象 3.2 原型鏈 3.3 與...
摘要:可能有信息敏感的同學已經了解到庫爆出嚴重安全漏洞,波及萬項目。以此為例,可見這次漏洞算是比較嚴重了。此外,凍結一個對象后該對象的原型也不能被修改。使用數據結構,不會存在原型污染狀況。 可能有信息敏感的同學已經了解到:Lodash 庫爆出嚴重安全漏洞,波及 400萬+ 項目。這個漏洞使得 lodash 連夜發版以解決潛在問題,并強烈建議開發者升級版本。 我們在忙著看熱鬧或者升級版本的同時...
閱讀 961·2023-04-26 02:49
閱讀 1172·2021-11-25 09:43
閱讀 2541·2021-11-18 10:02
閱讀 2919·2021-10-18 13:32
閱讀 1281·2019-08-30 13:54
閱讀 2074·2019-08-30 12:58
閱讀 3008·2019-08-29 14:06
閱讀 2154·2019-08-28 18:10