1.浏览器加载
HTML网页中,浏览器通过使用<script>
标签加载JavaScript脚本。是同步加载,执行完脚本才会继续向下渲染,会造成浏览器堵塞。
<script type="application/javascript />
<script>
标签打开defer
或async
属性,脚本就会异步加载。渲染引擎遇到这个命令就开始下载外部脚本,但是不会等它下载和执行,而是直接执行后面的命令。
defer
:要等整个压面渲染结束后,才会执行。按照页面出现顺序加载。
async
:下载完就执行,不能保证按序加载。
<script src="path/to/myModule.js" defer />
<script src="path/to/myModule.js" async />
2. ES6加载
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。是异步加载,等同于打开了<script>
标签的defer
属性。
<script type="module" src="./foo.js" />
3. ES6与CommonJS模块的差异
两大重大差异:
- commonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用; commonJS模块是运行时加载,ES6模块是编译时输出接口。
- commonJS加载的是一个对象(
module.exports
),该对象只有在脚本运行完才会执行。而ES6模块不是对象,它对外接口只是一种静态定义,在代码静态解析阶段就会生成。
CommonJS
模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代码说明,lib.js
模块加载以后,它的内部变化就影响不到输出的mod.counter
了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
ES6
模块的运行机制与 CommonJS
不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用
。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6
的impor
t有点像 Unix
系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6
模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
3. Node.js加载
Node.js
要求 ES6
模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs后缀名。Node.js
遇到.mjs
文件,就认为它是 ES6
模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
{
"type": "module"
}
如果这时还要使用CommonJS
模块,那么需要将 CommonJS
脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS
模块。
总结为一句话:.mjs
文件总是以 ES6
模块加载,.cjs
文件总是以 CommonJS
模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6
模块与 CommonJS
模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。