JavaScript学习总结(二)——Module模式(CommonJS)

文章目录

      • 一、 Module模式的基础
        • 1. 经典Module模式
        • 2.Revealing Module(揭示模块)模式
        • 3. Singleton(单例)模式
      • 二、Module模式的实现
        • 1.CommonJS模块
          • 1.1 NodeJS简介
          • 1.2 NodeJS中CommonJS的介绍
            • 1.2.1 Module对象
            • 1.2.2 require()函数的介绍
            • 1.2.3 CommonJS中模块的循环加载
            • 1.2.4 编写CommonJS模块
        • 2. ECMAScript 6中的模块
          • 2.1 基本语法
          • 2.2 ES6模块的特点
            • 2.2.1 ES6中的循环加载
          • 2.3 import()函数
        • 3. ES6模块和CommonJS模块的比较
          • 3.1 ES6模块和CommonJS的差异
          • 3.2 NodeJS中加载ES6和CommonJS模块
            • 3.2.1 兼容使用的格式要求
            • 3.2.2 package.json中ES6和CommonJS的区分
            • 3.2.3 ES6与CommonJS相互加载时的特点

一、 Module模式的基础

这部分内容是《javaScript设计模式》的笔记,这本书成书在2010年左右,内容有点老。所讲的模式是JS流行初期,工程师对其封装方式的一些尝试和总结。这部分内容是为了了解封装的概念和从无到有的过程。后面笔记中的ES6模块和CommonJS模块是根据这些模块特点对模块的标准实现方式。

1. 经典Module模式

Module模式主要解决javascript的封装问题;在module模式产生的时期,javascript还没有块作用域的概念。所以Module模式的封装依靠javascript函数的闭包来实现。所有的内容都定义在一个立即执行的函数中。返回一个对象,包含所有的公用API。特点如下:

  1. 每个模块只生成一个全局变量(如下例中的basketModule),我们通过这个模块变量来访问各个公用API,减少了全局变量的污染;
  2. 在函数的闭包里实现真正的程序逻辑,只暴露公共API接口,方便升级。
  3. 鉴于函数往往已声明并命名,这使得在调试器中显示调用堆栈变得更加容易。
/**经典Module模式
 * 通过闭包来实现对私有函数和变量的封装。基本结构就是一个立即执行的匿名函数,
 * 返回一个对象,包含所以的公共API。
 */
var basketModule = (function() {
    //函数创造一个私有作用域
    var basket = [];

    function doSomethingPrivate() {
        //还没想好要实现啥
    }

    function doSomethingElsePrivate() {}
    //返回一个暴露公有API的对象
    return {
        addItem: function(values) {
            basket.push(values);
        },
        getItemCount: function() {
            return basket.length;
        },
        doSomething: doSomethingPrivate,
        getTotal: function() {
            var itemCount = this.getItemCount(),
                total = 0;
            while (itemCount--) {
                total += basket[itemCount].price;
            }
            return total;
        }
    }
}());
basketModule.addItem({
    item: 'bread',
    price: 0.5
});
basketModule.addItem({
    item: 'butter',
    price: 0.3
});

console.log(basketModule.getItemCount());//2
console.log(basketModule.getTotal());   //0.8
console.log(basketModule.basket);//undefined

2.Revealing Module(揭示模块)模式

Revealing Module模式,是Module模式的一种改进;在闭包内定义所有的函数和变量(这样我们在定义变量名时,就更符合模块本身的含义,而不需要考虑作为API的命名),返回一个匿名对象,它拥有指向私有函数或变量的指针。匿名对象的属性是希望展示为共有的API。后面讲的CommonJS就是一种揭示模块的模式。它的特点如下:

  1. 优点:使脚本语法更加一致,在模块代码的底部也会很容易的指出哪些函数或变量可以被公开访问,从而改善可读性。(所有函数都是以函数形式定义的,最终返回一个匿名对象引用部分需公开的函数。而Module模式中,私有函数是以函数方式定义,而公有函数是以对象方法的形式定义的,这是一种语法不一致,还有就是作为方法,Module的公有函数中会是有this,而Revealing中一般不用。)
  2. 缺点:如果一个私有函数引用一个公有函数,在需要打补丁时,公有函数是不能被覆盖的,这是因为私有函数将继续引用私有实现。(首先补丁是模块外修改模块的bug。在Module模式中,公有方法在return语句中定义,私有函数不可能调用它;而revealing Module模式中,公有和私有方法都是内部定义的,难免会相互调用;这种情况下,我们在模块外修复公有函数,模块内私有函数调用的版本并没有被修复)
  3. 缺点:该模式并不适用于公有成员,只适用于函数。(这一点,没有发现与Module模式的不同;模块最好只提供函数和常量;全局变量的话,要慎重);
  4. 优点:在早期刚开始模块化的时候,重构成Revealing Module非常的方便。几乎只需要添加一个return语句就可以了。而Module模式还要有公有函数改为方法的修改。

