V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
XadillaX
V2EX  ›  Node.js

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

  •  6
     
  •   XadillaX · Nov 26, 2015 · 5439 views
    This topic created in 3816 days ago, the information mentioned may be changed or developed.

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

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

    https://v2ex.com/t/239429#reply1
    dogfeet
        16
    dogfeet  
       Nov 28, 2015
    @Arrowing 是否动态语言与是否应该有这个坑关系不大, Lua 也同样是动态语言。 JS 在这个地方的解决方案的确非常丑。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1574 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 787ms · UTC 16:44 · PVG 00:44 · LAX 09:44 · JFK 12:44
    ♥ Do have faith in what you're doing.