万普插件库

jQuery插件大全与特效教程

为什么说 ESM 模块少不了声明提升?

1. ESM 有声明提升效果

在新的 ES6 模块语法中,JavaScript 引擎不必执行代码来获取所有的 import 和 export,其只会解析代码并 “知道” 要加载什么模块,即 整个导入导出的解析过程完全是静态的。

那么不禁让人有一个疑问,即 ESM 模块是否存在变量类型声明提升 (Variable Hoisting) 效果呢?


假如有如下的导入导出代码:

// exporter.mjs
const name = "“高级前端进阶”"
export {name}
// importer.mjs
// ES6 模块的导入/导出语句位置不影响模块语句执行结果
console.log('名字为',name);
import {name} from "./exporter.mjs";

此时执行代码将正常输出 高级前端进阶,即在 import 导入之前就可以确定 exporter.mjs 模块的导出值,看起来确实有 import 导入提升的错觉。

import 语句不一定必须是脚本中第一行代码,但必须出现在模块的顶层作用域,不过放在第一行通常是最佳实践。

那么为什么会有这种效果呢?接下来带着大家一起聊聊 ESM 模块系统的更多细节和原理。

2. 为什么 ESM 模块会有声明提升

2.1 模块系统构建 Module Record 和 Module Map

ESM 模块系统根据地址查找 JS 文件并通过网络下载, 最终将模块文件解析为 Module Record。当解析完当前 Module Record 后,通过 递归子模块的方式 获取整个模块的依赖图谱,整个过程包含 resolving -> fetching -> parsing。

该过程被称为静态分析,不会运行任何 JavaScript 代码,只会识别 export/import 关键字,这就是为何说不能在非全局作用域下使用 import 的原因,动态导入除外。

非全局作用域:不能在函数 / 条件语句 / 循环等中使用 import 或 export,因为 ES6 模块系统的设计目标之一是支持静态代码分析和优化

然而,如果多个文件同时依赖一个文件呢?此时会不会引起死循环?答案是不会的。ESM 使用 Module Map 对全局的 Module Record 进行追踪、缓存,这样就可以保证模块只被 fetch 一次,而每个全局作用域中会有一个独立的 Module Map。

2.2 模块系统构建 Module Environment Record

创建Module Environment Record对象

在所有 Module Record 被解析完后,接下来 JS 引擎需要把所有模块进行链接。JS 引擎以入口文件的 Module Record 作为起点,以 深度优先的顺序去递归链接模块,为每个 Module Record 创建一个 Module Environment Record,用于管理 Module Record 中的变量。

Module Environment Record 中有一个 Binding 用来存放 Module Record 导出的变量。如上图所示,模块 main.js 导出一个 count 变量,在 Module Environment Record 中的 Binding 就会有一个 count。此时就 相当于 V8 的编译阶段,创建一个模块实例对象并添加相应的属性和方法,此时值为 undefined 或者 null, 同时为其分配内存空间。

 此时模块依赖关系已确定,但模块中的代码尚未执行。

父子模块链接

在子模块 count.js 中使用了 import 关键字对 main.js 进行导入,而 count.js 的 import 和 main.js 的 export 的变量指向的内存位置是一致的 ,这样就把父子模块之间的关系链接起来了。

注意,此时模块导入 / 出绑定已建立,即模块中的变量、函数或类的声明已在作用域中注册。此时声明的值还未初始化,即代码还未运行。

模块求值阶段

此时模块彼此链接完成,模块系统进入 Evaluation 求值阶段。 此时会执行对应模块文件中顶层作用域的代码,即不在任何函数、类或块作用域内的代码,包括变量的初始化、函数的定义、类的定义,以及其他任何在模块顶层作用域中编写的 JavaScript 代码。

模块系统会确保链接阶段中定义变量的值被放入内存。比如下面的示例:

// moduleA.js
console.log('Module A is executing');
export const value = 42;
export function greet() {
  console.log('Hello from module A');
}
  • 解析阶段: 识别出 export const value 和 export function greet,并建立导出列表。
  • 链接阶段: 准备导出 value 和 greet 的绑定。
  • 执行阶段: 执行 console.log('Module A is executing'),初始化 value 为 42,定义 greet 函数。

执行顶层代码的过程确保模块在被其他模块使用之前完成必要的初始化,例如:设置初始状态、执行必要的副作用(如日志记录或初始化外部资源),以及准备导出的功能。模块的执行顺序通常由模块的依赖关系决定。

3. 模块系统 import 和变量提升有什么不同

模块系统 import 提升和变量提升存在以下差异:

  • 初始化位置不同:变量提升指的是在函数或全局作用域中,变量声明被提升到其所在作用域的顶部,但变量初始化仍然发生在原来的代码位置。
  • 静态提升 vs. 运行时赋值:与此不同,import 语句的解析和执行在代码执行之前完成,因此不存在 “提升” 到顶部的概念,因为本来就在执行之前被处理。

参考资料

https://stackoverflow.com/questions/29329662/are-es6-module-imports-hoisted

https://stackoverflow.com/questions/29329662/are-es6-module-imports-hoisted/29334761

https://youtube.com/watch?v=EvfRXyKa_GI

https://zhuanlan.zhihu.com/p/422704350

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言