彻底搞清楚 ECMAScript 的模块化(二)

简介: 彻底搞清楚 ECMAScript 的模块化

导入/导出


模块本身具有私有作用域,所以内部成员都无法被外部所访问到。如果要把模块内的成员暴露出去,需要使用 export 关键词去修饰要暴露的变量,而如果需要导入其他模块的成员,则需要 import 关键词。

下面演示一下导入导出。

创建 module.js,其中暴露一个变量。


export var msg = "hello world";

创建 app.js 文件,导入这个变量并在控制台打印。


import { msg } from "./module.js";
console.log(msg);

创建 index.html,导入 app.js,用来测试。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module" src="./app.js"></script>
  </body>
</html>

在浏览器中打开 index.html,就可以看到正常的输出结果。

这就是最简单的导入导出用法。

export 除了可以导出变量以外,还可以导出 function 和 class。


export var msg = "hello world";
export function log(...args) {
  console.log(args);
}
export class Clazz {}

除了可以直接在变量声明前面使用 export,也可以在模块底部统一导出需要导出的成员。


var msg = "hello world";
function log(...args) {
  console.log(args);
}
class Clazz {}
export { msg, log, Clazz };

这种写法更为常用,因为可以很直观的知道这个模块导出了哪些成员,而且通过这种方式导出的成员,可以被重命名。


export { msg as myMsg, log, Clazz };

如果导出的成员被重命名了,导入的地方就需要使用重命名后的名字。


import { myMsg } from "./module.js";

如果导出成员的名字为 default,那么 default 的值就会作为这个模块的默认导出。由于 default 是关键字,所以导入的地方必须重命名。


export { msg as default, log, Clazz };


import { default as msg } from "./module.js";

针对这种情况,还有一种更简单的写法。通过 export 和 default 关键字的组合来导出默认的模块。


export { log, Clazz };
export default msg;

这样在导入模块的默认导出时,就可以随便起一个名字。


import iMsg from "./module.js";


导入/导出注意事项


语法上和字面量对象的区别


export 的语法和对象字面量语法完全一致,这就会产生迷惑行为。

修改 module.js 内容。


var name = "jack";
var age = 18;
const obj = { name, age };
export { name, age };

这就导致很多人认为 export 导出的就是一个对象,而导入就是对这个对象的解构。实际上这是错误的。

虽然写法一样,但意义却没有任何关系。

export 单独使用时,就必须用花括号包裹要导出的成员。

当 export 和 default 同时使用时,后面的花括号就表示一个对象。

这一点要区分开。


var name = "jack";
var age = 18;
const obj = { name, age };
// export { name, age };// 这是导出语法
export default { name, age }; // 这是默认导出一个对象

这时通过 import 来获取 module.js 的默认导出。

修改 app.js 的内容。


import { name, age } from "./module";
console.log(name, age);

会得到一个错误。


Uncaught SyntaxError: The requested module './module.js' does not provide an export named 'age'

这说明 import 和 export 一样,导出的不是一个对象的解构。花括号同样是固定写法。


成员的引用


模块中导出的成员都不是值传递,而是引用传递。即使是基础类型也是这样。

可以通过下面的实例来观察。

修改 module.js。


var name = "jack";
var age = 18;
const obj = { name, age };
export { name, age };
setTimeout(function () {
  age = 20;
}, 1000);

修改 app.js。


import { name, age } from "./module.js";
console.log(name, age);
setTimeout(function () {
  console.log(age);
}, 1500);

这样在 1 秒后,age 的值被修改为 20,1.5 秒后打印 age 的值,如果是 20,就意味着是引用传递。

1.5 秒后打印的结果是 20。

这和 nodejs 的 CommonJS 规范是完全不同的。


暴露出来的成员都是只读的


我们没有办法在模块外部修改模块内部暴露的成员。

比如尝试在 app.js 中修改 module.js 中暴露的成员 name。


import { name, age } from "./module.js";
name = "tom";

会得到一个错误。


Uncaught TypeError: Assignment to constant variable.

这和 const 有点像。


导入


引用路径


import 导入模块时,from 后面的字符串实际上是模块的路径。这个路径必须是完整的文件名,不可以省略扩展名。

