CommonJS
中规定每个文件是一个模块。每个模块是拥有各自的作用域的,各自作用域的变量互不影响。
// calculator.js
var name = 'calculator.js';
// index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js
这里可以看到,导入calculator.js
并不会覆盖index.js
中的name
字段
这样做区别于直接用标签插入页面中的好处在于
插入标签后顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而封装成
CommonJS
模块会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。
下面两种写法实质上是一样的
module.exports = {
name: 'calculater',
add: function(a, b) {
return a + b;
}
};
等同于
exports.name = 'calculater';
exports.add = function(a, b) {
return a + b;
};
其内在机制是将exports
指向了module.exports
,而module.exports
在初始化时是一个空对象。我们可以简单地理解为,CommonJS
在每个模块的首部默认添加了以下代码:
var module = {
exports: {},
};
var exports = module.exports;
因此,为exports.add
赋值相当于在module.exports
对象上添加了一个add
属性。
注意点一:不要直接给exports
赋值,否则会导致其失效。 如:
exports = {
name: 'calculater'
};
上面代码中,由于对exports
进行了赋值操作,使其指向了新的对象{name: 'calculater'}
,module.exports
却仍然是原来的空对象,因此name
属性并不会被导出。
注意点二:不要把module.exports
与exports
混用。
exports.add = function(a, b) {
return a + b;
};
module.exports = {
name: 'calculater'
};
上面的代码先通过exports
导出了add
属性,相当于module.exports = { add: function(){...}}
然后将module.exports
重新赋值为另外一个对象。这会导致原本拥有add
属性的对象丢失了,最后导出的只有name
。
注意点三:导出语句不代表模块的末尾
module.exports = {
name: 'lcylcy'
};
console.log('end');
module.exports
或exports
后面的代码依旧会照常执行。比如上面的console
会在控制台上打出“end
”,但在实际使用中,为了提高可读性,不建议采取上面的写法,而是应该将module.exports
及exports
语句放在模块的末尾。
在CommonJS
中使用require
进行模块导入。如:
// calculator.js
module.exports = {
add: function(a, b) {return a + b;}
};
// index.js
const calculator = require('./calculator.js');
const sum = calculator.add(2, 3);
console.log(sum); // 5
我们在index.js
中导入了calculator
模块,并调用了它的add
函数。当我们require
一个模块时会有两种情况:
1.
require
的模块是第一次被加载。这时会首先执行该模块,然后导出内容。
2.require
的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
请看下面的例子:
// calculator.js
console.log('running calculator.js');
module.exports = {
name: 'calculator',
add: function(a, b) {
return a + b;
}
};
// index.js
const add = require('./calculator.js').add;
const sum = add(2, 3);
console.log('sum:', sum);
const moduleName = require('./calculator.js').name;
console.log('end');
控制台的输出结果如下:
running calculator.js
sum: 5
end
从结果可以看到,两次require
了calculator.js
,但console.log('running calculator.js');
只执行了一遍。模块会有一个module
对象用来存放其信息,这个对象中有一个属性loaded
用于记录该模块是否被加载过。它的值默认为false
,当模块第一次被加载和执行过后会置为true
,后面再次加载时检查到module.loaded
为true
,则不会再次执行模块代码。有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require
即可。
require('./test.js');
另外,require
函数可以接收表达式,借助这个特性我们可以动态地指定模块加载路径。
const moduleNames = ['foo.js', 'bar.js'];
moduleNames.forEach(name => {
require('./' + name);
});
ES6 Module
也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。import
和export
也作为保留关键字在ES6
版本中加入了进来(CommonJS
中的module
并不属于关键字)。
请看下面的例子,我们将前面的calculator.js
和index.js
使用ES6
的方式进行了改写。
// calculator.js
export default {
name: 'calculator',
add: function(a, b) {
return a + b;
}
};
// index.js
import calculator from './calculator.js';
const sum = calculator.add(2, 3);
console.log(sum); // 5
ES6 Module
会自动采用严格模式,这在ES5
(ECMAScript 5.0
)中是一个可选项。以前我们可以通过选择是否在文件开始时加上“use strict”
来控制严格模式,在ES6 Module
中不管开头是否有“use strict”
,都会采用严格模式。如果将原本是CommonJS
的模块或任何未开启严格模式的代码改写为ES6 Module
要注意这点。
在ES6 Module
中使用export
命令来导出模块。
export
有两种形式:
1.命名导出
2.默认导出
一个模块可以有多个命名导出。它有两种不同的写法:
// 写法1
export const name = 'calculator';
export const add = function(a, b) { return a + b; };
// 写法2
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };
第1种写法是将变量的声明和导出写在一行;
第2种则是先进行变量声明,然后再用同一个export
语句导出。
两种写法的效果是一样的。
在使用命名导出时,可以通过as
关键字对变量重命名。如:
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add as getSum }; // 在导入时即为 name 和 getSum
与命名导出不同,模块的默认导出只能有一个。如:
export default {
name: 'calculator',
add: function(a, b) {
return a + b;
}
};
我们可以将export default
理解为对外输出了一个名为default
的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可。
// 导出字符串
export default 'This is calculator.js';
// 导出 class
export default class {...}
// 导出匿名函数
export default function() {...}
ES6 Module
中使用import
语法导入模块。首先我们来看如何加载带有命名导出的模块,请看下面的例子:
// calculator.js
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };
// index.js
import { name, add } from './calculator.js';
add(2, 3);
加载带有命名导出的模块时,那就要对应命名导入。import
后面要跟{ }
来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。
导入变量的效果相当于在当前作用域下声明了这些变量(name
和add
),并且不可对其进行更改,也就是所有导入的变量都是只读的。
与命名导出类似,我们可以通过as
关键字可以对导入的变量重命名。如:
import { name, add as calculateSum } from './calculator.js';
calculateSum(2, 3);
在导入多个变量时,我们还可以采用整体导入的方式。如:
import * as calculator from './calculator.js';
console.log(calculator.add(2, 3));
console.log(calculator.name);
使用import * as
可以把所有导入的变量作为属性值添加到
对象中,从而减少了对当前作用域的影响。
// calculator.js
export default {
name: 'calculator',
add: function(a, b) { return a + b; }
};
// index.js
import myCalculator from './calculator.js';
calculator.add(2, 3);
对于默认导出来说,那就要默认导入,import
后面直接跟变量名,并且这个名字可以自由指定(比如这里是myCalculator
),它指代了calculator.js
中默认导出的值。
从原理上可以这样去理解:
import { default as myCalculator } from './calculator.js';
// index.js
import React, { Component } from 'react';
这里的React
对应的是该模块的默认导出,而Component
则是其命名导出中的一个变量。
注意:这里的React
必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。
复合写法在工程中,有时需要把某一个模块导入之后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:
export { name, add } from './calculator.js';
等同于
import { name, add } from './calculator.js';
export { name, add };
复合写法目前只支持当被导入模块(这里的calculator.js
)通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆开写。
import calculator from "./calculator.js ";
export default calculator;
不能写成export default from './calculator.js'
除非写为
export { default } from calculator;
但是这种方式依然还是命名导出而不是默认导出,命名的变量为default
而已。
下篇:CommonJS与ES6 Module的本质区别
参考资料:
Webpack实战:入门、进阶与调优
扩展阅读:
阮一峰:ECMAScript 6 入门----Module 的语法