FED实验室 - 专注WEB端开发和用户体验

ECMAScript 6 modules: the final syntax

ECMAScript 6系列 煦涵 2629℃ 0评论

在2014年7月月底,TC39[1]还有另外一个会议,此次会议,ECMAScript6(ES6)模块语法的最后细节被敲定。本博客文章给出了完整的ES6模块系统的概述。

一、当前Javascript 模块系统

JavaScript没有内置的模块的支持,但社区创造了令人印象深刻的变通。两个最重要的(可惜不兼容)标准如下:
1. CommonJS 模块
该标准在Node.js得到了主要的实现(Node.js的模块有几个功能是超越CommonJS的)。特点:
a) 简洁的语法
b) 异步加载设计
c) 主要使用:server
2. 异步模块定义(AMD)
实现这个标准最流行的是RequireJS,特点:
a) 稍复杂的语法,使AMD工作不需要eval()(或者编译)
b) 异步加载设计
c) 主要使用: browsers
以上只是做了个简单的解释,如果你想更深入的了解,请戳这里:“Writing Modular JavaScript With AMD, CommonJS & ES Harmony” by Addy Osmani。

二、ECMAScript 6 模块

1. ECMAScript 6 的目标是设计一种使AMD和CommonJS用户都满意的格式:
a) 和CommonJS类似,它们有一个简洁的语法,偏好单一exprots,支持循环依赖。
b) 和AMD类似,它们直接支持异步加载模块和可配置模块加载。 被
2. 内置的语言允许ES6模块超越CommonJS的和AMD(详见后述):
a) 它们语法甚至比CommonJS的的更简洁。
b) 它们结构能被静态分析(对于静态检查,优化等)。
c) 它们对循环依赖的支持比CommonJS的更好。
3. ES6模块标准由两部分组成:
a) 声明性语法(导入和导出)
b) 编程式加载API: 按配置加载模块和按条件加载模块

三、 ES6 模块语法概述

有两种导入:可命名export(每个模块可以有多个)和默认export(每个模块一个)。
3.1 export导出(可以多个)
一个模块可以通过前缀的声明以关键字export,导出多个。这些export被它们的名字隔开,被称为“命名export”。看下例:

    //------ lib.js ------
    export const sqrt = Math.sqrt;
    export function square(x) {
        return x * x;
    }
    export function diag(x, y) {
        return sqrt(square(x) + square(y));
    }

    //------ main.js ------
    import { square, diag } from 'lib';
    console.log(square(11)); // 121
    console.log(diag(4, 3)); // 5

也有一些其他的方式来指定命名export(将在后面介绍),但是我们发现这是相当的方便的:简单的写你的代码,然后使用export关键字供外部调用。

如果你愿意,你还可以导入整个模块,并指其为命名的export 变量,如下面这样:

    //------ main.js ------
    import * as lib from 'lib';
    console.log(lib.square(11)); // 121
    console.log(lib.diag(4, 3)); // 5

上面相同的代码我们使用CommonJS语法:有一段时间,我尝试了几种巧妙的策略,使在Node.js中减少代码的冗余度。现在,我更喜欢下面简单但略显冗长的风格,让人想起了revealing module pattern

    //------ lib.js ------
    var sqrt = Math.sqrt;
    function square(x) {
        return x * x;
    }
    function diag(x, y) {
        return sqrt(square(x) + square(y));
    }
    module.exports = {
        sqrt: sqrt,
        square: square,
        diag: diag,
    };

    //------ main.js ------
    var square = require('lib').square;
    var diag = require('lib').diag;
    console.log(square(11)); // 121
    console.log(diag(4, 3)); // 5

3.2 默认export(单个)
在Node.js社区中,仅仅使用模块export单个值是非常流行的。但他们在前端开发也很普遍的,你经常有构造函数/类模型,每个模块一个模型。在ECMAScript中6模块可以选择一个默认的export,最重要的是被导出的值。默认的export特别容易导入。

下面的ECMAScript6模块“is” a single function:

    //------ myFunc.js ------
    export default function () { ... };

    //------ main1.js ------
    import myFunc from 'myFunc';
    myFunc();

一个ES6模块也可以export一个默认的类,像下面这样:

    //------ MyClass.js ------
    export default class { ... };

    //------ main2.js ------
    import MyClass from 'MyClass';
    let inst = new MyClass();

注意:默认的export声明的操作数是一个表达式,它往往不具有名称。相反,它是通过它的模块的名称来识别。
3.3 命名的export 和 默认export在同一个模块中
在Javascript中,下面的模式是很常见的的:一个库是一个单一的function,但是通过该函数的属性可以提供额外服务。例如包括jQuery和Underscore.js。以下是Underscore作为CommonJS的模块的一部分片段:

    //------ underscore.js ------
    var _ = function (obj) {
        ...
    };
    var each = _.each = _.forEach =
        function (obj, iterator, context) {
            ...
        };
    module.exports = _;

    //------ main.js ------
    var _ = require('underscore');
    var each = _.each;
    ...

从ES6的角度来看,函数_是默认的export,而each和forEach是命名的export。事实证明,你其实可以有命名export和一个默认的export在同一时间。作为一个例子,改写CommonJS模块为ES6模块。如下所示:

    //------ underscore.js ------
    export default function (obj) {
        ...
    };
    export function each(obj, iterator, context) {
        ...
    }
    export { each as forEach };

    //------ main.js ------
    import _, { each } from 'underscore';
    ...

需要注意的是CommonJS的版本和ECMAScript的版本6只大致相同。后者具有非嵌套的结构,而前者是嵌套的。可根据自己的喜好来选择不同的风格,但非嵌套结构有被静态分析的优势(为什么好,下面会解释)。在CommonJS的风格可能需要对象的命名空间,而通过ES6模块,可以通过变量的属性来调用。

The default export is just another named export

defalut export实际上只是一个用特殊的名字来命名的默认export,也就是说下面的两句是等价的:

    import { default as foo } from 'lib';
    import foo from 'lib';

同样,以下两个模块具有相同的default export:

    //------ module1.js ------
    export default 123;

    //------ module2.js ------
    const D = 123;
    export { D as default };

为什么我们需要命名的exports?
你可能感到奇怪——如果我们可以简单的使用default export对象(像CommonJS),为什么我们需要named export,答案是,你不能因为想使用静态结构的变量对象而丢掉其他与之相关的优势(将被描述在下一章)。

四、设计目标

如果你想了解ES6模块,它有助于了解什么样的目标影响了它们的设计。主要有:
4.1 default export are favored
该模块的语法提示的default export 模块看起来可能有陌点生,但它是有道理的,如果你认为一个主要的设计目标是使用default export尽可能的方便。引用David Herman
ECMAScript6利于single/default export风格,并给出了最灵活的语法来import默认值。import 命名export应该可以,甚至略简洁。
4.2 静态模块结构
在当前的JavaScript模块系统,你不得不执行代码,以找出什么是imports和exports。这是Es6和当前的Javascript系统不同的主要原因:通过在语言中构建模块系统,你可以同步执行一个静态的模块结构。让我们先看一下这是什么意思,然后它能带来什么好处。
一个模块结构被静态化,意味着你在编译时(静态化)能确定进口和出口- 你只需要看看源代码,您不必执行它。以下两个例子是CommonJS的模块如何不能做的。在第一个例子,你必须运行代码,找出导入了什么:

    var mylib;
    if (Math.random()) {
        mylib = require('foo');
    } else {
        mylib = require('bar');
    }

第二个例子,你必须运行代码,找出导出了什么:

    if (Math.random()) {
        exports.baz = ...;
    }

ES6给了你较少的灵活性,迫使你静态化。结果你得到一些好处[2],下面将要描述:
好处一:更快的查找
在CommonJS中,你require 一个库,你会得到一个返回的对象:

    var lib = require('lib');
    lib.someFunc(); // property lookup

因此,访问lib.someFun(),你必须做一个属性查询,这是缓慢的。
相反,如果在ES6中导入一个库,你能知道它的内容,并可以优化访问:

    import * as lib from 'lib';
    lib.someFunc(); // statically resolved

好处二:变量检查
With a static module structure, you always statically know which variables are visible at any location inside the module:

Global variables: increasingly, the only completely global variables will come from the language proper. Everything else will come from modules (including functionality from the standard library and the browser). That is, you statically know all global variables.
Module imports: You statically know those, too.
Module-local variables: can be determined by statically examining the module.
This helps tremendously with checking whether a given identifier has been spelled properly. This kind of check is a popular feature of linters such as JSLint and JSHint; in ECMAScript 6, most of it can be performed by JavaScript engines.

Additionally, any access of named imports (such as lib.foo) can also be checked statically.
好处三:准备好宏
Macros are still on the roadmap for JavaScript’s future. If a JavaScript engine supports macros, you can add new syntax to it via a library. Sweet.js is an experimental macro system for JavaScript. The following is an example from the Sweet.js website: a macro for classes.

    // Define the macro
    macro class {
        rule {
            $className {
                    constructor $cparams $cbody
                    $($mname $mparams $mbody) ...
            }
        } => {
            function $className $cparams $cbody
            $($className.prototype.$mname
                = function $mname $mparams $mbody; ) ...
        }
    }

    // Use the macro
    class Person {
        constructor(name) {
            this.name = name;
        }
        say(msg) {
            console.log(this.name + " says: " + msg);
        }
    }
    var bob = new Person("Bob");
    bob.say("Macros are sweet!");

For macros, a JavaScript engine performs a preprocessing step before compilation: If a sequence of tokens in the token stream produced by the parser matches the pattern part of the macro, it is replaced by tokens generated via the body of macro. The preprocessing step only works if you are able to statically find macro definitions. Therefore, if you want to import macros via modules then they must have a static structure.
好处四:准备好类型

Static type checking imposes constraints similar to macros: it can only be done if type definitions can be found statically. Again, types can only be imported from modules if they have a static structure.

Types are appealing because they enable statically typed fast dialects of JavaScript in which performance-critical code can be written. One such dialect is Low-Level JavaScript (LLJS). It currently compiles to asm.js.

好处五: 支持其他语言
If you want to support compiling languages with macros and static types to JavaScript then JavaScript’s modules should have a static structure, for the reasons mentioned in the previous two sections.

4.3 支持同步和异步加载
ECMAScript 6 modules must work independently of whether the engine loads modules synchronously (e.g. on servers) or asynchronously (e.g. in browsers). Its syntax is well suited for synchronous loading, asynchronous loading is enabled by its static structure: Because you can statically determine all imports, you can load them before evaluating the body of the module (in a manner reminiscent of AMD modules).

4.4 支持模块间的循环依赖

Two modules A and B are cyclically dependent on each other if both A (possibly indirectly/transitively) imports B and B imports A. If possible, cyclic dependencies should be avoided, they lead to A and B being tightly coupled – they can only be used and evolved together.

Why support cyclic dependencies?

Cyclic dependencies are not inherently evil. Especially for objects, you sometimes even want this kind of dependency. For example, in some trees (such as DOM documents), parents refer to children and children refer back to parents. In libraries, you can usually avoid cyclic dependencies via careful design. In a large system, though, they can happen, especially during refactoring. Then it is very useful if a module system supports them, because then the system doesn’t break while you are refactoring.

The Node.js documentation acknowledges the importance of cyclic dependencies [3] and Rob Sayre provides additional evidence:

Data point: I once implemented a system like [ECMAScript 6 modules] for Firefox. I got asked for cyclic dependency support 3 weeks after shipping.

That system that Alex Fritze invented and I worked on is not perfect, and the syntax isn’t very pretty. But it’s still getting used 7 years later, so it must have gotten something right.

Let’s see how CommonJS and ECMAScript 6 handle cyclic dependencies.

Cyclic dependencies in CommonJS

