ES6的出现,使得我们对Module
的理解停留在export
和import
的使用上,但整个JavaScript历史长河里,Module是怎样一个演变过程呢,下面就Module的进化过程做一个简单介绍,涉及的内容较多,每个模块涉入不深,供大家简单的了解。文中有什么说的不对的地方,欢迎提出指正。觉得内容还行的欢迎点赞,您的鼓励,是我前进的动力。
模块化诞生的初衷:web系统的庞大、复杂、团队的分工协作,使得后期维护的成本越来越高,而模块化是用于保持代码块之间相互独立而普遍使用的设计模式。
什么是
Module
呢?
Module
就是将一个复杂的程序依据一定的规则(规范)封装成几个块(文件)并进行组合在一起。块的内部数据/实现是私有的,只是向外部暴露一些接口(方法)与外部其它模块通信。
历史上,JavaScript一直没有模块体系,直到CommonJS
和AMD
的出现,下面来看下在ES6之前,是怎样将大程序拆分成互相依赖的小文件的。
模块化进化史
- 全局function模式 ----- 将不同的功能封装成不同的函数
// module1.js
let data = 'module1';
function foo() {
console.log(`foo():${data}`);
}
function bar() {
console.log(`bar():${data}`);
}
// module2.js
let data2 = 'module2';
function foo() { //与另一个模块中的函数冲突了
console.log(`foo():${data2}`);
}
// index.html
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
foo(); // foo():module2 foo()函数值的输出与js文件的加载顺序有关
bar(); // bar():module1
</script>
</body>
缺点:global变量被污染,容易造成命名冲突。
2.namespace模式 ----- 简单对象封装,减少了全局变量
// module1.js
let myModule1 = {
data: 'module1.js',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
// module2.js
let myModule2 = {
data: 'module2.js',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
// index.html
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
myModule1.foo()
myModule1.bar()
myModule2.foo()
myModule2.bar()
myModule1.data = 'other data' //能直接修改模块内部的数据
myModule1.foo(); // 输出other data
</script>
</body>
缺点:模块数据缺乏独立性,外部可更改内部数据。
3.IIFE模式(立即调用函数表达式)-----匿名函数自调用
优点:数据是私有的,外部只能通过暴露的方法操作
问题:如果当前模块依赖另一个模块怎么办?
// module.js
(function (window) {
let data = 'module'; // 数据
// 操作数据的函数
function foo() { // 用于暴露有函数
console.log(`foo():${data}`);
}
function bar() { // 用于暴露有函数
console.log(`bar():${data}`)
otherFun(); // 内部调用
}
function otherFun() { //内部私有的函数
console.log('otherFun()');
}
//暴露行为
window.myModule = {foo, bar}
})(window)
// index.html
<body>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo(); // foo():module
myModule.bar(); // bar():module otherFun()
//myModule.otherFun() // TypeError:myModule.otherFun is not a function
console.log(myModule.data); // undefined:不能访问模块内部数据
myModule.data = 'xxxx'; // 不能修改的模块内部的data
myModule.foo(); // foo():module,没有改变
</script>
</body>
4.IIFE增强模式----引入依赖(现代模块化实现的基石)
// module.js
(function (window, $) {
let data = 'NBA'; // 数据
// 操作数据的函数
function foo() { // 用于暴露有函数
console.log(`foo():${data}`);
$('body').css('background', 'red');
}
function bar() { // 用于暴露有函数
console.log(`bar() ${data}`);
otherFun(); // 内部调用
}
function otherFun() { // 内部私有的函数
console.log('otherFun()');
}
// 暴露行为
window.myModule = {foo, bar};
})(window, jQuery)
// index.html
<body>
// 引入的js必须有一定顺序
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
</body>
常见的模块化规范
在 ES6
之前,社区制定了一些模块加载方案,最主要的有CommonJS
和AMD
两种。前者用于服务器,后者用于浏览器。由于ES6模块化的出现,CommonJS
和AMD
规范渐渐很少被人使用。下面简单了解下两种规范如何使用。
1.CommonJS----用于服务器端,模块的加载是运行时同步加载的
- 定义暴露模块:exports
(1)exports.xxx = value;
(2)module.exports = value;
- 引入模块:require
(1) 第三方模块:var module = require('xxx模块名');
(2) 自定义模块:var module = require('模块文件相对路径');
2.AMD--- 专门用于浏览器端,模块的加载是异步的
- 定义暴露模块:
define()
(1) 定义没有依赖的模块
define(function() {
// 代码块
return 模块;
})
(2) 定义有依赖的模块
define(['module1', 'module2'], function(m1,m2) {
// 代码块
return 模块;
})
- 引入使用的模块
require(['module1', 'module2'], function(m1, m2) {
// 使用模块1和模块2
})
// 或者使用requirejs引入模块
requirejs(['module1', 'module2'], function(m1, m2) {
// 使用模块1和模块2
})
ES6模块化
ES6模块的设计思想是尽量静态化,使得在编译时就能确定模块之间的依赖关系,以及输入和输出的变量。ES6模块功能主要有两个命令构成:export
和import
。
下面简单介绍下这两个命令的使用方式及注意事项,更多详细介绍请参考阮一峰Module的语法。
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
ES6模块中默认采用严格模式,不管你头部有没有写"use strict"
1.export
导出export:作为一个模块,它可以选择性地给其它模块暴露(提供)自己的属性和方法,供其它模块使用。
// profile.js
// 写法一
export var firstName = 'zxy';
export var yesr = '2020';
//写法二 推荐写法
var firstName = 'zxy';
var year = '2020';
export {firstName, year}
注意事项:
-
export
命令除了输出变量,也可输出函数或类; -
export
输出的变量就是本来的名字,也可通过as关键字重命名;
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
-
export
语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部实时的值。 -
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。import
命令亦是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500); // 输出变量foo,值为bar,500ms之后变为baz
function foo() {
export default 'bar'; // SyntaxError
}
2.import
导入import:作为一个模块,可以根据需要,引入其它模块提供的属性或者方法,供自己模块使用。
// main.js
import { firstName, year } from './profile.js';
function setName(element) {
element.textContent = firstName ;
}
- 大括号中的变量名必须与被导入模块(profile.js)对外接口的名称相同,位置顺序无要求。
- import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径
- 如果想为输入的变量重新取一个名字,要在import命令中使用as关键字,将输入的变量重命名。
-
import
命令具有提升效果,会提升到整个模块的头部并首先执行。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。
import {firstName as name} from './profile.js';
foo();
import {foo} from 'module';
- 由于
import
是静态执行的,所以不能使用表达式和变量,只有在运行时才能得到结果的的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
- 如果多次重复执行同一句
import
语句,那么只会执行一次,而不会执行多次。
import 'loadsh';
import loadsh;
- 导入不存在的变量,值为
undefined
// module1.js
export var name = 'jack';
// module2.js
import {height} from './module1.js';
console.log(height); // 输出结果:undefined
- 声明的变量,对外都是只读的。请注意下方解释
// module1.js
export var name = 'jack';
// module2.js
import {name} from './module1.js';
name="修改字符串变量"; // 亲试,不会报错,输出:修改字符串变量
这里的对外只读并不是指
import
声明的变量都是只读的,引入后不可改写,而是当定义的接口或变量为只读时(通过Object.defineProperty()
),import
引入的变量就不可被更改。
3.模块的整体加载
- 除了指定加载某个输出值,还可以使用整体加载(即星号*)来指定一个对象,所有输出值都加载在这个对象上。
// moduleA.js
var name='zxy';
var age = '18';
var say = function() {
console.log('say hello');
}
export {name, age, say}
// moduleB.js
import * as obj from './moduleA.js';
obj.name; // zxy
obj.age; // 18
obj.say(); // say hello
4.export default命令
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以import
命令后面不用加大括号,因为只可能对应一个方法。
// moduleA.js
export default function() {
console.log('zxy')
}
// moduleB.js
import sayDefault from './moduleA.js';
sayDefault(); // zxy
-
export default
就是输出一个叫作default
的变量或方法,然后系统允许我们为它取任意名字。使用as关键字。 - 因为
export default
命令其实是输出一个叫default
的变量,因此它后面不能跟变量声明语句。
export var a = 1; // 正确
export default var a = 1; // 错误
var a = 1; export default a; // 正确
同样地,因为export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
export default 1; // 正确
export 1; // 错误
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default
。
-
import()方法
-import()
函数用途完成动态加载。import()
返回一个Promise
对象。
function initEntity() {
return Promise.all([
import('./A.js'),
import('./B.js');
]).then([transDefine] => {
// code
})
- 按需加载
btn.addEventListener('click', event =>{
import('./dialogBox.js');
}).then(dialogBox => {
dialogBox.open();
}).catch(error => {
// code
})
- 条件加载
if(condition) {
import('moduleA');
} else {
import('moduleB');
}
网友评论