Chapter 12

Promise コンストラクタと Executor 関数

PADAone🐕
PADAone🐕
2023.02.07に更新

このチャプターについて

Promise の基本概念』のチャプターでは抽象的な概念についてしか触れていなかったので、ここからはコード上での Promise について、インスタンスの作成方法などを通じて触れていきます。

Promise オブジェクト

まず、Promise とは「非同期処理の結果を表現するビルトインオブジェクト」であり、モダンな非同期処理ではこの Promise オブジェクトを介して非同期処理を行うのがベターです。

Promise オブジェクトは fetch() といった非同期 API (ECMAScript の一部ではなくブラウザやランタイムの環境が提供する機能)の処理の結果として返されるパターンが多いですが、Promise そのものはビルトインオブジェクトであり、ECMAScript (JavaScript の言語コア) の一部であることを忘れないようにしてください。

また、"Promise API" という言葉がありますが、これは Promise インスタンスを返すタイプの非同期 API である "Promise-based API" のことを指しており、Promise 自体が API であるわけではないので注意してください。他の解説によっては、Promise の静的メソッドである Promise.all() などを指している場合もあります。

Promise コンストラクタ

コード上で Promise() はコンストラクタ関数であり、new 演算子と併用して使用することで Prosmise オブジェクト(Promise インスタンス)を生成できます。Promise オブジェクトを作成する際には、Promise() コンストラクタには Executor関数 と呼ばれるコールバックを引数として渡します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise

deno コマンドで REPL 環境 を立ち上げて少しテストしてみると次のようになります。

❯ deno
Deno 1.20.4
exit using ctrl+d or close()
# deno REPL で見てみる
> const promise = new Promise(resolve => resolve(1))
undefined
> typeof promise
"object"
> promise instanceof Promise
true
# Promise.resolve() でつくっても同じ
> const np = Promise.resolve(1);
undefined
> np instanceof Promise
true

以降、各 Promise オブジェクトについて、コンストラクタ関数から作成されることや、関数やメソッドから返ってくるということを意識するために "Promise インスタンス" という言葉を多用していきます。

Promise インスタンスの作成は new Promise(executor) が基本形です。コールバック関数として引数に渡す executor 自身は引数を2つ受け取ります。

次のコードでは executor がコールバック関数であることに注目するため、あえて Promise コンストラクタの外で定義してみると次のようになります。

function executor(resolve, reject) {
  // 以下の処理は適当に形式をあわせて書いているだけです
  if (Math.random() < 0.5) { // 適当な条件
    resolve("Promise履行時の値");
    // resolve 関数は Promise インスタンスを履行(Fulfilled)状態にしたい時に呼び出す
  } else {
    reject("Promise拒否時の理由");
    // reject 関数は Promise インスタンスを拒否(Rejected)状態にしたい時に呼び出す
  }
}

// あえてコールバックをコンストラクタの外で定義している
const promise = new Promise(executor);

JavaScript では「関数は値」なのでこのように関数を他の値のように引数として渡すことができます。「コールバック関数」はこのように他の関数に引数として渡される関数のことを指します。

Promise() コンストラクタの引数として渡されるコールバック関数である executor の引数である resolvereject もコールバック関数です。慣習的に resolvereject となっていますが実際には名前は何でも OK です。executor の中において、resolve() 関数は Promise インスタンスを履行(Fulfilled)状態にしたいという時に呼び出し、reject() は Promise インスタンスを拒否(Rejected)状態にしたいという時に呼び出します。

この2つの関数はクセがあるので注意してください。(後述)。

executor は基本的には無名関数(匿名関数)でアロー関数の省略形などがよく使われるので注意してください。ここから、徐々に変形していきます。

まずは、Promise() コンストラクタの中でコールバック関数を無名関数として定義してみます。

const promise = new Promise(function (resolve, reject) {
  if (Math.random() < 0.5) {
    resolve("Promise履行時の値");
  } else {
    reject("Promise拒否時の理由");
  }
});

次はコールバック関数をアロー関数に変形します。この形式が多くの解説記事で見られるような一般的な形になります。

const promise = new Promise((resolve, reject) => {
  if (Math.random() < 0.5) {
    resolve("Promise履行時の値");
  } else {
    reject("Promise拒否時の理由");
  }
});

