摘要:我們已經回答了的構造函數和原型都是誰的問題,現在牽扯出來一個,我們繼續檢查的構造函數是全局對象上屬性叫的對象的原型是個匿名函數,按照關于構造函數的約定,它應該是構造函數的屬性我們給這個對象起個名字,叫。
我不確定JavaScript語言是否應該被稱為Object-Oriented,因為Object Oriented是一組語言特性、編程模式、和設計與工程方法的籠統稱謂,沒有一個詳盡和大家都認可的checklist去比較,就很難在主觀意見上互相認同。
但JavaScript百分之一百是一門Object語言。
這句話有兩個直接含義:
除了原始類型(primitive type)值之外,一切皆對象,包括函數;
一切對象都是構造出來的,有一個函數作為它的構造函數(constructor);
JavaScript的另一個標志性特性是原型重用(prototype-based reuse),我在這里故意避免使用繼承(inheritance)這個詞語,是不想讓讀者立刻聯想C++/Java語言的繼承,請忘記它們;
JavaScript里的對象并非是Class的實例化,它沒有靜態結構的概念;當然這不意味這對象沒有結構,但對象的結構只能由構造函數在運行時構造出來,因此構造函數在JavaScript里的地位是很高的,它是唯一負責結構的地方。
每個對象都有一個原型,對象可以使用和重載原型對象上的數據成員或方法,這是對象的唯一重用機制;
介紹原型概念的文章和書很多,假定你理解原型的基本概念;這里需要指出的問題是,對象之間的屬性重用,和面向對象里面說的重用是兩回事;
你可以從重用的如此簡單的定義看出,它唯一的設計目的是想減少對象的數量,它提供的機制就是讓多個對象共享原型對象上的屬性,同時又可以有重載能力;
但不要對此浮想連篇,它和Java語言里通過繼承重用靜態結構和行為是完全兩回事,即使說“JavaScript的原型化重用僅僅是行為重用,而Java的重用是結構和行為的雙重重用”,這樣的表述也沒有意義,因為前者在運行時對象之間發生后者在靜態編譯時發生,一個在說我們發明了活字印刷術讓印刷變得更容易,另一個在說我們發明了電腦上的字體,你需要顯示哪個字就來到我這里拿;雖然結果有時看起來很像,但是機制上完全風馬牛不相及,不要上了阮一峰老師的當。
前面寫的這三條,可以作為構造JavaScript對象系統的三個基礎假設;
在JavaScript里最最底層的概念,并非你在如何使用JavaScript語言的那些教材中看到的種種編程概念,而是兩個詞語:構造和原型(或者說結構與重用)。
每個對象必有構造函數和原型,整個JavaScript系統里你看到的所有東西,都可以在概念或模型上這樣去理解,雖然實現上是另一回事。
JavaScript對運行環境(runtime)的假設只有一個,就是單線程事件模型,其他關于虛擬機該怎樣實現并無定義,也沒有bytecode的定義;ECMA262采用了一種類似偽碼的方式定義了對對象、屬性、函數的基本操作邏輯,所有實現,解釋器也好,JIT也好,無論如何執行JavaScript腳本,只要保證語義一致即可;其實這種偽碼定義方式本身,就暗示了某種特性,但我們暫且不表。
單線程的事件模型不是萬能的,但絕大多數情況下讓編程變得簡單;缺乏runtime定義使得這門語言并不實用,開發者總是需要完整的東西,但好在JavaScript自誕生起就有了第一個runtime:網絡瀏覽器,這讓它有了立足之地,之后又出現Node.js,它又找到一個可以生存的地方。
扯遠了,我們說回構造和原型的問題。
創世紀假如今天我們冒充上帝,開始構造JavaScript的對象世界,在這個世界里沒有什么不是對象,也遵循前述原則;
我們開始犯愁的第一個問題,似乎我們掉進了雞生蛋蛋生雞的邏輯怪圈。
對吧,第一個對象造不出來,因為對象需要構造函數構造,而函數也是對象,所以我們前面說的那個對象必然不是第一個對象。
當然邏輯是邏輯,我們可以先捏幾個最原始的對象出來,然后把constructor和__proto__引用裝載上去,讓它們成為系統最初的亞當和夏娃。反正上帝本來也回答不了亞當的媽是誰的問題,我們也這么做。
最初在ECMA262里并沒有約定JavaScript實現必須提供能訪問每個對象的原型對象的方法,它只是一個概念;但是node/v8和js shell都提供了__proto__這個名字的屬性,可以給出任何對象的原型;另一個方法是使用Object.getPrototypeOf方法。
注意__proto__和function對象的prototype屬性是兩回事,prototype是function對象的特有屬性(就像Array對象有length這個特有屬性),__proto__才是對象的原型;下面的描述和代碼里都使用__proto__這個很別扭的名字指對象的原型,它沒歧義,和代碼一致,再發明一個名字只會制造更多的混亂。
現在打開node shell。
> let m = {} undefined > m.__proto__ {} > m.__proto__ === m false
我們創建了一個空對象,叫做m,它的原型也是一個空對象,雖然同為空對象但是它們并非一個對象,所以并不相等;
> m.__proto__.__proto__ null > let op = m.__proto__ undefined
再沿著原型鏈往上爬,看看原型的原型是誰?沒了。這很好,我們知道m的原型沒有原型了,我們先把m的原型叫做op。
誰構造的op呢?
> op.constructor [Function: Object] > op.constructor === Object true
op的構造函數是全局那個叫Object的對象,它本身是一個函數;不要把Object理解成namespace,或者把Object對象上的方法理解為“靜態方法”,Object就是一個對象,它被賦值給了全局對象的Object屬性,雖然它有特別的功能,但是要把它理解成我們正在構造的對象世界中的一員,它只是在對象世界開天辟地時被構造好了而已,而我們在討論的就是這個構造的過程。
我們已經回答了op的構造函數和原型都是誰的問題,現在牽扯出來一個Object,我們繼續檢查;
> Object.constructor [Function: Function] > Object.constructor === Function true > Object.__proto__ [Function]
Object的構造函數是全局對象上屬性叫Function的對象;Object的原型是個匿名函數,按照JavaScript關于構造函數的約定,它應該是構造函數的prototype屬性:
> Object.__proto__ === Function.prototype true > let fp = Function.prototype undefined
我們給這個對象起個名字,叫fp。
> fp [Function] > fp.constructor [Function: Function] > fp.constructor === Function true > fp.__proto__ {} > fp.__proto__.__proto__ null > fp.__proto__ === op true
這個fp也不是很麻煩,我們發現它是一個匿名函數,它的構造函數是Function,而它的原型是op。
最后來看Function
> Function.constructor [Function: Function] > Function.__proto__ [Function] > Function.__proto__ === fp true
Function自己耍了一個賴皮,自己是自己的構造函數所以解決了雞和蛋的問題。Function的原型和prototype屬性指向了同一個對象fp。
所以到此為止呢,我們扒開了JavaScript世界里最原始的幾個對象,他們的原型關系是:
Function and Object -> fp -> op -> null
至于構造函數呢,因為Object是function,它的prototype是op,按照JavaScript的約定:function對象的prototype屬性指向的對象應該把constructor屬性設置成該function對象,即:
functionObject.prototype.constructor = functionObject
同樣的道理,Function的prototype是fp,fp的constructor也要設置成Function。
這是JavaScript里最基礎的四個對象;其他的一切對象,在模型和概念中都可以構造出來;
如果你在寫一個解釋器,你在最初就要把這些東西創造出來,然后創造一個global對象(或者叫context),在這個對象上裝上Object和Function,讓他們成為全局對象,至于op和fp,就讓他們藏在里面好了;編程中沒有需要用到他們的地方,如果要找到他們,可以用Object.prototype或者Function.prototype來找到。
所以到此為止,我們啟動了JavaScript的對象世界,有了Function我們就可以構造函數對象,有了函數我們就可以構造更多的對象,如果語言上允許(即不需要通過native code實現特殊功能),我們可以繼續創建Object.prototype和Function.prototype上的那些函數對象并把他們裝載上去,在概念模型上,內置對象沒有什么了不起,他們仍然可以被理解成被構造出來的對象;
事實上所有的函數作用域和函數內的變量也可以被理解成對象和它的屬性,在本文的結尾我們會談這個問題,當然它只是模型上的;
我們闡述了一切皆對象的含義;這個對象模型夠簡單嗎?我認為是的;它只有對象,函數,原型三個概念。
一些人說JavaScript是Lisp穿了馬甲,從對象模型上是可以成立的;因為Lisp里的數據結構是List,它是一個鏈表,每個節點有兩個slot,一個用于裝載值,另一個裝載next;而JavaScript對象其實也是鏈表,只不過它給每個節點增加了一個字符串標簽,即所謂的property name;但如果你用for ... in語法遍歷對象內部的時候,你仍然能看到內部結構的順序是穩定的,仍然是鏈表;
給每個節點加上label是JavaScript設計上非常聰明的地方,因為它讓文科生也可以參與如火如荼的編程活動。
但是這個對象模型說完了好像什么也沒有說?怎么JavaScript書上講的那么多概念都沒有提到呢?
這是問題的本質,也是很多Java過來的程序員很費勁的地方;JavaScript利用上述的這個非常簡單的對象模型,去模擬,或者說實現,其他所有的編程概念。
JavaScript最初的設計目的只是用于非常簡單的一些小功能,需要可編程;不管Brenden Eich是天才、拙劣、還是巧合的模仿了Lisp,以及Smalltalk和Self,他把兩個非常簡單且獨一無二的事情結合在了一起:
Lisp是λ Calculus在編程語言上的直接實現;原型重用的意思則是:
JavaScript:讓我們消滅必須用靜態定義約定動態對象結構的做法吧,編程君!任何靜態能定義出來的結構,我們在運行時也可以通過不斷的復制獲得啊,只是會慢一點點而已。 編程君:內存不夠怎么辦? JavaScript:我們有原型?。?編程君:好吧,但你要請我吃冰激凌。
不談工程實現,僅僅在概念和模型上紙上談兵的話,JavaScript語言模型之簡單,是很多老牌語言和新興腳本語言都難以企及的,它非常純粹。
函數對象與構造函數在談構造函數之前我們先看一段代碼:
// 構造對象的方式1 const factory = (a, b) => { return { a: a, b: b, sum: function() { return this.a + this.b } } }
return語句后面返回的對象,被稱為ex nihilo對象,拉丁語,out of nothing的意思,即這個對象沒有用一個專門的構造函數去構造,而是用那個全局的Object去構造了。
如果你僅僅是想創建具有同樣結構的對象實現功能,這樣的工廠方法足夠了。但是這樣寫,一方面,重用不方便;另一方面,如果我只構造幾十個這樣對象,可能不是什么大問題,但是如果要構造一百萬個呢?構造一百萬個會引發什么問題?
讓我們來重新強調對象的另一個含義:對象是有生命周期的;因為函數也是對象,所以函數對象也不例外;這一點是JavaScript和Java的巨大差異,后者的函數,本質上是靜態存在的,或者說和程序的生命周期一致。但JavaScript里的函數對象并非如此。
前面的sum屬性對應的匿名函數對象,它是什么時候創建呢?在return語句觸發Object構造的時候。如果要創建一百萬個對象呢?這個函數對象也會被創建一百萬次,產生一百萬個函數對象實例!
換句話說,這個工廠方法創建的一百萬個對象不僅狀態各有一份,方法也各有一份,前者是我們的意圖,但后者是巨大的負擔,雖然運行環境不會真的蠢到去把代碼復制一百萬份,但函數對象確實存在那么多,對象再小也有基礎的內存消耗,數量多時內存消耗不管怎樣都會可觀的,如果對象具有不只一個函數,那浪費就更可觀了。
這是JavaScript的一切皆對象,包括函數也是對象的代價。
遇到這樣的問題一般有兩種辦法,一種是修改機制,即前面說的模型,引入新的概念;另一種是加入策略,即在語言實現層面增加約定,但是利用現有機制,不增加概念;
JavaScript的設計者選擇了后者,這也是JavaScript的看似古怪的構造函數的由來。
設計者說可以這樣來解決問題:如果一個函數對象的目的是構造其他對象(即構造函數),它需要一個對象作為它的合作者,裝載所有被構造的對象的公用函數,兩者之間的聯系這樣建立:
構造函數對象需要具有一個名稱為prototype的屬性,指向公用函數容器對象;
公用函數容器對象需要具有一個名稱為constructor的屬性,指向構造函數對象;
這個公用函數容器對象在創建function對象的時候,如果不是arrow function,它自動就有prototype屬性,指向一個空對象;如果是arrow函數,沒有這個屬性,arrow函數也不可以和new一起使用;
> function x() {} undefined > x.prototype x {} > const y = () => {} undefined > y.prototype undefined >
當調用構造函數時,通過使用new關鍵字明確表示要構造對象,這時函數的工作方式變了:
先創建一個空對象N,把它的原型__proto__設置成該構造函數對象的prototype屬性;
把N的constructor屬性設置為構造函數對象;
把N bind成構造函數的this;
運行構造函數;
返回新對象N,不管構造函數返回了什么;
new被定義成關鍵字是為了兼容其他語言使用者的習慣,寫成函數也一樣:
function NEW(constructor, ...args) { let obj = Object.create(constructor.prototype) obj.construtor = constructor constructor.bind(obj)(...args) return obj }
另一個關鍵字instanceof,則反過來工作,如果表達式是A instanceof B,如果不考慮繼承問題,就去判斷A.constructor === B即可;繼承的問題后面討論。
理解了這個過程就會明白,JavaScript里的構造函數問題,其實并非在發明構造函數的新語法,而是保持語言模型不變,讓他能夠構造共享原型的對象的一種方式。
這就是為什么在ES5語法里看到的構造函數和它的原型的代碼是類似這樣的:
function X(name) { this.name = name } X.prototype.hello = function() { console.log("hello " + this.name) } var x1 = new X("alice") x1.hello() var x2 = new X("bob") x2.hello()
但即使需要這樣做,上面的寫法也不是唯一的寫法,也可以這樣直接寫工廠方法:
let methods = { hello: function() { console.log("hello" + this.name) } } function createX(name) { let obj = Object.create(Object.assign({}, methods)) // 使用Object.assign可以merge多個methods obj.name = name return obj }
同樣實現構造共享原型的對象,只是返回的對象不具有constructor屬性,instanceof沒法用,但如果你不需要instanceof,也不需要設計多層的繼承,這是可用的方法;
總結一下關于構造函數的這一節;
首先JavaScript在定義函數時,并不區分這個函數是不是構造函數,是否是構造函數取決于你是否使用new調用;
其次,如果一個函數是構造函數,它不是一個人在戰斗,它需要和它的prototype屬性指向的對象合作,該對象將是構造的對象的原型,請把兩個對象而不是一個對象印在腦子里,這對后面理解繼承非常關鍵;
第三,和Java里那種數據成員和方法成員在心理上位于一個對象容器內不同,JavaScript的對象在設計上就要理解為數據(或者狀態)在自己身上,方法(函數對象)在原型身上,這仍然是兩個對象在合作,表現得象一個對象。
繼承JavaScript里的繼承仍然不是語言特性,在這個問題上我們繼續沿用前面的思路:用JavaScript的原型重用能力,去模擬,或者說實現Java語言里的繼承形式。
我們先說思路,假想我們就是Brenden Eich幾分鐘。
假如我們已經用構造共享原型的對象的思路,寫了一個構造函數BaseConstructor,它負責創建每個對象的數據或狀態屬性,也有了一個合作者BaseConstructor.prototype,它提供了方法BaseMethod1, ...;現在我們需要拓展它,要增加一部分狀態或者屬性,也要增加一部分方法,我們該怎么做?
首先我們考慮拓展方法,這不難,如果我們構建一個對象,把它的原型設置為BaseConstructor.prototype,然后在新對象里添加方法即可;
其次我們未來需要使用的對象應該都以該對象為原型,因為原有方法和擴展方法都能通過它訪問;這預示了我們需要一個新的構造函數以該對象作為prototype屬性;邏輯上可以是這樣:
Base <-> Base.prototype ^ ^ | * | call * __proto__ | * Extended <-> Extended.prototype
Extended函數可以創建Extended.prototype里擴展方法所需要的狀態或數據成員;但是Base.prototype里需要的狀態或者數據成員需要Base來創建,我們肯定不希望把Base里的代碼復制一份到Extended內;我們需要調用它來創建原有方法所需的狀態或數據成員。
function Base(name) { this.name = name} Base.prototype.printName = function() { console.log(this.name) } function Extended(name, age) { Base.bind(this)(name) this.age = age } Extended.prototype = Object.create(Base.prototype) Extended.prototype.constructor = Extended Extended.prototype.printAge = function() { console.log(this.age) }
這里tricky的地方有幾處:
第一,在Extended函數內,先把this bind到Base構造函數上,然后提供name參數調用它,這樣this就會具有printName所需的name屬性,實現結構繼承;
第二,我們使用Object.create方法創建了一個以Base.prototype為原型的新對象,把它設置為Extended.prototype,實現行為繼承;
第三,把Extended.prototype.constructor設置為Extended構造函數,這樣我們可以使用instanceof語法糖;
最后我們在Extended函數內創建新的狀態或數據屬性,我們也在Extended.prototype上添加新的函數方法;
或者我們說我們找到了一種方式既拓展了構造函數構造的新對象的數據屬性,也拓展了它的函數屬性,沿著兩條鏈平行實施,達到了我們的目的。
在JavaScript里使用這種在原有構造函數及其prototype對象上拓展出一對新的構造函數和prototype對象的拓展方式,我們稱之為繼承。
因為對象可以重載原型對象的屬性,所以在function.prototype的原型鏈上,重載函數的能力也具有了。
ClassJavaScript里沒有type系統意義上的Class的概念。class關鍵字仍然是語法糖。
class A { constructor () { // 這是構造函數 } method() { // 這是A.prototype上的方法 } }
這個語法比前面分開寫構造函數和prototype對象的寫法要簡潔干凈很多,但是帶著Java的Class的概念試圖去理解它,更容易被誤導了。
A在這里仍然是函數對象,只不過它只能當構造函數用,必須用new調用;其他還有一些細節差異,不贅述了;
如果是繼承呢?
class Base { constructor() {} method1() {} } class Extended extends Base { constructor() { super() //... } method2() {} }
也是大同小異;Extended構造函數內需要調用super()來實現調用Base構造函數構造屬性;這一句必須調用,否則沒有this,這是class語法和前面ES5語法的一個差異,在ES5語法內,新對象是在調用Extended構造函數時立刻創建的,在class語法中,這個對象是沿著super()向上爬到最頂層構造函數才創建的,所以如果不調用super就沒this了。
實際上在JavaScript里的繼承,應該當作一種Pattern來理解,即:使用構造函數和它的prototype屬性對象合作來模擬傳統OO語言里的繼承形式,把它叫做Inheritance Pattern恰當的多。
函數作用域前面我們曾冒充上帝,假想一個JavaScript程序啟動后,如何從零開始構造整個對象世界;現在我們得寸進尺,冒充上帝他媽,考慮站在執行器的視角上,如果拿到一份JavaScript腳本如何執行;
假定我們已經使用了底層語言,例如C/C++,實現了JavaScript的對象模型,即很容易創建對象,維護原型鏈。
我們先創建一個空對象,把它稱為global,先把標準的內置對象都作為全局變量名稱裝載進去;然后開始運行。
JavaScript是個單線程模型,所以假定我們用棧的方式來實現計算;基本操作符和表達式的棧計算就不多說了,我們只說遇到函數怎么辦。
一般來說遇到函數應該約定在棧上處理參數和返回值的方式,但這個無關緊要,有關緊要的問題是我們需要把傳統的Function Frame的概念,即對一個函數在棧上分配局部變量的概念,換個思維,我們不用Function Frame,而是創建一個空對象來表式一個Function Frame,我們一行一行的讀入代碼,遇到局部變量聲明就在這個對象上裝上一個屬性,遇到修改局部變量的時候就給它賦值;
如果這樣做,我們就可以把Function Scope(一般說Function Scope指的是代碼層面的Lexical Scope,這里我們把Function Scope和Function Frame混用)作為原型鏈串起來,詞法域中外圍的Function Scope是原型,內部的Function Scope是對象;這樣Function Scope的引用可能出現在棧上,但它本身并非分配在棧上;Function Scope對象的創建是在調用函數時,它的銷毀我們可以暫時指望垃圾回收器,可回收的時間是該函數已經完成執行且沒有其他Function Scope引用該Scope;
如果你仔細觀察在Function Scope構成的鏈上查找變量名(Identifier)的時候,其邏輯和在原型鏈上查找屬性的方式一模一樣;用這樣的方式也可以準確找到閉包變量,唯一的區別是這里需要小小的修改一下原型鏈的約定,原型上的屬性可以直接修改,因為閉包變量是可以賦值的;
這就是前面我們說Function Scope也可以當作是對象處理的原因。
你可以想象出來這個解釋器可以寫得多小和多簡單,而且如果沒有hoisting,它可以在源文件還沒下載完就開始投入運行,而不是一開始就把整個語法樹都解析出來;
如果你問為什么早期的JavaScript的var沒有block scope支持,因為block scope按照這種思路來說,需要為block scope多帶帶創建對象。
所以在這個討論里,你能對JavaScript最初呱呱墜地時的一些小想法獲得一些感受;它從一開始只想用一個令人震驚的簡單的方法做幾件簡單的小事情,比如賺一個億,但這并不說明它無能,相反,在數學和編程的世界里,越是簡單的事情越有無窮無盡的能量。
寫到這里,我想我說完了自己對JavaScript的一切皆對象的認知,歡迎探討。
最后鳴謝少婦白潔愿意出現在本文題目中。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81674.html
摘要:匿名函數是我們喜歡的一個重要原因,也是,它們分別消除了很多代碼細節上需要命名變量名或函數名的需要。這個匿名函數內,有更多的操作,根據的結果針對目錄和文件做了不同處理,而且有遞歸。 能和微博上的 @響馬 (fibjs作者)掰扯這個問題是我的榮幸。 事情緣起于知乎上的一個熱貼,諸神都發表了意見: https://www.zhihu.com/questio... 這一篇不是要說明白什么是as...
摘要:的科學定義是或者,它的標志性原語是。能解決一類對語言的實現來說特別無力的狀態機模型流程即狀態。容易實現是需要和的一個重要原因。 前面寫了一篇,寫的很粗,這篇講講一些細節。實際上Fiber/Coroutine vs Async/Await之爭不是一個簡單的continuation如何實現的問題,而是兩個完全不同的problem和solution domain。 Event Model 我...
摘要:目的是為了解決在重用的時候,持久和方法重用的問題。換句話說你不用擔心把組件寫成模式不好重用,如果你需要傳統的方式使用,一下即可。 這篇文章所述的思想最終進化成了一個簡單的狀態管理模式,稱React StateUp Pattern,詳細介紹請參閱:https://segmentfault.com/a/11... 寫了一個非常簡單的實驗性Pattern,暫且稱為PurifiedCompon...
摘要:一般這種情況會在類的構造函數內創建一個屬性,引用或詞法域的,但后面會看到我們有更好的辦法,避免這種手工代碼。 換句話說,StateUp模式把面向對象的設計方法應用到了狀態對象的管理上,在遵循React的組件化機制和基于props實現組件通訊方式的前提之下做到了這一點。 ---- 少婦白潔 閱讀本文之前,請確定你讀過React的官方文檔中關于Lifting State Up的論述: ht...
摘要:本文用于闡述模式的算法和數學背景,以及解釋了它為什么是里最完美的狀態管理實現。歡迎大家討論和發表意見。 本文用于闡述StateUp模式的算法和數學背景,以及解釋了它為什么是React里最完美的狀態管理實現。 關于StateUp模式請參閱:https://segmentfault.com/a/11... P-State, V-State 如果要做組件的態封裝,從組件內部看,存在兩種不同的...
閱讀 2105·2021-11-18 10:02
閱讀 2859·2021-09-04 16:41
閱讀 1148·2019-08-30 15:55
閱讀 1414·2019-08-29 17:27
閱讀 1085·2019-08-29 17:12
閱讀 2538·2019-08-29 15:38
閱讀 2861·2019-08-29 13:02
閱讀 2836·2019-08-29 12:29