返回首页 深度解析 ECMAScript 6

模块

回到 2007 年,那时我开始在 Mozilla 的 JavaScript 团队工作,搞笑的是一个典型的 Javascript 程序长度是一行。

这是谷歌地图发布后两年。在那不久之前,JavaScript 的主要用途是表单验证。的的确确,你的大部分<input onchange= >处理程序将会是……一行代码。

后来事情发生了改变。JavaScript 程序发展到了令人瞠目结舌的规模,社会也大力发展了与之相关的工作。你最需要的基本东西之一是一个模块系统,用来在多文件和多目录之间传递你的工作的方式——但仍要确定你所有的代码可以再需要的时候互相访问——同时能够高效地加载所有代码。所以自然的,JavaScript 有了一个模块系统,确切的说是几个。还有一些软件包管理器,用于安装所有软件和处理高级别依赖的工具。你可能会觉得拥有新模块语法的 ES6 出现的有些晚了。

好的,今天我们来看看 ES6 是否在现存系统上添加了东西,还有是否会在之上建立未来的标准和工具。但是首先,我们只是像潜水一样来看看 ES6 模块是什么样的。

模块基础

一个 ES6 模块是一个包含了 JS 代码的文件。没有特殊的模块关键词;一个模块读取起来最多只像一个脚本。有两个不同。

  • ES6 模块会自动严格模式代码,尽管你没有在其中写“use strict”。

  • 你可以使用导入和导出模块。

我们先来谈谈导出。在默认情况下,模块内声明的一切都是在本地对该模块而言。如果你想让模块中声明的东西成为公开的,那么其他模块就可以使用它,你必须导出该功能。这里有几种方式,最简单的方式是添加导出关键词。

// kittydar.js - Find the locations of all the cats in an image.
// (Heather Arthur wrote this library for real)
// (but she didn't use modules, because it was 2013)
export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}
export class Kittydar {
  ... several methods doing image processing ...
}
// This helper function isn't exported.
function resizeCanvas() {
  ...
}
...

你可以导出任何顶层函数,类,变量,let 或 const。

这就是你编写一个模块所需要知道的所有!你不必把所有都放到一个 IIFE 或回调上。只需继续下去并声明你所需要的所有事情。由于代码是一个模块而不是一个脚本,所有声明都限于该模块内,而不是在所有脚本和模块中全局可见。导出组成模块的公共 API 的声明,就大功告成了。

除了导出,模块内的代码只是非常简单的正常代码。它可以像 Object 和 Array 一样使用全局变量。如果你的模块在网络浏览器中运行,它便可以使用文件和 XMLHttpRequest。

在一个单独的文件中,我们可以导入和使用 detectCats() 函数:

// demo.js - Kittydar demo program
import {detectCats} from "kittydar.js";
function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}

当你运行一个包含导入声明的模块时,导入的模块首先加载,然后每个模块主体在一个深度优先遍历的依赖图内执行,通过跳过已经执行的任何东西来避免循环。

这些都是模块的基础知识。这真的很简单。;-)

导出列表

你可以写一个单独的列表来列出所有你想要导出的名字,外面加上大括号。

export {detectCats, Kittydar};
// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

输出列表并不一定是文件中的第一件事,它可以出现在一个模块文件的顶层作用域内的任何地方。你可以有多个导出列表,或将导出列表与其他导出声明混合起来,只要没有名字被导出超过一次。

重命名导入和导出

有时,导入的名字会和其他一些你同样需要用的名字发生冲突。所以,ES6 可以让你在导入它们的时候进行重命名。

// suburbia.js
// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...

同样,当你导出他们的时候也可以进行重命名。如果在偶然情况下你想让两个不同的名字导出相同的值,这是非常方便的。

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)
function v1() { ... }
function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

默认导出

新标准旨在实现现有 CommonJS 和 AMD 模块的互操作。因此,假设你有一个 Node 项目并且你已经完成了 npm 安装 lodash。你的 ES6 代码可以从 Lodash 导入单个函数。

