Hero Image
[JS] 提升(Hoisting)與暫時性死區(Temporal Dead Zone)

網路上時常見到充滿 function 與 var 的 JavaScript 求值題目, 筆者在釐清 Hoisting 和 TDZ 的觀念前時常覺得答案出乎意料, 雖然現在撰寫程式碼都已經避免使用 var,但是維護 legacy code 還是會用到,因此在這裡做個筆記。 Hoisting 對學過伺服器端語言(C#、C/C++…)的人來說,預期試圖對未宣告的變數取值會出現 ReferenceError是很正常的事, 在 JavaScript 中也是如此: console.log(x) // ReferenceError: x is not defined 但 JavaScript 把 var 宣告變數放在後面,x 前面對 x 取值就變成 undefine, 在 JavaScript 當中對變數取值獲得 undefine 代表變數處於宣告後已分配記憶體空間(初始化、initiation)但尚未賦值的狀態, 但是明明 JavaScript 在宣告前就取值,怎麼能夠認得 x、而且知道 x 被初始化為 undefined 呢? console.log(x) // undefined var x 原因在於 javascript 會先程式中的蒐集 var(let/const/function) 宣告並釐清對應的作用域, 最後再執行程式碼,這個行為就如同宣告被提升(Hoisting)到前面行數的程式碼中一樣。 JavaScript 跟傳統 OOP 語言一樣,在變數提升後、宣告初始化、賦值等等時機取用變數會獲得不同輸出結果, 這篇文章(link)中提到, 稱 JavaScript 為直譯式(解釋型)語言實際上是通俗的說法,語言本身沒有規定實現的方式(編/直譯), 舉例來說 Chrome 瀏覽器所使用的 Google V8 JavaScript engine, 所帶的解釋器 (interpreter) 會將程式編譯 (compile) 成字節碼 (bytecode), 最後再由編譯器 (compiler) 即時編譯 (JIT;just-in-time compilation) 編譯成機器碼執行, 而 JavaScript 被編譯時宣告會被蒐集到最頂端進行定義並區分作用域(scope),這個行為就是 Hoisting ,記住重點在於JS編譯後會先定義變數及區分作用域,編譯後的程式看起來就像這樣:

Hero Image
[JS] JavaScript 當中的原型繼承鏈模型

基於原型 (Prototype-Based) 的 JavaScript 一般物件導向式(OOP; Object Oriented Programming) 程式語言 (如:java、c++、c#) 當中的物件是由類別模板 (class) 產生實體物件 (instance),實體物件的屬性各自獨立。類別模板上可設置共用的靜態資源包含靜態方法 (static method)、靜態屬性 (static field),而這些靜態資源可以在沒有建立實體的情況下透過類別名稱直接取用。 JavaScript 中的物件通常隸屬於另一個物件,這種隸屬關係類似物件導向語言的繼承,而在這種關係中的上層物件稱為原型 (Prototype)。原型本身又有自己所屬的原型,這種物件層層繼承的關係稱為原型鏈 (Prototype Chain),幾乎所有物件的最上層原型是一個構造函數叫做 Object 的物件。 因此一般物件導向式語言稱為基於類別 (Class-Based) 的語言;而 Javascript 是基於原型 (Prototype-Based) 的語言。 建立物件原型 JavaScript 本身沒有類別模板的概念,是以構造函數 (constructor) 建立物件,物件可以將 constructor 屬性指向構造函數,但並非所有物件都有構造函數,具有構造函數的物件可直接以構造函數產生原型鏈下一層物件;不具有構造函數的物件只能在其他物件建立完成後,以其他方式設置為其他物件的原型。 建立原型的方法就是直接宣告一個函數,JavaScript 會自動把該函數作為構造函數,並自動建立一個隸屬於 Object.prototype 之下的匿名物件,並把宣告的函數指定給該匿名物件的 constructor 屬性。 // 宣告一個函數 Foo function Foo (){} // Foo.prototype 在 Foo 被宣告時自動建立 Foo.prototype // {constructor: ƒ} // Foo.prototype 的 constructor 屬性自動指向 Foo Foo.prototype.constructor === Foo // true 建立物件 透過構造函數 // 建立一個物件 let bar = new Foo{} // {} 直接對變數賦值 JavaScript 對變數賦值底層行為:以 Object 構造函數建立物件,然後對物件並賦值(故賦值發生在物件建立之後)