JavaScriptインタビューをマスターする:約束とは何ですか?

写真:Kabun(CC BY NC SA 2.0)
「JavaScriptのインタビューをマスターする」は、中級から上級レベルのJavaScriptポジションに応募する際に遭遇する可能性のある一般的な質問の候補者を準備するために設計された一連の投稿です。これらは、実際のインタビューでよく使用する質問です。

約束とは何ですか?

プロミスは、将来的には単一の値を生成する可能性のあるオブジェクトです。解決された値、または解決されない理由(ネットワークエラーが発生したなど)のいずれかです。約束は、履行、拒否、または保留の3つの可能な状態のいずれかになります。 Promiseユーザーは、コールバックを添付して、満たされた値または拒否の理由を処理できます。

Promiseは熱心です。つまり、Promiseコンストラクターが呼び出されるとすぐに、Promiseは指定したタスクを実行し始めます。遅延が必要な場合は、オブザーバブルまたはタスクをチェックしてください。

約束の不完全な歴史

1980年代には、約束と未来(同様の/関連するアイデア)の早期実装がMultiLispやConcurrent Prologなどの言語で登場し始めました。 「約束」という言葉の使用は、1988年にBarbara LiskovとLiuba Shriraによって作られました[1]。

JavaScriptの約束について初めて聞いたとき、Nodeは真新しく、コミュニティは非同期動作を処理する最良の方法について議論していました。コミュニティはしばらくの間約束を実験しましたが、最終的にはノード標準のエラー優先コールバックに落ち着きました。

同じ頃、DojoはDeferred APIを介してプロミスを追加しました。関心と活動の高まりは、最終的に、さまざまな約束をより相互運用可能にするように設計された新しく形成されたPromises / A仕様につながりました。

jQueryの非同期動作は、約束を中心にリファクタリングされました。 jQueryのpromiseサポートはDojoのDeferredと驚くほど類似しており、jQueryの非常に人気のためにJavaScriptで最も一般的に使用されるpromise実装になりました。ただし、人々が約束に基づいてツールを構築することを期待していた2つのチャネル(実行/拒否)チェーン動作と例外管理はサポートしていませんでした。

これらの弱点にもかかわらず、jQueryは公式にJavaScriptの約束を主流にし、Q、When、Bluebirdなどのより優れたスタンドアロンの約束ライブラリが非常に人気になりました。 jQueryの実装の非互換性は、promises / A +仕様として書き直され、ブランド変更されたpromise仕様の重要な明確化の動機となりました。

ES6はPromises / A +準拠のPromiseグローバルをもたらし、いくつかの非常に重要なAPIが新しい標準Promiseサポートの上に構築されました:特にWHATWG Fetch仕様とAsync Functions標準(この執筆時点でステージ3ドラフト)。

ここで説明する約束は、Promises / A +仕様と互換性があり、ECMAScript標準のPromise実装に焦点を合わせたものです。

約束の仕組み

promiseは、非同期関数から同期的に返されるオブジェクトです。次の3つの状態のいずれかになります。

  • Fulfilled:onFulfilled()が呼び出されます(たとえば、resolve()が呼び出されました)
  • 拒否:onRejected()が呼び出されます(たとえば、reject()が呼び出されました)
  • 保留中:まだ履行または拒否されていません

約束が保留中でない場合(解決または拒否された場合)に確定します。時々、人々は同じことを意味するために解決され、解決されたものを使用します。

一度確定すると、約束は再定住できません。 resolve()またはreject()を再度呼び出しても効果はありません。確定したプロミスの不変性は重要な機能です。

ネイティブJavaScriptプロミスはプロミス状態を公開しません。代わりに、約束をブラックボックスとして扱うことが期待されます。約束の作成を担当する機能のみが、約束のステータス、または解決または拒否するためのアクセス権を知っています。

指定した時間遅延後に解決するプロミスを返す関数は次のとおりです。

wait(3000)呼び出しは3000ms(3秒)待機してから、「Hello!」とログに記録します。すべての仕様互換プロミスは、解決または拒否された値を取ることができるハンドラーを渡すために使用する.then()メソッドを定義します。

ES6 promiseコンストラクターは関数を受け取ります。この関数は、resolve()とreject()の2つのパラメーターを取ります。上記の例では、resolve()のみを使用しているため、パラメータリストからreject()を除外しました。次に、setTimeout()を呼び出して遅延を作成し、完了したらresolve()を呼び出します。

