乐趣区

关于程序员:快速了解JavaScript的模块

概述

随着古代 JavaScript 开发 Web 利用变得复杂,命名抵触和依赖关系也变得难以解决,因而须要模块化。而引入模块化,能够防止命名抵触、不便依赖关系治理、进步了代码的复用性和和维护性,因而,在 JavaScript 没有模块性能的前提下,只能通过第三方标准实现模块化:

  • CommonJS:同步模块定义,用于服务器端。
  • AMD:异步模块定义,用于浏览器端。
  • CMD:异步模块定义,用于浏览器端。
  • UMD:对立 COmmonJSAMD 模块化计划的定义。

它们都是基于 JavaScript 的语法和词法个性“伪造”出相似模块的行为。而 TC-39 在 ECMAScript 2015 中退出了模块标准,简化了下面介绍的模块加载器,原生意味着能够取代上述的标准,成为浏览器和服务器通用的模块解决方案,比应用库更有效率。而 ES6 的模块化的设计指标:

  • CommonJS 一样简略的语法。
  • 模块必须是动态的构造
  • 反对模块的 异步加载 同步加载,能同时用在 serverclient
  • 反对模块加载的‘灵便配置’
  • 更好地反对模块之间的循环援用
  • 领有语言层面的反对,超过 CommonJSAMD

ECMAScript 在 2015 年开始反对模块规范,尔后逐步倒退,现曾经失去了所有支流浏览器的反对。ECMAScript 2015 版本也被称为 ECMAScript 6。

模块

ES6 模块借用了 CommonJSAMD 的很多优良个性,如下所示:

  • 模块代码只在加载后执行。
  • 模块只能加载一次。
  • 模块是单例。
  • 模块能够定义公共接口,其余模块能够基于这个公共接口察看和交互。
  • 模块能够申请加载其余模块。
  • 反对循环依赖。

ES6 模块零碎也减少了一些新行为。

  • ES6 模块默认在严格模式下执行。
  • ES6 模块不共享全局命名空间。
  • 模块顶级 this 的值是 undefined;惯例脚本中是 window
  • 模块中的 var 申明不会增加到 window 对象。
  • ES6 模块是异步加载和执行的。

浏览器运行时在晓得应该把某个文件当成模块时,会有条件地依照上述 ES6 模块行为来施加限度。与 <script type="module"> 关联或者通过 import 语句加载的 JavaScript 文件会被认定为模块。

导出

ES6 模块外部的所有变量,内部无奈获取,因而提供了 export 关键字从模块中导出实时绑定的函数、对象或原始值,这样其余程序能够通过 import 关键字应用它们。export 反对两种导出形式:命名导出和默认导出。不同的导出形式对应不同的导入形式。

在 ES6 模块中,无论是否申明 "use strict;" 语句,默认状况下模块都是在严格模式下运行。export 语句不能用在嵌入式脚本中。

命名导出

通过在申明的后面加上 export 关键字,一个模块能够导出多个内容。这些导出的内容通过名字辨别,被称为命名导出。

// 导出单个个性(能够导出 var,let,const)export let name = "小明";
export function sayHi(name) {console.log(`Hello, ${name}!`);
}
export class Sample {...}

或者导出当时定义的个性

let name = "小明";
const age = 18;
function sayHi(name) {console.log(`Hello, ${name}!`);
}
export {name, age, sayHi}

导出时也能够指定别名,别名必须在 export 子句的大括号语法中指定。因而,申明值、导出值和未导出值提供别名不能在一行实现。

export {name as username, age, sayHi}

但导出语句必须在模块顶级,不能嵌套在某个块中:

// 容许
export ...
// 不容许
if (condition) {export ...}

默认导出

默认导出就如同模块与被导出的值是一回事。默认导出应用 default 关键字将一个值申明为默认导出,每个模块只能有一个默认导出。反复的默认导出会导致 SyntaxError。如下所示:

// 导出当时定义的个性作为默认值
export default {
    name: "Xiao Ming",
    age: 18,
    sex: "boy"
};
export {sayHi as default}    // ES 6 模块会辨认作为别名提供的 default 关键字。此时,尽管对应的值是应用命名语法导出的,实际上则会称为默认导出 等同于 export default function sayHi() {}
// 导出单个个性作为默认值
export default function () {...}
export default class {...}

