zl程序教程

您现在的位置是:首页 >  后端

当前栏目

随笔-深入理解ES6模块化(一)

ES6 深入 理解 随笔 模块化
2023-09-14 09:13:41 时间

目录

导出语法深入理解

用法注意

分别暴露 export

统一暴露 export {}

默认暴露 export default

 导出的到底是啥?

导出语法总结

导入语法深入理解

用法注意

具名导入

默认导入

整体导入

导入的到底是啥

导入语法总结

导入导出复合写法


导出语法深入理解

用法注意

分别暴露 export

在阮一峰的ES6教程中关于export命令有这么一个注意点

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

所以下面的代码报错原因都是:export后面跟的不是变量 

// 报错
export 1;

// 报错
var m = 1;
export m;

有人肯定会有疑问,export 1 中 1确实不是变量,可以理解报错,但是 export m 中 m 就是变量啊,为什么还会报错?

其实这是我们一直以来口误导致的认识错误,所谓变量,其实只有在声明时的一瞬间有,声明过后,不管是单独使用,还是作为实参传递,它都是作为一个表达式,而不是变量。

我们可以通过JS代码编译成的AST抽象语法树来验证AST explorer

统一暴露 export {}

统一暴露的语法 export {} 中 {} 是一个对象吗?

var m = 1;
export {m};

如上图,很多人错误的理解{m}为是一个对象,且使用了对象的简写形式,其实不然。

export {} 是作为一个整体的语法存在,在{}中放入的是模块需要对外暴露内部变量,在形式上其实这里{}更像是一个数组,其数组元素就是模块内部变量。

而提供export {} 命令的原因,更多的是解决 export命令的不足,因为export必须要求和模块内部变量一一对应,这就需要 export 命令后面必须跟着声明式语句,而不能是表达式,限制性太高。

而 export {} 的提出,可以在表面上实现 基于模块内部变量表达式形式的导出。

默认暴露 export default

默认暴露和上面两种暴露最大的区别是,一个模块中默认暴露只能使用一次,而export 和 export {} 可以使用多次。

而默认暴露主要场景就是,针对一个模块只暴露一个接口的场景,此时我们需要对比export 和 export {} 只暴露一个接口的场景

export(导出时需要满足一一对应要求,导入时需要使用import {}命令接收 )

// b.js

export function b(){
    console.log('b')
}
// a.js

import {b} from './b.js'

export (导出时必须要搞一个变量名给export {} 命令,否则export {} 无法获取变量,且必须使用 import {} 命令接收)

// b.js

var b = function(){
    console.log('b')
}

export {b}
// a.js

import {b} from './b.js'

export default (由于默认暴露只能暴露一个接口,所以这个接口不需要名字就可以限定,同时模块只会暴露一个接口给import,所以imort也不需要名字限定,直接导出该接口即可,接口名可以任意定)

// b.js

export default function(){
    console.log('b')
}
// a.js

import haha from './b.js'

实际上 export default 底层还是 定义了一个接口,该接口的名字是 default 

相当于

function test() {
    console.log(1)
}

export {
    test as default
}

其次 分别暴露和统一暴露可以统称为具名暴露,即它们暴露的接口必须有名字,否则就会报错。而默认暴露的接口可以没有名字。

另外 具名暴露 不能暴露字面量,即没有变量名的值,因为它们需要体现和模块内部变量的一一对应关系。

而默认暴露由于只需要暴露一个接口,所以直接内置了一个默认接口default,既作为模块输出接口,又作为模块内部变量名字,所以默认暴露可以暴露字面量,底层也符合 输出接口和模块内部变量的一一对应关系。

 导出的到底是啥?

export,export {}, export default 这三个导出命令,导出的东西到底是啥?

MDN的介绍

在创建JavaScript模块时,export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。被导出的绑定值依然可以在本地进行修改。在使用import进行导入时,这些绑定值只能被导入模块所读取,但在export导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。

The export statement is used when creating JavaScript modules to export live bindings to functions, objects, or primitive values from the module so they can be used by other programs with the import statement. The value of an imported binding is subject to change in the module that exports it. When a module updates the value of a binding that it exports, the update will be visible in its imported value.

