在没有模块化概念之前,都是一个页面引入一个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); // fooconsole.log(Foo.bar); // barconsole.log(Foo.baz); // baz
// 对单个变量重命名import { foo, bar, baz as myBaz } from './foo.js';console.log(foo); // fooconsole.log(bar); // barconsole.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.jsexport const baz = 'origin:foo';// bar.jsexport * from './foo.js';export const baz = 'origin:bar';// main.jsimport { 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中不再存在
