从module.export,聊聊JS语言的形参实参传递

961 阅读3分钟

问题来源

在学习node的commonjs模块化的过程中,对于模块导出,node提供两种解决方式

exports.xxx = xxxx;
module.exports = xxxx;

但是在模块编译的过程中,外层包裹的函数里面是传递了exports的参数,但是为什么使用以下的方式就无法导出模块,以下是require的模拟代码:

function require(/* ... */) {
    const module = { exports: {} };
    ((module, exports) => {
      // Module code here. In this example, define a function.
      function someFunc() {}
      exports = '1111'; // 无法导出,不生效
      // At this point, exports is no longer a shortcut to module.exports, and
      // this module will still export an empty default object.
      module.exports = someFunc;
      // At this point, the module will now export someFunc, instead of the
      // default object.
    })(module, module.exports);
    return module.exports;
  }

console.log(require()); // [Function: someFunc]

初步分析

在编译的流程中,首先是先找到模块的位置,然后加载模块,中间会对模块进行首尾包裹,方便在后面执行的时候可以直接获取到module.exports的对象。 而在具体的实现中,是通过构造module对象,然后将module和exports传递到函数内,而exports本身也是一种对象引用,传递到里面之后,对这个引用 进行赋值没有生效,所以直接对js传递进来的引用没有办法直接改变对象本身,这就是涉及到求值策略的机制了。以下是基于C++的求值策略相关知识。

参数传递机制(求值策略)

按值传递

把参数的实际值复制给函数的形参,修改函数内的形参,不会影响到实参。C++默认使用此传值策略。

按指针传递

该方法把参数的地址赋值给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数

按引用传递

该方法把参数的引用赋值给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。

C++ 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的参数。而JavaScript则是只有按值传递的方式。如果 是基本类型,则复制基本类型的值给形参,如果是引用类型,则是复制引用值到形参中。

这里的引用类型是后面C++推出的新的复合类型,相当于指针功能的子集。而引用类型算是做了安全限制的指针,比如强制要求必须初始化,不能更改 指向等。因此在大部分场景,C++的求值策略一般推荐使用引用传递的方式。如在node源码中,buffer的compare内置方法就使用了:

20220511234354

node解决方案

js语言本身是使用C++的V8引擎实现的。js对基本类型采用的是传值,但是对于引用类型的,使用的是C++的按引用传递(引用是更安全的指针),传递的是引用类型的地址。只是相对于按指针传递来说,这个引用只能被修改,不能被覆盖,因此更为安全。现在大部分的C++应用也是推荐使用按引用传递的。

20220513131005

各种传值策略的特点

20220513131035

最佳实践

使用module.exports来统一导出模块,首先易读性强,另外也能避免歧义。

参考资料

ECMA-262-3 in detail. Chapter 8. Evaluation strategy

C++性能榨汁机之指针与引用

C++中函数调用时的三种参数传递方式详解

求值策略-wiki

深入浅出nodejs-2-2 Node的模块实现