ES6 的模块系统

此文为翻译,原文地址在这儿:https://hacks.mozilla.org/2015/08/es6-in-depth-modules/【转】javascript

ES6 是 ECMAScript 第 6 版本的简称,这是新一代的 JavaScript 的标准。ES6 in Depth 是关于 ES6 的一系列新特性的介绍。html

遥想 2007 年,笔者开始在 Mozilla 的 JavaScript 团队工做的时候,那个时候典型的 JavaScript 程序只有一行代码。java

两年以后, Google Map 被发布。可是在那以前不久,JavaScript 的主要用途仍是表单验证,固然啦,你的<input onchange=>处理器平均来讲只有一行。webpack

事过情迁,JavaScript 项目已经变得十分庞大,社区也发展出了一些有助于开发可扩展程序的工具。首先你须要的即是模块系统。模块系统让你得以将你的工做分散在不一样的文件和目录中,让它们以前得以互相访问,而且能够很是有效地加载它们。天然而然地,JavaScript 发展出了模块系统,事实上是多个模块系统(AMD,CommonJS,CMD,译者注)。不只如此,社区还提供了包管理工具(NPM,译者注),让你能够安装和拷贝高度依赖其余模块的软件。也许你会以为,带有模块特性的 ES6,来得有些晚了。git

模块基础

一个 ES6 的模块是一个包含了 JS 代码的文件。ES6 里没有所谓的 module 关键字。一个模块看起来就和一个普通的脚本文件同样,除了如下两个区别:es6

  • ES6 的模块自动开启严格模式,即便你没有写 'use strict'github

  • 你能够在模块中使用 import 和 exportweb

让咱们先来看看 export。在模块中声明的任何东西都是默认私有的,若是你想对其余模块 Public,你必须 export 那部分代码。咱们有几种实现方法,最简单的方式是添加一个 export 关键字。npm

// 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() { ... } ...

你能够在 functionclassvarlet 或 const 前添加 export编程

若是你想写一个模块,有这些就够了!不再用把代码放在 IIFE 或者一个回调函数里了。既然你的代码是一个模块,而非脚本文件,那么你生命的一切都会被封装进模块的做用域,再也不会有跨模块或跨文件的全局变量。你导出的声明部分则会成为这个模块的 Public API。

除此以外,模块里的代码和普通代码没啥大区别。它能够访问一些基本的全局变量,好比 Object 和 Array。若是你的模块跑在浏览器里,它将能够访问 document 和 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); }

要导入多个模块中的接口,你能够这样写:

import {detectCats, Kittydar} from "kittydar.js";

当你运行一个包含 import 声明的模块,被引入的模块会先被导入并加载,而后根据依赖关系,每个模块的内容会使用深度优先的原则进行遍历。跳过已经执行过的模块,以此避免依赖循环。

这即是模块的基础部分,挺简单的。

导出表

若是你以为在每一个要导出的部分前都写上 export 很麻烦,你能够只写一行你想要导出的变量列表,再用花括号包起来。

export {detectCats, Kittydar};

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

导出表不必定要出如今文件的第一行,它能够出如今模块顶级做用域中的任何一行。你能够写多个导出表,也能够在列表中再写上其余 export 声明,只要没有变量名被重复导出便可。

重名命导出和导入

若是导入的变量名刚好和你模块中的变量名冲突了,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 install lodash,你的 ES6 代码能够独立引入 Lodash 中的函数:

import {each, map} from "lodash"; each([3, 2, 1], x => console.log(x));

然而若是你已经习惯了 _.each 或者看不见 _ 的话就浑身难受,固然这样使用 Lodash 也是不错的方式

这种状况下,你能够稍微改变一下你的 import 写法,不写花括号:

import _ from "lodash";

这个简写等价于 import {default as _} from "lodash";。全部 CommonJS 和 AMD 模块在被 ES6 代码使用的时候都已经有了默认的导出,这个导出和你在 CommonJS 中 require() 获得的东西是同样的,那就是 exports 对象。

ES6 的模块系统被设计成让你能够一次性引入多个变量。但对于已经存在的 CommonJS 模块来讲,你能获得的只有默认导出。举个栗子,在撰写此文之时,据笔者所知,著名的 colors 模块并未特地支持 ES6。这是一个由多个 CommonJS 模块组成的模块,正如 npm 上的那些包。然而你依然能够直接将其引入到你的 ES6 代码中。

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

若是你想写本身的默认导出,那也很简单。这里面并无什么高科技,它和普通的导出没什么两样,除了它的导出名是 default。你可使用咱们以前已经介绍过的语法:

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

这样更好:

export default { field1: value1, field2: value2 };

export default 关键字后能够跟随任何值:函数,对象,对象字面量,任何你能说得出的东西。

模块对象

抱歉,这篇文章的内容有点多,但 JavaScript 已经算好的了:由于一些缘由,全部语言的模块系统都有一大堆没什么卵用的特性。所幸的是,我们只有一个话题要讨论了,呃,好吧,两个。

import * as cows from "cows";

当你 import *,被引入进来的是一个 module namespace object。它的属性是那个模块的导出,因此若是 “cows” 模块导出了一个名为 moo() 的函数,当你像这样引入了 “cows” 以后,你能够这样写 cows.moo()

聚合模块

有时候一个包的主模块会引入许多其余模块,而后再将它们以一个统一的方式导出。为了简化这样的代码,咱们有一个 import-and-export 的简写方法:

// 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";

