谈谈对前端模块化的理解是面试时常常会被问到的问题,我以面试者的口吻来写了如何分步骤回答这道问题。前端
将一个复杂程序安装必定的规则封装成几个块儿,并组合在一块儿。块的内部,数据和函数实现是私有的,只向外部暴露出来一些接口与外部的其余模块通讯。node
把不一样功能封装成不一样的全局函数,污染全局命名空间,容易引发命名冲突或数据不安全,并且模块成员之间看不出直接关系。git
简单对象封装,减小了全局变量,解决命名冲突,可是数据不安全,外部能够直接修改模块内部的数据。es6
数据是私有的,外部只能经过暴露的方法操做,将数据和行为封装到一个函数内部,经过给window添加属性来向外暴露接口,可是如何解决模块依赖呢?github
(function(window) {
let data = "hello world"
function sayHi() {
console.log(data)
}
window.myModule = { sayHi }
})(window)
myModule.sayHi()
复制代码
(function(window) {
const name = 'David'
window.myModule2 = { name }
})(window);
(function(window, myModule2) {
let data = "hello world"
function sayHi() {
console.log(`${myModule2.name} said: "${data}"`)
}
window.myModule = { sayHi }
})(window, myModule2)
myModule.sayHi()
复制代码
如今AMD和CMD已经逐渐退出历史舞台,咱们主要介绍经常使用的两种规范:CommonJS和ES6模块化。面试
Node应用中的规范,每一个文件就是一个模块,有本身的做用域。在文件中定义的变量,函数和类都是私有的,对其余文件是不可见的。其余文件只能引用它暴露的接口。在服务端,模块的加载是运行时同步加载的。浏览器
特色:全部代码都在其模块做用域不会污染全局,执行的顺序是模块出现的顺序。模块屡次加载只会在第一次加载时运行一次,而后缓存。缓存
基本语法:暴露模块:module.exports = xxx; exports.xxx = value。引入模块:require(xxx)。安全
module.exports = function() {
console.log('hello world')
}
let sayHi = require('./test')
sayHi()
复制代码
module.exports = 1
const num = require('./test')
console.log(num)
复制代码
let sayHi = function() {
console.log('hello world')
}
let num = 5
module.exports = {
sayHi: sayHi,
num: num
}
let { sayHi, num } = require('./test')
sayHi()
console.log(num)
复制代码
模块的加载机制:输入的是输出值的拷贝,一旦输出一个值,模块内部的变化影响不到已经输出的值。由于它只运行一次,以后都都用的是缓存中的值。babel
ES6 模块的设计思想是尽可能的静态化(编译时加载),使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。
特色:一个模块就是一个独立的文件,该文件内部的全部变量,外部没法获取。若是但愿外部可以读取模块内部的某个变量,就必须使用export关键字输出该变量。export语句输出的接口与其对应的值是动态绑定关系,即经过该接口能够取到模块内部实时的值。
基本语法:export和import。export命令用于规定模块的对外接口,import命令用于输入其余模块提供的功能。
export const firtsName = 'Wang'
export const secondName = 'Lin'
const firtsName = 'Wang'
const secondName = 'Lin'
export { firtsName, secondName }
复制代码
import { firtsName, secondName } from './test.js'
console.log(firtsName + secondName)
复制代码
特别注意,export命令规定的是对外的接口,必须与模块内部的变量创建一一对应关系。
export 1 //报错
var m = 1; export m;//报错
//正确写法:
export var m = 1;
var m = 1; export {m}
var m = 1; export {n as m}
复制代码
使用import命令时用户须要知道多要加载的变量名和函数名,不然没法加载。能够用export default命令为模块指定默认输出。在import时就能够为该匿名函数指定任意名字。
export default function() {
console.log('hi')
}
import sayHi from './test.js'
sayHi()
复制代码
一个模块只能有一个默认输出,所以export default就是输出一个叫作defalut的变量或方法。它只能使用一次,因此import命令后面才不用加大括号。
es6模块在浏览器中的加载规则,<script type=”module” src=”myModule.js”></script>
加一个type属性设为module,浏览器就会认为它是es6模块,默认它是异步加载,等同于打开了defer属性。
执行机制:遇到模块加载命令import就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值。ES6模块是动态引用,并不会缓存值,模块里面的变量绑定其所在的模块。因为ES6输入的模块变量只是一个符号连接,因此这个变量是只读的,对它从新赋值会报错。
循环加载指的是,a脚本的执行依赖于b脚本,而b脚本的执行又依赖于a脚本。循环加载表示存在强耦合,若是处理很差,还可能致使递归加载,使得程序没法执行。ES6模块和CommonJS模块在处理循环加载时的方法是不同的,返回的结果也不同。
CommonJS的一个模块就是一个脚本文件。require命令第一次加载该脚本时就会执行整个脚本,而后在内存中生成一个对象。所以它的以下方法:
let { sayHi, num } = require('./test')
复制代码
等同于:
let test = require('./test')
let sayHi = test.sayHi
let num = test.num
复制代码
它生成一个对象以后,之后每次用到这个模块就会在这个对象中取值。不管CommonJS模块加载多少次,只有第一次加载时会运行一次,之后再加载时返回第一次运行的结果。除非手动清除缓存。
由于脚本代码在require的时候就会所有执行,一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出。当它循环加载完成,两个脚本都会被所有执行一遍,所以循环加载的模块中的值可能会被改写。
//a.js
exports.done = false //第二步,设置done属性为false并导出
var b = require('./b.js') //第三步, 第一次require加载b.js脚本,进入到b.js中,a.js暂停在这里。
console.log('在a.js之中,b.done = %j', b.done) //第九步,执行完b.js继续执行a.js,此时从b.js导出的exports中的最终值done为true
exports.done = true //第十步,设置done属性为true并导出,这是a.js导出的最终值
console.log('a.js执行完毕') //第十一步,a.js执行完毕
//b.js
exports.done = false //第四步,设置done属性为false并导出
var a = require('./a.js') //第五步,第二次require加载a.js脚本,不会再运行a.js,直接从内存中exports中取值
console.log('在b.js之中,a.done = %j', a.done) //第六步,因a.js没有执行完,从exports中取到的值是已经执行的部分,而不是最后的值
exports.done = true //第七步,设置done属性为true并导出,这是b.js导出的最终值
console.log('b.js执行完毕') //第八步,b.js执行完毕
//main.js
var a = require('./a.js') //第一步,第一次require加载a.js脚本,进入到a.js中。第十二步,执行完a.js后,能够获得a.js的最终值,done为true
var b = require('./b.js') //第十三步,第二次require加载b.js脚本,不会再运行b.js,直接从内存中exports中取值
console.log('在main.js之中,a.done = %j, b.done = %j', a.done, b.done)// 第十四步,取到最终值输出。
//执行main.js
node main.js
//执行结果
在b.js之中,a.done = false
b.js执行完毕
在a.js之中,b.done = true
a.js执行完毕
在main.js之中,a.done = true, b.done = true
复制代码
由于ES6的模块是动态引用,变量不会被缓存,而是成为一个指向被加载模块的引用。只要引用存在,代码就能执行。ES6加载的变量都是动态引用其所在模块的,只要引用存在,代码就能执行。而CommonJS中require时就会直接加载引用的模块,可以用到的只有已经执行的部分,若是用到尚未执行的部分就会报错。
//a.js
import { bar } from './b.js' //第一步,加载b.js,进入b.js
console.log('a.js') //第六步,执行完b.js,开始执行a.js
console.log(bar) //第七步,bar在b.js中的引用为'bar',输出bar
export let foo = 'foo' //第八步,具名导出foo的值为'foo'
//b.js
import { foo } from './b.js' //第二步,加载a.js,这时因为a.js已经开始执行,因此不会重复执行,继续执行b.js
console.log('b.js') // 第三步,输出b.js
console.log(foo) //第四步,输出foo的值,此时a.js尚未执行完,foo在./b.js中的引用为undefined,输出undefined
export let bar = 'bar' //第五步,具名导出bar的值为'bar'
//执行a.js
babel-node a.js
//执行结果
b.js
undefined
a.js
bar
复制代码
形成两种模块加载方案在处理“循环加载”时不一样的缘由在于,它们二者加载模块的不一样,一个加载拿到的是值的拷贝,一次拿到,不会改变;一个拿到的是值的引用,会随着执行的过程发生变化。
github.com/ljianshu/Bl…以及阮一峰《ES6标准入门》