es6 exploringjs

https://exploringjs.com/es6

JavaScript已经有了很长一段时间的模块。然而,它们是通过库实现的,而不是内置到语言中的。ES6是JavaScript第一次拥有内置模块。

ES6模块存储在文件中。每个文件只有一个模块,每个模块只有一个文件。从模块导出内容有两种方法。这两种方法可以混合使用,但通常最好分开使用。

浏览器:脚本与模块

- Scripts Modules
HTML 元素 <script> <script type="module">
默认模式 non-strict 非严格的 strict 严格的
顶级变量 global 全局的 模块内部的
定级this的值 window undefined
执行 同步 异步
导入语句 no yes
程序导入(基于Promise的API) yes yes
文件扩展名 .js .js

JavaScript中的模块
尽管JavaScript从来没有内置模块,但社区已经融合到一种简单风格的模块上,ES5和更早版本的库支持这种风格。ES6也采用了这种风格:

  • 每个模块都是一段代码,在加载后执行。
  • 在该代码中,可能有声明(变量声明、函数声明等)。
    • 默认情况下,这些声明保留在模块的本地。
    • 您可以将其中一些标记为导出,然后其他模块可以导入它们。
  • 模块可以从其他模块导入内容。它通过模块说明符引用这些模块,字符串可以是:
    • 相对路径(“../model/user”):这些路径相对于导入模块的位置进行解释。文件扩展名.js通常可以省略。
    • 绝对路径('/lib/js/helpers'):直接指向要导入的模块的文件。
    • 名称('util'):必须配置模块名称所指的内容。
  • 模块是单件的。即使一个模块被多次导入,它也只存在一个“实例”。

这种模块方法避免了全局变量,只有模块说明符才是全局的。

ECMAScript 5模块系统

令人印象深刻的是,ES5模块系统在没有语言明确支持的情况下工作得多么好。两个最重要(不幸的是不兼容)的标准是:

  • CommonJS模块:该标准的主要实现在Node中。js(Node.js模块有一些超出CommonJS的特性)。https://nodejs.org/api/modules.html 特点:
    + 简洁的语法
    + 设计用于同步加载和服务器
  • 异步模块定义(AMD):这个标准最流行的实现是RequireJS。特点:
    • 稍微复杂一些的语法,使AMD可以在没有eval()(或编译步骤)的情况下工作
    • 设计用于异步加载和浏览器
      以上只是对ES5模块的简化解释。如果您想要更深入的资料,请看Addy Osmani的“用AMD、CommonJS和ES Harmony编写模块化JavaScript”。 https://addyosmani.com/writing-modular-js/

ECMAScript 6模块
ECMAScript 6模块的目标是创建CommonJS和AMD用户都满意的格式:

  • 与CommonJS类似,它们具有紧凑的语法、对单个导出的偏好以及对循环依赖的支持。
  • 与AMD类似,它们直接支持异步加载和可配置模块加载。

内置到语言中,ES6模块可以超越CommonJS和AMD(详细信息稍后解释):

  • 它们的语法比CommonJS的更简洁。
  • 它们的结构可以进行静态分析(用于静态检查、优化等)。
  • 他们对循环依赖的支持比CommonJS更好。

ES6模块标准有两部分:

  • 声明性语法(用于导入和导出)
  • 编程加载器API:配置模块加载方式和有条件加载模块

ES6模块的基础知识

有两种导出:命名导出(每个模块几个)和默认导出(每个模型一个)。如后文所述,可以同时使用这两者,但通常最好将它们分开。

命名导出(每个模块几个)
模块可以通过在其声明前面加上关键字export来导出多个内容。这些导出按名称进行区分,称为命名导出。

还有其他方法可以指定命名导出(稍后将进行解释),但我发现这一方法非常方便:只需编写代码,就好像没有外部世界一样,然后用关键字标记要导出的所有内容。

如果需要,还可以导入整个模块,并通过属性符号引用其命名导出:

CommonJS语法中的相同代码:有一段时间,我尝试了几种聪明的策略,以减少Node.js中模块导出的冗余。现在,我更喜欢以下简单但略显冗长的风格,让人联想到揭示模块模式:

