【问题标题】:Namespace import in node.jsnode.js 中的命名空间导入
【发布时间】:2026-02-07 20:15:02
【问题描述】:

我有一些允许合并命名空间的函数,非常类似于import,当模块包含很多函数时(我公开了一个包含几十个组合器的 API) 它为导出的每个项目生成大量 var f = target.f;

function getNamespace(name, exports){
    var output='';
    for(var item in exports){
        output += 'var ' + item + ' = '+name+ '.'+item + ';';
    }
    return output;
}

及用法:

var paco = require('./paco.js');

eval(paco.getNamespace('paco', paco));

// instead of paco.between(paco.start(),paco.content(),paco.end())
between(start(), content(), end())

问题

我有办法将评估“隐藏”到某个函数中吗?我既不想改变全局命名空间也不想调用vm.runInThisContext,只需要在调用类似于require的函数之后将一些局部变量添加到调用上下文中。

我的意思是我需要类似的东西

import('./paco'); 
// this should work like this
// var paco = require('./paco.js');       
// var between = paco.between;

但在调用范围内没有全局突变,也没有 eval。

【问题讨论】:

    标签: javascript node.js namespaces


    【解决方案1】:

    根据这个答案Global variables for node.js standard modules?global 对象与浏览器中的window 相同。因此,您可以向该对象添加密钥

    function getNamespace(exports) {
        for(var item in exports){
            global[item] = exports[item];
        }
    }
    

    并将其用作:

    paco.getNamespace(paco);
    

    根本不需要评估。

    【讨论】:

    • 正如我所说,我不能改变全局范围,因为我暴露了几十个函数以及当我使用几乎所有它们作为组合器时的典型情况
    • @MadHollander 但是您使用 eval 公开所有这些。 eval('var ' + item + ' = '+name+ '.'+item + ';');global[item] = exports[item]; 完全相同,除非您使用未提及的 (function() { })(); 包装函数。
    • 其实,没有。当我从我的函数中调用 importn 时,全局范围保持不变。
    【解决方案2】:

    没有。无法从外部模块修改本地范围。原因是,当在外部模块中调用 eval 时,它的上下文将是外部模块,而不是需要模块的范围。

    此外,vm.runInThisContext does not have access 到本地范围,所以这也不会帮助你。

    【讨论】:

      【解决方案3】:

      tl;dr:没有。

      为了理解为什么这是不可能的,了解 Node 在幕后做了什么很重要。

      假设我们在 test.js 中定义了一个函数:

      function foo() {
          var msg = 'Hello world';
          console.log(msg);
      }
      

      在传统的浏览器 JavaScript 中,只需将该函数声明放在一个文件中并使用 <script> 标签拉入该文件,就会导致在全局范围内声明 foo

      当您require() 一个文件时,Node 会做不同的事情。

      1. 首先,它根据a somewhat complex set of rules准确地确定应该加载哪个文件。

      2. 假设文件是​​JS文本(不是编译好的C++插件),Node的模块加载器callsfs.readFileSync获取文件内容。

      3. 源文本是匿名函数中的wrapped。 test.js 最终会看起来像这样:

        (function (exports, require, module, __filename, __dirname) {
        function foo() {
            var msg = 'Hello world';
            console.log(msg);
        }
        });
        

        对于曾经将自己的代码包装在匿名函数表达式中以防止变量泄漏到浏览器中的全局范围内的任何人来说,这应该看起来很熟悉。它还应该开始理解 Node 中的“神奇”变量是如何工作的。

      4. 模块加载器evals1 来自步骤 3 的源文本,然后调用生成的匿名函数,传入一个新的 exports 对象。 (见Module#_compile。)

        1 - 真正的vm.runInThisContext,类似于eval,只是它无法访问调用者的作用域

      5. 匿名包装函数返回后,内部缓存module.exports的值,然后由require返回。 (对require() 的后续调用返回缓存值。)

      正如我们所见,Node 通过简单地将文件的源代码包装在匿名函数中来实现“模块”。因此,不可能将函数“导入”到模块中,因为 JavaScript 不提供对函数的 execution context 的直接访问——即函数局部变量的集合。

      换句话说,我们无法循环访问函数的局部变量,也无法像使用对象的属性一样创建具有任意名称的局部变量。

      例如,对于对象,我们可以执行以下操作:

      var obj = { key: 'value' };
      for (var k in obj) ...
      obj[propertyNameDeterminedAtRuntime] = someValue;
      

      但是没有代表函数局部变量的对象,这对于我们将对象的属性(如模块的exports)复制到函数的局部范围中是必要的。

      您所做的是使用eval 在当前范围内生成代码。生成的代码使用 var 关键字声明局部变量,然后将其注入到调用 eval 的作用域中。

      没有办法将eval 调用移出模块,因为这样做会导致注入的代码插入到不同的范围内。请记住,JavaScript 具有静态作用域,因此您只能访问包含您的函数的词法作用域。

      另一种解决方法是使用with,但您应该避免使用with

      with (require('./paco.js')) {
          between(start(), content(), end())
      }
      

      with 不应使用,原因有两个:

      1. absolutely kills performance 因为 V8 无法执行名称查找优化。
      2. 已弃用,严格模式下禁止使用。

      老实说,我建议不要对 eval 做一些棘手的事情,而是帮您未来的维护者一个忙,并遵循将模块的导出分配给局部变量的标准做法。

      如果您经常输入,请将其设为单字符名称(或使用更好的编辑器)。

      【讨论】: