ES
里所有的文件都会共享一个作用域 -- 全局作用域
,而不像其他语言会用一些如 package
来限制代码的作用域。这不仅使其他语言转来的人很困惑,也很容易滋生 bug
。 ES5
里,普遍的做法是把代码放在 IIFE
里,利用函数作用域来封装代码。ES6
为了解决这个问题,推出了模块 ( module
) 的概念。
module
是处于严格模式下的 js
代码。其变量只存在于 module
的作用域内,不会被添加到全局作用域里。module
只能通过 export
方式对外提供数据, 它也可以通过 import
方式从其他 module
引入数据。
module
里的 this
为 undefined
; 里面也不允许 HTML
风格的注释。
script
标签包裹的代码不是 module
。
下面会先介绍 module
是什么,然后介绍怎么用script
标签创造 module
。
上面说过 module
里的数据要提供给外部使用,必须用 export
出去。而被 export
可以是
- 任何变量
- 函数
class
声明
如下所示:
// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// export function
export function sum(num1, num2) {
return num1 + num1;
}
// export class
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// this function is private to the module
function subtract(num1, num2) {
return num1 - num2;
}
// define a function...
function multiply(num1, num2) {
return num1 * num2;
}
// ...and then export it later
export multiply;
需要注意的是,除了用 export default
, 其他所有的都必须是有名的,因为导出的数据都是用这个名字来获取的。
除了可以导出一条声明之外,如:
export let name = "Nicholas";
还可以导出一个引用:
export multiply;
有了定义好的模块,我么可以在另一个这样使用它:
import { identifier1, identifier2 } from "./example.js";
from
关键字后面跟模块路径, {}
里的是需要被引入模块里的变量的标识符,也就是上一节说到的 module
里变量或引用的名字。
需要注意的是 import
进来的变量,就相当于用 const
声明的变量,意味着他们不能被二次声明,其值也不能被修改。
有时需要导入模块里所有有的对象,可以如下操作:
// import everything
import * as example from "./example.js";
console.log(
example.sum(
1,
example.magicNumber
)
); // 8
console.log(example.multiply(1, 2)); // 2
也许有人会问,被全部导出的模块,其默认值怎么获取? 也比较容易,moduleIdentifier.default
就可以,如:
// module1
const name = 'jeyvie'
export const hobby = 'coding'
export default name;
// module2
import * as m1 from './module1.js'
console.log(m1.default); // jeyvie
console.log(m1.hobby); // coding
注意点:
moudle
为单例,里面的代码只会被执行一次。导入moudle
后,它就会存在内存里,下次导入的时候直接从内存里复用它。import
和export
只能放在函数和其他声明外面。
上文有讲,导入的 bindings (可理解为变量) 相当于在当前模块里声明的常量,不可被修改。但是通过导入的方法可以:
// example.js
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}
// moudle A
import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // throws an error
这是因为 moudle A
里的 name
和 example.js
里的 name
不是一个东西。前者是后者在 moudle A
里的一个名字(可以理解为引用吧)。所以 example.js
里的 name
的值变了,moudle A
里的 name
值也会跟着变。
-
可以修改导出数据的名字
function sum(num1, num2) { return num1 + num2; } export { sum as add };
这样修改之后,其他模块用这用
add
来导入sum
方法了 -
也可以修改导入数据的名字
import { add as sum } from "./example.js"; console.log(typeof add); // "undefined" console.log(sum(1, 2)); // 3
-
导出默认值
可以在
default
后直接跟声明语句export default function (num1, num2) { return num1 + num2; }
注意,可以是函数声明,类声明,
{}
。但不能是变量声明。如用let
,const
,var
等声明变量。default
后也可以使用标识符function sum(num1, num2) { return num1 + num2; } export default sum;
-
导入默认值
-
可以直接导入, 命名随意,只要不与当前模块里其他变量名冲突就可以:
// import the default import sum from "./example.js"; console.log(sum(1, 2)); // 3
-
也可以同时导入默认值和其他值:
// example.js export let color = "red"; export default function (num1, num2) { return num1 + num2; } // moudle import sum, { color } from "./example.js"; console.log(sum(1, 2)); // 3 console.log(color); // "red"
这种语法里需要注意,默认值必须在其他值前面
-
default
还可以这样引入,放在{}
里,然后重命名,如:import { default as sum, color } from "./example.js"; console.log(sum(1, 2)); // 3 console.log(color); // "red"
-
-
我们可以把导入的东西直接导出去:
export { sum } from "./example.js";
等同于:
import { sum } from "./example.js"; export { sum }
-
我们还可以给它重命名
export { sum as add } from "./example.js";
-
可以把导入模块内容都导出去
export * from "./example.js";
但这样有个问题需要注意: 导出
./example.js
里所有的东西,那也就包括了他的default
, 这会影响当前模块的default
输出,因此,你就没法输出一个新的default
了。
模块里的变量虽然不会被添加到全局变量里,但能访问全局变量。
所以一些模块,可以用来做一些拓展全局方法之类的事,而不导出东西。这样的 moudle
, 在 polyfill
里常用到。下面举个例子:
// example.js
// module code without exports or imports
Array.prototype.pushAll = function (items) {
// items must be an array
if (!Array.isArray(items)) {
throw new TypeError("Argument must be an array.");
}
// use built-in push() and spread operator
return this.push(...items);
};
其他模块可以直接导入,其 Array
实例都有 pushAll
方法:
import "./example.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);
ES6
定义了模块的语法,但没有定义该如何加载模块。相比教于为所有的 js
环境订立一个加载的规范,ES6
只确定了语法,并把加载算法抽象为一个未定义的内部操作 -- HostResolveImportedModule
。浏览器 和 Node.js
开发者自己决定如何根据各自的环境实现 HostResolveImportedModule
。
在 ES6
之前,js
有三种加载 js
的方法
- 用
script
src
加载 - 用
script
包裹js
代码 - 在
worker
里执行加载的代码
为了支持 Es6
模块,需要升级这三种方法。
<script>
的 type
属性不存在或包含 js
内容类型如 text/javascript
时,它里面的或用src
引入的内容会被解析成普通的 js
代码,而不是 js
模块。
当将它的 type
属性设置为 module
时,它就会将它的内容解析成模块。如:
<!-- load a module JavaScript file -->
<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
加载模块是,之所以将type
设置为module
, 是因为 一, 根据内容区分module
和 一般js
比较苦难 二,浏览器遇到不识别的type
类型是,会忽略掉它。这样,就可以做到向后兼容了
模块和其他 js
不一样的地方在于,他们会用 import
来表示必须加载的其他文件,然后代码才能正确执行。为了支持这个特定,<script type="module">
的表现就像是加了 defer
属性。
虽然对其他 js
文件而言, defer
是可选的,但加载模块时总会用到它。所以, 当 HTML
解析器遇到有 src
的 <script type="module">
,它会马上加载文件但直到 html
解析完,才会以它们在html
里出现的顺序执行。所以说,第一个 <script type="module">
总会比第二个先执行,即使其中一个是内联而不是用src
加载的代码。
<!-- this will execute first -->
<script type="module" src="module1.js"></script>
<!-- this will execute second -->
<script type="module">
import { sum } from "./example.js";
let result = sum(1, 2);
</script>
<!-- this will execute third -->
<script type="module" src="module2.js"></script>
每个模块,都会先处理 import
内容,再执行其他代码。
先递归加载,然后递归执行。
script
加了 async
后,会使得 js
文件被加载后就马上执行。文档里中的顺序,不会影响他们的执行顺序。这对于 module
而言也是一样的。只是 module
里会保证模块的依赖加载完毕后再执行模块里的代码:
<!-- no guarantee which one of these will execute first -->
<script type="module" async src="module1.js"></script>
<script type="module" async src="module2.js"></script>
这个例子中,两个模块的异步加载,谁先加载完,谁先执行。
worker
可以在页面环境之外,执行加载的文件:
// load script.js as a script
let worker = new Worker("script.js");
可以传入第二个参数,来表示这是模块
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });
worker scipts
和 worker modules
的区别
- 前者只能加载同一个域的文件,后者也可以加载
CORS
- 前者可以用
self.importScripts()
加载文件,但这在后者里不管用,因为module
要用import
/
开头的指向根目录./
开头的指向当前目录../
指向父级目录URL
格式
比如当前文件路径为 https://www .example.com/modules/module.js
:
// imports from https://www.example.com/modules/example1.js
import { first } from "./example1.js";
// imports from https://www.example.com/example2.js
import { second } from "../example2.js";
// imports from https://www.example.com/example3.js
import { third } from "/example3.js";
// imports from https://www2.example.com/example4.js
import { fourth } from "https://www2.example.com/example4.js";
以下是无效的:
// invalid - doesn't begin with /, ./, or ../
import { first } from "example.js";
// invalid - doesn't begin with /, ./, or ../
import { second } from "example/index.js";
ES6
引入 module
来封装代码,它跟一般 js
的区别在于它的变量不会被添加到全局作用域里。module
里的 this
是undefined
。
module
需要对外暴露的数据要要export
, 需要引入的模块要用import
加载 js
有三种方法,所以对应的加载 module
的方法也有三种。
模块默认会按他们在文档中的顺序执行,如果添加了 async
的话,那哪个模块先加载完,哪个模块就先执行。
不管哪种,模块内部执行时,都会先保证 import
完毕。