模块之于程序,就如同细胞之于生物体,是具有特定功能的组成单元。不同的模块负责不同的工作,它们以某种方式联系在一起,共同保证程序的正常运转。
随着 JavaScript语言的发展,社区中产生了很多模块标准。在认识这些标准的同时,也要了解其背后的思想。例如,它为什么会有这个特性,或者为什么要这样去实现。这对我们自己编写模块也会有所帮助。
CommonJS是由JavaScript社区于2009年提出的包含模块文件IO控制台在内的一系列标准。在Nodejs的实现中采用了CommonJS标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS块和Nodejs中的实现并不完全一样,现在一般谈到CommonJS其实是Nodejs中的版本而非它的原始定义。
模块
CommonJS中规定每个文件是一个模块。CommonJS会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。
// calculator.js
var name ='calculator.js';
// index.js
var name ='index.js';
require('./calculator.js');
Console.1og(name); // index.js
这里有两个文件,在index;js中我们通过CommonJS的require函数加载calculatorjs。运行之后控制台结果是“indexjs”,这说明calculator;js中的变量声明并不会影响indexjs可见每个模块是拥有各自的作用域的。
导出
导出是一个模块向外暴露自身的唯一方式。在CommonJS中,通过module exports可以导出模块中的内容,如:
module.exports = {
name: 'calculator',
add: function(a,b){
return a+b
}
}
CommonJS模块内部会有一个module对象用于存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象:
var module =(...);
//棋块自身逻辑
module.exports =(...);
module.exports用来指定该模块要对外暴露哪些内容,在上面的代码中我们导出了一个对象,包含name和add两个属性。为了书写方便,CommonJS 也支持另一种简化的导出方式一直接使用exports
exports.name = 'calculator';
exports.add = function(a,b){
return a+b
}
在实现效果上,这段代码和上面的 module.exports 没有任何不同。其内在机制是将exports指向了module.exports
在使用exports时要注意一个问题,即不要直接给exports赋值,否则会导致其失效。
exports= {
name:'calculator'
};
上而代码中,由于对exports进行了赋值操作,使其指向了新的对象,module.exports却仍然是原来的空对象,因此name属性并不会被导出。
另一个在导出时容易犯的错误是不恰当地把module.exports与exports混用。要用只能用一个。
上面的代码先通过exports导出了add属性,然后将moduleexports重新赋值为另外一个对象。这会导致原本拥有add属性的对象丢失了,最后导出的只有name
另外,要注意导出语句不代表模块的末尾,在module.exports或exports后面的代码依旧会照常执行。
导入
在CommonJS中使用require进行模块导人
//calculator.js
module.exports = {
add: functiona,b) (return a + b;)
};
//index.js
const calculator = require(',/calculator.js');
const sum = calculator.add(2,3);
console.log(sum);// 5
我们在indexjs中导人了calculator模块,并调用了它的add函数。
当我们require一个模块时会有两种情况:
模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded 用于记录该模块是否被加载过。它的值默认为false,当模块第一次被执行过后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码
有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require 即可。
require('./task.js');
另外,require函数可以接收表达式,借助这个特性我们可以动态地指定模块加载路径。
const moduleName = ['foo.js','bar.js']
moduleNames.forEach(name=>{
require('./' + name);
})
模块
请看下面的例子,我们将前面的 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
ES6Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导人、导出语句。import和export也作为保留关键字在ES6版本中加入了进来
ES6 Module会自动采用严格模式,如果将原本是CommonJS的模块或任何未开启严格模式的代码改写为ES6Module要注意这点。
导出
在ES6Module中使用export命令来导出模块。export有两种形式:
一个模块可以有多个命名导出。它有两种不同的写法:
// 写法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}
上面两种写法的效果是一样的
在使用命名导出时,可以通过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理解为对外输出了一个名为deault的变量,因此不需像命名导出一样进行变量声明,直接导出值即可。
导入
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);
接下来处理默认导出,请看下面这个例子:
// 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';
复合写法目前只支持当被导入模块 (这里的 calculator.js)通过命名导出的方式暴露出来的变量
默认导出则没有对应的复合形式,只能将导人和导出拆开写。
import calculator from ",/calculator.js ";
export default calculator;