在没有模块化概念之前,都是一个页面引入一个JS
文件赋负责页面的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="A.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="B.js"></script>
</body>
</html>
如果A
页面和B
页面有相同的逻辑,就可以把逻辑单独抽离出去。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="A.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="B.js"></script>
</body>
</html>
但是这样还有问题:
1、如果common.js
文件中有很多功能的逻辑,而我只想用某一个函数,这样就导致了不合理性
2、假如common.js
文件内存在其他JS
文件的依赖关系,那就必须按照顺序引入
3、多个JS
文件的引入会造成变量污染
var a = function(){ //... }
var b = function(){
// 假如 A.js 文件需要依赖这里的 b 函数,在 A.html 页面就必须先引入 common.js 文件再引入 A.js 文件
}
// 假如 A.js 文件里也有一个 c 函数,这样 c 函数就会被覆盖
var c = function(){ //... }
所以,模块化的概念就是为了解决以上的问题!
立即执行函数
「立即执行函数」具有立即执行的特性,因为函数有自己的作用域和执行期上下文,可以利用立即执行函数创建模块的独立作用域。
var moduleA = (function(){
var a = [1, 2, 3, 4, 5].reverse(); // 数组进行反转
return {
a
}
})()
var moduleB = (function(moduleA){
var b = moduleA.a.concat([10, 11, 12])
return {
b
}
})(moduleA)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="module_A.js"></script>
<script type="text/javascript" src="module_B.js"></script>
<script type="text/javascript">
console.log(moduleA.a); // [5, 4, 3, 2, 1]
console.log(moduleB.b); // [5, 4, 3, 2, 1, 10, 11, 12]
</script>
</body>
</html>
这样虽然解决了变量污染的问题,但是依然必须按照顺序来加载文件。
CommonJS
直到NodeJS
的产生带来了真正的模块化,利用module.exports
将模块导出,利用require
将模块导入。CommonJS
是一种模块化的规范而不是任何的JS
代码,其来源于NodeJS
,所以CommonJS
只能运行在NodeJS
中。
:::info
CommonJS
只要引入就会创建一个模块的实例(实例化)
所有的文件都是同步进行加载的CommonJS
加载文件具有缓存的特性,当文件没有更改的时候再次引入就会使用缓存
:::
var moduleA = (function(){
var a = [1, 2, 3, 4, 5].reverse();
return {
a
}
})();
module.exports = {
moduleA
}
var moduleA = require("./module_A.js")
var moduleB = (function(){
var b = moduleA.a.concat([10, 11, 12])
return {
b
}
})();
AMD
AMD
,异步模块定义,是RequireJS
在推广过程中对模块定义的规范化产出。
它是「依赖前置」 (依赖必须一开始就写好)会先尽早地执行(依赖)模块 。换句话说,所有的require
都被提前执行(require
可以是全局或局部 )。
// 创建一个名为“a”的模块
define('a', function(require, exports, module) {
exports.getTime = function() {
return new Date();
}
});
// 创建一个名为“b”的模块,同时使用依赖require、exports和名为“a”的模块
define('b', ['require', 'exports', 'a'], function(require, exports, a) {
exports.test = function() {
return {
now: a.getTime()
};
}
});
require(['b'], function(b) {
console.log(b.test());
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<!-- 引入 require.js,然后使用 data-main 属性指定入口文件为 scripts/main -->
<script data-main="scripts/main" src="require.js"></script>
</body>
</html>
CMD
CMD
是SeaJS
在推广过程中对模块定义的规范化产出。
它推崇「依赖就近」,想什么时候**require**
就什么时候加载,实现了懒加载(延迟执行 ) ;
define(function(require, exports, module) {
exports.getTime = function() {
return new Date();
}
});
define(function(require, exports, module) {
/* 按需加载 a.js */
var a = require('./a');
console.log(a.getTime());
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="sea.js"></script>
<script type="text/javascript">
seajs.use('./scripts/main');
</script>
</body>
</html>
AMD
与CMD
的比较:AMD
:依赖前置,预执行(异步加载:依赖先执行)。CMD
:依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)
相关链接:
ESModule
AMD
和CMD
都是民间创建的用法,直到ES6
的发版带来了ESModule
模块化。
带有type="module"
属性的<script>
标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。模块可以嵌入在网页中, 也可以作为外部文件引入:
<script type="module">
// 模块代码
</script>
<script type="module" src="./main.js"></script>
所有模块都会像<script defer>
加载的脚本一样按顺序执行。解析到<script type="module">
标签后会立即下载模块文件,但执行会延迟到文档解析完成。
模块导出
ES6
模块支持两种导出:命名导出和默认导出。不同的导出方式对应不同的导入方式。
// export 关键字用于声明一个值为命名导出。
// 导出语句必须在模块顶级,不能嵌套在某个块中
// 允许
export ...
// 不允许
if (condition) {
export ...
}
导出值对模块内部JavaScript
的执行没有直接影响,因此export
语句与导出值的相对位置或者export
关键字在模块中出现的顺序没有限制。
// 允许
const foo = 'foo';
export { foo };
// 允许
export const foo = 'foo';
// 允许,但应该避免
export { foo };
const foo = 'foo';
导出时也可以提供别名,别名必须在export
的大括号语法中指定。
const foo = 'foo';
export { foo as myFoo };
以上的方式就是命名导出,export
还可以默认导出,默认导出使用default
关键字将一个值声明为默认导出,每个模块只能有一个默认导出。
const foo = 'foo';
export default foo; // 外部模块可以导入这个模块,而这个模块本身就是 foo 的值
因为命名导出和默认导出不会冲突,所以在一个模块中可以同时定义这两种导出:
const foo = 'foo';
const bar = 'bar';
export { bar };
export default foo;
模块导入
模块可以通过使用import
关键字使用其他模块导出的值。与export
类似,import
必须出现在模块的顶级:
// 允许
import ...
// 不允许
if (condition) {
import ...
}
模块标识符可以是相对于当前模块的相对路径,也可以是指向模块文件的绝对路径。它必须是纯字符串,不能是动态计算的结果。
// 错误
import a from "src" + "a.js";
如果不需要模块的导出,但仍想加载和执行模块,可以只通过路径加载它:
import './foo.js';
导入的模块都是只读的,所以不能进行修改,但是可以修改对象的属性:
import foo Foo './foo.js';
foo = 'foo'; // 错误
foo.bar = 'bar'; // 允许
命名导出的导入方式:
const foo = 'foo',
bar = 'bar',
baz = 'baz';
export { foo, bar, baz }
// 对全部变量重命名
import * as Foo from './foo.js';
console.log(Foo.foo); // foo
console.log(Foo.bar); // bar
console.log(Foo.baz); // baz
// 对单个变量重命名
import { foo, bar, baz as myBaz } from './foo.js';
console.log(foo); // foo
console.log(bar); // bar
console.log(myBaz); // baz
默认导出的导入方式:
export default const foo = "foo"
// 等效
import { default as foo } from './foo.js';
import foo from './foo.js';
如果模块同时导出了命名导出和默认导出,则可以在import
语句中同时取得它们。
import foo, { bar, baz } from './foo.js';
import { default as foo, bar, baz } from './foo.js';
import foo, * as Foo from './foo.js';
模块转移导出
如果想把一个模块的所有命名导出集中在一块,可以像下面这样在bar.js
中使用*导出:
export * from './foo.js';
如果两个模块内有相同的变量名则会静默覆盖:
// foo.js
export const baz = 'origin:foo';
// bar.js
export * from './foo.js';
export const baz = 'origin:bar';
// main.js
import { baz } from './bar.js';
console.log(baz); // origin:bar
import()
import
命令会被JavaScript
引擎静态分析,先于模块内的其他语句执行。所以,下面的代码会报错。
// 报错
if (x === 2) {
import MyModual from './myModual';
}
如果import
命令要取代Node
的require
方法,这就形成了一个障碍。因为require
是运行时加载模块,import
命令无法取代require
的动态加载功能。
const path = './' + fileName;
const myModual = require(path);
所以在ES2020
的时候提出了import()
来动态加载模块,改方法返回一个Promise
:
const someVariable = "test";
import(`./section-modules/${someVariable}.js`)
.then(module => {
// ...
})
.catch(err => {
// ...
});
import()
它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()
函数与所加载的模块没有静态连接关系,这点也是与import
语句不相同。**import()**
类似于**Node.js**
的**require()**
方法,区别主要是前者是异步加载,后者是同步加载。
CommonJS 和 ESModule 的区别:
- 模块导入导出语法不同:
commonjs
是module.exports
,exports
导出require
导入;ES6
则是export
导出,import
导入
2.CommonJS
是运行时加载模块,ES6
是在静态编译期间就确定模块的依赖
3.ES6
在编译期间会将所有import
提升到顶部,CommonJS
不会提升require
4.CommonJS
导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES6
是导出的一个引用,内部修改可以同步到外部。
5. 两者的循环导入的实现原理不同:CommonJS
是当模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。ES6
模块是动态引用,如果使用import
从一个模块加载变量(即import foo from 'foo'
),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
6.CommonJS
中顶层的this
指向这个模块本身,而ES6
中顶层this
指向undefined
。
7. 然后就是CommonJS
中的一些顶层变量在ES6
中不再存在