阮一峰教程

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口import命令用于输入其他模块提供的功能。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

MDN对于export导出的东西描述的很含糊,阮一峰大神在极力试图描述清楚export导出的东西,反复地使用了 “对外接口”,“值得引用”,“静态定义”,“动态引用”这几个词。

但是,我还是感觉阮一峰对于’模块‘和’模块的导出‘一直没说清楚,老是将两者混淆,不知道是用词不当,还是词不达意。

我觉得 无论是CommonJS还是ES6,模块都是指JS文件,每一个JS文件都是一个模块,因为每个JS文件只要被当成模块引入,就会形成一个模块作用域。即JS文件中的变量不会污染到全局。

阮一峰自己也在文章里面说了

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

所以模块就是指JS文件(暂时不考虑其他类型文件的情况),这没啥好争议的。

当前需要搞清楚的是ES6模块的导出,即JS文件中export命令导出的是啥?

我们已经知道CommonJS模块的导出module.exports是一个对象了,阮一峰也明确说了ES6模块的导出,即JS文件中export命令输出的东西不是一个对象,而是一个对外接口,是一个值得引用,是一个静态定义,是一个动态引用。

阮大神一下子给这么多定义,其实想表达的就是一个意思:export输出的不是一个具体数据,即不是模块内部的变量的值,而是对变量的引用。

这里“引用”,困扰了我很久,因为阮大神一直在强调export命令输出的东西是在编译时确定的

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

但是,“引用” 不应该是运行时产生的东西吗?说到引用,我就想到了对象,而对象必须是在运行期间产生的,所以引用这个词有歧义。阮大神后面又给出了一个更加确切的名词:“符号连接”

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

这个倒是有点感觉了,因为JS源码(a.mjs)

export var a = 1

function test(num){
  return num++
}

const b = test(a)

console.log(b);

会被解析器解析为AST抽象语法树(d8 --print-ast a.mjs)

