随笔-深入理解ES6模块化(三)
ES6模块化语法
ES6模块是什么
每一个JS文件都可以当成一个模块,即我们可以认为每一个JS文件就是一个模块。
甚至任意一个文件都可以当成模块。
ES6模块如何输入输出
JS模块(JS文件)中使用export语法定义模块输出,import语法定义模块输入
JS代码的静态解析阶段与运行阶段区分
JS代码在被JS引擎加载后,分为两个阶段
1、静态分析阶段(我们常说的编译阶段)
2、运行阶段
静态分析阶段的主要工作:
JS源码被解析器(Parser)解析,将JS源码转化生成抽象语法树(AST),并确定静态作用域,在AST生成后,解释器(Ignition)会将AST转化生成字节码(bytecode),在bytecode完成后,解释器(Ignition)会对字节码进行解释(为机器码)执行(交给cpu)
以上红色文字部分,就是JS的静态解析阶段,其余文字就是JS的运行阶段
静态作用域说明
如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量。即静态作用域可以由程序代码决定,在编译时就能完全确定。大多数语言都是静态作用域的。
ES6模块的输入输出是在静态解析阶段确定的
即 ES6模块的export和import是在JS静态解析阶段工作的。
那么如何理解呢?
首先在静态解析阶段,是没有函数,类,变量等概念的,这些概念都是代码运行后产生的。在静态解析阶段,JS文件中都是一些无意义的符号,但是在被解析器解析为AST树后,这些无意义的符号就有了含义,比如变量符号,函数符号,表达式符号,即AST就是给无意义的符号添加语义。
此时符号就有了符号引用,符号与符号之间可以通过符号引用进行静态关联。
根据这些静态关联,就可以确认函数的静态作用域,即函数在定义时就确定了其可以访问的外部变量,而不是在函数运行后,才能确定其访问的外部变量。
而ES6模块的export和import就是将要输出的变量或函数在静态解析阶段的符号引用导出,以及导入。
即和静态作用域原理相同,ES6模块的export和import也是在定义时就完成了对应符号引用的导出和导入。
ES6模块输出接口和输出变量区分
ES6模块的输出接口,其实就是符号引用。export命令就是将符号引用导出。
而输出变量,是运行时概念,在运行时产生。
输出接口就是输出变量在静态解析阶段对应的符号引用。
即输出接口是静态的符号引用,一直不会变,即使代码运行了,输出变量对应的符号引用也不会变。
即输出接口和输出变量是一一对应的。
export 命令
export命令后面只能跟着声明式语句,而不能跟表达式(如变量名,字面量)。
我们需要知道的是 export命令 需要导出的是输出接口,即 输出变量的静态符号引用。
export 1; // 输出的是表达式符号的引用。
var m = 1;
export m; // 输出的是表达式符号的引用。
变量只有在声明时,才会产生一个变量符号引用,而在使用变量时,会产生一个表达式符号引用。即 export m 和 export m+0 是等价的,此时m不再是变量符号,而是表达式符号m或m+0。
同样的,
export function fn(){}
export class Person{}
也只能是声明时导出。
export {} 命令
由于export命令的强约束性,导致export后面只能跟声明式语句,不利于代码编写,所以提供了一个语法糖。
即
export var a = 1
export function b(){}
export class C{}
可以写为
var a = 1
function b(){}
class C{}
export {a,b,C}
这种写法更加符合编程思想,但是需要注意两点:
1、export {} 中 {} 不是一个对象简写形式,更不是一个对象,而是export {} 语法组成部分,2、export {} 中 {} 的作用是收集模块对外暴露的输出变量的符号引用。即 export {a,b,C} 中a,b,C都是输出接口,而不是输出变量。
验证1
export {name:’test’, age:’18’}
或者
var obj = {}
export obj
都是错误的。
验证2
import {a,b,C} from filepath 中 a,b,C接收到的是filepath对应模块的输出接口,而不是输出变量。
输出接口可以获取输出变量,但是不能覆盖输出接口。
即 a = 1,并不是修改输出变量,而是修改输出接口,即输出变量的符号引用,而输出变量的符号引用是静态的,固定的,无法被修改。
export default 命令
上述export,export {} 命令其实都是具名导出命令,即要求输出接口对应的输出变量必须有名字。所以我们 无法使用 export , export {} 导出 字面量。
而 export default命令支持导出字面量。
即 export default 1;是合法的。
那么是不是说 export default命令就不需要保证输出接口和输出变量一一对应了呢?
其实 export default 命令 底层会创建一个 内置的输出变量 default,同时将命令后面跟着的字面量作为 内置输出变量 default的值。
验证:
// a.js
export default 1; // 相当于 export var default = 1 但是 default是关键字,不能用作变量名
// b.js
import {default as xxx} from ‘a.js’ // default是关键字不能用作变量名,所以要重命名
console.log(xxx) // 1
export default 是默认导出,只能导出一个输出接口,对应唯一一个内置的输出变量default。
ES6模块输入接口和输入变量区分
ES6模块导入时会产生一个输入接口,输入接口和导入模块的输出接口联通,同时输入接口会对应当前模块的一个内置输入变量,二者一一对应。输入变量可以通过输入接口到导入模块的输出接口获取到输出变量的值,这是实时获取的。
import moduleFilePath 命令
moduleFilePath是 需要导入的模块对应的文件所在的绝对路径或相对路径
import 可以触发导入的模块A中代码执行,但是只会执行一次。后面无论在哪使用import命令再次导入模块A,都不会再触发其内部代码执行。
我们需要注意的是 import命令 是在JS代码静态解析阶段完成模块之间的依赖,即输入输出接口的确定。但是import命令触发模块代码执行是在JS代码运行期间触发的,即在代码运行一开始,第一个执行的一定是该代码中import导入的模块的代码,即import有提升作用。
import {} from moduleFilePath 命令
import {} from moduleFilePath 中 {} 不是一个对象,而是语法的一部分。
import {} 叫具名导入,它专门用于导入 其他模块具名导出的接口。且import {} 中输入接口的名字必须和导入模块的输出接口名字相同。
import {} 也可以用于导入其他模块默认导出的接口default
import {default as xxx} from moduleFilePath,而这也证明了 export default导出的也是具名接口,只是名字不是有开发人员指定,而是内置好的接口default
另外 import {} 定义输入接口,必须导入模块的输出接口一一对应,要求二者名字必须相同,且通过输入接口访问的输出接口对应的输出变量,是只读的,不能通过输入接口去修改输出变量。
同时,import {} from moduleFilePath 也会执行导入模块的内部代码,和import命令特点一致,只会首次执行。
import * as xxx from moduleFilePath 命令
前面 import {} from … 都只能导入其他模块的部分接口,如果想要导入其他模块全部接口的化,需要都手写出来,不太方便。
所以有了整体导入的命令
import * as xxx from moduleFilePath
此时会将导入模块的所有输出接口全部封装进一个Module对象中,我们需要对该对象进行别名定义,以方便在模块中使用。
即 import * 整体导入必须要定义别名,否则无法访问到整体导入的对象。
和 import {} 相同的是,整体导入的对象也是只读的,我们不能修改它。
同时,整体导入也会执行导入模块的代码,和import命令特点一致,只会首次执行。
import xxx from moduleFilePath 命令
针对默认导出还有一个默认导入,由于默认导出确定只导出一个接口,且接口名确定是default,所以默认导入已经可以预知该如何导入了,即
import {default} from moduleFilePath , 由于default是保留字,所以需要重命名,即
import {default as xxx} from moduleFilePath ,由于对于所有的默认导出都可以这样来书写导入语句,所以为了方便起见,直接将上面代码优化为
import xxx from moduleFilePath ,而不需要使用 {} 包裹。 和 export default语法呼应
#
ES6模块加载
浏览器中加载ES6模块
ES6模块其实就是JS文件(暂时不考虑其他文件类型),而传统的浏览器引入JS文件的方式就是使用script标签导入。
但是为了让script标签能够区分 模块JS文件 和 非模块JS文件,即让script可以兼容引入以前非模块的JS文件。所以给script标签加入了 type = “module” 属性,来告诉浏览器引入的是ES6模块JS文件。
但是由于 只能触发模块JS文件代码执行(首次执行),而无法定义输入接口,所以无法获取到模块的输出。
想要获取到模块的输出,必须使用import命令,即浏览器加载ES6模块的第二种方式,内部JS导入ES6模块
浏览器加载ES6模块的方式有两种:
1、script标签设置type=module 引入 外部JS模块,此时无法获取外部JS模块的输出
2、script标签设置type=module 编写内部JS,在内部JS中使用 import命令导入外部JS模块,此时可以获取到外部JS模块的输出
需要注意的是,这两种方式其实都是依赖于script标签完成的模块加载,而script标签默认是同步加载的。
即浏览器从服务器请求加载好HTML文件,会自上而下逐行解析,构建DOM树,但是构建过程中,如果遇到script标签,则会暂停DOM树构建,即停止解析HTML,转而去加载script的引入的外部JS文件,等外部JS文件下载好后,继续执行JS文件中的代码,等JS文件代码执行完后,继续DOM树构建,如果下面还有script标签,则继续暂停DOM树构建,而去加载执行JS。
这种script标签默认同步加载执行JS文件的方式会对网页加载(DOM树构建)产生阻塞,造成网页延迟演示,网页无法做任何DOM操作,用户体验不好。
所以script标签还支持异步加载JS文件,只要设置defer或async属性
如果script标签设置defer属性,则script标签下载外部JS文件不会阻塞DOM构建,二者并发,等JS文件下载完毕,也不会立刻执行,而是等到DOM构建完成后执行。
如果script标签设置async属性,则script标签下载外部JS文件不会阻塞DOM构建,二者并发,等JS文件下载完毕,DOM还未构建完成的话,就会被阻塞,优先JS代码执行,JS代码执行完继续DOM构建。
而设置了 type=”module”的script标签,相当于带了 defer属性,即异步加载JS模块,不阻塞DOM构建,且会等DOM构建完成后,才执行JS模块代码。
Nodejs中加载ES6模块
Nodejs默认使用的是CommonJS模块化,所以ES6模块无法在Nodejs中直接加载使用,而需要做一些配置化工作:
方式一:将ES6模块文件的后缀名定义为mjs,来标识自己是ES6模块,此时Nodejs就会按照ES6模块化语法来处理mjs文件
方式二:将项目的package.json配置文件中 type属性改为 module,默认是 commonjs,此时Nodejs处理js文件就会按照ES6模块化语法来处理,而原来的commonjs模块就无法处理了,若需要兼容原来commonjs模块,则需要将commonjs模块后缀名改为 cjs
通常来说,不应将ES6模块和Commonjs模块混用,即ES6模块中无法直接加载commonjs模块,commonjs模块中也无法直接加载ES6模块
因为ES6模块化语法是 export , import
Commonjs模块化语法是 module.exports , require
import 和 require的区别不仅是用法上的,还有它们的加载时机和方式不同:
import 是在静态解析阶段加载模块输出接口,且是异步加载,即不阻塞后续JS执行
require 是在运行阶段加载模块输出对象,且是同步加载,会阻塞后续JS执行
export 和 module.exports 的区别也不仅是用法上,还有设计上的区别:
export 是在静态解析阶段完成模块的对外接口的输出,它输出的不是一个对象,也不是一个变量,而是静态解析阶段的 符号引用,外部可以通过符号引用直接获取到对应模块中输出变量的实时数据。
module.exports 是在运行阶段完成模块的对外输出,他是一个对象,挂载在该对象上数据都是拷贝数据(浅拷贝),和原模块中的输出变量没有关系了。外部获取module.exports,其实获取的是缓存数据,而不是原模块内的实时数据。
ES6模块和Commonjs模块的相同点就是:
二者对于同一模块多次加载都只会执行一次模块内代码,即首次加载执行,后面加载模块不执行其内部代码。
ES6模块的循环引用问题
请看上面例子,HTML中通过script标签加载a.js触发其内部代码执行。
a.js 执行第一步就是加载b.js触发其执行,
b.js 执行第一步也是去加载 a.js,如果也触发了a.js执行,那么形成了死循环,而ES6模块加载机制有一个特性避免了死循环:”模块只在第一次被加载时会执行内部代码,后面无论被加载几次都不会执行内部代码“
即:HTML中通过script标签加载a.js触发其内部代码执行 已经消耗掉了 首次执行权。 b.js此时加载a.js已经不会触发它内部代码二次执行了。这是避免死循环的根本原因。
那么此时 b.js 通过 import 导入的 foo 有值吗?
我们知道import和export在静态解析阶段就完成了 模块间输入输出接口 的联系。但是输出接口一一对应的输出变量的值,不是在静态解析阶段确定的,而是在运行阶段确定的。
即export只是导出了 变量foo的符号引用,没有导出变量foo的值。变量foo的值是在运行阶段给定的。
而此时 export let foo = ‘foo’ 代码赋值语句还没有执行。而let foo 又不存在声明提升(var foo有声明提升,且会有初始值undeifned),所以此时 foo是未初始化的,故而,b.js执行到第三行代码使用foo时,报错foo未初始化。
我们可以将a.js中 let foo改为 var foo试试,此时由于var foo 会在预编译时声明提升(静态解析阶段执行),所以运行阶段 foo是有值的,foo值为undefined
而函数声明也会提升,且会带着函数本身一起提升,即函数声明在静态解析阶段就会完成,所以使用函数声明来代替var变量声明
上述使用var变量和函数声明 来解决模块循环引用时,前一个模块未执行完,后一个模块就要用输出,导致报错变量未初始化的问题。其底层原因就是利用了 var变量声明 和 函数声明 也是在代码运行阶段之前的 静态解析阶段完成 这一特性。
ES6模块中import和export优先级问题
其实import和export只是在静态解析阶段完成 符号引用的导入与导出,不存在优先级问题。
优先级问题 指的是 运行阶段代码执行的先后顺序,而静态解析阶段无关。
但是 import 和 export 又和 运行阶段 有着密切的关系。
比如 import 除了提供输入接口,输入变量,还可能触发 导入模块的代码执行,而代码执行必须在运行阶段完成,所以import导入的模块的代码,会优先于本模块中所有的代码 第一步执行。
给人造成了一种错觉,import在运行阶段也会执行。
另外,export 只会提供 模块输出变量的符号引用,而输出变量的定义和赋值都是在运行阶段完成的,而这也给人造成一种错觉 export也会在运行阶段执行。
实际上,在运行阶段完全可以忽略 import 和 export的存在,只需要机制模块A可以访问模块B中输出变量,但是如果模块A访问模块B输出变量时,模块B还没有完成输出变量的定义和赋值,则会报错。
比如模块循环引用的问题
a.js 优先执行 b.js模块中代码,而b.js又会去加载a.js,但是a.js已经被执行过了,所以无法二次执行,b.js只能硬着头皮继续往下执行,遇到使用a.js输出变量foo时,会根据符号引用,找到a.js中的输出接口,但是此时输出接口一一对应的变量所在行代码还没有被执行,即还没有完成赋值操作,(let foo会在静态解析阶段完成变量声明,但是没有初始化,和var foo有区别,var foo在静态解析阶段机会完成变量声明,也会完成undeifned赋值)所以此时b.js中foo是一个未初始化的let变量。
Commonjs模块循环依赖的问题
Commonjs模块间也会存在循环依赖的问题,但是Commonjs解决循环依赖的底层逻辑和ES6模块是相同的。
即 同一个模块只会在首次加载时被触发执行,后续加载不会触发模块代码执行。
注意:a.js在require b.js后,自身代码暂停执行,因为 require是同步加载,会阻塞后续代码执行,直到 require返回。
b.js执行后,require a.js,但是a.js已经执行过了(node a 启动执行时),所以不会二次执行,此时b.js中第2行代码 require返回的对象是啥呢?
这时需要思考一个问题:
模块代码未执行完,那么模块的输出module.exports有值吗?答案:有值。
因为module.exports不是模块内部的变量,而是外部传入模块的变量,即Commonjs会将js模块代码封装进一个函数中,js模块中可以访问到的module.exports,或epxorts变量都是外部通过封装函数的参数传递进来的,所以一旦模块内部代码对于exports变量做了修改,其实就是对于外部该变量做了修改
所以就算模块内代码未执行完毕,exports变动也能实时反馈到模块外部。
所以此时b.js中第2行代码 require返回的对象就是 a.js做了exports.done = false后的exports对象。
所以b.js中第3行代码可以访问到a.done的值
还没有评论,来说两句吧...