JS学习笔记——模块
1.为什么JS最早没有模块而到ES6之后有了?
最初的JS脚本较小,对模块化的需求不高。后来随着JS的应用不断扩展,JS脚本的规模迅速增加。为了满足大规模JS工程的需求(松耦合以便合作和调试,复用性强以提高效率),制定JS模块化相关规范势在必行。JS有多个不同的模块化标准。例如:Node的JS模块化规范(CommonJS)和ES6制定模块化标准(ESM)。下文总结ESM的一些要点。2. ESM中的模块是什么?
ESM中的模块就是一个对外暴露(即“导出”)一些对象的,且可能引入(导入)别的模块暴露的对象的脚本文件。也就是说,ESM中的模块是一种特定的文件。这种文件的后缀是js或mjs。为了简便与适用性,一般使用js后缀。这种情况下,js模块文件在命名上与其他的js脚本文件没有区别。3. EMS语法
3.1 export
ESM使用export关键字导出对象。导出的对象可以是常量、变量、函数和类。
ESM有三种导出方法:
- 直接导出,即在导出对象定义前加一个“export”;
- 命名导出,即在模块末尾将需要导出的对象扩在一个大括号里,用"export"一并导出;
- 默认导出,即使用"export default"导出所需的对象作为默认导出对象;
//直接导出
export let a = 10;
export const b = "BBBB";
export const func = function(){
console.log(a);
}
//命名导出
let a = 10;
const b = "BBBB";
func(){
console.log(a);
}
export {a, b, func};
//默认导出
const b = "BBBB";
func(){
console.log(a);
}
export default func;
3.2 import
ESM使用import和from关键字导入对象。只有模块才能导入别的模块暴露的对象。
ESM有三种导入方法:
- (单)多项导入,即使用大括号把要导入的项括起来。这种情况下,导入的对象名称必须和源模块中的导出对象名称相同;
- 全体导入,即用星号代替导入模块的顶层命名空间;
- 默认导入,即导入其他模块默认导出的对象。这种情况下,导入名称可以和源模块导出名称不同。
这三种导入方法如下所示:
//多项导入
import {a, b, func} from "..../xx.js"; //模块路径
//单项导入
import {c} from ".../xx.js";
//全体导入
import * as ModuleName from ".../xxx.js";
console.log(ModuleName.a);
console.log(ModuleName.b); //如果被引用模块暴露对象a和b,那么ModuleName对象具有a和b属性。
//默认导入
import VariableName from ".../xxx.js";
3.3 重命名
重命名导出/导入对象可以使对象名称的语义更适用于使用场景,还可以解决导入对象重名的问题。
ESM使用**“as”**关键字对导出和导入对象重命名。
对导出对象重命名的方法如下所示:
export {
a as A,
b as B
};
导入对象名不可以重复。因此重命名在导入环节很有用。对导入对象重命名的方法如下所示:
import * as ModuleName from "module_path";
..................
import {a as A, b as B} from "module_path";
import {a as AA, b as BB} from "module_path_1";
3.4 模块聚合
有时,一些相关的导出项分布在多个模块中。在使用它们进行编程时,总是需要从多个模块导入对象,这很麻烦。为了减少导入语句,可以将多个相关联的模块导出的全部或部分对象聚合成一个父级模块,这样就可以一次性从父模块中导出所有模块了。
ESM使用**“export from”**语句聚合模块,如下所示:
export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';
上面的几句导出语句就构成了一个父模块。所以,父模块其实可以什么都不做,只是对子模块导出对象的简单粘合
3.5 其他
默认严格模式
模块自动开启严格模式(不管是否使用了"use strict")
await
模块导出、导入对象前可能需要进行一些异步操作,比如从服务器拉取数据或者代码之类的。在top-level使用await使得这样的情况更容易处理。这是ES 2021的新特性。
顶级导入/导出
只能在top-level(即一切函数作用域的外面)使用**“import”、“export”**关键字。也就是说,只能在顶级作用域导入/导出对象。
动态导入
可以使用**“import()”**函数在运行时异步导入模块。import函数可以被用于非top-level,也就是函数内部。
4. 浅析ESM底层原理
ES模块的底层原理可以参考Lin Clark的博客: ES Modules: A cartoon deep-dive。Lin Clark借助漫画,从整体到局部、从顶层流程到细节地阐述了ESM的工作原理。我认为他写得到非常好,也非常友好,但是作为一个小白,我感觉自己没有完全读透。
目前,我对ESM底层原理的理解为:
- 异步三段式:模块树结构构建、模块实例化(分配内存)和模块赋值依次异步进行。
- Live-Binding:源模块导出对象和施引模块引入对象之间动态绑定至一块固定内存,且施引模块不能改变引入对象指向的内存地址。(如下图(引自Lin Clark)所示)。这使得源模块的函数可以改变导出对象的值,而施引模块不能改变引用对象的值本身,但是可以改变其属性的值。
而这些特性解决了模块之间循环引用的问题。