ES6 标准对不同模式的 export 语句中能够应用什么不能够应用什么规定了限度。某些模式容许申明和赋值,某些模式只容许表达式,而某些模式则只容许简略标识符。留神,有的模式应用了分号,有的则没有。

上面列出几种会导致谬误的 export 模式:

// 会导致谬误的不同模式:// 行内默认导出中不能呈现变量申明
export default const name = '小刘';
// 只有标识符能够呈现在 export 子句中
export {123 as name}
// 别名只能在 export 子句中呈现
export const name = '小红' as uname;

留神:申明、赋值和导出标识符最好离开。这样不容易搞错了,同时也能够让 export 语句集中在一块。而且,没有被 export 关键字导出的变量、函数或类会在模块内放弃公有。

模块重定向

模块导入的值还能够再次导出,这样的话,能够在父模块集中多个模块的多个导出。能够应用 export from 语法实现:

export {default as m1, name} from './module1.js'
// 等效于
import {default as m1, name} from "./module1.js"
export {m1, name}

内部模块的默认导出也能够重用为以后模块的默认导出:

export {default} from './module1.js';

也能够在从新导出时,将导入模块批改为默认导出,如下所示:

export {name as default} from './module1.js';

而想要将所有命名导出能够应用如下语法:

export * from './module1.js';

该语法会疏忽默认导出。但这种语法也要留神导出名称是否抵触。如下所示:

// module1.js
export const name = "module1:name";
// module2.js
export * from './mudule1.js'
export const name = "module2:name";
// index.js
import {name} from './module2.js';
console.log(name); // module2:name

最终输入的是 module2.js 中的值,这个“重写”是静默产生的。

导入

应用 export 关键字定义了模块的对外接口当前,其它模块就能通过 import 关键字加载这个模块了。但与 export 相似,import 也必须呈现在模块的顶级:

// 容许
import ...
// 不容许
if (condition) {import ...}

模块标识符能够是绝对于以后模块的相对路径,也能够是指向模块文件的绝对路径。它必须是纯字符串,不能是动静计算的后果。例如,不能是拼接的字符串。

当应用 export 命名导出时,能够应用 * 批量获取并赋值给保留导出汇合的别名,而无须列出每个标识符:

const name = "Xiao Ming", age = 18, sex = "boy";
export {name, age, sex}

// 下面的命名导出能够应用如下模式导入(下面的代码是在 module1.js 模块中)
import * as Sample from "./module1.js"
console.log(`My name is ${Sample.name}, A ${Sample.sex},${Sample.age} years old.`);

也能够指名导入,只须要把名字放在 {} 中即可:

import {name, sex as s, age} from "./module1.js";
console.log(`My name is ${name}, A ${s},${age} years old.`);

import 引入是采纳的 Singleton 模式,屡次应用 import 引入同一个模块时,只会引入一次该模块的实例:

import {name, age} from "./module1.js";
import {sex as s} from "./module1.js";
// 等同于,并且只会引入一个 module1.js 实例
import {name, sex as s, age} from "./module1.js";

而应用默认导出的话,能够应用 default 关键字并提供别名来导入,也能够间接应用标识符就是默认导出的别名导入:

import {default as Sample} from "./module1.js"
// 与上面的形式等效
import Sample from "./module1.js"

而模块中同时有命名导出和默认导出,能够在 import 语句中同时导入。上面三种形式都等效。

import Sample, {sayHi} from "./module1.js"
import {default as Sample, sayHi} from "./module1.js"
import Sample, * as M1 from "./module1.js"

当然,也能够将整个模块作为副作用而导入,而不导入模块中的特定内容。这将运行模块中的全局代码,但实际上不导入任何值。

import './module1.js'

import 导入的值与 export 导出的值是绑定关系,绑定是不可变的。因而,import 对所导入的模块是只读的。然而能够通过调用被导入模块的函数来达到目标。

import Sample, * as M1 from "./module1.js"
Sample = "Modify Sample";    // 谬误
M1.module1 = "Module 1";    // 谬误
Sample.name = "小亮";       // 容许

这样做的益处是可能反对循环依赖,并且一个大的模块能够拆成若干个小模块时也能够运行,只有不尝试批改导入的值。

留神:如果要在浏览器中原生加载模块,则文件必须带有 .js 扩展名,不然可能无奈解析。而应用构建工具或第三方模块加载器打包或解析 ES6 模块,可能不须要蕴含扩展名。

import()

