V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
XadillaX
V2EX  ›  Node.js

一道关于 Node.js 全局变量的题目

  •  6
     
  •   XadillaX · 2015-11-26 22:48:09 +08:00 · 4440 次点击
    这是一个创建于 3067 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接: 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 里的两个东西 requiremodule 给弄过来。

      下面的东西就不需要那么关心了。

    VM

      接下去我们来讲讲 vm

       VM 是 node 中的一个内置模块,可以在文档中看到说明和使用方法。

      大致就是将代码运行在一个沙箱之内,并且事先赋予其一些 global 变量。

      而真正起到上述 varglobal 区别的就是这个 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,而 runInThisContextglobal 与当前上下文的 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 里面我们看到它执行了这个 cliReplcreateInternalRepl 函数,它的实现大概是这样的:

    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);
    
    16 条回复    2015-11-28 17:04:22 +08:00
    Cee
        1
    Cee  
       2015-11-26 23:36:14 +08:00
    Pretty cool.
    不过那么好的文章现在也没什么人看 :(
    mywaiting
        2
    mywaiting  
       2015-11-26 23:41:25 +08:00
    整这个不同环境会导致这种蛋疼的输出,其实意义不大啊。神烦这种涉及到上下文的题目。不过能深入到底层去折腾,足够点赞了~
    XadillaX
        3
    XadillaX  
    OP
       2015-11-26 23:46:15 +08:00 via Android   ❤️ 1
    @mywaiting 其实主要还是浅入浅出 vm 和 repl 以及 node 启动做了什么之类的。问题只是个引子罢了,只不过我想不出更好的题目 _(:з」∠)_
    baozijun
        4
    baozijun  
       2015-11-27 00:57:50 +08:00
    很棒哦,收藏,谢谢楼主的分享
    phoenixlzx
        5
    phoenixlzx  
       2015-11-27 01:04:10 +08:00 via Android
    好赞的分析,收藏!
    FrankFang128
        6
    FrankFang128  
       2015-11-27 01:15:42 +08:00
    很深入。
    不过,不要用全局变量就好啦
    hqs123
        7
    hqs123  
       2015-11-27 08:17:20 +08:00
    最近我也在学,学习下。
    coolicer
        8
    coolicer  
       2015-11-27 08:46:00 +08:00
    学习了。好长
    dogfeet
        9
    dogfeet  
       2015-11-27 09:41:07 +08:00
    需要用这么大篇文章来解释这个 this 的坑,也是佩服 JS 这门语言。 Lua 中的环境与 self 语法糖做的事情几乎与 JS 一样,但是就没有这个坑,理解起来简直不用脑。还有与 prototype 对应的元表。 JS 有硬伤啊。
    SpicyCat
        10
    SpicyCat  
       2015-11-27 10:05:06 +08:00
    佩服楼主的钻研精神。
    Arrowing
        11
    Arrowing  
       2015-11-27 10:40:20 +08:00
    @dogfeet 因为 javascript 是动态语言啊,足够灵活,也存在动态作用域的问题,函数执行前无法确定上下文对象的。
    bramblex
        12
    bramblex  
       2015-11-27 11:26:31 +08:00
    这东西挺好玩的。
    linea
        13
    linea  
       2015-11-27 13:20:58 +08:00
    看到了自己的名字,感觉一不小心就成名了- -
    Jeter
        14
    Jeter  
       2015-11-27 16:24:09 +08:00
    我记得《深入浅出 Nodejs 》里头有说过,执行 node someFile.js 的时候, someFile.js 里的东西都会用一个闭包包装起来,所以在 someFile 里面,使用 var 声明的变量不显式挂到 global 对象的话, this.a 很明显就是 undefined
    XadillaX
        15
    XadillaX  
    OP
       2015-11-27 16:42:08 +08:00
    @Jeter 本文解释的是 REPL 模式下为什么是 2 。

    下一篇文章也是从代码层面来解释为什么文件模式是 undefined ——怎么启动的代码,怎么包的闭包等。

    https://v2ex.com/t/239429#reply1
    dogfeet
        16
    dogfeet  
       2015-11-28 17:04:22 +08:00
    @Arrowing 是否动态语言与是否应该有这个坑关系不大, Lua 也同样是动态语言。 JS 在这个地方的解决方案的确非常丑。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1265 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 17:45 · PVG 01:45 · LAX 10:45 · JFK 13:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.