ECMAScriptES6挨的模块

面前的话语

  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 };

  在是示例中需要注意几单细节,除了export关键字外,每一个扬言和剧本中的同样型一样。因为导出的函数和好像声明需要发一个名称,所以代码中之每一个函数或类似为真正来这个称号。除非用default关键字,否则不能够因此这个语法导出匿名函数或相近

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

 

导入

  从模块中导出的效益可以经import关键字在另外一个模块中访问,import语句的有限单部分各自是设导入的标识符和标识符应当由哪个模块导入

  这是该语句的为主形式

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

  import后面的大括哀号表示于给定模块导入的绑定(binding),关键字from表示从哪个模块导入给定的绑定,该模块由象征模块路径的字符串指定(被叫作模块说明符)。浏览器采用的途径格式和污染给<script>元素的一模一样,也就是说,必须管文件扩展名也丰富。另一方面,Nodejs则以基于文件系统前缀区分本地文件及包之老规矩。例如,example是一个包而./example.js是一个地面文件

  当由模块中导入一个绑定时,它就是好像使const定义的一致。无法定义另一个同名变量(包括导入外一个同名绑定),也无法以import语句前应用标识符或改动绑定的值

【导入单个绑定】

  假设前面的演示在一个名为也”example.js”的模块中,我们得以导入并因强办法利用这模块中的绑定

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

  尽管example.js导出的函数不止一个,但这个示例导入的可独自来sum()函数。如果尝试为sum赋新价值,结果是丢弃来一个破绽百出,因为未可知叫导入的绑定重新赋值

  为了最好地兼容多个浏览器和Node.js环境,一定要是在字符串之前包含/、./或../来代表若导入的文本

【导入多个绑定】

  如果想由示例模块导入多只绑定,则足以一目了然地拿其列出如下

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

  以这段代码中,从example模块导入3只绑定sum、multiply与magicNumber。之后以其,就如她当当地定义之平等

【导入整个模块】

  特殊情形下,可以导入整个模块作为一个纯粹的对象。然后所有的导出都好当作目标的性能使用

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

  以当时段代码中,从example.js中导出的享有绑定被加载到一个受称作example的目标被。指定的导出(sum()函数、mutiply()函数和magicNumber)之后会作为example的习性让看。这种导入格式为称命名空间导入(namespaceimport)。因为example.js文件中不存在example对象,故而它当example.js中保有导出成员的命名空间对象要深受创造

  但是,不管在import语句被把一个模块写了小次,该模块将仅实行同样浅。导入模块的代码执行后,实例化过之模块于保存在内存中,只要别一个import语句引用它便可重复使用它

import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

  尽管当此模块中发出3单import语句,但example加载只实行同一不行。如果与一个应用程序中的别样模块也从example.js导入绑定,那么那些模块和这代码用以相同之模块实例

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

  ES6的import语句也变量、函数和类似创建的凡单独读绑定,而未是比如说正规变量一样简单地引用原始绑定。标识符只有在受导出的模块中得以修改,即便是导入绑定的模块也无从改变绑定的价值

export var name = "huochai";
export function setName(newName) {
    name = newName;
}

  当导入这半独绑定后,setName()函数可以转name的价

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

  调用setName(“match”)时会见回来导出setName()的模块中失去执行,并以name设置也”match”。此还改会自动在导入的name绑定上反映。其由是,name是导出的name标识符的本地名称。本段代码中所用的name和模块中导入的name不是跟一个

 

重命名

  有时候,从一个模块导入变量、函数或者类时,可能不欲用它们的老名称。幸运的凡,可以在导出过程与导入过程被改导出元素的称呼

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

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

  于此处,函数sum()是当地名称,add()是导出时用的称号。也就是说,当其他一个模块要导入这个函数时,必须使用add这个名称

import { add } from "./example.js";

  如果模块想利用不同的名来导入函数,也可采取as关键字

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

  这段代码导入add()函数时采取了一个导入名称来重命名sum()函数(当前达成下文中之本地名称)。导入时移函数的本土名称意味着就是模块导入了add()函数,在当前模块中呢未尝add()标识符

 

默认值

  由于当诸如CommonJS的另模块系统受,从模块中导出与导入默认值是一个广的做法,该语法被进行了优化。模块的默认值指的是经过default关键字指定的么变量、函数或近似,只能为每个模块设置一个默认的传导出价值,导出时屡屡用到default关键字是一个语法错误