默认输出(每个模块一个)
仅导出单个值的模块在nodejs社区中非常流行。但它们在前端开发中也很常见,在那里,您通常有模型和组件的类,每个模块一个类。ES6模块可以选择默认导出,即主导出值。默认导出特别容易导入。
以下ECMAScript 6模块“是”单个功能:

//------ myFunc.js ------
export default function () {} // no semicolon!

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

默认导出为类的ECMAScript 6模块如下所示:

//------ MyClass.js ------
export default class {} // no semicolon!

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

默认导出有两种样式:

  • 标签声明
  • 直接导出默认值

默认导出样式1:标签声明
您可以在任何函数声明(或生成器函数声明)或类声明前面加上关键字export default,使其成为默认导出:

export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!

在这种情况下,也可以省略名称。这使得默认导出成为JavaScript具有匿名函数声明和匿名类声明的唯一位置:

export default function () {} // no semicolon!
export default class {} // no semicolon!

为什么匿名函数声明而不是匿名函数表达式?

当您查看前两行代码时,您会认为导出默认操作数是表达式。它们只是出于一致性的原因而声明:操作数可以命名为声明,将其匿名版本解释为表达式会让人感到困惑(甚至比引入新类型的声明还要复杂)。

如果要将操作数解释为表达式,则需要使用括号:

export default (function () {});
export default (class {});

默认导出样式2:直接导出默认值

这些值通过表达式生成:

每个默认导出都具有以下结构。

export default «expression»;

这相当于:

const __default__ = «expression»;
export { __default__ as default }; // (A)

A行中的语句是一个导出子句

为什么有两种默认导出样式?
引入第二种默认导出样式是因为如果变量声明声明了多个变量,则无法将其有意义地转换为默认导出:

export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!

三个变量foo、bar和baz中的哪一个是默认导出?

Imports and exports 必须是顶层

正如后面更详细地解释的,ES6模块的结构是静态的,您不能有条件地导入或导出内容。这带来了各种好处。

通过仅允许在模块的顶层导入和导出,在语法上强制实施此限制:

if (Math.random()) {
    import 'foo'; // SyntaxError
}

// 您甚至无法嵌套“导入”和“导出” 在一个简单的块中:
{
    import 'foo'; // SyntaxError
}

Imports 提升
模块Imports已提升(内部移至当前范围的开始)。因此,无论您在模块中的何处提到它们,以下代码都能正常工作:

foo();

import { foo } from 'my_module';

导入是只读的
ES6模块的导入是导出实体上的只读,这意味着与模块体中声明的变量的连接保持活动状态,如以下代码所示。

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

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

在后面的章节中会解释如何在引擎盖下工作。

作为引用导入具有以下优点:

  • 它们支持循环依赖关系,即使对于非限定导入也是如此(如下一节所述)。
  • 合格导入和非合格导入的工作方式相同(它们都是间接的)。
  • 您可以将代码拆分为多个模块,它将继续工作(只要您不尝试更改导入的值)。

支持循环依赖性
如果A(可能是间接/可传递的)导入B,B导入A,则两个模块A和B之间会循环依赖。如果可能,应避免循环依赖,它们会导致A和B紧密耦合,只能一起使用和演化。

那么,为什么支持循环依赖?有时,您无法绕过它们,这就是为什么对它们的支持是一个重要功能。后面的部分有更多信息。

让我们看看CommonJS和ECMAScript 6是如何处理循环依赖的。

CommonJS中的循环依赖性

以下CommonJS代码正确地处理了两个模块a和b,这两个模块相互依赖。

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

//------ b.js ------
var a = require('a'); // (i)
function bar() {
    if (Math.random()) {
        a.foo(); // (ii)
    }
}
exports.bar = bar;

如果首先导入模块a,然后在第i行中,模块b在将导出添加到a之前获取a的导出对象。因此,b无法访问其顶层的a.foo,但一旦a的执行完成,该属性就会存在。如果之后调用了bar(),那么第ii行中的方法调用将起作用。
作为一般规则,请记住,对于循环依赖项,您不能访问模块主体中的导入。这是现象固有的,ECMAScript 6模块不会改变。

CommonJS方法的局限性包括:

  • nodejs 样式的单值导出不起作用。在这里,可以导出单个值而不是对象:
  module.exports = function () { ··· };

  • 如果模块a这样做了,那么一旦赋值完成,模块b的变量a就不会更新。它将继续引用原始导出对象。
  • 不能直接使用命名导出。也就是说,模块b不能像这样导入foo:
  var foo = require('a').foo;

foo只是未定义的。换句话说,您别无选择,只能通过a.foo引用foo。

这些限制意味着exporter和importers都必须意识到循环依赖性并明确支持它们。

ECMAScript6中的循环依赖
ES6模块自动支持循环依赖关系。也就是说,它们没有上一节中提到的CommonJS模块的两个限制:默认导出有效,无条件命名导入也有效(以下示例中的第i行和第iii行)。因此,可以按如下方式实现周期性相互依赖的模块。

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
    bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
    if (Math.random()) {
        foo(); // (iv)
    }
}

这段代码可以工作,因为如前一节所述,导入是关于导出的引用。这意味着即使是不合格的导入(例如第ii行中的bar和第iv行中的foo)也是引用原始数据的间接操作。因此,面对循环依赖关系,无论您是通过非限定导入还是通过其模块访问命名导出,都无关紧要:在这两种情况下都会涉及到一个间接寻址,它总是可以工作的。

详细Importing, exporting
导入方式

  • 默认导入:
  import localName from 'src/my_lib';
  • 命名空间导入:将模块作为对象导入(每个命名导出具有一个属性)。
  import * as my_lib from 'src/my_lib';
  • 命名导入:
  import { name1, name2 } from 'src/my_lib';

  • 您可以重命名命名导入:
  // Renaming: import `name1` as `localName1`
  import { name1 as localName1, name2 } from 'src/my_lib';

  // Renaming: import the default export as `foo`
  import { default as foo } from 'src/my_lib';
  • 空导入:只加载模块,不导入任何内容。程序中的第一个这样的导入执行模块体。
  import 'src/my_lib';

只有两种方式可以组合这些样式,并且它们的出现顺序是固定的;默认导出总是排在第一位。

  • 将默认导入与命名空间导入相结合:
  import theDefault, * as my_lib from 'src/my_lib';

  • 将默认导入与命名导入相结合
  import theDefault, { name1, name2 } from 'src/my_lib';

命名导出样式:内联与子句
有两种方法可以导出模块内的命名内容。

一方面,可以使用关键字export标记声明。

另一方面,您可以在模块末尾列出要导出的所有内容(风格与显示模块模式相似)。

您还可以使用不同的名称导出内容:

重新导出
重新导出意味着将另一个模块的导出添加到当前模块的导出中。您可以添加其他模块的所有导出:

导出*忽略默认导出。

或者可以更具选择性(重命名时可选):

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

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

使重新导出成为默认导出
以下语句使另一个模块foo的默认导出成为当前模块的默认导出:

export { default } from 'foo';

以下语句使模块foo的命名导出myFunc成为当前模块的默认导出:

export { myFunc as default } from 'foo';

所有导出方式

ECMAScript 6提供了几种导出样式:

  • 重新导出
    • 重新导出所有内容(默认导出除外)
  export * from 'src/other_module';
+ 通过条款重新导出
  export { foo as myFoo, bar } from 'src/other_module';

  export { default } from 'src/other_module';
  export { default as foo } from 'src/other_module';
  export { foo as default } from 'src/other_module';
+  通过子句指定导出:
  export { MY_CONST as FOO, myFunc };
  export { foo as default };
  • 内联命名导出:

    • 变量声明:
export var foo;
  export let foo;
  export const foo;
 + 函数声明:
  export function myFunc() {}
  export function* myGenFunc() {}

+  类声明:
  export class MyClass {}
  • 默认导出:

    • 函数声明(此处可以匿名):
  export default function myFunc() {}
  export default function () {}

  export default function* myGenFunc() {}
  export default function* () {}
+ 类声明(此处可以是匿名的):
  export default class MyClass {}
  export default class {}

+ 表达式:导出值。注意末尾的分号。
  export default foo;
  export default 'Hello world!';
  export default 3 * 7;
  export default (function () {});

在模块中同时具有命名导出和默认导出

以下模式在JavaScript中非常常见:库是单个函数,但通过该函数的属性提供附加服务。示例包括jQuery和Underscore.js。以下是作为CommonJS模块的Undercore的草图:

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

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

对于ES6,函数_是默认导出,而each和forEach是命名导出。事实证明,您实际上可以同时拥有命名导出和默认导出。例如,前一个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版本只是大致相似。后者是扁平结构,而前者是嵌套结构。

建议:避免混合默认导出和命名导出
我通常建议将这两种导出分开:对于每个模块,要么只有默认导出,要么只有命名导出。

然而,这不是一个非常有力的建议;有时把这两种混合起来可能是有意义的。一个示例是默认导出实体的模块。对于单元测试,还可以通过指定的导出使一些内部组件可用。

默认导出只是另一个命名导出
默认导出实际上只是具有特殊名称default的命名导出。也就是说,以下两种说法是等价的:

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

同样,以下两个模块具有相同的默认导出:

//------ module1.js ------
export default function foo() {} // function declaration!

//------ module2.js ------
function foo() {}
export { foo as default };

默认值:可以作为导出名称,但不能作为变量名称

不能使用保留字(例如default和new)作为变量名,但可以将它们用作导出的名称(也可以在ECMAScript 5中将它们用作属性名)。如果要直接导入此类命名导出,必须将其重命名为适当的变量名称。

这意味着默认值只能出现在重命名导入的左侧:

import { default as foo } from 'some_module';

它只能出现在重命名导出的右侧:

export { foo as default };

ECMAScript 6模块加载程序API
除了用于处理模块的声明性语法外,还有一个编程API。它允许您:

  • 以编程方式使用模块
  • 配置模块加载

模块加载程序API不是ES6标准的一部分

它将在一个单独的文档“JavaScript Loader Standard”中指定,该文档将比语言规范更具动态性。该文档的存储库声明: https://github.com/whatwg/loader/

[JavaScript Loader Standard]将ECMAScript模块加载语义的工作与Web浏览器以及Node.js的集成点整合在一起。
模块加载程序API正在工作
正如您在JavaScript Loader Standard的存储库中看到的那样,模块加载器API仍在工作中。你在这本书中读到的一切都是暂定的。为了了解API的外观,您可以查看GitHub上的ES6 Module Loader Polyfill。
正如您在JavaScript Loader Standard的存储库中看到的那样,模块加载器API仍在工作中。你在这本书中读到的一切都是暂定的。为了了解API的外观,您可以查看GitHub上的ES6 Module Loader Polyfill。
https://github.com/ModuleLoader/es-module-loader

加载器
加载器处理解析模块说明符(从导入结束时的字符串ID)、加载模块等。它们的构造函数是Reflect.Loader。每个平台都在全局变量System(系统加载程序)中保留一个默认实例,该变量实现其特定的模块加载样式。

Loader方法:导入模块
您可以通过基于Promises的API以编程方式导入模块:

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

System.import()使您能够:

  • 使用<script>元素中的模块(如果不支持模块语法,请参阅模块与脚本部分以了解详细信息)。
  • 有条件地加载模块。

System.import() 检索单个模块,可以使用Promise.all()导入多个模块:

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

更多装载方法
加载器有更多方法。三个重要因素是:

  • System.module(source, options?)
    • 将源代码中的JavaScript代码计算为模块(通过Promise异步交付)。
  • System.set(name, module)
    • 用于注册模块(例如,通过System.module()创建的模块)。
  • System.define(name, source, options?)
    • 两者都评估源代码中的模块代码并注册结果。

配置模块加载
模块加载器API将具有用于配置加载过程的各种挂钩。用例包括:

  • 导入时的Lint模块(例如,通过JSLint或JSHint)。
  • 导入时自动转换模块(它们可以包含CoffeeScript或TypeScript代码)。
  • 使用旧模块(AMD、Node.js)。

可配置模块加载是所在的区域nodejs和CommonJS是有限的。
在浏览器中使用ES6模块
让我们看看浏览器中如何支持ES6模块。

浏览器中对ES6模块的支持正在进行中

与模块加载类似,浏览器中模块支持的其他方面仍在研究中。您在这里读到的所有内容都可能发生变化。

浏览器:异步模块与同步脚本
在浏览器中,有两种不同的实体:脚本和模块。它们的语法和工作方式略有不同。
这是对差异的概述,详细信息将在后面解释:

Scripts脚本
脚本是嵌入JavaScript和引用外部JavaScript文件的传统浏览器方式。脚本具有用作以下内容的internet媒体类型:

  • 通过web服务器交付的JavaScript文件的内容类型。
  • <script>元素的属性类型的值。注意,对于HTML5,建议在<script>元素中省略包含或引用JavaScript的type属性。

以下是最重要的值:

  • text/javascript:是一个遗留值,如果在脚本标记中省略type属性,则用作默认值。它是Internet Explorer 8和更早版本的最安全的选择。
  • application/javascript:建议用于当前浏览器。

脚本通常是同步加载或执行的。JavaScript线程将停止,直到加载或执行代码。

Modules 模块

为了符合JavaScript通常的run-To-completion运行至完成语义,必须在不中断的情况下执行模块体。这为导入模块留下了两个选项:

  • 在执行主体时同步加载模块。这就是Nodejs做的。
  • 在执行主体之前异步加载所有模块。这就是AMD模块的处理方式。这是浏览器的最佳选择,因为模块是通过互联网加载的,执行时不必暂停。另一个好处是,这种方法允许并行加载多个模块。

ECMAScript 6为您提供了两全其美的功能:Nodejs的同步语法加上AMD的异步加载。为了实现这两种功能,ES6模块在语法上不如Node灵活。js-modules:导入和导出必须在顶层进行。这意味着它们也不能是有条件的。此限制允许ES6模块加载程序静态分析模块导入的模块,并在执行其主体之前加载它们。

脚本的同步特性阻止它们成为模块。脚本甚至无法以声明方式导入模块(如果您想这样做,则必须使用编程模块加载程序API)。

模块可以通过完全异步的<script>元素的新变体从浏览器中使用:

<script type="module">
    import $ from 'lib/jquery';
    var x = 123;

    // The current scope is not global
    console.log('$' in window); // false
    console.log('x' in window); // false

    // `this` is undefined
    console.log(this === undefined); // true
</script>

如您所见,元素有自己的范围,“内部”的变量是该范围的本地变量。请注意,模块代码隐式处于严格模式。这是一个好消息——不再“use strict”。

与普通<script>元素类似,<script type=“module”>也可用于加载外部模块。例如,以下标记通过主模块启动web应用程序(属性名称导入是我的发明,尚不清楚将使用什么名称)。

<script type="module" import="impl/main"></script>

通过自定义<script>类型支持HTML中的模块的优点是,很容易通过polyfill(库)将这种支持带到较旧的引擎。模块可能有专用元素,也可能最终没有专用元素(例如<module>)。

模块或脚本-上下文问题
文件是模块还是脚本,只取决于它的导入或加载方式。大多数模块都有导入或导出,因此可以检测到。但如果一个模块既没有也没有,那么它就无法与脚本区分开来。例如:

var x = 123;

这段代码的语义因其被解释为模块还是脚本而不同:

作为模块,变量x是在模块范围内创建的。

作为脚本,变量x成为全局变量和全局对象(浏览器中的窗口)的属性。

更现实的例子是一个模块,它安装了一些东西,例如全局变量中的polyfill或全局事件侦听器。这样的模块既不导入也不导出任何内容,并通过空导入激活:

import './my_module';

本节的来源

模块:状态更新,David Herman幻灯片
“模块与脚本”,David Herman的电子邮件。 https://mail.mozilla.org/pipermail/es-discuss/2013-November/034869.html

细节 imports 作为exports的引用
https://github.com/rauschma/imports-are-views-demo 本节中的代码在GitHub上提供。

导入在CommonJS和ES6中的工作方式不同:

在CommonJS中,导入是导出值的副本。

