什么是模块?
一个模块(module)就是一个文件。一个脚本就是一个模块。就这么简单。
模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数.
模块模式
把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行那些外部代码.
1.1 模块标识符
模块标识符是所有模块系统通用的概念.每个模块系统都有一个引用它的标识符,可能是字符串,可能是路径.
1.2 模块依赖
模块依赖的核心是管理依赖,当前模块可以通过模块标识符以及相关方法,声明一组外部依赖.
1.3 模块加载
模块在执行前,会先进行加载模块.当前模块内有外部模块依赖时,浏览器会对外部模块进行分析,检测外部模块是否存在其他模块依赖,如有会继续进行相同操作,如果没有则会递归的评估并加载所有依赖,直到所有依赖模块全部加载完成,最后进行模块执行.
1.4 入口
相互依赖的模块必须指定一个模块作为入口,这也是代码执行的起点.
1.5 异步依赖
使用期约可以让JavaScript通知模块系统在必要时加载新模块,并在模块加载完成后提供回调.
伪代码:
load('module').then(function(module){
module.doStuff();
});
1.6 动态依赖
模块系统支持开发者在程序结构中动态添加依赖.
if( 条件){
require('./module')
}
在上面这个模块中,是否加载module只有运行时才知道.
1.7 静态分析
模块中包含的发送到浏览器中的Javascript代码经常会被静态分析,分析工具会检测代码结构并在不实际执行代码的情况下推断其行为.
1.8 循环依赖
各模块间相互依赖,称为循环依赖.
//module1
require('./module3')
require('./module2')
console.log('module1')
//module2
require('./module3')
require('./module4')
console.log('module2')
//module3
require('./module1')
require('./module2')
console.log('module3')
//module4
require('./module1')
require('./module3')
console.log('module4')
在上面模块代码中,任何模块都可以为入口模块,即使他们存在循环依赖.
如果入口模块为module1,
浏览器: 加载module1=>发现依赖module3=>加载module3 => 发现依赖module1 (正在进行加载),跳过,继续进行加载,发现依赖module2=>加载module2=> 发现依赖module3 (正在进行加载),跳过,继续进行加载,发现依赖module4 => 加载module4 => 发现依赖module1 (正在进行加载),跳过,发现依赖module3 (正在进行加载),跳过,继续进行加载,输出module4.
最后输出结果为:
module4
module2
module3
module1
ES6之前的模块系统
2.1 使用函数作用域和立即调用函数表达式的模块
使用函数作用域和立即调用函数表达式将模块定义封装在匿名闭包里.模块定义是立即执行的.
var module1=(function(){
console.log('module1')
return {
num:1,
str:'dd'
}
})()
//
//
console.log(module1.num)//1
泄露模块模式
还有一种为泄露模块模式,返回一个对象,属性是对函数私有数据和变量的引用.
var module2=(function(){
var name='wta'
var fun=function(){
console.log('module2')
}return{
name:name,
fun:fun
}
})()
//
//
module2.fun()//module2
配置 拓展模块
var module3=(function(){
var name='wat'
return {
name:name
}
})(module3||{})
//
var module3=(function(module){
module.fun=function(){
console.log(module3)
}
return module
})(module3||{})
注意:尽量避免手写模块系统,除了恶意使用eval()外,并没有其他更好的动态加载依赖的方法.要添加异步加载和循环依赖非常难.
2.2 CommonJS
CommonJS概述了同步声明依赖的模块定义.CommonJS不能在浏览器中直接运行,其一般用在服务器端实现模块化代码组织.
其主要通过 require()实现模块依赖,通过exports对象定义自己的公共API.
模块第一次加载会被缓存,后续加载会取得缓存的模块,从而支持循环依赖.
在CommonJS中模块加载是模块系统执行的同步操作.
var module1=require('./module1')
var name='wta'
module.exports={
name=name,
num='ddd',
}
CommonJS可以托管类定义
//module1
class A{}
module.exports=A;
//module2
var A= require('./module1')
var a=new A();
如果想在浏览器中使用CommonJS需要提前把模块文件打包,把全局变量转化为原生JS结构,将模块代码封装在函数闭包中,最终只提供一个文件,为了以正确顺序打包模块,需要事先生成全面的依赖图.
2.3 异步模块定义AMD
异步模块定义AMD的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟问题.
AMD的核心是使用函数包装模块定义
参数一为模块名,参数二为依赖的模块(注意require,exports),参数三为模块内容.
define('module2',function(){
name='wta'
return name;
})
define('module1',['module2','require','exports'],function(module2){
return module2.name;
}
define('module3',['module1','require','exports'],function(module1,require,exports){
var module2=require('./module2')
exports= module2.name;
}
2.4 通用模块定义
(function(root,factory){
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
console.log('是commonjs模块规范,nodejs环境')
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(factory)
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
module.exports = factory()
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}
}(this, function() {
return {
name: '我是一个umd模块'
}
})
}
3.使用ES6模块
3.1 模块标签及定义
<script type='module'></script>
与传统脚本不同,所有模块会按顺序执行,解析到<script type='module'>标签后,会立即下载模块文件,但执行会延迟到文档解析完成.
<!--一般用于入口模块,只有通过外部文件加载的模块才可以使用import
一个文件有多少个入口模块没有限制,重复加载同一个模块也没有限制.
-->
<!--第二个执行-->
<script type='module'>
import'./moduleA.js'
</script>
<!--第三个执行-->
<script type='module' src='module.js'></script>
<!--第一个执行-->
<script></script>
3.2 模块加载
可以通过浏览器原生加载,也可以与第三方加载器和构造工具一起加载.
3.3 模块行为
模块代码只在加载后执行.
模块只能加载一次.
模块是单例.
模块可以定义公共接口
模块可以请求加载其他模块
支持循环依赖
ES6新行为
默认在严格模式下执行
不共享全局命名空间
顶级this的值是undefined
var声明不会添加到window对象
ES6是异步加载和执行的
3.4 模块导出 export
- export 关键字标记了可以从当前模块外部访问的变量和函数。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果想要外接获取模块内部的某个变量或方法,就必须使用export关键字输出该变量。
// profile.jsexport
var firstName = "John";
export var lastName = "Jackson";
export var year = 1999;
上述代码表示在profile.js文件(模块)中,输出了3个变量。
这种写法等价于:
var firstName = "John";
var lastName = "Jackson";var year = 1999;
export { firstName, lastName, year };
在export命令后面使用大括号指定了所要输出的一组变量,等价于在每个变量前面加export。
export命令除了可以输出变量,也可以输出函数和类,写法相同。
export输出的变量就是本来的名字,但是可以使用as关键字重命名。
function f1() {}function f2() {}export { v1 as Fun1, v2 as Fun2, v2 as Foo };
可以使用as关键字对同一个变量或方法重命名两次,使其在引入模块中,可以使用不同的名字进行引用。
export命令可以出现在模块的任意顶层作用域的位置,不能出现在块级作用域内。
export语句输出的值是动态绑定的,绑定其所在的模块。
export default命令
import命令在加载变量名或函数名时,需要事先知道导出的模块的接口名,否则无法加载。可以使用export default命令指定模块的默认输出接口。
// profile.js
export default function () {
console.log("my name is John Jackson, I was born in 1999");
}
上述代码中,profile模块默认输出的是一个函数。这样,导入模块就可以不用指定要加载的接口名了。
// main.js
import myName from './profile';
myName(); // "my name is John Jackson, I was born in 1999"
在main.js文件中,myName指代的就是profile文件输出的默认接口。这意味着import命令可以用任意名称指向profile文件输出的默认接口,而不需要知道接口名。
export default命令用在非匿名函数前也是可以的,此时函数名在模块外部是无效的,加载时视同匿名函数。
// profile.js
export default function sayName () {
console.log("my name is John Jackson, I was born in 1999");
}
// main.js
import myName from './profile';
myName(); // "my name is John Jackson, I was born in 1999"
一个模块只能有一个默认输出,因此export default在一个模块中只能使用一次。所以,对应的import命令可以不加大括号。
如果要在一条import命令中同时引入默认方法和其他变量,可以写成以下这样:
import customName, { otherMethod } from './module-name';
customName指代默认接口的命名,otherMethod指代其他接口。
3.5 模块导入 import
使用export命令定义了模块的对外接口后,其他JS文件就可以通过import命令加载这个模块的接口。
// main.js
import { firstName, lastName, year } from './profile';
import命令接受一个对象,里面指定了要从其他模块导入的变量名。对象中的变量名必须和要加载的模块导出的接口变量名一致(如果接口没有使用as关键字,就使用原始变量名,如果使用as关键字,则使用重命名后的接口名)。
同理,如果要对引入的变量名进行重命名,可以在import命令中使用as关键字,将输入的变量重命名。
// main.js
import { firstName as surname } from './profile';
上述写法中,需要书写每个接口的变量名,如果是要对整个模块进行整体加载,可以使用星号(*)指定一个对象,将输出模块的所有接口都加载到这个对象上。
// main.js
import * as person from './profile';
import命令具有提升效果,会提升到整个模块的顶部首先执行。
3.6 模块转移导出
模块之间可以继承。假设有个Circle模块继承了Shape模块。
// cicle.js
export * from 'Shape';
export var pi = 3.14159265359;
export default function area(r) {
return pi * r * r;
}
export {ar as a} from 'Shape';
export * 表示输出模块Shape的所有属性和方法,但不会输出Shape的默认方法。
module命令
模块之间可以继承。假设有个Circle模块继承了Shape模块。
// main.js
module person from './profile';
export * 表示输出模块Shape的所有属性和方法,但不会输出Shape的默认方法。
ES6模块加载的实质
ES6模块输出的是值的引用。
ES6模块遇到模块加载命令import时不会去执行模块,只会生成一个动态的只读引用。等到真的需要用到时,再到模块中取值。
ES6的输入有点像UNIX系统的“符号链接”,原始值变了,输入值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let count = 3;
export function add() { count++; }
// main.js
import { count, add } from './lib';
console.log(count); // 3
add();
console.log(count); // 4
由于ES6输入的模块变量只是一个“符号链接”,所以这个变量是只读的,对它进行重新赋值会报TypeError异常。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
obj指向的地址是只读的,无法为其重新赋值。