注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:
阮一峰ES6的书籍中Module 的语法
我的ES6入门学习规划
ES6推出了标准的module语法。在没有这个之前,以前都是amd或cmd的方式来写js的模块,ES6的模块要解决的问题跟它们是一样的,只不过在使用上各有特点。
理解es6的模块特点
nodejs中的模块规范cmd,是一种动态加载的方式,它只有在模块完整地加载、编译、执行完毕,才能拿到一个模块的输出对象,最后通过require这个对象去使用模块对外暴露的接口。1
2let fs = require('fs');
fs.readFile(...);
es6的模块不需要使用module.exports或require这些全局变量,而是通过export和import命令来完成模块接口的定义和引用:export命令用于对外提供接口,import命令用于引用其它接口。 先简单了解一个例子:1
2
3
4
5
6
7
8
9
10//es6_module_04.js
export let value = undefined;
export function getValue() {
return value;
}
setTimeout(() => {
value = 'not empty';
}, 1000);
上面这个模块定义两个对外接口,分别是value变量和getValue函数,初始化的时候value变量为未定义,但是这个模块执行完之后1.5s,value会被赋值。 接下来看如何引用这个模块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head>
<title></title>
<script type="module">
import {value, getValue} from './es6_module_04.js';
// 在es6_module_04加载&执行完以后,立即访问val和getVal接口
console.log('check immediately', value);// check immediately undefined
console.log('check immediately', getValue());// check immediately undefined
// 1500ms以后再访问一次
setTimeout(()=>{
console.log('check after 1.5s', value);// check immediately not empty
console.log('check after 1.5s', getValue());// check immediately not empty
}, 1500);
</script>
</head>
<body>
</body>
</html>
上面还包含了es6的模块如何在html中使用的知识点,这个会在下一篇笔记中记录,此处只是先借助它来运行es6的模块。在上面的html中,通过import引入前面定义的es6_module_04.js这个模块,并明确地引用了它对外暴露的两个接口:value和getValue;然后立即访问了一下这两个接口,由于es6_module_04刚执行完时,value的值就是undefined,所以第一次访问这两个接口的结果都是“check immediately undefined”;接着在1.5s以后,再次访问了两个接口,此时es6_module_04执行时设定的定时器早已执行,导致它内部的value已经被赋值,外部再重新访问接口时,成功地拿到了内部value变量的新值,最后两次打印都是“check immediately not empty”。
先把这个例子放到一边,等会再来说明为什么第二次访问module的接口,还能访问到内部变量的新值。 接下来把这个例子,放到node.js里面去试验一下,看看是什么效果。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//module.js
let value = undefined;
function getValue() {
return value;
}
setTimeout(() => {
value = 'not empty';
}, 1000);
module.exports = {
value: value,
getValue: getValue
};
1 | // main.js |
运行main.js:1
node main.js
最后的运行结果已经写在上面的注释里面,这个结果跟es6的模块唯一的区别就在于main.js里面1.5s以后直接访问模块内部value变量时的打印。 ES6的模块输出的是value最后的值,但是node里面输出的还是value的初始值。
node的结果是很好理解的。 因为它是动态加载,并且最后得到是一个模块的对象,用来访问模块内部的东西,所以外部最后在使用模块时,完全等同于使用模块的对象,也就是说外部使用的m.value以及m.getValue都是在使用m这个对象上的属性,而不是直接访问模块内部的value和getValue变量。 模块内部定时器给value赋值的时候,是给value变量赋值,而不是给模块对象的value属性赋值,所以定时器结束后,模块对象的value属性值并没有改变。模块对象的getValue属性,最终指向的也是模块内部的getValue函数,调用模块对象的getValue方法,等同于调用模块内部的getValue函数,在这个函数内部,始终能够访问到模块内部value变量的最终值,所以在main.js里面,通过m.getValue能取到模块内部value的最新状态。
通过这个例子可以看到,node的模块,本质是把模块内部的接口复制到模块对象上面,外部是通过模块对象才能使用内部的接口,如果模块的接口在模块被使用的过程中,有这种赋值更换的行为,很有可能就会出现像上面那样的不一致结果。 明白了这一点,在排查node模块使用时一些意外的不一致状态问题时,就会容易一些。 另外也有一些办法可以尽量去减少此类问题,最重要的是就是提前就想好该不该直接把属性部署在模块对象上,尤其是基础类型值,要特别谨慎;如果非要部署的话,最好改用setter或getter等行为方法来处理。
es6的模块跟node的比起来,形式上更简洁一些,而且看起来做的更好。 它的核心要点在于:
- 它是静态加载的方式来加载模块,它只需要在模块加载、编译完成时,就能确定这个模块有哪些对外接口
大概地原理就是在模块编译完成以后、模块执行前,通过模块内的所有export关键字,分析出这个模块对外定义的接口。 注意,在这个过程里面,它只是确定有哪些接口,核心点在于确定接口的名称,而不包含接口的值。 所以在它确定完一个模块的所有接口以后,这些接口是什么值,此时是完全未知的,这些值最起码要等到模块执行完以后才能绑定到接口上。
- 外部在引用一个模块时,不是引用模块的对象,es6的模块根本没有返回值,它是直接引用模块内部的接口,这是一种类似引用传递的机制
这就是为啥es6的模块里面,当定时器改变了value变量的值,外部模块能够实时拿到模块内接口最新值的根本原因,它们俩等同于就是同一个变量!
es6为啥要在node的模块规范这么普遍地适用之后再推出一个新的规范呢,在我看来,es6现在的module方式,除了最基本的作用外,还有2个突出的价值:
- 静态分析
因为现在es6的模块都是在编译后就能知道有哪些接口了,所以如果拿到一堆es6的模块,不就不需要执行,就能知道它有哪些api了吗?这对于使用js编程的ide来说,应该是很有用的,比如改进代码提示的功能。 利用静态分析地能力,ide能够更快更准确地帮助我们提示模块有哪些api可用。 这也是其他强类型语言,比如java这种,在ide里面很强大的能力。 另外静态分析也能帮助我们快速检查一遍代码的问题,比如一个模块引用了一个别的模块并没有暴露的接口,这个肯定是一个问题,不用等到部署到线上运行,在编译打包期间就能知道代码有错了。比如说前面的例子,如果把import简单改一下,后面随便加个不存在接口:1
import {value, getValue, setValue} from './es6_module_04.js';
在浏览器里面执行以下,就会抛出以下错误:1
Uncaught SyntaxError: The requested module './es6_module_04.js' does not provide an export named 'setValue'
相反,node里面访问一个模块对象不存在的接口,最多也就是因为undefined而报错,而不会明确地跟你说是这个模块内部未定义相关的接口,这样的错误是非得代码真正运行起来才能发现的。 这样一个错误检查机制有意义吗?很有意义,有的时候开发人员一个不小心,把代码多加了一个字符,真的是能够产生很严重的bug,所以越早地发现代码不应该的错误,越有好处。
- 更好地处理循环依赖
这个点的研究暂时只是引入,详细地介绍放在下一篇笔记。
以上关于es6的模块的基本内容,我认为是es6的核心,后面虽然也要详细地总结export import等等一些语法的规则,但都是一些语言上的规定,不是这个语法的重点。 掌握es6是静态加载的这一个要点,特别特别重要。
export语法
export是定义接口的关键字,它的语法只有几种正确的形式,编写模块时,按照这几种写法去写即可:
- 声明时export,可作用于let const function 以及class声明
1 | // 输出变量声明 |
注意,输出声明的时候不允许使用匿名函数或者匿名类。
- 引用已声明的变量进行export
1 | // 输出变量 |
借助大括号完成已声明的变量输出。 export在一个模块里面,应该放置在顶层位置,不能放在块级作用域当中,从阅读性的角度,最好是都放在模块的末尾,这样别人看的时候一目了然。 但是并没有强制一定是这样的,export可以有任意次数的使用,且上面两种方式可以并存。
- export的时候改名
静态加载时最终确定的是模块对外的接口列表,最重要的就是一个接口的名称,默认情况接口名称都直接是变量或函数的名称,但也可以进行改名。
1 | var a = 1; |
这个点看情况,有作用,但也不是那么有用。
import语法
import帮助我们引用其它模块接口。
- 引入某个模块对外的一组接口
1
import {firstName, lastName, year} from './profile.js';
大括号中的变量名必须与被加载的模块对外输出的接口名称相同。
引入时改名
1
import {firstName as name1, lastName as name2, year} from './profile.js';
仅仅执行某个模块
1
import 'lodash';
仅仅执行某个模块,但是不引入它的接口。 es6的模块仅会执行一次,重复引入,不会多次执行。
- 模块的整体引入
1
2
3
4import * as sth from './circle';
console.log(sth.area(4));
console.log(sth.circumference(14));
这个用法只是为了帮助我们简化模块引入,但绝对不是说把一个模块作为对象返回给sth这个变量。es6规定可以利用星号指定一个对象,保存模块对外的所有接口;此方式引入时,sth对象不允许修改,不允许重新给它上面的模块接口赋值,不允许增删属性。
- 其它要点
import引入的接口是只读的,类似const的效果
import后面的from指定模块的文件位置,允许相对路径、绝对路径、可以省略.js后缀
import命令具有提升效果,会提升到整个文件头部优先执行
import引入接口,不能在接口名称和模块位置上使用表达式,不能位于块级作用域当中,因为它是静态分析
cmd的require引入与import引入最好不要同时使用。
default接口
为了方便使用,可以使用export default来输出模块的默认接口1
2
3export default class {
// ...
}
其它模块引入这个模块的默认接口时,不需要使用大括号,并且可以用任意名字引入:1
import someName from './module';
这种方式可能更具备适用性。 利用default,可以实现已存在的变量直接通过export来输出,不需要大括号:1
2
3
4
5
6
7
8
9
10
11
12
13export default function() {
}
export default class {
}
export default 1;
export default 'hello';
export default new Object();
注意,没有default,以上输出都会报错。一个模块只能有一个默认输出,export default只能使用一次,否则报错。本质上,export default 就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。1
2
3
4
5
6
7
8
9
10
11
12// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
相反export default 后面不能接变量、函数或class声明,所以要注意default有无对export的作用区别。
如果一个模块同时有default接口,外部可以同时import默认接口和其它接口:1
import _, {each, forEach} from 'lodash';
复合写法
接口转发
1
2export {foo, bar} from 'module';
//从module中引入名为foo,bar的接口,然后对外又输出名为foo,bar的接口接口改名
1
2export { foo as myFoo } from 'my_module';
//改名后,外部引入当前模块只能使用myFoo接口,foo接口无效整体输出
1
2export * from 'my_module';
//这种方式会忽略掉my_module中的default接口具名接口改为默认接口的写法如下
1
export { es6 as default } from './someModule';
默认接口也可以改名为具名接口
1
export { default as es6 } from './someModule';
模块的继承
通过前面的复合写法,可以实现模块的继承。1
2export * from 'circle';
export var e = 2.71828182846;
这个模块会包含circle定义的行为和自己额外定义的行为。
export * 会忽略加载模块的default接口,当前模块可重新定义自己的default接口1
2
3
4
5export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
假如circle里面有area方法,当前模块可重新输出一个area方法,达到覆盖的目的1
2
3
4
5
6
7
8export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
export var area = function() {
//...
}
跨模块的常量
这个点是有用的,尤其在项目里面有很多需要常量的场景时。1
2
3
4
5
6
7
8
9
10
11
12
13
14// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
import()函数
ES6现在模块,缺乏动态加载的能力,现在有提案加入import()函数来实现动态加载,它接收一个模块路径作为参数,返回一个Promise. import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。1
2
3
4import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
这个import函数,通过babel转码是可用的,不过我目前也怎么用到需要它的场景。 对了,import()函数现在是动态加载用的,所以它的参数不再要求是固定字符串了,可以使用表达式。
如果模块有default输出接口,可以用参数直接获得。1
2
3
4
5
6
7
8
9import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
//或者使用解构
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
同时加载多个模块1
2
3
4
5
6
7
8Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
补充
es6的模块自动开启严格模式,无法关闭,要注意严格模式的一些特性。