Node.js 啟動方式:一道關於全域性變數的題目引發的思考

發表於2015-11-27

原題

題目是這樣的。

上題由我們親愛的小龍童鞋發現並在我們的 901 群裡提問的。

經過

然後有下面的小對話。

小龍:你們猜這個輸出什麼?

弍紓:2

力叔:2 啊

死月·絲卡蕾特:2

力叔:有什麼問題麼?

小龍:輸出 undefind。

死月·絲卡蕾特:你確定?

小龍:是不是我電腦壞了

力叔:你確定?

弍紓:你確定?

小龍:為什麼我 node 檔名跑出來的是 undefined?

鄭昱:-.- 一樣阿。undefined

以上就是剛見到這個題目的時候群裡的一個小討論。

分析

後來我就覺得奇怪,既然小龍驗證過了,說明他也不是隨地大小便,無的放矢什麼的。

於是我也驗證了一下,不過由於偷懶,沒有跟他們一樣寫在檔案裡面,而是直接 node 開了個 REPL 來輸入上述程式碼。

結果是 2!

結果是 2!

結果是 2!

於是這就出現了一個很奇怪的問題。

尼瑪為毛我是 2 他們倆是 undefined 啊!

不過馬上我就反應過來了——我們幾個的環境不同,他們是 $ node foo.js 而我是直接 node 開了個 REPL,所以有一定的區別。

而力叔本身就是前端大神,我估計是以 Chrome 的除錯工具下為基礎出的答案。

REPL vs 檔案執行

其實上述的問題,需要解釋的問題大概就是 a 到底掛在哪了。

因為細細一想,在 function 當中,this 指向的目標是 global 或者 window

還無法理解上面這句話的童鞋需要先補一下基礎。

那麼最終需要解釋的就是 a 到底有沒有掛在全域性變數上面。

這麼一想就有點細思恐極的味道了——如果在 node 線上執行環境裡面的原始碼檔案裡面隨便 var 一個變數就掛到了全域性變數裡面那是有多恐怖!

於是就有些釋然了。

但究竟是上面原因導致 REPL 和檔案執行方式不一樣的呢?

全域性物件的屬性

首先是弍紓找出了阮老師 ES6 系列文章中的全域性物件屬性一節。

全域性物件是最頂層的物件,在瀏覽器環境指的是 window 象,在 Node.js 指的是 global 物件。ES5 之中,全域性物件的屬性與全域性變數是等價的。

上面程式碼中,全域性物件的屬性賦值與全域性變數的賦值,是同一件事。(對於Node來說,這一條只對REPL環境適用,模組環境之中,全域性變數必須顯式宣告成global物件的屬性。)

有了阮老師的文章驗證了這個猜想,我可以放心大膽繼續看下去了。

repl.js

知道了上文的內容之後,感覺首要檢視的就是 Node.js 原始碼中的 repl.js 了。

先是結合了一下自己以前用自定義 REPL 的情況,一般的步驟先是獲取 REPL 的上下文,然後在上下文裡面貼上各種自己需要的東西。

關於自定義 REPL 的一些使用方式可以參考下老雷寫的《Node.js 定製 REPL 的妙用》。

有了之前寫 REPL 的經驗,大致明白了 REPL 裡面有個上下文的東西,那麼在 repl.js 裡面我們也找到了類似的程式碼。

看到了關鍵字 vm。我們暫時先不管 vm,光從上面的程式碼可以看出,context 要麼等於 global,要麼就是把 global 上面的所有東西都粘過來。

然後順帶著把必須的兩個不在 global 裡的兩個東西 require 和 module 給弄過來。

下面的東西就不需要那麼關心了。

VM

接下去我們來講講 vm

VM 是 node 中的一個內建模組,可以在文件中看到說明和使用方法。

大致就是將程式碼執行在一個沙箱之內,並且事先賦予其一些 global 變數。

而真正起到上述 var 和 global 區別的就是這個 vm 了。

vm 之中在根作用域(也就是最外層作用域)中使用 var 應該是跟在瀏覽器中一樣,會把變數粘到 global(瀏覽器中是 window)中去。

我們可以試試這樣的程式碼:

其輸出結果是:

如文件中所說,vm 的一系列函式中跑指令碼都無法對當前的區域性變數進行訪問。各函式能訪問自己的 global,而 runInThisContext 的 global 與當前上下文的 global 是一樣的,所以能訪問當前的全域性變數。

所以出現上述結果也是理所當然的了。

所以在 vm 中跑我們一開始丟擲的問題,答案自然就是 2 了。

Node REPL 啟動的沙箱

最後我們再只需要驗證一件事就能真相大白了。

平時我們自定義一個 repl.js 然後執行 $ node repl.js 的話是會啟動一個 REPL,而這個 REPL 會去調 vm,所以會出現 2 的答案;或者我們自己在程式碼裡面寫一個 vm 然後跑之前的程式碼,也是理所當然出現 2

那麼我們就輸入 $ node 來進入的 REPL 跟我們之前講的 REPL 是不是同一個東西呢?

如果是的話,一切就釋然了。

首先我們進入到 Node 的入口檔案——C++ 的 int main()

它在 Node.js 原始碼 src/node_main.cc 之中。

就在主函式中執行了 node::Start。而這個 node::Start 又存在 src/node.cc 裡面。

然後在 node::Start 裡面又呼叫 StartNodeInstance,在這裡面是 LoadEnvironment 函式。

最後在 LoadEnvironment 中看到了幾句關鍵的語句:

還有這麼一段關鍵的註釋。

也就是說,啟動 node 的時候,在做了一些準備之後是開始載入執行 src 資料夾下面的node.js 檔案。

在 92 行附近有針對 $ node foo.js 和 $ node 的判斷啟動不同的邏輯。

在上述節選程式碼的第一個 else if 中,就是對 $ node foo.js 這種情況進行處理了,再做完各種初始化之後,使用 Module.runMain(); 來執行入口程式碼。

第二個 else if 裡面就是 $ node 這種情況了。

我們在終端中開啟 $ node 的時候,TTY 通常是關連著的,所以 require('tty').isatty(0)為 true,也就是說會進到條件分支並且執行裡面的 cliRepl 相關程式碼。

我們進入到 lib/module.js 看看這個 Module.requireRepl 是什麼東西。

所以我們還是得轉入 lib/internal/repl.js 來一探究竟。

上面在 node.js 裡面我們看到它執行了這個 cliRepl 的 createInternalRepl 函式,它的實現大概是這樣的:

轉頭一看這個 lib/internal/repl.js 頂端的模組引入,赫然看到一句話:

真相大白。

小結

最後再梳理一遍。

在於 Node.js 的 vm 裡面,頂級作用域下的 var 會把變數貼到 global 下面。而 REPL 使用了 vm。然後 $ node 進入的一個模式就是一個特定引數下面啟動的一個 REPL

所以我們一開始提出的問題裡面在 $ node foo.js 模式下執行是 undefined,因為不在全域性變數上,但是啟用 $ node 這種 REPL 模式的時候得到的結果是 2

番外

小龍:我用 node test.js 跑出來是 a: undefined;那我應該怎麼修改“環境”,來讓他跑出:a: 2 呢?

於是有了上面寫的那段程式碼。

相關文章