import {each, map} from "lodash";

each([3, 2, 1], x => console.log(x));

但是也许你已经习惯了看到_.each 而不是 each 并且你还是想用原来的方式编写程序。或者你想用_作为一个函数,因为它在 Lodash 中是有效的

对于这一点,你可以用一个稍微不同的语法:导入没有花括号的模块。

从“lodash”导入;

import _ from "lodash";

Shorthand 等同于从“lodash”导入{default as _}。所有的 CommonJS 和 AMD 模块都呈现为 ES6 有一个 default 导出,如果你为该模块调用了require() 你会得到同样的东西——那便是导出对象。

ES6 可以让你导出多件事情,但对于现有 CommonJS 模块,默认导出的就是你得到的。比如,截至本文发稿,著名的色彩包装没有任何的特别 ES6 支持。它是一个 CommonJS 模块的集合,就像大部分 npm 的集合。但是你可以把它导入到你的 ES6 代码。

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";

如果你想让你的 ES6 模块有一个默认的导出,那是非常容易的。默认导出并没有什么神奇的,它就像其它任何一个导出,除非它被命名为 “default”。就像我们之前谈过的一样,你可以使用重命名语法。

let myObject = {
  field1: value1,
  field2: value2
};
export {myObject as default};

关键字导出默认值后可以跟任何值:函数,类,对象文本,无论什么。

模块对象

很抱歉,这一期让大家久等了。但是 JavaScript 并不孤单:出于某种原因,所有语言的模块系统往往分别有很多又小又无聊的便利功能。幸运的是,只剩下一件事了。呃,两件。

import * as cows from "cows";

从”cows”导入*作为cows; 当你输入*,导入的是一个模块命名空间对象。它的属性是模块的导出。因此,如果”cows”模块导出了一个名为moo()的函数,然后用这种方式导入”cows”之后,你可以写:cows.moo()

聚集模块

有时,一个包的主模块要多于导入所有包的其他模块然后以统一方式导出它们。为了简化这种代码,还有一个集全功能于一身的导入与导出 shorthand:

// world-foods.js - good stuff from all over
// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";
// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";
// import "singapore" and export ALL of its exports
export * from "singapore";

这些导出语句中的每一个都与一个后跟一导出的导入语句类似。不像真的导入,这没有给你的作用域添加再导出绑定。所以如果你打算写一些使用了 Tea 的 world-foods.js 代码,不要用这个 shortland。你会发现它并不存在。

如果”singapore”导出的任何命名与其他导出发生了冲突,就会成为一个错误,所以谨慎使用 export*

呼!我们的语法部分完成了!到了有趣的部分。

import 实际上是用来做什么的?

你会相信…它什么都没做吗?

哦,你没有那么轻易上当。你相信该标准几乎没有说 import 是用来做什么的吗?这会是一件好事吗?

ES6 把模块加载的细节全部交给了implementation。模块执行的其余部分都有详述

大致来说,当你让 JS 引擎运行一个模块时,它会像在执行这四个步骤:

1、解析:implementation 读取模块的源代码,检查语法错误。

2、加载:加载所有导入的模块(递归)。这是还未标准化的部分。

3、连接:对于每个新加载的模块,implementation 都会创建一个模块作用域,并用该模块中的所有绑定声明填充,包括从其他模块导入的东西。

这是当你想从“paleo”导入{cake}的一部分,但是“paleo”并没有真的导出任何名为 cake 的东西,你会得到一个错误。这太糟糕了,因为你与真正运行一些 JS 代码近在咫尺,运行成功后就会得到 cake!

4、运行时间:最后,implementation 执行各新加载模块的语句。此时,导入处理已经近似完成了,所以当执行到有一个导入声明的一行代码时…什么都没发生!

看到了吗?我说的是“什么都没发生”。对于编程语言,我从不撒谎。

