◆ module の実行空間でもグローバルは共有してた

気がつけば Node.js も 7 なんですね

ブラウザ JavaScript や Node.js で ES module はいつくらいから使えるなるんだろうとふと思って
そういえば今の Node.js のモジュールってどうなってるんだろうと思ったので少し調べてみました

メモ書きを軽くまとめたものなのであんまり説明なしです



バージョン
> process.version
'v7.0.0'

node.js のモジュール読み込みと言えば require
エラーぽい値を入れてみる
> require(1)
AssertionError: path must be a string
    at Module.require (module.js:499:3)
    at require (internal/module.js:20:19)
    at repl:1:1
    ~~
native code じゃなくて JavaScript っぽい

require のソース
> console.log(require.toString())
function require(path) {
    try {
      exports.requireDepth += 1;
      return self.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }
self.require を呼んでる

self ってなんだろ
さっきの StackTrace は
    at Module.require (module.js:499:3)
    at require (internal/module.js:20:19)
だったので
Module.require?
> module.__proto__.constructor.name
'Module'

> var m = module.__proto__.constructor

> console.log(m.require.toString())
TypeError: Cannot read property 'toString' of undefined
Module.require ないみたい

インスタンスのメソッドかも
> console.log(m.prototype.require.toString())
function (path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
}
assert にさっきのエラーもあるのでこれみたいですね

Module._load を呼んでる
> console.log(m._load.toString())
function (request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
}
長い……

けど必要なところは これくらいかな
var module = new Module(filename, parent);
tryModuleLoad(module, filename);

Module 関数
> console.log(m.toString())
function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}
プロパティセットしてるだけでここではロードしてないです

tryModuleLoad みればよさそう
だけど この関数スコープ的に Node.js の REPL 上だと無理そう

名前的に load ぽいメソッド呼び出しに try catch してるだけだと思うけど
メソッド一覧見てみる
> m.prototype.
m.prototype.__defineGetter__      m.prototype.__defineSetter__
m.prototype.__lookupGetter__      m.prototype.__lookupSetter__
m.prototype.__proto__             m.prototype.constructor
m.prototype.hasOwnProperty        m.prototype.isPrototypeOf
m.prototype.propertyIsEnumerable  m.prototype.toLocaleString
m.prototype.toString              m.prototype.valueOf

m.prototype._compile              m.prototype.load
m.prototype.require
load メソッドというのがあるのでこれの予感
> console.log(m.prototype.load.toString())
function (filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
}
拡張子ごとに用意された関数呼び出してる

_extensions は
> console.log(m._extensions)
{ '.js': [Function], '.json': [Function], '.node': [Function] }
3種類だけみたい

.node なんて拡張子もロードできるんだ

とりあえず .js を見ます
> console.log(m._extensions[".js"].toString())
function (module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
}
ファイル読み込んでBOM削除してコンパイルしてる

_compile
> console.log(m.prototype._compile.toString())
function (content, filename) {
  // Remove shebang
  var contLen = content.length;
  if (contLen >= 2) {
    if (content.charCodeAt(0) === 35/*#*/ &&
        content.charCodeAt(1) === 33/*!*/) {
      if (contLen === 2) {
        // Exact match
        content = '';
      } else {
        // Find end of shebang line and slice it off
        var i = 2;
        for (; i < contLen; ++i) {
          var code = content.charCodeAt(i);
          if (code === 10/*\n*/ || code === 13/*\r*/)
            break;
        }
        if (i === contLen)
          content = '';
        else {
          // Note that this actually includes the newline character(s) in the
          // new output. This duplicates the behavior of the regular expression
          // that was previously used to replace the shebang line
          content = content.slice(i);
        }
      }
    }
  }

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  if (process._debugWaitConnect) {
    if (!resolvedArgv) {
      // we enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (filename === resolvedArgv) {
      delete process._debugWaitConnect;
      const Debug = vm.runInDebugContext('Debug');
      Debug.setBreakPoint(compiledWrapper, 0, 0);
    }
  }
  var dirname = path.dirname(filename);
  var require = internalModule.makeRequireFunction.call(this);
  var args = [this.exports, require, this, filename, dirname];
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result = compiledWrapper.apply(this.exports, args);
  if (depth === 0) stat.cache = null;
  return result;
}
ながああぁぁぁぁああい
ここで気づいたけど最初の方とインデントレベルが違う
Node.js 全体で統一はされてないみたい

必要なところはこんなの?
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
  filename: filename,
  lineOffset: 0,
  displayErrors: true
});

var result = compiledWrapper.apply(this.exports, args);
.js のファイルの中身を wrap して this context を指定して実行してるみたい

wrap は実行してみたほうが分かりやすかったので
> m.wrap("###")
'(function (exports, require, module, __filename, __dirname) { ###\n});'
.js ファイルの中身を関数の本体にしてる
Function 関数で関数にしないでまだ関数定義の書かれた文字列
__filename とか require が使えるのは引数として渡されてたからだったんだねー

vm.runInThisContext
> console.log(vm.runInThisContext.toString())
function (code, options) {
  var script = new Script(code, options);
  return script.runInThisContext(options);
}
Script はたぶん vm.Script
> console.log(vm.Script.toString())
function ContextifyScript() { [native code] }
とうとう native code キターー!

script.runInThisContext の方は
> console.log(vm.Script.prototype.runInThisContext.toString())
function (options) {
  if (options && options.breakOnSigint) {
    return sigintHandlersWrap(() => {
      return realRunInThisContext.call(this, options);
    });
  } else {
    return realRunInThisContext.call(this, options);
  }
}
見れた けど realRunInThisContext が見当たらないし native code が入ってきたのでここまで



実行前のコンパイルはよくわからなかったけど .js ファイルの中身を本体にした関数を作って実行してるみたい

ロードする module にこういうの書いて
var local_val = 1
global_val = 2

ロード後に
console.log(global_val)
console.log(local_val)
ってすると global_val は見れて local_val は存在しなくてリファレンスエラーです

モジュールの実行は完全に別の空間かと思ってましたが グローバルは共有してるんですね