es6 exploringjs
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
2021-10-08 axios
2021-10-08 取整
2021-10-08 HTTP 请求
2021-10-08 http 请求响应的数据类型
2021-10-08 ArrayBuffer, Stream
2020-10-08 egg-vue-webpack-boilerplate
2018-10-08 vue 笔记