JavaScript-入门书-Primer--五-

JavaScript 入门书(Primer)(五)

例外处理

原文:jsprimer.net/basic/error-try-catch/

本章将学习 JavaScript 中的异常处理。

try...catch 语句

*try...catch语句是用于标记可能发生异常的代码块,并描述异常发生时处理方式的语句。

当try...catch语句的try块中发生异常时,try块中的后续处理不会执行,处理将转移到catch块。catch块在try块内发生异常时,会与发生的错误对象一起调用。finally块无论try块内是否发生异常,都会在try语句的最后执行。

在下面的代码中,try块中发生异常,执行了catch块的处理,最后执行了finally块的处理。

try {
    console.log("try 節:この行は実行されます");
    // 未定義の関数を呼び出してReferenceError 例外が発生する
    undefinedFunction();
    // 例外が発生したため、この行は実行されません
} catch (error) {
    // 例外が発生したあとはこのブロックが実行される
    console.log("catch 節:この行は実行されます");
    console.log(error instanceof ReferenceError); // => true
    console.log(error.message); // => "undefinedFunction is not defined"
} finally {
    // このブロックは例外の発生に関係なく必ず実行される
    console.log("finally 節:この行は実行されます");
} 

此外,如果catch块和finally块中有一个存在,则可以省略另一个块。如果只写了finally块,则即使没有捕获到异常,finally块也会在执行后执行。

// catch 節のみ
try {
    undefinedFunction();
} catch (error) {
    console.error(error);
}
// finally 節のみ
try {
    undefinedFunction();
} finally {
    console.log("この行は実行されます");
}
// finally 節のみでは例外がキャッチされないため、この行は実行されません 

throw 语句

*throw语句可以使用户抛出异常。抛出的异常对象可以作为函数的参数在catch块中访问。在catch块中可以引用对象的标识符称为异常标识符。

在下面的代码中,使用catch块的error标识符引用了捕获到的错误对象。

try {
    // 例外を投げる
    throw new Error("例外が投げられました");
} catch (error) {
    // catch 節のスコープでerrorにアクセスできる
    console.log(error.message); // => "例外が投げられました"
} 

错误对象

*throw语句可以抛出任何对象作为异常。这里将探讨使用throw语句抛出的错误对象。

Error

*Error对象的实例可以通过new Error("错误消息")创建。构造函数的第一个参数是作为错误消息的字符串。传递的错误消息可以通过 Error 的message属性进行引用。

在下面的代码中,使用assertPositiveNumber函数创建错误对象,并将其作为异常抛出。抛出的对象可以通过catch块的异常标识符(error)获取,并可以检查错误消息。

// 渡された数値が0 以上ではない場合に例外を投げる関数
function assertPositiveNumber(num) {
    if (num < 0) {
        throw new Error(`${num} is not positive.`);
    }
}

try {
    // 0 未満の値を渡しているので、関数が例外を投げる
    assertPositiveNumber(-1);
} catch (error) {
    console.log(error instanceof Error); // => true
    console.log(error.message); // => "-1 is not positive."
} 

throw语句可以抛出任何对象作为异常,但通常推荐抛出Error对象的实例。原因将在后面讨论堆栈跟踪。Error对象在实例创建时,会包含有关实例创建的文件名或行数等对调试有用的信息。如果抛出不是Error对象的实例,如字符串等,则无法获取堆栈跟踪。

因此,不建议使用throw语句抛出非Error对象。

// 文字列を例外として投げるアンチパターンの例
try {
    throw "例外が投げられました";
} catch (error) {
    // catch 節の例外識別子は、投げられた値を参照する
    console.log(error); // => "例外が投げられました"
} 

内置错误

*错误有几种类型,这些类型被定义为内置错误。内置错误是指 ECMAScript 规范或执行环境中预定义的错误对象。可以抛出的内置错误对象是继承自Error对象的实例。因此,可以像用户定义的错误一样处理异常。

内置错误有多种类型,但这里将介绍一些代表性的。

ReferenceError

*ReferenceError是在尝试引用不存在的变量或函数等标识符时发生的错误。在下面的代码中,由于正在引用不存在的变量,因此抛出了ReferenceError异常。

try {
    // 存在しない変数を参照する
    console.log(x);
} catch (error) {
    console.log(error instanceof ReferenceError); // => true
    console.log(error.name); // => "ReferenceError"
    console.log(error.message); // エラーメッセージが表示される
} 

SyntaxError

*SyntaxError是在尝试解释非法代码时发生的错误。基本上,SyntaxError异常是在 JavaScript 执行前的解析阶段发生的。因此,在执行前发生的SyntaxError异常不能在执行时的try...catch语句中被捕获。

// JavaScriptとして正しくない構文をパースするとSyntaxErrorが発生する
foo! bar! 

在下面的代码中,使用eval函数强制在运行时发生SyntaxError,并确认语法错误是SyntaxError。

try {
    // eval 関数は渡した文字列をJavaScriptとして実行する関数
    // 正しくない構文をパースさせ、SyntaxErrorを実行時に発生させる
    eval("foo! bar!");
} catch (error) {
    console.log(error instanceof SyntaxError); // => true
    console.log(error.name); // => "SyntaxError"
    console.log(error.message); // エラーメッセージが表示される
} 

TypeError

*TypeError是在值不是期望的类型时发生的错误。在下面的代码中,由于正在调用函数的对象不是函数,因此抛出了TypeError异常。

try {
    // 関数ではないオブジェクトを関数として呼び出す
    const fn = {};
    fn();
} catch (error) {
    console.log(error instanceof TypeError); // => true
    console.log(error.name); // => "TypeError"
    console.log(error.message); // エラーメッセージが表示される
} 

抛出内置错误

*可以创建内置错误的实例,并将该实例作为异常抛出。与普通的Error对象一样,可以通过new每个内置错误对象来创建实例。

例如,如果要限制函数参数为字符串,可以抛出TypeError异常。即使不查看消息,仅凭错误名称就可以立即了解与类型相关的异常。

// 文字列を反転する関数
function reverseString(str) {
    if (typeof str !== "string") {
        throw new TypeError(`${str} is not a string`);
    }
    return Array.from(str).reverse().join("");
}

try {
    // 数値を渡す
    reverseString(100);
} catch (error) {
    console.log(error instanceof TypeError); // => true
    console.log(error.name); // => "TypeError"
    console.log(error.message); // => "100 is not a string"
} 

错误和调试

*在 JavaScript 开发中,理解调试过程中发生的错误非常重要。通过利用错误提供的信息,可以了解源代码中发生异常的位置和类型。

所有错误都是由扩展自Error对象的对象声明的。也就是说,错误具有表示名称的name属性和表示内容的message属性。通过检查这两个属性,可以在许多情况下提供开发帮助。

在下面的代码中,未包含在try...catch块中的部分发生了异常。

function fn() {
    // 存在しない変数を参照する
    x++;
}
fn(); 

加载此脚本后,控制台将输出有关抛出异常的日志。以下是 Firefox 中的执行示例。

控制台中的错误显示(Firefox)

此错误日志包含以下信息。

消息 含义
ReferenceError: x is not defined 错误类型为ReferenceError,表示x未定义。
error.js:3:5 异常发生在error.js的第 3 行第 5 列。也就是x++;。

此外,消息后面还显示了异常的堆栈跟踪。堆栈跟踪记录了程序执行过程,指出了哪些处理导致错误。

  • 堆栈跟踪的第一行是实际发生异常的位置。也就是说,在第 3 行的 x++; 发生了异常

  • 接下来的行记录了该代码的调用者。也就是说,执行第 3 行代码的是第 5 行的fn函数调用

堆栈跟踪记录了调用者的信息。

控制台显示的错误日志包含大量信息。MDN 的JavaScript 错误参考详细列出了浏览器抛出的内置错误类型和消息。在开发过程中遇到内置错误时,查阅参考以寻找解决方法是一个好习惯。

console.error和堆栈跟踪

*console.error方法可以将消息与堆栈跟踪一起输出到控制台。

运行以下代码,比较console.log和console.error的输出结果。

function fn() {
    console.log("メッセージ");
    console.error("エラーメッセージ");
}

fn(); 

在 Firefox 中运行此代码,控制台输出将如下图所示。

console.log 和 console.error 的输出结果

console.log只有消息,而console.error除了消息外还输出堆栈跟踪。因此,在控制台输出错误消息时,使用console.error可以更容易进行调试。

此外,大多数浏览器都提供了过滤console.log和console.error输出的功能。通过使用console.log进行普通日志输出,使用console.error进行与错误相关的日志输出,可以更容易区分日志的重要性。

[ES2022] 错误原因

*通过捕获错误并重新抛出带有新消息的另一个错误,可以提供有用的调试信息。通过创建新的 Error 对象并抛出来实现。但是,这种方法会导致原始错误的堆栈跟踪丢失。

function somethingWork() {
    throw new Error("本来のエラー");
}

try {
    somethingWork();
} catch (error) {
    // `error` が持っていたスタックトレースが失われるため、実際にエラーが発生した場所がわからなくなる 
    throw new Error("somethingWork 関数でエラーが発生しました");
} 

要解决堆栈跟踪丢失的问题,可以使用 ES2022 中引入的 Error 的cause选项。在创建新错误对象时,通过将原始错误对象传递给第二个参数的cause选项,可以保留原始的堆栈跟踪。

// 数値の文字列を受け取り数値を返す関数
// 'text' など数値にはならない文字列を渡された場合は例外を投げられる
function safeParseInt(numStr) {
    const num = Number.parseInt(numStr, 10);
    if (Number.isNaN(num)) {
        throw new Error(`${numStr} is not a numeric`);
    }
    return num;
}

// 数字の文字列を二つ受け取り、合計を返す関数
function sumNumStrings(a, b) {
    try {
        const aNumber = safeParseInt(a);
        const bNumber = safeParseInt(b);
        return aNumber + bNumber;
    } catch (e) {
        throw new Error("Failed to sum a and b", { cause: e });
    }
}

try {
    // 数値にならない文字列 'string' を渡しているので例外が投げられる
    sumNumStrings("string", "2");
} catch (err) {
    console.error(err);
} 

加载此脚本后,控制台将输出带有从sumNumsStrings到safeParseInt抛出的堆栈跟踪的错误日志。以下是 Firefox 中的执行示例。

包含 safeParseInt 堆栈跟踪的 console.error 输出结果

总结

*本章介绍了异常处理和错误对象。

  • try...catch语法可以处理try块中发生的异常

  • catch块和finally块可以同时或单独编写

  • throw语句可以抛出异常,将Error对象作为异常抛出

  • Error对象包含了在 ECMAScript 规范和执行环境中定义的内置错误

  • Error对象中记录了堆栈跟踪,对调试非常有用

  • 使用 Error Cause,可以创建一个新错误,继承另一个错误的堆栈跟踪*************

异步处理:Promise/Async Function

原文:jsprimer.net/basic/async/

本章我们将学习 JavaScript 的异步处理。异步处理是 JavaScript 中一个非常重要的概念。此外,由于浏览器和 Node.js 等 API 中有些内容只能通过异步处理来处理,因此无法避免异步处理。JavaScript 有用于处理异步处理的内置对象 Promise,以及称为 Async Function 的语法支持。

本章将从异步处理是什么开始,探讨异步处理中的异常处理和异步处理的方法。

同步处理

*许多编程语言将代码的评估方式分为同步处理(sync)和异步处理(async)两大类。

之前编写的代码被称为同步处理。在同步处理中,代码按顺序处理,一个处理完成后再进行下一个处理。因此,同步处理中正在执行的处理只有一个,这使得操作非常直观。

另一方面,如果执行了同步阻塞处理,则会出现问题。因为同步处理中,一个处理完成之前,无法进行下一个处理。

下面的代码中的blockTime函数执行指定的timeout毫秒的无限循环,这是一个同步阻塞处理。使用Date.now方法返回自 1970 年 1 月 1 日午夜 0 时 0 分 0 秒以来经过的毫秒数来判定timeout毫秒是否已过。调用blockTime函数时,直到指定时间过去,下一个处理(任务 B)不会被调用。