オプションでresolve。()またはreject()を値とともに使用できます。これらの値は、.then()でアタッチされたコールバック関数に渡されます。

値でreject()するとき、常にErrorオブジェクトを渡します。通常、2つの可能な解決状態が必要です。通常のハッピーパス、または例外-通常のハッピーパスの発生を止めるものです。 Errorオブジェクトを渡すと、明示的になります。

重要な約束ルール

約束の標準は、Promises / A +仕様コミュニティによって定義されました。 JavaScript標準のECMAScriptの約束など、標準に準拠する実装が多数あります。

仕様に従う約束は、特定のルールセットに従う必要があります。

  • promiseまたは「thenable」は、標準に準拠した.then()メソッドを提供するオブジェクトです。
  • 保留中の約束は、履行済みまたは拒否済みの状態に移行する場合があります。
  • 履行または拒否された約束は解決され、他の状態に移行してはなりません。
  • 約束が確定したら、値(未定義の場合があります)が必要です。その値は変更できません。

このコンテキストの変更は、アイデンティティ(===)比較を指します。オブジェクトが満たされた値として使用され、オブジェクトのプロパティが変化する場合があります。

すべてのプロミスは、次のシグネチャを持つ.then()メソッドを提供する必要があります。

promise.then(
  onFulfilled ?:関数、
  onRejected ?:関数
)=>約束

.then()メソッドは次のルールに準拠する必要があります。

  • onFulfilled()とonRejected()はどちらもオプションです。
  • 指定された引数が関数ではない場合、無視する必要があります。
  • onFulfilled()は、約束が満たされた後、最初の引数として約束の値を使用して呼び出されます。
  • onRejected()は、プロミスが拒否された後に呼び出され、拒否の理由が最初の引数になります。理由は有効なJavaScript値かもしれませんが、拒否は基本的に例外と同義であるため、Errorオブジェクトを使用することをお勧めします。
  • onFulfilled()とonRejected()のどちらも複数回呼び出すことはできません。
  • .then()は同じ約束で何度も呼び出されます。つまり、約束はコールバックを集約するために使用できます。
  • .then()は、新しいpromise、promise2を返す必要があります。
  • onFulfilled()またはonRejected()が値xを返し、xがプロミスである場合、promise2はxとロックインします(同じ状態と値を仮定します)。それ以外の場合、promise2はxの値で満たされます。
  • onFulfilledまたはonRejectedが例外eをスローする場合、promise2はeを理由として拒否する必要があります。
  • onFulfilledが関数ではなく、promise1が満たされる場合、promise2はpromise1と同じ値で満たされる必要があります。
  • onRejectedが関数ではなく、promise1が拒否された場合、promise2は、promise1と同じ理由で拒否されなければなりません。

約束の連鎖

.then()は常に新しいプロミスを返すため、エラーを処理する方法と場所を正確に制御してプロミスをチェーンすることができます。 Promiseを使用すると、通常の同期コードのtry / catch動作を模倣できます。

同期コードと同様に、連鎖はシリアルで実行されるシーケンスになります。つまり、次のことができます。

fetch(url)
  .then(プロセス)
  .then(保存)
  .catch(handleErrors)
;

各関数、fetch()、process()、およびsave()がpromiseを返すと仮定すると、process()はfetch()が完了する前に開始する前に待機し、save()はprocess()が完了する前に開始する前に待機します。 handleErrors()は、前のプロミスのいずれかが拒否した場合にのみ実行されます。

複数の拒否を伴う複雑なプロミスチェーンの例を次に示します。

エラー処理

promiseには成功ハンドラとエラーハンドラの両方があり、これを行うコードを見るのは非常に一般的です。

save()。then(
  handleSuccess、
  handleError
);

しかし、handleSuccess()がエラーをスローするとどうなりますか? .then()から返された約束は拒否されますが、拒否をキャッチするものはありません。つまり、アプリのエラーが飲み込まれます。おっとっと!

そのため、上記のコードはアンチパターンであると考える人もいますが、代わりに次のことをお勧めします。

保存する()
  .then(handleSuccess)
  .catch(handleError)
;

違いはわずかですが、重要です。最初の例では、save()操作で発生したエラーがキャッチされますが、handleSuccess()関数で発生したエラーは飲み込まれます。

.catch()がなければ、成功ハンドラーのエラーはキャッチされません。

2番目の例では、.catch()はsave()またはhandleSuccess()からの拒否を処理します。

.catch()を使用すると、両方のエラーソースが処理されます。 (図のソース)

