首发于公众号 大迁世界,欢送关注。 每周一篇实用的前端文章 ️ 分享值得关注的开发工具 分享集体守业过程中的趣事

快来收费体验ChatGpt plus版本的,咱们出的钱 体验地址:https://chat.waixingyun.cn能够退出网站底部技术群,一起找bug,...

如果你问开发人员:"对你来说最难的 JS 题目是什么?",你绝不会听到他说是 ES6 模块。但统计数据更能阐明问题!咱们统计了咱们电报频道中各种主题的问答谬误答案数量,发现 ES6 模块是最难的主题之一。

测验 #1: 53%的答案正确

// index.mjsimport { default } from './module.mjs';console.log(default);
// module.mjsexport default 'bar';

首先,让咱们记住各种导入和导出语法:

如果检查表中的 Import 语法,就会发现没有与咱们的代码相匹配的语法:

import { default } from ‘./module.mjs’;

因为禁止应用这种语法。测验代码会呈现以下谬误:

SyntaxError: Unexpected reserved word

import { default } from ‘./module.mjs’; 行中, defaultexport 的名称,也是该作用域中的变量名称,这是被禁止的,因为 default 是一个保留字。解决办法很简略:

import { default as foo } from ‘./module.mjs’;

当初, default 是导出的名称, foo 是变量的名称。换句话说,如果你想在默认导出中应用命名导入语法,就必须重命名它。就是这样,非常简单!

测验 #2:35% 的正确答案

// index.jsconsole.log('index.js');import { sum } from './helper.js';console.log(sum(1, 2));
// helper.jsconsole.log('helper.js');export const sum = (x, y) => x + y;

没有多少开发人员晓得的一个重要的细微差别是,导入是被晋升的。也就是说,在引擎解析代码时,导入就会被加载。所有依赖项都将在代码运行前加载。

因而,咱们将依照以下程序查看日志:

helper.js, index.js, 3

如果心愿在导入申明之前执行某些代码,可思考将其移至独自的文件中:

// new index.jsimport './logs.js';import { sum } from './helper.js';console.log(sum(1, 2));

logs.js

console.log('index.js');

当初咱们有了预期的输入后果:

index.js, helper.js, 3

测验 #3:42% 的正确答案

index.mjs

// index.mjsimport './module.mjs';import { num } from './counter.mjs';console.log('index num =', num);

module.mjs

// module.mjsimport { num } from './counter.mjs';console.log('module num =', num);

counter.mjs

// counter.mjsif (!globalThis.num) {  globalThis.num = 0;}export const num = ++globalThis.num;

Modules are singletons. 模块是单例。

无论从同一地位或不同地位导入模块多少次,模块都只会被执行和加载一次。换句话说,模块实例只有一个。

测验 #4:34% 的正确答案

index.mjs

// index.mjsimport './module.mjs?param=5;'

module.mjs

// module.mjsconsole.log(import.meta.url);

这个问题如果没有对ES6有比拟深的了解,就不太好答复进去。

依据 MDN:

import.meta 对象为 JavaScript 模块提供特定于上下文的元数据。它蕴含无关模块的信息。

它返回一个带有 url 属性的对象,url 属性示意模块的根本 URL。对于内部脚本,url 将是获取脚本的 URL;对于内嵌脚本,url 将是蕴含脚本的文档的根本 URL。

请留神,这将包含查问参数和/或哈希值(即,跟在 "?" 或 "#" 之后的局部)。

测验 #5:45% 的正确答案

index.js

import myCounter from './counter';myCounter += 1;console.log(myCounter);

counter.js

// counter.jslet counter = 5;export default counter;

另一个大多数开发者容易漠视的十分重要的点是,在导入模块的作用域中,导入的变量体现得像常量。

为了使代码失常工作,咱们能够导出一个对象,例如,并更改其属性。

测验 #6:11%的正确答案

// index.mjsimport foo from './module.mjs';console.log(typeof foo);
// module.mjsfoo = 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

测验 #7:17%的正确答案

// index.mjsimport defaultFoo, { foo } from './module.mjs';setTimeout(() => {  console.log(foo);  console.log(defaultFoo);}, 2000);
// module.mjslet 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 列中。因而,这意味着当以这种形式导出数据时,它将与导入值进行实时绑定。

测验 #8: 40% 的正确答案

// index.mjsimport { shouldLoad } from './module1.mjs';let num = 0;if (shouldLoad) {  import { num } from './module2.mjs';}console.log(num);
// module1.mjsexport const shouldLoad = true;
//module2.mjsexport const num = 1;

import { num } from ‘./module2.mjs’; 即将会出错,因为导入构造必须位于脚本的顶层:

