一文彻底搞定es6模块化
前情回顾
- 在上篇文章中我们讲到了
CommonJs
,如果还没有看,可以查找本文章所在的专栏进行学习。 -
CommonJs
有很多优秀的特性,下面我们再简单的回顾一下:
模块代码只在加载后运行;
模块只能加载一次;
模块可以请求加载其他模块;
支持循环依赖;
模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互;
天下苦 CommonJs 久矣
-
Es Module
的独特之处在于,既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载。 - 支持
Es module
模块的浏览器可以从顶级模块加载整个依赖图,且是异步完成。浏览器会解析入口模块,确定依赖,并发送对依赖模块的请求。这些文件通过网络返回后,浏览器就会解析它们的依赖,,如果这些二级依赖还没有加载,则会发送更多请求。 - 这个异步递归加载过程会持续到整个应用程序的依赖图都解析完成。解析完成依赖图,引用程序就可以正式加载模块了。
-
Es Module
不仅借用了CommonJs
和AMD
的很多优秀特性,还增加了一些新行为:
Es Module
默认在严格模式下执行;Es Module
不共享全局命名空;Es Module
顶级的this
的值是undefined
(常规脚本是window
);模块中的
var
声明不会添加到window
对象;Es Module
是异步加载和执行的;
export 和 import
- 模块功能主要由两个命令构成:
exports
和import
。 -
export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
export的基本使用
- 导出的基本形式:
export const nickname = "moment"; export const address = "广州"; export const age = 18;
- 当然了,你也可以写成以下的形式:
const nickname = "moment"; const address = "广州"; const age = 18; export { nickname, address, age };
- 对外导出一个对象和函数
export function foo(x, y) { return x + y; } export const obj = { nickname: "moment", address: "广州", age: 18, }; // 也可以写成这样的方式 function foo(x, y) { return x + y; } const obj = { nickname: "moment", address: "广州", age: 18, }; export { foo, obj };
- 通常情况下,
export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
const address = "广州"; const age = 18; export { nickname as name, address as where, age as old };
- 默认导出,值得注意的是,一个模块只能有一个默认导出:
export default "foo"; export default { name: 'moment' } export default function foo(x,y) { return x+y } export { bar, foo as default };
export 的错误使用
- 导出语句必须在模块顶级,不能嵌套在某个块中:
if(true){ export {...}; }
-
export
必须提供对外的接口:
// 1只是一个值,不是一个接口export 1// moment只是一个值为1的变量const moment = 1export moment// function和class的输出,也必须遵守这样的写法function foo(x, y) { return x+y }export foo复制代码
import的基本使用
- 使用
export
命令定义了模块的对外接口以后,其他js文件就可以通过import
命令加载整个模块
import {foo,age,nickname} from '模块标识符'
- 模块标识符可以是当前模块的相对路径,也可以是绝对路径,也可以是纯字符串,但不能是动态计算的结果,例如凭借的字符串。
-
import
命令后面接受一个花括弧,里面指定要从其他模块导入的变量名,而且变量名必须与被导入模块的对外接口的名称相同。 - 对于导入的变量不能对其重新赋值,因为它是一个只读接口,如果是一个对象,可以对这个对象的属性重新赋值。导出的模块可以修改值,导入的变量也会跟着改变。
- 从上图可以看得出来,对象的属性被重新赋值了,而变量的则报了
Assignment to constant variable
的类型错误。 - 如果模块同时导出了命名导出和默认导出,则可以在
import
语句中同时取得它们。可以依次列出特定的标识符来取得,也可以使用*
来取得:
// foo.js export default function foo(x, y) { return x + y; } export const bar = 777; export const baz = "moment"; // main.js import { default as foo, bar, baz } from "./foo.js"; import foo, { bar, baz } from "./foo.js"; import foo, * as FOO from "./foo.js";
动态 import
- 标准用法的
import
导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。 - 关键字
import
可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个promise
。
import("./foo.js").then((module) => { const { default: foo, bar, baz } = module; console.log(foo); // [Function: foo] console.log(bar); // 777 console.log(baz); // moment});复制代码
使用顶层 await
- 在经典脚本中使用
await
必须在带有async
的异步函数中使用,否则会报错:
import("./foo.js").then((module) => { const { default: foo, bar, baz } = module; console.log(foo); // [Function: foo] console.log(bar); // 777 console.log(baz); // moment });
- 而在模块中,你可以直接使用
Top-level await
:
const p = new Promise((resolve, reject) => { resolve(777); });const result = await p;console.log(result); // 777正常输出
import 的错误使用
- 由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 错误 import { 'b' + 'ar' } from './foo.js'; // 错误 let module = './foo.js'; import { bar } from module; // 错误 if (x === 1) { import { bar } from './foo.js'; } else { import { foo } from './foo.js'; }
在浏览器中使用 Es Module
- 在浏览器上,你可以通过将
type
属性设置为module
用来告知浏览器将script
标签视为模块。
<script type="module" src="./main.mjs"></script><script type="module"></script>
- 模块默认情况下是延迟的,因此你还可以使用
defer
的方式延迟你的nomodule
脚本:
<script type="module"> console.log("模块情况下的"); </script> <script src="./main.js" type="module" defer></script> <script> console.log("正常 script标签"); </script>
- 在浏览器中,引入相同的
nomodule
脚本会被执行多次,而模块只会被执行一次:
<script src="./foo.js"></script> <script src="./foo.js"></script> <script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
模块的默认延迟
- 默认情况下,
nomodule
脚本会阻塞HTML
解析。你可以通过添加defer
属性来解决此问题,该属性是等到HTML
解析完成之后才执行。
-
defer
和async
是一个可选属性,他们只可以选择其中一个,在nomodule
脚本下,defer
等到HTML
解析完才会解析当前脚本,而async
会和HTML
并行解析,不会阻塞HTML
的解析,模块脚本可以指定async
属性,但对于defer
无效,因为模块默认就是延迟的。 - 对于模块脚本,如果存在
async
属性,模块脚本及其所有依赖项将于解析并行获取,并且模块脚本将在它可用时进行立即执行。
Es Module 和 Commonjs 的区别
- 讨论
Es Module
模块之前,必须先了解Es Module
与Commonjs
完全不同,它们有三个完全不同:
-
CommonJS
模块输出的是一个值的拷贝,Es Module
输出的是值的引用; -
CommonJS
模块是运行时加载,Es Module
是编译时输出接口。 -
CommonJS
模块的require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
- 第二个差异是因为
CommonJS
加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而Es Module
不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 -
Commonjs
输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。具体可以看上一篇写的文章。 -
Es Module
的运行机制与CommonJS
不一样。JS引擎
对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,import
就是一个连接管道,原始值变了,import
加载的值也会跟着变。因此,Es Module
是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
Es Module 工作原理的相关概念
- 在学习工作原理之前,我们不妨来认识一下相关的概念。
Module Record
- 模块记录(
Module Record
) 封装了关于单个模块(当前模块)的导入和导出的结构信息。此信息用于链接连接模块集的导入和导出。一个模块记录包括四个字段,它们只在执行模块时使用。其中这四个字段分别是:
-
Realm
: 创建当前模块的作用域; -
Environment
:模块的顶层绑定的环境记录,该字段在模块被链接时设置; -
Namespace
:模块命名空间对象是模块命名空间外来对象,它提供对模块导出绑定的基于运行时属性的访问。模块命名空间对象没有构造函数; -
HostDefined
:字段保留,以按host environments
使用,需要将附加信息与模块关联。
Module Environment Record
- 模块环境记录是一种声明性环境记录,用于表示ECMAScript模块的外部作用域。除了普通的可变和不可变绑定之外,模块环境记录还提供了不可变的
import
绑定,这些绑定提供了对存在于另一个环境记录中的目标绑定的间接访问。
不可变绑定就是当前的模块引入其他的模块,引入的变量不能修改,这就是模块独特的不可变绑定。
Es Module 的解析流程
- 在开始之前,我们先大概了解一下整个流程大概是怎么样的,先有一个大概的了解:
- 阶段一:构建(
Construction
),根据地址查找js
文件,通过网络下载,并且解析模块文件为Module Record
; - 阶段二:实例化(
Instantiation
),对模块进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址; - 阶段三:运行(
Evaluation
),运行代码,计算值,并且将值填充到内存地址中;
Construction 构建阶段
-
loader
负责对模块进行寻址及下载。首先我们修改一个入口文件,这在HTML
中通常是一个<script type="module"></script>
的标签来表示一个模块文件。
- 模块继续通过
import
语句声明,在import
声明语句中有一个 模块声明标识符(ModuleSpecifier
),这告诉loader
怎么查找下一个模块的地址。
- 每一个模块标识号对应一个
模块记录(Module Record)
,而每一个模块记录
包含了JavaScript代码
、执行上下文
、ImportEntries
、LocalExportEntries
、IndirectExportEntries
、StarExportEntries
。其中ImportEntries
值是一个ImportEntry Records
类型,而LocalExportEntries
、IndirectExportEntries
、StarExportEntries
是一个ExportEntry Records
类型。
ImportEntry Records
- 一个
ImportEntry Records
包含三个字段ModuleRequest
、ImportName
、LocalName
;
- ModuleRequest: 一个模块标识符(
ModuleSpecifier
); - ImportName: 由
ModuleRequest
模块标识符的模块导出所需绑定的名称。值namespace-object
表示导入请求是针对目标模块的命名空间对象的; - LocalName: 用于从导入模块中从当前模块中访问导入值的变量;
- 详情可参考下图:
- 下面这张表记录了使用
import
导入的ImportEntry Records
字段的实例:
导入声明 (Import Statement From) | 模块标识符 (ModuleRequest) | 导入名 (ImportName) | 本地名 (LocalName) |
---|---|---|---|
import React from "react"; | "react" | "default" | "React" |
import * as Moment from "react"; | "react" | namespace-obj | "Moment" |
import {useEffect} from "react"; | "react" | "useEffect" | "useEffect" |
import {useEffect as effect } from "react"; | "react" | "useEffect" | "effect" |
ExportEntry Records
- 一个
ExportEntry Records
包含四个字段ExportName
、ModuleRequest
、ImportName
、LocalName
,和ImportEntry Records
不同的是多了一个ExportName
。
- ExportName: 此模块用于导出时绑定的名称。
-
下面这张表记录了使用
export
导出的ExportEntry Records
字段的实例:导出声明 导出名 模块标识符 导入名 本地名 export var v; "v" null null "v" export default function f() {} "default" null null "f" export default function () {} "default" null null "default" export default 42; "default" null null "default" export {x}; "x" null null "x" export {v as x}; "x" null null "v" export {x} from "mod"; "x" "mod" "x" null export {v as x} from "mod"; "x" "mod" "v" null export * from "mod"; null "mod" all-but-default null export * as ns from "mod"; "ns "mod" all null 回到主题
只有当解析完当前的
Module Record
之后,才能知道当前模块依赖的是那些子模块,然后你需要resolve
子模块,获取子模块,再解析子模块,不断的循环这个流程 resolving -> fetching -> parsing,结果如下图所示:
- 这个过程也称为
静态分析
,不会运行JavaScript代码,只会识别export
和import
关键字,所以说不能在非全局作用域下使用import
,动态导入除外。 - 如果多个文件同时依赖一个文件呢,这会不会引起死循环,答案是不会的。
-
loader
使用Module Map
对全局的MOdule Record
进行追踪、缓存这样就可以保证模块只被fetch
一次,每个全局作用域中会有一个独立的 Module Map。
MOdule Map 是由一个 URL 记录和一个字符串组成的key/value的映射对象。URL记录是获取模块的请求URL,字符串指示模块的类型(例如。“javascript”)。模块映射的值要么是模块脚本,null(用于表示失败的获取),要么是占位符值“fetching(获取中)”。
linking 链接阶段
- 在所有
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
的变量指向的内存位置是一致的,这样就把父子模块之间的关系链接起来了。如下图所示:
- 需要注意的是,我们称
export
导出的为父模块,import
引入的为子模块,父模块可以对变量进行修改,具有读写权限,而子模块只有读权限。
Evaluation 求值阶段
- 在模块彼此链接完之后,执行对应模块文件中顶层作用域的代码,确定链接阶段中定义变量的值,放入内存中。
Es module 是如何解决循环引用的
- 在
Es Module
中有5种状态,分别为unlinked
、linking
、linked
、evaluating
和evaluated
,用循环模块记录(Cyclic Module Records
)的Status
字段来表示,正是通过这个字段来判断模块是否被执行过,每个模块只执行一次。这也是为什么会使用Module Map
来进行全局缓存Module Record
的原因了,如果一个模块的状态为evaluated
,那么下次执行则会自动跳过,从而包装一个模块只会执行一次。Es Module
采用深度优先
的方法对模块图进行遍历,每个模块只执行一次,这也就避免了死循环的情况了。
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
- 看下面的例子,所有的模块只会运行一次:
// main.js import { bar } from "./bar.js"; export const main = "main"; console.log("main"); // foo.js import { main } from "./main.js"; export const foo = "foo"; console.log("foo"); // bar.js import { foo } from "./foo.js"; export const bar = "bar"; console.log("bar");
- 通过
node
运行main.js
,得出以下结果:
- 好了,这篇文章到这也就结束了。《JavaScript视频教程》
以上是一文彻底搞定es6模块化的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

async是es7的。async和await是ES7中新增内容,是对于异步操作的解决方案;async/await可以说是co模块和生成器函数的语法糖,用更加清晰的语义解决js异步代码。async顾名思义是“异步”的意思,async用于声明一个函数是异步的;async和await有一个严格规定,两者都离不开对方,且await只能写在async函数中。

在ES6中,可以利用数组对象的reverse()方法来实现数组反转,该方法用于颠倒数组中元素的顺序,将最后一个元素放在第一位,而第一个元素放在最后,语法“array.reverse()”。reverse()方法会修改原始数组,如果不想修改需要配合扩展运算符“...”使用,语法“[...array].reverse()”。

步骤:1、将两个数组分别转为set类型,语法“newA=new Set(a);newB=new Set(b);”;2、利用has()和filter()求差集,语法“new Set([...newA].filter(x =>!newB.has(x)))”,差集元素会被包含在一个set集合中返回;3、利用Array.from将集合转为数组类型,语法“Array.from(集合)”。

为了浏览器兼容。ES6作为JS的新规范,加入了很多新的语法和API,但现代浏览器对ES6新特性支持度不高,所以需将ES6代码转为ES5代码。在微信web开发者工具中,会默认使用babel将开发者ES6语法代码转换为三端都能很好支持的ES5的代码,帮助开发者解决环境不同所带来的开发问题;只需要在项目中配置勾选好“ES6转ES5”选项即可。

在es6中,暂时性死区是一个语法错误,是指let和const命令使区块形成封闭的作用域。在代码块内,使用let/const命令声明变量之前,该变量都是不可用的,在变量声明之前属于该变量的“死区”;这在语法上,称为“暂时性死区”。ES6规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

不是,require是CommonJS规范的模块化语法;而es6规范的模块化语法是import。require是运行时加载,import是编译时加载;require可以写在代码的任意位置,import只能写在文件的最顶端且不可在条件语句或函数作用域中使用;require运行时才引入模块的属性所以性能相对较低,import编译时引入模块的属性所所以性能稍高。

map是有序的。ES6中的map类型是一种储存着许多键值对的有序列表,其中的键名和对应的值支持所有数据类型;键名的等价性判断是通过调用“Objext.is()”方法来实现的,所以数字5与字符串“5”会被判定为两种类型,可以分别作为两种独立的键出现在程序中。
