模块化是组织代码的一种方式。将全部的js
业务逻辑代码写在一个文件里面,不只致使文件庞大,并且难以管理和维护。javascript
好比:html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
<script> ...// 一些业务逻辑 </script>
</body>
</html>
复制代码
为了方便维护,能够经过外部引入的方式:前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
...
<script type="javascript" src="...(file path)"></script>
</body>
</html>
复制代码
这种方式我的看来也是一种模块化
的方式,只不过这种方式存在许多弊端。vue
// test.js是基于jQuery.js开发的,也就是说test.js是依赖于jQuery.js的,因此jQuery必须先于test.js引入。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
...
<script type="javascript" src="jQuery.js"></script>
<script type="javascript" src="test.js"></script>
</body>
</html>
复制代码
// a.js
var a=1;
// b.js
var a=2;
//test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="a.js"></script>
<script src="b.js"></script>
<title>js</title>
</head>
<body>
<script> alert('a的值为'+a); </script>
</body>
</html>
复制代码
执行结果以下:java
为了解决这些问题,一些模块化的规范就出现了。jquery
先想想,为何模块很重要?git
由于有了模块,咱们就能够更方便地使用别人的代码,想要什么功能,就加载什么模块。github
可是,这样作有一个前提,那就是你们必须以一样的方式编写模块,不然你有你的写法,我有个人写法,岂不是乱了套!考虑到 Javascript
模块如今尚未官方规范,这一点就更重要了。面试
js
不像其余高级语言有模块系统,标准库较少和更缺少包管理系统express
js
起初只有全局对象的形式,经过一个个小函数来实现不一样的模块功能
function m1(){
  //...
}
function m2(){
  //...
}
复制代码
渐渐发展,经过构建对象的形式,来武装不一样的功能
var module1 = new Object ({
  _count : 0,
  m1 : function (){
    //...
  },
  m2 : function (){
    //...
  }
});
复制代码
上面的函数 m1()
和 m2()
,都封装在 module1
对象里。使用的时候,就是调用这个对象的属性:
module1.m1();
可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。
module1._count = 5;
复制代码
继续发展,经过当即执行函数和闭包的形式来分离一个又一个的小组件
3、当即执行函数写法
使用"当即执行函数"(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。
var module1 = (function(){
  var _count = 0;
  var m1 = function(){
    //...
  };
  var m2 = function(){
    //...
  };
  return {
  m1 : m1,
  m2 : m2
  };
})();
复制代码
使用上面的写法,外部代码没法读取内部的_count 变量。
console.info (module1._count); //undefined
复制代码
module1
就是Javascript
模块的基本写法。
下面,再对这种写法进行加工。
放大模式( augmentation)
若是一个模块很大,必须分红几个部分,或者一个模块须要继承另外一个模块,这时就有必要采用"放大模式"(augmentation)。
var module1 = (function (mod){
  mod.m3 = function () {
    //...
  };
  return mod;
})(module1);
复制代码
上面的代码为 module1
模块添加了一个新方法 m3()
,而后返回新的 module1
模块。
宽放大模式(Loose augmentation)
在浏览器环境中,模块的各个部分一般都是从网上获取的,有时没法知道哪一个部分会先加载。若是采用上一节的写法,第一个执行的部分有可能加载一个不存在的空对象,这时就要采用"宽放大模式"。
var module1 = ( function (mod){
  //...
  return mod;
})(window.module1);
复制代码
与"放大模式"相比,"宽放大模式"就是"当即执行函数"的参数能够是空对象。
当对象多起来的时候,又开始经过命名空间,来实现分级管理
输入全局变量
独立性是模块的重要特色,模块内部最好不与程序的其余部分直接交互。
为了在模块内部调用全局变量,必须显式地将其余变量输入模块。
var module1 = (function ($, YAHOO) {
  //...
})(jQuery, YAHOO);
复制代码
上面的 module1
模块须要使用 jQuery
库和 YUI
库,就把这两个库(实际上是两个模块)看成参数输入 module1
。这样作除了保证模块的独立性,还使得模块之间的依赖关系变得明显
commonJS
规范的提出成了javascript
历史上最重要的里程碑。主流的模块化的规范有:
commonJs
规范AMD
规范(Asynchronous Module Definition) (异步模块定义)CMD
规范ES6 module
CommonJS
规范CommonJS
规范的使用Node.js
是commonJS
规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module
、exports
、require
、global
。实际使用时,用module.exports
定义当前模块对外输出的接口(不推荐直接用exports
),用require
加载模块。
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上须要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模块时,不须要带路径
var http = require('http');
http.createService(...).listen(3000);
复制代码
commonJS
用**同步
**的方式加载模块。在服务端,模块文件都存在本地磁盘,读取很是快,因此这样作不会有问题。可是在浏览器端,限于网络缘由,更合理的方案是使用异步加载。
commomJS
的实现原理commonJS
简化版源码
function Module(id, parent){
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = []
}
Module.prototype.require = function(path){
return Module._load(path, this)
}
//由此可知,require 并非全局命令,而是每一个模块提供的一个内部方法,也就是说,只有在模块内部才能使用require命令,(惟一的例外是REPL 环境)。另外,require 其实内部调用 Module._load 方法。
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:若是有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
module.exports = Module;
复制代码
Module
类的一个实例。module
是global
全局对象的一个属性。global
对象也有一个全局函数require()
,global
对象的require()
是对module.require()
函数的进一步抽象和封装。AMD
规范AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。
AMD
规范有了服务器端模块之后,很天然地,你们就想要客户端模块。并且最好二者可以兼容,一个模块不用修改,在服务器和浏览器均可以运行。
可是,因为一个重大的局限,使得CommonJS
规范不适用于浏览器环境。仍是上一节的代码,若是在浏览器中运行,会有一个很大的问题。
var math = require('math');
math.add(2, 3);
复制代码
第二行math.add(2, 3)
,在第一行require('math')
以后运行,所以必须等math.js
加载完成。也就是说,若是加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,由于全部的模块都存放在本地硬盘,能够同步加载完成,等待时间就是硬盘的读取时间。可是,对于浏览器,这倒是一个大问题,由于模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
所以,浏览器端的模块,不能采用"同步加载"(synchronous
),只能采用"异步加载"(asynchronous
)。这就是AMD
规范诞生的背景。
AMD
规范的使用AMD
也采用require()
语句加载模块,可是不一样于CommonJS
,它要求两个参数:
require([module], callback);
复制代码
第一个参数[module]
,是一个数组,里面的成员就是要加载的模块;第二个参数callback
,则是加载成功以后的回调函数。若是将前面的代码改写成AMD
形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
复制代码
math.add()
与math
模块加载不是同步的,浏览器不会发生假死。因此很显然,AMD
比较适合浏览器环境。
目前,主要有两个Javascript
库实现了AMD
规范:require.js和curl.js。
AMD
规范的写法require.js 加载的模块,采用 AMD
规范。也就是说,模块必须按照 AMD
的规定来写。 具体来讲,就是模块必须采用特定的 define()
函数来定义。若是一个模块不依赖其余模块。那么能够直接定义在 define()
函数之中。 假定如今有一个 math.js
文件,它定义了一个math
模块。那么,math.js
就要这样写:
// math.js
define(function (){
 var add = function (x,y){
  return x+y;
 };
 return {
  add: add
 };
});
复制代码
加载方法以下:
// main.js
require(['math'], function (math){
 alert(math.add(1,1));
});
复制代码
若是这个模块还依赖其余模块,那么define()
函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['myLib'], function(myLib){
 function foo(){
  myLib.doSomething();
 }
 return {
  foo : foo
 };
});
复制代码
当 require()
函数加载上面这个模块的时候,就会先加载myLib.js
文件。
加载非规范的模块
理论上,require.js 加载的模块,必须是按照 AMD 规范、用 define()
函数定义的模块。可是实际上,虽然已经有一部分流行的函数库(好比jQuery
)符合 AMD
规范,更多的库并不符合。那么,require.js
是否可以加载非规范的模块呢? 回答是能够的。 这样的模块在用 require()
加载以前,要先用 require.config()
方法,定义它们的一些特征。 举例来讲,underscore
和 backbone
这两个库,都没有采用 AMD
规范编写。若是要加载它们的话,必须先定义它们的特征。
require.config({
 shim: {
  'underscore': {
   exports: '_'
  },
  'backbone': {
   deps: ['underscore', 'jquery'],
   exports: 'Backbone'
  }
 }
});
复制代码
require.config()
接受一个配置对象,这个对象除了有前面说过的paths
属性以外,还有一个 shim
属性,专门用来配置不兼容的模块。具体来讲,每一个模块要定义: (1)exports
值(输出的变量名),代表这个模块外部调用时的名称; (2)deps
数组,代表该模块的依赖性。 好比,jQuery
的插件能够这样定义:
shim: { 
'jquery.scroll': {  
deps: ['jquery'], 
exports: 'jQuery.fn.scroll'
}
}
复制代码
CMD
规范CMD
是Common Module definition
的缩写,即通用模块定义。CMD
规范是国内发展出来的,就像AMD
有个requireJS
,CMD
有个浏览器的实现SeaJS
,SeaJS
要解决的问题和requireJS
同样,只不过在模块定义方式和模块加载(能够说运行、解析)时机上有所不一样
CMD
规范的写法define
,用来定义模块。在CMD
中,一个模块就是一个文件,格式为:
define( factory );
复制代码
JSON
数据模块:define({ "foo": "bar" });
复制代码
2.经过字符串定义模板模块:
define('this is `data`.');
复制代码
factory 为函数的时候,表示模块的构造方法,执行构造方法即可以获得模块向外提供的接口。
require 是一个方法,接受模块标识做为惟一参数,用来获取其余模块提供的接口:require(id)
define(function( require, exports ){
var a = require('./a');
a.doSomething();
});
复制代码
require
是同步往下执行的,须要的异步加载模块可使用 require.async
来进行加载:
define( function(require, exports, module) {
require.async('.a', function(a){
a.doSomething();
});
});
复制代码
require.resolve( id )
可使用模块内部的路径机制来返回模块路径,不会加载模块。
exports 是一个对象,用来向外提供模块接口
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
define( function(require, exports, module) {
// 模块代码
});
复制代码
示例:
// math.js
define(function(require,exports,module) {
exports.add = function() {
var sum = 0,i = 0,args = arguments,l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
});
复制代码
// increment.js
define(function(require,exports,module) {
var add = require('math').add;
exports.increment = function(val) {
return add(val,1);
};
});
复制代码
// program.js
define(function(require ,exports,module) {
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
module.id == "program";
});
复制代码
AMD
规范和CMD
规范的比较AMD
是提早执行,CMD
是延迟执行。CMD
推崇 as lazy as possibleCMD
推崇依赖就近,AMD
推崇依赖前置。// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖能够就近书写
b.doSomething()
// ...
});
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
// ...
})
复制代码
AMD
(requirejs
)模块会提早执行**,用户体验好,**而CMD
(seajs
)性能好,由于只有在须要时候才执行。ES6
模块规范ES6
的语言规格中引入了模块化功能,也就很好的取代了以前的commonjs
和AMD
规范,成为了浏览器和服务器的通用的模块解决方案,在现今(vuejs
,ReactJS
)等框架大行其道中,都引入了ES6
中的模块化(Module
)机制。
Es6
中模块导出的基本语法模块的导出,export
关键字用于暴露数据,暴露给其余模块
使用方式是,能够将export
放在任何变量,函数或类声明的前面,从而将他们从模块导出,而import
用于引入数据,例如以下所示:
将下面这些js
存储到exportExample.js
中,分别导出的是数据,函数,类:
exportExample.js
// 1. 导出数据,变量前面加上export关键字
export var name = "随笔川迹"; // 导出暴露name变量
export let weChatPublic = "itclanCoder"; // 暴露weChatPublic
export const time = 2018; // 暴露time
// 2. 导出函数,函数前面加上export关键字
export function sum(num1,num2){
return num1+num2;
}
/* * * 以上等价于 * function sum(num1,num2){ * return num1+num2; * } * export sum; * 也能够这样:在定义它时没有立刻导出它,因为没必要老是导出声明,能够导出引用,所以下面这段代码也是能够运行的 */
// 3. 导出类,类前面加上export关键字
export class People{
constructor(name,age){
this.name = name;
this.age = age;
}
info(){
return `${this.name}${this.age}岁了`;
}
}
复制代码
注意:一个模块就是一个独立的文件,该文件内部的全部变量,外部没法获取,一样,任何未显示导出的变量,函数或类都是模块私有的,若没有用export
对外暴露,是没法从模块外部访问的 例如:
function countResult(num1,num2){
return num1-num2;
}
// 没有经过export关键字导出,在外部是没法访问该模块的变量或者函数的
复制代码
对应在另外一个模块中经过import
导入以下所示,模块命名为importExample.js
import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"
var people = new People("小美",18); // 实例化perople对象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());
复制代码
注意:在上面的示例中,除了export
关键字外,每个声明与脚本中的如出一辙,由于导出的函数和类声明须要有一个名称,因此代码中的每个函数或类也确实有这个名称,除非用default
关键字,不然不能用这个语法导出匿名函数或类。
Es6
中模块导入的基本语法若是想从一个文件(模块)访问另外一个文件(模块)的功能,则须要经过import
关键字在另外一个模块中引入数据,import语句的两个部分组成分别是**:要导入的标识符和标识符应当从那个模块导入,**另外,导入的标识符的顺序能够是任意位置,可是导入的标识符(也就是大括号里面的变量)与export暴露出的变量名应该是一致的。具体的写法以下:
import {identifer1,indentifer2} from "./example.js" // import {标识符1,标识符2} from "本地
复制代码
1. 导入单个绑定
// 只导入一个
import {sum} from "./example.js"
console.log(sum(1,2)); // 3
sum = 1; // 抛出一个错误,是不能对导入的绑定变量对象进行改写操做的
复制代码
2. 导入多个绑定
若是想从示例模块中导入多个绑定,与单个绑定类似,多个绑定值之间用逗号隔开便可:
// 导入多个
import {sum,multiply,time} from "./exportExample.js"
console.log(sum(1,2)); // 3
console.log(multiply(1,2)); // 3
console.log(time); // 2018
复制代码
在这段代码中,从exportExample.js
模块导入3
个绑定,sum
,multiply
和time
以后使用它们,就像使用本地定义的同样 等价于下面这个: **无论在import语句中把一个模块写了多少次,该模块将只执行一次,导入模块的代码执行后,实例化过的模块被保存在内存中,**只要另外一个import
语句使用它就能够重复使用它.
import {sum} from "./exportExample.js"
import {multiply} from "./exportExample.js"
import {time} from "./exportExample.js
复制代码
3. Es6
中导入整个模块
特殊状况下,能够导入整个模块做为一个单一的对象,而后全部的导出均可以做为对象的属性使用,例如:
// 导入一整个模块
import * as example from "./exportExample.js"
console.log(example.sum(1,example.time));
consoole.log(example.multiply(1,2));// multiply与sum函数功能同样
复制代码
在上面这段代码中,从本地模块的exportExample.js
中导出的全部绑定被加载到一个被称做为example
的对象中,指定的导出sum()
函数,multiply()
函数和time
以后做为example
的属性被访问,这种导入格式被称为命名空间导入,由于exportExample.js
文件中不存在example
对象,因此它被做为exportExample.js
中全部导出成员的命名空间对象而被建立
Es6
中如何给导入导出时标识符重命名从一个模块导入变量,函数或者类时,咱们可能不但愿使用他们的原始名称,就是导入导出时模块内的标识符(变量名,函数,或者类)能够不用一一对应,保持一致**,**能够在导出和导入过程当中改变导出变量对象的名称
使用方式①: 使用as
关键字来指定变量,函数,或者类在模块外应该被称为何名称,例如以下一函数:
function sum(num1,num2){
return num1+num2;
}
export {sum as add} // as后面是从新指定的函数名
复制代码
如上代码,函数sum
是本地名称,add
是导出时使用的名称,换句话说,当另外一个模块要导入这个函数时,必须使用add这个名称:
若在importExample.js
一模块中,则导入的变量对象应是add
而不是sum
,是由它导出时变量对象决定的
import {add} from "./exportExample.js"
复制代码
使用方式②: 使用as
关键字来指定变量,函数,或者类在主模块内应该被称为何名称,例如以下一函数:
// exportExample.js
export function sum(num1,num2){
return num1+num2;
}
复制代码
// importExample.js
import {sum as add} from "./exportExample.js"
console.log(sum(1,2)); // 3
复制代码
如上代码导入add
函数时使用了一个导入名称来重命名sum
函数,注意这种写法与前面导出export
时的区别,使用import
方式时,从新命名的标识符在前面,as
后面是本地名称,可是这种方式,即便导入时改变函数的本地名称,即便模块导入了add
函数,在当前模块中也没有add()
标识符,如上对add
的类型检测就是很好的验证.
ES6
匿名方式的导入和导出若是在不给导出的标识符(变量,函数,类)呢,那么能够经过导出default
关键字指定单个变量,函数或者类, 在import
的时候, 名字随便写, 由于每个模块的默认接口就一个。
//a.js
let sex = "boy";
export default sex //(sex不能加大括号)
//本来直接export sex外部是没法识别的,加上default就能够了.可是一个文件内最多只能有一个export default。 其实此处至关于为sex变量值"boy"起了一个系统默认的变量名default,天然default只能有一个值,因此一个文件内不能有多个export default。
复制代码
// b.js
//本质上,a.js文件的export default输出一个叫作default的变量,而后系统容许你为它取任意名字。因此能够为import的模块起任何变量名,且不须要用大括号包含
import any from "./a.js"
import any12 from "./a.js"
console.log(any,any12) // boy,boy
复制代码