Node.js 启动方式:一道关于全局变量的题目引发的思考

456 查看

原题

题目是这样的。

上题由我们亲爱的小龙童鞋发现并在我们的 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 中看到了几句关键的语句:

还有这么一段关键的注释。