ES6学习笔记12:模块

ES6模块介绍

ES6在引入了原生模块功能,最大的区别在于以往的CommonJS,AMD等不同,原生模块使得JS引擎在预编译阶段就能完成模块的引入,并能通过Tree Shaking加载指定功能的代码段,大大提升了效率并降低了内存的浪费;此外,类似于CommonJS等方式还需要注意引入时产生的副本问题,ES6原生模块引入的只是模块导出的视图,不存在副本问题。

参考2参考3对JavaScript的模块给了非常详尽的说明,且这两篇文章在很多中文论坛也有各种版本的翻译。本文的主要内容并不不是对其进行直接翻译,而是对其核心知识点进行总结。

使用闭包和匿名函数自调用函数构建命名空间

在介绍模块之前,我们需要先了解一下JS中的命名空间,这是浏览器端解决不同模块变量名污染的最常用方法。而闭包和匿名函数自调用是一切的基础。

闭包

来看一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
// 外层函数
function addX(x){
// 返回内存函数,也可返回一般对象等
return function(y) {
return x + y;
}
}
let add2 = addX(2);
let add5 = addX(5);
console.log(add2(10)); // 12
console.log(add5(10)); // 15

JS闭包的本质就是通过返回一个能访问外层函数作用域的对象(包括函数),使得GC无法回收其内存,并得到一个相对独立的关闭作用域空间。

在上例中,addX()函数接收了一个参数x,x本不能在外部被访问到(实际上也确实不能被访问到),但是我们通过暴露(返回)一个能访问x的函数对象,使得外部能够通过该函数对象访问到x。这个就是闭包。

立即调用函数表达式(IIFE)

再来看如下例子。

1
2
3
4
5
6
let str = 'hello, world';
(function() {
let msg = 'hello, private world';
console.log(str);
console.log(msg);
}())

首先我们看到这里有一个匿名函数,其定义后跟了一个括号,表示立刻被调用。注意最外层的括号是一定要的,因为function关键字表示为函数声明(JS中是不能存在匿名函数声明),最外层的括号表示这是一个函数表达式(相关参考)。

上述方法使得我们能在自己的私有空间中写相应逻辑,而不用担心和外部环境产生冲突,因为我们可以在匿名函数内部中定义私有的变量,用时又能访问外部变量。

全局引用

此外,我们还可以传值到匿名函数中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
const $ = {};
(function($) {
const name = 'my private JQuery';
$.sayHello = function() {
console.log(`hello, ${name}`);
}
$.ajax = function() {
// do something.
console.log('This is my JQuery.ajax()');
}
}($))
$.sayHello(); // hello, my private JQuery
$.ajax(); // This is my JQuey.ajax()

很明显我们这里利用匿名函数自调用和闭包的特性,$是上述代码中的唯一全局变量,我们创建了一个属于变量$的独立私有内存空间。

对象接口

我们也可以在匿名函数中返回更复杂的对象。

1
2
3
4
5
6
7
8
9
const $ = (function() {
const name = 'Object Interface';
return {
sayHello() {
console.log(`hello, ${name}`);
}
}
}())
$.sayHello(); // hello, Object Interface

暴露模块模式

当然,我们也可以选择性的暴露一部分函数,区分『私有方法』和『公有方法』。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const $ = (function() {
let name = 'Revealing module pattern';
function getName() {
return name;
}
function sayHello() {
console.log(`hello, ${getName()}`);
}
return {
sayHello
}
}())
$.sayHello(); // hello, Revealing module pattern
$.getName(); // error

JS中的三方模块

上面的所有方法中最大的缺点之一就是如果模块与模块之间有依赖,那必须保证引入的顺序是没有错的。如果你开发过JQuery并同时使用过基于JQuery的一些UI库,那应该有过这样的经历,JQuery必须在那些UI库之前引入,因为UI库需要使用JQuery对象,JQuery对象的定义必须放在UI库文件执行之前。

另一个缺点是不同的模块之间依然存在全局对象冲突,虽然开源模块的开发一般都很注意避免和主流前端框架的全局对象有命名冲突,但有时依然存在这样的问题。比如如果来一个新的库,正好他使用的一个全局对象命名也为$符号,而你恰好有必须使用JQuery时,就会出现这种问题。

