話說在前頭(面試捷徑說在後頭)
最近一直從原理去理解js基礎,發現自己收貨頗豐。以前一觸碰原理性解析的文章或者部落格時一直很抵, 但是最近強行去了解到本質就感覺語言設計還是那麼的清爽。一些規則和設計也是為了解決問題 高效的解決問題問題。 一門語言也是程式設計師的工具吧,使用好工具不僅僅只是要看懂說明書(api),應用語言api,一定要懂得這門語言的特點和秉性,這樣使用起來就會收放自如,才能發揮它的最大的潛能。
知己知彼,融會貫通,無功勝有功(吹牛逼了)
比如: JS最開始純解釋性語言,js虛擬機器的解析器將原始碼轉編譯成中間程式碼,之後直接使用直譯器執行中間程式碼,然後直接輸出結果。
js解析的原理都一樣,但是也有好幾種js虛擬機器,他們之間也存在一些差異,蘋果公司Safari的javaScriptCore虛擬機器,firefox有TraceMonkey虛擬機器,而Google則使用v8虛擬機器
那V8是怎麼執行js程式碼的呢? v8是混合編譯執行和解釋執行這兩種手段,混合使用編譯器和直譯器的技術成為JIT(Just In Time)技術
所以我認為js是解釋性語言,只是為了優化js執行速度,混合加入了編譯器及時編譯技術。《你不知道js》說js是門編譯語言,有點歧義,不能夠更好的說明現代js的核心原理。
MDN中則表達的很準確,感覺MDN才是學標準規範js的最佳地方:
javaScript 是一種具有 函式優先 的輕量級,解釋型或即時編譯型的程式語言。
這個表達總結多規範,多精美。看著都爽,乾貨滿滿
從V8解析javaScript認識該語言特性
1. 特點一:一等公民
js的函式可以向變數一樣,拿來賦值,能夠作為一個引數傳遞給另外一個函式,一個函式返回另外一個函式。一等公民設計,就有了閉包的出現,就有了函數語言程式設計等。
當函式內部引用了外部變數時,使用這個函式進行賦值、傳參或者作為返回值,你需要保證這些被引用的外部變數確定存在的,這就讓函式作為一等公民的麻煩,因為虛擬機器還需要處理函式引用的外部變數。
所以當初js創造的時候若要支援函式是一等公民時候,就必須有詞法作用域的出現,有了詞法作用域和函式一等公民的特性,閉包自然是他們的產物。並不是js為了去建立閉包。
閉包就是函式和它的外部環境的繫結形成的,js的開發者使用這個特性進行簡潔的程式碼編寫, 所以第三方程式碼都是大量的使用,可以讓自己的程式碼更簡潔。所以明白閉包,是閱讀第三方原始碼的基礎。
2. 特點二: 函式及物件
js是一門基於物件的語言,大部分內容是由物件組成,這些物件還可以動態修改其內容,這就造就了js是一門超級靈活的語言,加大了理解和使用這門語言的難度 在js中函式也是一個物件,函式也有屬性和值, 我們宣告的函式原型物件是Function,是Function建構函式建立出來的。 還有一個原型繼承的主要的公開屬性 prototype。 還有兩個隱藏屬性 name code. v8是靠這兩個隱藏屬性實現函式可呼叫的特性, - name:就是函式的名字,如果這個函式沒有被設定,name這個函式就是我們常見的匿名函式(anonymous) - code:表示函式程式碼,以字串儲存在記憶體中,當執行一個函式呼叫語句(函式名加括號)時,V8便會從函式物件中取出code屬性值,也就是函式程式碼,然後再解釋執行這段函式程式碼
如果在物件中的屬性值是函式,我們就叫這個屬性稱為方法,所以我們說物件具備屬性和方法。
3. 特點三:物件屬性分為 排序屬性 和 常規屬性
物件有兩個隱藏屬性,element 和 properties 分別指向elements 物件和 properties屬性
排序屬性(elements): 物件中屬性名為數字的屬性,根據索引值升序排列 常規屬性(properties): 物件中屬性名為字串就被稱為常規屬性,根據建立時間升序排列
V8,為了有效的提升儲存和訪問這兩種屬性的效能,使用了兩個線性的資料結構, v8會先從elements屬性中按照順序讀所有的元素,然後再在properties讀取所有元素,這樣就完成了一次索引查詢物件屬性操作。
只是這樣設計的話,在索引properties屬性的時候,會先使用隱藏屬性properties找到properties物件,再在properties物件中找到這個屬性,多了一個properties物件查詢的操作。 於是v8除了一個優化策略,將部分常規屬性直接儲存到物件本身,稱為物件內屬性。 物件內屬性數量是固定的,預設是10個,如果這個物件常規屬性多出了10個,那麼將剩餘的常規屬性放在常規屬性物件當中。
將常規屬性放到物件內屬性使用線性結構儲存,這些屬性也稱快屬性,查屬性非常快,線性結構刪除和新增屬性特別耗時間和記憶體開銷。 當常規屬性特別多的時候,將部分屬性放到properties裡面儲存,這裡的儲存策略是慢屬性策略,非線性資料結構儲存(詞典)。
4.特點四: 函式表示式 和 立即呼叫函式表示式(IIFE)
函式表示式 和 函式宣告
前面說了函式是一等公民,可以作為賦值, 就像var fun = function() {}.
這裡有一個函式宣告,js也是允許的但是函式宣告會造成一個變數提升問題
func()
function func() {console.lohg('我輸出了')}
複製程式碼
上面程式碼能夠正常執行,但是
func1()
func1 = function() {console.log('我輸出了')}
複製程式碼
這程式碼就會報錯
VM130:1 Uncaught TypeError: func1 is not a function at :1:1
複製程式碼
為什麼呢,這個就要明白,表示式 和 語句,作用域,編譯階段,執行階段這些概念。
記住:函式表示式本質是表示式,函式宣告本質是語句。 js在編譯(解析)階段只會解析語句,不會有任何結果輸出。但是會將相關變數放到作用域裡面,解析的語句時不會執行。在執行階段只要作用域有這個變數就可以使用,所以func()函式正常執行了 也就是知道為什麼使用函式表示式時候為什麼報這個錯,因為在編譯階段的時候,foo這個變數還是undefined是一個原生物件而不是函式,所以這行程式碼執行的時候會這個報錯
立即呼叫函式表示式
這個是藉助 js有一個括號操作符() 裡面可以放一個表示式 (a=3)
如果把a=3替換成一個函式,因為()裡面只能放表示式,所以會把函式認為是一個函式表示式,表示式在執行階段都會返回一個函式物件(在作用域中找到)函式物件加上一個()括號就是函式呼叫,那這個括號裡面的函式就立即被執行了,
這樣的表示式我們稱為 立即呼叫函式表示式
因為立即呼叫函式表示式也是一個表示式,所以V8在編譯階段,並不會為該表示式建立函式物件,這樣表示式裡面的屬性和方法就不會被其他程式碼訪問到,就不會汙染js的全域性環境, 在ES6之前,js沒有私有作用域的概念,沒有模組化,會造成你模組的變數可能覆蓋別人的變數,所以使用立即呼叫函式表示式,封裝起來,避免了相互之間的變數汙染。
5. 特點: 原型繼承
語言中繼承簡單的說,就是一個物件可以訪問另外一個物件的屬性和方法,js是使用原型繼承的策略
js物件有一個隱藏屬性 proto 稱之為物件的原型,這個屬性指向的記憶體當中另外一個物件,這個物件就是原型物件(prototype) 那個物件可以訪問其原型物件的方法和屬性
看上圖,我們只要將物件的隱藏屬性__proto__不指向內部給的原型物件,指向你自己宣告的實力物件即可完成繼承,很簡單很明瞭,沒有基於類那些繁文縟節。 原型繼承核心就是使用隱藏屬性__proto__完成繼承,我們實際開發當中不能直接使用這個隱藏屬性完成繼承: - 這個屬性是隱藏屬性,不是標準定義的 - 使用這個屬性會造成嚴重的效能問題,要是你把這個屬性指向了你自己的宣告的物件,V8內部生成做了優化的prototype物件就會以為沒有被引用會被丟棄回收。市面上存在一些繼承方案,媽的取了一大堆名詞煩的不行,但是不知道本質你會被他們搞暈掉。 1.原型鏈繼承 2.借用建構函式繼承,3.組合繼承(組合原型鏈繼承和借用建構函式繼承)(常用),4.寄生繼承 5.寄生組合繼承,6.原型式繼承
建構函式:屬性通過引數進行傳遞,函式體內,通過this設定屬性值。
function DogFactory(type,color){
this.type = type
this.color = color
}
複製程式碼
然後結合 關鍵字 new就可以建立物件了 .
var singleDog = new DogFactory()
複製程式碼
有個小八卦,為什麼使用new 關鍵字配合一個函式,V8(js虛擬機器)就會返回一個物件了?大家可以去自行搜尋
new本質,V8做了這些事:
var dog = {}
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog, 'Dog', 'Black')
複製程式碼
如圖:
就是建立了一個空物件,再把空物件的原型指向DogFactory函式的公開屬性prototype,再使用空物件dog去呼叫空物件,這樣dog就是建構函式的this,執行建構函式就將屬性填充操作 ,最終建立了dog物件。 其實這裡也完成了一個繼承操作,上面我們講了函式即物件,所以建構函式也是物件,我們生成的物件dog繼承了建構函式的屬性,算是原型繼承吧。
如果根據繼承的概念我們可以實現一些繼承,
原型鏈繼承:將子的建構函式的prototype指向父例項物件(只要是這個函式作為建構函式去建立物件的時候,函式的prototype就指向這個建立物件的原型)這樣也完成了繼承
關鍵程式碼:Worker.prototype = new Person();
藉助建構函式繼承:就是在子建構函式去呼叫父建構函式 使用 call apply都行,改變this屬性獲取父建構函式的屬性,完成繼承
關鍵程式碼: Person.call(this, name, age);
組合繼承:上面程式碼都會造成效能問題和繼承追溯問題(V8引擎分配的原型物件銷燬 和 constructor指向混亂)
原型物件上還有一個公用屬性 constructor這個屬性指向建構函式
建構函式的prototype指向原型物件
使用call可以替換掉建構函式的this
關鍵程式碼:
js Person.call(this, name, age); Worker.prototype = new Person(); Worker.prototype.constructor = Worker;
原型式繼承:
js function Empty(){}; Empty.prototype = o;
這個關鍵還是建構函式的prototype指向發生變化
js var woker={ name: 'jia', age: 18, job: '打雜的' } var mine = Object.create(woker);
原理和上面相同,ECMAScript 5 通過新增 Object.create()方法規範化了原型式繼承。
寄生繼承:就是原型式繼承套個殼子
寄生組合式繼承:集寄生式繼承和組合繼承的優點與一身,是實現基於型別繼承的最有效方式
萬變不離奇宗,只要明白函式的公開屬性prototype,物件的的隱藏屬性__proto__ 建構函式 繼承這些概念,繼承問題迎刃而解。 面試你還要準備這麼多繼承的方式麼,還要記這些程式碼麼,看到本質,你可以和麵試官慢慢講
上面的繼承有很多效能問題(這些問題你們可以去檢視),寄生組合式繼承算是ES6前繼承最優解. 在ES6出現了class關鍵字,extends 雖然是語法糖,但是也做到了繼承方式統一,繼承最優解了。也許有些相容問題,但是有babel呀,怕個毛啊!!!
特性6: 作用域鏈
jsS是基於詞法作用域的,詞法作用域就是指: 查詢作用域的順序是按照函式定義時的位置來決定的。
查詢變數,其查詢順序都是按照當前函式作用域 --> 全域性作用域這個路徑來的
函式作用域: 每個函式在執行時都需要查詢自己的作用域,當函式裡面使用到某個變數或者呼叫某個函式時,便會優先在該函式作用域中查詢相關內容.函式執行完之後,作用域就隨之銷燬
全域性作用域: 全域性作用域在V8啟動的過程中就被建立了,且一直儲存在記憶體中不會被銷燬,直到V8退出
如果在當前函式作用域中沒有查詢到變數,那麼V8就會去全域性作用域中去查詢,這個查詢的線路就叫作用域鏈
首先當V8啟動時,會建立全域性作用域,全域性作用域中包括this, window等變數,還有一些全域性Web API介面。
特性7 型別轉換 1 + '2' ? 3 : '12'
在標題的簡單表示式中,涉及到了兩種不同型別的資料相加。要想理清以上兩個問題,要知道型別的概念,js的操作型別的策略
在高階語言中,都會給操作的資料賦予指定的型別,型別可以確認一個值或者一組值的具有特定的意義和目的。
每種計算機語言都定義了自己的型別,還定義瞭如何操作這些型別,另外還定義了這些型別應該如何相互作用,我們就把這個稱為型別系統
V8是嚴格根據ECMAscript規範來執行操作,有興趣大家可以去了解一下,通俗的講,V8有一個ToPrimitve方法,其作用是將a和b轉換為原生資料型別 轉換流程是:
- 如果valueOf沒有返回原始型別,那麼就使用toString方法返回值;
- 如果兩個方法都不返回基本型別值,便會觸發一個TypeError的錯誤;
最後你還是不理解(一定要花時間理解),那就可以很確信一點,V8會通過ToPrimitve方法將物件型別轉換為原生型別(拆箱?),最後就是兩個原生型別相加,如果一個值得型別時字串時,則另一個值也需要轉換為字串,然後將另一個值轉換成字串 然後做字串的連線運算,其他情況都會轉化數字型別值,做數字相加。面試去吧,哈哈哈哈。
面試捷徑
面試哪什麼捷徑,好事多磨。一定要懂得語言的秉性,從原理去了解語言的特性和基礎策略。 這樣才能百戰百勝。加油,我是水貨,騙流量的,上面都是筆記(他不是筆記,沒看完吧,一下就翻過來了)