从 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 }
目录
相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
1月前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
31 2
[JS]作用域的“生产者”——词法作用域
|
4月前
|
JavaScript 前端开发
浅谈js作用域
浅谈js作用域
34 0
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
2月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域是指程序中可访问的变量、对象和函数的集合。它分为函数作用域和局部作用域。函数作用域内的变量仅在函数内部可见,而全局作用域的变量在整个网页中均可访问。局部变量在函数执行完毕后会被销毁,而全局变量则在整个脚本生命周期中都存在。未使用 `var` 关键字声明的变量默认为全局变量。
|
2月前
|
JavaScript 前端开发
js作用域
js作用域
17 1
|
3月前
|
JavaScript 前端开发
js 变量作用域与解构赋值| 22
js 变量作用域与解构赋值| 22
|
3月前
|
JavaScript 前端开发
JavaScript基础知识-作用域(action scope)
关于JavaScript基础知识中作用域的介绍。
43 1
JavaScript基础知识-作用域(action scope)