もちろん、save()エラーはネットワークエラーかもしれませんが、handleSuccess()エラーは、開発者が特定のステータスコードを処理するのを忘れたためかもしれません。それらを異なる方法で処理したい場合はどうしますか?あなたはそれらの両方を処理することを選ぶことができます:

保存する()
  .then(
    handleSuccess、
    handleNetworkError
  )
  .catch(handleProgrammerError)
;

好みに応じて、すべてのプロミスチェーンを.catch()で終了することをお勧めします。繰り返す価値があります:

すべてのpromiseチェーンを.catch()で終了することをお勧めします。

約束をキャンセルするにはどうすればよいですか?

新しいプロミスユーザーがしばしば疑問に思う最初のことの1つは、プロミスをキャンセルする方法です。アイデアは次のとおりです。「キャンセル」を理由として約束を拒否するだけです。 「通常の」エラーとは異なる方法で対処する必要がある場合は、エラーハンドラーで分岐を行います。

ここに、人々が自分で約束をキャンセルするときに犯すよくある間違いをいくつか示します。

.cancel()をプロミスに追加する

.cancel()を追加すると、Promiseは非標準になりますが、別のPromiseのルールにも違反します。Promiseを作成する関数のみが、Promiseを解決、拒否、またはキャンセルできます。それを公開することはそのカプセル化を壊し、それについて知らない場所で約束を操作するコードを書くことを人々に奨励します。スパゲッティと約束の破綻を避けてください。

片付けを忘れる

一部の賢い人々は、Promise.race()をキャンセルメカニズムとして使用する方法があることを理解しています。それに関する問題は、キャンセル制御が約束を作成する関数から取得されることです。これは、タイムアウトのクリアやデータへの参照のクリアによるメモリの解放など、適切なクリーンアップアクティビティを実行できる唯一の場所です。

拒否されたキャンセル約束の処理を忘れる

約束の拒否を処理するのを忘れると、Chromeがコンソール全体に警告メッセージをスローすることをご存知ですか?おっとっと!

複雑すぎる

取り消されたTC39のキャンセルの提案は、キャンセルのための別のメッセージングチャネルを提案しました。また、キャンセルトークンと呼ばれる新しい概念を使用しました。私の意見では、このソリューションは約束の仕様をかなり肥大化させたものであり、投機が直接サポートしないという条件で提供された唯一の機能は拒否とキャンセルの分離であり、IMOはそもそも必要ではありません。

例外があるかキャンセルされているかに応じて切り替えますか?そのとおり。それが約束の仕事ですか?私の意見では、いや、そうではありません。

約束キャンセルの再考

一般的に、プロミスがプロミス作成時に解決/拒否/キャンセルする方法を決定するために必要なすべての情報を渡します。そうすれば、promiseに.cancel()メソッドは必要ありません。約束の作成時にキャンセルするかどうかをどのようにして知ることができるのか疑問に思われるかもしれません。

「キャンセルするかどうかまだわからない場合、約束を作成するときに何を渡すかをどのように知ることができますか?」

将来の潜在的な価値に代わることができる何らかの種類のオブジェクトがある場合にのみ…ああ、待ってください。

キャンセルするかどうかを表すために渡す値は、約束そのものである可能性があります。これはどのように見えるかです:

デフォルトのパラメータ割り当てを使用して、デフォルトでキャンセルしないように指示しています。これにより、キャンセルパラメータが便利にオプションになります。次に、以前のようにタイムアウトを設定しますが、今回はタイムアウトのIDをキャプチャして、後でクリアできるようにします。

cancel.then()メソッドを使用して、キャンセルとリソースのクリーンアップを処理します。これは、プロミスが解決する前にキャンセルされる場合にのみ実行されます。キャンセルが遅すぎると、チャンスを逃してしまいます。その列車は駅を出ました。

注:noop()関数が何のためにあるのか疑問に思うかもしれません。 noopという単語はno-opの略で、何もしない機能を意味します。これがないと、V8は警告をスローします:UnhandledPromiseRejectionWarning:未処理のプロミス拒否。ハンドラがnoop()であっても、常にプロミスの拒否を処理することをお勧めします。

約束キャンセルの抽象化

wait()タイマーにはこれで問題ありませんが、このアイデアをさらに抽象化して、覚えておく必要があるすべてのものをカプセル化できます。

  1. デフォルトでキャンセルの約束を拒否します。キャンセルの約束が渡されない場合、キャンセルしたりエラーをスローしたりしません。
  2. キャンセルを拒否する場合は、必ずクリーンアップを実行してください。
  3. onCancelクリーンアップ自体がエラーをスローする可能性があり、そのエラーも処理する必要があることに注意してください。 (上記の待機例ではエラー処理が省略されていることに注意してください。忘れがちです!)

