1、es6模块用于浏览器中
标准写法是:
<script type="module">
console.log('module')
</script>
Chrome 61之前:按照预期效果是会非堵塞加载,然后再执行。事实上是里面的代码根本不会执行。
又比如说:
<script type="module" src="./bar.js"></script>
<script src="./foo.js"></script>
chrome版本61之后(含):
当chrome的版本更新到61.0之后,以上都可用了。
**注意:**import后面的必须跟url,而不能是省略写法。
'./bar.js'
可以'./bar'
错误,会加载bar而不是bar.js'bar.js'
错误,报错Failed to resolve module specifier 'bar.js'
示例一:
<script type="module">
console.log('module')
</script>
会正常非堵塞加载,然后再打印module
示例二:
<script type="module" src="./bar.js"></script>
<script src="./foo.js"></script>
会先加载./foo.js
,再加载./bar.js
示例三:
// html文件
<script type="module">
import bar from './bar.js'
console.log(bar)
</script>
// bar.js
export default 'bar'
可以正常打印bar
示例四:
// html文件
<script type="module" src="./foo.js"></script>
// foo.js
import bar from './bar.js'
console.log(bar)
// bar.js
export default 'bar'
可以正常打印bar
其他可以的示例:
// 示例五(配套的import写法略,下面如果没有必要必须写,那么会省略export或者import)
export let bar = 'bar'
// 示例六(全导出)
import * as bar from './bar.js'
// 示例七(普通导出 + 默认导出)
export let bar = 'bar', bar2 = 'bar2'
export default 'default bar'
// 示例八(继承导出)
// baz.js
export let baz = 'baz'
// bar.js
export * from './baz.js'
export let bar = 'bar'
// foo.js
import * as bar from './bar.js'
console.log(bar)
// 最后输出结果Module {bar: "bar", baz: "baz"}
chrome版本61之前(不含):
你以为会先启用异步加载bar.js
,然后再加载foo.js
文件么?
对不起,bar.js
文件不会被浏览器加载。
原因依然是,不支持。心很累。
┑( ̄Д  ̄)┍
老规矩,对于不支持的附个阮一峰的博客
2、es6模块与CommonJS的异同
先附一个我写的关于CommonJS和AMD、CMD的介绍。
看完后可以对CommonJS有一个初步的了解。
首先,CommonJS最常见应用的地方是在Node.js里面。
其次,CommonJS的加载,是同步的,即在require的时候才去加载,由于是在服务器环境,所以基本没有影响。
第三,CommonJS的模块,每次加载的是值的拷贝,但这并不意味着模块里的代码会被执行多次,而是执行一次之后,将值缓存起来,以后还需要加载这个模块时,会去检查缓存里有没有,如果没有才去加载,有的话直接输出缓存的值。
es6模块的区别在于:
- 一般用于前端代码中(也可以用于Node.js,但写法有所区别);
- 模块的加载是静态的,编译时输出接口,所以才有静态优化一说(而CommonJS是同步的,运行时加载);
- es6模块导出的变量或者是其他,是按引用传递的,即假如里面代码更改了这个变量的值,其他地方用到这个变量,也会随之被更改。但CommonJS的不会;
前两点比较好理解,重要的是第三点。如代码:
// bar.js
let bar = '1'
function anotherBar() {
bar = 'bar'
}
module.exports = {
bar, anotherBar
}
// foo.js
let bar = require('./bar.js')
console.log(bar.bar) // 1
bar.anotherBar()
console.log(bar.bar) // 1
输出内容是:1和1,而不是1和bar。
原因在于,在执行到module.exports这一步时,输出内容已定。这里输出的是一个对象。这里对象的值显然是确定的,{bar, anotherBar}
是对象的简写
而es6模块输出的是一个引用,如果{bar, anotherBar}
出现在export后面,那么这并不是一个对象,更不是对象的简写。
那如何获取更改后的值呢?办法是利用对象的按引用传递特性,换种写法:
// bar.js
let bar = {
a: 1
}
function anotherBar() {
bar.a = 2
}
module.exports = {
bar, anotherBar
}
// foo.js
let bar = require('./bar.js')
console.log(bar.bar.a) // 1
bar.anotherBar()
console.log(bar.bar.a) // 2
即输出的是bar这个对象,更改的bar的子属性,是可以获取到更改后的值的。
至于为什么es6的按引用传递的,如下(引自阮一峰的博客):
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
重点是只读引用,即你以为你是通过import创造了一个变量然后将export导出的值赋给这个变量,然而这并不对。import导入的变量,实际上就是export导出的那个变量,这两个是同一个东西。
这也解释了为什么通过import导入的变量,是不能修改的原因,因为是只读的。
3、Node中使用es6模块
3.1、怎么用
最新版Node下载链接,本章必须使用Node.js v8.5+
Node.js默认使用的是CommonJS,而不是es6的模块方法。
如果直接在js文件里使用es6模块的语法,然后用node运行这个js文件,是会报错的。
解决办法是用不同的后缀名区分CommonJS模块和es6模块。
- CommonJS模块的后缀名保持
.js
不变; - es6模块的后缀名使用
.mjs
;
在CommonJS的模块里面,只能使用CommonJS语法;而在es6模块里面,既可以使用CommonJS的语法,也可以使用es6的语法。
以es6模块的方式运行.mjs
文件的方法如下:
node --experimental-modules foo.mjs
效果是node运行foo.mjs文件,中间的那一段不能省略,表示实验性功能。
最简单的示例如下:
// bar.mjs
export default 'bar'
// foo.mjs
import bar from './bar.mjs'
console.log(bar) // bar
3.2、一些注意点
1、首先,既然想要在Node里启用es6模块,那么一般是两种原因:
- 想要试试新技术(略);
- 想要模块复用(同时用在浏览器环境下和Node.js环境下);
第二点的话,需要注意一些问题:
- 虽然阮一峰的博客里,CommonJS的模块是可以加载es6的模块的。但是实际我验证的时候,结论是否定的,
.js
结尾的文件无法加载.mjs
的es6模块。测试例子是这个链接里第一段代码;(如果你验证可以使用,请私信告诉我) - 但相反,es6的模块是可以正常加载CommonJS的模块的;
因为以上两点,所以如果你需要使用es6的模块,那么你必须以es6的模块为主,只在es6模块里引用CommonJS模块,而放弃(至少是暂时放弃)在CommonJS模块里引用es6模块。
es6 use CommonJS -----> yes
CommonJS use es6 -----> no
不仅如此,还有一个很大的不同点:
es6模块在浏览器环境下,导出的变量是按引用传递的,说明参照本博客第二节。
但是在Node环境下(v8.5.0),执行代码,es6模块导出的变量并非按引用传递,而是取得缓存值,即取值的拷贝(虽然引入进来后依然不能改)。
证明代码如下:
// bar.mjs
let bar = 'bar'
export default bar;
setTimeout(() => {
bar = 'BAR'
console.log('bar查看更改后的导出变量:' + bar)
}, 500)
// foo.mjs
import bar from './bar'
console.log(bar)
setTimeout(() => {
console.log('foo查看更改后的bar: ' + bar)
}, 1000)
// 执行命令
node --experimental-modules foo.mjs
// 输出结果
bar
bar查看更改后的导出变量:BAR
foo查看更改后的bar: bar
这点不同在你需要调用方法修改原模块内的导出变量时,需要极为注意。
3.3、es6模块里引用CommonJS模块
- CommonJS的导出方法没任何变化;
- es6的引入方法,是使用
import 变量名 from 模块
的方式;
第一点没啥好说的,该怎么写怎么写。先上CommonJS的导出方法
module.exports = a
这个a可能是字符串、对象,也可能是函数、类。
但无论是什么,es6模块在导入的时候,整体导入module.exports
这个变量。
以上代码相当于:
export default a
或者说:
export module.exports
但无论哪种,请记住,引入CommonJS模块时,引入的是一个拷贝,而不是引用。
并且,在Node(v8.5.0)版本下,引入es6模块,引入的也是一个拷贝,而不是引用。
总结:
因为以上原因,所以写运行在Node的es6模块时,就当做是写es6模块语法的CommonJS模块吧。
既要符合es6语法,也要符合CommonJS的一些特殊特性。
所以(那为啥要用es6模块语法写?我选择CommonJS,→_→)
3.4、循环加载
因为Node(v8.5.0)的es6模块的引用,并不是按引用传递。
所以,阮一峰博客ES6 模块的循环加载这一章节根本不成立嘛。
不过随便提提吧。
CommonJS模块是按值传递的,所以当循环加载的时候,再次加载,通常输出的是缓存的结果(除非像上面第二节里最后那个例子那样)。
所以你只要推断一下,是第二节里面的哪个例子,就知道加载的是缓存还是实时的值了。
又因为import
命令是前置的(会被提升到作用域顶部),因此你可以根据代码推断一下,在import一个模块的时候,另外一个模块能不能取到值。如果取不到值那自然没啥好说的了。
在这个过程中,比较不明确的问题的是:
- 当a引入b时,此时开始执行b;
- 执行b的时候,又遇见引入a的语句;
- 这时候怎么处理?
答案很简单,如果引入的模块正在执行中,那么会跳过这个引入的模块,不去重复执行。
例如第二步时的a,正在执行中,所以不会再去执行一遍a。
即使是三个模块循环引用,道理也是一样的。
另外,又因为import会被前置,因此也不用担心模块执行完了(脱离执行中),会再次执行的问题(而且显然不可能,因为模块只会被执行一遍,之后取得要么是按引用传递的值,要么是拷贝的值)。