【导出默认值】

  下面是一个利用default关键字之略示例

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

  这个模块导出了一个函数作为其的默认值,default关键字表示马上是一个默认的导出,由于函数被模块所代表,因而它不欲一个名

  也得以以export default事后续加默认导出值的标识符,就比如这么

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

  先定义sum()函数,然后重新以那个导出为默认值,如果要算默认值,则好利用此办法。为默认导出值指定标识符的老三种植方式是行使重命名语法,如下所示

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

  以重命名导出时标识符default有特有含义,用来指示模块的默认值。由于default是JS中之默认关键字,因此不克用那用于变量、函数或近乎的称谓;但是,可以将该看做属性名称。所以用default来再次命名模块是为尽可能和非默认导出的定义一致。如果想当一如既往漫长导出语句被并且指定多单导出(包括默认导出),这个语法非常实惠

【导入默认值】

  可以采取以下语法从一个模块导入一个默认值

// 导入默认值
import sum from "./example.js";
console.log(sum(1, 2)); // 3

  这长达import语句从模块example.js中导入了默认值,请留心,这里没行使大括声泪俱下,与非默认导入的情形例外。本地名称sum用于表示模块导出的别默认函数,这种语法是太单纯的,ES6的主创者希望其会成web上主流的模块导入形式,并且可以使用已有些对象

  对于导出默认值和同一要多个非默认绑定的模块,可以用相同条告句子导入所有导出的绑定

export let color = "red";
export default function(num1, num2) {
    return num1 + num2;
}

  可以为此以下即长达import语句导入color和默认函数

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"

  于即时段代码中,默认导出(export)值为再度命名吧sum,并且还导入了color

 

静态加载

   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"; // 语法错误
}

  以上之写法会报错,是为以静态分析阶段,这些语法都是无奈得到值的

  这样的规划,固然好编译器提高效率,但为招致无法在运作时加载模块。在语法上,条件加载就无可能实现。如果import命令要代表
Node
require方,这就形成了一个障碍。因为require大凡运作时加载模块,import令无法代替require的动态加载功能

const path = './' + fileName;
const myModual = require(path);

  上面的言辞就是动态加载,require到底加载哪一个模块,只有运行时才了解。import喻句子做不至即一点

 

再次导出

  可能得重新导出模块已导入的情节

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

  虽然这么好运行,但就透过平等久告句也可以就同样的职责

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

  这种样式的export在指定的模块中查找sum声明,然后将那个导出。当然,对于同样的值也可不同之名号导出

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

  这里的sum是从example.js导入的,然后还用add这个名字将那导出

  如果想导出另一个模块中之兼具值,则可采取*模式

export * from "./example.js";

  导出一切是指导出默认值及具有命名导出值,这或会见影响可以起模块导出的情节。例如,如果example.js有默认的导出价值,则动用是语法时将无法定义一个新的默认导出

 

无论绑定导入

  某些模块可能无导出任何事物,相反,它们可能只修改全局作用域中之对象。尽管模块中的顶层变量、函数和相近非会见自行地出现在全局意图域中,但立刻并无意味着模块无法访问全局作用域。内建对象(如Array及Object)的共享定义可以于模块中访问,对这些目标所开的反将体现在任何模块中

  例如,要于所有数组上加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);

  这段代码导入并实施了模块中涵盖的pushAll()方法,所以pushAll()被补充加至数组的原型,也就是说现在模块中的所有数组都得使pushAll()方法了

  [注意]无论是绑定导入最有或给运被创造polyfill和Shim

 

加载模块

  虽然ES6概念了模块的语法,但其并无定义如何加载这些模块。这正是规范复杂性的一个体现,应由不同之兑现环境来决定。ES6没有品味为具备JS环境创建同套统一之正经,它独自规定了语法,并拿加载机制抽象到一个免定义的中方法HostResolveImportedModule中。Web浏览器与Node.js开发者可以经过对个别环境的回味来控制哪些落实HostResolveImportedModule 

【在Web浏览器被运用模块】

  即使以ES6产出以前,Web浏览器为产生强艺术得以拿JS包含在Web应用程序中,这些本子加载的方法分别是

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

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

  3、通过Web Worker或Service 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>

  此示例中之首先单<script>元素采用src属性加载了一个外部的模块文件,它与加载脚本之间的绝无仅有区别是type的价是”module”。第二只<script>元素包含了直坐在网页中之模块。变量result没有露到全局作用域,它只是设有让模块中(由<script>元素定义),因此不见面给补充加到window作为它们的性

  于Web页面中引入模块的长河看似于引入脚本,相当简单。但是,模块实际的加载过程也闹一对见仁见智

  ”module”与”text/javascript”这样的情节类型并不相同。JS模块文件及JS脚本文件具有同样的始末类型,因此无法单独冲内容类型进行区分。此外,当无法识别type的值经常,浏览器会忽略<script>元素,因此无支持模块的浏览器将自动忽略<script
