28

この疑似例のように、ループ内で無名関数を呼び出すコードがいくつかあります。

for (i = 0; i < numCards; i = i + 1) {
    card = $('<div>').bind('isPopulated', function (ev) {
        var card = $(ev.currentTarget);
        ....

JSLintは、「ループ内で関数を作成しないでください」というエラーを報告します。コードJSLintをクリーンに保つのが好きです。匿名関数をループの外に移動して、名前付き関数として呼び出すことができることはわかっています。それはさておき、ここに私の質問があります:

Javascriptインタープリターは、実際に反復ごとに関数のインスタンスを作成しますか?または、実際には「コンパイル」された関数インスタンスが1つだけで、同じコードが繰り返し実行されるのでしょうか。つまり、関数をループの外に移動するJSLintの「提案」は、実際にコードの効率に影響を与えますか?

4

4 に答える 4

42

部分的には、関数式または関数宣言のどちらを使用しているかによって異なります。それらは異なるものであり、異なるタイミングで発生し、周囲のスコープに異なる影響を与えます。それでは、区別から始めましょう。

関数function、結果を右側の値として使用するプロダクションです。たとえば、結果を変数またはプロパティに代入したり、パラメータとして関数に渡したりします。これらはすべて関数です。:

setTimeout(function() { ... }, 1000);

var f = function() {  ... };

var named = function bar() { ... };

(名前付き関数式と呼ばれる最後のものは使用しないでください。 実装にはバグがあり、特に IEがあります。)

対照的に、これは関数宣言です:

function bar() { ... }

それはスタンドアロンです。結果を右側の値として使用していません。

それらの間の2つの主な違い:

  1. 関数式は、プログラム フローで検出された場所で評価されます。宣言は、制御が含まれるスコープ (たとえば、含まれる関数またはグローバル スコープ) に入ったときに評価されます。

  2. 関数の名前 (ある場合) は、関数宣言の包含スコープで定義されます。関数用ではありません(ブラウザーのバグを除く)。

あなたの無名関数は関数式であるため、インタープリターが最適化を行うことを禁止すると(これは自由に実行できます)、ループごとに再作成されます。したがって、実装が最適化されると思われる場合は使用しても問題ありませんが、名前付き関数に分割すると、他の利点があり、重要なことに、コストはかかりません。また、コードをどれだけ深く検査するかによって、インタープリターが各反復で関数の再作成を最適化できない理由についてのメモについては、カサブランカの回答を参照してください。

より大きな問題は、ループ、条件の本体などで関数宣言を使用した場合です。

function foo() {
    for (i = 0; i < limit; ++i) {
        function bar() { ... } // <== Don't do this
        bar();
    }
}

技術的には、仕様の文法をよく読むと、それを行うのは無効であることがわかりますが、実際にそれを強制する実装は事実上ありません。実装が行うことはさまざまであり、それを避けるのが最善です。

私のお金のために、あなたの最善の策は、次のように単一の関数宣言を使用することです:

function foo() {
    for (i = 0; i < limit; ++i) {
        bar();
    }

    function bar() {
        /* ...do something, possibly using 'i'... */
    }
}

同じ結果が得られます。実装がすべてのループで新しい関数を作成する可能性はありません。名前を持つ関数の利点が得られ、何も失うことはありません。

于 2010-10-13T19:06:46.497 に答える
24

Javascript インタープリターは、反復ごとに関数のインスタンスを本当に作成しますか?

関数オブジェクトが他の場所で変更されるかどうかがわからないため、そうする必要があります。関数は標準の JavaScript オブジェクトであるため、他のオブジェクトと同様にプロパティを持つことができることに注意してください。これを行う場合:

card = $('<div>').bind('isPopulated', function (ev) { ... })

ご存知のとおり、bindオブジェクトを変更できます。たとえば、次のようになります。

function bind(str, fn) {
  fn.foo = str;
}

関数オブジェクトがすべての反復で共有されている場合、これは明らかに間違った動作になります。

于 2010-10-13T19:25:03.523 に答える
2

インタプリタは、その関数が外部スコープ内の変数の現在の値をキャプチャする必要があるクロージャである可能性があるという理由だけで、反復ごとに新しい関数オブジェクトを実際に作成する場合があります。

そのためJSLint、タイトなループで多くの無名関数を作成することからあなたを怖がらせたいのです。

于 2010-10-13T19:03:08.707 に答える
2

JSLintにブー。それは頭の上の鈍器のようなものです。新しい関数オブジェクトは、遭遇するたびに作成されますfunction(宣言ではなくステートメント/式です --編集: これは白い嘘です。TJ Crowders の回答を参照してください)。通常、これはクロージャーなどのループで行われます。より大きな問題は、誤ったクロージャーを作成することです。

例えば:

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    alert(i)
  }, 10)
}

「奇妙な」動作になります。これは、JS が変数のスコープとクロージャーに使用する規則を理解していないため、「ループ内で関数を作成すること」の問題ではありません (変数はクロージャーではバインドされず、スコープ (実行コンテキスト) はバインドされています)。

ただし、関数内にクロージャを作成したい場合があります。次のそれほど驚くべきコードを考えてみましょう:

for (var i = 0; i < 10; i++) {
  setTimeout((function (_i) { 
    return function () {
      alert(_i)
    }
  })(i), 10)
}

大野!私はまだ関数を作成しました!

于 2010-10-13T19:08:24.070 に答える