如果你问开发人员:"对你来说最难的 JS 题目是什么?",你绝不会听到他说是 ES6 模块。但统计数据更能说明问题!我们统计了我们电报频道中各种主题的问答错误答案数量,发现 ES6 模块是最难的主题之一。
// index.mjs
import { default } from './module.mjs';
console.log(default);
// module.mjs
export default 'bar';
首先,让我们记住各种导入和导出语法:
如果检查表中的 Import 语法,就会发现没有与我们的代码相匹配的语法:
import { default } from ‘./module.mjs’;
因为禁止使用这种语法。测验代码会出现以下错误:
SyntaxError: Unexpected reserved word
在 import { default } from ‘./module.mjs’;
行中, default
是 export
的名称,也是该作用域中的变量名称,这是被禁止的,因为 default
是一个保留字。解决方法很简单:
import { default as foo } from ‘./module.mjs’;
现在, default
是导出的名称, foo
是变量的名称。换句话说,如果你想在默认导出中使用命名导入语法,就必须重命名它。就是这样,非常简单!
// index.js
console.log('index.js');
import { sum } from './helper.js';
console.log(sum(1, 2));
// helper.js
console.log('helper.js');
export const sum = (x, y) => x + y;
没有多少开发人员知道的一个重要的细微差别是,导入是被提升的。也就是说,在引擎解析代码时,导入就会被加载。所有依赖项都将在代码运行前加载。
因此,我们将按照以下顺序查看日志:
helper.js, index.js, 3
如果希望在导入声明之前执行某些代码,可考虑将其移至单独的文件中:
// new index.js
import './logs.js';
import { sum } from './helper.js';
console.log(sum(1, 2));
logs.js
console.log('index.js');
现在我们有了预期的输出结果:
index.js, helper.js, 3
index.mjs
// index.mjs
import './module.mjs';
import { num } from './counter.mjs';
console.log('index num =', num);
module.mjs
// module.mjs
import { num } from './counter.mjs';
console.log('module num =', num);
counter.mjs
// counter.mjs
if (!globalThis.num) {
globalThis.num = 0;
}
export const num = ++globalThis.num;
Modules are singletons. 模块是单例。
无论从同一位置或不同位置导入模块多少次,模块都只会被执行和加载一次。换句话说,模块实例只有一个。
index.mjs
// index.mjs
import './module.mjs?param=5;'
module.mjs
// module.mjs
console.log(import.meta.url);
这个问题如果没有对ES6有比较深的理解,就不太好回答出来。
根据 MDN:
import.meta
对象为 JavaScript 模块提供特定于上下文的元数据。它包含有关模块的信息。
它返回一个带有 url 属性的对象,url 属性表示模块的基本 URL。对于外部脚本,url 将是获取脚本的 URL;对于内嵌脚本,url 将是包含脚本的文档的基本 URL。
请注意,这将包括查询参数和/或哈希值(即,跟在 "?" 或 "#" 之后的部分)。
index.js
import myCounter from './counter';
myCounter += 1;
console.log(myCounter);
counter.js
// counter.js
let counter = 5;
export default counter;
另一个大多数开发者容易忽视的非常重要的点是,在导入模块的作用域中,导入的变量表现得像常量。
为了使代码正常工作,我们可以导出一个对象,例如,并更改其属性。
// index.mjs
import foo from './module.mjs';
console.log(typeof foo);
// module.mjs
foo = 25;
export default function foo() {}
首先,这
export default function foo() {}
等于
function foo() {}
export { foo as default }
这也等于
function foo() {}
export default foo
现在是时候回想起函数是如何被提升的,以及变量的初始化总是在函数/变量声明之后进行。
引擎处理完模块代码后,看起来是这样的:
function foo() {}
foo = 25;
export { foo as default }
因此,测验结果就是 number
。
// index.mjs
import defaultFoo, { foo } from './module.mjs';
setTimeout(() => {
console.log(foo);
console.log(defaultFoo);
}, 2000);
// module.mjs
let foo = 'bar';
export { foo };
export default foo;
setTimeout(() => {
foo = 'baz';
}, 1000);
在大多数情况下,导入的数据是实时的。也就是说,如果导出的值发生了变化,这种变化会反映在导入的变量上。
但默认导出并非如此:
export default foo;
使用这种语法时,导出的不是变量,而是变量值。可以像这样导出默认值,而无需使用变量:
export default ‘hello’;
export default 42;
如果查看测验 #1 中使用导出语法的表格,就会发现 export default function () {}
与 export default foo ( Export of values )
所处的列 ( Default export ) 不同。
这是因为它们的行为方式不同,函数仍然作为活引用传递:
// module.mjs
export { foo };
export default function foo() {};
setTimeout(() => {
foo = 'baz';
}, 1000);
// index.mjs
import defaultFoo, { foo } from './module.mjs';
setTimeout(() => {
console.log(foo); // baz
console.log(defaultFoo); //baz
}, 2000);
export { foo as default };
位于 Named Export 列,与这两列都不同。但对我们来说,唯一重要的是它不在 Export of values 列中。因此,这意味着当以这种方式导出数据时,它将与导入值进行实时绑定。
// index.mjs
import { shouldLoad } from './module1.mjs';
let num = 0;
if (shouldLoad) {
import { num } from './module2.mjs';
}
console.log(num);
// module1.mjs
export const shouldLoad = true;
//module2.mjs
export const num = 1;
import { num } from ‘./module2.mjs’;
行将会出错,因为导入结构必须位于脚本的顶层:
SyntaxError: Unexpected token ‘{‘
这是一个重要的限制,加上在文件路径中使用变量的限制,使得 ES6 模块成为静态模块。这意味着,与 Node.js 中使用的 Common.js 模块不同,不必执行代码就能找出模块之间的所有依赖关系。
在这个使用 Common.js
模块的示例中,要确定将加载 a
或 b
模块,需要运行以下代码:
let result;
if (foo()) {
result = require('a');
} else {
result = require('b');
}
模块的静态特性有很多好处。以下是其中一些:
总是知道导入数据的确切结构。这有助于在执行代码前发现错别字。
异步加载。这是因为模块是静态的,可以在执行模块主体之前加载导入。
支持循环依赖关系。我们将在下一次测验中详细探讨这种可能性。
高效捆绑。在此不多赘述,您可以在本文中自行了解 Rollup 捆绑程序如何有效地构建 ES6 模块。
// index.mjs
import { double, square } from './module.mjs';
export function calculate(value) {
return value % 2 ? square(value) : double(value);
}
// module.mjs
import { calculate } from './index.mjs';
export function double(num) {
return num * 2;
}
export function square(num) {
return num * num;
}
console.log(calculate(3));
在上面的代码中,我们可以看到循环依赖关系: index.mjs
从 module.mjs
导入 double
和 square
函数,而 module.mjs
从 index.mjs
导入 calculation
函数。
这段代码之所以能运行,是因为 ES6 模块本质上非常支持循环依赖关系。例如,如果我们将这段代码改写为使用 Common.js 模块,它将不再工作:
// index.js
const helpers = require('./module.js');
function calculate(value) {
return value % 2 ? helpers.square(value) : helpers.double(value);
}
module.exports = {
calculate
}
// module.js
const actions = require('./index.js');
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
console.log(actions.calculate(3)); // TypeError: actions.calculate is not a function
module.exports = {
double,
square
}
index.js
开始加载
加载在第一行中断,以加载 module.js
:const helpers = require(‘./module.js’);
module.js 开始加载
在 console.log(actions.calculate(3));
行中,由于 actions.calculate 未定义,代码出错。这是因为 Common.js 同步加载模块。 index.js 尚未加载,其导出对象目前为空。
如果调用一个带延迟的导入函数, index.js
模块将有时间加载,代码也将相应地工作:
// module.js
const actions = require('./index.js');
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
function someFunctionToCallLater() {
console.log(actions.calculate(3)); // Works
}
module.exports = {
double,
square
}
从前面的测验中了解到的,ES6 模块支持循环依赖关系,因为它们是静态的--模块的依赖关系在代码执行之前就已加载。
使上述代码工作的另一个因素是提升。当调用 calculate
函数时,我们还没有进入定义该函数的行。
下面是捆绑模块后的代码:
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
console.log(calculate(3));
function calculate(value) {
return value % 2 ? square(value) : double(value);
}
如果没有变量提升,它将无法工作。
如果我们将计算声明函数改为函数表达式:
export let calculate = function(value) {
return value % 2 ? square(value) : double(value);
}
会出现以下错误:
ReferenceError: Cannot access ‘calculate’ before initialization
// index.mjs
import { num } from './module.mjs';
console.log(num);
export let num = 0;
num = await new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
});
顶层 await
是一个非常有用的特性,许多开发者都不了解,也许是因为它是在最近的 ECMAScript 2022 中才引入的。啊,真不错!
顶层 await 使模块能够像大型异步函数一样运作:通过顶层 await,ECMAScript 模块(ESM)可以等待资源,导致导入它们的其他模块在开始评估其主体之前必须等待。
模块的标准行为是,在加载模块导入的所有模块并执行其代码之前,模块中的代码不会被执行(参见测验 #2)。事实上,随着顶级等待的出现,一切都没有改变。模块中的代码不会被执行,直到所有导入模块中的代码都被执行,只是现在这包括等待模块中所有等待的承诺被解决。
// index.js
console.log('index.js');
import { num } from './module.js';
console.log('num = ', num);
// module.js
export let num = 5;
console.log('module.js');
await new Promise((resolve) => {
setTimeout(() => {
console.log('module.js: promise 1');
num = 10;
resolve();
}, 1000);
});
await new Promise((resolve) => {
setTimeout(() => {
console.log('module.js: promise 2');
num = 20;
resolve();
}, 2000);
});
输出:
module.js
module.js: promise 1
module.js: promise 2
index.js
num = 20
如果我们删除 module.js
中第 5 行和第 13 行的等待,并在文件 index.js 中添加超时,就会像这样:
console.log('index.js');
import { num } from './module.js';
console.log('num = ', num);
setTimeout(() => {
console.log('timeout num = ', num);
}, 1000);
setTimeout(() => {
console.log('timeout num = ', num);
}, 2000);
输出:
module.js
index.js
num = 5
module.js: promise 1
timeout num = 10
module.js: promise 2
timeout num = 20
我们将在今后的测验中再次使用顶级等待功能。
//index.mjs
import { shouldLoad } from './module1.mjs';
let num = 0;
if (shouldLoad) {
({ num } = import('./module2.mjs'));
}
console.log(num);
// module1.mjs
export const shouldLoad = true;
//module2.mjs
export const num = 1;
import()
调用(通常称为动态导入)是一种类似函数的表达式,它允许异步动态加载 ECMAScript 模块。它允许绕过导入声明的语法限制,有条件或按需加载模块。
该功能在 ES2020 中引入。
import(module)
返回一个 promise ,该承诺会履行到一个包含模块所有输出的对象。由于 import(module)
返回的是一个 promise,为了修正测验代码,我们必须在导入调用之前添加 await 关键字:
if (shouldLoad) {
({ num } = await import('./module2.mjs'));
}
在这里,我们再次使用顶层 await
,这让我们想起了这一功能的酷炫之处。
我敢肯定,你的应用程序至少有一次出错崩溃了:
SyntaxError: await is only valid in async functions
当试图从全局作用域调用异步函数时,经常会出现这种情况。为了解决这个问题,我们必须躲避丑陋的代码:
(async () => {
await [someAsyncFunc]();
})();
这不仅难看,而且在使用此模式异步加载模块时可能会导致错误。例如
// module1.mjs
let num;
(async () => {
({ num } = await import(‘./module2.mjs’));
})();
export { num };
// module2.mjs
export const num = 5;
导入 module1.mjs
时, num
的结果会是什么 - 来自 module2
或 undefined
的值?这取决于何时访问变量:
import { num } from './module1.mjs';
console.log(num); // undefined
setTimeout(() => console.log(num), 100); // 5
有了顶级 await ,只要您访问从 module1
导入的 num
,它就永远不会是 undefined
:
let { num } = await import('./module2.mjs');
export { num };
import { num } from './module1.mjs';
console.log(num); // 5
// index.mjs
const module1 = await import('./module1.mjs');
const module2 = await import('./module2.mjs');
console.log(module1, module2);
function multiply(num1, num2) { return num1 * num2; }
console.log(multiply(module1, module2));
// module1.mjs
export default await new Promise((resolve) => resolve(1));
// module2.mjs
export default await new Promise((resolve) => resolve(2));
上述代码会出错:
TypeError: Cannot convert object to primitive value
同意,一个相当意外的错误措辞。让我们来看看这个错误从何而来。
在这段代码中,我们使用了动态导入,这在前面的示例中已经介绍过。要理解这段代码中的问题,我们需要仔细看看 import()
的返回值。
变量 module1
和 module2
的值与我们的预期不同。 import()
返回一个 promise ,该promise 将实现一个与命名空间导入形状相同的对象:
import * as name from moduleName
default
输出可作为名为 default
的键使用。
因此,在变量 module1
和 module2
中分别有对象 { default: 1 }
和 { default: 2 }
,而不是值 1
和 2
。
那么,为什么两个对象相乘时会出现如此奇怪的错误,而不是我们习惯的 NaN
呢?
这是因为返回的对象具有 null
原型。因此,它没有用于将对象转换为基元的 toString()
方法。如果这个对象有一个 Object
原型,我们就会在控制台中看到 NaN
。
要修复测验代码,我们需要做以下更改:
console.log(pow(module1.default, module2.default));
或
const { default: module1 } = await import('./module1.mjs');
const { default: module2 } = await import('./module2.mjs');
// index.js
import * as values from './intermediate.js';
console.log(values.x);
// module1.js
export const x = 1;
// module2.js
export const x = 2;
// intermediate.js
export * from './module1.js';
export * from './module2.js';
export * from ‘module’
语法会将 "模块"文件中所有已命名的导出内容重新导出为当前文件中已命名的导出内容。如果存在多个同名导出,则不会重新导出其中任何一个。
因此,运行这段代码时,我们会在控制台中看到 undefined 。只有 17% 的答题者回答正确,大多数答题者(59%)认为这段代码会出错。事实上,这种无声的失败似乎并不是严格模式的典型表现。(如果您知道这种行为的原因,请在评论中告诉我。
顺便提一下,如果在同样的情况下显式导入 x ,就会出现预期的错误:
import { x } from ‘./intermediate.js’;
SyntaxError: The requested module ‘./intermediate.js’ contains conflicting star exports for name ‘x’
冲
欢迎长按图片加刷碗智为好友,定时分享 Vue React Ts 等。
最后:
vue2与vue3技巧合集
VueUse源码解读