function taskA() {
    console.log("タスクAを実行 at " + Date.now());
}
function taskB() {
    console.log("タスクBを実行 at " + Date.now());
}
// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
    // Date.now()は現在の時間をUnix 時間(1970 年 1 月 1 日午前 0 時 0 分 0 秒から経過した時間)のミリ秒を返す
    const startTime = Date.now();
    // `timeout`ミリ秒経過するまで無限ループをする
    while (true) {
        const diffTime = Date.now() - startTime;
        if (diffTime >= timeout) {
            return; // 指定時間経過したら関数の実行を終了
        }
    }
}
taskA();
blockTime(1000); // 他の処理を1000ミリ秒(1 秒間)ブロックする
taskB(); 

如果同时进行阻塞处理,在浏览器中会引发大问题。这是因为 JavaScript 基本上是在浏览器的主线程(也称为 UI 线程)中执行的。主线程还负责执行与 UI 相关的处理,例如显示更新。因此,如果主线程被 JavaScript 的处理所占用,则显示将无法更新,看起来就像冻结了一样。

single-thread-tasks

在之前的例子中,处理被阻塞了 1 秒钟,因此会导致 1 秒钟内的滚动等操作无法执行。

异步处理

*异步处理按顺序处理代码,但不会等待一个异步处理完成就评估下一个处理。也就是说,在异步处理中,同时执行的处理有多个。

在 JavaScript 中,setTimeout函数是异步处理的代表性函数之一。setTimeout函数将回调函数注册到计时器,以便在delay毫秒后调用。

setTimeout(コールバック関数, delay); 

在下面的代码中,表面上看起来是任务 A → 异步任务 → 任务 B 的顺序。然而,使用setTimeout函数将异步任务注册为 1000 毫秒(1 秒)后执行。因此,实际上执行顺序是任务 A → 任务 B → 异步任务。

function taskA() {
    console.log("タスクAを実行 at " + Date.now());
}
function taskB() {
    console.log("タスクBを実行 at " + Date.now());
}
function taskAsync() {
    console.log("非同期のタスクを実行 at " + Date.now());
}

taskA();
setTimeout(() => {
    taskAsync();
}, 1000);
taskB(); 

因此,异步处理(setTimeout的回调函数)的执行顺序与代码的表面顺序不同。

异步处理与计时器

异步处理是在主线程中执行的

*正如之前所介绍的,主线程(也称为 UI 线程)是执行重型的 JavaScript 同步处理,这会阻塞其他处理(如屏幕更新)的问题。在异步处理中也有类似的问题。这是因为 JavaScript 中的大部分异步处理都是在主线程中执行的。

虽然从名称上看,异步处理似乎是在主线程之外执行的,但基本上异步处理也像同步处理一样在主线程中执行。在本节中,我们将简要地了解异步处理是如何在主线程中执行的。

下面的代码使用setTimeout函数注册了计时器的回调函数,并测量了实际花费的时间。使用setTimeout函数注册了 10 毫秒后执行异步任务的异步任务,并在其之后立即调用了一个持续 1 秒钟的同步阻塞处理。运行此代码时,由于同步阻塞处理,异步任务比注册到计时器的时间(10 毫秒后)晚得多地被调用。

function taskA() {
    console.log("タスクAを実行 at " + Date.now());
}
function taskB() {
    console.log("タスクBを実行 at " + Date.now());
}
function taskAsync() {
    console.log("非同期のタスクを実行 at " + Date.now());
}
// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
    const startTime = Date.now();
    while (true) {
        const diffTime = Date.now() - startTime;
        if (diffTime >= timeout) {
            return; // 指定時間経過したら関数の実行を終了
        }
    }
}

const startTime = Date.now();
taskA();
// 10ミリ秒後にコールバック関数を呼び出すようにタイマーに登録する
setTimeout(() => {
    const endTime = Date.now();
    taskAsync();
    console.log(`非同期処理のコールバックが呼ばれるまで${endTime - startTime}ミリ秒かかりました`);
}, 10);
blockTime(1000); // 1 秒間処理をブロックする
taskB(); 

如果异步处理(setTimeout的回调函数)在主线程之外的线程中执行,那么这个异步处理不应该受到主线程中同步阻塞处理的影响。然而,实际上,这个异步处理也受到主线程中同步阻塞处理的影响。

在许多环境中,异步处理回调函数被调用之前可能需要 1 秒钟以上。这是因为虽然可以注册在 10 毫秒后执行异步回调函数的计时器,但由于同步阻塞处理,异步任务的执行也会推迟。因此,从异步处理也受到同步处理影响的事实来看,可以知道它们是在同一个主线程中执行的。

异步处理与阻塞的同步处理

在 JavaScript 中,除了少数例外,异步处理被视为并行处理(Concurrent)。并行处理是指将处理分成一定的单位,在执行处理的同时切换处理的过程。因此,如果在执行异步处理之前有非常重的处理,就会导致异步处理的执行延迟。

但是,在某些执行环境中,异步处理中也可能存在可以在主线程之外执行的 API。 例如,在浏览器中,可以使用Web Worker API 在主线程之外执行 JavaScript。 在这个 Web Worker 中的异步处理是并行处理(Parallel)。 并行处理是指同时执行多个排他性处理。

Web Worker 在主线程之外的不同 Worker 线程中执行,因此即使在 Worker 线程中执行同步阻塞处理,也不会影响主线程。 但是,Web Worker 和主线程之间的数据交互需要使用postMessage方法。 因此,虽然可以将重处理移动到 Worker 中,但主线程和 Worker 线程之间的数据交互存在限制,这是一种权衡的关系。

这样,不能一概而论所有非同步处理,但了解基本非同步处理(如定时器等)是在主线程中执行的这一性质是很重要的。 JavaScript 的大部分非同步处理都是非同步执行的处理。 本书介绍的非同步处理大多数是在主线程中切换处理而执行的并行处理(Concurrent)。

非同步处理与异常处理

*介绍了非同步处理与同步处理流程的不同之处。 这对非同步处理中的异常处理也有很大的影响。

在同步处理中,可以使用try...catch结构来捕获同步发生的异常(详细信息请参考“异常处理”章节)。

try {
    throw new Error("同期的なエラー");
} catch (error) {
    console.log("同期的なエラーをキャッチできる");
}
console.log("この行は実行されます"); 

在非同步处理中,即使使用try...catch结构也无法捕获非同步发生的异常。 下面的代码在 10 毫秒后引发了一个非同步错误。 但是,try...catch结构无法捕获如下非同步错误。

try {
    setTimeout(() => {
        throw new Error("非同期的なエラー");
    }, 10);
} catch (error) {
    // 非同期エラーはキャッチできないため、この行は実行されません
}
console.log("この行は実行されます"); 

try块是捕获该块内发生的异常的结构。 但是,使用setTimeout函数注册的回调函数实际上执行并抛出异常是在所有同步处理完成之后。 也就是说,标记为范围外的try块中可能会发生异常。

因此,setTimeout函数的回调函数中的异常需要按如下方式在回调函数内以同步错误的形式捕获。

// 非同期処理の外
setTimeout(() => {
    // 非同期処理の中
    try {
        throw new Error("エラー");
    } catch (error) {
        console.log("エラーをキャッチできる");
    }
}, 10);
console.log("この行は実行されます"); 

这样可以在回调函数内捕获错误,但非同步处理之外无法知道是否在非同步处理中发生了异常。 要知道非同步处理之外是否发生了异常,需要有一种方法将非同步处理中发生的异常传递到非同步处理之外。

此外,JavaScript 中的 HTTP 请求或文件读写等处理也提供了作为异步处理 API。 这些异步处理在连接不到网络或文件不存在的情况下会失败。 因此,异步处理中的异常处理(错误处理)非常重要。

非同步处理中发生的异常的处理方法有很多种。 本章主要介绍主要非同步处理和异常处理方法,主要关注 Promise 和 Async Function 两种。

Async Function 是建立在 Promise 之上的语法结构。 因此,理解两者都很重要,而不是只理解其中之一。

[ES2015] Promise

*Promise是 ES2015 中引入的用于表示异步处理状态或结果的内置对象。 异步处理返回 Promise 实例,可以在 Promise 实例上注册状态变化时调用的回调函数。

下面的代码是一个处理 Promise 的异步处理的示例。

  • 执行异步处理的部分(asyncPromiseTask函数):返回 Promise 的实例

  • 处理异步处理结果的部分:接收 Promise 的实例,通过回调函数注册成功时的处理和失败时的处理

// asyncPromiseTask 関数は、Promiseインスタンスを返す
function asyncPromiseTask() {
    return new Promise((resolve, reject) => {
        // さまざまな非同期処理を行う
        // 非同期処理に成功した場合は、resolveを呼ぶ
        // 非同期処理に失敗した場合は、rejectを呼ぶ
    });
}
// asyncPromiseTask 関数の非同期処理が成功した時、失敗した時に呼ばれる処理をコールバック関数として登録する
asyncPromiseTask().then(()=> {
    // 非同期処理が成功したときの処理
}).catch(() => {
    // 非同期処理が失敗したときの処理
}); 

asyncPromiseTask函数返回Promise这个内置对象的实例。 Promise实例表示asyncPromiseTask函数内进行的异步处理是成功还是失败的状态。 此外,可以通过对Promise实例使用then或catch方法,将成功时或失败时调用的处理注册为回调函数,以处理异步处理的结果。

Promise 看起来可能有些复杂,但它是用于封装异步处理的状态或结果的类似对象。 同步函数执行函数后立即得到结果,而非同步函数执行函数后不会立即得到结果。 因此,非同步函数返回一个封装了异步处理状态的 Promise 对象,并在结果确定时将结果传递给之前注册的回调函数。

实际上,让我们看看Promise的使用方法。

Promise实例的创建

*使用new运算符创建Promise实例,此时需要传递一个名为executor的函数作为参数,该函数接受resolve和reject两个参数。 在executor函数中执行非同步处理,如果非同步处理成功,则调用resolve函数,如果失败,则调用reject函数。

const executor = (resolve, reject) => {
    // 非同期の処理が成功したときはresolveを呼ぶ
    // 非同期の処理が失敗したときはrejectを呼ぶ
};
const promise = new Promise(executor); 

使用此Promise实例的then方法,您可以注册在 Promiseresolve(成功)或reject(失败)时调用的回调函数。 将成功时的回调函数传递给then方法的第一个参数,将失败时的回调函数传递给then方法的第二个参数。

// `Promise`インスタンスを作成
const promise = new Promise((resolve, reject) => {
    // 非同期の処理が成功したときはresolve()を呼ぶ
    // 非同期の処理が失敗したときにはreject()を呼ぶ
});
const onFulfilled = () => {
    console.log("resolveされたときに呼ばれる");
};
const onRejected = () => {
    console.log("rejectされたときに呼ばれる");
};
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
promise.then(onFulfilled, onRejected); 

Promise构造函数的resolve和reject,以及then方法的onFulfilled和onRejected之间有如下关系。

  • 当resolve(成功)时

    • 将调用onFulfilled。
  • 当发生reject(失败)时

    • 将调用onRejected

Promise.prototype.then 和 Promise.prototype.catch

*因为以前没有像 Promise 这样将函数传递给构造函数的模式,所以我们将介绍具体的then方法用法示例。 同时,我们还将研究作为then方法别名的catch方法。

下面的代码中的dummyFetch函数创建并返回一个Promise实例。 如果成功获取资源,则调用resolve函数;如果失败,则调用reject函数。

传递给resolve的值将传递给then方法的第一个回调函数(onFulfilled)。 传递给reject的错误对象将传递给then方法的第二个回调函数(onRejected)。

/**
 * 1000ミリ秒未満のランダムなタイミングでレスポンスを疑似的にデータ取得する関数
 * 指定した`path`にデータがある場合、成功として**Resolved**状態のPromiseオブジェクトを返す
 * 指定した`path`にデータがない場合、失敗として**Rejected**状態のPromiseオブジェクトを返す
 */
