模块(module)是什么呢? 模块是为了软件封装,复用。当今开源运动盛行,咱们能够很方便地使用别人编写好的模块,而不用本身从头开始编写。在程序设计中,咱们一直强调避免重复造轮子(Don't Repeat Yourself,DRY)。javascript
想象一下,没有模块的日子,第三库基本都是导出一个全局变量供开发者使用。例如jQuery
的$
,lodash
的_
。这些库已经尽可能避免了全局变量冲突,只使用几个全局变量。可是仍是不能避免有冲突,jQuery
还提供了noConflict
。更遑论咱们本身编写的代码。html
最初,Javascript 中是没有模块的概念的。这可能与一开始 Javascript 的定位有关。Javascript 最初只是但愿给网页增长动态元素,定位是简单易用的脚本。 可是,随着网页端功能愈来愈丰富,程序愈来愈庞大,软件变得愈来愈难以维护。特别是随着 NodeJs 的兴起,Javascript 语言进入服务端编程领域。在编写大型复杂的程序,模块更是必须品。java
模块只是一个抽象概念,要想在实际编程中使用还须要规范。若是没有规范,我有这种写法,你用那种写法,岂不是乱了套。node
目前,模块的规范主要有3中,CommonJS模块、AMD模块和ES6模块。本文着重讲解 CommonJS 模块(以 Node 实现为表明)和ES6模块。git
CommonJS 实际上是一个通用的 Javascript 语言规范,并不只仅是模块的规范。Node 中的模块遵循 CommonJS 标准。es6
Node 中提供了一个require
方法用来加载模块。例如:github
var fs = require('fs');
fs.readFile('file1.txt', 'utf8', function (err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
复制代码
导入模块以后就可使用模块中定义的接口了,如上例中的readFile
。编程
在 Node 中大致上有3种模块,普通模块、核心模块和第三方模块。 普通模块与核心模块的导入方式稍微有些区别。普通模块是咱们本身编写的模块,核心模块是 Node 提供的模块。上面咱们使用的fs
就是核心模块。导入普通模块时,须要在require
的参数中指定相对路径。例如:api
var myModule = require('./myModule');
myModule.func1();
复制代码
模块myModule
的后缀.js
后缀能够省略。框架
Node 将核心模块编译进了引擎。导入核心模块只须要指定模块名,Node 引擎直接查找核心模块字典。
第三方模块的导入也是指定模块名,可是模块的查找方式有所不一样。 首先,在项目目录下的node_modules
目录中查找。 若是没有找到,接着去项目目录的父目录中查找。 直到找到加载该模块,或者到根目录还未找到返回失败。
在咱们平常的编程中,常常须要将一些功能封装在一个模块中,方便本身或他人使用。在 Node 中定义模块的语法很简单。模块单独在一个文件中,文件中可使用exports
导出接口或变量。例如:
function addTwoNumber(a, b) {
return a + b;
}
exports.addTwoNumber = addTwoNumber;
复制代码
假设该模块在文件myMath.js
中。在同一目录下,咱们能够这样来使用:
var myMath = require('./myMath');
console.log(myMath.addTwoNumber(10, 20)); // 30
复制代码
函数具体是怎么导出的呢?除了exports
,咱们常常看到的module.exports
,__dirname
,__filename
是从哪里来的? 在执行require
函数的时候,咱们能够理解 Node 额外作了一些处理。
function _doRequire(module, exports, __filename, __dirname) {
// 模块文件内容
}
复制代码
exports
属性,而后推算出__filename
(当前导入的这个模块的全路径文件名)和__dirname
(模块文件所在路径):var module = {};
module.exports = {}
// __filename = ...
// __dirname = ...
复制代码
_doRequire(module, module.exports, __filename, __dirname);
复制代码
require
返回的是module.exports
的值。按照上面的过程,咱们能够很清楚地理解模块的导出过程。而且也能很快地判断一些写法是否有问题:
错误写法:
function addTwoNumber(a, b) {
return a + b;
}
exports = {
addTwoNumber: addTwoNumber;
}
复制代码
这种写法为何不对?exports
实际上初始时是module.exports
的一个引用。给exports
赋一个新值后,module.exports
并无改变,仍是指向空对象。最后返回的对象是module.exports
,没有addTwoNumber
接口。
正确写法:
function addTwoNumber(a, b) {
return a + b;
}
// 正确写法一
exports.addTwoNumber = addTwoNumber;
// 正确写法二
module.exports.addTwoNumber = addTwoNumber;
// 正确写法三
module.exports = {
addTwoNumber: addTwoNumber
};
复制代码
exports
和module.exports
开始指向的是同一个对象。写法一经过exports
设置属性,一样对module.exports
也可见。写法二经过module.exports
设置属性也能够导出。 写法三直接设置module.exports
就更不用说了。
建议在程序开发中,坚持一种写法。我的以为写法三显示设置相对较容易理解。
**有一点须要注意:不是只有对象能够导出,函数、类等值也能够。**例以下面就导出了一个函数:
function addTwoNumber(a, b) {
return a + b;
}
module.exports = addTwoNumber;
复制代码
ES6 在标准层面为 Javascript 引入了一套简单的模块系统。ES6 模块彻底能够取代 CommonJS 和 AMD 规范。当前热门的开源框架 React 和 Vue 都已经使用了 ES6 模块来开发。
ES6 模块使用export
导出接口,import from
导入须要使用的接口:
// myMath.js
export var pi = 3.14;
export function addTwoNumber(a, b) {
return a + b;
}
// 或
var pi = 3.14;
function addTwoNumber(a, b) {
return a + b;
}
export { pi, addTwoNumber };
复制代码
// main.js
import { addTwoNumber } from './myMath';
console.log(addTwoNumber(10, 20));
复制代码
在myMath.js
中经过export
导出一个变量pi
和一个函数addTwoNumber
。上例中演示了两种导出方式。一种是一个个导出,对每个须要导出的接口都应用一次export
。第二种是在文件中某处集中导出。固然,也能够混合使用这两种方式。推荐使用第二种导出方式,由于能在一处比较清楚的看出模块导出了哪些接口。
ES6 模块有一些须要了解和注意的特性。
ES6 模块最重要的特性是“静态加载”,导入的接口是只读的,不能修改。NodeJS 中的模块,是动态加载的。
静态加载就是“编译”时就已经肯定了模块导出,能够作到高效率,而且便于作静态代码分析。同时,静态加载也限制了模块的加载在文件中全部语句以前,而且导入语法中不能含有动态的语法结构(例如变量、if语句等)。
例如:
// 能够调用,由于模块加载是“编译”时进行的。
funcA();
import { funcA, funcB } from './myModule';
// 错误,导入语法中含有变量
var foo = './myModule';
import { funcA, funcB } from './myModule';
// 错误,在if语句中
if (foo == "myModule") {
import { funcA, funcB } from './myModule';
} else {
import { funcA, funcB } from './hisModule';
}
// 错误,导出的接口是只读的,不能修改
import { funcA, funcB } from './myModule';
funcA = function () {};
复制代码
导出的接口与模块中定义的变量或函数必须是一一对应的。并且模块内相应的值修改了,外部也能感知到。看下面代码:
// 错误,导出值1,模块中没有对应
export 1;
// 错误,实际上也是导出1,模块中没有对应
var m = 1;
export m;
// 能够这样来导出,导出的m与模块中的变量m对应
export var m = 1;
// 能够这样导出
var m = 1;
export {m};
复制代码
var foo = "bar";
setTimeout(2000, () => { foo = "baz"});
// 2s后foo变为"baz",外部能感知到
复制代码
在导出模块时,能够为接口指定一个别名。这样,后续能够修改内部接口而保持导出接口不变。例如:
// myModule.js
var funcA = function () {
}
var funcB = function () {
}
export {
funcA as func1,
funcB as func2,
funcB as myFunc,
}
复制代码
上面咱们导出以别名func1
导出函数funcA
,以别名func2
和myFunc
导出函数funcB
。func2
和myFunc
都是指向同一个函数funcB
的。下面看看使用这个模块:
// main.js
import { func1, func2, myFunc } from './myModule';
复制代码
一样的,导入模块时也能够指定别名:
// main.js
import { func1 as func } from './myModule';
复制代码
上面介绍的模块导入必须知道接口名字。有时候,用户学习一个模块时但愿可以快速上手,不想去看文档(怎么会有这个懒的人🤣)。ES6 提供了default导出。例如:
// myModule.js
export default function () {
console.log('hi');
}
// default导出方式能够看作是导出了一个别名为default的接口
var f = function () {
console.log('hi');
}
export { f as default };
复制代码
在外部导入的时候,须要省略花括号:
// main.js
import func from './myModule';
func();
复制代码
也能够两种方式,同时使用:
// myModule.js
function foo() {
console.log('foo');
}
export default foo;
function bar() {
console.log('bar');
}
export { bar };
复制代码
// main.js
import foo, { bar } from './myModule';
复制代码
ES6 还容许一种总体加载的方式导入模块。经过使用import *
能够导入模块中导出的全部接口:
// myModule.js
export function funcA() {
console.log('funcA');
}
export function funcB() {
console.log('funcB');
}
复制代码
// main.js
import * as m from './myModule';
m.funcA();
m.funcB();
复制代码
总体加载所在的那个对象(m
),应该是能够静态分析的,因此不容许运行时改变。因此,下面的写法都是不容许的:
// main.js
import * as m from './myModule';
// 错误
m.name = 'darjun';
m.func = function () {};
复制代码
Node 因为已经有 CommonJS 的模块规范了,与 ES6 模块不兼容。为了使用 ES6 模块,Node 要求 ES6 模块采用.mjs
后缀名,并且文件中只能使用import
和export
,不能使用require
。并且该功能还在试验阶段,Node v8.5.0以上版本,指定--experimental-modules
参数才能使用:
// myModule.mjs
var counter = 1;
export function incCounter() {
console.log('counter:', counter);
counter++;
}
复制代码
// main.mjs
import { incCounter } from './myModule';
incCounter();
复制代码
使用下面命令行运行程序:
$ node --experimental-modules main.mjs
复制代码
随着 Javascript 在大型项目中占用举足轻重的位置,模块的使用称为必然。Node 中使用 CommonJS 规范。ES6 中定义了简单易用高效的模块规范。ES6 规范化是个必然的趋势,因此在掌握当前 CommonJS 规范的前提下,学习 ES6 模块势在必行。