[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 1
. LITERAL ID 0
. SUSPEND COUNT 1
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (00000237DAE926D8) (mode = VAR, assigned = true) "a"
. . FUNCTION "test" = function test
. . VARIABLE (00000237DAE92A70) (mode = CONST, assigned = false) "b"
. EXPRESSION STATEMENT at -1
. . YIELD at 0
. . . VAR PROXY local[0] (00000237DAE92600) (mode = TEMPORARY, assigned = false) ".generator_object"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 15
. . . INIT at 15
. . . . VAR PROXY module (00000237DAE926D8) (mode = VAR, assigned = true) "a"
. . . . LITERAL 1
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 72
. . . INIT at 72
. . . . VAR PROXY local[2] (00000237DAE92A70) (mode = CONST, assigned = false) "b"
. . . . CALL
. . . . . VAR PROXY local[1] (00000237DAE929E8) (mode = LET, assigned = true) "test"
. . . . . VAR PROXY module (00000237DAE926D8) (mode = VAR, assigned = true) "a"
. EXPRESSION STATEMENT at 83
. . ASSIGN at -1
. . . VAR PROXY local[3] (00000237DAE92CE8) (mode = TEMPORARY, assigned = true) ".result"
. . . CALL
. . . . PROPERTY at 91
. . . . . VAR PROXY unallocated (00000237DAE92DD8) (mode = DYNAMIC_GLOBAL, assigned = false) "console"
. . . . . NAME log
. . . . VAR PROXY local[2] (00000237DAE92A70) (mode = CONST, assigned = false) "b"
. RETURN at -1
. . VAR PROXY local[3] (00000237DAE92CE8) (mode = TEMPORARY, assigned = true) ".result"

[generating bytecode for function: test]
--- AST ---
FUNC at 33
. KIND 0
. LITERAL ID 1
. SUSPEND COUNT 0
. NAME "test"
. PARAMS
. . VAR (00000237DAE947F8) (mode = VAR, assigned = true) "num"
. DECLS
. . VARIABLE (00000237DAE947F8) (mode = VAR, assigned = true) "num"
. RETURN at 43
. . POST INC at 53
. . . VAR PROXY parameter[0] (00000237DAE947F8) (mode = VAR, assigned = true) "num"

AST会被Ignition解释器解释为字节码(d8 --print-bytecode a.mjs)

[generated bytecode for function:  (0x036e0824fa5d <SharedFunctionInfo>)]
Parameter count 1
Register count 6
Frame size 48
         0000036E0824FB06 @    0 : ae fb 00 01       SwitchOnGeneratorState r0, [0], [1] { 0: @29 }   
         0000036E0824FB0A @    4 : 27 fe f7          Mov <closure>, r4
         0000036E0824FB0D @    7 : 27 02 f6          Mov <this>, r5
         0000036E0824FB10 @   10 : 64 0a f7 02       InvokeIntrinsic [_CreateJSGeneratorObject], r4-r5
         0000036E0824FB14 @   14 : 26 fb             Star r0
         0000036E0824FB16 @   16 : 81 01 00 00       CreateClosure [1], [0], #0
         0000036E0824FB1A @   20 : 26 fa             Star r1
         0000036E0824FB1C @   22 : 25 fb             Ldar r0
         0000036E0824FB1E @   24 : af fb fb 04 00    SuspendGenerator r0, r0-r3, [0]
         0000036E0824FB23 @   29 : b0 fb fb 04       ResumeGenerator r0, r0-r3
         0000036E0824FB27 @   33 : 26 f7             Star r4
         0000036E0824FB29 @   35 : 64 0b fb 01       InvokeIntrinsic [_GeneratorGetResumeMode], r0-r0 
         0000036E0824FB2D @   39 : a1 02 02 00       SwitchOnSmiNoFeedback [2], [2], [0] { 0: @49, 1: @46 }
         0000036E0824FB31 @   43 : 25 f7             Ldar r4
         0000036E0824FB33 @   45 : a8                Throw
         0000036E0824FB34 @   46 : 25 f7             Ldar r4
         0000036E0824FB36 @   48 : aa                Return
         0000036E0824FB37 @   49 : 0c 01             LdaSmi [1]
         0000036E0824FB39 @   51 : 2c 01 00          StaModuleVariable [1], [0]
         0000036E0824FB3C @   54 : 2b 01 00          LdaModuleVariable [1], [0]
         0000036E0824FB3F @   57 : 26 f6             Star r5
         0000036E0824FB41 @   59 : 5d fa f6 00       CallUndefinedReceiver1 r1, r5, [0]
         0000036E0824FB45 @   63 : 26 f9             Star r2
         0000036E0824FB47 @   65 : 13 04 02          LdaGlobal [4], [2]
         0000036E0824FB4A @   68 : 26 f6             Star r5
         0000036E0824FB4C @   70 : 28 f6 05 04       LdaNamedProperty r5, [5], [4]
         0000036E0824FB50 @   74 : 26 f7             Star r4
         0000036E0824FB52 @   76 : 59 f7 f6 f9 06    CallProperty1 r4, r5, r2, [6]
         0000036E0824FB57 @   81 : 26 f8             Star r3
         0000036E0824FB59 @   83 : aa                Return 
Constant pool (size = 6)
0000036E0824FAC5: [FixedArray] in OldSpace
 - map: 0x036e080404b1 <Map>
 - length: 6
           0: 29
           1: 0x036e0824fa85 <SharedFunctionInfo test>
           2: 10
           3: 7
           4: 0x036e081c6971 <String[#7]: console>
           5: 0x036e081c69e5 <String[#3]: log>
Handler Table (size = 0)
Source Position Table (size = 0)
[generated bytecode for function: test (0x036e0824fa85 <SharedFunctionInfo test>)]
Parameter count 2
Register count 1
Frame size 8
         0000036E0824FC6E @    0 : 25 02             Ldar a0
         0000036E0824FC70 @    2 : 76 00             ToNumeric [0]
         0000036E0824FC72 @    4 : 26 fb             Star r0
         0000036E0824FC74 @    6 : 4c 00             Inc [0]
         0000036E0824FC76 @    8 : 26 02             Star a0
         0000036E0824FC78 @   10 : 25 fb             Ldar r0
         0000036E0824FC7A @   12 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

以上过程就可以其实就是JS源码的预编译阶段工作(当然预编译不止这些工作,还有其他工作),和Java有点类型,将源码变为为字节码。

我们不关注其中具体流程,只是去理解,阮一峰教程所说的 ES6模块export输出的是一种静态定义的概念。

阮一峰教程中,一直在强调import,export在编译期间就完成了,即在运行之前就完成了,那么export导出的东西肯定不是运行时产生的对象,所以只能是AST树或者字节码中东西,

阮一峰教程中,还说:export的接口必须和模块内变量一一对应。

那么在AST树,字节码中,模块内变量还不能算是变量,因为变量概念也是在运行期间产生的,此时变量还只能称为符号,如AST树中,每个变量声明后,都对应一个地址,这个地址不是代码运行时产生的引用地址,而是代码编译生成的静态定义,因为我们要运行代码,总是需要先将代码加载到内存中,而代码中每个变量符号都有一个对应的内存地址,而ES6模块export导出的就是这个内存地址,它是一种静态定义,和代码运行无关,但是在代码运行时,可以靠着这个静态定义找到对应的变量,获取实时的值。

导出语法总结

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。 

 关于前两点,之前已经解释过了,但是最后一点,需要注意的是,import和export命令是在编译期间工作的,而无论是无论是函数块,if块,代码块,它们之中的代码都是在运行期间才能生效的,所以必须将import和export命令放置于模块顶层

导入语法深入理解

用法注意

具名导入

import {a} from './a.js'

注意具名导入对应的是具名暴露,即export、export {} 命令暴露的接口都由 具名导入 import {} 来接收

和exprot {} 相同,import {} 中的 {} 也不是对象,而是语法的一部分,行为类似于数组,用于接收具名暴露的接口,接收的输入变量的名字要和具名暴露的输出接口的名字相同,当然可以使用as来定义别名。

默认导入

improt xxx from './a.js'  // xxx可以是任何有效标识符

和具名导入的区别是 没有了{} ,而是直接使用一个输入变量来接收默认暴露的输出接口,且输入变量的名字无要求

这是因为默认暴露的输出接口其实是一个名为default的接口,而improt命令可以识别出default名字的输出接口,而了解到只有一个输出接口,所以底层相当于 improt {default as xxx} from './a.js',经过简化,最终变为 improt xxx from './a.js'

整体导入

import * as xxx from './a.js'   // xxx可以是任何有效标识符,最好和模块名相同,比如a

当一个模块对外暴露的接口过多,而我们又需要全部接口时,使用具名导入,就要写很多的输入变量,很麻烦,所以可以使用 * 来表示将所有输出接口封装进一个Module对象中,我们可以通过as来定义Module对象别名,如a,然后通过a.xxx的方式来访问需要的输入变量。

需要注意当整体导入 默认暴露的接口时,此时会将默认暴露的隐式接口名 default 给传入 Module对象中,在使用对应的输入变量时,需要加一个default,即 a.default.xxx

导入的到底是啥

阮一峰教程中

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

这里又来一个名词“输入接口”,我们通过上一节了解了export对外提供的是“输出接口”,它本质是一种静态定义,是变量符号,而import主要是接收输出接口,即接收静态定义的变量符号,建立符号连接,

import {a,b,c} 中a,b,c有两层含义:输入接口,本地变量,

其中输入接口和本地变量一一对应,输入接口和输出接口本质都是指向静态定义的变量符号。

导入语法总结

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

这里其实可以说明a其实不是变量,而是一个静态定义,一个连接,但是它可以用来获取连接的输出变量的值

 

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

其实这是和node_module有关,即当我们指定import ... form的是一个模块名而不是文件路径时,import命令就会去node_module中找对应名字的模块,而node_module中模块的名字定义在package.json,需要注意的是

 当前ES6导入第三方模块,只能在Nodejs中使用,在浏览器端,不支持ES6根据模块名导入模块。

 

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

import语句会执行所加载的模块。如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。也就是说,import语句是 Singleton 模式。

导入导出复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar