你不容错过的JavaScript高级语法(模块化)

语言: CN / TW / HK

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的使用 - 下载源码:https://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文件时,需要开启本地服务,而不能直接打开运行在浏览器上。 image.png 这个在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),运行代码,计算值,并且将值填充到内存地址中。 image.png 阶段一: image.png 阶段二和三: image.png 所以,从上面可以看出在导出文件中,修改变量的值会影响到导入文件中的值。而且导入文件被限制修改导出文件的值。

es6 module 和common.js的区别