于是,为了避免使用全局对象,JS社区出现了两种主流的模块技术,CommonJS和AMD。(本文的核心在于一般JS模块的ES6的模块的介绍,所以并不打算详述这两种方式的使用,感兴趣的读者可以去查阅相关资料,笔者强烈推荐参考2和参考3两篇文章,本文的主要内容也是通过总结该两篇文章而来;另外除了CommonJS和AMD这两种主流模块定义,还有其他模块模块定义)

CommonJS

CommonJS是一个设计和实现声明JS模块的志愿者工作小组,如果你有用过的NodeJS,应该对这种方式不陌生。一个CommonJS模块就是一个可复用的导出JS对象的JS片段,每个模块都有一个自己工作的上下文。其使用module.exports和require()来导出和引入模块。

CommonJS的主要优点是它避免了全局命名空间污染;以及显式声明了模块之间的依赖关系。缺点(严格来说同步并不能算缺点,应视具体情况而定)是CommonJS都是同步的,即模块在JS线程中是一个一个加载的。

AMD

AMD(Asynchronous Module Definition)是另外一种模块技术,从名字就可以知道,它与CommonJS最大的区别就在于AMD是异步的,另一个重要的区别是CommonJS只能导出对象,而AMD可以导出对象,函数,JSON,字符串,等等其他类型。

ES6原生模块

好了,终于到了ES6了,ES6中加入了模块功能相比于之前的模块是更激动人心的。窃以为ES6的模块出现之后,上面的模块方式将逐渐被淘汰。

首先其基本语法类似CommonJS,其基本用法示例如下。

1
2
// constants.js
export NAME = 'es6';
1
2
3
// main.js
import NAME from './constants';
console.log(NAME); // es6

注意Babel可以将ES6的模块编译为CommonJS,AMD等格式,具体请查阅Babel官方。

原生模块和其他模块的区别

ES6的模块兼顾CommonJS的声明式语法和AMD异步加载的优点,同时更好地支持循环依赖,其最大的优点有三个,其一是ES6在预编译阶段完成依赖处理;其二ES6引用的是模块的视图而非副本;其三是Tree Shaking。

一般模块定义(CommonJS和AMD等)每一次引用都会创建一个新的副本,在使用的时候尤其需要注意(在参考3中有相关示例代码,这里不再详述),而ES6的模块引入的只是导出的一个只读视图(Read-Only View),从而不需要小心副本带来的问题;

此外最关键的一个区别是ES6会进行所谓的『Tree Shaking』,即我们引入的代码是经过预编译筛选优化的,不同于一般模块定义,原有模块中我们运行时不需要的代码,即使导出了(只要我们没有使用),也会在预编译阶段晒除掉,最终执行的只有我们需要的代码。

浏览器端构建ES6模块的一般流程

虽然ES6模块有诸多优点,但在浏览器端并没有大范围支持(截止目前主流浏览器中只有最新的微软Edge支持),而且市场占有率还无法完全忽视的很多旧版浏览器就更不用说了。

为了在前端开发中也能利用ES6模块,需要一些工具的帮助,以下是一般做法:

为了在前端开发中也能利用ES6模块,需要一些工具的帮助,以下是一般做法:

  1. 使用编译器(如BabelJS)讲ES6模块编译成ES5的CommonJS,AMD等模块定义,然后使用诸如Browserify或者Webpack等打包工具创建一个或多个打包文件。这种方式并不影响开发过程中使用ES6模块。
  2. 使用RollupJS,与方法一类似,但是在打包前它也有一个Tree Shaking过程,会优化你的代码,使打包后的文件最小化。需要注意的是有些特定的模块定义格式需要其他辅助工具帮助(如Browserify,WebPack,RequireJS等)。

模块加载器

ES6中还有一个模块加载器(Module Loader),由于一些原因,这个还不能算作ES6的标准,如果想利用Babel实现Polyfill的话必须引入格外的扩展模块。

知识点总结

  1. 熟练使用匿名闭包构建匿名空间;
  2. 熟练使用ES6模块;
  3. 了解ES6模块和其他模块的区别;
  4. 了解浏览器端构建ES6模块的一般流程。

参考

  1. BabelJS - Learn ES2015
  2. JavaScript Modules: A Beginner’s Guide - Preethi Kasireddy
  3. JavaScript Modules Part 2: Module Bundling - Preethi Kasireddy
  4. Explain JavaScript’s encapsulated anonymous function syntax - StackOverflow
  5. RollUp.JS
  6. ModuleLoader/es-module-loader - GitHub