JavaScript < ES5、ES6、ES7、… >ECMAScript 6

ES6(十六):ES6中的模块

2019-02-07  本文已影响82人  CodeMT

前面的话


  JS用"共享一切"的方法加载代码,这是该语言中最易出错且容易令人感到困惑的地方。在ES6以前,在应用程序的每一个JS中定义的一切都共享一个全局作用域。随着web应用程序变得更加复杂,JS代码的使用量也开始增长,这一做法会引起问题,如命名冲突和安全问题。ES6的一个目标是解决作用域问题,也为了使JS应用程序显得有序,于是引进了模块。本文将详细介绍ES6中的模块

概述

模块是自动运行在严格模式下并且没有办法退出运行的JS代码。与共享一切架构相反的是,在模块顶部创建的变量不会自动被添加到全局共享作用域,这个变量仅在模块的顶级作用域中存在,而且模块必须导出一些外部代码可以访问的元素,如变量或函数。模块也可以从其他模块导入绑定

另外两个模块的特性与作用域关系不大,但也很重要。首先,在模块的顶部,this的值是undefined;其次,模块不支持HTML风格的代码注释,这是从早期浏览器残余下来的JS特性

脚本,也就是任何不是模块的JS代码,则缺少这些特性。模块和其他JS代码之间的差异可能乍一看不起眼,但是它们代表了JS代码加载和求值的一个重要变化。模块真正的魔力所在是仅导出和导入需要的绑定,而不是将所用东西都放到一个文件。只有很好地理解了导出和导入才能理解模块与脚本的区别

导出

可以用export关键字将一部分己发布的代码暴露给其他模块,在最简单的用例中,可以将export放在任何变量、函数或类声明的前面,以将它们从模块导出

// 导出数据
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;// 导出函数
export function sum(num1, num2) {
  return num1 + num1;
}
// 导出类
export class Rectangle {
  constructor(length, width) {        
    this.length = length;this.width = width;
  }
}
// 此函数为模块私有
function subtract(num1, num2) {
  return num1 - num2;
}
// 定义一个函数……
function multiply(num1, num2) {
  return num1 * num2;
}
// ……稍后将其导出
export { multiply };

另外,在定义multiply()函数时没有马上导出它。由于不必总是导出声明,可以导出引用,因此这段代码可以运行。此外,这个示例并未导出subtract()函数,任何未显式导出的变量、函数或类都是模块私有的,无法从模块外部访问

导入

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

【导入单个绑定】

假设前面的示例在一个名为"example.js"的模块中,我们可以导入并以多种方式使用这个模块中的绑定

// 单个导入
import { sum } from "./example.js";
console.log(sum(1, 2)); // 3
sum = 1; // 出错

【导入多个绑定】

如果想从示例模块导入多个绑定,则可以明确地将它们列出如下

// 多个导入
import {sum, multiply, magicNumber} from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2

【导入整个模块】

特殊情况下,可以导入整个模块作为一个单一的对象。然后所有的导出都可以作为对象的属性使用

// 完全导入
import * as example from "./example.js";
console.log(example.sum(1,example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2
import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

【导入绑定的一个微妙怪异之处】

ES6import语句为变量、函数和类创建的是只读绑定,而不是像正常变量一样简单地引用原始绑定。标识符只有在被导出的模块中可以修改,即便是导入绑定的模块也无法更改绑定的值

export var name = "huochai";
export function setName(newName) {
  name = newName;
}
import { name, setName } from "./example.js";
console.log(name); // "huochai"
setName("match");
console.log(name); // "match"
name = "huochai"; // error

重命名

有时候,从一个模块导入变量、函数或者类时,可能不希望使用它们的原始名称。幸运的是,可以在导出过程和导入过程中改变导出元素的名称

假设要使用不同的名称导出一个函数,则可以用as关键字来指定函数在模块外的名称

function sum(num1, num2) {
  return num1 + num2;
}
export { sum as add };
import { add } from "./example.js";
import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3

默认值

由于在诸如CommonJS的其他模块系统中,从模块中导出和导入默认值是一个常见的做法,该语法被进行了优化。模块的默认值指的是通过default关键字指定的单个变量、函数或类,只能为每个模块设置一个默认的导出值,导出时多次使用default关键字是一个语法错误

【导出默认值】

下面是一个使用default关键字的简单示例

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

【导入默认值】

可以使用以下语法从一个模块导入一个默认值

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

[注意]import语句中,默认值必须排在非默认值之前

// 等价于上个例子
import { default as sum, color } from "example";
console.log(sum(1, 2)); // 3
console.log(color); // "red"

静态加载

ES6中的模块与node.js中的模块加载不同,nodeJS中的require语句是运行时加载,而ES6中的import是静态加载,所以有一些语法限制

1、不能使用表达式和变量等这些只有在运行时才能得到结果的语法结构

// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;

2、importexport命令只能在模块的顶层,不能在代码块之中,如不能在if语句和函数内使用

if (flag) {
  export flag; // 语法错误
}
// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
function tryImport() {
  import flag from "./example.js"; // 语法错误
}
const path = './' + fileName;
const myModual = require(path);

重新导出

可能需要重新导出模块已经导入的内容

import { sum } from "./example.js";
export { sum }
export { sum } from "./example.js";
export { sum as add } from "./example.js";
export * from "./example.js";

无绑定导入

某些模块可能不导出任何东西,相反,它们可能只修改全局作用域中的对象。尽管模块中的顶层变量、函数和类不会自动地出现在全局作用域中,但这并不意味着模块无法访问全局作用域。内建对象(如ArrayObject)的共享定义可以在模块中访问,对这些对象所做的更改将反映在其他模块中

例如,要向所有数组添加pushAll()方法,则可以定义如下所示的模块

// 没有导出与导入的模块
Array.prototype.pushAll = function(items) {    
  // items 必须是一个数组
if (!Array.isArray(items)) {        
  throw new TypeError("Argument must be an array.");
}    
// 使用内置的 push() 与扩展运算符
  return this.push(...items);
};
import "./example.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);

[注意]无绑定导入最有可能被应用于创建polyfillShim

加载模块

虽然ES6定义了模块的语法,但它并没有定义如何加载这些模块。这正是规范复杂性的一个体现,应由不同的实现环境来决定。ES6没有尝试为所有JS环境创建一套统一的标准,它只规定了语法,并将加载机制抽象到一个未定义的内部方法HostResolveImportedModule中。Web浏览器和Node.js开发者可以通过对各自环境的认知来决定如何实现HostResolveImportedModule

【在Web浏览器中使用模块】

即使在ES6出现以前,Web浏览器也有多种方式可以将JS包含在Web应用程序中,这些脚本加载的方法分别是

1、在<script>元素中通过src属性指定一个加载代码的地址来加载JS代码文件

2、将JS代码内嵌到没有src属性的<script>元素中

3、通过Web WorkerService Worker的方法加载并执行JS代码文件

为了完全支持模块功能,Web浏览器必须更新这些机制

在<script>中使用模块

<script>元素的默认行为是将JS文件作为脚本加载,而非作为模块加载,当type属性缺失或包含一个JS内容类型(如"text/javascript")时就会发生这种情况。<script>元素可以执行内联代码或加载src中指定的文件,当type属性的值为"module"时支持加载模块。将type设置为"module"可以让浏览器将所有内联代码或包含在src指定的文件中的代码按照模块而非脚本的方式加载

<!-- 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>

Web浏览器中的模块加载顺序

模块与脚本不同,它是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些文件必须被加载进该模块才能正确执行。为了支持该功能,<script type="module">执行时自动应用defer属性

加载脚本文件时,defer是可选属性加载模块时,它就是必需属性。一旦HTML解析器遇到具有src属性的<script type="module">,模块文件便开始下载,直到文档被完全解析模块才会执行。模块按照它们出现在HTML文件中的顺序执行,也就是说,无论模块中包含的是内联代码还是指定src属性,第一个<scpipt type="module">总是在第二个之前执行

<!-- 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>

1、下载并解析module1.js

2、递归下载并解析module1.js中导入的资源

3、解析内联模块

4、递归下载并解析内联模块中导入的资源

5、下载并解析module2.js

6、递归下载并解析module2.js中导入的资源

1、递归执行module1.js中导入的资源

2、执行module1.js

3、递归执行内联模块中导入的资源

4、执行内联模块

5、递归执行module2.js中导入的资源

6、执行module2.js

[注意] <script type="module">元素会忽略defer属性,因为它执行时defer属性默认是存在的

Web浏览器中的异步模块加载

<script>元素上的async属性应用于脚本时,脚本文件将在文件完全下载并解析后执行。但是,文档中async脚本的顺序不会影响脚本执行的顺序,脚本在下载完成后立即执行,而不必等待包含的文档完成解析

async属性也可以应用在模块上,在<script type="module">元素上应用async属性会让模块以类似于脚本的方式执行,唯一的区别是,在模块执行前,模块中所有的导入资源都必须下载下来。这可以确保只有当模块执行所需的所有资源都下载完成后才执行模块,但不能保证的是模块的执行时机

<!-- 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>

将模块作为Woker加载

Worker,例如Web WorkerService Woker,可以在网页上下文之外执行JS代码。创建新Worker的步骤包括创建一个新的Worker实例(或其他的类),传入JS文件的地址。默认的加载机制是按照脚本的方式加载文件

// 用脚本方式加载 script.js
let worker = new Worker("script.js");
// 用模块方式加载 module.js
let worker = new Worker("module.js", { type: "module" });

【浏览器模块说明符解析】

浏览器要求模块说明符具有以下几种格式之一

1、以/开头的解析为从根目录开始

2、以./开头的解析为从当前目录开始

3、以../开头的解析为从父目录开始

4、URL格式

// 从 https://www.example.com/modules/example1.js 导入
import { first } from "./example1.js";
// 从 from https://www.example.com/example2.js 导入
import { second } from "../example2.js";
// 从 from https://www.example.com/example3.js 导入
import { third } from "/example3.js";
// 从 from https://www2.example.com/example4.js 导入
import { fourth } from "https://www2.example.com/example4.js";
// 无效:没有以 / 、 ./ 或 ../ 开始
import { first } from "example.js";
// 无效:没有以 / 、 ./ 或 ../ 开始
import { second } from "example/index.js";

总结

下面对AMDCMDCommonJSES6module进行总结对比

AMDrequireJS在推广过程中对模块定义的规范化产出。AMD是一个规范,只定义语法API,而requireJS是具体的实现。类似于ECMAScriptjavascript的关系,由下面代码可知,AMD的特点是依赖前置,对于依赖的模块提前执行

// AMD
define(['./a', './b'], function(a, b) {  
  // 依赖必须一开始就写好
  a.doSomething()    
  // 此处略去 n 行    
  b.doSomething()    
  ...
})
// CMD
define(function(require, exports, module) { 
  var a = require('./a')
  a.doSomething()  
  // 此处略去 n 行   
  var b = require('./b') 
  // 依赖可以就近书写  
  b.doSomething()   
  // ... 
})
// math.js
exports.add = function () {
  var sum = 0, i = 0,args = arguments, l = args.length;while (i < l) {
    sum += args[i++];
  }    
  return sum;
};
// program.js
var math = require('math');
exports.increment = function (val) {
  return math.add(val, 1);
};
//example.js
export default function(num1, num2) {
  return num1 + num2;
}
// 导入默认值
import sum from "./example.js";
console.log(sum(1, 2)); // 3
上一篇下一篇

猜你喜欢

热点阅读