原題
題目是這樣的。
1 2 3 4 5 6 |
var a = 2; function foo(){ console.log(this.a); } foo(); |
上題由我們親愛的小龍童鞋發現並在我們的 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 之中,全域性物件的屬性與全域性變數是等價的。
1 2 3 4 5 |
window.a = 1; a // 1 a = 2; window.a // 2 |
上面程式碼中,全域性物件的屬性賦值與全域性變數的賦值,是同一件事。(對於Node來說,這一條只對REPL環境適用,模組環境之中,全域性變數必須顯式宣告成global物件的屬性。)
有了阮老師的文章驗證了這個猜想,我可以放心大膽繼續看下去了。
repl.js
知道了上文的內容之後,感覺首要檢視的就是 Node.js 原始碼中的 repl.js 了。
先是結合了一下自己以前用自定義 REPL 的情況,一般的步驟先是獲取 REPL 的上下文,然後在上下文裡面貼上各種自己需要的東西。
1 2 3 4 5 6 |
var r = relp.start(" ➜ "); var c = r.context; // 在 c 裡面貼上各種上下文 c.foo = bar; // ... |
關於自定義 REPL 的一些使用方式可以參考下老雷寫的《Node.js 定製 REPL 的妙用》。
有了之前寫 REPL 的經驗,大致明白了 REPL 裡面有個上下文的東西,那麼在 repl.js 裡面我們也找到了類似的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
REPLServer.prototype.createContext = function() { var context; if (this.useGlobal) { context = global; } else { context = vm.createContext(); for (var i in global) context[i] = global[i]; context.console = new Console(this.outputStream); context.global = context; context.global.global = context; } context.module = module; context.require = require; this.lines = []; this.lines.level = []; // make built-in modules available directly // (loaded lazily) exports._builtinLibs.forEach(function(name) { Object.defineProperty(context, name, { get: function() { var lib = require(name); context._ = context[name] = lib; return lib; }, // allow the creation of other globals with this name set: function(val) { delete context[name]; context[name] = val; }, configurable: true }); }); return context; }; |
看到了關鍵字 vm
。我們暫時先不管 vm
,光從上面的程式碼可以看出,context
要麼等於 global
,要麼就是把 global
上面的所有東西都粘過來。
然後順帶著把必須的兩個不在 global
裡的兩個東西 require
和 module
給弄過來。
下面的東西就不需要那麼關心了。
VM
接下去我們來講講 vm
。
VM 是 node 中的一個內建模組,可以在文件中看到說明和使用方法。
大致就是將程式碼執行在一個沙箱之內,並且事先賦予其一些 global
變數。
而真正起到上述 var
和 global
區別的就是這個 vm
了。
vm
之中在根作用域(也就是最外層作用域)中使用 var
應該是跟在瀏覽器中一樣,會把變數粘到 global
(瀏覽器中是 window
)中去。
我們可以試試這樣的程式碼:
1 2 3 4 5 6 |
var vm = require('vm'); var localVar = 'initial value'; vm.runInThisContext('var localVar = "vm";'); console.log('localVar: ', localVar); console.log('global.localVar: ', global.localVar); |
其輸出結果是:
1 2 |
localVar: initial value global.localVar: vm |
如文件中所說,vm
的一系列函式中跑指令碼都無法對當前的區域性變數進行訪問。各函式能訪問自己的 global
,而 runInThisContext
的 global
與當前上下文的 global
是一樣的,所以能訪問當前的全域性變數。
所以出現上述結果也是理所當然的了。
所以在 vm
中跑我們一開始丟擲的問題,答案自然就是 2
了。
1 2 3 4 5 6 7 |
var vm = require("vm"); var sandbox = { console: console }; vm.createContext(sandbox); vm.runInContext("var a = 2;function foo(){console.log(this.a);}foo();", sandbox); |
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 之中。
1 2 3 4 |
int main(int argc, char *argv[]) { setvbuf(stderr, NULL, _IOLBF, 1024); return node::Start(argc, argv); } |
就在主函式中執行了 node::Start
。而這個 node::Start
又存在 src/node.cc 裡面。
然後在 node::Start
裡面又呼叫 StartNodeInstance
,在這裡面是 LoadEnvironment
函式。
最後在 LoadEnvironment
中看到了幾句關鍵的語句:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js"); Local<Value> f_value = ExecuteString(env, MainSource(env), script_name); //... Local<Function> f = Local<Function>::Cast(f_value); //... Local<Object> global = env->context()->Global(); //... Local<Value> arg = env->process_object(); f->Call(global, 1, &arg); |
還有這麼一段關鍵的註釋。
1 2 3 4 5 6 7 |
// Now we call 'f' with the 'process' variable that we've built up with // all our bindings. Inside node.js we'll take care of assigning things to // their places. // We start the process this way in order to be more modular. Developers // who do not like how 'src/node.js' setups the module system but do like // Node's I/O bindings may want to replace 'f' with their own function. |
也就是說,啟動 node
的時候,在做了一些準備之後是開始載入執行 src 資料夾下面的node.js 檔案。
在 92 行附近有針對 $ node foo.js
和 $ node
的判斷啟動不同的邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// ... } else if (process.argv[1]) { // make process.argv[1] into a full path var path = NativeModule.require('path'); process.argv[1] = path.resolve(process.argv[1]); var Module = NativeModule.require('module'); // ... startup.preloadModules(); if (global.v8debug && process.execArgv.some(function(arg) { return arg.match(/^--debug-brk(=[0-9]*)?$/); })) { var debugTimeout = +process.env.NODE_DEBUG_TIMEOUT || 50; setTimeout(Module.runMain, debugTimeout); } else { // Main entry point into most programs: Module.runMain(); } } else { var Module = NativeModule.require('module'); if (process._forceRepl || NativeModule.require('tty').isatty(0)) { // REPL var cliRepl = Module.requireRepl(); cliRepl.createInternalRepl(process.env, function(err, repl) { // ... }); } else { // ... } } |
在上述節選程式碼的第一個 else if
中,就是對 $ node foo.js
這種情況進行處理了,再做完各種初始化之後,使用 Module.runMain();
來執行入口程式碼。
第二個 else if
裡面就是 $ node
這種情況了。
我們在終端中開啟 $ node
的時候,TTY 通常是關連著的,所以 require('tty').isatty(0)
為 true
,也就是說會進到條件分支並且執行裡面的 cliRepl
相關程式碼。
我們進入到 lib/module.js 看看這個 Module.requireRepl
是什麼東西。
1 2 3 |
Module.requireRepl = function() { return Module._load('internal/repl', '.'); } |
所以我們還是得轉入 lib/internal/repl.js 來一探究竟。
上面在 node.js
裡面我們看到它執行了這個 cliRepl
的 createInternalRepl
函式,它的實現大概是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function createRepl(env, opts, cb) { // ... opts = opts || { ignoreUndefined: false, terminal: process.stdout.isTTY, useGlobal: true }; // ... opts.replMode = { 'strict': REPL.REPL_MODE_STRICT, 'sloppy': REPL.REPL_MODE_SLOPPY, 'magic': REPL.REPL_MODE_MAGIC }[String(env.NODE_REPL_MODE).toLowerCase().trim()]; // ... const repl = REPL.start(opts); // ... } |
轉頭一看這個 lib/internal/repl.js 頂端的模組引入,赫然看到一句話:
1 |
const REPL = require('repl'); |
真相大白。
小結
最後再梳理一遍。
在於 Node.js 的 vm
裡面,頂級作用域下的 var
會把變數貼到 global
下面。而 REPL 使用了 vm
。然後 $ node
進入的一個模式就是一個特定引數下面啟動的一個 REPL
。
所以我們一開始提出的問題裡面在 $ node foo.js
模式下執行是 undefined
,因為不在全域性變數上,但是啟用 $ node
這種 REPL 模式的時候得到的結果是 2
。
番外
小龍:我用 node test.js 跑出來是
a: undefined
;那我應該怎麼修改“環境”,來讓他跑出:a: 2
呢?
於是有了上面寫的那段程式碼。
1 2 3 4 5 6 7 |
var vm = require("vm"); var sandbox = { console: console }; vm.createContext(sandbox); vm.runInContext("var a = 2;function foo(){console.log(this.a);}foo();", sandbox); |