在ES6中,导入是导出值的实时只读引用。
以下各节解释了这意味着什么。

在CommonJS中,导入是导出值的副本
使用CommonJS(Node.js)模块,事情以相对熟悉的方式运行。

如果将值导入变量,则会复制该值两次:一次是导出时(a行),另一次是导入时(B行)。

//------ lib.js ------
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter, // (A)
    incCounter: incCounter,
};

//------ main1.js ------
var counter = require('./lib').counter; // (B)
var incCounter = require('./lib').incCounter;

// The imported value is a (disconnected) copy of a copy
console.log(counter); // 3
incCounter();
console.log(counter); // 3

// The imported value can be changed
counter++;
console.log(counter); // 4

如果通过导出对象访问该值,在导出时仍会复制一次:

//------ main2.js ------
var lib = require('./lib');

// The imported value is a (disconnected) copy
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 3

// The imported value can be changed
lib.counter++;
console.log(lib.counter); // 4

在ES6中,导入是导出值的实时只读视图
与CommonJS相反,导入是对导出值的视图。换句话说,每次导入都是与导出数据的实时连接。导入是只读的:
不合格导入(import x from 'foo')类似于const声明的变量。
模块对象foo的属性(import * as foo from 'foo')与冻结对象的属性类似。

以下代码演示导入与视图的相似性:

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

//------ main1.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError

如果通过星号(*)导入模块对象,则会得到相同的结果:

//------ main2.js ------
import * as lib from './lib';

// The imported value `counter` is live
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 4

// The imported value can’t be changed
lib.counter++; // TypeError

请注意,虽然无法更改导入的值,但可以更改导入所引用的对象。例如:

//------ lib.js ------
export let obj = {};

//------ main.js ------
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

为什么采用新的importing方法
为什么引入这样一种相对复杂的import机制,而这种机制与既定做法背道而驰?
循环依赖项:主要优点是它支持循环依赖项,即使对于非限定导入也是如此。

合格和不合格的import的作用相同。在CommonJS中,它们没有:限定导入提供对模块导出对象属性的直接访问,非限定导入是其副本。
您可以将代码拆分为多个模块,它将继续工作(只要您不尝试更改导入的值)。

另一方面,模块折叠,将多个模块组合成一个模块也变得更简单。

以我的经验来看,ES6 import 很管用,你很少需要考虑幕后发生了什么。

实施意见
进口如何作为引擎盖下的exports视图?导出通过数据结构导出条目进行管理。所有exports条目(转口条目除外)都有以下两个名称:

本地名称:是模块中存储导出的名称。

导出名称:导入模块访问导出所需的名称。

导入实体后,始终通过具有两个组件模块和本地名称的指针访问该实体。换句话说,该指针指的是模块内的绑定(变量的存储空间)。

让我们检查各种导出创建的导出名称和本地名称。下表(改编自ES6规范)提供了概述,后续章节提供了更多详细信息。

Export

作为规范中的视图导入
本节提供了指向ECMAScript 2015(ES6)语言规范的指针。

管理导入:

ES6模块的设计目标

如果您想理解ECMAScript 6模块,了解哪些目标影响了它们的设计会有帮助。主要有:

支持默认导出

静态模块结构

支持同步和异步加载

支持模块之间的循环依赖关系

以下小节解释了这些目标。

默认导出受欢迎的
表明默认导出“是”模块的模块语法可能有点奇怪,但如果您考虑到一个主要设计目标是使默认导出尽可能方便,那么这是有意义的。引用David Herman的话:

ECMAScript 6支持单一/默认导出样式,并为导入默认导出提供了最简单的语法。导入命名导出可能甚至应该稍微不那么简洁。

静态模块结构

当前的JavaScript模块格式具有动态结构:导入和导出的内容可以在运行时更改。ES6引入自己的模块格式的一个原因是启用静态结构,这有几个好处。但在我们深入讨论之前,让我们先看看静态结构的含义。

这意味着您可以在编译时确定导入和导出(静态)-您只需要查看源代码,不必执行它。ES6在语法上强制这样做:您只能在顶层导入和导出(永远不能嵌套在条件语句中)。导入和导出语句没有动态部分(不允许变量等)。

