这两天学习了阮一峰老师关于ES6模块的教程,打算在这里做下学习小结。这并不是自己第一次学习此类知识,但是因为平常使用未加谨记,故常常忘记。阮老师博客
JS模块化的由来
对于其它语言来说,模块化是天生就支持的基础特性,例如 java
的 package
。而对于JS来说,初始并不支持模块化,以前写过 jquery
的都记得,我们经常一个JS文件写的
满满当当的,各种各样的函数都在一个JS文件中相互调用或放在顶层作用域window下面实现跨文件调用。
这样的写法对于一个大型项目来说是非常不便于管理和维护的,所有模块化呼之欲出。
ES6模块化的使用
ES6模块化是为了处理JS模块化而新引入的标准,我想现在大部分同学都在项目中已经用的溜得飞起,就是我们常常写的 import
,export
// a.js export const a = 1; // b.js import { a } from './a.js'; 复制代码
因为大家在项目中经常使用,所有简单的导入导出就不再唠叨了,我们主要看看几个需要注意的地方
- 什么样的导出写法才是正确的
我想经常会有人在声明导出的时候出错
const a = 2; // error export a; // error export 2; // ok export const a = 2; 复制代码
对于 export
命令来说,我们必须在其后加上声明语句,不能直接导出一个声明后的变量名或者是变量值。
我们可以理解为 export
定义了一个接口,我们必须通过 export + 声明
的方式来声明这个接口并定义输出的变量
const a = 2; // ok export default 2; // ok export default a; // error export default const a = 2; 复制代码
和上面的写法完全相反,这也是导致我们经常写错的原因
我们可以这样理解,对于 export default
来说,其实际等于 export const default =
,因为它实际是声明加上将其后的变量或值赋值给 default
,所以不能再加上重复声明
- 什么样的导入才是正确的
我之前常常误解,将import { a } from './a.js'
作为一种解构来理解,所以常常会有以下的错误写法
// a.js export default { a: 1, b: 2 } // b.js import { a, b } from './a.js' 复制代码
这样的写法显然是错误的,我应该纠正自己的理解
import moduleA from './a.js' const {a, b} = moduleA; 复制代码
对于 export const a = 1;
这样的写法就是对应 import { a } from 'xxx'
,这是一种标准约定的写法。
而对于 export default
来说,我们仅仅 import a from 'xxx'
,如果 a
是个对象,此时应该再进一步去解构对象才对
- 静态解析
对于es6模块来说,其输出接口是在解析阶段就已经确定的,而不是在运行时进行定义的,怎么理解呢
// a.js export let count = 1 export const addCount = () => { count += 1 } 复制代码
// b.js import { count, addCount } from './a.js' conole.log(count) // 1 addCount() conole.log(count) // 2 复制代码
可以发现,我们使用export
,import
之后,其实就是定义了它们之间的联系,不管在模块中定义的变量a
如何变化,其导入处的 a
变量值也会跟着变化。
它就像我们在 A模块
定义了 变量A
,在 B模块
引入 变量A
,此时两个模块的 变量A
是同一个内存。
- 执行1次
在不同的模块中,或者在同一个模块中,导入另一个模块 B
的时候,仅在第一次导入会执行 B
模块
// a.js import { b } from './b.js' // c.js import defaultB from './b.js' 复制代码
以上代码仅执行一次 b.js
,所以我们在项目代码中多次引入同个模块的时候可以做到模块数据共享,不会多次初始化重置数据
- 依赖循环
在项目复杂的时候,将有可能遇到依赖循环的问题,我在项目中也有遇到,并且被eslint
检查报红了。
如果可以的话,当然是修改依赖避免循环最理想。但是如果无法避免的时候呢?
其实依赖循环本身并没有问题,只是如果我们不清楚其执行机制,有可能导致问题,所以
如果我们有遇到依赖循环的情况,应该好好理解。
我们举个例子
// a.js console.log('enter a') import { nameB } from './b.js' console.log('nameb in ajs', nameB) export var nameA = 'a' console.log('load a') 复制代码
// b.js console.log('enter b') import { nameA } from './a.js' console.log('namea in bjs', nameA) export const nameB = 'b' console.log('load b') setTimeout(() => { console.log('namea in bjs', nameA) }, 0); 复制代码
最后打印的结果
// enter b // namea in bjs undefined // load b // enter a // nameb in ajs b // load a // namea in bjs a 复制代码
我们来分析下
- 进入
a.js
因为import
提升,所以最先执行import { nameB } from './b.js'
- 进入
a.js
同样声明提前,首先执行import { nameA } from './a.js'
- 此时发现依赖循环,所以不会进入
a.js
,而是继续执行b.js
- 打印
enter b
- 执行
console.log('namea in bjs', nameA)
,此时因为a.js
未执行余下部分,所以nameA
为undefined
,打印namea in bjs undefined
- 执行
export const nameB = 'b'
- 打印
load b
b.js
执行完毕,此时继续执行a.js
- 打印
enter a
- 执行
console.log('nameb in ajs', nameB)
,因为b.js
已经执行完毕,所以nameB
的值为b
,打印nameb in ajs b
- 执行
export var nameA = 'a'
定义a.js
的输出接口 - 打印
load a
- 最后执行
b.js
中的延迟函数,此时a.js
已经加载完毕,打印namea in bjs a
总结以下,当
a
,b
文件依赖循环时,假设首先进入a.js
,此时会先执行b.js
,在执行b.js
的时候,导入的a.js
中的变量是获取不到值的,因为a.js
未执行完毕,当b.js
执行结束再回到a.js
,此时再执行剩余部分,并且导入的b.js
变量都是有值的,如果后面再执行b.js
中的函数或其它逻辑时,因为a.js
加载完毕,所以导入的值此时也可以获取到值了。
所以只要我们不是同步的立即执行函数,在某些时候依赖循环是没有问题的,我们只要注意初始化时候是获取不到值的就行。
与commonJS的差异
commonJS
是 NODE
环境中定义并实现的一套模块化标准,现在比较普遍存在的就是 commonJS
与 ESM
即上文的 es6模块化
,至于 AMD
等属于之前 ESM
未流行之前的替代方案,我们就不分析了。
- 使用方式不同
对于 commonJS
来说,我们通过 exports
,require
来引用及导出
// a.js exports.a = 1; // b.js const a = require('./a.js').a 复制代码
- 声明提升及作用域的区别
对于 ESM
来说,不管我们在什么位置使用 import
都会提升到最顶部执行,而 require
不会如此。
还有就是 ESM
不允许定义在块级作用域中
if (1) { // error import a from './a.js' } 复制代码
而 require
可以
if (1) { // ok require('./a.js') } 复制代码
所以 require
是可以按需加载的,而 import
不行,当然这边所说的并不包括 import()
- 静态解析VS运行时加载
前面说 ESM
属于 静态解析
,而 commonJS
是运行时加载,什么意思呢
也就是说 ESM
在代码解析阶段就已经完成模块定义,而 commonJS
是在执行 require
才执行模块加载及定义的。
所以 ESM
不支持动态路径
// error import { a } from `./${path}/a.js` 复制代码
这就相当于写了
// error const `{path}a` = 1; 复制代码
所以在代码解析阶段就出错了
而 require
支持动态路径,就和平常执行函数一样
// ok require(`./${path}/a.js`) 复制代码
- commonJS的加载原理
我们再来简单说说 commonJS
的加载原理,在执行 require
的时候,就会定义一个新的模块对象,并存储在全局环境中
{ id: '...', exports: {}, loaded: false } 复制代码
对于同一个模块来说,会生成唯一ID,并在执行模块的时候,往 exports
添加变量,所以 exports
其实导出了变量的浅拷贝,我们往后在原模块中所作的修改,不再会同步到引用模块中
// a.js let count = 1 exports.count = count const addCount = () => { count += 1 } exports.addCount = addCount 复制代码
// b.js const moduleA = require('./a.js') conole.log(moduleA.count) // 1 moduleA.addCount() const moduleA2 = require('./a.js') conole.log(moduleA.count) // 1 conole.log(moduleA2.count) // 1 复制代码
可以看到,虽然我们调用 addCount
改变了 count
但是依然是旧值,就是因为 moduleA
来自 exports
,其为原模块的浅拷贝,当我们再次调用 require
其实也是调用在全局存储的 exports
对象,不会更新其值。
注意我们上面说的是浅拷贝,引用对象还是会有所差别的
其它
按照标准来说,在同一个模块中是不支持 ESM
和 commonJS
混用的,也就是不支持同时使用 import
和 require
。我们在 NODE
项目的时候可以通过 package.json
的 type: module/commonjs
来指定 JS
文件的模块类型。
但是为什么我们在平常开发项目的时候可以使用 ESM
及 commonJS
呢?
因为 webpack
在编译的时候,其实会按照标准对模块进行编译打包,这时候会将 ESM
及 commonJS
的模块都打包成 webpack
自身的模块实现,也就是我们常见的 webpack_require
逻辑的实现。所以其实是 webpack
的打包机制兼容了不同的模块标准进行打包编译。
最后
ESM
及 commonJS
是现在比较流行的两种前端模块标准,ESM
用于浏览器,commonJS
主要用于 NODE
项目,我们在项目中常常使用,所以还是得好好学习一番。写的比较粗糙,有错误的希望不吝指正。good good staduy day day up