2015年5月7日 星期四

JavaScript 的各種迷霧

Standard
 在 Facebook 看到很多人都在整理自己的程式筆記,我也整理關於 JavaScript 語言常被誤解部份的筆記,然後分享出來供各位同好參考。

講實話,從 1995 年開始的 JavaScript,直到前幾年,都讓我恨的牙癢癢,甚至覺得他是個垃圾,因為當時的他太沒有規範,效能極差。直到近年來才逐步改善,我也才慢慢擺脫對他的排斥及恨意。

但就算已經有標準規範,JavaScript 語言對很多人來說依然很頭痛,除了其 Asynchronous 非同步的機制外,更要面對其極為動態(Dynamic)的特性,而從 OOP 的角度來看,其 classless 的設計更讓人不知所措(ES6 之後開始支援 Class),與傳統的 OOP 相去甚遠。所以,JavaScript 處處與其他語言完全不一樣的概念及思維,讓很多人在裡面處處誤解。

所以若是想學習 JavaScript 深入的概念,最好的方式就是放下其他語言的既有概念,直接了解 JavaScript 的核心機制,雖然會讓人手足無措,但卻是最好的手段。否則,很容易練就或死背很多怪招,寫出難以除錯或過於複雜的程式碼,當然,更嚴重的是誤導他人。

要深入或精通 JavaScript,務必了解並思考其執行程式時發生什麼事,每一行程式碼當下被執行時的狀態,遠遠比程式碼排列來得重要。只要掌握每一行程式碼的『執 行當下』,所有 JavaScript 的疑難雜症,都能迎刃而解。關於這一點,有興趣閱讀硬梆梆定義文件的,可以參考 ECMA-626 的『10.3 Execution Contexts』一節。(參考網址:http://www.ecma-international.org/ecma-262/5.1/#sec-10.3

關於 this 關鍵字的機制

很多人會搞糊塗 this 關鍵字,就因為他是一個會被 Execution Contexts 大量影響的東西。JavaScript 在 Function 或 Context 被執行時,才會依當下情況去決定 this 為何。這代表,使用 this 時在哪一個 function 或地方不重要,而是當進入這個使用 this 的 context 時(如:Function 被執行時)才會決定 this 是什麼,this 是什麼只跟你如何去呼叫 this 所在 Function,及 Context 內狀態的改變有關。關於 Function Call 怎麼決定 this 值,可參考:http://www.ecma-international.org/ecma-262/5.1/#sec-11.2.3

當然,純講理論很難懂,可以大概歸納出 this 的使用方式與情景,有開發者已經整理了很清楚的文件,以及常誤觸的陷阱,可參考:https://software.intel.com/…/blo…/2013/10/09/javascript-this

若實在懶得看懂硬梆梆的文件,從實作面來看,就是記住簡單兩種規則而已。

一般來說,決定 Function 內的 this 為何,說明如下:
  1. 只要直接呼叫 Function ,就會代 global 進去當 this,這也是為什麼 Browser 裡面會是 window,因為在 Browser 裡的 global 就是 window。
  2. 如果一個 Function 處於一個 Object 上,像是 a.b() 這樣執行後,b 函數的內部會視 a 為 this。因為當 JavaScript 在透過 a 物件存取底下的 Function 並呼叫他時,會自動把依附的 a 物件(Caller)帶入當 this。

總歸來說,所有的 this 都是在執行期、呼叫當下所決定的,除非你使用 bind() 這類的做法事先去強制寫死他的 caller。這就能解釋,為什麼 this 總是不能往內部一層層自動傳進去,因為所有的 Function Call 都遵循著同樣的規則。

註:在嚴格模式下 Function 裡的 this 會是 undefined,不是他預設是 undefined,而是嚴格模式下呼叫一個 Normal function 時,他不會自動代入 global,所以才是 undefined。在嚴格模式下不能取得 base value(即 global),這點可參閱:http://www.ecma-international.org/ecma-262/5.1/#sec-8.7

Function 的存在

因為所有的型別都是 Object 的衍生物,甚至是 Function 也不例外,所以 JavaScript 裡的 Function 物件可以隨機被動態生成和定義,與其他大多數語言完全不一樣。其他語言多半將 Function 看做是一個寫死程式碼的區塊,放在一個固定的記憶體位置上,然後跳過去執行。而這樣可以動態定義 Function 的特性,導致 JavaScript 天生就支援 Lambda 、匿名函數等特色,因為可以依照任何情況,重新排列組合,甚至生成 Function 的執行流程。

關於記憶體及物件管理機制

JavaScript 的設計是以可動態擴充性為考量,所以也衍生出一套記憶體管理機制,以 Object 為基本記憶體管理單位。所有存取 Object,都透過一套 『Identifier -> Value』 的機制,而在執行上 Value 其實就是 Object。

與其他語言不太一樣的是,JavaScript 在利用 var 宣告 variable 時,實際上是建立一個 Identifier,並不是實體配置一塊物件記憶體。所以在 Identifier 建立的剛開始,並未對應到實際有意義的參考物件。直到我們額外去建立物件後,才讓 Identifier 去對應。可以說,這就是 key-value 的形式,而 Identifier 就是個 key。這部份可以參閱:http://www.ecma-international.org/ecma-262/5.1/#sec-12.2

由此可知, Identifier 本身不是 Object 的實體,因為他只是個 key 而已,就像 C/C++ 語言中的指標。

有趣的是,這樣 Reference 的機制,與 JavaScript object 中的 key-value 機制是一樣的,這也是為什麼 Object 可以隨意掛勾到任何地方。因為 JavaScript 程式中,我們一定是透過 Identifier/key 去取得並使用 Object,而且可能有很多不同的 Identifier/key 都指向同一個 Object。

記憶體回收

簡單來說,JavaScript 記憶體回收,就是當 Object 沒有被任何 Identifier 指向時,就被視為垃圾而回收釋放記憶體。

new 運算子如何建立一個物件

在定義上,用 new 建立物件要經歷過一些程序,甚至要檢查 Type 等工作,這部份的細節可參考原始定義:http://www.ecma-international.org/ecma-262/5.1/#sec-11.2.2

不過,對於大多數開發者來說,不需了解太多細節,較簡化的流程可參考 Mozilla 提供的文件:https://developer.mozilla.org/…/Jav…/Reference/Operators/new
簡而言之,new operator 會去做到幾件事:

  1. 用 Object.create() 等方法去參考 Function.prototype,建立一個物件
  2. 跑 Constructor(也就是開發者定義的 Function)
  3. 如果 Constructor 內沒有回傳一個物件,就以步驟 1 建立的物件回傳出來。
這一切工作是在『內部(internal)』完成,完成後回傳物件出來。

如果你很清楚這些行為,可以把這些工作往外移,自己實作,並完全不用到『new 關鍵字』,但是通常開發者不會這麼做惡整自己。用 new 以後,等他回傳配置完成的物件,才是一般性的做法。