从 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 }
目录
相关文章
|
2月前
|
JavaScript 前端开发
浅谈js作用域
浅谈js作用域
28 0
|
4月前
|
JavaScript 前端开发 开发者
JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。
【6月更文挑战第27天】JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。变量默认值为`undefined`,函数则整体提升。`let`和`const`不在提升范围内,存在暂时性死区。现代实践推荐明确声明位置以减少误解。
37 2
|
9天前
|
JavaScript 前端开发
js 变量作用域与解构赋值| 22
js 变量作用域与解构赋值| 22
|
9天前
|
缓存 JavaScript 前端开发
了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化
该文章详细讲解了JavaScript中的作用域、闭包概念及其应用场景,并简要分析了函数柯里化的使用。
了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化
|
1月前
|
JavaScript 前端开发
JavaScript基础知识-作用域(action scope)
关于JavaScript基础知识中作用域的介绍。
26 1
JavaScript基础知识-作用域(action scope)
|
1月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域
23 9
|
2月前
|
JavaScript 前端开发
使用 let 将有助于避免 JavaScript 中各种 var 引起的作用域问题。
这段内容介绍了JavaScript编程时的一系列最佳实践,包括使用`===`而非`==`进行比较、以`let`和`const`取代`var`定义变量、始终使用分号、采用合适的命名规范、利用模板字符串拼接、偏好ES6箭头函数、在控制结构中使用大括号、减少代码嵌套、应用默认参数、正确使用`switch`语句中的`break`与`default`分支、避免通配符导入以及简化布尔判断和避免不必要的三元运算符。遵循这些规则有助于提升代码的清晰度和可维护性。
16 2
|
2月前
|
JavaScript 前端开发
JavaScript基础&实战(4)js中的对象、函数、全局作用域和局部作用域
这篇文章介绍了JavaScript中对象的基本概念和操作,包括对象属性和方法的使用、对象字面量的创建、函数的定义和作用域的概念,以及全局作用域和局部作用域的区别和特性。
JavaScript基础&实战(4)js中的对象、函数、全局作用域和局部作用域
|
2月前
|
自然语言处理 资源调度 JavaScript
JS 逆向基础篇:JS作用域和浏览器对象属性补环境
JS 逆向基础篇:JS作用域和浏览器对象属性补环境
75 1
|
2月前
|
自然语言处理 JavaScript 前端开发
探析JS作用域
【8月更文挑战第2天】探析JS作用域
33 11