CommonJS中规定每个文件是一个模块。
将一个JS文件直接通过scripts标签插入页面中与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。
//calculator.js
var name = 'calculator.js;
//index.js
require('./calculator.js');
var name = 'index.js';
console.log(name); // index.js
在index.js中通过CommonJS的require函数加载calculator.js,运行之后控制台打印的结果是"index.js"。这说明calculator.js中的变量声明并不会影响index.js。
导出是一个模块向外暴露自身的唯一方式。在CommonJS中,通过module.exports可以导出模块中的内容。
module.exports = {
name:'calculator',
add: function(a,b){
return a+b;
}
}
CommonJS模块内部会有一个module对象用于存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象;module.exports用来指定该模块要对外暴露哪些内容。
var module = {...};
module.exports ={...};
为了书写方便,CommonJS也支持另一种简化的导出方式,直接使用exports。
但不要直接给exports赋值,否则会导致失败。
因为直接对exports进行了赋值操作,使其指向了新的对象,module.exports却仍然是原来的空对象,因此添加的属性并不会被导出。
exports.name = 'calculator';
exports.add = function(a,b){
return a+b;
}
//失败
exports = {
name:'calculator',
add: function(a,b){
return a+b;
}
}
其内在机制是将exports指向了module.exports,而module.exports在初始化时是一个空对象,可以简单地理解为,CommonJS在每个模块的首部默认添加了以下代码:
var module = {
exports:{}
}
var exports = module.exports;
导出语句不代表模块的末尾,在module.exports或exports后面的代码依旧会照常执行。在实际使用中,为了提高可读性,应该将module.exports及exports语句放在模块的末尾。
在CommonJS中使用require进行模块导入。
当require一个模块时会有两种情况:
模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded用于记录该模块是否被加载过,它的值默认为false,当模块第一次被加载和执行过后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码。
//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
require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径。
const moduleNames = ['foo.js','bar.js'];
moduleNames.forEach(name=>{
require('./'+name);
});
有时加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂载全局对象上,此时直接使用require即可。
require('./task.js');
ES6 Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。import和export也作为保留关键字在ES6版本中加入了进来。
ES6 Module会自动采用严格模式,这在ES5中是一个可选项。以前可以通过选择是否在文件开始时加上"use strict"来控制严格模式,在ES6 Module中不管开头是否有"use strict"都会采用严格模式。
在ES6 Module中使用export命令来导出模块。export有两种形式:命名导出、默认导出。
一个模块可以有多个命令导出,它有两种不同的写法:
//写法1是将变量的声明和导出写在一行
export const name = 'calculator';
export const add = function(a,b){
return a+b;
}
//写法2则是先进性变量声明,然后再用同一个export语句导出
const name = 'calculator';
const add = function(a,b){
return a+b;
}
export {name,add};
在使用命令导出时,可以通过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语法导入模块。
加载带有命名导出的模块时,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);
与命名导出类似,可以通过as关键字对导入的变量命名。
import {name,add as calculateSum} from './calculator.js';
calculateSum(2,3);
在导入多个变量时,可以采用整体导入的方式。使用import * as
可以把所有导入的变量作为属性值添加到
对象中,从而减少对当前作用域的影响。
import * as calculator from 'calculator .js';
console.log(calculator.add(2,3));
console.log(calculator.name);
对于默认导出来说,import后面直接跟变量名,并且这个名字可以自由指定,它指代了默认导出的值。
//calculator.js
export default {
name:'calculator',
add: function(a,b){
return a+b;
}
}
//index.js
import myCalculator from 'calculator.js';
myCalculator .add(2,3);
从原理上可以这样去理解:
import {default as myCalculator } from 'calculator.js';
两种导入方式混合时,默认导出的变量名必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。
import React , {Companent} from 'react';
CommonJS与ES6 Module最本质的区别是前者对模块依赖的解决是动态的,而后者是静态的。动态的含义是,模块依赖关系的建立发生在代码运行阶段;而静态则是模块依赖的建立发生在代码编译阶段。
使用CommonJS,当模块A加载模块B时,会执行B中的代码,并将其module.exports对象作为require函数的返回值进行返回。并且require的模块路径可以动态指定,支持传入一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。
ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域,因为ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖的关系。它相比于CommonJS来说具备以下几点优势:
在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
CommonJS中的值拷贝:
//calculator.js
var count = 0;
module.exports = {
count:count,
add:function(a,b){
count += 1;
return a+b;
}
}
//index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;
console.log(count); //0。这里的count是对calculator.js中count值的拷贝
add(2,3);
console.log(count); //0。calculator.js中变量值的改变不会对这里拷贝值造成影响
count += 1;
add(2,3);
console.log(count); //1。拷贝的值可以更改
index.js中的count是对calculator.js中count的一份值拷贝,因此在调用add函数时,虽然更改了原本calculator.js中count的值,但是并不会对index.js中导入时创建的副本造成影响;另一方面,在CommonJS中允许对导入的值进行更改,在index.js中更改count将其赋于新值,由于是值得拷贝,这些操作不会影响calculator.js本身。
ES6 Module中值的动态映射:
//calculator.js
let count = 0;
const add = function(a,b){
count += 1;
return a+b;
}
export {count ,add};
//index.js
import {count,add} from './add-content.js';
console.log(count); //0。对calculator.js中count值的映射
add(2,3);
console.log(count); //1。实时反映calculator.js中count值的变化
count += 1;
add(2,3);
console.log(count); //不可更改,会抛出错误
ES6 Module中导入的变量其实是对原有值的动态映射,index.js中的count是对calculator.js中的count值的实时反映,当通过调用add函数更改了calculator.js中count值时,index.js中count的值也随之变化;不可以对ES6 Module导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里可以实时观察到原有的事物,但是并不可以操纵镜子里的镜像。
//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);
经过Webpack打包后将会成为如下形式(为了易读性只展示代码的大体结构):
上面的bundle分别以下几个部分:
__webpack__require
函数。对模块加载的实现,在浏览器中可以通过调用__webpack__require__(module_id)
来完成模块导入。bundle是如何在浏览器中执行的呢?
__webpack__require__
函数等,为模块的加载和执行做一些准备工作。__webpack__require__
),则会暂时交出执行权,进入__webpack__require__
函数体内进行加载其他模块的逻辑。__webpack__require__
中会判断即将加载的模块是否存在于installedModules中,如果存在则直接取值,否则会到第三步,执行该模块的代码来获取导出值。