原文链接:https://xcoder.in/2015/11/26/a-js-problem-about-global/
原题
题目是这样的。
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 之中,全局对象的属性与全局变量是等价的。
window.a = 1; a // 1 a = 2; window.a // 2
上面代码中,全局对象的属性赋值与全局变量的赋值,是同一件事。(对于Node来说,这一条只对REPL环境适用,模块环境之中,全局变量必须显式声明成global对象的属性。)
有了阮老师的文章验证了这个猜想,我可以放心大胆继续看下去了。
repl.js
知道了上文的内容之后,感觉首要查看的就是 Node.js 源码中的 repl.js 了。
先是结合了一下自己以前用自定义 REPL 的情况,一般的步骤先是获取 REPL 的上下文,然后在上下文里面贴上各种自己需要的东西。
var r = relp.start(" ➜ ");
var c = r.context;
// 在 c 里面贴上各种上下文
c.foo = bar;
// ...
关于自定义 REPL 的一些使用方式可以参考下老雷写的《Node.js 定制 REPL 的妙用》。
有了之前写 REPL 的经验,大致明白了 REPL 里面有个上下文的东西,那么在 repl.js 里面我们也找到了类似的代码。
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
)中去。
我们可以试试这样的代码:
var vm = require('vm');
var localVar = 'initial value';
vm.runInThisContext('var localVar = "vm";');
console.log('localVar: ', localVar);
console.log('global.localVar: ', global.localVar);
其输出结果是:
localVar: initial value
global.localVar: vm
如文档中所说,vm
的一系列函数中跑脚本都无法对当前的局部变量进行访问。各函数能访问自己的 global
,而 runInThisContext
的 global
与当前上下文的 global
是一样的,所以能访问当前的全局变量。
所以出现上述结果也是理所当然的了。
所以在 vm
中跑我们一开始抛出的问题,答案自然就是 2
了。
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 之中。
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
中看到了几句关键的语句:
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);
还有这么一段关键的注释。
// 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
的判断启动不同的逻辑。
// ...
} 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
是什么东西。
Module.requireRepl = function() {
return Module._load('internal/repl', '.');
}
所以我们还是得转入 lib/internal/repl.js 来一探究竟。
上面在 node.js
里面我们看到它执行了这个 cliRepl
的 createInternalRepl
函数,它的实现大概是这样的:
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 顶端的模块引入,赫然看到一句话:
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
呢?
于是有了上面写的那段代码。
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 file.js
这种模式下执行的代码环境也与node 模块的加载机制也有一些关系,因为命令行下直接进入
node,不需要有模块的加载. 这也是这两种模式的很大区别,而楼主文章的剖析的很细。
在有模块加载机制的情况下,每个file
都是一个单独的环境,其中得任何变量在不使用global.xx = oo
的情况下是不会被加载到全局的
不然世界就乱了。而每个环境的require, module, exports
都是被注入这个环境的全局变量,而注入的这些变量既不是global
上也不是这个环境的this
。
下面是模块加载机制的部分代码
Module.prototype._compile = function(content, filename) {
var self = this;
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
};
最后就是
(function (exports, require, module, \_\_filename, \_\_dirname) {
// 模块源码
});
上面的这个self.exports
就是module中的,而他得初始值就是空对象{}
点击 以及这个
所以每个文件里面的this
就是{}
以及exports.name = 'strucoder'
也可以写成 this.name = 'strucoder'
而上文中的self 就是我们文件中写的module
@William17 这个倒是真心好理解 自豪地采用 CNodeJS ionic
@flyingcodes 不会的,都是学习的精神可嘉!佩服!
node什么时候变这么高级了?
不错的分析
来自酷炫的 CNodeMD
@flyingcodes 题目只是我做分析的一个引子,何必认真?
赞赞赞,学习了。