模块化主要是帮助我们更好的组织代码, 模块允许我们将相关的变量和函数放在一个模块中。 在ES6模块化之前,JS语言并没有模块的概念,只有函数作用域和全局作用域非常容易发生命名冲突。 之前的RequireJS, SeaJS, AMD, UMD,CMD在一定层面上都是为了解决JS模块化的问题。ES6模块取其精华:
- 它提供了简洁的语法
- 以及异步的, 可配置的模块加载
什么是模块
模块是自动运行在严格模式下并且没有办法退出运行的JavaScript代码
- 在模块的顶部this的值是undefined
2.其模块不支持html风格的代码注释 - 除非用default关键字,否则不能用这个语法导出匿名函数或类
任何未限制导出的变量、函数或类都是模块私有的,无法从模块外部访问
为什么要使用模块
目前最普遍的JS运行平台便是浏览器,在浏览器中,所有的代码都运行在同一个全局上下文中, 这使得你即使更改应用中的很小一部分, 你也要担心可能会产生的命名冲突。
传统的JS应用被分离在多个文件夹中,并且在构建的时候连接在一起,这稍显笨重。所以人们开始将每个文件内的代码都包在一个自执行函数中: (function(){ ... })();
。 这种方法创建了一个本地作用域,于是最初的模块化的概念产生了, 之后的CommonJS和AMD系统中所称的模块, 也是由此实现的。
创建模块
一个JS模块就是一个对其他模块暴露一些内部的属性、方法的文件。 这里仅讨论浏览器中的ES2016模块系统。
每个模块都有自己的上下文
和传统的JS不同,在使用模块时,你不必担心污染全局作用域。恰恰相反,你需要把所有你需要用到的东西从其他模块中导入进来,这样会使得模块之间的依赖关系更为清晰
导入导出
可以使用ES6的新关键字 import
和 exports
来导入或导出模块中的对象。 模块可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值等。
默认导出
每一个模块都支持导出一个不具名的变量,这为默认导出:
// helloWord.js
export default function(){
console.log('111');
}
// main.js
import hello from './helloWord';
import anotherHello from './helloWord';
hello(); // 111
anotherHello(); //111
console.log(hello === anotherHello); //true
等价的CommonJS语法:
// helloWord.js
module.exports = function(){
console.log('111');
}
//main.js
var hello = require('./helloWord');
var anotherHello = require('./helloWord');
hello(); // 111
anotherHello(); //111
console.log(hello === anotherHello); //true
任何JS值都是可以被默认导出的:
// helloWord.js
export default 3.14
//export default { foo: 'bar' };
//export default 'hello word';
// main.js
import hello from './helloWord';
console.log(hello); // 3.14
这里如果把注释放开,同样输出3.14, 当有多条export default语句,只会输出第一条export default的值
// helloWord.js
export default 3.14
export default { foo: 'bar' };
export default 'hello word';
// main.js
import hello from './helloWord';
console.log(hello); // 3.14
具名导入
除了默认导出外, ES6的模块系统还支持导出任意数量个具名的变量:
const PI = 3.14
const value = 42;
export function hello(){
console.log('2111');
}
export {PI,value}
// 等同于CommonJS语法
// var PI = 3.14;
// var value = 42;
// module.exports.hello = function(){
// console.log('2111');
// }
// module.exports.PI = PI;
// module.exports.value = value;
导入的时候:
import {PI,value,hello} from './helloWord';
console.log(PI,value,hello());
导入的时候可以使用as
关键字来重命名导入的变量:
import {PI as PI2,value as val,hello as helloWord} from './helloWord';
console.log(PI2,val,helloWord());
结果是一样的
导入所有
最简单的,在一条命令中导入一个模块中所有变量的方法, 是使用*
标记。 这样一来,被导入模块中所有导出的变量都会变成它的属性, 默认导出的变量则会被置于default
属性中。
// helloWord.js
const PI = 3.14
const value = 42;
export function hello(){
console.log('2111');
}
export {PI,value}
// main.js
import * as Hello from './helloWord';
console.log(Hello);
注意一点
import * as foo from
和import foo from
的区别。 后者仅仅会导入默认导出的变量,而前者则会在一个对象中导入所有,如:
// helloWord.js
const PI = 3.14
const value = 42;
const foo = {
'a':'123'
}
export function hello(){
console.log('2111');
}
export {PI,value,foo}
// main.js
import * as foo from './helloWord';
console.log(foo);
console.log(foo.foo);
对比四种模块价值规范
- CommonJS
- AMD
- CMD
- ES6模块
1. CommonJS
commonJS是服务器端的模块化规范, node.js 就是参照commonJS规范实现的。commonJS中有一个全局的方法 require()用来加载模块
function myModule(){
this.hello = function(){
return "hello"
}
this.goodbye = function(){
return "goodbye"
}
}
module.exports = myModule
其实module
变量代表当前模块
这样就可以在其他模块中使用这个模块
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello();
myModuleInstance.goodbye();
关于commonJS的更多,见CommonJS规范
2. AMD
commonJS定义模块的方式和引入模块的方式还是比较简单的,但不适合浏览器端, 因为commonJS是同步加载的。 而AMD是异步加载的,模块的加载不影响它后面语句的运行。 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。 这个用require.js实现AMD规范的模块化, 用require.config()指定引用路径等,
通过define()
来定义模块, 用requier加载模块
首先我们需要引入require.js文件和一个入口文件main.js. main.js中配置require.config()并规定项目中用到的基础模块。
/** 网页中引入require.js及main.js **/
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
引用模块的时候,我们将模块名放在[]
中作为 require()
的第一参数; 如果我们定义的模块本身也依赖其他模块, 那就需要将他们放在[]
中作为define
的第一参数
// 定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
define
的第一个参数是依赖的模块, 必须是一个数组。 通过return来暴露接口
通过 require()
来加载模块, 模块的名字默认为模块加载器请求的指定脚本的名字
require(['main'],function(main){
alert(main.foo());
})
require.js就是根据AMD规范来实现的, 优点是:
- 实现js文件的异步加载, 避免网页失去响应
- 管理模块之间的依赖性,便于代码的编写和维护
3. CMD
CMD也是异步模块定义
CMD与AMD的区别:
CMD相当于按需加载, 定义一个模块的时候不需要立即制定依赖模块,在需要的时候require就可以了,比较方便。
而AMD则相反,定义模块的时候需要制定依赖模块, 并以形参方式引入回调函数中。
// CMD 按需加载
define(function(require,exports,module){
var a = require('./helloWord');
a.hello();
var b = require('./counter');
console.log(b.foo);
// 2111
// aaa
})
// AMD 定义模块的时候需要制定依赖
define(['./helloWord','./counter'],function(a,b){
a.hello();
console.log(b.foo);
});
ES6
ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单, 完全可以取代现有的CommonJS和AMD规范, 成为浏览器和服务器通用的模块解决方案。
ES6模块主要有两个功能: export
和import
export
用于对外输出本模块(一个文件可以理解为一个模块)变量的接口
import
用于在一个模块中加载另一个含有exoport接口的模块
参考:
https://segmentfault.com/a/1190000010058955
https://juejin.im/post/5aaa37c8f265da23945f365c
https://segmentfault.com/a/1190000004100661
http://es6.ruanyifeng.com/#docs/module
深入系列:
https://github.com/mqyqingfeng/Blog/issues/108
https://zhuanlan.zhihu.com/p/33843378?group_id=947910338939686912
面试题
题目一:ES6与commonJS模块的差异
1. commonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
- commonJS模块一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6模块如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,原始值变了,import加载的值也会跟着变。需要开发者自己保证,真正取值的时候能够取到值。
**2. commonJS 模块是运行时加载, ES6模块是编辑时输出接口
运行时加载: commonJS模块就是对象,即在输入时是加载整个模块,生成一个对象,然后再从整个对象上读取方法,这种加载称为”运行时加载“。 commonJS脚本代码在require的时候,就会全部执行。一旦出现某个模板被”循环加载“,就只能输出已经执行的部分,还未执行的部分不会输出。
编译时加载: ES6模块不是对象,而是通过export命令显式指定输出的代码, import时指定加载某个输出值,而不是加载整个模块,这种加载称为”编译时加载“