ES6 模块
模块的定义
模块(Modules)是使用不同方式加载的JS文件。
1】模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式。
2】在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在。
3】模块顶级作用域的this
值为undefined
。
4】模块不允许在代码中使用HTML风格的注释。
5】对于需要让模块外部代码访问的内容,模块必须导出它们。
6】允许模块从其他模块导入绑定。
模块的真实力量是按需导出和导入代码的能力,而不用将所有的内容放在同一文件内。对于导出与导入的清楚理解,是辨别模块与脚本差异的基础。
基本的导出
export
关键字将已发布代码部分公开给其他模块。最简单的方式就是讲export
放置在任意变量、函数或类声明之前,从模块中将它们公布出去。
// 导出数据
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// 导出函数
export function sum(num1, num2){
return num1 + num2;
}
// 导出类
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// 此函数为模块私有
function subtract(num1, num2){
return num1-num2;
}
// 定义一个函数
function multiply(num1, num2) {
return num1*num2;
}
// 将其导出
export { multiply };
注意:
1】除了export
关键字之外,每个声明都与正常形式完全一致。每个被导出的函数或类都有名称,这是因为导出的函数声明与类声明必须要有名称。不能使用这种语法来导出匿名函数或者匿名类,除非使用了default
关键字。
2】multiply()函数没有在定义时被导出。这是因为不仅能导出声明,还可以导出引用。
3】此例并未导出subtract()函数。此函数在模块外部不可被访问,因为任意没有被显式导出的变量、函数或类都会在模块内保持私有。
基本的导入
import { identifier1, identifier2 } from "./example.js"'\;
导入绑定的列表看起来与对象解构相似,但实则并无关联。
当从模块导入了一个绑定时,该绑定表现得就像使用了const的定义。这意味着不能再定义另一个同名变量(包括导入另一个同名绑定),也不能在对应的import语句之前使用此标识符(也就是要受暂时性死区限制),更不能修改它的值。
导入单个绑定
import { sum } from "./example.js";
console.log(sum(1,2)); // 3
sum = 1; //出错
导入多个绑定
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1,2)); // 2
完全导入一个模块
允许将整个模块当做单一对象进行导入,该模块的所有导出都会作为对象的属性存在。
// 完全导入
import * as example from "./example.js";
console.log(example.sum(1,example.magicNumber)); // 8
console.log(multiply(1,2)); // 2
在此代码中,example.js中所有导出的绑定都被加载到一个名为example的对象中,具名导出(sum()函数、multiple()函数与magicNumber)都成为example的可用属性。
这种导入格式被称为命名空间导入,这是因为该example对象并不在example.js文件中,而是作为一个命名空间对象被创建使用,其中包含了example.js的所有导出成员。
无论对同一模块使用了多少次import语句,该模块都只会被执行一次。
在导出模块的代码执行之后,已被实例化的模块就会被保留在内存中,并随时都被其他import所引用。
模块语法的限制
export 与 import 都有一个重要的限制,那就是它们必须被用在其他语句或表达式的外部。例如,以下代码有语法错误。
if(flag) {
export flag; // 语法错误
}
此处的 export 语句位于一个 if 语句内部,这是不被许可的。导出语句不能是有条件的,也不能以任何方式动态使用。原因之一是模块语法需要让 JS 能静态判断需要导出什么,正因为此,你只能在模块的顶级作用域使用 export 。类似的,你不能在一个语句内部使用 import ,也只能将其用在顶级作用域。这意味着以下代码也有语法错误:
function tryImport() {
import flag from "./example.js"; // 语法错误
}
出于与不能动态导出绑定相同的原因,你也不能动态导入绑定。 export 与 import 关
键字被设计为静态的,以便让诸如文本编辑器之类的工具能轻易判断模块有哪些信息可
用。
导入绑定的一个微妙怪异点
ES6的 import 语句为变量、函数与类创建了只读绑定,而不像普通变量那样简单引用了原始绑定。尽管导入绑定的模块无法修改绑定的值,但负责导出的模块却能做到这一点。
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}
当你导入这两个绑定后,setName()函数还可以改变name的值。
import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // 报错
调用 setName("Greg") 会回到导出 setName() 的模块内部,并在那里执行,从而将 name 设置为 "Greg" 。注意这个变化会自动反映到所导入的 name 绑定上,这是因为绑定的 name是导出的 name 标识符的本地名称,二者并非同一个事物。
重命名导出与导入
假设想用不同的名称来导出一个函数,可以使用as
关键字来指定新的名称,以便在模块外部用此名称指代目标函数。
function sum(num1, num2) {
return num1 + num2
}
export { sum as add };
当另一个模块需要导入此模块时,它必须改用add
这个名称:
import { add } from "./example.js";
假若模块导入函数时想用另一个名称,同样也可以用as
关键字:
import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2));
模块的默认值
模块的默认值是使用 default 关键字所指定的单个变量、函数或类,而你在每个模块中只能设置一个默认导出,将default关键字用于多个导出会是语法错误。
导出默认值
以下是使用default关键字的一个简单例子:
export default function(num1, num2) {
return num1 + num2;
}
此模块将一个函数作为默认值进行导出,defalut
关键字标明了这是一个默认导出。此函数并不需要有名称,因为它就代表这个模块自身。
你也能在export defalut
后面放置一个标识符,以指定默认的导出,正如:
function sum(num1, num2) {
return num1 + num2;
}
export default sum;
此处 sum() 函数先被定义了,随后它作为模块的默认值被导出。若默认值需要计算才能得出,你或许会选择这种方式。
将标识符作为默认导出来指定的第三种方式,是使用重命名语法,如下:
function sum(num1, num2) {
return num1 + num2;
}
export { sum as default };
default 标识符有特别含义,既作为重命名导出,又标明了模块需要使用的默认值。由于default 在 JS 中是一个关键字,它就不能被用作变量、函数或类的名称(但它可以被用作属性名称)。因此使用 default 来重命名一个导出是个特例,与非默认导出的语法保持了一致性。若你想用单个语句一次性进行多个导出,并要求包含默认导出,这种语法就非常有用。
导入默认值
// 导入默认值
import sum from "./example.js";
console.log(sum(1, 2)); // 3
这个导入语句从 example.js 模块导入了其默认值。注意此处并未使用花括号,与之前在非默认的导入中看到的不同。本地名称 sum 被用于代表目标模块所默认导出的函数。
对于既导出了默认值、又导出了一个或更多非默认的绑定的模块,你可以使用单个语句来导入它的所有导出绑定。例如,假设你有这么一个模块:
export let color = "red";
export default function(num1, num2) {
return num1 + num2;
}
你可以像下面这样使用 import 语句,来同时导入 color 以及作为默认值的函数:
import sum, { color } from "./example.js";
console.log(sum(1, 2)); // 3
console.log(color); // "red"
要记住在 import 语句中默认名称必须位于非默认名称之前。
如同导出默认值,你也能使用重命名语法进行默认值的导入:
// 等价于上个例子
import { default as sum, color } from "example";
console.log(sum(1, 2)); // 3
console.log(color); // "red"
绑定的再导出
将当前模块已导入的内容重新再导出。
import { sum } from "./example";
export { sum };
export { sum } from "./example.js";
也可以选择将一个值用不同名称导出:
export { sum as add } from "./example.js";
将来自另一个模块的所有值完全导出,可以使用星号(*)模式:
export * from "./example.js";
无绑定的导入
有些模块也许没有进行任何导出,相反只是修改全局作用域的对象。尽管这种模块的顶级变量、函数或类最新并不会自动被加入全局作用域,但这并不意味着该模块无法访问全局作用域。诸如Array与Object之类的内置对象的共享定义在模块内部是可访问的,并且对于这些对象的修改会反映到其他模块中。
例如:想为所有数组添加一个pushAll()方法,可以像下面这样定义一个模块:
// 没有导出与导入的模块
Array.prototype.pushAll = function(items) {
// items必须为一个数组
if(!Array.isArray(items)){
throw new TypeError("Argument must be an array");
}
// 使用内置的push()与扩展运算符
return this.push(...items);
}
这是一个有效的模块,尽管此处没有任何导出与导入。此代码可以作为模块或者脚本来使用。由于它没有导出任何东西,你可以使用简化的导入语法来执行此模块的代码,而无需导入任何绑定:
import "./example.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);
此代码导入并执行了包含pushAll()的模块,于是pushAll()就被添加到数组的原型上。这意味着现在pushAll()在当前模块内的所有数组上都可用。
无绑定的导入最有可能被用于创建 polyfill 与 shim (为新语法在旧环境中运行提供向下兼容的两种方式)。
在Web浏览器中使用模块
// 为了执行模块,添加了"module"值作为type的选项。将type设置为"module",就告诉浏览器将内联代码或是指定文件中的代码当做模块,而不是当做脚本。
<script type="module" src="module.js"></script>
<!-- include a module inline -->
<script type="module">
import { sum } from "./example.js";
let result = sum(1, 2);
</script>
// 此例中第一个 '<script>' 元素使用 src 加载了外部模块文件,与加载脚本唯一的区别是将 type 指定为 "module" 。第二个 '<script>' 元素则包含了一个直接嵌入到网页内的模块,result 变量并未被暴露到全局,因为它只在使用 '<script>'元素定义的这个模块内部存在,因此也没有被添加为 window 对象的属性。