为什么要有模块化

早期的网站一般只是用来展示一些静态的内容,只有 HTML 和 CSS。后来为了增强用户体验,使用户可以与网页做一些简单交互,NetScape 创建了 JavaScript 这门脚本语言,使用它可以动态修改 html。

后来,Google 发明了 Ajax 技术,这大大促进了 JavaScript 在编写网页时所占的比重,人们清楚的看到了 JavaScript 的威力,于是,JavaScript 开始在 WEB 前端中发挥越来越重要的作用,前端程序员需要维护的 JavaScript 代码也越来越多,越来越复杂。

现在,一个项目的前端代码往往是由多个人协作开发的。项目组长搭好整体结构,写好一些通用的、全局的内容,然后将需要实现的功能分配给各个组员,组员们写好代码后,项目组长再统一将 JS 代码引入到 HTML 中。这样就会出现一个问题:因为所有的 JS 代码共用一个作用域,所以很容易导致命名冲突(两个组员可能会使用同一个变量名)。

所以,“前端模块化” 的需求就诞生了。

ES5 模块化

ES5 中还没有专门用来模块化的语法,所以得自己想办法实现模块化。

利用函数作用域防止命名冲突

下面是一个 JavaScript 文件的简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.js
;(function() { // 前面的分号是为了防止其他文件的干扰
var a = 1;
var b = 2;
function sum(num1, num2) {
return num1 + num2;
}
var flag = true;

if(flag) {
console.log(sum(a, b));
}
})() // 在定义函数的同时调用函数

将所有操作都定义在匿名函数内部,然后调用该函数,这样,所有的变量都定义在函数的局部作用域中,对全局作用域不会有影响。

这样做解决了命名冲突问题,但如果想在其他 JS 文件中调用该文件定义的变量或函数就不行了,所以还需要再加工一下才能真正实现模块化。

ES5 实现模块化的方法

将上面的例子加工一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// a.js
var moduleA = (function() {
var obj = {} // 定义一个空对象

var a = 1;
var b = 2;
function sum(num1, num2) {
return num1 + num2;
}
var flag = true;

if(flag) {
console.log(sum(a, b));
}

obj.sum = sum; // 将需要导出的变量全部赋值给 obj 的属性
obj.flag = flag;

return obj; // 返回 obj
})()

用一个变量(moduleA)接收该函数的返回值,这样就可以在全局作用域中通过 moduleA.xxx 的方式拿到我们需要的变量或函数了。

以上是 ES5 中模块化的最基本的封装,事实上关于模块化还有很多高级的话题。

ES6 模块化规范

模块化的核心:导入导出。

ES6 中原生支持了模块的导入导出。

ES6 中模块的导入导出

举例说明:

  1. 首先,在引入外部 JS 文件时,需要给 script 标签加上一个 type 属性:

    1
    2
    3
    <!-- index.html -->
    <script src="a.js" type="module"></script>
    <script src="b.js" type="module"></script>

    这样,每个 JS 文件都会被当做一个模块,不同模块中的相同变量名不会再发生冲突。

  2. 导出需要共享的变量或函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // a.js
    let a = 1;
    let b = 2;
    function sum(num1, num2) {
    return num1 + num2;
    }
    let flag = true;

    // 导出方式一:
    export {
    sum, flag
    }

    // 导出方式二:
    export sum;
    export flag;
    export let s = "hello world"; // 也可以在定义的同时导出
    export function add(num1, num2) {
    return num1 + num2;
    }
  3. 在需要的地方导入并使用:

    1
    2
    3
    4
    // b.js
    import {sum, flag} from "./a.js"; // import 后是对象的解包语法;from 后跟相对路径

    console.log(sum(1, 2));

export default

1
2
3
4
5
6
7
8
9
// a.js
let a = 1;
let b = 2;
function sum(num1, num2) {
return num1 + num2;
}
let flag = true;

export default sum;
1
2
// b.js
import add from "./a.js";

使用这种方式导出时:

  1. 导入不再需要写大括号。
  2. 名字可以任意起,如上例中导出的是 sum,但导入时重命名为 add。
  3. 一个文件中只能有一个 export default

其他模块化规范

现在比较常见的模块化规范还有 CommonJS、AMD、CMD。注意:JavaScript 不原生支持这些模块化规范,它们只能在特定的场景中使用。

CommonJS

NodeJS 中的模块化就是按照 CommonJS 规范实现的。

导出:

1
2
3
4
5
6
7
8
9
10
11
12
// a.js
let a = 1;
let b = 2;
function sum(num1, num2) {
return num1 + num2;
}
let flag = true;

module.exports = {
sum,
flag
}

导入:

1
2
3
4
5
6
7
// b.js
let moduleA = require("./a.js");
console.log(moduleA.sum(1, 2));

// 也可以在导入时直接解包:
let {sum, flag} = require("./a.js");
console.log(sum(1, 2));