function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/success")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
// /success/data のリソースは存在するので成功しonFulfilledが呼ばれる
dummyFetch("/success/data").then(function onFulfilled(response) {
    console.log(response); // => { body: "Response body of /success/data" }
}, function onRejected(error) {
    // この行は実行されません
});
// /failure/data のリソースは存在しないのでonRejectedが呼ばれる
dummyFetch("/failure/data").then(function onFulfilled(response) {
    // この行は実行されません
}, function onRejected(error) {
    console.error(error); // Error: "NOT FOUND"
}); 

Promise 的then方法接受两个回调函数参数:成功(onFulfilled)和失败(onRejected),但两者都可以省略。

下面的代码中的delay函数返回一个在一定时间后解决(resolve)的Promise实例。 在这个Promise实例上,仅注册了成功时的回调函数。

function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve();
        }, timeoutMs);
    });
}
// `then`メソッドで成功時のコールバック関数だけを登録
delay(10).then(() => {
    console.log("10ミリ秒後に呼ばれる");
}); 

另一方面,then方法也可以仅注册失败时的回调函数。 在这种情况下,第一个参数应传递undefined,如then(undefined, onRejected)。 Promise 提供了catch方法,作为执行类似操作的替代方法,即then(undefined, onRejected)。

在下面的代码中,我们使用then方法和catch方法处理失败的错误,但两者的含义相同。 由于向then方法传递undefined不够直观,因此建议仅在注册失败处理时使用catch方法。

function errorPromise(message) {
    return new Promise((resolve, reject) => {
        reject(new Error(message));
    });
}
// 非推奨: `then`メソッドで失敗時のコールバック関数だけを登録
errorPromise("thenでエラーハンドリング").then(undefined, (error) => {
    console.log(error.message); // => "thenでエラーハンドリング"
});
// 推奨: `catch`メソッドで失敗時のコールバック関数を登録
errorPromise("catchでエラーハンドリング").catch(error => {
    console.log(error.message); // => "catchでエラーハンドリング"
}); 

Promise 和异常

*如果在构造函数中发生异常,则会自动捕获异常。 发生异常的Promise实例将被视为失败,就像调用reject函数一样。 因此,如果 Promise 内部发生异常,则将调用then方法的第二个参数或catch方法中注册的错误处理回调函数。

function throwPromise() {
    return new Promise((resolve, reject) => {
        // Promiseコンストラクタの中で例外は自動的にキャッチされrejectを呼ぶ
        throw new Error("例外が発生");
        // 例外が発生すると、これ以降のコンストラクタの処理は実行されません
    });
}

throwPromise().catch(error => {
    console.log(error.message); // => "例外が発生"
}); 

因此,在 Promise 处理中,无需使用try...catch语法,自动捕获异常。

Promise 的状态

*现在我们已经了解了 Promise 的then方法和catch方法的处理方式,接下来我们将整理一下Promise实例的状态。

Promise实例内部实际上有三种状态。

  • 已解决

    • 当resolve(成功)时的状态。 这时将调用onFulfilled。
  • 已解决

    • 当发生reject(失败)或异常时的状态。 这时将调用onRejected。
  • 挂起

    • 不是已解决或已拒绝的状态

    • 创建 Promise 实例时的初始状态

这些状态是 ECMAScript 规范中确定的内部状态。 但是,没有方法从 Promise 实例中获取这些状态。 因此,虽然不能直接处理此状态作为 API,但可以帮助理解 Promise。

Promise 实例的状态在创建时为挂起,一旦变为已解决或已拒绝,状态就不会再改变。 因此,将状态更改为已解决或已拒绝后,它将被称为已解决(已确定)。

一旦Promise实例变为已解决(已解决或已拒绝),它将不会再转换为其他状态。 因此,即使在调用resolve后调用reject,该Promise实例仍将保持最初的已解决状态。

在下面的代码中,由于状态不会更改,因此不会调用注册的 onRejected 回调函数。 使用then方法注册的回调函数仅在状态更改时调用一次。

const promise = new Promise((resolve, reject) => {
    // 非同期でresolveする
    setTimeout(() => {
        resolve();
        // すでにresolveされているため無視される
        reject(new Error("エラー"));
    }, 16);
});
promise.then(() => {
    console.log("Fulfilledとなった");
}, (error) => {
    // この行は呼び出されない
}); 

同样,Promise构造函数内部多次调用resolve也不会改变Promise实例的状态。 因此,即使多次调用resolve,then方法注册的回调函数也只会被调用一次。

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
        resolve(); // 二度目以降のresolveやrejectは無視される
    }, 16);
});
promise.then(() => {
    console.log("最初のresolve 時に一度だけ呼ばれる");
}, (error) => {
    // この行は呼び出されない
}); 

当Promise实例的状态更改时,您可以注册一次性回调函数以通过then和catch方法深入了解。

此外,对于已更改状态的Promise实例,甚至可以稍后注册then或catch方法的回调函数。 对于已更改状态的Promise实例,通过then或catch方法注册的回调函数也会异步地调用。

具体来说,我们将与已更改状态的Promise实例一起查看Promise.resolve和Promise.reject方法的结果。

Promise.resolve

*Promise.resolve方法是将状态变为已解决的Promise实例。

const fulfilledPromise = Promise.resolve(); 

Promise.resolve方法是将new Promise的语法简化为糖衣语法。 糖衣语法是指可以用更简洁的方式表达相同含义的语法。 Promise.resolve方法的糖衣语法如下。

// const fulfilledPromise = Promise.resolve(); と同じ意味
const fulfilledPromise = new Promise((resolve) => {
    resolve();
}); 

Promise.resolve方法也可以将值传递给resolve。

// `resolve(42)`された`Promise`インスタンスを作成する
const fulfilledPromise = Promise.resolve(42);
fulfilledPromise.then(value => {
    console.log(value); // => 42
}); 

使用Promise.resolve方法创建的已解决状态的Promise实例也可以通过then方法注册回调函数。 状态已更改的Promise实例上注册的回调函数将始终在异步时机执行。

执行此代码后,所有同步处理完成后,then方法中的回调函数将在异步时机执行。

const promise = Promise.resolve();
promise.then(() => {
    console.log("2\. コールバック関数が実行されました");
});
console.log("1\. 同期的な処理が実行されました"); 

Promise.resolve方法作为new Promise的糖衣语法,其执行顺序与使用new Promise相同。 下面的代码与之前使用的Promise.resolve方法具有相同的操作。

const promise = new Promise((resolve) => {
    console.log("1\. resolveします");
    resolve();
});
promise.then(() => {
    console.log("3\. コールバック関数が実行されました");
});
console.log("2\. 同期的な処理が実行されました"); 

执行此代码时,首先执行Promise的构造函数,然后执行同步处理,最后通过then方法异步调用之前注册的回调函数。

Promise.reject

*Promise.reject方法创建一个Rejected状态的Promise实例。

const rejectedPromise = Promise.reject(new Error("エラー")); 

Promise.reject方法是new Promise的语法糖。 因此,Promise.reject方法与以下代码具有相同的意义。

const rejectedPromise = new Promise((resolve, reject) => {
    reject(new Error("エラー"));
}); 

即使是使用Promise.reject方法创建的Rejected状态的Promise实例,也可以通过then或catch方法注册回调函数。 对于已经变为Rejected状态的Promise实例注册的回调函数,将始终在非同步的时间点执行。 这与Fulfilled的情况相同。

Promise.reject(new Error("エラー")).catch(() => {
    console.log("2\. コールバック関数が実行されました");
});
console.log("1\. 同期的な処理が実行されました"); 

Promise.resolve或Promise.reject可以简写,因此在测试代码等情况下经常被使用。 此外,Promise.reject在接下来要解释的 Promise 链中用于操作 Promise 的状态。

Promise 链

*Promise 是提供非同步处理统一接口的内置对象。 使用 Promise 的统一处理方法在处理多个非同步处理时特别有效。 之前,我们只是对单个Promise实例使用then或catch方法注册一组回调处理。

有时需要按顺序处理多个非同步处理。 Promise 提供了一种简单的方法来编写由多个非同步处理组成的一系列非同步处理。

这个机制的关键在于then或catch方法始终创建并返回一个新的Promise实例。 因此,可以将then方法的返回值(Promise实例)进一步注册then方法以进行处理。 这被称为方法链,但在本书中,我们将 Promise 通过方法链连接称为Promise 链(请参考“数组”章节)。

在下面的代码中,我们使用then方法进行 Promise 链。 在 Promise 链中,只要 Promise 不失败(Rejected状态),就会按顺序调用注册在then方法中的成功时回调函数。 因此,在下面的代码中,将按顺序在控制台输出1和2。

// Promiseインスタンスでメソッドチェーン
Promise.resolve()
    // thenメソッドは新しい`Promise`インスタンスを返す
    .then(() => {
        console.log(1);
    })
    .then(() => {
        console.log(2);
    }); 

这个 Promise 链与每次将新的变量放入并连接处理的方式在结果上是相同的。

// Promiseチェーンを変数に入れた場合
const firstPromise = Promise.resolve();
const secondPromise = firstPromise.then(() => {
    console.log(1);
});
const thirdPromise = secondPromise.then(() => {
    console.log(2);
});
// それぞれ新しいPromiseインスタンスが作成される
console.log(firstPromise === secondPromise); // => false
console.log(secondPromise === thirdPromise); // => false 

让我们来看一个更具体的 Promise 链的例子。

在下面的代码中,asyncTask函数随机返回 Fulfilled 或 Rejected 状态的Promise实例。 对于这个函数返回的Promise实例,我们使用then方法编写了成功时的处理。 由于then方法的返回值是新的Promise实例,因此我们可以继续使用catch方法编写失败时的处理。

// ランダムでFulfilledまたはRejectedの`Promise`インスタンスを返す関数
function asyncTask() {
    return Math.random() > 0.5
        ? Promise.resolve("成功")
        : Promise.reject(new Error("失敗"));
}

// asyncTask 関数は新しい`Promise`インスタンスを返す
asyncTask()
    // thenメソッドは新しい`Promise`インスタンスを返す
    .then(function onFulfilled(value) { 
        console.log(value); // => "成功"
    })
    // catchメソッドは新しい`Promise`インスタンスを返す
    .catch(function onRejected(error) {
        console.log(error.message); // => "失敗"
    }); 

当asyncTask函数成功(resolve)时,只有注册在then方法中的成功时处理会被调用,而注册在catch方法中的失败时处理则不会被调用。 另一方面,当asyncTask函数失败(reject)时,注册在then方法中的成功时处理不会被调用,而只有注册在catch方法中的失败时处理会被调用。

在之前的代码中,Promise 的状态和回调函数的处理流程如下。

promise-chain

当 Promise 的状态变为Rejected时,将调用最近的失败处理(catch或then的第二引数)。 在这种情况下,将跳过成功时的处理(then的第一引数)。

在下面的代码中,对于Rejected的 Promise,我们使用 Promise 链来编写then → then → catch的处理。 在这种情况下,最近的失败处理(catch)将被调用,但中间的两个成功处理(then)则不会执行。

// RejectedなPromiseは次の失敗時の処理までスキップする
const rejectedPromise = Promise.reject(new Error("失敗"));
rejectedPromise.then(() => {
    // このthenのコールバック関数は呼び出されません
}).then(() => {
    // このthenのコールバック関数は呼び出されません
}).catch(error => {
    console.log(error.message); // => "失敗"
}); 

与 Promise 构造函数处理相同,then或catch中发生的异常将自动捕获。 当发生异常时,then或catch方法将返回Rejected的Promise实例。 因此,当发生异常时,将调用最接近的失败处理(catch或then的第二个参数)。

Promise.resolve().then(() => {
    // 例外が発生すると、thenメソッドはRejectedなPromiseを返す
    throw new Error("例外");
}).then(() => {
    // このthenのコールバック関数は呼び出されません
}).catch(error => {
    console.log(error.message); // => "例外"
}); 

此外,一旦在 Promise 链中使用catch方法捕获失败,下一步调用的是成功处理。 这是因为then和catch方法返回的是Fulfilled状态的 Promise 实例。 因此,一旦捕获,就会返回到使用下一个then注册的处理的 Promise 链。

