为什么要模块化?
原始写法
// 文件加载顺序
<script src='a.js'></script>
<script src='b.js'></script>
<script src='m.js'></script>
// a.js
var name = '小明'
var flag = true
if(flag) {
console.log('我是小明,哈哈')
}
function sum(num1,num2) {
return num1 + num2
}
sum(10,20) // 30
name // 小明
// b.js
var name = '小红'
var flag = false
name // 小红
// m.js
if(flag) {
console.log('我会输出吗?')
}
此时m.js中的flag被b.js文件修改为false,所以m.js中不会正常输出。常规模式的 JavaScript开发,全局变量命名会发生冲突,而且变量依赖文件顺序。
历史上的模块化方案
对象写法
变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它们没有妥善的将内部私有的函数或变量隐藏起来,就很容易引发冲突。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间。所有需要暴露给外界的功能都会成为这个对象的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
var MyReallyCoolLibrary = {
awesome:'stuff',
doSomething:function() {
...
},
doAnotherThing:function() {
...
}
}
问题:这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接修改内部计数器的值。
MyReallyCoolLibrary.count = 2;
闭包写法
使用立即执行函数表达式,可以达到不暴露私有成员的目的。(function foo() {...})()
是一个函数表达式,意味着 foo
只能在 ...
所代表的位置中被访问,外部作用域则不行。foo
变量名被隐藏在自身中,意味着不会非必要的污染外部作用域。
// a.js
(function foo() { // foo也可以省略直接作为匿名函数表达式
var name = 'lxx'
var flag = true
if(flag) {
console.log('I am lxx')
}
function sum(num1, num2) {
return num1 + num2
}
})()
// b.js
(function(){
var name = '小红'
var flag = false
})()
// m.js
if(flag) {
console.log('我会输出吗?')
}
问题:虽然解决了全局变量命名冲突的问题,但是 m.js 中无法再使用变量 flag
,因为被封闭在函数作用域中。
原始模块化解决方案
让立即执行函数返回一个对象,函数内部的属性和方法作为这个对象的属性和方法。
// a.js
var ModuleA = (function (){
var obj = {}
obj.name = 'lxx'
obj.flag = true
if(obj.flag) {
console.log('I am lxx')
}
obj.sum = function sum(num1,num2) {
return num1 + num2
}
return obj
})()
// b.js
var ModuleB = (function(){
var obj = {}
obj.name = '小红'
obj.flag = false
return obj
})()
// m.js
if(ModuleA.flag) {
console.log('我会输出吗?')
}
解决了全局变量命名冲突问题,只需要保证输出的 ModuleA 和 ModuleB 不会命名冲突即可。这就是模块最基础的封装。
请结合阅读《你不知道的JavaScript 上卷 第五章 模块》
CommonJS
node中每一个js文件就是一个模块。node 中实现 commonjs 模块化的本质是对象的引用赋值。
exports
相当于模块中的全局对象,在什么也不导出时,exports
是一个空对象。
想把谁导出就把谁作为 exports
的属性,exports.name = name
把 name
导出,将 name
赋值给 exports
的 name
属性。
// bar.js
const name = 'lxx'
const age = 25
function sayHello(name) {
console.log('hello,' + name)
}
// 在导出之前,输出 exports 结果是 {}
console.log(exports)
exports.name = name
exports.age = age
exports.sayHello = sayHello
// 在导出之后,输出 exports 结果是 { name:'lxx', age:25, sayHello:[Function:sayHello]}
console.log(exports)
require()
是模块中的一个函数,这个函数会拿到导出的 exports
对象,并将其作为 require
函数的返回值。
// main.js
const bar = require('./bar') // require 函数有一个返回值,即bar=exports
console.log(bar) // { name:'lxx', age:25 }
bar.sayHello('lxx') // hello,lxx
因为 bar
是一个对象,所以可以使用对象解构赋值。
const { name,age,sayHello } = require('./bar')
name // lxx
age // 25
sayHello('lxx') // hello,lxx
require(x) 查找规则
- x 是一个 Node 核心模块,直接返回核心模块并停止查找。
- x 是一个路径,以
./
或../
或/
开头。- 第一步,将 x 当做一个文件在对应目录下查找
- 有后缀,按照后缀名查找对应文件
- 没有后缀,按以下顺序查找:x.js x.json x.node
- 第二步,没有找到文件,将 x 作为目录,按如下顺序查找:x/index.js x/index.json x/index.node
- 第一步,将 x 当做一个文件在对应目录下查找
- x 不是路径,也不是核心模块,从逐层目录的 node_modules 中查找。
- 都没有找到报错
not found
module.exports 和 exports 的关系
CommonJS 规范中是没有 module.exports
概念的,但是为了实现模块的导出,Node 中使用的是 Module 类。每一个模块都是 Module 的一个实例,即 module
。在 Node 中真正用于导出的不是 exports
而是 module.exports
,module
才是导出的真正实现者。
- 为什么
exports
也可以导出呢?exports
是module.exports
的引用,也就是说bar = exports = module.exports = {}
内存地址是一样的,目的都是引用同一个对象,Node 源码中就是这样的赋值顺序。 - 既然有
module.exports
那么exports
存在的意义是什么? CommonJS 的规范中要求有一个exports
作为导出。其实是 NodeJS 为了满足 CommonJS 规范做了一个妥协,增加了一个exports
,其实是不需要的。
CommonJS 规范的缺点
CommonJS 加载模块是同步的。意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行。这个在服务器不会有什么问题,因为服务器加载的文件都是本地文件,加载速度非常快。
require('./bar')
console.log('后面的代码')
// 结果:先执行 require 进来的内容,然后再执行后面 main 中的代码
浏览器加载 js 文件需要先从服务器将文件下载下来,之后再加载运行。采用同步的方式意味着后续的 js 代码都无法正常运行,即使是一些简单的 DOM 操作。所以在浏览器中通常不使用 CommonJS 规范。
当然在 webpack 中使用 CommonJS 是另一回事,因为它会将代码转成浏览器可以直接执行的代码。
ES Module
export
export
有两种模块导出方式:命名式导出和默认导出。命名式导出每个模块可以有多个,默认导出每个模块仅一个。
命名式导出
因为导出用名称进行区分,称之为命名式导出。
export { myFunction } // 导出一个已定义的函数
export const foo = Math.sqrt(2) // 导出一个常量
模块导出时,可以使用 as
关键字对导出成重命名
var name = 'itbilu'
var domain = 'http://itbilu.com'
export { name as siteName, domain }
💣 注意下面的语法有严重错误的情况:
// 错误演示
export 1 // 绝对不可以
var a = 100
export a
export
在导出接口的时候,必须与模块内部的变量具有一一对应的关系。
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1
,第二种写法通过变量 m
,还是直接输出 1
。1
只是一个值,不是接口。
默认导出
默认导出也被称为定义式导出。命名式导出可以导出多个值,但在 import
引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其他类型的值,这样在模块 import
导入时也会很容易引用。
const D = 123
export default D
export 的写法
-
直接导出
export let name = 'lxx' export let age = 18
-
大括号语法
let name = 'lxx' let age = 18 export { name, age }
💣
export {...}
中的{}
在这里不是对象,因为尝试export { name:name }
会报错!{}
放置的是要导出的变量的绑定列表。 -
导出函数或类
export function Mul(num1, num2) { return num1 + num2 } export class Person { run() {} } ```
-
export default 在某些情况下,一个模块中包含某个功能,我们并不希望给这个功能命名,而是让导入者自己命名,此时就可以使用
export default
。export default function (arg) { console.log(arg) } import argument from './a/js' argument(10)
import
import 导入方式
-
命名式导入
import { myMember } from 'my-module' impost { foo, bar } from 'my-module'
-
默认导入 在模块导出时,可能会存在默认导出。同样的,在导入时可以使用
import
指令导入这些默认值。import myDefault from 'my-module'
-
命名空间导入 相当于把所有导出的变量或方法放入一个对象中,通过点语法获取。
import { flag, sum, name, age, Mul, Person } from './a.js' import * as a from './a.js' a.flag a.sum(1,2)
💣
import{}
{} 在这里也不是对象 -
import 和 export 结合使用
// format.js export function price(num) { console.log('$'+num) } export function time() { console.log('2022-01-14') }
在 index.js 中导入导出所有内容,作为统一出口。
// index.js export { price, time } from './format.js'
使用时,直接加载 index.js 就好了。
// main.js import { price, time } from './index'
import 错误用法
import
不能放在任何代码块中,因为 import
需要在 js 代码于解析阶段就确定依赖关系。把解析阶段的代码放在运行阶段执行肯定会报错。
let flag = true
// 此处代码处于运行阶段
if(flag) {
import { price, time } from './index'
}
import()
为解决上面的需求,可以使用 import()
,它是一个异步执行的函数,返回一个 Promise。vue router 的懒加载就运用了 import()
。
let flag = true
if(flag) {
import('./modules.foo.js').then((res) => {
// 拿到导出的内容
console.log(res.name)
console.log(res.age)
}).catch(err => {
...
})
}
ES Module 加载过程
ES Module 通过 export
导出的是变量本身的引用。
export 在导出变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment record)
模块环境记录和变量进行绑定,并且绑定是实时的。
如果导出的模块中修改了变量,那么导入的地方可以实时获取最新的变量。
💣 在导入的地方不可以修改变量,因为它只是被绑定到了这个变量上,其实是一个常量。
浏览器中使用
在浏览器中使用模块,,这时文件才会被当做一个模块。且 ES Module 是异步的,不会堵塞后续代码执行。
<script src="index.js" type="module"></script>
<script src="normal.js"></script> // 不会堵塞normal.js的执行