从 global.console 看 Node.js 中的作用域

最近有小伙伴来问我,为什么这段代码不起作用?

var c = global.console;

global.console = {
  log: function(text) {
    c.log('Foo: ' + text);
  }
};

console.log('bar');  // expected 'Foo: bar', got 'bar'

Node.js 中的 global

Node.js 中存在一个全局对象 global文档),类似浏览器里的 window。挂载在上面的变量,可以被所有模块共享,并且站在作用域链的最顶端。

global.foo = 1;
console.log(foo);  // 1

bar = 2;
console.log(global.bar); // 2

只读的 global.console

Node.js 里,console这样挂载到 global 上的:

global.__defineGetter__('console', function() {
  return NativeModule.require('console');
});

这里用了 Object.prototype.__defineGetter__()文档),类似 ES5 里用 Object.defineProperty() 定义 getter。由于只定义了 getter,没有定义 setter,就会有只读的效果。

本地 console 与 Node.js 中的模块封装机制

但是,下面的代码如果保存在文件中运行,就能成功改变 console

var c = global.console;

var console = {
  log: function(text) {
    c.log('Foo: ' + text);
  }
}

console.log('bar');  // expected 'Foo: bar', got 'Foo: bar'

这是因为 src/node.js实现了一个 wrapper,读取的代码文件将会被包装后再编译运行。这个 wrapper 长这样:

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

在 Node.js 代码里所接触到的 exportsrequiremodule 等,也都是从这个 wrapper 里传进来的。在此处示意图如下:

 --------------------------------------------------------------------
| global.__defineGetter__('console', ....)                           |
|                                                                    |
|  ----------------------------------------------------------------  |
| | (function (exports, require, module, __filename, __dirname) {  | |
| |                                                                | |
| |   ---------------------------------------------------------    | |
| |  | // test.js                                              |   | |
| |  | var console = {                                         |   | |
| |  |   ...                                                   |   | |
| |  |                                                         |   | |
| |  |                                                         |   | |
| |  |  }                                                      |   | |
| |   ---------------------------------------------------------    | |
| |                                                                | |
| |                                                                | |
|  ----------------------------------------------------------------  |
 --------------------------------------------------------------------

这个 console 相当于 wrapper 里的一个本地变量,由于修改的并不是 global.console,因此就不会被 global.console 的只读性限制。并且在作用域查找的时候,会先于 global.console 被找到,所以上面的代码能够成功修改 console。注意这里使用了 var 来声明新的 console,所以能够作为本地变量。如果不用 var,直接声明,那么相当于修改 global.console,依然是徒劳的。

REPL 中的 console

但是,如果在命令行运行 node 打开 REPL,粘贴上面的代码,会发现 console 又不能被修改了,输出的还是 bar。这是为什么呢?

REPL 中没有自己的上下文,与 global 是一起的(源代码),这点与浏览器中的情形类似,即使用了 var 在全局声明对象后,还是会挂到 global 上。

 ----------------------------------------------------------------
| global.__defineGetter__('console', ....)                       |
|                                                                |
|   ---------------------------------------------------------    |
|  | // REPL                                                 |   |
|  | var console = {...}                                     |   |
|   ---------------------------------------------------------    |
|                                                                |
|                                                                |
 ----------------------------------------------------------------

Node.js 的 REPL 有一个选项 useGlobal,关掉它的话 console 将会直接用 = 放到全局上下文而不是设置 getter,这样它就不再只读了。开启一个自定义的 REPL 方法如下,将下面的代码存进一个文件:

var repl = require('repl');

repl.start({
  useGlobal: false
});

再用 Node 运行这个文件,就可以发现 global.console 可以被修改了,此时示意图如下:

 ----------------------------------------------------------------
| global.console = ...                                           |
|                                                                |
|   ---------------------------------------------------------    |
|  | // REPL                                                 |   |
|  | var console = {...}                                     |   |
|   ---------------------------------------------------------    |
|                                                                |
|                                                                |
 ----------------------------------------------------------------

useGlobaltrue 时,查看 global.console 的 property descriptor,如下:

> Object.getOwnPropertyDescriptor(global, 'console');
{ get: [Function],
  set: undefined,
  enumerable: true,
  configurable: true }

useGlobalfalse 时如下:

> Object.getOwnPropertyDescriptor(global, 'console');
Object {
  value:
   Console {
     log: [Function: bound ],
     info: [Function: bound ],
     warn: [Function: bound ],
     error: [Function: bound ],
     dir: [Function: bound ],
     time: [Function: bound ],
     timeEnd: [Function: bound ],
     trace: [Function: bound trace],
     assert: [Function: bound ] },
  writable: true,
  enumerable: true,
  configurable: true }