但是现在我们到了这个系统的有趣部分。有一个很酷的把戏。因为系统并没有指定 loading 是如何工作的,而且你可以提前查看源代码的导入声明来找出所有的依赖,ES6 的 implementation 在编译时免费做了所有的工作,它把你所有的模块集成到一个单一文件里来把它们传送到网络上!像WebPACK一样的工具实际上都是这样做的。

这是一个大问题,因为在网页上加载脚本需要时间,而且每当你获取一个,你可能会发现它包含着需要你加载几十个的导入声明。一个简单的加载会需要很多的网络往返。但是有了 WebPACK,你不仅可以在 ES6 中使用模块,还会获取到所有的软件工程效益而没有运行时的性能损失。

一个 ES6 模块加载的详细说明在初步准备中——现在已经完成了。它不是最终标准的一个原因是,在如何实现捆绑功能上没有达成共识。我希望会有人可以解决它,因为就像我们会看到的,模块加载真的应该被标准化。捆绑放弃真是再好不过了。

静态与动态,或:规则,以及如何打破它们

对于动态语言,JavaScript 已经有了一个令人惊讶的静态模块系统。

  • 所有的 import 和 export 都只允许存在于顶层模块中。导入和导出没有条件,但是你不能在函数作用域内使用 import。

  • 所有导出的标识必须在源代码中有明确的命名。你不能以编程方式循环一个数组,也不能以数据驱动的方式导出一堆名字。

  • 模块对象被冻结。我们无法以 polyfill 方式破解一个新功能到一个模块对象。

  • 模块的所有依赖必须在任何模块代码运行之前加载、解析和连接。于import而言,没有可以延迟加载的语法。

  • 导入错误没有错误恢复。一个应用程序可能有数百个模块,如果任何一个组成部分没有加载或连接成功,整个程序都不会运行。你不能在一个 try/catch 块中导入。(这里的好处是,因为系统是静态的,所以 WebPACK 在你编译的时候可以检测到这些错误。)

  • 在依赖加载之前不允许模块运行任何代码。这代表着模块无法控制他们的依赖是怎样加载的。

只要你的需求是静态的,系统是相当不错的。但是你可以想象一下有时你需要一个小黑客,对吗?

那就是为什么你用的任何模块加载系统都有一个编程 API,并依靠 ES6 的静态 import/export 语法。例如,WebPACK 包含了一个你可以用来进行“代码分离”的API,在需求下缓慢加载一些模块捆绑。相同的 API 可以帮助你打破上述的大部分其他规则。

ES6 模块语法是非常静态的,这样非常好——它以提供强大的编译工具的形式进行了回报。但是设计静态语法是为了让它辅助一个有着丰富动态的、程序化的装载器 API。

什么时候可以使用 ES6 模块?

如果现在你要使用模块,你需要一个像TraceurBabel这样的编译器。在本系列的早些时候,加斯顿·席尔瓦展示了如何使用 Babel 和 Broccolito为网页编译 ES6 代码。在那篇文章的基础上,加斯顿有一个与 ES6 模块相关的工作实例。阿克塞尔·劳施迈耶进行了讲述,讲述中还包括一个使用 Babel 和 WebPACK 的例子。

ES6 模块系统主要由戴夫·赫尔曼和山姆·托宾·霍克施塔特设计,他们在多年的公开辩论中始终站在所有来者(包括我)的对立面为系统的静态部分而辩护。乔恩·考皮尔德正在在火狐浏览器中推行模块。JavaScript 程序标准的额外工作正在进行中。给 HTML 添加像 <script type=module之类东西的工作有望跟进。

这就是 ES6。

这是如此有趣,我不希望它结束。也许我们可以再做一集。我们可以讨论在 ES6 说明中的还没有足够大到拥有单独介绍文章的零碎的东西。也许谈一点关于未来要把握的东西。下面,请聆听 ES6 的出色结论。

上一篇: let 和 const 下一篇: 未来前景