SyntaxError: Unexpected token ‘{‘

这是一个重要的限度,加上在文件门路中应用变量的限度,使得 ES6 模块成为动态模块。这意味着,与 Node.js 中应用的 Common.js 模块不同,不用执行代码就能找出模块之间的所有依赖关系。

在这个应用 Common.js 模块的示例中,要确定将加载 ab 模块,须要运行以下代码:

let result;if (foo()) {    result = require('a');} else {    result = require('b');}

模块的动态个性有很多益处。以下是其中一些:

  1. 总是晓得导入数据的确切构造。这有助于在执行代码前发现错别字。
  2. 异步加载。这是因为模块是动态的,能够在执行模块主体之前加载导入。
  3. 反对循环依赖关系。咱们将在下一次测验中具体探讨这种可能性。
  4. 高效捆绑。在此不多赘述,您能够在本文中自行理解 Rollup 捆绑程序如何无效地构建 ES6 模块。

测验 #9: 3% 的正确答案

// index.mjsimport { double, square } from './module.mjs';export function calculate(value) {  return value % 2 ? square(value) : double(value);}
// module.mjsimport { calculate } from './index.mjs';export function double(num) {  return num * 2;}export function square(num) {  return num * num;}console.log(calculate(3));

在下面的代码中,咱们能够看到循环依赖关系: index.mjsmodule.mjs 导入 doublesquare 函数,而 module.mjsindex.mjs 导入 calculation 函数。

这段代码之所以能运行,是因为 ES6 模块实质上十分反对循环依赖关系。例如,如果咱们将这段代码改写为应用 Common.js 模块,它将不再工作:

// index.jsconst helpers = require('./module.js');function calculate(value) {  return value % 2 ? helpers.square(value) : helpers.double(value);}module.exports = {  calculate}
// module.jsconst 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 functionmodule.exports = {  double,  square}
  1. index.js 开始加载
  2. 加载在第一行中断,以加载 module.jsconst helpers = require(‘./module.js’);
  3. module.js 开始加载
  4. console.log(actions.calculate(3)); 行中,因为 actions.calculate 未定义,代码出错。这是因为 Common.js 同步加载模块。 index.js 尚未加载,其导出对象目前为空。

如果调用一个带提早的导入函数, index.js 模块将有工夫加载,代码也将相应地工作:

// module.jsconst 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

测验 #10: 31%的正确答案

// index.mjsimport { 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.jsconsole.log('index.js');import { num } from './module.js';console.log('num = ', num);
// module.jsexport 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.jsmodule.js: promise 1module.js: promise 2index.jsnum = 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.jsindex.jsnum = 5module.js: promise 1timeout num = 10module.js: promise 2timeout num = 20

咱们将在今后的测验中再次应用顶级期待性能。

测验 #11: 16%的正确答案

//index.mjsimport { shouldLoad } from './module1.mjs';let num = 0;if (shouldLoad) {   ({ num } = import('./module2.mjs'));}console.log(num);
// module1.mjsexport const shouldLoad = true;
//module2.mjsexport 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.mjslet num;(async () => {    ({ num } = await import(‘./module2.mjs’));})();export { num };
// module2.mjsexport const num = 5;

导入 module1.mjs 时, num 的后果会是什么 - 来自 module2undefined 的值?这取决于何时拜访变量:

import { num } from './module1.mjs';console.log(num); // undefinedsetTimeout(() => 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

测验 #12: 21% 的正确答案

// index.mjsconst 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.mjsexport default await new Promise((resolve) => resolve(1));
// module2.mjsexport default await new Promise((resolve) => resolve(2));

上述代码会出错:

TypeError: Cannot convert object to primitive value

批准,一个相当意外的谬误措辞。让咱们来看看这个谬误从何而来。

在这段代码中,咱们应用了动静导入,这在后面的示例中曾经介绍过。要了解这段代码中的问题,咱们须要认真看看 import() 的返回值。

变量 module1module2 的值与咱们的预期不同。 import() 返回一个 promise ,该promise 将实现一个与命名空间导入形态雷同的对象:

import * as name from moduleName

default 输入可作为名为 default 的键应用。

因而,在变量 module1module2 中别离有对象 { default: 1 }{ default: 2 } ,而不是值 12

那么,为什么两个对象相乘时会呈现如此奇怪的谬误,而不是咱们习惯的 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');

测验 #13:17%的正确答案

// index.jsimport * as values from './intermediate.js';console.log(values.x);
// module1.jsexport const x = 1;
// module2.jsexport const x = 2;
// intermediate.jsexport * 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’

最初

交换

首发于公众号 大迁世界,欢送关注。 每周一篇实用的前端文章 ️ 分享值得关注的开发工具 ❓ 有疑难?我来答复

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。