下面是书上的简单例子,可以看出它的结构比上面Module模式更清爽。

/**Revealing Module模式
 * 在前面module模式的基础上使输出的API命名更加一致,方便维护
 * ES6中的export{}跟它有点像
 */
var myRevealingModule = (function() {
    var privateCounter = 0;

    function privateFunction() {
        privateCounter++;
    }

    function publicFuntion() {
        publiceIncrement(); //开始时,先自增一次,这样就不会跟之前的相同了
    }

    function publiceIncrement() {
        privateFunction();
    }

    function publicGetCount() {
        return privateCounter;
    }
    //暴露公有指针指向私有函数和属性上
    return {
        start: publicFuntion,
        increment: publiceIncrement,
        count: publicGetCount
    }
}());
myRevealingModule.start();
console.log(myRevealingModule.count());//1
myRevealingModule.increment();
console.log(myRevealingModule.count());//2

3. Singleton(单例)模式

Singleton模式限制了类的实例化只有一次;即:在实例不存在的情况下,创建类的新实例;如果实例已经存在,它简单的返回该对象的引用。它适用的场景:

  1. 当类只能有一个实例且客户可以从一个众所周知的访问点访问它时:
  2. 该唯一的实例应该是通过子类化可拓展的,并且客户应该无需更改代码就能使用一个可拓展的实例时。
    下面是一个简单的例子,有这些特点:
  3. Singleton:返回的对象内部有一个getInstance方法,它里面有一个if语句用来保证只有一次实例化;我们实际需要的是instance对象。
  4. 延迟执行:或者叫惰性执行。我们得到了模块mySingleton,当我们真正需要实例时,才会调用getInstance方法。
  5. 可拓展性:最后的例子是可拓展性的表述。因为返回的是mySingleton模块,我们用的是getInstance得到的实例。所以我们可以对getInstance进行拓展,从而对实例做拓展。而前面的Module真正的API是返回的函数,如果我们对它进行拓展,会切断它跟私有对象函数直接的联系。
/**Singleton模式
 * 单例模式的特点是它限制了类的实例化次数只能一次,使用场景:
 * 1、当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时;
 * 2、该唯一的实例应该是通过子类化可拓展的,而且客户应该无需更改代码就能使用一个拓展的实例时。
 */
var mySingleton = (function() {
    var instance;

    function init() {
        //Singleton 私有方法和变量
        function privateMethod() {
            console.log('I am private');
        }
        var privateVariable = 'I\'m also private';
        var privateRandomNumber = Math.random();
        return {
            //public methods and variable
            publicMethod: function() {
                console.log('The public can see me!');
            },
            publicProperty: 'I am also public',
            getRandomNumber: function() {
                return privateRandomNumber;
            }
        };
    };

    return {
        //获取Singleton的实例,如果存在直接返回;不存在,创建新实例然后返回
        //这个地方返回一个函数,可以延时加载,先记载mySingleton,使用时再调用getInstance
        getInstance: function() {
            if (!instance) {
                instance = init();
            }
            return instance;
        }
    }
}());
var singleA = mySingleton.getInstance();
var singleB = mySingleton.getInstance();
console.log(singleA === singleB); //true
/**
 * 下面的代码实现了拓展,
 * FooSingleton是BaseSingleton的子类,我们使用一个类似工厂函数来
 * 选择不同instance对象。
 * */
mySingleton.getInstance = function() {
    if (this._instance == null) {
        if (isFoo()) {
            this._instance = new FooSingleton();
        } else {
            this._instance = new BasicSingleton();
        }
    }
    return this._instance;
}

二、Module模式的实现

1.CommonJS模块

CommonJS是模块的一种实现理论。NodeJS中的模块是CommonJS的一种实现方式。Webpack的等工具也支持CommonJS。此处讨论的CommonJS是以NodeJS中的实现为准的。解释CommonJS的基本特征。