executor 関数の第二引数である reject省略可能なので書かない場合もよくあります。拒否状態などを気にせず、履行状態のみを考えます(実際には、executor 関数の中でエラーが発生すると Promise インスタンスは自動的に拒否(Rejected)状態へと移行します)。

// reject 関数を省略
const promise = new Promise((resolve) => {
  resolve("Promise履行時の値");
});

さらにアロー関数は引数が1つのときにカッコを省略できるので次のように文字数を少なくして書けます。

const promise = new Promise(resolve => {
  resolve("Promise履行時の値");
});

resolve() 関数の名前は何でも良かったので、名前を短くして文字数をもっと減らしてみます。

const promise = new Promise(res => {
  res("Promise履行時の値");
});

これで最初の書き方よりもかなり楽に書けていることが分かります。さすがに res というような書き方はあまりしないと思いますが、この先にでてくるものとの差異を明らかにするため、わざとやっています。

アロー関数の return 省略を使って、もっと文字数を減らしてみます。

const promise = new Promise(res => res("Promise履行時の値"));

これはアロー関数の省略形の中でも最も短い形式となっています。ですが、実は上のコードは res => {return res("Promise履行時の値")} の省略形となっています。return に注意してください。

アロー関数の省略は以下のようにでき、下の3つのコートはすべて等価です。従って、return を上のように省略できています。

(a) => {
  return a + 100;
}
// 等価
(a) => a + 100;
// 等価
a => a + 100;

ここまで、new Promise(executor) というコードをなるべく短く書けるように省略してきましたが、実は上記のコードと同じようなことを Promise() コンストラクタ関数を使用せずに Promise.resolve() という Promise の静的メソッド(static method)を使って実現できます。

const promise1 = Promise.resolve("Promise履行時の値");
// この2つは大体同じ
const promise2 = new Promise(res => res("Promise履行時の値"));

executor 関数の引数である res 関数と静的メソッドである Promise.resolve() は別物であることに注目してください。

この Promise.resolve() は最も文字数が少なく書けるので、Promise オブジェクトの初期化やテストコードを書く際に活用できる便利なショートカットとして覚えてください。実際に Promise オブジェクトを作成する際には new Promise(excutor) が基本となります。

さて、executor 関数の引数は2つありました。resolve (上の例では res) と reject です。reject 自体は第二引数であり、省略できたので上記のように短く書くために無視してきましたが、これでは不公平なので reject についても省略形で書けるようにします。次のコードでは、executor 関数の中で reject() 関数のみを書いて Promise インスタンスを拒否状態にしています。

const promise = new Promise((resolve, reject) => {
  reject("Promise拒否時の理由");
});

reject がそのまま省略可能であったのに対して、reject を使いたい場合に resolve 関数が省略できないのは第一引数だからです。第一引数がないのに第二引数は書けません

ただし、上述の通り resolvereject について名前は何でも良いのでそれを利用して次のように字数が減るように書くことができます。

const promise = new Promise((_, rej) => {
  rej("Promise拒否時の理由");
});

アンダースコア(_)という記号によってあえて使わない resolve 関数の名前を最も短い一文字にしています。この書き方は別に推奨というわけではないですが、こうやってできるということを認識するために書いています。

さて、resolve でやったようにアロー関数のさらなる省略形でもっと文字数を減らしてみます。

const promise1 = new Promise((_, rej) => rej("Promise拒否時の理由"));
// 2つは等価
const promise2 = new Promise((_, rej) => {
  return rej("Promise拒否時の理由");
});

かなり文字数が減りましたね。予想できると思いますが、これらのコードと同じことを Promise オブジェクトの静的メソッドである Promise.reject() を利用してもっと短く書くことが可能です。

const promise1 = new Promise((_, rej) => rej("Promise拒否時の理由"));
// この2つは大体同じ
const promise2 = Promise.reject("Promise拒否時の理由");

この Promise.reject() も初期化やテストなどで活用できる便利なショートカットとして使えますが、基本は new Promise(executor) です。

関数式とアロー関数の補足

アロー関数について触れましたが、少し補足します。

まずアロー関数の前に通常の関数宣言とは別の方法で関数を定義する「関数式」について触れておきます。function キーワードを使用した関数宣言は次のようになります。

function myFunc() {
  console.log("arg1:", arg1);
  console.log("arg2:", arg1);
}

myFunc("hello", "zenn");
/* 出力
 * arg1: hello
 * arg2: zenn
 */

通常の関数宣言は上書きでてきしまうので、const 宣言を使った変数への代入という形で関数の定義を行います。これが「関数式」です。関数式の使い方は次のようになります。

// helloZenn.js
const myFunc = function (arg1, arg2) {
  console.log("arg1:", arg1);
  console.log("arg2:", arg1);
};

myFunc("hello", "zenn");
/* 出力
 * arg1: hello
 * arg2: zenn
 */

関数を変数 myFunc に代入しているわけです。呼び出し時には () をつけることで関数の実行となります。() をつけなければただの値として評価されます。例えば次のコードでは、() をつけずに console.log() の引数として渡しています。

// helloZenn-value.js
const myFunc = function (arg1, arg2) {
  console.log("arg1:", arg1);
  console.log("arg2:", arg1);
};

console.log(myFunc); // => [Function: myFunc]

これを実行すると次の出力を得ます。

❯ deno run helloZenn-value.js
[Function: myFunc]

このように、JavaScript では「関数は値」であり、関数を他の値と同じように変数に代入したりコールバック関数として引数に渡すことができるようになっています。コールバック関数として引数に渡す場合は () をつけずに渡します。() をつけてしまった場合には関数実行の結果としてその関数の返却する返り値が引数として渡されるので注意してください。

関数式で関数の定義をするメリットとしてははじめに言ったように const 宣言で 関数の上書きを出来ないようにしています

ではここでアロー関数を使って関数式を書き換えてみます。function キーワードを取り払って、アロー記号 => をつけます。

// helloZenn-allow.js
const myFunc = (arg1, arg2) => {
  console.log("arg1:", arg1);
  console.log("arg2:", arg1);
};

myFunc("hello", "zenn");
/* 出力
 * arg1: hello
 * arg2: zenn
 */

これだけです。さらに関数内のコードが単一の式の場合は return を省略できます。このようなアロー関数の省略形を使って書くと次のように書けます。

// 全部同じ意味
const increment0 = (num) => {
  return num + 1;
}
// 単一の式のみなので `return` を省略できる
const increment1 = (num) => num + 1;
// 引数が1つのみなので括弧を省略できる
const increment2 = num => num + 1;

ただし、次のコードのように返り値としてオブジェクトリテラルを返す場合には注意が必要です。

const returnObject = () => {
  return { key: "value" };
}

アロー関数の省略形として次の書き方は誤りです。

// この書き方は誤り
const returnObject = () => { key: "value" };
// こうなってしまっている
const returnObject = () => {
  key: "value"
};

この場合はオブジェクトリテラルを () でくくることで return を省略した形で書くことができます。

// 正しい書き方
const returnObjectOmit = () => ({ key: "value" });

参考
https://typescriptbook.jp/reference/functions/arrow-functions

あとはコールバック関数にアロー関数を渡す際になどでたまに見かける書き方として、引数をあえて「使用しないアンダースコア1つ」にして () を書かずに文字数を少なくするというものがあります。

const zeroParamFn = _ => {
  console.log("使わない引数をあえて1個のアンダースコアにする");
};
Promise.resolve()
  .then(_ => console.log("コールバックでたまに見かける書き方"));
setTimeout(_ => {
  console.log("推奨でないという話も聞く");
}, 3000);

これは、ただの「書き方のスタイル」で引数が0個のときのアロー関数の省略形 () => {...} よりも文字数が少なく出来るというだけのものです。

参考
https://stackoverflow.com/questions/41085189/using-underscore-variable-with-arrow-functions-in-es6-typescript

アロー関数と関数式の説明は以上になります。
実際にはアロー関数を使うことで通常の関数宣言や関数式と異なる挙動がいくつかありますが、その説明は別の記事や本で補足してください。

https://qiita.com/suin/items/a44825d253d023e31e4d

https://typescriptbook.jp/reference/functions/function-expression-vs-arrow-functions

以降、Promise インスタンスを返す関数や async/await を使った非同期関数などが登場しますが、このアロー関数を使って定義する場合があるので注意してください。