Promise.reject(new Error("エラー")).catch(error => {
    console.error(error); // Error: エラー
}).then(() => {
    console.log("thenのコールバック関数が呼び出される");
}); 

就这样,通过then方法或catch方法链接,编写成功或失败的处理被称为 Promise 链。

在 Promise 链中返回值

在 Promise 链中,可以通过回调返回的值将其传递给下一个回调函数。

then和catch方法的回调函数可以返回数字、字符串、对象等任意值。 回调函数返回的值将作为参数传递给下一个then的回调函数。

Promise.resolve(1).then((value) => {
    console.log(value); // => 1
    return value * 2;
}).then(value => {
    console.log(value); // => 2
    return value * 2;
}).then(value => {
    console.log(value); // => 4
    // 値を返さない場合は undefined を返すのと同じ
}).then(value => {
    console.log(value); // => undefined
}); 

在这里,我们以then方法为基础进行解释,但是catch方法是then方法的语法糖,因此具有相同的行为。 一旦在 Promise 链中捕获一次,下一步调用的是成功处理。 因此,通过catch方法返回的值将作为参数传递给下一个then方法的回调函数。

Promise.reject(new Error("失敗")).catch(error => {
    // 一度 catchすれば、次に呼ばれるのは成功時のコールバック
    return 1;
}).then(value => {
    console.log(value); // => 1
    return value * 2;
}).then(value => {
    console.log(value); // => 2
}); 

通过回调函数返回Promise实例

*一旦在 Promise 链中捕获一次,下一步调用的是成功处理(then方法)。 这是因为,当回调函数返回任意值时,将创建一个以该值解析为Fulfilled状态的Promise实例。 但是,如果回调函数返回Promise实例,则情况就会有所不同。

如果在回调函数中返回Promise实例,则then或catch方法的返回值将是具有相同状态的Promise实例。 换句话说,如果在then方法中返回Rejected状态的Promise实例,则下一步调用的是失败处理。

在下面的代码中,then方法的回调函数使用Promise.reject方法返回了一个Rejected的Promise实例。 Rejected的Promise实例会跳过then方法的成功处理,直到调用了下一个catch方法的失败处理。

Promise.resolve().then(function onFulfilledA() {
    return Promise.reject(new Error("失敗"));
}).then(function onFulfilledB() {
    // onFulfilledBは呼び出されません
}).catch(function onRejected(error) {
    console.log(error.message); // => "失敗"
}).then(function onFulfilledC() {
    console.log("onFulfilledCは呼び出されます");
}); 

在这段代码中,Promise 的状态和回调函数的处理流程如下所示。

then-rejected-promise.png

通常情况下,一旦进行了catch处理,下一步调用的是成功处理。 使用此Promise实例返回机制,即使进行了catch处理,也可以保持Rejected状态。

在下面的代码中,我们在catch方法中输出日志,并使用Promise.reject方法返回了一个Rejected的Promise实例。 这样,我们可以在处理链中保持错误的同时记录错误日志。

function main() {
    return Promise.reject(new Error("エラー"));
}
// mainはRejectedなPromiseを返す
main().catch(error => {
    // mainで発生したエラーのログを出力する
    console.error(error);
    // Promiseチェーンはそのままエラーを継続させる
    return Promise.reject(error);
}).then(() => {
    // 前のcatchでRejectedなPromiseが返されたため、この行は実行されません
}).catch(error => {
    console.log("メインの処理が失敗した");
}); 

[ES2018] 在 Promise 链的最后写入处理

*Promise 的finally方法允许注册一个回调函数,在成功或失败时都会被调用。 这类似于try...catch...finally结构中的finally块。

// `promise`にはResolvedまたはRejectedなPromiseインスタンスがランダムで入る
const promise = Math.random() < 0.5 ? Promise.resolve() : Promise.reject();
promise.then(() => {
    console.log("Promiseのthenメソッド");
}).catch((error) => {
    console.log("Promiseのcatchメソッド");
}).finally(() => {
    // 成功、失敗どちらの場合でも呼び出される
    console.log("Promiseのfinallyメソッド");
}); 

在下面的代码中,我们获取资源并在then中注册成功处理,在catch中注册失败处理。 此外,我们使用一个名为isLoading的变量来管理是否正在获取资源。 无论成功与否,一旦获取完成,isLoading都将设置为false。 我们可以在then和catch中都将false分配给isLoading,但使用finally方法可以将分配集中到一个位置。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースを取得中かどうかのフラグ
let isLoading = true;
dummyFetch("/resource/A").then(response => {
    console.log(response);
}).catch(error => {
    console.error(error);
}).finally(() => {
    isLoading = false;
    console.log("Promiseのfinallyメソッド");
}); 

在 Promise 链中进行顺序处理

*Promise 链中编写异步处理流程的一个主要优点是,它可以适应异步处理的各种模式。

在这里,我们将考虑逐个处理多个异步处理作为一个典型的例子。 使用 Promise 进行顺序处理并不复杂,只需简单地使用then连接异步处理即可。

在下面的代码中,我们依次获取 Resource A 和 Resource B。 我们将获取的每个资源添加到变量results中,并在所有获取完成后将其输出到控制台。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}

const results = [];
// Resource Aを取得する
dummyFetch("/resource/A").then(response => {
    results.push(response.body);
    // Resource Bを取得する
    return dummyFetch("/resource/B");
}).then(response => {
    results.push(response.body);
}).then(() => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
}); 

使用Promise.all将多个 Promise 组合在一起

*通过使用Promise.all,可以将多个 Promise 作为一个 Promise 处理。

Promise.all方法接受一个Promise实例数组,并返回一个新的Promise实例。 如果数组中的所有Promise实例都处于Fulfilled状态,则返回的Promise实例也将处于Fulfilled状态。 反之,如果有一个Rejected,则返回的Promise实例也将处于Rejected状态。

通过then方法注册的回调函数,将接收到一个包含 Promise 结果的数组。 在这种情况下,数组中的元素顺序与传递给Promise.all方法的数组的 Promise 元素的顺序相同。

// `timeoutMs`ミリ秒後にresolveする
function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(timeoutMs);
        }, timeoutMs);
    });
}
const promise1 = delay(1);
const promise2 = delay(2);
const promise3 = delay(3);

Promise.all([promise1, promise2, promise3]).then(function(values) {
    console.log(values); // => [1, 2, 3]
}); 

在前面的 Promise 链中,获取资源的例子是逐步进行的,先获取资源 A,然后获取资源 B。但是,如果无论是先获取资源 A 还是 B 都没有问题,那么可以使用Promise.all方法将多个 Promise 合并为一个 Promise。此外,同时获取资源 A 和 B 将更快地完成处理。

下面的代码同时开始获取资源 A 和 B。当两个资源都获取完成后,then的回调函数会以数组的形式传递 A 和 B 的结果。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}

const fetchedPromise = Promise.all([
    dummyFetch("/resource/A"),
    dummyFetch("/resource/B")
]);
// fetchedPromiseの結果をDestructuringでresponseA, responseBに代入している
fetchedPromise.then(([responseA, responseB]) => {
    console.log(responseA.body); // => "Response body of /resource/A"
    console.log(responseB.body); // => "Response body of /resource/B"
}); 

如果传递的 Promise 中有一个变为Rejected,则会调用失败时的处理。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}

const fetchedPromise = Promise.all([
    dummyFetch("/resource/A"),
    dummyFetch("/not_found/B") // Bは存在しないため失敗する
]);
fetchedPromise.then(([responseA, responseB]) => {
    // この行は実行されません
}).catch(error => {
    console.error(error); // Error: NOT FOUND
}); 

Promise.race

*Promise.all方法等待多个 Promise 全部完成。Promise.race方法接收一个 Promise,但只要有一个 Promise 完成(变为 Settled 状态),就会执行下一步操作。

Promise.race方法接收一个Promise实例数组,并返回一个新的Promise实例。这个新的Promise实例将与数组中第一个Settled状态的Promise实例具有相同的状态。

  • 配列の中で一番最初にSettledとなったPromiseがFulfilledの場合は、新しいPromiseインスタンスもFulfilledになる

  • 配列中第一个Settled的 Promise 如果是Rejected,那么新的Promise实例也会变为Rejected

换句话说,通过同时执行多个 Promise 的异步处理并使它们竞争,可以在第一个完成的Promise实例上调用下一步处理。

下面的代码定义了一个名为delay的函数,该函数返回一个在timeoutMs毫秒后变为Fulfilled的Promise实例。Promise.race方法接收一个在 1 毫秒、32 毫秒、64 毫秒、128 毫秒后完成的Promise实例数组。在这个数组中,第一个完成的是在 1 毫秒后变为Fulfilled的Promise实例。

// `timeoutMs`ミリ秒後にresolveする
function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(timeoutMs);
        }, timeoutMs);
    });
}
// 1つでもresolveまたはrejectした時点で次の処理を呼び出す
const racePromise = Promise.race([
    delay(1),
    delay(32),
    delay(64),
    delay(128)
]);
racePromise.then(value => {
    // もっとも早く完了するのは1ミリ秒後
    console.log(value); // => 1
}); 

在这种情况下,第一个resolve的值将使racePromise也resolve。因此,then方法的回调函数将收到值1。

其他delay函数创建的Promise实例也会在 32 毫秒、64 毫秒、128 毫秒后被resolve。但是,一旦Promise实例变为Settled(Fulfilled或Rejected),它将保持不变,不会再改变状态,也不会再调用then的回调函数。因此,racePromise会被多次resolve,但除了第一次外都会被忽略,因此then的回调函数只会被调用一次。

使用Promise.race方法可以实现基于 Promise 的异步操作的超时处理。这里的超时是指,如果经过一定时间仍未完成处理,则将其视为错误处理。

下面的代码使用Promise.race方法竞争timeout函数和dummyFetch函数返回的Promise实例。dummyFetch函数会以随机时间获取资源并resolve返回Promise实例。timeout函数会在指定的毫秒数后reject返回Promise实例。

通过让这两个Promise实例竞争,如果dummyFetch先完成,则处理成功,如果timeout先完成,则处理失败,可以实现超时处理。

// `timeoutMs`ミリ秒後にrejectする
function timeout(timeoutMs) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error(`Timeout: ${timeoutMs}ミリ秒経過`));
        }, timeoutMs);
    });
}
function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// 500ミリ秒以内に取得できなければ失敗時の処理が呼ばれる
Promise.race([
    dummyFetch("/resource/data"),
    timeout(500),
]).then(response => {
    console.log(response.body); // => "Response body of /resource/data"
}).catch(error => {
    console.log(error.message); // => "Timeout: 500ミリ秒経過"
}); 

使用 Promise 可以创建各种异步处理模式。有关更详细的 Promise 用法,请参阅在线公开文档“JavaScript Promise 的本”。

另一方面,Promise 只是一个内置对象,因此在处理异步操作之间的协作时,需要使用类似 Promise 链的特殊写法和外观。此外,关于错误处理,Promise 使用 catch 方法和 finally 方法,类似于 try...catch 语法。但是,由于 Promise 只是一个对象而不是语法,因此必须将它们实现为方法链。

在 ES2017 中,为了解决 Promise 链的不雅外观,引入了名为 Async Function 的语法结构。

[ES2017] 异步函数

*ES2017 引入了一种称为 Async Function 的语法,用于定义执行异步操作的函数。Async Function 与普通函数不同,它定义了一个必须返回 Promise 实例的函数。

通过在函数前加上 async,可以定义 Async Function。这个 doAsync 函数总是返回一个 Promise 实例。

async function doAsync() {
    return "値";
}
// doAsync 関数はPromiseを返す
doAsync().then(value => {
    console.log(value); // => "値"
}); 

这个 Async Function 的写法如下,与下面的写法意思相同。在 Async Function 中,返回的不是 return 的值,而是将返回值包装成 Promise 实例返回,类似于 Promise.resolve(返回值)。

// 通常の関数でPromiseインスタンスを返している
function doAsync() {
    return Promise.resolve("値");
}
doAsync().then(value => {
    console.log(value); // => "値"
}); 