这种 export-from 的表达式和后面跟了一个 export 的 import-from 表达式相似。但和真正的导入不一样,它并不会在你的做用域中加入二次导出的变量绑定。因此若是你打算在 world-foods.js 写用到了 Tea 的代码,就别使用这个简写形式。

若是 "singapore" 导出的某一个变量恰巧和其余的导出变量名冲突了,那么这里就会出现一个错误。因此你应该谨慎使用 export *

Whew!咱们介绍完语法了,接下来进入有趣的环节。

import 到底干了啥

啥也没干,信不信由你。

噢,你好像看起来没那么好骗。好吧,那你相信标准几乎没有谈到 import 该作什么吗?你认为这是一件好事仍是坏事呢?

ES6 将模块的加载细节彻底交给了实现,其他的执行部分则规定得很是详细

大体来讲,当 JS 引擎运行一个模块的时候,它的行为大体可概括为如下四步:

  1. 解析:引擎实现会阅读模块的源码,而且检查是否有语法错误。

  2. 加载:引擎实现会(递归地)加载全部被引入的模块。这部分咱还没标准化。

  3. 连接:引擎实现会为每一个新加载的模块建立一个做用域,而且将模块中的声明绑定填入其中,包括从其余模块中引入的。

当你尝试 import {cake} from "paleo" 可是 “paleo” 模块并无导出叫 cake 的东西时候,你也会在此时获得错误。这很糟糕,由于你离执行 JS,品尝 cake 只差一步了!

  1. 执行:终于,JS 引擎开始执行刚加载进来的模块中的代码。到这个时候,import 的处理过程已经完成,所以当 JS 引擎执行到一行 import 声明的时候,它啥也不会干。

看到了不?我说了 import “啥也没干”,没骗你吧?有关编程语言的严肃话题,哥从不说谎。

不过,如今我们能够介绍这个体系中有趣的部分了,这是一个很是酷的 trick。正由于这个体系并无指定加载的细节,也由于你只须要看一眼源码中的 import 声明就能够在运行前搞清楚模块的依赖,某些 ES6 的实现甚至能够经过预处理就完成全部的工做,而后将模块所有打包成一个文件,最后经过网络分发。像 webpack 这样的工具就是作这个事情的。

这很是的了不得,由于经过网络加载资源是很是耗时的。假设你请求一个资源,接着发现里面有 import 声明,而后你又得请求更多的资源,这又会耗费更多的时间。一个 naive 的 loader 实现可能会发起许屡次网络请求。但有了 webpack,你不只能够在今天就开始使用 ES6,还能够获得一切模块化的好处而且不向运行时性能妥协。

原先咱们计划过一个详细定义的 ES6 模块加载规范,并且咱们作出来了。它没有成为最终标准的缘由之一是它没法与打包这一特性调和。模块系统须要被标准化,打包也不该该被放弃,由于它太好了。

动态 VS 静态,或者说:规矩和如何打破规矩

做为一门动态编程语言,JavaScript 使人惊讶地拥有一个静态的模块系统。

  • import 和 export 只能写在顶级做用域中。你没法在条件语句中使用引入和导出,你也不能在你写的函数做用域中使用import

  • 全部的导出必须显示地指定一个变量名,你也没法经过一个循环动态地引入一堆变量。

  • 模块对象被封装起来了,咱们没法经过 polyfill 去 hack 一个新 feature。

  • 在模块代码运行以前,全部的模块都必须经历加载,解析,连接的过程。没有能够延迟加载,惰性 import 的语法。

  • 对于 import 错误,你没法在运行时进行 recovery。一个应用可能包含了几百个模块,其中的任何一个加载失败或连接失败,这个应用就不会运行。你没法在 try/catch 语句中 import。(不过正由于 ES6 的模块系统是如此地静态,webpack 能够在预处理时就为你检测出这些错误)。

  • 你没办法 hook 一个模块,而后在它被加载以前运行你的一些代码。这意味着模块没法控制它的依赖是如何被加载的。

只要你的需求都是静态的话,这个模块系统仍是很 nice 的。但你仍是想 hack 一下,是吗?

这就是为啥你使用的模块加载系统可能会提供 API。举个栗子,webpack 有一个 API,容许你 “code splitting”,按照你的需求去惰性加载模块。这个 API 也能帮你打破上面列出的全部规矩。

ES6 的模块是很是静态的,这很好——许多强大的编译器工具所以收益。并且,静态的语法已经被设计成能够和动态的,可编程的 loader API 协同工做。

我什么时候能开始使用 ES6 模块?

若是你今天就要开始使用,你须要诸如 Traceur 和 Babel 这样的预处理工具。这个系列专题以前也有文章介绍了如何使用 Babel 和 Broccoli 去生成可用于 Web 的 ES6 代码。那篇文章的栗子也被开源在了 GitHub 上。笔者的这篇文章也介绍了如何使用 Babel 和 webpack。

ES6 模块系统的主要设计者是 Dave Herman 和 Sam Tobin-Hochstadt,此二人不顾包括笔者在内的数位委员的反对,始终坚持现在你见到的 ES6 模块系统的静态部分,争论长达数年。Jon Coppeard 正在火狐浏览器上实现 ES6 的模块。以后包括 JavaScript Loader 规范在内的工做已经在进行中。HTML 中相似 <script type=module> 这样的东西以后也会和你们见面。

这即是 ES6 了。

欢迎你们对 ES6 进行吐槽,请期待下周 ES6 in Depth 系列的总结文章。

相关文章
相关标签/搜索