【JavaScript狂补知识第②弹】JS模块化
概述
什么是模块化?
模块化指的是按照一定的规则,将大文件拆分成多个相互依赖的小模块。
模块之间都是相互隔离独立的,模块中的数据都是私有的,可以通过导出手段把模块中的数据、函数等导出供其他模块使用,通过导入实现模块间数据、函数的共享。
为什么需要模块化?
随着编写应用的复杂性越来越高,如果不进行模块化,会逐渐出现全局污染、依赖混乱、数据安全等问题:
- 全局污染
1 | // news.js |
通过在F12控制台中调用getTodayData函数我们发现最后返回的是天气的数据,因为weather.js是后引入的,会同名覆盖,这就是所谓的全局污染问题,因为通过这种引入方式我们把对应js文件的函数、数据等都放到了window这个全局对象中去了,导致全局污染问题。
- 依赖混乱
对于通过<script type="text/javascript" src="xxx.js"></script>
这一引入方式的项目,由于可能项目本身需要很多依赖,而不同依赖可能有依赖另一个依赖,因此如果引入的顺序不同可能就导致了项目本身出现问题,即所谓的依赖混乱。
- 数据安全
对于通过<script type="text/javascript" src="xxx.js"></script>
这一引入方式的项目,会把对应js所有数据都放在window对象中,因此可能会泄露一些敏感的用户信息,导致出现数据安全问题。
常见的模块化规范
- CommonJS —— 服务端应用广泛
- AMD
- CMD
- ES6 —— 浏览器端应用广泛
CommonJS
导出
在CommonJS规范中,在一个js文件导出数据、方法有两种方式:
- module.exports = { }
- exports.xxx = xxx
1 | const username = '小张' |
对于上述两种方式,打印导出的内容均为{ username: '小张', getAge: [Function: getAge] }
。
exports和module.exports的区别:
- 谨记一条原则:无论使用 exports 导出成员,或是 module.exports 导出成员,最终导出的结果,都是以 module.exports 所指向的对象为准。
- exports是对module.exports的一个引用变量,两者在最初均指向同一个空对象,exports的写法是为了方便给导出对象添加属性,不能使用exports = value的方式导出数据和方法,因为这样直接改变了exports这个变量的指向,module.exports的内容并没有改变。
1
2
3
4
5
6
7
8
9
10 const username = '小张'
const token = '<<token>>'
const age = 18
const getAge = () => {
return age
}
exports = { a: 1 }
// 最后导出的对象为空
导入
通过require
可以导入对应文件的module.exports对象,可以直接利用一个变量接收,也可以对导入结果进行解构。
1 | // 导入方式一 |
扩展理解
在CommonJS规范中,每个模块在执行时是把js文件所写的内容包裹在一个内置函数中执行的,每个模块都有自己的作用域,即所谓的数据隔离,可以通过console.log(arguments.callee.toString())
来进行验证:
1 | function (exports, require, module, __filename, __dirname) { |
CommonJS是适合在服务端的模块化,如果想在浏览器端使用,需要使用Browserify这一工具来预编译使用CommonJS模块化规范编写的JS文件,最后即可将预编译完的JS文件引入html中使用。
ES6
ES6是支持在浏览器端和服务端的模块化规范,默认是支持浏览器端,如果想支持服务端有两种方式:
将js文件后缀改为mjs即可
在项目根目录下添加package.json文件,内容如下:
1
2
3{
"type": "module"
}
导出
分别导出:在需要导出的数据或者方法前添加export关键字
1
2
3
4
5
6
7export const username = '小张'
const age = 18
export const motto = '相信明天会更好'
export function getAge() {
return age
}统一导出:
1
2
3
4
5
6
7
8
9
10
11const username = '小张'
const age = 18
const motto = '相信明天会更好'
function getAge() {
return age
}
export {
username, motto, getAge
}默认导出:
1
2
3
4
5
6
7
8
9
10
11const username = '小张'
const age = 18
const motto = '相信明天会更好'
function getAge() {
return age
}
export default{
username, motto, getAge
}
对于分别导出、统一导出,最后导出的每一个数据/方法都会作为module的一个属性,而默认导出会使得module多了一个default对象,default对象的内容就是导出的内容,如下图所示:
注意:不同的导出方式可以混合使用
导入
全部导入:
1
import * as user from './user.js'
命名导入:通过导入和导出一样的命名这种方式来导入,如果想重命名则需要使用as关键字(针对分别导入、统一导入)
1
2
3
4import { username as name, getAge } from './user.js'
console.log(name)
console.log(getAge())默认导入:可以用自定义变量名接受默认导出的default对象(针对默认导出)
1
2
3
4import user from './user.js'
console.log(user)
// { username: '小张', motto: '相信明天会更好', getAge: [Function: getAge] }动态导入:导入的结果和全部导入一致,具体的使用可以参考下述例子
1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="./index.js"></script>
<button id="btn">导入模块</button>
</body>
</html>1
2
3
4
5
6
7// index.js
const btn = document.getElementById('btn')
btn.onclick = async () => {
const user = await import('./user.js') // import是异步
console.log(user)
}无绑定导入:如果只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入,即import可以不接受任何数据,直接导入一个文件
1
import './log.js'
CJS和ES6区别
加载模块方式:CommonJS模块是同步加载的,这意味着在加载模块时,会阻塞代码的执行,直到模块加载完成;ES6模块是异步加载的,这意味着在加载模块时,不会阻塞代码的执行。
数据引用:CommonJS导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,不会同步到外部;ES6是导出数据的引用,内部修改可以同步到外部。
依赖关系处理:CommonJS对模块依赖的解决是“动态的”而ES6是“静态的”。在这里“动态的”含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。
ES5和ES6的一些区别可以参考:ES5与ES6语法的区别及优缺点分析_es5和es6语法差异-CSDN博客
最后
对于AMD和CMD两种方式现在貌似用的比较少了,这里就不过多叙述(狗头.jpg