import
语句中使用变量吗?import
语句中使用解构吗?eval()
模块的代码吗?JavaScript 长期以来一直都有模块。但是,它们是通过库实现的,而不是内置于语言中的。ES6 是 JavaScript 第一次拥有内置模块。
ES6 模块存储在文件中。每个文件只有一个模块,每个模块只有一个文件。您可以通过两种方式从模块中导出内容。这两种方式可以混合使用,但通常最好单独使用它们。
可以有多个命名导出
//------ 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
您也可以导入整个模块
//------ main.js ------
import
*
as
lib
from
'lib'
;
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
可以有一个默认导出。例如,一个函数
//------ myFunc.js ------
export
default
function
()
{
···
}
// no semicolon!
//------ main1.js ------
import
myFunc
from
'myFunc'
;
myFunc
();
或者一个类
//------ MyClass.js ------
export
default
class
{
···
}
// no semicolon!
//------ main2.js ------
import
MyClass
from
'MyClass'
;
const
inst
=
new
MyClass
();
请注意,如果默认导出函数或类(它们是匿名声明),则末尾没有分号。
脚本 | 模块 | |
---|---|---|
HTML 元素 | <script> |
<script type="module"> |
默认模式 | 非严格模式 | 严格模式 |
顶层变量是 | 全局的 | 模块本地的 |
顶层 this 的值 |
window |
undefined |
执行方式 | 同步 | 异步 |
声明式导入 (import 语句) |
否 | 是 |
程序化导入(基于 Promise 的 API) | 是 | 是 |
文件扩展名 | .js |
.js |
尽管 JavaScript 从未内置模块,但社区已经融合了一种简单的模块样式,ES5 及更早版本中的库都支持这种样式。ES6 也采用了这种样式
'../model/user'
):这些路径是相对于导入模块的位置进行解释的。文件扩展名 .js
通常可以省略。'/lib/js/helpers'
):直接指向要导入的模块的文件。'util'
):模块名称所指的内容必须进行配置。这种模块化方法避免了全局变量,唯一全局的是模块说明符。
令人印象深刻的是,ES5 模块系统在没有语言的明确支持的情况下也能很好地工作。两个最重要(不幸的是不兼容)的标准是
以上只是对 ES5 模块的简化说明。如果您想了解更多深入的资料,请参阅 Addy Osmani 的“使用 AMD、CommonJS 和 ES Harmony 编写模块化 JavaScript”。
ECMAScript 6 模块的目标是创建一种格式,使 CommonJS 和 AMD 的用户都满意
内置于语言中使 ES6 模块能够超越 CommonJS 和 AMD(详细信息将在后面解释)
ES6 模块标准有两部分
有两种导出:命名导出(每个模块多个)和默认导出(每个模块一个)。正如后面解释的那样,可以同时使用两者,但通常最好将它们分开。
模块可以通过在其声明前加上关键字 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
还有其他方法可以指定命名导出(将在后面解释),但我发现这种方法非常方便:只需编写代码,就好像没有外部世界一样,然后用关键字标记要导出的所有内容。
如果您愿意,您也可以导入整个模块,并通过属性表示法引用其命名导出
//------ main.js ------
import
*
as
lib
from
'lib'
;
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
CommonJS 语法中的相同代码:有一段时间,我尝试了几种聪明的策略,以便在 Node.js 中减少模块导出的冗余。现在我更喜欢以下简单但稍微冗长的风格,它让人想起揭示模块模式
//------ 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
只导出单个值的模块在 Node.js 社区中非常流行。但在前端开发中它们也很常见,在前端开发中,您通常拥有模型和组件的类,每个模块一个类。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
();
默认导出有两种样式
您可以在任何函数声明(或生成器函数声明)或类声明前加上关键字 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
的操作数是表达式。它们只是出于一致性原因的声明:操作数可以是命名声明,将它们的匿名版本解释为表达式会令人困惑(甚至比引入新类型的声明更令人困惑)。
如果您希望将操作数解释为表达式,则需要使用括号
export
default
(
function
()
{});
export
default
(
class
{});
这些值是通过表达式生成的
export
default
'abc'
;
export
default
foo
();
export
default
/^xyz$/
;
export
default
5
*
7
;
export
default
{
no
:
false
,
yes
:
true
};
每个默认导出都具有以下结构。
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
中的哪一个是默认导出?
正如后面将更详细地解释的那样,ES6 模块的结构是静态的,您不能有条件地导入或导出内容。这带来各种好处。
此限制是通过语法强制执行的,仅允许在模块的顶层进行导入和导出
if
(
Math
.
random
())
{
import
'foo'
;
// SyntaxError
}
// You can’t even nest `import` and `export`
// inside a simple block:
{
import
'foo'
;
// SyntaxError
}
模块导入会被提升(在内部移动到当前作用域的开头)。因此,你在模块中提及它们的位置并不重要,以下代码可以正常工作
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 代码正确处理了两个模块 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
的导出对象之前获取它。因此,b
无法在其顶层访问 a.foo
,但在 a
的执行完成后,该属性存在。如果之后调用 bar()
,则第 ii 行中的方法调用有效。
作为一般规则,请记住,对于循环依赖,你无法在模块主体中访问导入。这是现象固有的,并且不会随着 ECMAScript 6 模块而改变。
CommonJS 方法的局限性是
module
.
exports
=
function
()
{
···
};
如果模块 a
这样做,则一旦赋值完成,模块 b
的变量 a
将不会更新。它将继续引用原始导出对象。
b
不能像这样导入 foo
var
foo
=
require
(
'a'
).
foo
;
foo
将只是 undefined
。换句话说,你除了通过 a.foo
引用 foo
之外别无选择。
这些限制意味着导出器和导入器都必须意识到循环依赖并明确支持它们。
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
)也是引用原始数据的间接寻址。因此,面对循环依赖,你是通过非限定导入还是通过其模块访问命名导出并不重要:两种情况下都涉及间接寻址,并且它始终有效。
ECMAScript 6 提供了几种导入样式2
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
var
myVar1
=
···
;
export
let
myVar2
=
···
;
export
const
MY_CONST
=
···
;
export
function
myFunc
()
{
···
}
export
function
*
myGeneratorFunc
()
{
···
}
export
class
MyClass
{
···
}
另一方面,你可以在模块的末尾列出你想要导出的所有内容(这在风格上类似于揭示模块模式)。
const
MY_CONST
=
···
;
function
myFunc
()
{
···
}
export
{
MY_CONST
,
myFunc
};
你也可以用不同的名称导出内容
export
{
MY_CONST
as
FOO
,
myFunc
};
重新导出意味着将另一个模块的导出添加到当前模块的导出中。你可以添加其他模块的所有导出
export
*
from
'src/other_module'
;
export *
会忽略默认导出3。
或者你可以更有选择性(可以选择重命名)
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 提供了几种导出样式4
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。以下是 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 的视角,函数 _
是默认导出,而 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
:可以用作导出名称,但不能用作变量名称 你不能使用保留字(例如 default
和 new
)作为变量名称,但你可以使用它们作为导出的名称(你也可以在 ECMAScript 5 中使用它们作为属性名称)。如果你想直接导入此类命名导出,则必须将它们重命名为正确的变量名称。
这意味着 default
只能出现在重命名导入的左侧
import
{
default
as
foo
}
from
'some_module'
;
并且它只能出现在重命名导出的右侧
export
{
foo
as
default
};
在重新导出中,as
的两侧都是导出名称
export
{
myFunc
as
default
}
from
'foo'
;
export
{
default
as
otherFunc
}
from
'foo'
;
// The following two statements are equivalent:
export
{
default
}
from
'foo'
;
export
{
default
as
default
}
from
'foo'
;
除了用于处理模块的声明式语法外,还有一个编程 API。它允许你
加载器处理解析模块说明符(import-from
末尾的字符串 ID)、加载模块等。它们的构造函数是 Reflect.Loader
。每个平台都在全局变量 System
(系统加载器)中保留一个默认实例,该实例实现其特定的模块加载方式。
你可以通过基于 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?)
source
中的 JavaScript 代码评估为模块(通过 Promise 异步传递)。System.set(name, module)
System.module()
创建的模块)。System.define(name, source, options?)
source
中的模块代码并注册结果。模块加载器 API 将具有用于配置加载过程的各种钩子。用例包括
可配置模块加载是 Node.js 和 CommonJS 受限的领域。
让我们看看浏览器如何支持 ES6 模块。
在浏览器中,有两种不同的实体:脚本和模块。它们的语法略有不同,工作方式也不同。
以下是差异概述,稍后将详细解释
脚本 | 模块 | |
---|---|---|
HTML 元素 | <script> |
<script type="module"> |
默认模式 | 非严格模式 | 严格模式 |
顶层变量是 | 全局的 | 模块本地的 |
顶层 this 的值 |
window |
undefined |
执行方式 | 同步 | 异步 |
声明式导入 (import 语句) |
否 | 是 |
程序化导入(基于 Promise 的 API) | 是 | 是 |
文件扩展名 | .js |
.js |
脚本是嵌入 JavaScript 和引用外部 JavaScript 文件的传统浏览器方式。脚本具有 互联网媒体类型,用作
<script>
元素的 type
属性的值。请注意,对于 HTML5,建议如果 <script>
元素包含或引用 JavaScript,则省略 type
属性。以下是最重要的值
text/javascript
:是一个遗留值,如果您在脚本标记中省略 type
属性,则用作默认值。对于 Internet Explorer 8 及更早版本,它是 最安全的选择。application/javascript
:建议 用于当前浏览器。脚本通常是同步加载或执行的。JavaScript 线程程在代码加载或执行完毕之前会停止。
为了与 JavaScript 通常的运行到完成语义保持一致,模块的主体必须在不中断的情况下执行。这为导入模块留下了两种选择
ECMAScript 6 为您提供了两全其美的优势:Node.js 的同步语法加上 AMD 的异步加载。为了使两者成为可能,ES6 模块在语法上不如 Node.js 模块灵活:导入和导出必须发生在顶层。这意味着它们也不能是有条件的。此限制允许 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">
也可以用于加载外部模块。例如,以下标记通过 main
模块启动 Web 应用程序(属性名 import
是我的发明,目前尚不清楚将使用什么名称)。
<
script
type
=
"module"
import
=
"impl/main"
></
script
>
通过自定义 <script>
类型在 HTML 中支持模块的优势在于,可以通过 polyfill(库)轻松地将该支持引入旧引擎。最终可能会也可能不会有一个专用于模块的元素(例如 <module>
)。
文件是模块还是脚本仅取决于其导入或加载方式。大多数模块都有导入或导出,因此可以检测到。但是,如果模块两者都没有,则它与脚本没有区别。例如
var
x
=
123
;
这段代码的语义取决于它是被解释为模块还是脚本而有所不同
x
在模块作用域中创建。x
变为全局变量和全局对象(浏览器中的 window
)的属性。更现实的例子是一个安装某些东西的模块,例如全局变量中的 polyfill 或全局事件侦听器。这样的模块既不导入也不导出任何东西,并且通过空导入激活
import
'./my_module'
;
导入在 CommonJS 和 ES6 中的工作方式不同
以下部分解释了这意味着什么。
使用 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
如果通过 exports 对象访问该值,则它在导出时仍会被复制一次
//------ 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
与 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
为什么要引入这种相对复杂的导入机制,而这种机制偏离了既定的做法?
根据我的经验,ES6 导入就可以工作,您很少需要考虑幕后发生的事情。
在幕后,导入如何作为导出的视图工作?导出通过数据结构*导出条目*进行管理。所有导出条目(重新导出除外)都有以下两个名称
导入实体后,始终通过具有*模块*和*本地名称*这两个组件的指针访问该实体。换句话说,该指针引用模块内部的*绑定*(变量的存储空间)。
让我们检查一下各种导出创建的导出名称和本地名称。下表(改编自 ES6 规范)给出了概述,后续部分有更多详细信息。
语句 | 本地名称 | 导出名称 |
---|---|---|
export {v}; |
'v' |
'v' |
'v' |
'v' |
export {v as x}; |
'v' |
'v' |
'v' |
'x' |
export const v = 123; |
export const v = 123; |
'v' |
export const v = 123; |
'v' |
export function f() {} |
'f' |
'v' |
'f' |
'f' |
'v' |
function
foo
()
{}
export
{
foo
};
function
foo
()
{}
export
{
foo
as
bar
};
'default'
export
function
foo
()
{}
export default 123;
function
foo
()
{}
export
{
foo
};
'*default*'
16.7.3.1 导出子句
foo
foo
foo
导出名称:bar
export
default
123
;
16.7.3.2 内联导出
const
*
default
*
=
123
;
// *not* legal JavaScript
export
{
*
default
*
as
default
};
这是一个内联导出
本地名称:foo
导出名称:foo
默认导出有两种
export
default
function
foo
()
{}
16.7.3.2 内联导出
function
foo
()
{}
export
{
foo
as
default
};
*可提升声明*(函数声明、生成器函数声明)和类声明的默认导出类似于普通的内联导出,因为创建了命名的本地实体并对其进行了标记。
所有其他默认导出都是关于导出表达式结果的。
16.7.3.3.1 默认导出表达式
export
default
function
()
{}
这相当于
function
*
default
*
()
{}
// *not* legal JavaScript
export
{
*
default
*
as
default
};
*可提升声明*(函数声明、生成器函数声明)和类声明的默认导出类似于普通的内联导出,因为创建了命名的本地实体并对其进行了标记。
以下代码默认导出表达式 123
的结果
如果默认导出表达式,则会得到
本地名称:*default*
default
*default*
不是合法的标识符,因此您无法从模块内部访问该绑定。不同类型的导出创建的导出名称和本地名称,请参见“源代码模块记录”一节中的表 42。“静态语义:ExportEntries”一节提供了更多详细信息。您可以看到导出条目是静态设置的(在评估模块之前),评估导出语句在“运行时语义:评估”一节中进行了描述。
要理解 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/1 中,请求文件的成本相对较高。这种情况将在 HTTP/2 中发生变化,这就是为什么这个原因在那里不重要。
原因 #3 将继续引人注目。只有使用具有静态结构的模块格式才能实现这一点。
模块打包器 Rollup 证明了 ES6 模块可以有效地组合在一起,因为它们都适合单个作用域(在重命名变量以消除名称冲突之后)。这得益于 ES6 模块的两个特点
例如,请考虑以下两个 ES6 模块。
// lib.js
export
function
foo
()
{}
export
function
bar
()
{}
// main.js
import
{
foo
}
from
'./lib.js'
;
console
.
log
(
foo
());
Rollup 可以将这两个 ES6 模块打包成以下单个 ES6 模块(注意已消除的未使用导出 bar
)
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 等 linter 的一个流行功能;在 ECMAScript 6 中,大部分检查都可以由 JavaScript 引擎执行。
此外,还可以静态检查对命名导入(例如 lib.foo
)的任何访问。
宏仍在 JavaScript 未来的路线图上。如果 JavaScript 引擎支持宏,您可以通过库向其中添加新语法。Sweet.js 是一个用于 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 的模块应该具有静态结构。
ECMAScript 6 模块必须独立于引擎是同步加载模块(例如,在服务器上)还是异步加载模块(例如,在浏览器中)。它的语法非常适合同步加载,而异步加载则通过其静态结构来实现:因为您可以在静态时确定所有导入,所以您可以在评估模块体之前加载它们(其方式类似于 AMD 模块)。
支持循环依赖是 ES6 模块的一个关键目标。原因如下
循环依赖并非天生就是坏事。特别是对于对象,您有时甚至希望存在这种依赖关系。例如,在某些树(例如 DOM 文档)中,父级引用子级,子级引用父级。在库中,您通常可以通过精心设计来避免循环依赖。但是,在一个大型系统中,它们可能会发生,尤其是在重构期间。如果模块系统支持它们,那么这将非常有用,因为系统在您重构时不会崩溃。
Node.js 文档承认了循环依赖的重要性,Rob Sayre 提供了更多证据
数据点:我曾经为 Firefox 实现了一个类似 [ECMAScript 6 模块] 的系统。我在发布 3 周后被要求提供循环依赖支持。
Alex Fritze 发明并由我参与开发的系统并不完美,而且语法也不是很漂亮。但是,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
})
}
import
语句中使用变量吗? 不,您不能。请记住——导入的内容不得依赖于在运行时计算的任何内容。因此
// Illegal syntax:
import
foo
from
'some_module'
+
SUFFIX
;
import
语句中使用解构吗? 不,您不能。import
语句看起来像解构,但完全不同(静态的、导入是视图等)。
因此,您不能在 ES6 中执行以下操作
// Illegal syntax:
import
{
foo
:
{
bar
}
}
from
'some_module'
;
您可能想知道——如果我们可以简单地默认导出对象(如在 CommonJS 中),为什么还需要命名导出?答案是您无法通过对象强制执行静态结构,并且会失去所有相关的优势(本章对此进行了解释)。
eval()
模块的代码吗? 不,您不能。对于 eval()
来说,模块是太高级的结构。模块加载器 API 提供了从字符串创建模块的方法。从语法上讲,eval()
接受脚本(不允许使用 import
和 export
),而不是模块。
乍一看,ECMAScript 6 中内置的模块似乎是一个无聊的功能——毕竟,我们已经有了几个很好的模块系统。但是 ECMAScript 6 模块有几个新特性
ES6 模块也将——希望如此——结束目前占主导地位的 CommonJS 和 AMD 标准之间的碎片化。拥有一个单一的、原生的模块标准意味着
navigator
的属性。Math
和 JSON
之类的对象充当函数的命名空间。将来,可以通过模块提供此类功能。