从零开始学_JavaScript_系列(68)——es6模块的使用

来源:互联网 发布:刘国梁事件真相知乎 编辑:程序博客网 时间:2024/06/06 21:42

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,而不能是省略写法。

  1. './bar.js' 可以
  2. './bar' 错误,会加载bar而不是bar.js
  3. '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.jsexport default 'bar'

可以正常打印bar

示例四:

// html文件<script type="module" src="./foo.js"></script>// foo.jsimport bar from './bar.js'console.log(bar)// bar.jsexport 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.jsexport let baz = 'baz'// bar.jsexport * from './baz.js'export let bar = 'bar'// foo.jsimport * 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模块的区别在于:

  1. 一般用于前端代码中(也可以用于Node.js,但写法有所区别);
  2. 模块的加载是静态的,编译时输出接口,所以才有静态优化一说(而CommonJS是同步的,运行时加载);
  3. es6模块导出的变量或者是其他,是按引用传递的,即假如里面代码更改了这个变量的值,其他地方用到这个变量,也会随之被更改。但CommonJS的不会;

前两点比较好理解,重要的是第三点。如代码:

// bar.jslet bar = '1'function anotherBar() {    bar = 'bar'}module.exports = {    bar, anotherBar}// foo.jslet bar = require('./bar.js')console.log(bar.bar)    // 1bar.anotherBar()console.log(bar.bar)    // 1

输出内容是:1和1,而不是1和bar。

原因在于,在执行到module.exports这一步时,输出内容已定。这里输出的是一个对象。这里对象的值显然是确定的,{bar, anotherBar}是对象的简写

而es6模块输出的是一个引用,如果{bar, anotherBar}出现在export后面,那么这并不是一个对象,更不是对象的简写。

那如何获取更改后的值呢?办法是利用对象的按引用传递特性,换种写法:

// bar.jslet bar = {    a: 1}function anotherBar() {    bar.a = 2}module.exports = {    bar, anotherBar}// foo.jslet bar = require('./bar.js')console.log(bar.bar.a)  // 1bar.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模块。

  1. CommonJS模块的后缀名保持.js不变;
  2. es6模块的后缀名使用.mjs

在CommonJS的模块里面,只能使用CommonJS语法;而在es6模块里面,既可以使用CommonJS的语法,也可以使用es6的语法。

以es6模块的方式运行.mjs文件的方法如下:

node --experimental-modules foo.mjs

效果是node运行foo.mjs文件,中间的那一段不能省略,表示实验性功能。

最简单的示例如下:

// bar.mjsexport default 'bar'// foo.mjsimport bar from './bar.mjs'console.log(bar)        // bar

3.2、一些注意点

1、首先,既然想要在Node里启用es6模块,那么一般是两种原因:

  1. 想要试试新技术(略);
  2. 想要模块复用(同时用在浏览器环境下和Node.js环境下);

第二点的话,需要注意一些问题:

  1. 虽然阮一峰的博客里,CommonJS的模块是可以加载es6的模块的。但是实际我验证的时候,结论是否定的,.js结尾的文件无法加载.mjs的es6模块。测试例子是这个链接里第一段代码;(如果你验证可以使用,请私信告诉我
  2. 但相反,es6的模块是可以正常加载CommonJS的模块的;

因为以上两点,所以如果你需要使用es6的模块,那么你必须以es6的模块为主,只在es6模块里引用CommonJS模块,而放弃(至少是暂时放弃)在CommonJS模块里引用es6模块。

es6 use CommonJS -----> yesCommonJS use es6 -----> no

不仅如此,还有一个很大的不同点:

es6模块在浏览器环境下,导出的变量是按引用传递的,说明参照本博客第二节。

但是在Node环境下(v8.5.0),执行代码,es6模块导出的变量并非按引用传递,而是取得缓存值,即取值的拷贝(虽然引入进来后依然不能改)。

证明代码如下:

// bar.mjslet bar = 'bar'export default bar;setTimeout(() => {    bar = 'BAR'    console.log('bar查看更改后的导出变量:' + bar)}, 500)// foo.mjsimport bar from './bar'console.log(bar)setTimeout(() => {    console.log('foo查看更改后的bar: ' + bar)}, 1000)// 执行命令node --experimental-modules foo.mjs// 输出结果barbar查看更改后的导出变量:BARfoo查看更改后的bar: bar

这点不同在你需要调用方法修改原模块内的导出变量时,需要极为注意。

3.3、es6模块里引用CommonJS模块

  1. CommonJS的导出方法没任何变化;
  2. 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一个模块的时候,另外一个模块能不能取到值。如果取不到值那自然没啥好说的了。

在这个过程中,比较不明确的问题的是:

  1. 当a引入b时,此时开始执行b;
  2. 执行b的时候,又遇见引入a的语句;
  3. 这时候怎么处理?

答案很简单,如果引入的模块正在执行中,那么会跳过这个引入的模块,不去重复执行。

例如第二步时的a,正在执行中,所以不会再去执行一遍a。

即使是三个模块循环引用,道理也是一样的。

另外,又因为import会被前置,因此也不用担心模块执行完了(脱离执行中),会再次执行的问题(而且显然不可能,因为模块只会被执行一遍,之后取得要么是按引用传递的值,要么是拷贝的值)。

阅读全文
0 0
原创粉丝点击