重要的是 Async Function 是建立在 Promise 之上的语法。因此,请注意要理解 Async Function,您需要先理解 Promise。

此外,在 Async Function 内部,可以使用 await 表达式,该表达式等待 Promise 的异步操作完成。通过使用 await 表达式,可以像处理同步操作一样处理异步操作,使得处理流程更易于阅读。

本节将介绍 Async Function 和 await 表达式。

Async Function 的定义

*Async Function 是通过在函数定义中添加 async 关键字来定义的。JavaScript 的函数定义可以是函数声明、函数表达式、箭头函数、方法简写等。无论哪种定义方法,只要在前面加上 async 关键字,就可以将其定义为 Async Function。

// 関数宣言のAsync Function 版
async function fn1() {}
// 関数式のAsync Function 版
const fn2 = async function() {};
// Arrow FunctionのAsync Function 版
const fn3 = async() => {};
// メソッドの短縮記法のAsync Function 版
const obj = { async method() {} }; 

这些 Async Function 除了以下几点外,通常具有与普通函数相同的性质。

  • Async Function 必定返回 Promise 实例

  • Async Function 内部可以使用 await 表达式

Async Function 返回 Promise

*定义了 Async Function 的函数必定返回 Promise 实例。具体来说,Async Function 返回的值可以考虑以下三种情况。

  1. 当 Async Function 返回值时,它将返回一个包含该值的 Fulfilled 的 Promise

  2. 当 Async Function 返回 Promise 时,它将直接返回该返回值的 Promise

  3. 如果在 Async Function 内发生异常,则返回一个包含该错误的 Rejected 的 Promise

在下面的代码中,可以确认 Async Function 根据各自的返回值返回什么样的 Promise 实例。这 1 到 3 的行为与 Promise 的 then 方法的返回值及其回调函数的关系几乎相同。

// 1\. resolveFnは値を返している
// 何もreturnしていない場合はundefinedを返したのと同じ扱いとなる
async function resolveFn() {
    return "返り値";
}
resolveFn().then(value => {
    console.log(value); // => "返り値"
});

// 2\. rejectFnはPromiseインスタンスを返している
async function rejectFn() {
    return Promise.reject(new Error("エラーメッセージ"));
}

// rejectFnはRejectedなPromiseを返すのでcatchできる
rejectFn().catch(error => {
    console.log(error.message); // => "エラーメッセージ"
});

// 3\. exceptionFnは例外を投げている
async function exceptionFn() {
    throw new Error("例外が発生しました");
    // 例外が発生したため、この行は実行されません
}

// Async Functionで例外が発生するとRejectedなPromiseが返される
exceptionFn().catch(error => {
    console.log(error.message); // => "例外が発生しました"
}); 

在任何情况下,Async Function 都会返回一个 Promise。从调用者的角度来看,Async Function 与返回 Promise 的普通函数没有区别。

await 表达式

*await 表达式是以下位置可用的表达式。在以下位置之外,await 表达式会导致语法错误,因此无法使用。

  • Async Function 的函数定义下方

  • 在 ECMAScript 模块的最底层

首先来看一下通常使用的 Async Function 的函数定义下方的 await 表达式。

await 表达式会在右边的 Promise 实例被评估为 Fulfilled 或 Rejected 之前等待异步操作的完成。一旦 Promise 实例的状态发生变化,就会继续执行下一行的处理。

async function asyncMain() {
    // PromiseがFulfilledまたはRejectedとなるまで待つ
    await Promiseインスタンス;
    // Promiseインスタンスの状態が変わったら処理を再開する
} 

在普通处理流程中,执行异步操作时,不会等待异步操作完成就执行下一行(下一句)。但是,await 表达式会在异步操作执行并完成之前,不执行下一行(下一句)。因此,使用 await 表达式可以使异步操作像同步操作一样,按照从上到下的顺序执行。

// async functionは必ずPromiseを返す
async function doAsync() {
    // 非同期処理
}
async function asyncMain() {
    // doAsyncの非同期処理が完了するまでまつ
    await doAsync();
    // 次の行はdoAsyncの非同期処理が完了されるまで実行されない
    console.log("この行は非同期処理が完了後に実行される");
} 

await 表达式返回 await 右边(Promise 实例)的评估结果(表达式请参考“文与表达式”章节)。await 表达式的评估方法取决于评估的 Promise 的状态(Fulfilled 或 Rejected)。

当 await 右边的 Promise 被评估为 Fulfilled 时,resolve 的值将成为 await 表达式的返回值。

在下面的代码中,await 右边的 Promise 实例以 42 这个值进行 resolve。因此,await 表达式的返回值是 42,并且这个值也赋给了 value 变量。

async function asyncMain() {
    const value = await Promise.resolve(42);
    console.log(value); // => 42
}
asyncMain(); // Promiseインスタンスを返す 

这与使用 Promise 编写的代码具有相同的意义。使用 await 表达式,可以不使用回调函数就表达异步处理的流程。

function asyncMain() {
    return Promise.resolve(42).then(value => {
        console.log(value); // => 42
    });
}
asyncMain(); // Promiseインスタンスを返す 

当 await 表达式的右边的 Promise 被评估为 Rejected 时,会立即抛出错误。此外,Async Function 内发生的异常会自动捕获。因此,当 await 表达式中的 Promise 被评估为 Rejected 时,该 Async Function 将返回一个 Rejected 的 Promise。

在这个代码示例中,await右侧的Promise实例处于Rejected状态。因此,await表达式会抛出错误。为了自动捕获这个错误,asyncMain函数返回一个Rejected的 Promise。

async function asyncMain() {
    // `await`式で評価した右辺のPromiseがRejectedとなったため、例外がthrowされる
    const value = await Promise.reject(new Error("エラーメッセージ"));
    // await 式で例外が発生したため、この行は実行されません
}
// Async Functionは自動的に例外をキャッチできる
asyncMain().catch(error => {
    console.log(error.message); // => "エラーメッセージ"
}); 

await表达式会抛出异常,这意味着这个异常可以用try...catch结构捕获(有关详细信息,请参阅“异常处理”章节中的“try...catch 结构”)。由于常规的异步处理在完成前会执行下一行,因此无法在try...catch结构中捕获异常。因此,在 Promise 中使用catch方法来捕获 Promise 内部发生的异常。

在下面的代码中,使用try...catch结构捕获了由await表达式引发的异常。因此,asyncMain函数返回一个 Resolved 的 Promise,而catch方法中的回调函数不会被调用。

async function asyncMain() {
    // await 式のエラーはtry...catchできる
    try {
        // `await`式で評価した右辺のPromiseがRejectedとなったため、例外がthrowされる
        const value = await Promise.reject(new Error("エラーメッセージ"));
        // await 式で例外が発生したため、この行は実行されません
    } catch (error) {
        console.log(error.message); // => "エラーメッセージ"
    }
}
// asyncMainはResolvedなPromiseを返す
asyncMain().catch(error => {
    // すでにtry...catchされているため、この行は実行されません
}); 

这样使用await表达式,就可以像使用try...catch结构一样处理异步处理,而且代码的视觉效果也像同步处理一样,直到当前行(当前语句)的处理完成,才会评估下一行。

Promise 链转换为 Async Function*

*使用 Async Function 和await式可以将以 Promise 链形式表示的异步处理以同步处理的外观来编写。首先,让我们看看逐次执行多个异步处理的 Promise 链的例子。然后,我们将以相同的方式使用 Async Function 和await式重写这些处理,并进行比较。

下面的fetchAB函数使用 Promise 链按顺序获取资源 A 和 B。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースAとリソースBを順番に取得する
function fetchAB() {
    const results = [];
    return dummyFetch("/resource/A").then(response => {
        results.push(response.body);
        return dummyFetch("/resource/B");
    }).then(response => {
        results.push(response.body);
        return results;
    });
}
// リソースを取得して出力する
fetchAB().then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
}); 

使用 Async Function 和await式编写相同处理的方式如下。使用await表达式等待资源获取,并将结果逐次添加到变量results中,从而实现逐次处理。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースAとリソースBを順番に取得する
async function fetchAB() {
    const results = [];
    const responseA = await dummyFetch("/resource/A");
    results.push(responseA.body);
    const responseB = await dummyFetch("/resource/B");
    results.push(responseB.body);
    return results;
}
// リソースを取得して出力する
fetchAB().then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
}); 

如果使用 Promise 链编写fetchAB函数,那么在回调中处理可能会变得复杂。另一方面,使用 Async Function 和await式编写时,只需按顺序执行获取和添加即可,没有嵌套,外观简单。

Async Function 与组合*

*到目前为止,我们已经了解了基本的 Async Function 的工作方式,接下来让我们看看与其他结构或 Promise API 结合使用的 Async Function 的使用方法。

Async Function 与循环处理*

*在执行多个异步处理时,Async Function 可以与 for 循环等循环处理结合使用。

在下面的代码中,实现了fetchResources函数,该函数通过传递指定资源的路径数组来按顺序获取这些资源。在 Async Function 内使用 for 循环进行循环处理,在 for 循环中使用await语句等待资源的获取,并将结果添加到其中。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// 複数のリソースを順番に取得する
async function fetchResources(resources) {
    const results = [];
    // for...of 文でresourcesの配列から1つずつ要素を取り出して処理
    for (const resource of resources) {
        // ループ内で非同期処理の完了を待っている
        const response = await dummyFetch(resource);
        results.push(response.body);
    }
    // 反復処理がすべて終わったら結果を返す(返り値となるPromiseを`results`でresolveする)
    return results;
}
// 取得したいリソースのパス配列
const resources = [
    "/resource/A",
    "/resource/B"
];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
}); 

在 Async Function 中,可以轻松地将 for 循环等现有结构与非同步处理结合使用。如果只有 Promise,则需要使用 Promise 链和回调函数来实现循环处理。

Promise API 与 Async Function 结合使用*

*使用 Async Function 和await式可以使异步处理看起来像同步处理。由于这种同步处理的外观,在执行多个异步处理时,容易编写出产生无谓等待时间的代码。

在先前的fetchResources函数中,是按顺序逐个获取资源的。例如,如果获取资源 A 和 B,所需的时间是资源 A 和 B 获取时间的总和。假设资源 A 需要 1 秒,资源 B 需要 2 秒,那么获取所有资源需要 3 秒。

如果获取的顺序没有意义,可以通过同时获取多个资源来消除多余的等待时间。在先前的例子中,如果同时获取资源 A 和 B,那么所有资源最多只需要等待资源 B 获取的 2 秒即可全部获取。

在 Promise 链中,使用Promise.all方法将多个异步处理汇总为一个Promise实例,以同时获取。由于await表达式评估的是Promise实例,因此也可以将await表达式与Promise.all方法结合使用。

在下面的代码中,结合使用Promise.all方法和 Async Function 实现了fetchAllResources函数,该函数同时获取资源。Promise.all方法接收多个 Promise 作为数组,并返回一个将它们汇总为一个 Promise 的函数。通过await``Promise.all方法返回的Promise实例,可以以数组的形式获取异步处理的最终结果。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// 複数のリソースをまとめて取得する
async function fetchAllResources(resources) {
    // それぞれのリソースを取得する非同期処理を呼び出す
    const promises = resources.map((resource) => {
        return dummyFetch(resource);
    });
    // すべてのリソースが取得できるまで待つ
    // Promise.allは [ResponseA, ResponseB] のように結果が配列となる
    const responses = await Promise.all(promises);
    // 取得した結果からレスポンスのボディだけを取り出す
    return responses.map((response) => {
        return response.body;
    });
}
const resources = [
    "/resource/A",
    "/resource/B"
];
// リソースを取得して出力する
fetchAllResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
}); 

如此,Async Function 和await表达式可以与现有的 Promise API 结合使用。由于 Async Function 内部也使用 Promise 的机制,因此它们不是对立关系,而是共存关系。

Async Function 中的 await 式await式只能在 Async Function 的直下使用

*在 Async Function 中使用await表达式时,需要注意await表达式只能在函数的直下使用。

