接前面的:
- JavaScript引擎与运行时入门小记:在哪些环境下可以运行JavaScript
- JavaScript学习笔记(一):理解概念,一等函数、编程范式、原型、单线程
- JavaScript学习笔记(二):全局对象globalThis,this用法,全局对象们
- JavaScript学习笔记(三):7种原始数据类型,对象类型(引用类型),typeof,原始类型包装器
继续学习JavaScript。
JavaScript 的模块系统是一种用于组织和重用代码的机制,它允许将代码分割成独立的文件或模块。JavaScript 原生的模块系统,是 ES6 引入的。
ES6模块(ESM)
模块的概念不复杂,但是和想象的不一样:竟然只能在module文件中使用import
!
模块使用尝试1
Node.js环境下,先写了一个模块文件:
1 2 3 |
|
然后写个使用它的脚本:
1 2 3 4 5 |
|
使用使用node.js运行一下,直接报错:
1 2 3 4 5 6 7 8 |
|
不过错误信息很直观,照做即可:
- 方法一:把后缀从
.js
改成.mjs
1 2 3 |
|
- 方法二:创建一个
package.json
文件,指定类型:
1 2 3 |
|
而后
1 2 3 |
|
- 方法三:使用 import()函数改写myapp.js文件
1 2 3 4 |
|
- 方法四:使用node的命令行参数
1 2 3 |
|
模块使用尝试2
在firefox下,写一个html文件,调用前面的 mylib.mjs
文件:
1 2 3 4 5 6 7 8 9 10 11 |
|
页面显示内容:
1 |
|
注:使用import必须指定script的类型为module,不然报错
1 |
|
模块使用尝试3
回到Qt下,看看QJSEngine。一个完整例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
结果
1 2 |
|
同样注意,只能使用importModule()
,而不能用evaluate()
,不然报错如下:
1 |
|
注意,QJSEngine 完全不支持
import()
写法,也就没办法像Node.js中的方法三那样使用。
文件后缀 .js .mjs
在WEB中,文件后缀用什么都所谓的,只要MIME类型是text/javascript
即可。对浏览器来说,判断是否module是通过type="module"
来进行的。尽管如此,V8文档建议模块的后缀选择.mjs
:
- 开发过程中,后缀
.mjs
对项目中任何人都更清晰:这是一个模块而不是一个传统的脚本。二者处理方式不同,区分他们很重要。 - 可以确保Node.js等运行时以及Babel等工具将文件解析为模块。
需要注意,web服务器需要保证发送.mjs文件时设置正确的头:
Content-Type: text/javascript
。
传统脚本 与 模块
尽管单靠后缀不能区分 传统脚本和模块。我们还是要看看传统脚本 与 模块的区别:
- 模块默认启用
strict mode
。(传统脚本需要使用'use strict';
来启用) - 模块不支持HTML风格的注释
<!-- Comments -->
- 模块具有词法顶层作用域。
var foo='1+1=10';
不会创建名为foo的全局变量。模块中的this
也不指向全局this
,而是undefined
。 import
和export
只能在模块中使用,在传统脚本中无效。- 模块中可以使用顶级的
await
。传统脚本不行。
比如,传统脚本中,只能:
1 2 3 |
|
在模块中,可以:
1 |
|
- 在浏览器中,传统脚本可以多次执行,模块只执行一次
1 2 3 4 5 6 7 8 |
|
export
在模块中,函数、var、let、const 和 类 都可以被导出。但它们必须是顶级项,不能在函数内部使用 export
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
或者在最后一行统一导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
导出的时候,还可以顺便改下名字:
1 |
|
使用它:
1 2 3 4 5 |
|
export default
关于这个东西,mdn解释说:
this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems
看个例子:
1 2 3 4 5 6 7 8 9 |
|
特点:import时,不需要使用大括号{}
,而且名字随便取
等同于
1 2 3 |
|
import
它有import声明和类似函数的import()两种用法
import声明
import声明(不要和import函数)的语法如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
模块名module-name
和 JavaScript 运行环境相关:
- 一般都是指向.js文件的 相对或绝对路径。
- Node.js下支持不带后缀的名字,指向
node_modules
中的包。 - 浏览器下支持一个
importmap
东西,通过JSON格式建立模块指示符(module specifier)和真实路径的关系。这样import时使用模块指示符就行了。
四种形式:
- Named import:
import { export1, export2 } from "module-name";
- Default import:
import defaultExport from "module-name";
- Namespace import:
import * as name from "module-name";
- Side effect import:
import "module-name";
import()
好处是在普通js文件中可以使用
1 2 3 4 5 |
|
ES6之前
有了ESM之后,用CommonJS和AMD实现模块似乎意义不大了,不过思想还是很有用。
对比一下CommonJS和AMD:
- 设计目标: CommonJS 主要为服务器端设计,而 AMD 主要为客户端设计。
- 加载方式: CommonJS 是同步加载,而 AMD 是异步加载。
- 导出和引入: CommonJS 使用
module.exports
和require
,而 AMD 使用define
和require
。 - 加载时机: CommonJS 在编译时加载模块,而 AMD 在运行时异步加载模块。
CommonJS
CommonJS是一种规范,用于统一JavaScript在浏览器之外的实现。它定义了一套通用的应用程序使用的API,以填补JavaScript标准库过于简单和不足。每个模块内部有两个变量可以使用,require和module。
一个js文件可看作是一个模块,比如myLib.js:
1 2 3 |
|
要在myApp.js中使用它:
1 2 3 4 5 |
|
不同于ESM中export和import两个关键字,CommonJS中的exports和require分别是对象和函数。
其中:
1 |
|
给exports添加属性是安全的,但是直接赋值会有问题,会断开和module.exports
链接,而module.exports
是真正要导出的东西。
基本原理
- 每个模块都是一个独立的文件: 在 Node.js 中,每个文件都被视为一个模块。每个模块都有自己的作用域,不会污染全局作用域。
- module 对象的存在: 在每个模块的执行过程中,Node.js 在内部创建了一个名为
module
的对象。这个对象包含了模块的一些信息和接口。
- exports 和 require:
module
对象上有一个属性叫做exports
,最初指向一个空对象{}
。通过给exports
赋值,可以将模块的接口导出。另外,require
函数用于引入其他模块,实际上是获取其他模块的exports
导出的内容。
- 包装模块代码: 在实际执行模块代码之前,Node.js 会在模块代码的周围加上一个函数包装,形成一个闭包,这个闭包的参数包括
exports
、require
、module
、__filename
和__dirname
等变量。这个包装函数的大致形式如下:
1 2 3 |
|
这个包装函数使得模块的代码在一个相对独立的作用域中执行。
AMD
AMD:Asynchronous Module Definition。AMD 规范是由 RequireJS 实现并推广的。
RequireJS 是一个用于浏览器端的 JavaScript 模块加载器,它遵循 AMD 规范,提供了在浏览器环境下实现模块化开发的工具,define
和require
两个函数是它实现的。
- 使用define定义AMD模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
这个define
函数返回一个对象,这个对象的属性就是模块的公开API。
- 在HTML,通过
requried
使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
在这个例子中,我们首先加载了RequireJS库。然后,我们使用require
函数来加载我们的模块。这个函数接受两个参数:一个是模块的路径数组,另一个是一个回调函数。当所有模块都加载完成时,回调函数会被调用,加载的模块会作为回调函数的参数。
define
define
函数是 AMD(异步模块定义)规范中用于定义模块的关键函数。它接受三个参数:模块的名称、依赖数组和模块的工厂函数。以下是 define
函数的基本结构:
1 |
|
moduleName
(可选):指定模块的名称,通常是一个字符串。如果省略,一些 AMD 实现会尝试根据脚本文件的路径来猜测模块名称。dependencies
(可选):一个数组,包含当前模块所依赖的其他模块的名称。这些模块会在加载当前模块之前被异步加载。factoryFunction
:一个函数,用于定义模块的实现。该函数在模块加载时执行,并接收参数,这些参数是依赖模块的导出对象。函数的返回值将被视为当前模块的导出内容。
前面例子,可以写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
require
require
函数是 AMD(异步模块定义)规范中用于加载模块的函数。它接受两个参数:模块的名称和一个回调函数。以下是 require
函数的基本结构:
1 |
|
moduleName
:指定要加载的模块的名称。callback
:一个回调函数,用于在模块加载完成后执行。这个回调函数通常包含对加载模块导出内容的操作。
Node.js确定模块类型
截至Node.js v21,它同时支持CommonJS和ESM两种模块。如何确定和区分模块类型?
- https://nodejs.org/docs/latest-v21.x/api/packages.html#determining-module-system
手册中说了很多,只关心最简单的规则:
.mjs
文件,识别为 EMS模块.cjs
文件,识别为 CommonJS模块.js
文件,如果最近的(上层)package.json
文件中,如果type
为module,则识别为EMS模块,如果为type
为commonjs,则识别为CommonJS模块,如果没有type
,由命令行--experimental-default-type
(当前默认值是commonjs,以后可能改为module)确定。
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
- https://v8.dev/features/modules#mjs
- https://en.wikipedia.org/wiki/CommonJS
- https://wiki.commonjs.org/wiki/Modules/1.1
- https://javascript.ruanyifeng.com/nodejs/module.html
- https://github.com/amdjs/amdjs-api/blob/master/AMD.md