75331


JavaScriptのPromiseについて勉強しているのですが、下記のスクリプトで使われている引数 `...args`
に何が格納されているのかよくわかりません。 google
chromeのコンソールにコピペして実行すると`console.log()`出力しようとしても定義されていないと出ます。  
プログラム自体はカウントダウン後、ロケットが発車した後にタイムアウトでエラーを引き起こします。  
よろしくお願いします。

[code]

    function countdown(seconds) {
        return new Promise(function(onFulfilled, onRejected) {
            const timeoutIds =[];
            for(let i=seconds; i>=0; i--) {
                timeoutIds.push(setTimeout(
                    function() {
                        if(i===13) {
                            timeoutIds.forEach(clearTimeout);
                            onRejected(new Error(`${i}という数は不吉過ぎます`));
                        }
                        else if(i>0){
                            console.log(i + '...');
                        }
                        else{
                            console.log("GO!");
                            onFulfilled();
                        }
                    }, 
                    (seconds-i)*1000));
            }
        });
    }
    
    function launch() {
        return new Promise(function(onFulfilled, onRejected) {
            if(Math.random() < 0.5) return; //ここを追加した。
            console.log("発射!");
            setTimeout(function() {
                onFulfilled("周回軌道に乗った!");
            }, 2*1000); /*超高速ロケット*/
        });
    }
    
    function addTimeout(fn,
                                            period
                                            ) {
        if(period === undefined) period = 1000;
        return function(...args) {
            return new Promise(function(onFulfilled, onRejected) {
                const timeoutId = setTimeout(onRejected, period, new Error("プロミス タイムアウト"));
                    fn(...args)
                    .then(function(...args) {
                        clearTimeout(timeoutId);
                        onFulfilled(...args);
                    })
                    .catch(function(...args) {
                        clearTimeout(timeoutId);
                        onRejected(...args);
                        console.log(...args); //Uncaught ReferenceError: args is not defined
                    });
            });
        }
    }
    
    countdown(3)
        .then(addTimeout(launch, 4*1000))
        .then(function(msg) {/*関数launch内のonFulfilledの引数がmsgに渡る*/
            console.log(msg);
        })//カウントダウンが13以上だとonRejectedが実行され下記のcatchに処理が移る。
        .catch(function(err) {
            console.error("管制塔、管制塔。トラブル発生..." + err);
    })
[/code]

### 参照

Ethan Brown. Learning JavaScript, 3rd Edition. O'Reilly. イーサン ブラウン ムシャ ヒロユキ
ムシャ ルミ (訳) 2017.


CC BY-SA 4.0
2021-04-16T08:08:35.073
75324
2021-04-16T20:13:30.360
22565
question
javascript
JavaScriptのPromiseにタイムアウトを追加するスクリプトで引数 ...argsに何が入っているか分からない。


コメントにもありますが `...args` 自体の説明はMDNに譲ります。

残余引数 - JavaScript | MDN  
<https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/rest_parameters>

* * *

`function
addTimeout()`は任意のPromiseを返す関数を受け取って、そのPromiseにtimeoutの機能を追加して返すためのラッパー(Wrapper)です。(当該書籍は持っていませんが…)

このラッパーが返す関数の引数を、受け取った関数と同じ引数にしたいところですが、任意の関数 `fn` が渡されるため、引数の個数などが特定できません。

そのため、`...args` を使用して引数すべてを参照し、それを `fn`
に[スプレッド構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)を使用してすべてそのまま渡すことで実現しています。