如果在不是 Async Function 的普通函数中使用await表达式,则会引发语法错误(SyntaxError)。这是为了防止错误的await表达式使用而设定的规范。

// asyncではない関数では`await`式は利用できない
function main(){
    // SyntaxError: await is only valid in async functions
    await Promise.resolve();
} 

Async Function 内でawait式を使って処理を待っている間も、関数の外側では通常どおり処理が進みます。 次のコードを実行してみると、Async Function 内でawaitしても、Async Function 外の処理は停止していないことがわかります。

async function asyncMain() {
    // 中でawaitしても、Async Functionの外側の処理まで止まるわけではない
    await new Promise((resolve) => {
        setTimeout(resolve, 16);
    });
}
console.log("1\. asyncMain 関数を呼び出します");
// Async Functionは外から見れば単なるPromiseを返す関数
asyncMain().then(() => {
    console.log("3\. asyncMain 関数が完了しました");
});
// Async Functionの外側の処理はそのまま進む
console.log("2\. asyncMain 関数外では、次の行が同期的に呼び出される"); 

このようにawait式でAsync Function 内の非同期処理を一時停止しても、Async Function 外の処理が停止するわけではありません。 Async Function 外の処理も停止できてしまうと、JavaScriptでは基本的にメインスレッドで多くの処理をするため、UIを含めた他の処理が止まってしまいます。 これがawait式がAsync Functionの外で利用できない理由の1つです。

这种规范在尝试在回调函数内使用 Async Function 时可能会引起混淆。 具体例子,可以看看之前逐次获取资源的fetchResources関数。

在之前的fetchResources関数中,使用了 for 循环和await表达式。这时,不能使用Array的forEach方法。

将fetchResources関数从简单的 for 循环转换为Array的forEach方法时,会出现语法错误(SyntaxError)。这是因为await表达式只能在 Async Function 中使用的语法。

async function fetchResources(resources) {
    const results = [];
    // Syntax Errorとなる例
    resources.forEach(function(resource) {
        // Async Functionではないスコープで`await`式を利用しているためSyntax Errorとなる
        const response = await dummyFetch(resource);
        results.push(response.body);
    });
    return results;
} 

因此,如果要将Array的forEach方法中的回调函数定义为 Async Function,那么在回调函数中就不能使用await表达式。

这种语法错误可以通过将Array的forEach方法中的回调函数定义为 Async Function 来解决。但是,仅仅将回调函数定义为 Async Function 并不能改变fetchResources函数总是返回一个空数组解决的 Promise 的不当行为。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースを順番に取得する
async function fetchResources(resources) {
    const results = [];
    // コールバック関数をAsync Functionに変更
    resources.forEach(async function(resource) {
        // await 式を利用できるようになった
        const response = await dummyFetch(resource);
        results.push(response.body);
    });
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    // しかし、resultsは空になってしまう
    console.log(results); // => []
}); 

为什么会这样,我们可以通过查看fetchResources関数的运行来了解。

将 Async Function 作为forEach方法的回调函数传递,并在回调函数中使用await表达式等待异步处理的完成。 但是,仅在回调函数内部等待异步处理完成,而在回调函数外部,fetchResources関数的处理会继续进行。

在fetchResources関数中添加一些控制台日志可以使情况更清晰。 只有当forEach方法的回调函数完成时,才会发生,而fetchResources関数的调用在所有调用结束之后。 因此,在forEach方法的回调函数中完成资源获取之前,fetchResources関数将使用当前的results,即空数组,解决。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースを順番に取得する
async function fetchResources(resources) {
    const results = [];
    console.log("1\. fetchResourcesを開始");
    resources.forEach(async function(resource) {
        console.log(`2\. ${resource}の取得開始`);
        const response = await dummyFetch(resource);
        // `dummyFetch`が完了するのは、`fetchResources`関数が返したPromiseが解決された後
        console.log(`5\. ${resource}の取得完了`);
        results.push(response.body);
    });
    console.log("3\. fetchResourcesを終了");
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log("4\. fetchResourcesの結果を取得");
    console.log(results); // => []
}); 

因此,当结合使用 Async Function 和回调函数时,需要注意。

为了解决这个问题,可以像第一个fetchResources関数那样,通过结合使用 for 循环和await表达式而无需使用回调函数。 另外,还可以通过将多个异步处理组合到一个 Promise 中来避免在循环中使用await表达式,例如fetchAllResources関数。

[ES2022] Module 直下でのawait式

*之前已经介绍过,ES2021 中await表达式只能在 Async Function 的直接作用域中使用。 在 ES2022 中,除了这一点之外,在 Module 的直接作用域中也可以使用await表达式。

在“JavaScript 是什么”章节中首次讨论了 JavaScript 具有"Script"和"Module"作为执行上下文的概念。 例如,在浏览器中,<script>将作为"Script"执行,而<script type="module">将作为"Module"执行。

只有在将 JavaScript 作为"Module"执行时,才能在顶层(最外层的作用域)中使用await表达式而无需 Async Function。

例如,如果将以下代码作为"Module"执行,就可以看到在顶层可以使用await表达式,而无需使用 Async Function。

// このコードはModuleのトップレベルでのみ動作する
console.log("実行開始");
const startTime = Date.now();
// awaitを使って1 秒待つ
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`実行終了: ${Date.now() - startTime}ms 経過しました`); 

这种在 Module 顶层使用await表达式的功能称为顶层await。

在 ES2021 之前,只有在 Async Function 的直接作用域中才能使用await表达式。 因此,在像主要处理调用其他处理的脚本的起始点处使用await的情况下,可能会使用立即执行函数来包装await。

// awaitを使うためだけに、Async Functionの即時実行関数を実行している
(async function() {
    // awaitを使う処理
    const result = await doAsyncTask();
    // ...
})(); 

然而,由于 ES2022 中引入了顶层await,因此在"Module"中,此类即时执行函数是不必要的。

[专栏] 错误优先回调

*在 ECMAScript 2015(ES2015)之前,没有规范处理异步处理中的异常。 因此,在 ES2015 之前,人们普遍使用错误优先回调这种方式来处理异步处理中的异常。

错误优先回调是一种规则,用于确定在异步处理中如何调用回调函数。

  • 如果处理失败,将传递错误对象作为回调函数的第一个参数进行调用。

  • 如果操作成功,则将null传递给回调函数的第一个参数,并将成功的结果传递给第二个参数及以后的参数进行调用。

换句话说,这个规则涵盖了处理失败和成功两种情况的单个回调函数。

例如,在 Node.js 中有一个名为fs.readFile的函数,用于从文件系统异步加载文件。 由于文件可能不存在或因权限问题而导致读取失败,因此在传递给fs.readFile関数的第二个参数中,应该传递符合错误优先回调风格的函数。

如果读取文件失败,将在回调函数的第一个参数中传递一个Error对象。 如果读取文件成功,将在回调函数的第一个参数中传递null,第二个参数中传递读取的数据。

fs.readFile("./example.txt", (error, data) => {
    if (error) {
        // 読み込み中にエラーが発生しました
    } else {
        // データを読み込むことができました
    }
}); 

这种错误优先回调在 Node.js 中得到了广泛的应用,并且在 Node.js 的标准 API 中也被使用。 更多细节请参考“用例:Node.js CLI 应用程序”章节。

现在让我们尝试编写一些使用错误优先回调处理异步异常的实际代码。

下面的dummyFetch函数是一个模拟获取资源的异步操作。 它接受任意路径作为第一个参数,并接受符合错误优先回调风格的函数作为第二个参数。

这个dummyFetch函数会根据任意路径是否匹配资源来调用第二个参数的回调函数,如果匹配,则传递null和响应对象,如果不匹配,则传递错误对象。

/**
 * 1000ミリ秒未満のランダムなタイミングでレスポンスを疑似的にデータ取得する関数
 * 指定した`path`にデータがある場合は`callback(null, レスポンス)`を呼ぶ
 * 指定した`path`にデータがない場合は`callback(エラー)`を呼ぶ
 */
function dummyFetch(path, callback) {
    setTimeout(() => {
        // /success からはじまるパスにはリソースがあるという設定
        if (path.startsWith("/success")) {
            callback(null, { body: `Response body of ${path}` });
        } else {
            callback(new Error("NOT FOUND"));
        }
    }, 1000 * Math.random());
}
// /success/data にリソースが存在するので、`response`にはデータが入る
dummyFetch("/success/data", (error, response) => {
    if (error) {
        // この行は実行されません
    } else {
        console.log(response); // => { body: "Response body of /success/data" }
    }
});
// /failure/data にリソースは存在しないので、`error`にはエラーオブジェクトが入る
dummyFetch("/failure/data", (error, response) => {
    if (error) {
        console.log(error.message); // => "NOT FOUND"
    } else {
        // この行は実行されません
    }
}); 

因此,如上所述,将错误对象或null作为回调函数的第一个参数,然后将数据传递给后续参数的规则被称为错误优先回调。

错误优先回调并不是语言规范,而是社区制定的一种规则,用于处理异步处理函数的调用方式。 因此,即使违反了错误优先回调的规则,代码也不会因此而无法运行。 此外,与 Promise 不同,错误优先回调没有自动捕获回调中抛出的异常的机制。

自从 Promise 标准化以来,许多异步 API 都以 Promise 为基础提供。这也适用于 Node.js 的核心模块,目前(Node.js v20.11.1)提供了基于 Promise 的 API。因此,在新代码中使用错误优先回调的情况相当有限。

总结

*在本章中,我们学习了关于 Promise 和 Async Function 的异步处理。

  • 非同期処理是在等待处理完成之前评估下一个处理的过程

  • 即使是异步处理,也可能在主线程中执行

  • Promise 是处理异步操作的内置对象,引入自 ES2015

  • Async Function 是处理异步操作的语法,引入自 ES2017

  • Async Function 是建立在 Promise 之上的语法,因此与 Promise 结合使用

  • 错误优先回调是处理异步操作中异常的一种规则,在 Node.js 中被广泛使用。

有关 Promise 和 Async Function 的应用模式,请参考「JavaScript Promiseの本」。

[ES2015] Map/Set

原文:jsprimer.net/basic/map-and-set/

JavaScriptでデータの集まり(コレクション)を扱うビルトインオブジェクトは、ObjectやArrayだけではありません。 この章では、ES2015で導入されたマップ型のコレクションであるMapと、セット型のコレクションであるSetについて学びます。

Map

*Mapはマップ型のコレクションを扱うためのビルトインオブジェクトです。 マップとは、キーと値の組み合わせからなる抽象データ型です。 他のプログラミング言語の文脈では辞書やハッシュマップ、連想配列などと呼ばれることもあります。

マップの作成と初期化

*Mapオブジェクトをnewすることで、新しいマップを作成できます。 作成されたばかりのマップは何も持っていません。 そのため、マップが持つ要素の数を返すsizeプロパティは0を返します。

const map = new Map();
console.log(map.size); // => 0 

Mapオブジェクトをnewで初期化するときに、コンストラクタに初期値を渡せます。 コンストラクタ引数として渡せるのはエントリーの配列です。 エントリーとは、1つのキーと値の組み合わせを[キー, 値]という形式の配列で表現したものです。

次のコードでは、Mapに初期値となるエントリー(配列)の配列を渡しています。

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
// 2つのエントリーで初期化されている
console.log(map.size); // => 2 

要素の追加と取り出し

*Mapには新しい要素をsetメソッドで追加でき、追加した要素をgetメソッドで取り出せます。

setメソッドは特定のキーと値を持つ要素をマップに追加します。 ただし、同じキーで複数回setメソッドを呼び出した際は、後から追加された値で上書きされます。

getメソッドは特定のキーにひもづいた値を取り出します。 また、特定のキーにひもづいた値を持っているかを確認するhasメソッドがあります。

const map = new Map();
// 新しい要素の追加
map.set("key", "value1");
console.log(map.size); // => 1
console.log(map.get("key")); // => "value1"
// 要素の上書き
map.set("key", "value2");
console.log(map.get("key")); // => "value2"
// キーの存在確認
console.log(map.has("key")); // => true
console.log(map.has("foo")); // => false 

deleteメソッドはマップから要素を削除します。 deleteメソッドに渡されたキーと、そのキーにひもづいた値がマップから削除されます。 また、マップが持つすべての要素を削除するためのclearメソッドがあります。

const map = new Map();
map.set("key1", "value1");
map.set("key2", "value2");
console.log(map.size); // => 2
map.delete("key1");
console.log(map.size); // => 1
map.clear();
console.log(map.size); // => 0 

マップの反復処理

*マップが持つ要素を列挙するメソッドとして、forEach、keys、values、entriesがあります。

forEachメソッドはマップが持つすべての要素を、マップへの挿入順に反復処理します。 コールバック関数には引数として値、キー、マップの3つが渡されます。 配列のforEachメソッドと似ていますが、インデックスの代わりにキーが渡されます。 配列はインデックスにより要素を特定しますが、マップはキーにより要素を特定するためです。

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const results = [];
map.forEach((value, key) => {
    results.push(`${key}:${value}`);
});
console.log(results); // => ["key1:value1","key2:value2"] 

keysメソッドはマップが持つすべての要素のキーを挿入順に並べたIteratorオブジェクトを返します。 同様に、valuesメソッドはマップが持つすべての要素の値を挿入順に並べたIteratorオブジェクトを返します。 これらの返り値はIteratorオブジェクトであって配列ではありません。 そのため、次の例のようにfor...of文で反復処理を行ったり、Array.fromメソッドに渡して配列に変換して使ったりします。

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const keys = [];
// keysメソッドの返り値(Iterator)を反復処理する
for (const key of map.keys()) {
    keys.push(key);
}
console.log(keys); // => ["key1","key2"]
// keysメソッドの返り値(Iterator)から配列を作成する
const keysArray = Array.from(map.keys());
console.log(keysArray); // => ["key1","key2"] 

entriesメソッドはマップが持つすべての要素をエントリーとして挿入順に並べたIteratorオブジェクトを返します。 先述のとおりエントリーは[キー, 値]の配列です。 そのため、配列の分割代入を使うとエントリーからキーと値を簡単に取り出せます。

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const entries = [];
for (const [key, value] of map.entries()) {
    entries.push(`${key}:${value}`);
}
console.log(entries); // => ["key1:value1","key2:value2"] 

また、マップ自身もiterableなオブジェクトなので、for...of文で反復処理できます。 マップをfor...of文で反復したときは、すべての要素をエントリーとして挿入順に反復処理します。 つまり、entriesメソッドの返り値を反復処理するときと同じ結果が得られます。

const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const results = [];
for (const [key, value] of map) {
    results.push(`${key}:${value}`);
}
console.log(results); // => ["key1:value1","key2:value2"] 

マップとしてのObjectとMap

*ES2015でMapが導入されるまで、JavaScriptにおいてマップ型を実現するためにObjectが利用されてきました。 何かをキーにして値にアクセスするという点で、MapとObjectはよく似ています。 ただし、マップとしてのObjectにはいくつかの問題があります。

  • Objectのprototypeオブジェクトから継承されたプロパティによって、意図しないマッピングを生じる危険性がある

  • また、プロパティとしてデータを持つため、キーとして使えるのは文字列かSymbolに限られる

Objectにはprototypeオブジェクトがあるため、いくつかのプロパティは初期化されたときから存在します。 Objectをマップとして使うと、そのプロパティと同じ名前のキーを使おうとしたときに問題となります (詳細は「オブジェクト」の章の「プロパティの存在を確認する」を参照)。

たとえばconstructorという文字列はObject.prototype.constructorプロパティと衝突してしまいます。 そのためconstructorのような文字列をオブジェクトのキーに使うことで意図しないマッピングを生じる危険性があります。

const map = {};
// マップがキーを持つことを確認する
function has(key) {
    return typeof map[key] !== "undefined";
}
console.log(has("foo")); // => false
// Objectのプロパティが存在する
console.log(has("constructor")); // => true 

このマップとして使うオブジェクトの問題は、ObjectのインスタンスをObject.create(null)のように初期化して作ることで回避されてきました (詳細は「プロトタイプオブジェクト」の章の「Object.prototypeを継承しないオブジェクト」を参照)。

ES2015では、これらの問題を根本的に解決するMapが導入されました。 Mapはプロパティとは異なる仕組みでデータを格納します。 そのため、Mapのプロトタイプが持つメソッドやプロパティとキーが衝突することはありません。 また、Mapではマップのキーとしてあらゆるオブジェクトを使えます。

ほかにもMapには次のような利点があります。

  • マップのサイズを簡単に知ることができる

  • マップが持つ要素を簡単に列挙できる

  • オブジェクトをキーにすると参照ごとに違うマッピングができる

たとえばショッピングカートのような仕組みを作るとき、次のようにMapを使って商品のオブジェクトと注文数をマッピングできます。

// ショッピングカートを表現するクラス
class ShoppingCart {
    constructor() {
        // 商品とその数を持つマップ
        this.items = new Map();
    }
    // カートに商品を追加する
    addItem(item) {
        // `item`がない場合は`undefined`を返すため、Nullish coalescing 演算子(`??`)を使いデフォルト値として`0`を設定する
        const count = this.items.get(item) ?? 0;
        this.items.set(item, count + 1);
    }
    // カート内の合計金額を返す
    getTotalPrice() {
        return Array.from(this.items).reduce((total, [item, count]) => {
            return total + item.price * count;
        }, 0);
    }
    // カートの中身を文字列にして返す
    toString() {
        return Array.from(this.items).map(([item, count]) => {
            return `${item.name}:${count}`;
        }).join(",");
    }
}
const shoppingCart = new ShoppingCart();
// 商品一覧
const shopItems = [
    { name: "みかん", price: 100 },
    { name: "リンゴ", price: 200 },
];

// カートに商品を追加する
shoppingCart.addItem(shopItems[0]);
shoppingCart.addItem(shopItems[0]);
shoppingCart.addItem(shopItems[1]);

// 合計金額を表示する
console.log(shoppingCart.getTotalPrice()); // => 400
// カートの中身を表示する
console.log(shoppingCart.toString()); // => "みかん:2,リンゴ:1" 

当使用Object作为映射时,许多问题可以通过使用Map对象来解决,但Map并不总是Object的替代品。作为映射的Object具有以下优点。

  • 由于存在字面量表达式,因此易于创建

  • 由于具有规定的 JSON 表示,因此可以使用JSON.stringify函数轻松地将Object转换为 JSON。

  • 不论是原生 API 还是外部库,许多函数都设计为以Object作为映射传递。

在以下示例中,在接收到登录表单的 submit 事件后,向服务器发送 POST 请求。为了向服务器发送 JSON 字符串,使用JSON.stringify函数。因此,创建了一个Object映射,其中包含表单的输入内容。在这种情况下,使用Object作为这种简单映射是合适的。

// URLとObjectのマップを受け取ってPOSTリクエストを送る関数
function sendPOSTRequest(url, data) {
    // fetchを使ってPOSTリクエストを送る
    fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
    }).catch((error) => {
        console.error(error);
    });
}

// formのsubmitイベントを受け取る関数
function onLoginFormSubmit(event) {
    const form = event.target;
    const data = {
        userName: form.elements.userName,
        password: form.elements.password,
    };
    sendPOSTRequest("/api/login", data);
} 

WeakMap

*WeakMap与Map一样,是用于处理映射的内置对象。与Map的不同之处在于,它使用弱引用(Weak Reference)来持有键。

弱引用是一种特殊的引用,它不会阻止垃圾收集(GC)释放对象。通常,只有当对象不再被任何地方引用时,GC 才能从内存中释放对象。弱引用例外地,即使存在对该对象的弱引用,GC 也可以释放该对象。

因此,弱引用用于防止因持续引用不再需要的对象而导致的内存泄漏。由于WeakMap中不再需要的键及其关联的值会自动删除,因此不会引起内存泄漏。

在以下代码中,首先将obj设置为{},然后在WeakMap中将该obj作为键并设置值("value")。接下来,将obj赋值为另一个值(此处为null),则obj最初引用的{}值将不再被任何地方引用。此时,WeakMap持有对{}的弱引用,但弱引用不会阻止 GC,因此{}作为不再需要的值将被 GC 从内存中释放。

同时,WeakMap可以丢弃与已释放对象({})关联的值("value")。但是,实际何时从内存中释放取决于 JavaScript 引擎的实现。

const map = new WeakMap();
// キーとなるオブジェクト
let obj = {};
// {} への参照をキーに値をセットする
map.set(obj, "value");
// {} への参照を破棄する
obj = null;
// GCが発生するタイミングでWeakMapから値が破棄される 

WeakMap与Map相似,但不是可迭代的。因此,不存在用于列举键的keys方法或返回数据数量的size属性。此外,由于它使用弱引用持有键,因此可以作为键使用的只有引用类型的对象和Symbol(es2023)。

WeakMap的主要用途之一是将私有值存储在类中。将this(类实例)作为WeakMap的键,可以保持无法从实例外部访问的值。此外,当类实例不再被引用时,这些值将自动释放。

在以下代码中,使用WeakMap管理触发事件的监听器函数(事件监听器)。事件监听器是指当事件发生时被调用的函数。如果使用Map实现此映射,则事件监听器将一直保留在内存中,直到显式删除。在这里使用WeakMap,则传递给addListener方法的listener在EventEmitter实例不再被引用时将自动释放。

// イベントリスナーを管理するマップ
const listenersMap = new WeakMap();

class EventEmitter {
    addListener(listener) {
        // this(インスタンス)にひもづいたリスナーの配列を取得する
        const listeners = listenersMap.get(this) ?? [];
        const newListeners = listeners.concat(listener);
        // this をキーにして、新しいリスナーの配列をセットする
        listenersMap.set(this, newListeners);
    }
    // ...EventEmitterには他にもメソッドがあるが省略...
}

// `event`は`EventEmitter`のインスタンスへの参照をもつ
let event = new EventEmitter();
// `EventEmitter`のインスタンスへイベントリスナーを追加する
event.addListener(() => {
    // `EventEmitter`のインスタンスに紐づくイベントリスナーの処理
});
// `event`へ`null`を代入することで、`EventEmitter`のインスタンスへの参照がなくなる
// インスタンスがどこからも参照されなくなったため、紐づいていたイベントリスナーが自動的に解放される
event = null; 

此外,它还常用于临时保存从某个对象计算出的结果。在以下示例中,保存了 HTML 元素的高度计算结果,以避免在第二次及以后进行相同的计算。

const cache = new WeakMap();

function getHeight(element) {
    if (cache.has(element)) {
        return cache.get(element);
    }
    const height = element.getBoundingClientRect().height;
    // elementオブジェクトに対して高さをひもづけて保存している
    cache.set(element, height);
    return height;
} 

[专栏] 键的等价性与 NaN

*Map中设置值时可以使用任何对象作为键。在这种情况下,该映射是否已经包含特定的键,即插入和覆盖的判断基本上与===运算符相同。

然而,键为NaN的处理方式例外。在Map中,键的比较中,NaN总是被视为等价的。这种行为被称为Same-value-zero算法。

在以下代码中,NaN之间的===比较结果为false,而Map的键中NaN之间的比较结果是一致的。

const map = new Map();
map.set(NaN, "value");
// NaNは===で比較した場合は常にfalse
console.log(NaN === NaN); // => false
// MapはNaN 同士を比較できる
console.log(map.has(NaN)); // => true
console.log(map.get(NaN)); // => "value" 

Set

*Set是用于处理集合的内置对象。集合是指不包含重复值的集合。Set可以列举添加的值,因此它类似于不包含重复值的数组,常用于保证值的唯一性。然而,与数组不同,集合没有顺序,也不能通过索引访问元素。

创建和初始化集合

*通过new创建Set对象可以创建一个新的集合。创建的集合一开始没有任何内容。因此,返回集合中元素数量的size属性将返回 0。

const set = new Set();
console.log(set.size); // => 0 

当使用new初始化Set对象时,可以将初始值传递给构造函数。构造函数参数可以是可迭代对象。

在以下代码中,将可迭代对象数组作为初始值传递。此外,Set确保不包含重复的相同值,因此相同的值只存储一次。

// "value2"が重複するため、片方は無視される
const set = new Set(["value1", "value2", "value2"]);
// セットのサイズは2になる
console.log(set.size); // => 2 

值的添加和提取

*要向创建的集合中添加值,可以使用add方法。正如之前所述,集合保证不包含重复的值。因此,当将已存在于集合中的值传递给add方法时,它将被忽略。

此外,还有一个has方法可以用来检查集合是否包含特定的值。

const set = new Set();
// 値の追加
set.add("a");
console.log(set.size); // => 1
// 重複する値は追加されない
set.add("a");
console.log(set.size); // => 1
// 値の存在確認
console.log(set.has("a")); // => true
console.log(set.has("b")); // => false 

要从集合中删除值,可以使用delete方法。delete方法会删除传递给它的值。此外,还有clear方法可以删除集合中所有的值。

const set = new Set();
set.add("a");
set.add("b");
console.log(set.size); // => 2
set.delete("a");
console.log(set.size); // => 1
set.clear();
console.log(set.size); // => 0 

集合的遍历

*要遍历集合中的值,可以使用forEach方法。forEach方法会按照集合的插入顺序遍历集合中所有的元素。

const set = new Set(["a", "b"]);
const results = [];
set.forEach((value) => {
    results.push(value);
});
console.log(results); // => ["a","b"] 

集合中创建迭代器对象的keys、values、entries方法类似于Map,但集合中没有与Map中的键相当的元素。因此,keys方法实际上是values方法的别名,它返回一个按插入顺序列出集合中所有值的迭代器对象。同样,entries方法返回一个按插入顺序列出[值, 值]形式条目的迭代器对象。

const set = new Set(["a", "b"]);
// keysで列挙
const keysResults = [];
for (const value of set.keys()) {
    keysResults.push(value);
}
console.log(keysResults); // => ["a","b"]
// entriesで列挙
const entryResults = [];
for (const entry of set.entries()) {
    // entryは[値, 値]という配列
    entryResults.push(entry);
}
console.log(entryResults); // => [["a","a"], ["b", "b"]] 

由于Set对象自身也是一个可迭代的对象,因此可以使用for...of语句进行遍历。使用for...of语句遍历Set对象时,值也会按照集合的插入顺序被取出。由于keys、values、entries方法可以获取相同的信息,因此基本上可以通过遍历Set对象本身来代替。

const set = new Set(["a", "b"]);
const results = [];
for (const value of set) {
    results.push(value);
}
console.log(results); // => ["a","b"] 

WeakSet

WeakSet是一个包含有值引用的集合。WeakSet与Set类似,但它不是可迭代的,因此不能遍历添加的值。换句话说,WeakSet只能进行值的添加和删除、存在性检查等操作。可以说它是一个专注于检查数据唯一性的集合。

此外,由于WeakSet的特性是只包含有值的引用,因此可以作为其值的只能是引用类型的对象和Symbol(es2023)。

总结

本章我们学习了Map和Set。

  • Map是一个内置对象,用于处理由键值对组成的集合。

  • Map的键可以避免与原型对象的属性和名称冲突,从而避免意外的映射。

  • WeakMap是一个与键为弱引用的Map类似的内置对象。

  • Set是一个内置对象,用于处理不包含重复值的有序集合。

  • WeakSet是一个与值为弱引用的Set类似的内置对象。

^(es2023). ES2023 中已经修改了规范,使得Symbol也可以被处理。 ↩*************

JSON

原文:jsprimer.net/basic/json/

この章では、JavaScriptと密接な関係にあるJSONというデータフォーマットについて見ていきます。

JSONとは

JSONはJavaScript Object Notationの略で、JavaScriptのオブジェクトリテラルをベースに作られた軽量なデータフォーマットです。 JSONの仕様はECMA-404として標準化されています。 JSONは、人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行える形式になっています。 そのため、多くのプログラミング言語がJSONを扱う機能を備えています。

JSONはJavaScriptのオブジェクトリテラル、配列リテラル、各種プリミティブ型の値を組み合わせたものです。 ただしJSONとJavaScriptは一部の構文に違いがあります。 たとえばJSONでは、オブジェクトリテラルのキーを必ずダブルクォートで囲まなければいけません。 また、小数点から書きはじめる数値リテラルや、先頭がゼロからはじまる数値リテラルも使えません。 これらは機械がパースしやすくするために仕様で定められた制約です。

{
    "object": {
        "number": 1,
        "string": "js-primer",
        "boolean": true,
        "null": null,
        "array": [1, 2, 3]
    }
} 

JSONの細かい仕様に関してはjson.orgの日本語ドキュメントにわかりやすくまとまっているので、参考にするとよいでしょう。

JSONオブジェクト

JavaScriptでJSONを扱うには、ビルトインオブジェクトであるJSONオブジェクトを利用します。 JSONオブジェクトはJSON 形式の文字列とJavaScriptのオブジェクトを相互に変換するためのparseメソッドとstringifyメソッドを提供します。

JSON 文字列をオブジェクトに変換する

JSON.parseメソッドは引数に与えられた文字列をJSONとしてパースし、その結果をJavaScriptのオブジェクトとして返す関数です。 次のコードは簡単なJSON 形式の文字列をJavaScriptのオブジェクトに変換する例です。

// JSONはダブルクォートのみを許容するため、シングルクォートでJSON 文字列を記述
const json = '{ "id": 1, "name": "js-primer" }';
const obj = JSON.parse(json);
console.log(obj.id); // => 1
console.log(obj.name); // => "js-primer" 

文字列がJSONの配列を表す場合は、JSON.parseメソッドの返り値も配列になります。

const json = "[1, 2, 3]";
console.log(JSON.parse(json)); // => [1, 2, 3] 

与えられた文字列がJSON 形式でパースできない場合は例外が投げられます。 また、実際のアプリケーションでJSONを扱うのは、外部のプログラムとデータを交換する用途がほとんどです。 外部のプログラムが送ってくるデータが常にJSONとして正しい保証はありません。 そのため、JSON.parseメソッドは基本的にtry...catch構文で例外処理をするべきです。

const userInput = "not json value";
try {
    const json = JSON.parse(userInput);
} catch (error) {
    console.log("パースできませんでした");
} 

オブジェクトをJSON 文字列に変換する

JSON.stringifyメソッドは第一引数に与えられたオブジェクトをJSON 形式の文字列に変換して返す関数です。 HTTP 通信でサーバーにデータを送信するときや、 アプリケーションが保持している状態を外部に保存するときなどに必要になります。 次のコードはJavaScriptのオブジェクトをJSON 形式の文字列に変換する例です。

const obj = { id: 1, name: "js-primer", bio: null };
console.log(JSON.stringify(obj)); // => '{"id":1,"name":"js-primer","bio":null}' 

JSON.stringifyメソッドにはオプショナルな引数が2つあります。 第二引数はreplacer 引数とも呼ばれ、関数あるいは配列を渡せます。 関数を渡した場合は引数にプロパティのキーと値が渡され、その返り値によって文字列に変換される際の挙動をコントロールできます。 次の例は値がnullであるプロパティを除外してJSONに変換するreplacer 引数の例です。 replacer 引数の関数でundefinedが返されたプロパティは、変換後のJSONに含まれなくなります。

const obj = { id: 1, name: "js-primer", bio: null };
const replacer = (key, value) => {
    if (value === null) {
        return undefined;
    }
    return value;
};
console.log(JSON.stringify(obj, replacer)); // => '{"id":1,"name":"js-primer"}' 

replacer 引数に配列を渡した場合はプロパティの許可リストとして使われ、その配列に含まれる名前のプロパティだけが変換されます。

const obj = { id: 1, name: "js-primer", bio: null };
const replacer = ["id", "name"];
console.log(JSON.stringify(obj, replacer)); // => '{"id":1,"name":"js-primer"}' 

第三引数はspace 引数とも呼ばれ、変換後のJSON 形式の文字列を読みやすくフォーマットする際のインデントを設定できます。 数値を渡すとその数値分の長さのスペースで、文字列を渡すとその文字列でインデントされます。 次のコードはスペース2 個でインデントされたJSONを得る例です。

const obj = { id: 1, name: "js-primer" };
// replacer 引数を使わない場合はnullを渡して省略するのが一般的です
console.log(JSON.stringify(obj, null, 2));
/*
{
   "id": 1,
   "name": "js-primer"
}
*/ 

また、次のコードはタブ文字でインデントされたJSONを得る例です。

const obj = { id: 1, name: "js-primer" };
console.log(JSON.stringify(obj, null, "\t"));
/*
{
   "id": 1,
   "name": "js-primer"
}
*/ 

JSONにシリアライズできないオブジェクト

JSON.stringifyメソッドはJSONで表現可能な値だけをシリアライズします。 そのため、値が関数やSymbol、あるいはundefinedであるプロパティなどは変換されません。 ただし、配列の値としてそれらが見つかったときには例外的にnullに置き換えられます。 またキーがSymbolである場合にもシリアライズの対象外になります。 代表的な変換の例を次の表とサンプルコードに示します。

シリアライズ前の値 シリアライズ後の値
文字列・数値・真偽値 対応する値
null null
配列 配列
オブジェクト オブジェクト
関数 変換されない(配列のときはnull)
undefined 変換されない(配列のときはnull)
Symbol 変換されない(配列のときはnull)
RegExp {}
Map, Set {}
BigInt 例外が発生する
// 値が関数のプロパティ
console.log(JSON.stringify({ x: function() {} })); // => '{}'
// 値がSymbolのプロパティ
console.log(JSON.stringify({ x: Symbol("") })); // => '{}'
// 値がundefinedのプロパティ
console.log(JSON.stringify({ x: undefined })); // => '{}'
// 配列の場合
console.log(JSON.stringify({ x: [10, function() {}] })); // => '{"x":[10,null]}'
// キーがSymbolのプロパティ
JSON.stringify({ [Symbol("foo")]: "foo" }); // => '{}'
// 値がRegExpのプロパティ
console.log(JSON.stringify({ x: /foo/ })); // => '{"x":{}}'
// 値がMapのプロパティ
const map = new Map();
map.set("foo", "foo");
console.log(JSON.stringify({ x: map })); // => '{"x":{}}' 

オブジェクトがシリアライズされる際は、そのオブジェクトの列挙可能なプロパティだけが再帰的にシリアライズされます。 RegExpやMap、Setなどのインスタンスは列挙可能なプロパティを持たないため、空のオブジェクトに変換されます。

此外,JSON.stringify方法有时会序列化失败。常见的情况是当尝试序列化具有循环引用的对象时,会引发异常。例如,如果递归遍历某个对象的属性,直到发现自身时,序列化将变得不可能。不仅仅是JSON.parse方法,JSON.stringify方法也要进行异常处理,以确保安全使用。

const obj = { foo: "foo" };
obj.self = obj;
try {
    JSON.stringify(obj);
} catch (error) {
    console.error(error); // => "TypeError: Converting circular structure to JSON"
} 

使用toJSON方法进行序列化

如果对象具有toJSON方法,则JSON.stringify方法将使用toJSON方法的返回值,而不是默认的字符串转换。就像下面的例子一样,它将递归地处理作为参数直接传递以及作为参数属性出现的情况。

const obj = {
    foo: "foo",
    toJSON() {
        return "bar";
    }
};
console.log(JSON.stringify(obj)); // => '"bar"'
console.log(JSON.stringify({ x: obj })); // => '{"x":"bar"}' 

toJSON方法被用于将自定义类以特殊格式进行序列化。

总结

*在本章中,我们学习了关于 JSON 的知识。

  • JSON 是基于 JavaScript 对象文字的轻量级数据格式。

  • 使用JSON对象进行序列化和反序列化。

  • 也有一些无法被序列化为 JSON 格式的对象。

  • JSON.stringify利用了要序列化的对象的toJSON方法**********

posted @ 2025-12-13 17:35  绝不原创的飞龙  阅读(32)  评论(0)    收藏  举报