2023前端面试:CommonJS、AMD、UMD、ES6模块规范的历史发展进程和区别

338 阅读6分钟

历史原因

很长一段时间,JavaScript 都没有语言级(language-level)模块语法。早期的项目也比较简单也比较小,所以那时是没有“模块化”的思想的。那带来的问题是:

  1. 命名冲突:在全局命名空间下定义的变量容易造成命名冲突,这会导致代码出现错误或者不可预测的行为。
  2. 依赖管理:代码的依赖关系很难清晰地描述,这会导致代码的可维护性和可扩展性变得非常差。
  3. 文件加载:在一个大型的JavaScript应用程序中,可能会有成百上千个文件需要加载,这会导致应用程序的加载时间变得非常长。
  4. 可读性:在没有模块化的情况下,代码的结构和组织很难理解,这会降低代码的可读性和可维护性。

因此,模块化编程规范的出现,可以解决上述问题,使得javaScript应用程序更加易于开发、维护和扩展。

各模块化编程规范的发展

JavaScript模块(2006年)

JavaScript模块的概念早在2006年就被提出来了,但当时没有任何官方的规范。这个时期,开发者们通常使用命名空间来避免全局变量的污染,但这种方式难以管理和维护。

CommonJS规范(2009年)

2009年,Node.js的出现催生了CommonJS规范的诞生。CommonJS规范定义了一个标准的模块格式,使得JavaScript代码可以更好地组织和共享。这个规范包含了模块的定义、引入和导出方式,以及模块的生命周期管理等内容。

在CommonJS规范中,每个模块都被视为一个独立的作用域,它可以通过“require”函数来引入其他模块,并通过“exports”对象来导出自己的内容。这个规范被广泛用于Node.js中,也被其他JavaScript运行时环境所采用。

在 Node.js 环境下,CommonJS 规范是内置的,不需要额外的库,require函数是由 Node.js 实现的。Node.js 中的require函数可以加载本地文件和第三方模块,加载的模块可以通过module.exports` 导出数据。

// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add,
  subtract
};

在另一个模块中,可以使用“require”函数引入该模块并使用其公开的函数和变量,如下所示:

javascript

// app.js
const math = require('./math.js');

console.log(math.add(1, 2)); // 输出 3
console.log(math.subtract(3, 2)); // 输出 1

这样,就可以将代码分解为多个模块,并将这些模块导入到一个主文件中。这种模块化的方式使得代码更易于维护和扩展,同时避免了全局变量的命名冲突。

AMD规范(2011年)

AMD(Asynchronous Module Definition)规范是一种用于浏览器端的模块规范。它与CommonJS规范不同的是,它支持异步加载模块,避免了阻塞浏览器的问题。

在AMD规范中,模块的定义和导入使用的是“define”函数,这个函数可以异步地加载模块,并在模块加载完成后执行回调函数。AMD规范被广泛用于浏览器端的JavaScript模块化开发。

// 定义dog.js
define(function () {
console.log("狗狗")
return {
say: () => "我是一条狗"
}
})
// 定义main.js
define(function(require) {
var dog = require('./dog')
console.log(dog.say())
})

// index.html引用
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>

<script>
// main.js在当前目录
requirejs(["main"])
</script>

在使用模块时,需要使用 require 函数来异步加载模块,并在回调函数中使用模块。例如:

在浏览器环境下,通常使用 AMD 规范或 ES6 规范进行模块化开发。浏览器中的 require 函数需要通过第三方库实现,如 RequireJS 就是一个比较流行的实现 AMD 规范的库。

在浏览器中使用 AMD 规范时,需要引入 RequireJS 这个库,RequireJS 可以在浏览器中实现 AMD 规范。

UMD规范 (2012年)

UMD规范是一种用于编写通用模块的规范,它可以兼容CommonJS、AMD和全局变量等多种模块化方案。 下面是一个使用UMD规范的示例:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    // 方法定义
    function myFunc() {};

    // 暴露接口
    return myFunc;
}));

这个示例使用了一个自执行函数,这个函数首先检查当前环境支持哪种模块化方案,然后根据情况将模块公开。在函数的最后,返回了要暴露的函数。

总之,UMD规范为JavaScript模块化提供了更多的灵活性和兼容性,使得开发者可以在不同的环境中使用相同的代码。

ES6模块规范(2015年)

ES6(ECMAScript 2015)是JavaScript的一个重要版本,它引入了许多新特性,其中包括了一种新的模块规范。ES6模块规范定义了一种新的模块格式,它可以在浏览器和Node.js中使用。

ES Module是一种在语言层面上支持的模块化规范,也是ES6(ECMAScript 2015)规范中新增的一项功能。ES Module的主要目的是让JavaScript在不同的环境中实现更加高效、可靠和安全的模块化。

ES Module的语法使用了import和export关键字来定义和导出模块,示例如下:

javascript

// 模块定义
// 定义一个默认导出
export default function() {
  console.log('Hello from module!');
}

// 定义命名导出
export function foo() {
  console.log('Hello from foo!');
}
export function bar() {
  console.log('Hello from bar!');
}

// 模块导入
import myModule, { foo, bar } from './my-module.js';

myModule(); // 输出 "Hello from module!"
foo(); // 输出 "Hello from foo!"
bar(); // 输出 "Hello from bar!"

在上面的示例中,我们定义了一个默认导出和两个命名导出,并通过import语句导入了模块,并分别调用了其中的函数。

ES Module相较于CommonJS和AMD等模块化规范具有以下优势

  1. 编译时静态分析:ES Module在编译时可以进行静态分析,这意味着可以在代码执行前检查模块依赖关系,从而提高代码的性能和可靠性。
  2. 单独的命名空间:ES Module中每个模块都有自己独立的命名空间,避免了命名冲突和全局变量污染的问题。
  3. 非循环依赖:ES Module支持非循环依赖,避免了CommonJS中存在的循环依赖问题。

需要注意的是,ES6是JavaScript语言的一个版本,而ES Module是其中的一项功能,因此它们之间有密切的关系。在ES6中引入了ES Module规范,使得JavaScript在语言层面上支持模块化,使得开发者可以更加方便地管理和组织代码。

模块化时代,最受欢迎的webpack

webpack和requirejs不一样的地方是,webpack在编译时确定了入口文件及文件的依赖关系,通过解析文件按文件依赖关系进行打包,webpack可以把es6的模块化代码,打包成AMD,UMD,可以直接查看https://webpack.js.org/configuration/output/#outputlibrarytarget

module.exports = {
  mode: 'development',
  entry: './example/entry.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    library: 'MyLibrary',
    libraryTarget: 'amd'   //commonjs,commonjs2, window,amd, global,
  }
};