但是在一些打包工具中,比如 Webpack,可以省略文件扩展名,或者只填写目录名,省略 index.js。


import utils from "./utils/index.js"; // 标准语法
import utils from "./utils/index"; // 打包工具的语法,省略扩展名
import utils from "./utils"; // 打包工具的语法,省略 index.js

这一点和 CommonJS 是不同的。

在网页开发中,引用网络资源时,如果使用的是相对路径,可以省略掉./,但是 import 不可以省略。


<img src="cat.png" />
<!-- 等价于 -->
<!-- <img src='./cat.png'/> -->

但是 import 不可以省略。


import utils from "./utils/index.js"; // 正确语法
import utils from "utils/index.js"; // 错误语法

如果省略了/ ./ 或者 ../,那就会以字母开头,会被认为是加载第三方模块,这点和 CommonJS 是相同的。

除了相对路径,也可以通过项目的绝对路径或者 http 的 URL 来导入模块。


只去执行某个模块代码


比如模块 A 只是输出一串文字,并没有导出任何成员。


// moduleA.js
console.log("hello, world");

在模块 B 中想要执行模块 A 的内容,只需要导入这个模块,不需要导入成员,就可以执行里面的脚本。


// moduleB.js
import {} from "./moduleA.js";

除此之外,还有一种简单写法。就是省略成员列表和 from 关键词。


import "./moduleA.js";


导出成员较多


如果一个模块导出的成员非常多,而且其中很多成员都会被使用到,那么可以使用星号(*)的方式导入。

这种导入需要使用 as 关键词给导出的所有成员重命名,然后导出的成员都会被挂载到这个对象身上。

比如在 module.js 中导出了两个成员。


var name = "jack";
var age = 18;
export { name, age };

那么在 app.js 中用星号的方式导入。


import * as mod from "./module.js";
console.log(mod);

就可以在控制台看到这个对象。


{
  "age": 18,
  "name": "jack"
}


import 不可以导入变量


当一个模块路径是在代码运行阶段得到的,那么无法使用 import 导入。

import 关键词只可以导入在代码运行前已知的模块路径。

所以 import 关键词只可以出现在模块文件的最顶部。

错误示例。


var modulePath = "./module.js"
import * as mod from modulePath
console.log(mod)

当我们遇到需要动态导入的机制时,就需要使用 import 函数。

因为这里的 import 是一个函数,而不是关键词,所以可以在代码的任何位置去执行。

import 函数返回一个 Promise 对象。模块暴露的对象会通过 Promise 中 then 参数函数的参数拿到。


var modulePath = "./module.js";
import(modulePath).then((module) => {
  console.log(module);
});


同时导出默认成员和命名成员


如果 module.js 同时暴露了默认成员和命名成员,而在 app.js 想同时导入它们,可以用下面这种写法。

module.js


var name = "jack";
var age = 18;
export { name, age };
export default "hi";

app.js


import { name, age, default as title } from "./module.js";
console.log(name, age, title);

除了重命名这种写法,还可以直接在花括号外前面直接导出默认成员,更加简洁。

这种写法必须让 default 成员在命名成员的前面,保证花括号和 from 关键词是在一起的。


import title, { name, age } from "./module.js";
console.log(name, age, title);


直接导出导入成员


import 可以和 export 组合使用,便于我们直接导出。


export { name, age, default as title } from "./module.js";
// console.log(name, age, title);

但是这种导出需要注意两个点。

第一是 default 要写到花括号中。

第二是直接 export 的话,模块内部无法访问到这些成员。

这种语法通常在一个模块文件夹中的 index.js 上使用。

比如有一个 components 文件夹,里面存放了很多组件。

每个组件都以一个模块的形式维护。这样会有 button.js、table.js、avatar.js 等等。

使用的时候就需要挨个导入,非常麻烦。


import { Button } from "./components/button.js";
import { Table } from "./components/table.js";
import { Avatar } from "./components/avatar.js";

优化这种导入导出体验的办法是在 components 文件夹下创建一个 index.js,由它负责导出所有的组件。

index.js


export { Button } from "./button.js";
export { Table } from "./table.js";
export { Avatar } from "./avatar.js";

这样在导入时会非常简单。


