原文连接:http://www.javashuo.com/article/p-fowgdsnn-k.htmljavascript
理想状况下,开发者只须要实现核心的业务逻辑,其余均可以加载别人已经写好的模块。html
可是,Javascript不是一种模块化编程语言,在es6之前,它是不支持”类”(class),因此也就没有”模块”(module)了。前端
Javascript社区作了不少努力,在现有的运行环境中,实现”模块”的效果。java
模块就是实现特定功能的一组方法。
只要把不一样的函数(以及记录状态的变量)简单地放在一块儿,就算是一个模块。node
1
2
3
4
5
6
|
function m1(){
//...
}
function m2(){
//...
}
|
上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就好了。jquery
这种作法的缺点很明显:”污染”了全局变量,没法保证不与其余模块发生变量名冲突,并且模块成员之间看不出直接关系。程序员
为了解决上面的缺点,能够把模块写成一个对象,全部的模块成员都放到这个对象里面es6
1
2
3
4
5
6
7
8
9
|
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
|
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性express
1
|
module
1.m1();
|
这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。编程
1
|
module._
count = 1;
|
使用”当即执行函数”(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var module = (function() {
var _count = 0;
var m1 = function() {
alert(_count)
}
var m2 = function() {
alert(_count +
1)
}
return {
m1: m1,
m2: m2
}
})()
|
使用上面的写法,外部代码没法读取内部的_count变量。
1
|
console.info(module._count); //undefined
|
module就是Javascript模块的基本写法。
在es6之前,尚未提出一套官方的规范,从社区和框架推广程度而言,目前通行的javascript模块规范有两种:CommonJS 和 AMD
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
这标志”Javascript模块化编程”正式诞生。前端的复杂程度有限,没有模块也是能够的,可是在服务器端,必定要有模块,与操做系统和其余应用程序互动,不然根本无法编程。
node编程中最重要的思想之一就是模块,而正是这个思想,让JavaScript的大规模工程成为可能。模块化编程在js界流行,也是基于此,随后在浏览器端,requirejs和seajs之类的工具包也出现了,能够说在对应规范下,require统治了ES6以前的全部模块化编程,即便如今,在ES6 module被彻底实现以前,仍是这样。
在CommonJS中,暴露模块使用module.exports和exports,不少人不明白暴露对象为何会有两个,后面会介绍区别
在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就能够像下面这样加载。
1
|
var math = require('math');
|
而后,就能够调用模块提供的方法:
1
2
|
var math = require('math');
math.add(
2,3); // 5
|
正是因为CommonJS 使用的require方式的推进,才有了后面的AMD、CMD 也采用的require方式来引用模块的风格
有了服务器端模块之后,很天然地,你们就想要客户端模块。并且最好二者可以兼容,一个模块不用修改,在服务器和浏览器均可以运行。
可是,因为一个重大的局限,使得CommonJS规范不适用于浏览器环境。仍是上一节的代码,若是在浏览器中运行,会有一个很大的问题
1
2
|
var
math = require('math');
math.add(2, 3);
|
第二行math.add(2, 3),在第一行require(‘math’)以后运行,所以必须等math.js加载完成。也就是说,若是加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,由于全部的模块都存放在本地硬盘,能够同步加载完成,等待时间就是硬盘的读取时间。可是,对于浏览器,这倒是一个大问题,由于模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
所以,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。
模块必须采用特定的define()函数来定义。
1
|
define(id?, dependencies?, factory)
|
若是一个模块不依赖其余模块,那么能够直接定义在define()函数之中。
1
2
3
4
5
6
7
8
9
|
// math.js
define(
function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
|
若是这个模块还依赖其余模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
1
2
3
4
5
6
7
8
|
define([
'Lib'], function(Lib){
function foo(){
Lib.doSomething();
}
return {
foo : foo
};
});
|
当require()函数加载上面这个模块的时候,就会先加载Lib.js文件。
AMD也采用require()语句加载模块,可是不一样于CommonJS,它要求两个参数:
1
|
require([module], callback);
|
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。若是将前面的代码改写成AMD形式,就是下面这样:
1
2
3
|
require(['math'], function (math) {
math.add(2, 3);
});
|
math.add()与math模块加载不是同步的,浏览器不会发生假死。因此很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:
1
2
3
4
|
define(
function(require, exports, module) {
var clock = require('clock')
;
clock.start()
;
})
;
|
CMD与AMD同样,也是采用特定的define()函数来定义,用require方式来引用模块
1
|
define(id?, dependencies?, factory)
|
1
2
3
4
5
|
define(
'hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
|
若是一个模块不依赖其余模块,那么能够直接定义在define()函数之中。
1
2
3
|
define(
function(require, exports, module) {
// 模块代码
});
|
注意:带 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。
AMD和CMD最大的区别是对依赖模块的执行时机处理不一样,而不是加载的时机或者方式不一样,两者皆为异步加载模块。
AMD依赖前置,js能够方便知道依赖模块是谁,当即加载;
而CMD就近依赖,须要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是不少人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到能够忽略。
ES6标准发布后,module成为标准,标准使用是以export指令导出接口,以import引入模块,可是在咱们一向的node模块中,咱们依然采用的是CommonJS规范,使用require引入模块,使用module.exports导出接口。
export语法声明用于导出函数、对象、指定文件(或模块)的原始值。
注意:在node中使用的是exports,不要混淆了
export有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每一个模块能够多个,而默认导出每一个模块仅一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
|
模块能够经过export前缀关键词声明导出对象,导出对象能够是多个。这些导出对象用名称进行区分,称之为命名式导出。
1
2
|
export { myFunction }; // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量
|
咱们可使用*和from关键字来实现的模块的继承:
1
|
export * from 'article';
|
模块导出时,能够指定模块的导出成员。导出成员能够认为是类中的公有对象,而非导出成员能够认为是类中的私有对象:
1
2
3
4
5
|
var name = 'IT笔录';
var domain = 'http://itbilu.com';
export {name, domain}; // 至关于导出
{name:name,domain:domain}
|
模块导出时,咱们可使用as关键字对导出成员进行重命名:
1
2
3
4
|
var name = 'IT笔录';
var domain = 'http://itbilu.com';
export {name as siteName, domain};
|
注意,下面的语法有严重错误的状况:
1
2
3
4
5
|
// 错误演示
export 1; // 绝对不能够
var a = 100;
export a;
|
export在导出接口的时候,必须与模块内部的变量具备一一对应的关系。直接导出1没有任何意义,也不可能在import的时候有一个变量与之对应
export a
虽然看上去成立,可是a的值是一个数字,根本没法完成解构,所以必须写成export {a}
的形式。即便a被赋值为一个function,也是不容许的。并且,大部分风格都建议,模块中最好在末尾用一个export导出全部的接口,例如:
1
|
export {
fun as default,a,b,c};
|
默认导出也被称作定义式导出。命名式导出能够导出多个值,但在在import引用时,也要使用相同的名称来引用相应的值。而默认导出每一个导出只有一个单一值,这个输出能够是一个函数、类或其它类型的值,这样在模块import导入时也会很容易引用。
1
2
|
export default function() {}; // 能够导出一个函数
export default class(){}; // 也能够出一个类
|
默认导出能够理解为另外一种形式的命名导出,默认导出能够认为是使用了default名称的命名导出。
下面两种导出方式是等价的:
1
2
3
4
|
const D = 123;
export default D;
export { D as default };
|
使用名称导出一个模块时:
1
2
3
4
5
6
|
// "my-module.js" 模块
export function cube(x) {
return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
export { foo };
|
在另外一个模块(脚本文件)中,咱们能够像下面这样引用:
1
2
3
|
import { cube, foo } from 'my-module';
console.log(cube(3)); // 27
console.log(foo); // 4.555806215962888
|
使用默认导出一个模块时:
1
2
3
4
|
// "my-module.js"模块
export default function (x) {
return x * x * x;
}
|
在另外一个模块(脚本文件)中,咱们能够像下面这样引用,相对名称导出来讲使用更为简单:
1
2
3
|
// 引用 "my-module.js"模块
import cube from 'my-module';
console.log(cube(3)); // 27
|
import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
import的语法跟require不一样,并且import必须放在文件的最开始,且前面不容许有其余逻辑代码,这和其余全部编程语言风格一致。
1
2
3
4
5
6
7
8
9
|
import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
|
咱们能够经过指定名称,就是将这些成员插入到看成用域中。导出时,能够导入单个成员或多个成员:
注意,花括号里面的变量与export后面的变量一一对应
1
2
|
import {myMember} from "my-module";
import {foo, bar} from "my-module";
|
经过*符号,咱们能够导入模块中的所有属性和方法。当导入模块所有导出内容时,就是将导出模块(’my-module.js’)全部的导出绑定内容,插入到当前模块(’myModule’)的做用域中:
1
|
import * as myModule from "my-module";
|
导入模块对象时,也可使用as对导入成员重命名,以方便在当前模块内使用:
1
|
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
|
导入多个成员时,一样可使用别名:
1
|
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
|
导入一个模块,但不进行任何绑定:
1
|
import "my-module";
|
在模块导出时,可能会存在默认导出。一样的,在导入时可使用import指令导出这些默认值。
直接导入默认值:
1
|
import myDefault from "my-module";
|
也能够在命名空间导入和名称导入中,同时使用默认导入:
1
2
3
4
|
import myDefault, * as myModule from "my-module"; // myModule 作为命名空间使用
或
import myDefault, {foo, bar} from "my-module"; // 指定成员导入
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// --file.js--
function getJSON(url, callback) {
let xhr = new XMLHttpRequest();
xhr.onload =
function () {
callback(
this.responseText)
};
xhr.open(
"GET", url, true);
xhr.send();
}
export function getUsefulContents(url, callback) {
getJSON(url,
data => callback(JSON.parse(data)));
}
// --main.js--
import { getUsefulContents } from "file";
getUsefulContents(
"http://itbilu.com", data => {
doSomethingUseful(data);
});
|
1
2
3
4
5
6
|
// d.js
export default function() {}
// 等效于:
function a() {};
export {a as default};
|
在import的时候,能够这样用:
1
2
3
4
|
import a from './d';
// 等效于,或者说就是下面这种写法的简写,是同一个意思
import {default as a} from './d';
|
这个语法糖的好处就是import的时候,能够省去花括号{}。
简单的说,若是import的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的as语法。
因此,下面这种写法你也应该理解了吧:
1
|
import $,{each,map} from 'jquery';
|
import后面第一个$是{defalut as $}的替代写法。
as简单的说就是取一个别名,export中能够用,import中其实能够用:
1
2
3
4
5
6
7
|
// a.js
var a = function() {};
export {a
as fun};
// b.js
import {fun as a} from './a';
a();
|
上面这段代码,export的时候,对外提供的接口是fun,它是a.js内部a这个函数的别名,可是在模块外面,认不到a,只能认到fun。
import中的as就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之因此是这样,是由于有的时候不一样的两个模块可能经过相同的接口,好比有一个c.js也经过了fun这个接口:
1
2
|
// c.js
export function
fun() {};
|
若是在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。
Module.exports
The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.
译文:module.exports对象是由模块系统建立的。 有时这是难以接受的;许多人但愿他们的模块成为某个类的实例。 为了实现这个,须要将指望导出的对象赋值给module.exports。 注意,将指望的对象赋值给exports会简单地从新绑定到本地exports变量上,这可能不是你想要的。
Module.exports
The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:
译文:exports变量是在模块的文件级别做用域内有效的,它在模块被执行前被赋于 module.exports 的值。它有一个快捷方式,以便 module.exports.f = … 能够被更简洁地写成exports.f = …。 注意,就像任何变量,若是一个新的值被赋值给exports,它就再也不绑定到module.exports(实际上是exports.属性会自动挂载到没有命名冲突的module.exports.属性)
从Api文档上面的能够看出,从require导入方式去理解,关键有两个变量(全局变量module.exports,局部变量exports)、一个返回值(module.exports)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function require(...) {
var module = { exports: {} };
((module, exports) => {
// 你的被引入代码 Start
// var exports = module.exports = {}; (默认都有的)
function some_func() {};
exports = some_func;
// 此时,exports再也不挂载到module.exports,
// export将导出{}默认对象
module.exports = some_func;
// 此时,这个模块将导出some_func对象,覆盖exports上的some_func
// 你的被引入代码 End
})(
module, module.exports);
// 不论是
exports仍是module.exports,最后返回的仍是module.exports
return module.exports;
}
|
demo.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
console.log(exports); // {}
console.log(module.exports); // {}
console.log(exports === module.exports); // true
console.log(exports == module.exports); // true
console.log(module);
/**
Module {
id:
'.',
exports: {},
parent:
null,
filename:
'/Users/larben/Desktop/demo.js',
loaded:
false,
children: [],
paths:
[
'/Users/larben/Desktop/node_modules',
'/Users/larben/node_modules',
'/Users/node_modules',
'/node_modules' ] }
*
/
|
注意