hiroakirigit
@hiroakirigit

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

JavaScriptのクロージャの挙動がわからない

解決したいこと

クロージャの挙動がよくわかりません。
独習JavaScript内の問題の解答に関して、自分が書いたコードと回答のコードの違いで、実行結果が変わる理由がわからないため質問させていただきます。

出された問題

実行をしたら、下記の場合1と場合2が両方実行されるようにします。
※場合1と場合2は最初から定義されているので、自分で作成するのはdelayMessageFactory関数だけになります。

クロージャを使って次の挙動を満たす関数(delayMessageFactory)を実装してみてください。
場合1
//2秒後にalertで「こんにちは」が出力される
const dialog = delayMessageFactory( alert, 2000 );
dialog( "こんにちは" )

場合2
//1秒後にコンソールログで「こんばんは」が出力される
const log = delayMessageFactory( console.log, 1000 );
log( "こんばんは" )

自分が書いたコード

function delayMessageFactory( fn1, time ){
    function alert( greeting ){
        setTimeout(fn1( greeting ), time)

    }
    return alert;
}

テキスト内の回答例

比較しやすいように引数と、変数は自分が書いたコードをベースに合わせてあります。

function delayMessageFactory( fn1, time ){
    function alert( greeting ){
        setTimeout(function(){
            fn1( greeting );
        }, time)

    }
    return alert;
}

↓↓回答例のスクリーンショットです。
スクリーンショット 2022-10-01 19.56.24.png

自分の理解

setTimeOutのコールバック関数に、実引数のconsole.logあるいはalertをそのまま返せば良いと思っていました。
しかし回答では、コールバック関数に無名関数を一度挟んでから、その中の処理で実引数で渡された関数を処理する記述になっているかと思います。(解釈が間違っていたら申し訳ございません。。。)

自分が書いたコードと回答例だと何が変わるのでしょうか?

普段あまりクロージャに触れていないこともあり、初歩的な質問ですがご回答いただけますと幸いです。

1

2Answer

setTimeout(fn1( greeting ), time)

この書き方ですと、

  1. fn1( greeting ) を実行
  2. その戻り値を引数にして、setTimeout(戻り値, time) を実行
  3. 戻り値は関数ではないため、setTimeoutは例外発生

という実行順序になり、時間待ちする前にfn1関数を呼び出してしまいます。

        setTimeout(function(){
            fn1( greeting );
        }, time)

この書き方なら、

  1. function() { fn1( greeting); } で関数を作る
  2. その関数を引数にして、setTimeout(関数, time) を実行
  3. time時間経過後に関数が呼ばれ、その関数から fn1( greeting ) を実行

という実行順序になります。

> const fn1 = (name) => console.log("Hi,", name);
undefined
> fn1( "Taro" )
Hi, Taro   // fn1関数が出力したメッセージ
undefined  // fn1関数からの戻り値は undefined
> setTimeout(fn1( "Taro" ), 1000)
Hi, Taro   // 最初にメッセージが表示されてしまう
Uncaught:  // 引数が 関数(function) ではなく undefined だという例外が発生
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
    at __node_internal_captureLargerStackTrace (node:internal/errors:477:5)
    at new NodeError (node:internal/errors:387:5)
    at __node_internal_ (node:internal/validators:233:11)
    at setTimeout (node:timers:141:3) {
  code: 'ERR_INVALID_CALLBACK'
}
> setTimeout(function(){fn1( "Taro" );}, 1000)
Timeout {  // setTimeoutからの戻り値(Timeoutオブジェクト)
  _idleTimeout: 1000,
  _idlePrev: [TimersList],
  _idleNext: [TimersList],
  _idleStart: 107226,
  _onTimeout: [Function (anonymous)],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(refed)]: true,
  [Symbol(kHasPrimitive)]: false,
  [Symbol(asyncId)]: 707,
  [Symbol(triggerId)]: 5
}
> Hi, Taro  // 1秒経過後にメッセージが表示される
5Like

Comments

  1. @hiroakirigit

    Questioner

    私のコードだと、setTimeOut関数の第一引数としてあるコールバック関数に渡していたのは、関数そのものではなく、関数による戻り値を渡していたんですね。。。

    エラー内容まで載せていただきありがとうございます。理解することができました!
  2. 理解できてよかったです。
    質問をクローズしておいてください。

解説については @shiracamus さんが書いてくださっている通りなのですが、setTimeout の引数の部分を一度変数においてみると理解しやすいかもしれません。

image.png

@hiroakirigit さんの回答
function delayMessageFactory( fn1, time ){
    function alert( greeting ){
        const x = fn1( greeting );
        setTimeout(x, time)

    }
    return alert;
}

image.png

テキスト内の回答例
function delayMessageFactory( fn1, time ){
    function alert( greeting ){
        const x = function() {
            fn1( greeting );
        };
        setTimeout(x, time)

    }
    return alert;
}

テキスト内の回答例では x は関数であり、setTimeout に関数を渡していることがわかるでしょうか?
@hiroakirigit さんの回答では xfn1( greeting ) を呼び出した結果の戻り値になってしまうので、うまく動かないというわけです。

ところで、実引数のconsole.logあるいはalertをそのまま返せば良いと思っていました。 は非常にいい疑問でして、もし引数を取らない関数を setTimeout に渡すのであれば、無名関数にくるむ必要はなくなります。ただし () をつけてはダメです。() は関数を実行し返り値を受け取ることを意味するからです。

// コンソールに "Hello" と表示する関数 showHello があったとして
function showHello() {
    console.log('hello');
}

// それを 1000ms 後に呼び出したいなら、関数をそのまま渡すことができる
setTimeout(showHello, 1000);
// setTimeout(showHello(), 1000); // ← 括弧をつけてしまうと、showHello の返り値を渡すことになるのでうまく動かない
3Like

Comments

  1. @hiroakirigit

    Questioner

    一度、変数に置き換えてみるのすごくわかりやすかったです!
    モヤモヤが解決しました!ありがとうございます。

    コールバック関数のとる引数の有無で、無名関数で包むか、そのまま関数を渡しても良いかも変わるんですね。奥が深くて、面白い。。。

Your answer might help someone💌