Node 模块与 ES6 模块

·  阅读 310

这是我参与更文挑战的第 19 天,活动详情查看:更文挑战

首先了解一下为什么代码需要模块化。将代码模块化的目的,是为了能够将不同来源的代码模块组装成大型程序。各个模块都封装和隐藏了自身的实现细节,模块与模块之间互不影响,不存在耦合。

通过模块化的编程方式,开发者们只需要关心自己所负责模块的实现,使用其它模块时只需要关心它们所暴露的接口方法,能够极大地提升编程效率和项目的维护成本,同时提高代码的可读性和可复用性。

ES6 之前,JavaScript 都没有内置对模块的支持,实践中常用的模块化形式是基于闭包实现。之后,Node 编程环境提供了基于 require() 的模块实现,但没有被 JavaScript 官方采用。直到 ES6,使用了 exportimportimport() 的模块。

基于闭包的模块实现

我们知道函数中声明的变量和函数都只在函数的作用域内,属于函数私有,外部无法影响,这也是闭包的原理。通过闭包的方式,能够将代码的实现细节进行封装,只返回需要暴露给外部的公共 API,从而实现代码的模块化。

下面这部分代码通过闭包的方式实现一个统计模块,只暴露了 mean()stddev() 函数供外部使用,对外隐藏实现细节。

const stats = (function() {
    // 模块私有的辅助函数
    const sum = (x, y) => x + y;
    const square = x => x * x;

    // 要导出的公共 API
    function mean(data) {
        return data.reduce(sum)/data.length;
    }
    function stddev(data) {
        let m = mean(data);
        return Math.sqrt(
            data.map(x => x - m)
                .map(square)
                .reduce(sum)/(data.length-1)
        );
    }

    // 将公共 API 作为一个对象属性导出
    return { mean, stddev }
}())

// 使用 stats 模块,不需要关注实现细节
stats.mean([1, 3, 5, 7, 9])    // => 5
stats.stddev([1, 3, 5, 7, 9])  // => 3.1622776601683795
复制代码

Node 中的模块

Node 中的模块,也叫 CommonJS 模块,基于 CommonJS 规范。使用 exportsmodule.exports 进行模块 API 的导出,使用 require() 进行模块的导入。

导出

Node 中,每个文件就是一个模块,内部定义了一个全局的 Module 对象,以及一个 exports 变量。可以输出看一下:

console.log(module); // 试试看
console.log(module.exports);   // {}
console.log(exports === module.exports);  // true
复制代码

编写一个 stats 模块,通过 exports 导出。

// stats.js
const sum = (x, y) => x + y;
const square = x => x * x;

exports.mean = data => data.reduce(sum)/data.length;
exports.stddev = function(d) {
    let m = exports.mean(d);
    return Math.sqrt(d.map(x => x - m).map(square).reduce(sum)/(d.length-1));
};
复制代码

也可以通过 module.exports 导出。

module.exports = {
    mean,
    stddev
}
复制代码

导入

Node 使用 require() 方法进行模块的导入。如果是内置的模块或者是已安装的模块,直接通过名字导入,如果是自己代码的模块,则通过文件相对路径导入。

// index.js
const fs = require('fs'); // 内置的文件系统模块
const express = require('express'); // 已安装的模块
const stats = require('./stats.js'); // 项目代码模块
复制代码

输出当前文件下的 module 看看。如果在前文的时候你有输出过 module,那你现在会发现 children 数组里多了几个 module 对象。required()方法就是将导入的模块存到了当前module对象的children数组下。

其基本工作原理跟下面这段代码类似:

const modules = {};
function require(moduleName) { return modules[moduleName]; }

modules['stats.js'] = (function() {
    const exports = {};
    
    // stats.js 文件的内容
    const sum = (x, y) => x + y;
    const square = x => x * x;
    exports.mean = function(data) {...};
    exports.stddev = function(d) {...};

    return exports;
}());
复制代码

ES6 中的模块

ES6JavaScript 添加了 importexport 关键字,用于模块的导入和导出。

导出

要从 ES6 模块导出常量、变量、函数或类,使用 export 关键字即可。

export const PI = Math.PI;
export function degreesToRadians(d) { return d * PI / 180; }

// 或者导出花括号,注意,这不是对象
export { PI, degreesToRadians }

// 默认导出
export {
    mean,
    stddev
}
复制代码

export 只对有名字的声明有效,使用 export default 的默认导出可以导出任意表达式。他们两个一般不混用,但没有禁止这种使用方式。

导入

导入模块使用 import 关键字实现。

// 导入默认导出,需要给它命名
import stats from './stats.js'

// 非默认导出有名字,只能通过名字引用它们
import { PI, degressToRadians } from './stats.js';
// 可以改名字
import { PI as pi } from './stats.js';
// 也可以都赋值到一个对象上
import * as stats from './stats.js';

// 如果既有默认导出,也有非默认导出,可以这样引入
import stats, { PI, degressToRadians } from './stats.js';

// 导入没有任何导出的模块
import './analytics.js';
复制代码

导入没有任何导出的模块,该模块的代码会在首次导入时运行一次,之后导入就什么也不做。用于一些特殊情况。(比如希望执行该段代码但是不需要用到导出值)。

此外,ES2020 引入了动态导入 import()。传给import() 一个模块标识符,会返回一个 promise,如下所示:

import('./stats.js').then(stats => {
    let average = stats.mean(data);
})

// 或者使用 async 函数
async analyzeData(data) {
    let stats = await import('./stats.js');
    return {
        average: stats.mean(data),
        stddev: stats.stddev(data)
    }
}
复制代码

需要注意的是,import()看起来像函数调用,但它其实是一个操作符,圆括号表示操作符语法必须的部分。

Node 模块与 ES6 模块的对比

分别了解了 Node 模块跟 ES6 模块以后,我们对它们进行更加深入的分析与对比。通过上文知道它们的语法是不一样的,使用的 API 不同。

Node 模块导出是一个对象,而 ES6 模块是多个 API 导出。

Node 模块是动态导入,可以写在任何地方。ES6 模块是静态导入,只能写在顶层(import 会被变量提升)。

ES6 模块使用 import 被导入的变量是只读的,不能被赋值,其次被导入的变量是引用传递,与原变量是绑定的,如果被改变了,其它导入该模块的地方也会受到影响。而 Node 模块导入的是值的浅拷贝。

相关资料

《JavaScript 权威指南》

require() 源码解读

深入 CommonJs 与 ES6 Module

CommonJS

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改