此文为翻译,原文地址在这儿: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
和 export
。web
让咱们先来看看 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() { ... } ...
你能够在 function
、class
、var
、let
或 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 引擎运行一个模块的时候,它的行为大体可概括为如下四步:
解析:引擎实现会阅读模块的源码,而且检查是否有语法错误。
加载:引擎实现会(递归地)加载全部被引入的模块。这部分咱还没标准化。
连接:引擎实现会为每一个新加载的模块建立一个做用域,而且将模块中的声明绑定填入其中,包括从其余模块中引入的。
当你尝试 import {cake} from "paleo"
可是 “paleo” 模块并无导出叫 cake
的东西时候,你也会在此时获得错误。这很糟糕,由于你离执行 JS,品尝 cake
只差一步了!
执行:终于,JS 引擎开始执行刚加载进来的模块中的代码。到这个时候,import
的处理过程已经完成,所以当 JS 引擎执行到一行 import
声明的时候,它啥也不会干。
看到了不?我说了 import “啥也没干”,没骗你吧?有关编程语言的严肃话题,哥从不说谎。
不过,如今我们能够介绍这个体系中有趣的部分了,这是一个很是酷的 trick。正由于这个体系并无指定加载的细节,也由于你只须要看一眼源码中的 import
声明就能够在运行前搞清楚模块的依赖,某些 ES6 的实现甚至能够经过预处理就完成全部的工做,而后将模块所有打包成一个文件,最后经过网络分发。像 webpack 这样的工具就是作这个事情的。
这很是的了不得,由于经过网络加载资源是很是耗时的。假设你请求一个资源,接着发现里面有 import
声明,而后你又得请求更多的资源,这又会耗费更多的时间。一个 naive 的 loader 实现可能会发起许屡次网络请求。但有了 webpack,你不只能够在今天就开始使用 ES6,还能够获得一切模块化的好处而且不向运行时性能妥协。
原先咱们计划过一个详细定义的 ES6 模块加载规范,并且咱们作出来了。它没有成为最终标准的缘由之一是它没法与打包这一特性调和。模块系统须要被标准化,打包也不该该被放弃,由于它太好了。
做为一门动态编程语言,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 协同工做。
若是你今天就要开始使用,你须要诸如 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 系列的总结文章。