約束をラップするために使用できるキャンセル可能なpromiseユーティリティを作成しましょう。たとえば、ネットワーク要求などを処理するには…署名は次のようになります。

speculation(fn:SpecFunction、shouldCancel:Promise)=> Promise

SpecFunctionはPromiseコンストラクターに渡す関数とまったく同じですが、1つの例外があります。onCancel()ハンドラーが必要です。

SpecFunction(resolve:Function、reject:Function、onCancel:Function)=> Void

この例は、それがどのように機能するかの要点を示すための単なる例示であることに注意してください。他にも考慮すべきエッジケースがいくつかあります。たとえば、このバージョンでは、約束が既に確定した後にキャンセルすると、handleCancelが呼び出されます。

オープンソースライブラリであるSpeculationとしてカバーされているエッジケースを使用して、この製品のメンテナンス版を実装しました。

改善されたライブラリ抽象化を使用して、以前からキャンセル可能なwait()ユーティリティを書き換えましょう。最初の推測をインストールします。

npm install-投機を保存

これでインポートして使用できます:

これにより、noop()、onCancel()、関数、またはその他のエッジケースでエラーをキャッチすることを心配する必要がないため、物事が少し簡単になります。これらの詳細はspeculation()によって抽象化されています。実際のプロジェクトで自由に使用してください。

ネイティブJSプロミスのエクストラ

ネイティブのPromiseオブジェクトには、あなたが興味を持ちそうな余分なものがいくつかあります。

  • Promise.reject()は拒否されたプロミスを返します。
  • Promise.resolve()は、解決されたプロミスを返します。
  • Promise.race()は配列(または任意の反復可能)を取り、反復可能の最初の解決済みプロミスの値で解決するプロミスを返すか、拒否する最初のプロミスの理由で拒否します。
  • Promise.all()は配列(または任意の反復可能)を取り、反復可能引数内のすべてのpromiseが解決したときに解決するpromiseを返します。

結論

PromiseはJavaScriptのいくつかのイディオムの不可欠な部分になりました。これには、最新のajaxリクエストに使用されるWHATWG Fetch標準や、非同期コードを同期的に見せるために使用されるAsync Functions標準が含まれます。

この記事の執筆時点では非同期関数はステージ3ですが、まもなくJavaScriptの非同期プログラミングで非常に一般的で非常によく使用されるソリューションになると予測しています。つまり、約束を理解することはJavaScriptにとってさらに重要になります近い将来の開発者。

たとえば、Reduxを使用している場合は、redux-sagaを確認することをお勧めします。reduxの副作用を管理するために使用されるライブラリで、ドキュメント全体で非同期関数に依存しています。

経験豊富なPromiseユーザーでさえ、Promiseとは何か、彼らがどのように機能するか、そしてこれを読んだ後にそれらをより良く使用する方法をよりよく理解してくれることを願っています。

シリーズを見る

  • クロージャーとは何ですか?
  • クラス継承とプロトタイプ継承の違いは何ですか?
  • 純粋な機能とは何ですか?
  • 関数構成とは何ですか?
  • 関数型プログラミングとは何ですか?
  • 約束とは何ですか?
  • ソフトスキル
  1. バーバラリスコフ;リウバ・シュリラ(1988)。 「約束:分散システムでの効率的な非同期プロシージャコールの言語サポート」。プログラミング言語の設計と実装に関するSIGPLAN '88会議の議事録。米国ジョージア州アトランタ、pp。260–267。 ISBN 0–89791–269–1、ACMにより公開。 ACM SIGPLAN Notices、Volume 23、Issue 7、1988年7月にも公開されています。
EricElliottJS.comで無料レッスンを開始してください

エリック・エリオットは、「Composing Software」および「Programming JavaScript Applications」という本の著者です。 EricElliottJS.comとDevAnywhere.ioの共同設立者として、開発者に不可欠なソフトウェア開発スキルを教えています。彼は暗号化プロジェクトの開発チームを構築して助言し、Adobe Systems、Zumba Fitness、The Wall StreetJournal、ESPN、BBC、およびUsher、Frank Ocean、Metallicaなどのトップレコーディングアーティストのソフトウェアエクスペリエンスに貢献しています。

彼は世界で最も美しい女性との遠隔生活を楽しんでいます。