1.1 NodeJS简介

NodeJS是一种服务器端javaScript环境。它是基于Chrome的V8引擎(这个不影响我们使用,只是底层处理快慢的问题)。它内置了操作系统的API模块,通过加载这些模块,我们可以向使用C/C++一样使用javaScript。它与客户端javaScript有下面的不同:

  1. 在客户端javascript中,this指向Window对象(无论我们引入多少个javascript文件,它都是当前网页的Window对象)。在NodeJS中,我们编写JS文件都是一个独立的module,它本身是一个对象;在它这个模块中的this指向这个module。
  2. 在shell中运行js文件。输入的是: node name.js 熟悉C语言程序运行的人知道,node是程序名,而name.js只是一个参数;也就是在执行name.js之前node程序已经运行并准备好了环境,有两个全局变量global和process对象是当前运行这个node程序的信息。name.js只是这个程序中的一段代码,而非整个程序。
  3. NodeJS中我们的代码是通过CommonJS来组织和联系的。
1.2 NodeJS中CommonJS的介绍
1.2.1 Module对象

上面我们介绍了NodeJS中,所有的JS文件都是一个module对象,它是NodeJS自动调用的,只要我们用node运行一个js文件。或者是在js文件中require其他模块,都会调用这个函数。Module构造函数的作用是将我们的js文件,统一包装成格式一致的CommonJS模块。通过module.exports输出公有API,通过id/parent/children来标识模块及其之间的联系。
从下面摘抄的源码中可以看出来:

  1. Module构造函数需要id和parent两个参数;id是Module的唯一标识,在nodeJS中是module的绝对路径(因为系统中路径都是唯一的),也就是说在加载一个模块之前要先知道它的路径,这一步是由后面介绍的require()函数来实现的;parent是一个对象,指向要调用require()函数的模块。在node的参数中我们加载入口模块时,id为当前路径".",parent为null。
  2. children属性是一个数组,记录当前模块的依赖项,每个要加载的模块是单独的一项。
  3. loaded属性,表示当前模块是否加载完成的标识;在module中输出时,当前模块都是false;但在输出语句之前加载的子模块都为true。
  4. exports属性:实际上是module.exports属性,就是当前模块输出的内容,注意模块中还定义了一个var exports = module.exports;根据JS的性质,exports是module.exports的引用。我们可以通过exports.prop=val;来给module.exports添加属性,但要注意不能使用module=obj的方式来赋值。因为复制会切断exports与module.exports之间的联系;实际输出的module.exports并没有被obj赋值。
  5. 从下面的代码中可以看出:以前children中可能会包含多次输入的模块名,而在新的代码中,多了一步检查,消除了重复的可能。
/**NodeJS早期的代码中Module函数*/
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 = [];
}

module.exports = Module;
var module = new Module(filename, parent);

/************************************************************
 * 最新的node源码中Module函数
 ************************************************************/
function updateChildren(parent, child, scan) {
  const children = parent && parent.children;
  if (children && !(scan && children.includes(child)))
    children.push(child);
}

const moduleParentCache = new SafeWeakMap();
function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  moduleParentCache.set(this, parent);
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}
1.2.2 require()函数的介绍