规范的 import 关键字导入模块是动态的,会使所有被导入的模块,在加载时就被编译。而最新的 ES11 规范中引入了动静导入函数 import(),不用事后加载所有模块。该函数会将模块的门路作为参数,并返回一个 Promise,在它的 then 回调里应用加载后的模块:

import ('./module1.mjs')
    .then((module) => {// Do something with the module.});

这种应用形式也反对 await 关键字。

let module = await import('./module1.js');

import() 的应用场景如下:

  • 按需加载。
  • 动静构建模块门路。
  • 条件加载。

加载

ES6 模块既能够通过浏览器原生加载,也能够与第三方加载器和构建工具一起加载。

齐全反对 ES6 模块的浏览器能够从顶级模块异步加载整个依赖图。浏览器会解析入口模块,确定依赖,并发送对依赖模块的申请。这些文件通过网络返回后,浏览器会解析它们的内容,确认依赖,如果二级依赖还没有加载,则会发送更多申请。这个异步递归加载过程会继续到整个依赖图都解析实现。解析完依赖,利用就能够正式加载模块了。

模块文件按需加载,且后续模块的申请会因为每个依赖模块的网络提早而同步提早。即,module1 依赖 module2module2 依赖 module3。浏览器在对 module2 的申请实现之前并不知道要申请 module3。这种架子啊形式效率高,也不须要内部工具,但加载大型利用的深度依赖图可能要花费很长时间。

HTML

想要在 HTML 页面中应用 ES6 模块,须要将 type="module" 属性放在 <script> 标签中,来申明该 <script> 所蕴含的代码在浏览器中作为模块执行。它能够嵌入在网页中,也能够作为内部文件引入:

<script type="module">
    // 模块代码
</script>
<script type="module" src="./module1.js"></script>

<script type="module">模块加载的程序与 <script defer> 加载的脚本一样按程序执行。但执行会提早到文档解析实现,但执行程序就是 <script type="module"> 在页面中呈现的程序。

也能够给模块标签增加 async 属性。这样影响是双重的,不仅模块执行程序不再与 <script> 标签在页面中的程序绑定,模块也不会期待文档实现解析才执行。不过,入口模块必须期待其依赖加载实现。

Worker

Worker 为了反对 ES6 模块,在 Worker 构造函数中能够接管第二个参数,其 type 属性的默认值是 classic,能够将 type 设置为 module 来加载模块文件。如下所示:

// 第二个参数默认为{type: 'classic'}
const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module'});

在基于模块的工作者外部,self.importScripts() 办法通常用于在基于脚本的工作者中加载内部脚本,调用它会抛出谬误。这是因为模块的 import 行为蕴含了 importScripts()

向后兼容

如果浏览器原生反对 ES6 模块,能够间接应用,而不反对的浏览器能够应用第三方模块零碎(System.js)或在构建时将 ES6 模块进行转译。

脚本模块能够应用 type="module" 属性设定,而对于不反对模块的浏览器,能够应用 nomodule 属性。此属性会告诉反对 ES6 模块的浏览器不执行脚本。不反对模块的浏览器无奈辨认该属性,从而疏忽该属性。如下所示:

// 反对模块的浏览器会执行这段脚本
// 不反对模块的浏览器不会执行这段脚本
<script type="module" src="module.js"></script>
// 反对模块的浏览器不会执行这段脚本
// 不反对模块的浏览器会执行这段脚本
<script nomodule src="script.js"></script>

总结

ES6 在语言层面上反对了模块,完结了 CommonJSAMD 这两个模块加载器的长期决裂情况,从新定义了模块性能,集两个标准于一身,并通过简略的语法申明来裸露。

模块的应用不同形式加载 .js 文件,它与脚本有很大的不同:

  1. 模块始终应用 use strict 执行严格模式。
  2. 在模块的顶级作用域创立的变量,不会被主动增加到共享的全局作用域,它们只会在模块顶级作用域的外部存在。
  3. 模块顶级作用域的 this 值为 undefined
  4. 模块不容许在代码中应用 HTML 格调的正文。
  5. 对于须要让模块内部代码拜访的内容,模块必须导出它们。
  6. 容许模块从其余模块导入绑定。
  7. 模块代码执行一次。导出仅创立一次,而后会在导入之间共享。

浏览器对原生模块的反对越来越好,但也提供了持重的工具以实现从不反对到反对 ES6 模块的过渡。

更多内容请关注公众号「海人为记

退出移动版