import { Button, Table, Avatar } from "./components/index.js";


ES Modules in Browser


由于 ES Module 是 2014 年才提出的,到现在还是有很多浏览器原生不支持这种用法。

所以使用 ES Module 时,还需要考虑兼容性的问题。


Polyfill 兼容方案


一般来说,我们使用 ES Module 编写的源代码都会经过编译工具的编译转换为 ES5,再拿去浏览器中工作。

但是也有办法让浏览器直接支持 ES Module。

社区提供了一个叫做 browser-es-module-loader 的模块,其实就是两个 js 文件。在 html 中先导入这两个模块,就可以让 ES Module 工作了。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
    <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    <script type="module" src="./app.js"></script>
  </body>
</html>

上面用到的两个包,第一个是 babel 运行在浏览器的版本,第二个是用来加载 es module 的。

原理比较简单,首先 browser-es-module-loader 会通过 ajax 方式或者直接提取标签中代码的方式加载 module 文件,然后交给 babel 转换,得到浏览器可以直接运行的代码。

需要注意,这只是解决了模块导入的问题,如果代码中存在 ES6 的语法,还需要额外的 polyfill。

但是这种做法会有一个小问题。

在直接支持 es module 的浏览器中,模块的代码会被执行两次。原因很简单,浏览器本身就会执行一次模块中的代码,polyfill 也会执行一次模块中的代码。

解决这个问题的方法就是给 script 添加 nomodule 属性。

被添加 nomodule 属性的脚本,只会在不支持 es module 的浏览器中去执行,而支持 es module 的浏览器则不执行。


<script
  nomodule
  src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"
></script>
<script
  nomodule
  src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"
></script>

polyfill 方案仅适合开发测试阶段使用,而不适合生产阶段去使用。因为所有的模块代码都是实时编译的,性能非常差。


相关文章
|
6月前
|
C语言 开发者
模块化程序设计
模块化程序设计
64 0
|
5月前
|
编译器 C++ 开发者
C++一分钟之-C++20新特性:模块化编程
【6月更文挑战第27天】C++20引入模块化编程,缓解`#include`带来的编译时间长和头文件管理难题。模块由接口(`.cppm`)和实现(`.cpp`)组成,使用`import`导入。常见问题包括兼容性、设计不当、暴露私有细节和编译器支持。避免这些问题需分阶段迁移、合理设计、明确接口和关注编译器更新。示例展示了模块定义和使用,提升代码组织和维护性。随着编译器支持加强,模块化将成为C++标准的关键特性。
281 3
|
6月前
|
前端开发 JavaScript 容器
早期 JavaScript 中的高阶函数与模块化实现
早期 JavaScript 中的高阶函数与模块化实现
|
6月前
|
存储 Web App开发 自然语言处理
从 ECMAScript 6 角度谈谈执行上下文
发现很多书籍和资料,包括《JavaScript高级程序设计》、《JavaScript权威指南》和网上的一些博客专栏,都是从 ES3 角度来谈执行上下文,用ES6规范解读的比较少,所以想从ES6的角度看一下执行上下文。文章主要从这几个方面介绍:ES6规范中的词法环境,ES6规范中定义的执行上下文结构和从ES6规范看实际代码的执行流程
175 0
从 ECMAScript 6 角度谈谈执行上下文
|
JavaScript 前端开发 内存技术
彻底搞清楚 ECMAScript 的模块化(三)
彻底搞清楚 ECMAScript 的模块化
386 0
|
JavaScript 前端开发 Go
彻底搞清楚 ECMAScript 的模块化(一)
彻底搞清楚 ECMAScript 的模块化
118 0
|
JavaScript
用大白话让你理解TypeScript的要点.(二)
用大白话让你理解TypeScript的要点.(二)
用大白话让你理解TypeScript的要点.(二)
|
JavaScript 索引
用大白话让你理解TypeScript的要点.(三)
用大白话让你理解TypeScript的要点.(三)
用大白话让你理解TypeScript的要点.(三)
|
JavaScript Java C语言
用大白话让你理解TypeScript的要点.(一)
用大白话让你理解TypeScript的要点.(一)
用大白话让你理解TypeScript的要点.(一)