上面我们已经学习了一个Module对象包含那些内容;下面的部分,我们介绍如何加载一个模块;每一个使用Module对象封装的模块,都有一个require函数,我们使用全局require会调用Module对象中的相应函数。下面代码是对require()函数的简单模拟,它没有实现require的全部功能,但展现了require的主要功能。我们以下面的代码为例,解析require()函数的主要功能:

  1. 解析模块的完整路径,称之为id;这个任务由require.resolve来实现。主要的路径拓展规则如下:
    • moduleName为路径名:
      • 相对路径的参照路径调用require的模块的路径,据此可以得到绝对路径;
      • 作为文件名
        • 如果存在moduleName的文件,直接返回此文件
        • 否则,在module后面以此添加.js/.json/.node来查找相应的文件,找到了就直接返回。
      • moduleName作为文件夹的名字
        • 查找moduleName/package.json中的main字段;main字段为一个路径;按照处理路径名的方式处理这个路径;
        • 如果没有package.json,那么依次查找moduleName/index.js, moduleName/index.json, moduleName/index.node;找到为止。
    • moduleName为模块名
      • 如果有此名字的内置模块,直接返回内置模块的id;
      • 按照下面的层次结构来查找模块:(在目录中查找的方式按照上面文件夹内的查找方式进行)
        • 应用程序(node 直接加载模块)目录内的node_module子目录;
        • 应用程序的父目录中的node_module目录;
        • 继续向上面父目录中的node_module目录,直到根目录;
        • 最后在全局安装的模块中查找。
    • 找不到,抛出错误。
    • 这一步有两点注意:1. 不要将自定义模块跟内置模块重名,重名情况下,只能通过路径方式加载自定义模块。2.我们可以利用上面先查找当前目录再查找全局安装目录的规则,在此时莫个模块的新版本时,将新模块下载到应用程序目录;就可以不改变源码来测试新功能了。
      伪码实现见:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYoszsKY-1593688664133)(https://github.com/nodejs/node/blob/master/doc/api/modules.md)]
  2. 如果过去加载过模块,它应该在缓存中。这种情况下,直接返回它。(这样就避免了模块代码的多次运行);
  3. 如果模块尚未加载,需要为首次加载设置运行环境;特别是调用上面介绍的Module构造函数定义Module对象;parent是当前调用require函数的模块。其中module.exports用于输出模块代码的公共API。
  4. module对象被缓存。
  5. 调用loadModule()函数,源代码从文件中被读取,并执行;从而得到真正的module.exports。(注意这里的module.exports是执行结果,它与加载模块中的源代码不再有直接联系)。另外我们的代码相对于实际的require有以下不足:
    • 在我们的代码中使用eval来运行读取的代码,这样是非常危险的。通常做法会采用相对安全的vm模块来运行读取的源代码。
    • 在实际require中不仅考虑加载js模块,还要能处理C/C++写的.node模块,在REPL中模块加载的问题等。
  6. 返回我们需要的公共接口。

下面的代码摘自《Node.js设计模式》

/**require()的简单实现  */
const require = Module.prototype.require = (moduleName) => {
    console.log(`Require invoked for module: ${moduleName}`);
    const id = require.resolve(moduleName); //[1]
    if (require.cache[id]) { //[2]
        return require.cache[id].exports;
    }

    //module metadata
    const module = new Module(id, this);//[3]
  

    //Update the cache
    require.cache[id] = module; //[4]

    //load the module
    loadModule(id, module, require); //[5]

    // return exported variable
    return module.exports; //[6]
}

require.cache = {};

//用于解析module的完整路径
require.resolve = (moduleName) => {
    /* reslove a full module id from the moduleName */
};

/**Module加载器 */
function loadModule(filename, module, require) {
    const wrappedSrc = `(function(module,exports,require){
        ${fs.readFileSync(filename,'utf8')}
    })(module,module.exports,require);`;
    eval(wrappedSrc);
}
1.2.3 CommonJS中模块的循环加载

上面我们已经对require函数的实现原理,做了解析;我们现在通过循环加载的例子来对这个过程进行进一步的分析:

  1. main.js中require(‘a.js’):解析a的路径,生成moduleA(此时exports为空);然后缓存moduleA;然后load moduleA,也就是执行脚本;
  2. 在a.js中遇到require(‘b.js’): 此时moduleA.exports.done=false;与步骤1相似,加载moduleB;遇到require(‘a.js’),此时moduleA,已经缓存,直接返回(可以看出缓存避免了死循环);此时 a.done=false;继续执行脚本b,直到结束。
  3. 返回a.js继续执行,后面的语句更新moduleA.exports.done=true;执行结束,返回main.js
  4. main.js中执行require(‘b.js’),已缓存,直接返回,然后输出结果。
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

shell运行结果:

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
1.2.4 编写CommonJS模块

下面是各种使用CommonJS编写模块的例子;注意:输出API为module.exports,它有一个引用为exports;当我们使用命名导出的方式时(也就是以exports属性的形式导出),可以使用exports.prop = val的方式;一旦需要赋值,则要使用module.exports= obj的方式,并且在此模块中都应以module.exports的方式添加输出API。这个具体就是JS中命名的原理。下面是输出API的几种形式:

  1. 命名导出
    这其实就是揭示module模式的一种形式
/**命名导出Named exports */
module.exports.info = (message) => {
    console.log("info:", message);
};
module.exports.verbose = (message) => {
    console.log("verbose:", message);
};

  1. 导出函数
    这种只输出一个函数的方式,看似功能受限,实际上它是一直欧冠你完美方式,把中心放在模式的一个单一功能上。
//导出函数
/**
 * 只要使用这种直接赋值的方式,就必须用module.exports;为保证不覆盖其他输出,
 * 这一句必须在其他给module.exports添加属性的语句之前。
 * 而且这就切断了module.exports与exports直接的关系。
 * 后面都要用module.exports
 * */
module.exports = (message) => {
    console.log("logger: ", message);
};
  1. 导出构造函数
//logger2.js
//导出构造函数
class Logger {
    constructor(name) {
        this.name = name;
    }
    log(message) {
        console.log(`[${this.name}]: ${message}`);
    }
    info(message) {
        console.log(`info: ${message}`);
    }
    verbose(message) {
        console.log(`verbose:  ${message}`);
    }
}

module.exports = Logger;
  1. 导出实例
    有点类似于Singleton模式,但并不是严格意义上的Singleton模式。
//logger3.js
//导出实例
function Logger(name) {
    this.count = 0;
    this.name = name;
}

Logger.prototype.log = function(message) {
    this.count++;
    console.log(`[${this.name}][${this.count}] ${message}`);
}

module.exports = new Logger('DEFAULT');
module.exports.Logger = Logger;
  1. 修改其他模块或全局变量
    这是修改第三方模块bug的一种方法。
//patcher.js
//修改其他模块或者全局变量
require('./logger3').customMessage = () => {
    console.log("This is a new function added by patcher.js");
}

对上面模块的调用如下:

const logger = require("./logger.js");
logger("This is an logger message");
logger.info("This is an information message");
logger.verbose("This is a verbose message");

const Logger = require("./logger2.js");
const dbLogger = new Logger("DB");
dbLogger.log("class output message");

require('./patcher');
const logger3 = require("./logger3");
logger3.log("instance message");
const costomLogger = new logger3.Logger('CUSTOM');
costomLogger.log("instance message");
logger3.customMessage();

2. ECMAScript 6中的模块

2.1 基本语法

ES6通过export和import来实现模块的导出和导入:

  • export:
    • export实际导出的是一个匿名对象,而所有导出的API是它的属性,不是一般意义上的解构赋值。(这个后面的例子会说明,这有点类似于有一个全局变量——我们说的名对象,要导出的属性在定义时就是以它的属性的名义定义的——直接表现就是改变a的值,会改变引入的a的值。
    • export default val;这句就使val作为导出的对象,此时如果我们在模块中改变b的值,不会影响我们引入的b。
  • import:
    • import {val1,val2,…} from ‘moduleName’; moduleName可以是模块名字(通过配置文件来高数引擎怎么导入),也可以是模块的路径。
    • 如果对module的api不是很了解可以使用*来代替整个模块输出的匿名变量。

下面是基本语法的例子:

export var a = 5; //导出变量
export function add(a,b){return a+b;}   //导出函数
var c =6;
function substract(a,b){return a-b;}    
export {c as aliasC ,substract};   //把要导出的放在一起
improt * as moduleName from "moduleName";
// 上面的这些语句可以在一个文件中相当于exprot{a,add,c,substract};但不提倡
//另外一种只导出一个变量的办法(这个不提倡)
var b = 4;
export default b;
2.2 ES6模块的特点

ES6的模块引入有下面的特点:

  1. 使用ES6模块,就自动采用严格模式(这是ES5中的定义,尤其重要的一定就是不能使用全局this);
  2. ES6的引入是静态的,它会自动被提前到文件的开始,并且不能在语句内使用(因为静态阶段,语句还未执行,那么也就无法引入了);
  3. ES6的引入类似于C/C++中的include,是把这段代码嵌套在代码文件的头部,嵌套后像一般脚本文件那样开始执行。但是只有import的变量在当前文件中可见(就好像其他代码是static声明的,文件作用域;而导出的是全局作用域)。
  4. ES6的引入是Singleton的,只引入一次,不会重复引入,无论我们写几次import语句;
  5. import和export之间传递的是对象(在不是export default的情况下,这是一个匿名对象,而所有export的变量和函数是它的属性;import时,也是整个引入,当只有{}内命名的变量可见。
  6. import的变量是只读的,给它们赋值会报错。

下面是模块的特点的例子,我们定义了三个模块,由main来引用;之所以文件后缀为mjs,是因为这是nodeJS使用ES6module的约定。

//lib.mjs
 var foo = 'bar';
 setTimeout(() => foo = 'baz', 100);
 var a = 10;
 export { foo, a };

//lib2.mjs
 export { foo, a }
from './lib.mjs';
//console.log(a); 报错,它是直接导出,没有导入此文件。

//lib3.mjs
var foo2 = 'true';
setTimeout(() => foo2 = 'false', 1000);
export default foo2;

//main.mjs
import * as obj from './lib.mjs';
import foo2 from './lib3.mjs';
import { a, foo } from './lib2.mjs';

//obj.b = 'false';//报错,引入的变量是只读的。
//main: baz baz foo2: true;foo的值改变了,说明引入的是整个脚本,lib.mjs中异步函数对foo的改变
//在main中有体现;而foo2没有改变说明传递的是对象的引用,直接改变对象,不能被传递。
setTimeout(() => console.log("main:", foo, obj.foo, 'foo2:', foo2), 3000);
console.log('the same module? ', a === obj.a); //true,说明两次引入的是同一个模块,且实例化了一次
2.2.1 ES6中的循环加载

关于ES6静态加载的解释:

  1. 作用域链: 每一段JS代码(全局或函数内)都有一个与之相对应的作用域链。这个作用域链是一个对象列表或链表。当我们遇到变量x时,就从这个作用域链中查找。
  2. 我们又知道在JS中var定义的变量和function定义的函数会加入到这个作用域链中,也就是所谓的变量提前;而变量加入作用域链的过程是在脚本执行之前完成的。执行这个的过程就是JS的静态编译期。
  3. ES6的export和import就是在这个时期进行的,JS文件先将自己的变量加入到作用域链中,然后将import的变量加入到作用域链中。
  4. 另外,前面说过了,ES6模块的加载是单例的。不妨就想象成C/C++的include过程。

下面,我们以一个例子来对ES6的循环加载做个分析:

  1. main.mjs中没有定义变量,那么它先处理 import {foo} from ‘a.mjs’; 编译转移到a.mjs;
  2. a.mjs中,先将自有变量foo和函数f加入到a.mjs的作用域链中;然后处理 import { bar } from “./b.mjs”;编译转移到b.mjs;
  3. b.mjs中,现将自有变量bar加入到b.mjs的作用域链中;然后处理import { foo, f } from ‘./a.mjs’;因为a.mjs被main.mjs导入过,moduleA已经存在(如果a.mjs中的语句为export let foo=‘foo’; let定义的变量不会加入a的作用域链。那么import会报错:ReferenceError: Cannot access ‘foo’ before initialization);b.mjs直接import foo和f;
  4. 此时,b.mjs的作用域链已经完成了;直接执行b.mjs,从a.mjs中导入的foo现在为undefined,foo为函数直接调用。(如果a.mjs语句为:export var f= function(){ return ‘function improved’;}; 此阶段仅添加了变量f,而不知道其为function,那么会报错TypeError: f is not a function);
  5. b.mjs执行完,a.mjs的import语句也就完成了,执行a.mjs;
  6. a.mjs执行完,foo被赋值;main.mjs中import a完成。执行import b,直接从前面运行生产的moduleB中得到;执行main.mjs。
//a.mjs
import { bar } from "./b.mjs";
console.log("a.mjs", bar);
//换作let foo='foo',会报错
export var foo = 'foo';
//换作 var f= function(){ return 'function improved';},会报错
export function f() { return "function improved"; };

//b.mjs
import { foo, f } from './a.mjs';
console.log("b.mjs", foo, f());
export var bar = 'bar';

//main.mjs
import { foo } from "./a.mjs";
import { bar } from "./b.mjs";
console.log("main.mjs");

运行结果:

$ node main.mjs

b.mjs undefined function improved
a.mjs bar
main.mjs
2.3 import()函数

import()函数是ES2020引入的动态加载模块的方法;与上面介绍的ES6模块有以下区别:

  1. 它是动态加载的,可以写于文件的任何位置;它与所加载的模块没有静态链接关系,而是类似与Node中require()方法加载的模块,是脚本运行的结果;但与Node中不同的是,它是异步加载。在webpack中实现了这个方法。
  2. 它的语法是 import(specifier); specifier是加载模块的路径,返回一个Promise对象。

下面是一个简单的例子,异步加载一个脚本来作为click的事件处理函数。

button.addEventListener('click', event => {
    import ('./dialog.js').then(dialog => { dialog.open(); }).catch(err => console.err(err));
});

3. ES6模块和CommonJS模块的比较

3.1 ES6模块和CommonJS的差异

主要差异有:

  1. CommonJS模块输出的是一个值的拷贝(上面已经解释过是load脚本运行的返回值);ES6模块输出的是值的引用。这个区别的表现就是ES6导入的变量是只读的。而CommonJS导入的对象可以修改。另外,如果ES6模块中有异步代码的话;异步代码对输出变量的改变会反映在导入它的模块上。
  2. CommonJS在运行时加载,它可以放在代码的语句中;ES6模块是编译时加载的,所以它必须在代码的最外层,而且会被提前到文件开头。
  3. CommonJS是同步加载的,考虑到它加载时运行模块的脚本,所以CommonJS内部不能有异步代码;在ES6中,没有这个限制。
3.2 NodeJS中加载ES6和CommonJS模块

在模块管理中,我们组好保证使用引用模块的方式是相同的,要么全部是ES6模块,要么是CommonJS模块。但有时我们会混合使用或者说定义兼容的模块。下面我们来讨论这个问题。(nodeJS要高于v13.2版本)

3.2.1 兼容使用的格式要求

NodeJS最早只支持CommonJS模块格式,现在又支持ES6模块格式,那么就必须有所区分。

  1. 只要JS文件中使用export或import命令,它的后缀就必须为.mjs,且文件内部不能使用require()来加载模块;同时默认为严格模式;
  2. 相应的使用.cjs来标识CommonJS文件;不能使用ES6命令。
3.2.2 package.json中ES6和CommonJS的区分

package.json是模块的说明文件(nodeJS的包管理程序npm使用);我们可以使用npm init来初始化这样一个文件。为了在nodeJS中兼容ES6模块,它增加了type和export字段。

  1. type字段为module的JS文件被理解成ES6模块; type为commonjs或者没有此字段,js文件被理解成commonJS模块。注意:如果文件名为cjs
  2. main字段: NodeJS加载模块时,识别的入口字段。(这是老版本使用的字段)
  3. exports字段:它是新加的字段;NodeJS加载时识别的入口字段。它的优先级高于main字段;它是一个对象,可以包含多个路径字段,相当于别名。这样,我们就可以在加载不同方式的模块时,采用不同的字段。有三个默认别名要理解:
    • “.”:“pathtofile”,是main字段的别名
    • “require”: 是CommonJS入口地址的字段别名
    • “default”: 是ES6模块入口地址的字段别名。
3.2.3 ES6与CommonJS相互加载时的特点
  1. 使用import导入自定义CommonJS模块时,必须整体加载,它相当于直接加载了module.exports这个对象,然后给它起了一个别名。import导入内置模块时,可以跟正常ES6模块一样,这应该是NodeJS的内置模块的兼容性做的好。
    语法如下:
//导入自定义CommonJS模块
import cjsModule from './index.cjs'; //后面这个文件一定要是cjs后缀
//导入内置模块
import { createRequire } from 'module';
//定义require函数(跟CommonJS中require一样),但次函数可以在ES6中使用
const require = createRequire(import.meta.url); 
const cjsModule = require('./index.cjs');
  1. CommonJS中不能使用require()加载ES6模块,只能使用import()这个方法来加载。

下面的例子中我们定义了一个兼容的模块myModule,这个模块文件夹中有三个文件:package.json,index.cjs和wrapper.js;具体内容如下:

//package.json
{
    "name": "mymodule",
    "version": "1.0.0",
    "description": "an example",
    "main": "index.cjs",
    "exports": {
        "1require": "./index.cjs",
        "default": "./wrapper.js"
    },
    "type": "module",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "afc",
    "license": "MIT"
}
//index.cjs
module.exports.name = "value";
//wrapper.js
import cjsModule from "myModule";
export const name = cjsModule.name;

//main.js加载CommonJS模块
console.log(require("myModule").name);

//main.mjs加载ES6模块
import { name } from 'myModule';
console.log(name);

参考数目:

  • 《Node学习指南(第二版)》Shelley Powers 著
  • 《Node.js设计模式(第二版)》Mario Casciaro | Luciano Mannino 著
  • 《JavaScript设计模式》 Addy Osmani 著
  • 《ES6标准入门(第三版)》阮一峰 著

你可能感兴趣的:(nodeJS,native,JS)