以下是两个没有静态结构的CommonJS模块示例。在第一个示例中,您必须运行代码以找出它导入的内容:

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

在第二个示例中,您必须运行代码以找出它导出的内容:

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

ECMAScript 6模块不够灵活,迫使您保持静态。因此,您可以获得以下几个好处。

好处:打包期间消除死代码

在前端开发中,模块通常按以下方式处理:
在开发过程中,代码存在的模块很多,通常很小。

对于部署,这些模块被绑定到几个相对较大的文件中。

捆绑的原因是:

为了加载所有模块,需要检索的文件更少。

压缩捆绑文件比压缩单独的文件稍微高效一些。

捆绑期间,可以删除未使用的导出,这可能会节省大量空间。

原因#1对于HTTP/1很重要,因为请求文件的成本相对较高。这将随着HTTP/2而改变,这就是为什么这个原因在那里并不重要。

原因#3仍将令人信服。它只能通过具有静态结构的模块格式来实现。

优点:紧凑捆绑,无自定义捆绑格式
模块捆绑器Rollup证明ES6模块可以有效组合,因为它们都适合一个范围(在重命名变量以消除名称冲突之后)。由于ES6模块的两个特点,这是可能的:

它们的静态结构意味着bundle格式不必考虑有条件加载的模块(这样做的一种常见技术是将模块代码放入函数中)。

导入是导出的只读视图,这意味着您不必复制导出,可以直接引用它们。

例如,考虑以下两个ES6模块。

// lib.js
export function foo() {}
export function bar() {}

// main.js
import {foo} from './lib.js';
console.log(foo());

汇总可以将这两个ES6模块捆绑到以下单个ES6模块中(请注意,已消除未使用的导出栏):

function foo() {}

console.log(foo());

Rollup方法的另一个好处是,捆绑包没有自定义格式,它只是一个ES6模块。

优点:更快地查找导入
如果您需要CommonJS中的库,则会返回一个对象:

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

因此,通过访问命名导出lib.someFunc意味着您必须执行属性查找,这很慢,因为它是动态的。

相反,如果在ES6中导入库,则可以静态了解其内容并优化访问:

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

优点:变量检查

使用静态模块结构,您总是可以静态地知道哪些变量在模块内的任何位置可见:

全局变量:越来越多的完全全局变量将来自语言本身。其他一切都来自模块(包括标准库和浏览器的功能)。也就是说,您静态地知道所有全局变量。

模块导入:您也静态地知道这些。

模块局部变量:可以通过静态检查模块来确定。

这极大地有助于检查给定标识符的拼写是否正确。这种检查是诸如JSLint和JSHint等过梁的一个流行特性;在ECMAScript 6中,大部分可以由JavaScript引擎执行。

此外,还可以静态检查命名导入(如lib.foo)的任何访问。

优点:可用于宏

宏仍在JavaScript未来的路线图上。如果JavaScript引擎支持宏,则可以通过库向其添加新语法。sweetjs是JavaScript的实验性宏系统。下面是sweet.js的一个例子网站:类的宏。

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

对于宏,JavaScript引擎在编译之前执行预处理步骤:如果解析器生成的标记流中的标记序列与宏的模式部分匹配,则会被通过宏体生成的标记替换。预处理步骤仅在能够静态查找宏定义时有效。因此,如果要通过模块导入宏,则它们必须具有静态结构。

优点:适合类型

静态类型检查施加了类似于宏的约束:只有在静态找到类型定义时才能进行。同样,类型只有在具有静态结构时才能从模块导入。

类型很有吸引力,因为它们支持静态类型的快速JavaScript方言,可以在其中编写性能关键的代码。其中一种方言是低级JavaScript(LLJS)。

优点:支持其他语言

如果您想支持将带有宏和静态类型的语言编译为JavaScript,那么JavaScript的模块应该具有静态结构,原因在前两节中提到。

本节来源
“Static module resolution” by David Herman http://calculist.org/blog/2012/06/29/static-module-resolution/
支持同步和异步加载

