你不容错过的JavaScript高级语法(模块化)
theme: qklhk-chocolate highlight: a11y-dark
众所周知,js在前端开发中的地位。学好它,真的很重要。
下面这篇文章,介绍一下模块化。
什么是模块化?
到底什么是模块化、模块化开发呢? - 事实上模块化开发最终的目的是将程序划分成一个个小的结构。 - 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构。 - 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用。 - 也可以通过某种方式,导入另外结构中的变量、函数、对象等。 上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程。
模块化的历史
在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的: - 这个时候我们只需要讲JavaScript代码写到
js
//main.js
require.config({
baseUrl: '', // 默认是main.js的文件夹路径
paths: {
foo: "./foo"
}
})
require(["foo"], function(foo) {
console.log("main:", foo)
})
js
// foo.js
define(function() {
const name = "zh"
const age = 22
function sum(num1, num2) {
return num1 + num2
}
return {
name,
age,
sum
}
})
```
CMD规范
CMD规范也是应用于浏览器的一种模块化规范:
- CMD 是Common Module Definition(通用模块定义)的缩写。它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来。
- AMD实现的比较常用的库是SeaJS。
SeaJS的使用
- 下载源码:http://github.com/seajs/seajs。 找到dist文件夹下的sea.js。
- 引入sea.js和使用主入口文件。
html
// index.html
<script src="./sea.js"></script>
<script>
seajs.use("./main.js")
</script>
js
//main.js
define(function(require, exports, module) {
const foo = require("./foo")
console.log("main:", foo)
})
```js
// foo.js
define(function(require, exports, module) {
const name = "zh"
const age = 22
function sum(num1, num2) {
return num1 + num2
}
// exports.name = name
// exports.age = age
module.exports = {
name,
age,
sum
}
});
```
ES Module
ES Module和CommonJS的模块化有一些不同之处: - 一方面它使用了import和export关键字来实现模块化。 - 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式。 - export负责将模块内的内容导出。 - import负责从其他模块导入内容。 - 采用ES Module将自动采用严格模式:use strict。
基本使用
html
// index.html
<script src="./main.js" type="module"></script>
```js
// foo.js
let obj = {
name: "zh",
age: 22
}
export default sum
js
// main.js
import foo from './foo.js'
console.log(foo)
```
- 在html文件加载入口文件的时候,需要指定type为module。
- 在打开html文件时,需要开启本地服务,而不能直接打开运行在浏览器上。
这个在MDN上面有给出解释:
- 你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。
- 你需要通过一个服务器来测试。
exports关键字
export关键字将一个模块中的变量、函数、类等导出。
我们希望将其他中内容全部导出,它可以有如下的方式:
- 方式一:在语句声明的前面直接加上export关键字。
js
export const name = "zh"
export const age = 22
- 方式二:将所有需要导出的标识符,放到export后面的 {}
中。注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的。所以: export {name: name},是错误的写法。
```js
const name = "zh"
const age = 22
function foo() {
console.log("foo function")
}
export {
name,
age,
foo
}
- 方式三:导出时给标识符起一个别名。(基本没用,一般在导入文件中起别名)。然后在导入文件中就只能使用别名来获取。
js
export {
name as fName,
age as fAge,
foo as fFoo
}
```
import关键字
import关键字负责从另外一个模块中导入内容。
导入内容的方式也有多种:
- 方式一:import {标识符列表} from '模块'。注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容。
js
import { name, age } from "./foo.js"
- 方式二:导入时给标识符起别名。
js
import { name as fName, age as fAge } from './foo.js'
- 方式三:通过 *
将模块功能放到一个模块功能对象(a module object)上。然后通过起别名来使用。
js
import * as foo from './foo.js'
export和import结合使用
表示导入导出。 ```js import { add, sub } from './math.js' import {otherProperty} from './other.js'
export {
add,
sub,
otherProperty
}
等价于
js
// 导入的所有文件会统一被导出
export { add, sub } from './math.js'
export {otherProperty} from './other.js'
```
等价于
js
export * from './math.js'
export * from './other.js'
为什么要这样做呢?
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。 这样方便指定统一的接口规范,也方便阅读。这个时候,我们就可以使用export和import结合使用。
default用法
前面我们学习的导出功能都是有名字的导出(named exports): - 在导出export时指定了名字。 - 在导入import时需要知道具体的名字。
还有一种导出叫做默认导出(default export) ```js // foo.js const name = "zh" cconst age = 22 export { name, // 或者这样的默认导出 // age as default }
export default age
js
// 导入语句: 导入的默认的导出
import foo, {name} from './foo.js'
console.log(foo, name) // 22 zh
``` - 默认导出export时可以不需要指定名字。 - 在导入时不需要使用 {},并且可以自己来指定名字。 - 它也方便我们和现有的CommonJS等规范相互操作。 注意:在一个模块中,只能有一个默认导出(default export)。
import函数
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
js
if(true) {
import foo from './foo.js'
}
为什么会出现这个情况呢?
- 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系。
- 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况。
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
- 如果根据不同的条件,动态来选择加载模块的路径。
这个时候我们需要使用 import() 函数来动态加载。import函数返回的结果是一个Promise。
js
import("./foo.js").then(res => {
console.log("res:", res.name)
})
es11新增了一个属性。meta属性本身也是一个对象: { url: "当前模块所在的路径" }
js
console.log(import.meta)
ES Module的解析流程
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
ES Module的解析过程可以划分为三个阶段: - 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)。 - 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。 - 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中。 阶段一: 阶段二和三: 所以,从上面可以看出在导出文件中,修改变量的值会影响到导入文件中的值。而且导入文件被限制修改导出文件的值。