Skip to content

Latest commit

 

History

History
428 lines (290 loc) · 12.1 KB

13.module.md

File metadata and controls

428 lines (290 loc) · 12.1 KB

module

背景

ES 里所有的文件都会共享一个作用域 -- 全局作用域,而不像其他语言会用一些如 package 来限制代码的作用域。这不仅使其他语言转来的人很困惑,也很容易滋生 bugES5 里,普遍的做法是把代码放在 IIFE 里,利用函数作用域来封装代码。ES6 为了解决这个问题,推出了模块 ( module ) 的概念。

module 是什么

module 是处于严格模式下的 js 代码。其变量只存在于 module 的作用域内,不会被添加到全局作用域里。module 只能通过 export 方式对外提供数据, 它也可以通过 import 方式从其他 module 引入数据。

module 里的 thisundefined; 里面也不允许 HTML 风格的注释。

script 标签包裹的代码不是 module

下面会先介绍 module 是什么,然后介绍怎么用script 标签创造 module

初步了解 export

上面说过 module 里的数据要提供给外部使用,必须用 export 出去。而被 export 可以是

  • 任何变量
  • 函数
  • class 声明

如下所示:

// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// export function
export function sum(num1, num2) {
  return num1 + num1;
}
// export class
export class Rectangle {
  constructor(length, width) {
    this.length = length;
    this.width = width;
  }
}
// this function is private to the module
function subtract(num1, num2) {
  return num1 - num2;
}
// define a function...
function multiply(num1, num2) {
  return num1 * num2;
}

// ...and then export it later
export multiply;

需要注意的是,除了用 export default , 其他所有的都必须是有名的,因为导出的数据都是用这个名字来获取的。

除了可以导出一条声明之外,如:

export let name = "Nicholas";

还可以导出一个引用:

export multiply;

初步了解 import

有了定义好的模块,我么可以在另一个这样使用它:

import { identifier1, identifier2 } from "./example.js";

from 关键字后面跟模块路径, {} 里的是需要被引入模块里的变量的标识符,也就是上一节说到的 module 里变量或引用的名字。

需要注意的是 import 进来的变量,就相当于用 const 声明的变量,意味着他们不能被二次声明,其值也不能被修改。

导入整个模块

有时需要导入模块里所有有的对象,可以如下操作:

// import everything
import * as example from "./example.js";
console.log(
  example.sum(
    1,
    example.magicNumber
  )
);          // 8
console.log(example.multiply(1, 2));    // 2

也许有人会问,被全部导出的模块,其默认值怎么获取? 也比较容易,moduleIdentifier.default 就可以,如:

// module1
const name = 'jeyvie'
export const hobby = 'coding'
export default name;

// module2
import * as m1 from './module1.js'
console.log(m1.default); // jeyvie
console.log(m1.hobby); // coding

注意点:

  1. moudle 为单例,里面的代码只会被执行一次。导入 moudle后,它就会存在内存里,下次导入的时候直接从内存里复用它。
  2. importexport 只能放在函数和其他声明外面。

导入的 bindings 怪事

上文有讲,导入的 bindings (可理解为变量) 相当于在当前模块里声明的常量,不可被修改。但是通过导入的方法可以:

// example.js
export var name = "Nicholas";
export function setName(newName) {
    name = newName;
}

// moudle A
import { name, setName } from "./example.js";
console.log(name);  // "Nicholas"
setName("Greg");
console.log(name);  // "Greg"
name = "Nicholas";  // throws an error

这是因为 moudle A 里的 nameexample.js 里的 name 不是一个东西。前者是后者在 moudle A 里的一个名字(可以理解为引用吧)。所以 example.js 里的 name 的值变了,moudle A 里的 name 值也会跟着变。

重命名

  1. 可以修改导出数据的名字

    function sum(num1, num2) {
      return num1 + num2;
    }
    export { sum as add };
    

    这样修改之后,其他模块用这用 add 来导入 sum 方法了

  2. 也可以修改导入数据的名字

    import { add as sum } from "./example.js";
    console.log(typeof add);    // "undefined"
    console.log(sum(1, 2));     // 3
    

默认值的导入导出

  1. 导出默认值

    可以在 default 后直接跟声明语句

    export default function (num1, num2) {
      return num1 + num2;
    }
    

    注意,可以是函数声明,类声明,{}。但不能是变量声明。如用 let, const , var 等声明变量。

    default 后也可以使用标识符

    function sum(num1, num2) {
      return num1 + num2;
    }
    export default sum;
    
  2. 导入默认值

    1. 可以直接导入, 命名随意,只要不与当前模块里其他变量名冲突就可以:

      // import the default
      import sum from "./example.js";
      console.log(sum(1, 2));     // 3
      
    2. 也可以同时导入默认值和其他值:

      // example.js
      export let color = "red";
      export default function (num1, num2) {
        return num1 + num2;
      }
      
      // moudle
      import sum, { color } from "./example.js";
      console.log(sum(1, 2));     // 3
      console.log(color);         // "red"
      

      这种语法里需要注意,默认值必须在其他值前面

    3. default 还可以这样引入,放在 {} 里,然后重命名,如:

      import { default as sum, color } from "./example.js";
      console.log(sum(1, 2));     // 3
      console.log(color);         // "red"
      

重新导出

  1. 我们可以把导入的东西直接导出去:

    export { sum } from "./example.js";
    

    等同于:

    import { sum } from "./example.js";
    export { sum }
    
  2. 我们还可以给它重命名

    export { sum as add } from "./example.js";
    
  3. 可以把导入模块内容都导出去

    export * from "./example.js";
    

    但这样有个问题需要注意: 导出 ./example.js 里所有的东西,那也就包括了他的 default, 这会影响当前模块的 default 输出,因此,你就没法输出一个新的 default 了。

引入没有 export 的模块

模块里的变量虽然不会被添加到全局变量里,但能访问全局变量。 所以一些模块,可以用来做一些拓展全局方法之类的事,而不导出东西。这样的 moudle, 在 polyfill 里常用到。下面举个例子:

// example.js
// module code without exports or imports
Array.prototype.pushAll = function (items) {
  // items must be an array
  if (!Array.isArray(items)) {
    throw new TypeError("Argument must be an array.");
  }
  // use built-in push() and spread operator
  return this.push(...items);
};

其他模块可以直接导入,其 Array 实例都有 pushAll 方法:

import "./example.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);

加载模块

ES6 定义了模块的语法,但没有定义该如何加载模块。相比教于为所有的 js 环境订立一个加载的规范,ES6 只确定了语法,并把加载算法抽象为一个未定义的内部操作 -- HostResolveImportedModule。浏览器 和 Node.js 开发者自己决定如何根据各自的环境实现 HostResolveImportedModule

web 浏览器中模块的用法

ES6 之前,js 有三种加载 js 的方法

  1. script src 加载
  2. script 包裹 js 代码
  3. worker 里执行加载的代码

为了支持 Es6 模块,需要升级这三种方法。

结合 <script> 使用模块

<script>type 属性不存在或包含 js 内容类型如 text/javascript 时,它里面的或用src引入的内容会被解析成普通的 js 代码,而不是 js 模块。

当将它的 type 属性设置为 module时,它就会将它的内容解析成模块。如:

<!-- load a module JavaScript file -->
<script type="module" src="module.js"></script>
<!-- include a module inline -->
<script type="module">
	import { sum } from "./example.js";
	let result = sum(1, 2);
</script>

script 加载模块是,之所以将 type 设置为 module, 是因为 一, 根据内容区分 module 和 一般 js 比较苦难 二,浏览器遇到不识别的 type 类型是,会忽略掉它。这样,就可以做到向后兼容了

浏览器里模块的加载顺序

模块和其他 js 不一样的地方在于,他们会用 import 来表示必须加载的其他文件,然后代码才能正确执行。为了支持这个特定,<script type="module"> 的表现就像是加了 defer 属性。

虽然对其他 js 文件而言, defer 是可选的,但加载模块时总会用到它。所以, 当 HTML 解析器遇到有 src<script type="module"> ,它会马上加载文件但直到 html 解析完,才会以它们在html里出现的顺序执行。所以说,第一个 <script type="module"> 总会比第二个先执行,即使其中一个是内联而不是用src加载的代码。

<!-- this will execute first -->
<script type="module" src="module1.js"></script>
<!-- this will execute second -->
<script type="module">
import { sum } from "./example.js";
let result = sum(1, 2);
</script>
<!-- this will execute third -->
<script type="module" src="module2.js"></script>

每个模块,都会先处理 import 内容,再执行其他代码。

先递归加载,然后递归执行。

浏览器中模块的异步加载

script 加了 async 后,会使得 js 文件被加载后就马上执行。文档里中的顺序,不会影响他们的执行顺序。这对于 module 而言也是一样的。只是 module 里会保证模块的依赖加载完毕后再执行模块里的代码:

<!-- no guarantee which one of these will execute first -->
<script type="module" async src="module1.js"></script>
<script type="module" async src="module2.js"></script>

这个例子中,两个模块的异步加载,谁先加载完,谁先执行。

worker 里用 module

worker 可以在页面环境之外,执行加载的文件:

// load script.js as a script
let worker = new Worker("script.js");

可以传入第二个参数,来表示这是模块

// load module.js as a module
let worker = new Worker("module.js", { type: "module" });

worker sciptsworker modules 的区别

  1. 前者只能加载同一个域的文件,后者也可以加载 CORS
  2. 前者可以用 self.importScripts() 加载文件,但这在后者里不管用,因为module要用 import

浏览器模块路径解析规则

  1. / 开头的指向根目录
  2. ./ 开头的指向当前目录
  3. ../ 指向父级目录
  4. URL 格式

比如当前文件路径为 https://www .example.com/modules/module.js:

// imports from https://www.example.com/modules/example1.js
import { first } from "./example1.js";
// imports from https://www.example.com/example2.js
import { second } from "../example2.js";
// imports from https://www.example.com/example3.js
import { third } from "/example3.js";
// imports from https://www2.example.com/example4.js
import { fourth } from "https://www2.example.com/example4.js";

以下是无效的:

// invalid - doesn't begin with /, ./, or ../
import { first } from "example.js";
// invalid - doesn't begin with /, ./, or ../
import { second } from "example/index.js";

总结

ES6 引入 module 来封装代码,它跟一般 js 的区别在于它的变量不会被添加到全局作用域里。module 里的 thisundefined

module 需要对外暴露的数据要要export, 需要引入的模块要用import

加载 js 有三种方法,所以对应的加载 module的方法也有三种。

模块默认会按他们在文档中的顺序执行,如果添加了 async 的话,那哪个模块先加载完,哪个模块就先执行。

不管哪种,模块内部执行时,都会先保证 import 完毕。