type=”module”>来提供优良的为后兼容性

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>

  这3个<script>元素以它被指定的次第执行,所以模块module1.js保证会在内联模块前执行,而内联模块保证会在module2.js模块之前实施

  每个模块都得以自一个要么多单其他的模块导入,这会如问题复杂化。因此,首先分析模块以识别所有导入语句;然后,每个导入语句都碰发一样糟沾过程(从网络或由缓存),并且以具有导入资源且让加载与执行后才会履时模块

  用<script
type=”module”>显式引入和用import隐式导入的富有模块都是本需要加载并实行之。在此示例中,完整的加载顺序如下

  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>

  在斯示例中,两独模块文件给异步加载。只是略地扣押这个代码判断不产生哪位模块先实施,如果module1.js第一得下载(包括其兼具的导入资源),它以先行实行;如果module2.js首先做到下载,那么她将优先实施

拿模块作为Woker加载

  Worker,例如Web Worker和Service
Woker,可以于网页上下文之外执行JS代码。创建新Worker的手续包括创造一个初的Worker实例(或外的近乎),传入JS文件之地方。默认的加载机制是依照剧本的方法加载文件

// 用脚本方式加载 script.js
let worker = new Worker("script.js");

  为了支持加载模块,HTML标准的开发者向这些构造函数添加了次个参数,第二单参数是一个对象,其type属性的默认值为”script”。可以拿type设置为”module”来加载模块文件

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

  以此示例中,给老二个参数传入一个对象,其type属性的价为”module”,即以模块而休是本子的方法加载module.js。(这里的type属性是以仿效<script>标签的type属性,用以区分模块和本子)所有浏览器被的Worker类型都支持第二个参数

  Worker模块通常与Worker脚本一起以,但为生一对不等。首先,Worker脚本只能由与援的网页相同的源加载,但是Worker模块不会见全盘受限,虽然Worker模块具有同样的默认限制,但它或者得以加载并走访具有相当的跨域资源共享(CORS)头的文本;其次,尽管Worker脚本可以采取self.importScripts()方法以另外脚本加载到Worker中,但self.importScripts()却始终无法加载Worker模块,因为该采取import来导入

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

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

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

  2、以./开头的剖析为于当前目录开始

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

  4、URL格式

  例如,假设有一个模块文件在https://www.example.com/modules/modules.js,其中包含以下代码

// 从 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";

  此示例中之每个模块说明符都适用于浏览器,包括最后一尽遭之好完整的URL(为了支持跨域加载,只需要保证www2.example.com的CORS头的布局是对的)尽管没有就的模块加载器规范将提供解析其他格式的不二法门,但时,这些是浏览器默认情况下唯一可分析的模块说明符的格式

  因此,一些押起正常的模块说明称在浏览器被实际上是低效的,并且会招致错误

// 无效:没有以 / 、 ./ 或 ../ 开始
import { first } from "example.js";
// 无效:没有以 / 、 ./ 或 ../ 开始
import { second } from "example/index.js";

  由于当时半只模块说明符的格式不科学(缺少科学的胚胎字符),因此它们无法给浏览器加载,即使在<script>标签中作src的值时彼此都可以正常工作。<script>标签及import之间的这种作为差异是故为底

 

总结

  下给AMD、CMD、CommonJS和ES6的module进行总结对比

  AMD是requireJS在拓宽过程遭到针对模块定义的规范化产出。AMD是一个正式,只定义语法API,而requireJS是切实的兑现。类似于ECMAScript同javascript的涉及

  由下代码可知,AMD的性状是指前置,对于因之模块提前实施

// AMD
define(['./a', './b'], function(a, b) {  // 依赖必须一开始就写好
    a.doSomething()    
    // 此处略去 n 行    
    b.doSomething()    
    ...
})

  CMD 是 SeaJS
在放开过程遭到针对模块定义的规范化产出,它的风味是依赖就近,对于依靠的模块延迟执行

// CMD
define(function(require, exports, module) { 
    var a = require('./a')
     a.doSomething()  
    // 此处略去 n 行   
    var b = require('./b') // 依赖可以就近书写  
    b.doSomething()   
    // ... 
})

  CommonJS规范重点以NodeJS后端应用,前端浏览器不支持该专业

// 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);
};

  ES6的Module模块主要通过export和import来进行模块的导入和导出

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