[code]

        return function(...args) {
            return new Promise(function(onFulfilled, onRejected) {
    
                fn(...args)
    
[/code]

引数がない関数を渡した場合、`args` は空配列になります。

[code]

    function foo() { return 'Hello'; }
    
    function wrapper(fn) {
      return function(...args) {
        console.log(args);
        return fn(...args);
      }
    }
    
    const r = wrapper(foo)();
    console.log(r);
[/code]

* * *

`.then(function(...args)` の `...args` には、`fn`
(実際には`launch()`)が返したPromiseが成功した時の戻り値が入ります。なので、タイムアウト処理を停止後、その値を全て`onFulfilled`にスプレッド構文で渡してPromiseを正常終了させます。

質問文のコードの場合、実際には`"周回軌道に乗った!"`が1つだけ入るはずです。

[code]

        return function(...args) {
            return new Promise(function(onFulfilled, onRejected) {
    
                fn(...args)
                    .then(function(...args) {
                        clearTimeout(timeoutId);
                        onFulfilled(...args);
                    })
    
[/code]

* * *

さて、`.catch(function(...args)`ですが、この `...args` には `launch()`
が返したPromiseが失敗した時のErrorが入ります。

[code]

                fn(...args)
                    .then(function(...args) {
                        clearTimeout(timeoutId);
                        onFulfilled(...args);
                    })
                    .catch(function(...args) {
                        clearTimeout(timeoutId);
                        onRejected(...args);
                        console.log(...args);
                    });
    
[/code]

ですが、`launch()` の実装を見ればわかる通り、この関数は `onRejected`
の呼び出しもしませんし、Errorのthrowもしないので、少なくとも質問文のコードではここに到達することはあり得ません。

おそらく別の所で出た`console.log()`と混同したか、別の場所にかけたbreakpointからcatchに書かれたfunction内の`args`を参照したため`args
is not defined`になったものと推測します。

例えば、以下のようにコードを書き換えれば、`.catch()`に到達し、引数にもErrorが入っていることが確認できるはずです。

[code]

    function launch() {
        return new Promise(function(onFulfilled, onRejected) {
            // 必ずエラーを起こす
            if (true) { throw new Error("error in launch!!!"); }
    
            console.log("発射!");
            setTimeout(function() {
                onFulfilled("周回軌道に乗った!");
            }, 2*1000); /*超高速ロケット*/
        });
    }
    
    function addTimeout(fn, period) {
        if(period === undefined) period = 1000;
        return function(...args) {
            return new Promise(function(onFulfilled, onRejected) {
                const timeoutId = setTimeout(onRejected, period, new Error("プロミス タイムアウト"));
                    fn(...args)
                    .then(function(...args) {
                        clearTimeout(timeoutId);
                        onFulfilled(...args);
                    })
                    .catch(function(...args) {
                        clearTimeout(timeoutId);
                        onRejected(...args);
    
                        // throw した Error を参照する
                        console.info(args[0].toString());
                    });
            });
        }
    }
    
    addTimeout(launch, 4*1000)();
[/code]

* * *

> プログラム自体はカウントダウン後、ロケットが発車した後にタイムアウトでエラーを引き起こします。

質問文のスニペットを複数回実行すればわかりますが、だいたい50%の確率で成功したりタイムアウトしたりするはずです。

これは以下のコードにより、50%の確率で`onFulfilled`も`onRejected`も呼ばれずにこの非同期処理が完了しないまま時間経過して、`addTimeout()`内でセットした`setTimeout()`が発火して`"プロミス
タイムアウト"`になるためです。

[code]

    function launch() {
        return new Promise(function(onFulfilled, onRejected) {
            if(Math.random() < 0.5) return; //ここを追加した。
    
[/code]

Promiseが必ず正しく成功または失敗するようにコーディングするのはプログラマーの責務です。ただし人間誰しもミスをしてしまうもので、このように適切にPromiseが終了しないコーディングをしてしまうことが稀にあります。

例えばESLintで `no-promise-executor-return` ルールを設定しておくと、このようなコーディングミスを発見することができます。

no-promise-executor-return - Rules - ESLint - Pluggable JavaScript linter  
<https://eslint.org/docs/rules/no-promise-executor-return#disallow-returning-
values-from-promise-executor-functions-no-promise-executor-return>


CC BY-SA 4.0
2021-04-16T19:39:31.313
75331
2021-04-16T20:13:30.360
2021-04-16T20:13:30.360
3068
3068
75324
answer