ECMAScript 6模块必须独立于引擎是同步(例如在服务器上)还是异步(例如在浏览器中)加载模块。它的语法非常适合同步加载,异步加载通过其静态结构启用:因为您可以静态确定所有导入,所以可以在评估模块主体之前加载它们(以一种类似于AMD模块的方式)。

支持模块之间的循环依赖
支持循环依赖性是ES6模块的关键目标。原因如下:

循环依赖并非天生邪恶。特别是对于对象,有时甚至需要这种依赖关系。例如,在一些树(如DOM文档)中,父对象指子对象,子对象指父对象。在库中,通常可以通过精心设计来避免循环依赖关系。然而,在大型系统中,它们可能会发生,尤其是在重构期间。如果模块系统支持它们,那么这将非常有用,因为在重构时系统不会崩溃。

nodejs文档承认循环依赖的重要性,Rob Sayre提供了其他证据:

数据点:我曾经为Firefox实现过类似[ECMAScript 6模块]的系统。我被要求提供循环依赖支持。
我和亚历克斯·弗里茨发明的系统并不完美,语法也不太好。但7年后它仍在使用中,所以它一定有正确的地方。

常见问题解答:模块
我可以使用变量指定要从哪个模块导入?

import语句是完全静态的:它的模块说明符总是固定的。如果要动态确定要加载的模块,则需要使用编程加载器API:

const moduleSpecifier = 'module_' + Math.random();
System.import(moduleSpecifier)
.then(the_module => {
    // Use the_module
})

我可以有条件或按需导入模块吗?
导入语句必须始终位于模块的顶层。这意味着您不能将它们嵌套在if语句、函数等中。因此,如果您想有条件或按需加载模块,则必须使用编程加载器API:

if (Math.random()) {
    System.import('some_module')
    .then(some_module => {
        // Use some_module
    })
}

我可以在导入语句中使用变量吗?

不,你不能。请记住,导入的内容不得依赖于运行时计算的任何内容。因此:

// Illegal syntax:
import foo from 'some_module'+SUFFIX;

我可以在导入语句中使用destructuring吗?
不,你不能。import语句看起来只是破坏,但完全不同(静态、导入是视图等)。

因此,在ES6中不能这样做:

// Illegal syntax:
import { foo: { bar } } from 'some_module';

是否需要命名导出?为什么不默认导出对象?
您可能会想,如果我们可以简单地默认导出对象(如CommonJS),为什么需要命名导出?答案是,您不能通过对象强制执行静态结构,并且会失去所有相关的优点(这将在[静态模块结构]中进行解释)。

我可以eval模块的代码吗?
不,你不能。模块是eval()的高级构造。模块加载器API提供了从字符串创建模块的方法。语法上,eval()接受脚本(不允许导入和导出),而不是模块。

ECMAScript 6模块的优点

乍一看,将模块内置到ECMAScript 6中似乎是一个无聊的特性——毕竟,我们已经有了几个好的模块系统。但ECMAScript 6模块有几个新功能:

更简洁的语法

静态模块结构(有助于消除死代码、优化、静态检查等)

自动支持循环依赖项

ES6模块也有望结束当前主流标准CommonJS和AMD之间的分裂。模块采用单一的本地标准意味着:

无更多UMD(通用模块定义):UMD是模式的名称,允许多个模块系统(例如CommonJS和AMD)使用相同的文件。一旦ES6成为唯一的模块标准,UMD就会过时。

新的浏览器API将成为模块,而不是全局变量或navigator的属性。

不再使用对象作为名称空间:Math和JSON等对象作为ECMAScript 5中函数的名称空间。将来,可以通过模块提供此类功能。

进一步阅读

CommonJS与ES6:“JavaScript模块”(由Yehuda Katz编写)是ECMAScript 6模块的快速介绍。特别有趣的是第二页,其中CommonJS模块与其ECMAScript 6版本并排显示。

es6
https://262.ecma-international.org/6.0/

https://github.com/pl2476/exploring-es6
https://juejin.cn/post/6844903425763704846
https://gitee.com/xv/exploring-es6-cn

posted @ 2022-10-08 17:01  Running00  阅读(23)  评论(0编辑  收藏  举报