Description
一、模块化
前端模块化的好处都已经被说烂了,归纳为两点:
- 避免全局变量污染;
- 有效的处理依赖关系。
ES2015终于引入了模块的概念,最近学习了下,顺便记下笔记。
二、准备工作
- 在Chrome浏览器环境运行代码;
- 新建个目录,目录下包含两个文件:
index.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>import</h1>
<script src="index.js" type="module"></script>
</body>
</html>
index.js
三、模块导出 export
- 一个文件定义一个模块,通过
export
语句导出该模块输出的变量; export
语句有两种语法格式:
- 命名导出
- 默认导出
3.1 命名导出
命名导出就是明确导出的变量名称。
在目录下创建math.js,内容如下:
// Case 1: export后面跟变量输出声明语句
export var PI = 3.14;
// Case 2: export后面直接跟变量定义语句
export var add = function (x, y) { // 导出函数print
return x + y;
}
这表示math.js模块导出变量PI
和add
。用NodeJS的模块格式可表示为:
var PI = 3.14;
var add = function (x, y) { // 导出函数print
return x + y;
}
module.exports.PI = PI;
module.exports.add = add;
index.js内容:
import * as Math from "./math.js"; // import是导入模块,后面会说。
console.log(Math.PI);
console.log(Math.add(1, 2));
用浏览器打开页面,看看输出结果是否OK:
3.14
3
如果导出多个变量,可以采用简写格式:
// 调整math.js内容
var PI = 3.14;
var add = function (x, y) {
return x + y;
}
export { PI, add }; // 简写格式,统一列出需要输出的变量
重命名导出变量
简写格式还可以对输出的变量重命名:
// 再次修改math.js
var PI = 3.14;
var add = function (x, y) {
return x + y;
}
export { PI, add as Add}; // 把输出变量add重命名为Add(注意不用双引号)
通过关键字as
把输出变量add
重命名为Add
(Add是个字面量,不是字符串不需要引号)。
同样在index.js模块也要修改下:
import * as Math from "./math.js";
console.log(Math.PI);
console.log(Math.Add(1, 2)); // Add方法名称改动了。
export
语句后面可以跟什么?
命名导出需要同时指定导出的变量名称和变量值,所以export
语句后面跟的是个变量,不可以是表达式,因为表达式只有值,没有名字。
// 语法错误:Declaration or statement expected
export 3.14
3.2 默认导出
通过关键字default
修饰export
可以指定一个模块的默认输出变量值(在导入模块的默认输出时,不需要指定导出变量名称,这个后面再说)。
// Case 3 常量
export default 25;
// Case 4 变量
var PI = 3.14;
export default PI
// Case 5 函数
export default function add2( x, y) {
return x + y;
}
- 一个模块最多只能有一个默认导出;
- 默认输出可以视为名字是
default
的模块输出变量; - 默认导出后面可以是任意表达式,因为它只需要值。
export default 3.14
3.3 总结:
- 一个文件一个模块;
export
命名导出;export default
默认导出;export
语句必须在模块作用域的最外层,即export
不可以出现在任意花括号内,如函数语句里,子代码块里(控制语句)。
四、模块导入
通过import
语句导入外部模块。对应export
语句的两种导出方式,import
也分别存在两种不同的模块导入语法格式。
4.1 导入模块的命名输出
修改index.js:
import { PI, Add } from './math.js';
console.log(PI);
console.log(Add(1, 2));
表示:导入math.js模块里输出的变量PI, Add。
重命名导入的变量
该格式还支持对导入的变量重命名:
import { PI as pi, Add as add} from './math.js';
通配符*
如果导入一个模块所有命名输出,可采用通配符*
。
// 修改index.js
import * as Math from './math.js'; // 此时必须通过as指定个别名
console.log(Math.PI);
console.log(Math.Add(1, 2));
表示导入模块math.js所有命名输出变量,并通过Math
变量访问所有命名导出的变量。
Math
变量是个特殊的对象,叫模块对象。
Object.prototype.toString.call(Math); // [object Module]
并且这个对象和它的属性都是只读的(后面细说)。
4.2 导入模块的默认输出
// 修改math.js:
var PI = 3.14;
var add = function (x, y) {
return x + y;
}
export { PI, add as Add }; // 简写格式,统一列出需要输出的变量
export default function say() { // 默认输出
console.log("I am default export");
}
修改index.js:
import say from "./math.js";
say();
-
表示导入模块math.js的默认输出,此时可以用
as
重命名;
可以利用重命名方式避免导入模块的变量名称和本模块变量命名冲突。 -
导入模块默认输出的名字可以任意命名。
import say2 from "./math.js"; //
say2();
- 如果同时导入模块的命名输出和默认输出,可采用格式:
import say, * as Math from './math.js';
// OR
import say, { PI, Add } from './math.js';
默认导入一定放在命名导入前面
// 非法
import * as Math, say from './math.js';
// 非法
import { PI, Add }, say from './math.js';
4.3 只导入
如果只导入一个模块,但不引用模块的输出变量,可以简写为:
import './math.js'
此时只会触发模块math.js的执行。
4.5 导入不存在的变量
如果导入一个模块不存在的命名导出或者默认导出会抛SyntaxError
:
- 不存在命名导出
Uncaught SyntaxError: The requested module './liveBinding.js' does not provide an export named 'c'
- 不存在的默认导出
Uncaught SyntaxError: The requested module './liveBinding.js' does not provide an export named 'default'
这样说明了默认导出就是个名为default
的命名导出,一种简便的导出方式。
4.6 总结:
import xx from "xxx"
导入默认输出;import { xx } from "xxx"
导入指定的命名输出;import * as xx from "xxx"
导入全部命名输出;import "xxx"
只导入;- 同样
import
语句也必须在模块的最顶层作用域里,即import不可以出现在任意花括号内,如函数语句里,子代码块里(控制语句)。
注意在动态导入没这个限制。
五、特性
The static import statement is used to import read only live bindings which are exported by another module
- read only:引用方修改导入模块的变量值不影响原模块的变量值;
- live bindings:模块修改了其输出变量的值会影响其他引入模块的取值。
5.1 严格模式执行
模块都是在严格模式下执行的。
5.2 一个文件一个模块
- 一个JS文件定义一个模块。
import
语句必须在模块的最外层
即import
语句不可以出现在任意花括号内,如函数语句里,子代码块里(限于静态导入,后面会看到动态导入)import
语句会自动提升到代码的顶层
即无论import
为何第几行,都会提升到模块的最顶层
// cycleRef.js
console.log('foo')
import { bar } from './cycleRefBar.js'
console.log(bar);
export function foo() {
}
console.log('foo end')
// cycleRefBar
console.log('bar')
import { foo } from './cycleRef.js'
console.log(foo)
export function bar() {
}
console.log('bar end')
// main.js
import './cycleRef.js'
在一个模块文件里,var
变量和函数声明也是会提升的。在循环引用中依旧可以正常函数声明,但是const/let
声明的变量则必须是先定义后访问,否则报错。
import
为啥必须放在最外层
ES模块是静态的,在编译阶段就可以确定模块的所有依赖和导出。浏览器在加载模块时会递归的方式(根据模块依赖)去加载JS资源。等加载完所有依赖时才开始执行JS。
静态导入的优点:
- 可以在编译阶段进行优化,比如并行加载脚本等;
- 避免在JS阶段再去加载JS,防止影响用户体验。
综上:静态的导入导致import
为啥必须放在在外层。
5.3 只读的
import
导入的变量和变量的属性都是只读的**,不能也不应该修改引入的变量值
import * as Math from './math.js';
// TypeError: Cannot assign to read only property 'Count' of object '[object Module]'
Math.Count = 12;
5.4 实时绑定( live bindings)
export
导出的变量(任意类型的变量)都是实时绑定的,即模块内修改了导出的变量值,引入的模块里的变量值也是会发生变化的。- 默认导出没有实时绑定
只有命名导出变量会进行实时绑定,默认导出没有实时绑定。因为默认导出本质是导出的是个值,不是变量,所以不会进行实时绑定。
// ./liveBinding.js
export let a = 1;
let b = {
name: 'b'
}
export default b;
setTimeout(() => {
a = 2;
b = 22;
console.log('livebinding: b', b)
console.log('livebinding: a', a)
}, 0)
// main.js
import b, { a as a1 } from './liveBinding.js'
console.log('a', a1)
console.log('b', b)
setTimeout(() => {
console.log('a', a1)
console.log('b', b)
}, 1000)
默认导出就是变量赋值,上面表达的含义就是把模块./liveBinding.js
默认导出的值赋值给变量b
。
六、再看export
语句
了解了export
和import
基本用法后,我们再看看export
语句另一个语法规则:导出引入的模块的变量。
上面的例子里export
语句都是导出模块自身定义的变量,其实export
还可以导出模块引入模块的输出。
在目录添加文件log.js:
export var Log = function(msg) {
console.log(msg);
}
export default 'I am log.js';
修改math.js:
var Count = 0;
var increase = function() {
Count++;
}
var Person = {
name: 'Bob'
}
var changeName = function() {
Person.name = 'John';
}
export { Count, Person, increase, changeName};
export default function say() {
console.log(`Person:${JSON.stringify(Person)}, Count:${Count}`)
}
export * from './log.js'; //
修改index.js:
import say, * as Math from './math.js';
Math.Log('hello'); // 该方法来之log.js模块
查看下输出是否OK。
其中export * from './log.js';
表示导出模块log.js所有的命名输出。等价于:
export { Log } from './log.js';
注意: 这种语法格式export * from './log.js';
不可以定义别名,而花括号的语法格式export { Log } from './log.js'
可以定义别名,即:
export { Log as log} from './log.js'
怎么在math.js模块里导出模块log.js的默认输出呢?
只能采用先导入,再导出方式:
import logName from './log.js';
export { logName }
七、PK CommonJs
CommonJs(Nodejs)和ES Module都遵循一个文件一个模块,但存在一些差异:
-
CommonJs模块只导出是单个值,即
module.exports
,ES Module可以导出多个值(export
)
本质上CommonJs只导出是一个变量,变量挂载的属性可以在执行阶段任意时刻进行赋值,修改等。而ES Module静态语法是通过export
语句声明导出的变量。 -
CommonJS模块输出的是被输出值的拷贝(即module.exports的引用),ES Module导出的是变量的绑定(非默认导出);
CommonJs导出变量就如同ES Module的默认导出。