模块化是一种将 JavaScript 程序拆分为可按需导入的独自模块的机制,随着现在 JavaScript 脚本体积越来越大、越来越简单,JavaScript 的模块化机制也变得越来越重要,当初,简直所有最新浏览器都反对 js 原生模块化机制。
模块化的意义何在?
js 模块化机制将 js 代码拆分到不同的细小文件中,有以下长处:
- 每个文件都具备公有命名空间,防止全局净化、变量抵触。
- 逻辑拆散,能够将不同逻辑代码放在不同的 js 文件中。
- 进步代码的可复用性、可维护性和可读性
基于对象、闭包的模块化
基于对象的模块化
在 CommonJs、ES6Module 呈现以前,为了防止全局变量净化,罕用的一种办法就是将一类变量放到一个对象中,这样每个对象里的属性(变量)就都是该对象公有的,防止了变量抵触的问题。
let a = {sayHello: 'hello1'}
let b = {sayHello: 'hello2'}
这样即便呈现雷同的变量名,也不会造成抵触,将每个逻辑点相干的变量放到一个对象中,尽量减少变量净化的状况。js 内置对象 Math 也是由这种思路实现的。
基于闭包的模块化
IIFE(立刻调用函数表达式)
IIFE 是一个定义时就会调用的函数,定义一个 IIFE 很简略,只须要写两个小括号,第一个括号里申明一个匿名函数,第二个括号里传入实参。
// IIFE 有两种写法格调,两种都能够失常应用
(function (arg) {console.log(arg)
})(1); // IIFE 前面必须要加分号,示意完结
// 1
(function (arg) {console.log(arg)
}(2)); // IIFE 前面必须要加分号,示意完结
// 2
立刻调用函数表达式(IIFE)具备以下长处:
- 函数内的变量不会造成全局净化。
- 函数执行完后就会立刻销毁,不会造成资源节约。
设想一下有一个工具,它能解析代码文件,将每个文件的内容包装成一个立刻调用函数表达式中,还能够跟踪每个函数的返回值,将所有内容拼装成一个大文件。
一些代码打包工具就是基于这种思维实现的。
两种形式的不足之处
两种办法尽管都能实现公有命名空间,防止变量净化问题,然而依然存在一些显著的缺点。
对于基于对象实现的模块化而言:
- 申明变量就变成了申明对象的一个属性,没方法应用申明变量的一些有用机制,可能会导致反复命名属性造成属性笼罩的问题。
- 对象之间可能呈现笼罩的状况
- 代码仍在一个文件里,会导致文件代码量越来越大
// 意外地笼罩了属性 b
let o = {b: 1}
o.a = 1;
o.b = 2; // 笼罩了属性 b,并且 js 不会呈现任何提醒
let a = 1;
let b = 2;
let b = 3; // js 会报错,提醒不能反复申明
对于基于闭包的模块化而言:
- IIFE 中的变量和函数不可复用
- 应用不不便
- 难以测试,难以保护
此外两种形式都并不是真正的现实下的模块化,都存在不能将代码拆分到不同文件中,难以保护,公有命名空间的实现有缺点等问题。
Node.js 的模块化(CommonJs)
现实中的模块化,应该是能够将不同代码拆分到不同的文件中,这样有利于可维护性和可读性,不同代码文件之间能够相互导入,有利于可复用性,每个代码文件都具备公有命名空间,这样能够防止变量抵触和净化。
CommonJs 模块机制实现了以上要求,CommonJs 是 Nodejs 内置的模块化机制。它能够将简单的程序拆分成任意多个代码文件,每个文件都是一个领有公有命名空间的独立模块,能够抉择导出其中一个或者所有的变量和函数,另一个代码文件能够导入到本人的文件中,实现变量及函数的复用。
node 的导出
node 的导出有两种形式,一种是module.exports
,另一种是exports
。这两个对象都是全局内置的,能够间接应用,他们的用法如下
// 你能够将变量独自一个一个的导出
exports.a = "a";
exports.b = 123;
exports.fn = function() {console.log('我是一个函数,而且还是匿名的')
}
// 切记,这样写是不行的,具体起因稍后解释
exports = {
a,
b
}
// 也能够一起导出
let c = true;
let fn2 = function(){console.log('我是一个函数,而且还是匿名的')
}
module.exports = {
c,
fn2
}
你可能会感觉 module.exports
和exports
很像, 事实上他们的确有关系,module.exports
和 exports
援用的是同一个对象,也就是说 exports.a
等同于module.exports.a
。
具体点说就是 module.exports
是一个对象,node 会将它外面的属性和办法都公开,供其余模块导入,而 exports
是指向 module.exports
的变量,exports.xx
其实就是module.exports.xx
。
同时这也解释了后面为什么不能间接让 exports = xx
,因为这样会扭转exports
本来的指向。
node 的导入
既然有导出,天然就有导入,nodejs 模块通过调用 require()
实现对其余模块数据的导入。
你可能看到过以下这两种类型的导入形式
const fs = require('fs');
const user = require('./user.js')
第一种是导入 nodejs 内置的 fs 模块,无需写门路,而第二种是导入用户本人写的模块,因而要写门路(门路能够是相对路径,也能够是绝对路径)。
模块的分类
事实上,后面咱们说的 node 内置模块也称为 外围模块,它在 nodejs 源代码编译的时候就会被编译成二进制文件,nodejs 启动时,这些外围模块就会间接被加载进内存,所以外围模块加载时,绝对于文件模块,外围模块援用时不须要进行文件定位和动静编译,速度上有劣势,导入时间接写模块名,不必填写门路,如 http,fs,path 等罕用模块都属于外围模块。
而用户本人写的代码模块叫做 文件模块,文件模块也依据导入的形式不同分为 <u> 门路模式引入的模块 </u> 和 <u> 自定义模块 </u>。
const express = require('express');// 自定义模块
const usersRouter = require('./routes/users');// 以门路模式引入的模块
对于门路模式引入的模块,因为提供了确切的门路,引入时 require 办法会把指定的门路转化为硬盘上的实在门路,并用这个门路作为索引将编译后的后果进行缓存。因为指定了门路,这种模式的文件模块在路径分析时能够节俭大量的工夫,其速度比自定义模块要快,然而比外围模块要慢
- 外围模块
-
文件模块
- 以门路导入的模块
- 自定义模块
路径分析
对于自定义模块,自定义模块会遵循以下策略进行 路径分析,这会消耗大量工夫。
- 查找当前目录下的 node_modules 目录,看是否有匹配项
- 查找父级目录下的 node_modules 目录,看是否有匹配项
- 依照这个规定始终往父目录搜寻直到到根目录下的 node_modules
文件定位
当实现路径分析之后,导入的门路没有文件扩展名,node 会对文件进行扩展名进行剖析,会依照.js,.node,.json 这个程序一一进行尝试。
如果门路指向的不是一个文件,而是一个目录,那么:
- 首先会在命中的目录下寻找 package.json 这个文件并用 JSON.parse 进行解析,取出 json 文件中 main 属性的值,作为命中的文件
- 如果找不到 package.json 或者对应的 main 属性,那么会用这个目录上面 index 文件作为命中文件,仍旧是依照.js,.node,.json 这个程序一一进行尝试
- 如果仍旧找不到 index,那么此次文件定位失败,将会依照下面提到的门路遍历规定,往上一级持续寻找
因为一层一层的查找,自定义模块的路径分析须要消耗大量的事件,会导致搜寻效率较为低下,所以自定义模块的加载性能要比以门路模式加载的形式要慢。
缓存
另外,node 会将导入过的模块进行 缓存,下一次援用时,会先查看缓存中有没有对应的文件,优先从缓存中进行加载,缩小不必要的耗费。
总结
因而,node 能够用 module.exports
和exports
进行导出操作,其中 module.exports
指向的是要导出的对象,而 exports
是对 module.exports
的指向;node 通过 require()
进行导入操作,导入的形式能够是有门路的,也能够是无门路的(外围模块,自定义模块)。
node 的模块分以下几类:
- 外围模块
-
文件模块
- 以门路导入的模块
- 自定义模块
加载速度:缓存 > 外围模块 > 以门路导入的模块 > 自定义模块
ES6 的模块化
ES6 为 JavaScript 增加了 import
和export
关键字,将模块化作为外围语言个性来反对了。
ES6 Module 在概念上和 CommonJs 基本相同,都是将代码拆分到不同的代码文件中,每个代码文件都是一个模块,模块之间能够相互导入和导出。
ES6 Module 根本应用
// a.js
export a = 'a';
export fn1(){console.log("hello ES6")
}
// 和 CommonJs 一样,es6 Module 也能够一起导出
let b = 1;
let fn2 = function(){console.log("hello ES6 模块")
}
export {b,fn2}
// b.js
import a from "./a.js"
console.log(a.a);
a.fn1() // hello ES6
留神:export {b,fn2}
看似如同是申明了一个对象字面量,然而实际上这里的花括号并不会定义对象字面量,这种导出语法仅仅是要求在一对花括号中给出一个逗号分隔的列表。
另外,ES6 的模块会主动采纳严格模式,不论你有没有在模块头部加上"use strict";
。
扩大
除了下面的根本用法外,import
和 export
还有一些扩大应用办法。
使用解构
import {a} from "./a.js"console.log(a) // 等于 import a from "./a.js"console.log(a.a)
通过 as
关键字重命名
import {a as b} from "./a.js"console.log(b) // 等于 import {a} from "./a.js"console.log(a) // 也能够在导出时设置别名 export {a as b}
执行一个模块,然而不导入任何值能够这样写
import "./a.js"
如果多次重复执行同一句 import
语句,那么只会执行一次,而不会执行屡次。
整体加载,即用星号(*
)指定一个对象,所有输入值都加载在这个对象下面。
import * as moduleA from './a.js';console.log(moduleA.a) // 'a'
从下面的例子来看,导入须要先晓得导出的函数 / 变量名,能力应用,这样并不是很不便,咱们能够设置一个默认值。
// moduleA.jsexport default function () { console.log('foo');}
这样,导入的时候不须要晓得 moduleA 导出的函数名或变量名。
// moduleB.js// 能够命名为任意名称 import fn from "./moduleA"fn() // foo
下面代码的 import
命令,能够用任意名称指向 moduleA.js
输入的办法,这时就不须要晓得原模块输入的函数名。须要留神的是,这时 import
命令前面,不应用大括号。
留神:一个模块中 export default
只能存在一个
实质上,export default
就是输入一个叫做 default
的变量或办法,而后零碎容许你为它取任意名字。所以,上面的写法也是无效的。
// modules.jsfunction add(x, y) {return x * y;}export {add as default};
export 与 import 的复合写法
如果在一个模块之中,先输出后输入同一个模块,import
语句能够与 export
语句写在一起。
export {foo, bar} from 'my_module';// 能够简略了解为 import {foo, bar} from 'my_module';export {foo, bar};
更多复合写法见网道 -ES6
动态导入和动静导入
后面应用的 import
导入具备以下特点:
- 存在晋升行为,会晋升到整个模块的头部,首先执行
- 导入的变量和函数都是只读的,因为它的实质是输出接口
- 编译时导入
import
和 export
命令只能在模块的顶层,不能在代码块之中(比方,在 if
代码块之中,或在函数之中),因为引擎解决 import
是在编译阶段,在代码执行前就先将模块内容导入,这时不会去剖析或执行 if
语句。
// 报错 if (true) {import moduleA from "./a.js"}
正是因为 import
是动态导入,所以要导入的模块是编写代码时就确定了的,无奈在代码执行时决定。
// 报错 const path = './' + fileName;import a from path;
为了解决这个问题,ES2020 提案 引入 import()
函数,反对动静加载模块。
// 下面的例子能够这样写 const path = './' + fileName;import(path).then(module=>{}).catch(err=>{});
import
和 import()
最大的区别就是前者是动态导入,后者是动静导入,并且后者返回的是一个 Promise。
import()
函数能够用在任何中央,不仅仅是模块,非模块的脚本也能够应用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()
函数与所加载的模块没有动态连贯关系,这点也是与 import
语句不雷同。import()
相似于 Node 的 require
办法,区别次要是前者是异步加载,后者是同步加载。
import()
适宜:
- 按需加载
- 条件加载
- 动静门路加载
import()
还能够配合 解构
和asyn/await
应用。
import('./myModule.js').then(({export1, export2}) => {// ...·});async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); // 同时加载多个模块 const [module1, module2, module3] = await Promise.all([import('./module1.js'), import('./module2.js'), import('./module3.js'), ]);}main();
CommonJs vs Es6 Module
CommonJs | Es6 Module |
---|---|
反对 node 程序 | 反对 web,将来也将反对 node |
CommonJs 能够动静加载语句,代码产生在运行时 | Es Module 既能动静导入,也能动态导入 |
CommonJs 导出值是拷贝,能够批改导出的值 | ES6 Module 输入的是值的援用,并且是只读的 |
CommonJs 会缓存导入的模块 | 不会缓存值 |
写到最初
随着官网将模块化作为外围语言个性来反对,将来的模块化计划很可能会是对立应用 ES6 Module(Node13 开始反对 ES6 模块),然而因为目前绝大多数 node 程序都应用 CommonJs,将来较长一段时间可能依然是两种模块计划并行应用。
最初,码字不易,如果这篇文章对你有帮忙,还请点个赞,谢谢。
参考
局部内容参考自:
网道 -ES6 Module
《JavaScript 权威指南第七版》