In CommonJS, if a module B requires a module A whose body is currently being evaluated, it gets back A’s exports object in its current state (line #1 in the following example). That enables B to refer to properties of that object inside its exports (line #2). The properties are filled in after B’s evaluation is finished, at which point B’s exports work properly.

    //------ a.js ------
    var b = require('b');
    exports.foo = function () { ... };

    //------ b.js ------
    var a = require('a'); // (1)
    // Can’t use a.foo in module body,
    // but it will be filled in later
    exports.bar = function () {
        a.foo(); // OK (2)
    };

    //------ main.js ------
    var a = require('a');

As a general rule, keep in mind that with cyclic dependencies, you can’t access imports in the body of the module. That is inherent to the phenomenon and doesn’t change with ECMAScript 6 modules.

The limitations of the CommonJS approach are:

Node.js-style single-value exports don’t work. In Node.js, you can export single values instead of objects, like this:
module.exports = function () { … }
If you did that in module A, you wouldn’t be able to use the exported function in module B, because B’s variable a would still refer to A’s original exports object.

You can’t use named exports directly. That is, module B can’t import a.foo like this:
var foo = require(‘a’).foo;
foo would simply be undefined. In other words, you have no choice but to refer to foo via the exports object a.

CommonJS has one unique feature: you can export before importing. Such exports are guaranteed to be accessible in the bodies of importing modules. That is, if A did that, they could be accessed in B’s body. However, exporting before importing is rarely useful.

Cyclic dependencies in ECMAScript 6

In order to eliminate the aforementioned two limitations, ECMAScript 6 modules export bindings, not values. That is, the connection to variables declared inside the module body remains live. This is demonstrated by the following code.

    //------ lib.js ------
    export let counter = 0;
    export function inc() {
        counter++;
    }

    //------ main.js ------
    import { inc, counter } from 'lib';
    console.log(counter); // 0
    inc();
    console.log(counter); // 1

Thus, in the face of cyclic dependencies, it doesn’t matter whether you access a named export directly or via its module: There is an indirection involved in either case and it always works.

五、More on importing and exporting

5.1 Importing
ECMAScript 6 provides the following ways of importing [4]:

    // Default exports and named exports
    import theDefault, { named1, named2 } from 'src/mylib';
    import theDefault from 'src/mylib';
    import { named1, named2 } from 'src/mylib';

    // Renaming: import named1 as myNamed1
    import { named1 as myNamed1, named2 } from 'src/mylib';

    // Importing the module as an object
    // (with one property per named export)
    import * as mylib from 'src/mylib';

    // Only load the module, don’t import anything
    import 'src/mylib';

5.2 Exporting

There are two ways in which you can export things that are inside the current module [5]. On one hand, you can mark declarations with the keyword export.

export var myVar1 = ...;
    export let myVar2 = ...;
    export const MY_CONST = ...;

    export function myFunc() {
        ...
    }
    export function* myGeneratorFunc() {
        ...
    }
    export class MyClass {
        ...
    }
The “operand” of a default export is an expression (including function expressions and class expressions). Examples:
    export default 123;
    export default function (x) {
        return x
    };
    export default x => x;
    export default class {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
    };

On the other hand, you can list everything you want to export at the end of the module (which is once again similar in style to the revealing module pattern).

    const MY_CONST = ...;
    function myFunc() {
        ...
    }

    export { MY_CONST, myFunc };

You can also export things under different names:

    export { MY_CONST as THE_CONST, myFunc as theFunc };

Note that you can’t use reserved words (such as default and new) as variable names, but you can use them as names for exports (you can also use them as property names in ECMAScript 5). If you want to directly import such named exports, you have to rename them to proper variables names.
5.3 Re-exporting

Re-exporting means adding another module’s exports to those of the current module. You can either add all of the other module’s exports:

 export * from 'src/other_module';

Or you can be more selective (optionally while renaming):

    export { foo, bar } from 'src/other_module';

    // Export other_module’s foo as myFoo
    export { foo as myFoo, bar } from 'src/other_module';

六、Module meta-data

ECMAScript 6 also provides a way to access information about the current module (such as the module’s URL) from inside that module. This is done as follows.

    import { url } from this module;
    console.log(url);

this module is simply a token indicating that we import the meta-data “as a module”. It could just as well be module_meta_data.

You can also access the meta-data via an object:

    import * as metaData from this module;
    console.log(metaData.url);

Node.js uses module-local variables such as __fileName for this kind of meta-data.

七、eval() and modules

eval() does not support module syntax. It parses its argument according to the Script grammar rule and scripts don’t support module syntax (why is explained later). If you want to evaluate module code, you can use the module loader API (described next).

八、The ECMAScript 6 module loader API

In addition to the declarative syntax for working with modules, there is also a programmatic API. It allows you to:

  • Programmatically work with modules and scripts
  • Configure module loading

Loaders handle resolving module specifiers (the string IDs at the end of import...from), loading modules, etc. Their constructor is Reflect.Loader. Each platform keeps a customized instance in the global variable System (the system loader), which implements its specific style of module loading.

8.1 导入模块和加载脚本

You can programmatically import a module, via an API based on ES6 promises:

    System.import('some_module')
    .then(some_module => {
        ...
    })
    .catch(error => {
        ...
    });

System.import() enables you to:

  • Use modules inside <script> elements (where module syntax is not supported, consult Sect. “Further information” for details).
  • Load modules conditionally.

System.import() retrieves a single module, you can use Promise.all() to import several modules:

    Promise.all(
        ['module1', 'module2', 'module3']
        .map(x => System.import(x)))
    .then(function ([module1, module2, module3]) {

    });

More loader methods:

8.2 配置模块加载

The module loader API has various hooks for configuration. It is still work in progress. A first system loader for browsers is currently being implemented and tested. The goal is to figure out how to best make module loading configurable.

The loader API will permit many customizations of the loading process. For example:

  1. Lint modules on import (e.g. via JSLint or JSHint).
  2. Automatically translate modules on import (they could contain CoffeeScript or TypeScript code).
  3. Use legacy modules (AMD, Node.js).

Configurable module loading is an area where Node.js and CommonJS are limited.

九、更多信息

The following content answers two important questions related to ECMAScript 6 modules: How do I use them today? How do I embed them in HTML?

  • Using ECMAScript 6 today gives an overview of ECMAScript 6 and explains how to compile it to ECMAScript 5. If you are interested in the latter, start reading in Sect. 2. One intriguing minimal solution is the ES6 Module Transpiler which only adds ES6 module syntax to ES5 and compiles it to either AMD or CommonJS.
  • Embedding ES6 modules in HTML: The code inside <script> elements does not support module syntax, because the element’s synchronous nature is incompatible with the asynchronicity of modules. Instead, you need to use the new <module> element. The blog post “ECMAScript 6 modules in future browsers” explains how <module> works. It has several significant advantages over <script> and can be polyfilled in its alternative version <script type="module">.
  • CommonJS vs. ES6: “JavaScript Modules” (by Yehuda Katz) is a quick intro to ECMAScript 6 modules. Especially interesting is a second page where CommonJS modules are shown side by side with their ECMAScript 6 versions.

十、ECMAScript 6 模块的好处

At first glance, having modules built into ECMAScript 6 may seem like a boring feature – after all, we already have several good module systems. But ECMAScript 6 modules have features that you can’t add via a library, such as a very compact syntax and a static module structure (which helps with optimizations, static checking and more). They will also – hopefully – end the fragmentation between the currently dominant standards CommonJS and AMD.

Having a single, native standard for modules means:

  • No more UMD (Universal Module Definition): UMD is a name for patterns that enable the same file to be used by several module systems (e.g. both CommonJS and AMD). Once ES6 is the only module standard, UMD becomes obsolete.
  • New browser APIs become modules instead of global variables or properties of navigator.
  • No more objects-as-namespaces: Objects such as Math and JSON serve as namespaces for functions in ECMAScript 5. In the future, such functionality can be provided via modules.

Acknowledgements: Thanks to Domenic Denicola for confirming the final module syntax. Thanks for corrections of this blog post go to: Guy Bedford, John K. Paul, Mathias Bynens, Michael Ficarra.

十一、参考

“A JavaScript glossary: ECMAScript, TC39, etc.”
Static module resolution” by David Herman
Modules: Cycles” in the Node.js API documentation
Imports” (ECMAScript 6 specification)
Exports” (ECMAScript 6 specification)

原文地址:http://www.2ality.com/2014/09/es6-modules-final.html(可能需要墙)

下面是「FED实验室」的微信公众号二维码,欢迎扫描关注:

FED实验室

行文不易,如有帮助,欢迎打赏!

赞赏支持 喜欢 (1)
捐赠共勉
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址