20

次の 2 つの関数があるとします。

function change(args) {
    args[0] = "changed";
    return " => ";
}
function f(x) {
    return [x, change(f.arguments), x];
}
console.log(f("original"));

Opera を除くほとんどのブラウザでは、これは を返します["original", " => ", "original"]

しかし、fこのように関数を変更すると、

function f(x) {
    return [x, change(f.arguments), x];
    eval("");
}

["original", " => ", "changed"]IE9、Safari 5、Firefox 16 および 17 で復活します。

に置き換えるeval("")arguments、Chrome でも変更されます。

jsFiddle の独自のブラウザーでテストできます。

この行動が全くわかりません。これらのステートメントが実行される前に関数が戻った場合、それらのステートメントは戻り値にどのように影響しますか? ステートメントが実行されたとしても、なぜそれらは引数の変更に何らかの影響を与えるのでしょうか?

4

3 に答える 3

9

TL;DR

考えられる原因は、および/またはを含む関数コードの非標準function.argumentsとブラウザの最適化の相互作用です。ただし、各ブラウザーの実装の詳細に詳しい人だけが、その理由を詳しく説明できます。evalarguments

ここでの主な問題は、非標準の使用にあるようFunction.prototype.argumentsです。使用しないと、奇妙な動作はなくなります。

arguments仕様ではobjectについてのみ言及しており、接頭辞が[funcName].. それがどこから来たのかはわかりませんが、おそらく ES3 以前のもので、下位互換性のためにブラウザーに保持されていました。Cory's answer が述べているように、その使用は現在 MDN では推奨されていませんただし、 MSDNはそれに対して何も述べていません。また、ブラウザ間の互換性についてこの仕様で言及されていることもわかりました*。これは、ベンダーによって一貫して実装されているようには見えません (すべてのテストに合格するブラウザはありません)。また、arguments関数のプロパティとして使用することは、厳密モードでは許可されていません (繰り返しますが、これは ECMA 仕様にはなく、IE9 は制限を無視しているようです)。

それから来evalargumentsご存知のように、ECMAScript 仕様では、これらの言語構造を使用できるように、いくつかの追加 操作を実行する必要があります (の場合、呼び出しが直接evalかどうかによって操作が異なります)。これらの操作はパフォーマンスに影響を与える可能性があるため、(一部?) JavaScript エンジンは最適化を実行して、使用されていない場合はそれらを回避します。これらの最適化は、オブジェクトの非標準プロパティの使用と組み合わされて、得られた奇妙な結果を引き起こしているようです。残念ながら、各ブラウザの実装の詳細がわからないため、その理由について正確な回答はできません。evalargumentsFunctionこれらの副次的効果が見られます。

(*)ちなみにSO ユーザーが書いた仕様です。

テスト

eval方法(直接呼び出しと間接呼び出し)を確認し、IE、Firefox、および Chrome で対話するargumentsためのいくつかのテストを実行しました。fn.arguments非標準のfn.arguments.

fn.arguments最初のテストでは、 と が厳密に等しいかどうか、およびargumentsの存在がeval何らかの形で影響するかどうかをチェックするだけです。必然的に、あなたが質問で言ったように、私の Chrome テストは の存在によって汚染されarguments、結果に影響を与えます。結果は次のとおりです。

                       |  no eval  |  direct eval call  |  indirect eval call
-----------------------+-----------+--------------------+---------------------
IE 9.0.8112.16421      |  true     |  true              |  true
FF 16.0.2              |  false    |  false             |  false
Chrome 22.0.1229.94    |  true     |  false             |  true

IE と Firefox のほうが一貫性があることがわかります。オブジェクトは IE では常に等しく、Firefox では決して等しくありません。evalただし、Chrome では、関数コードに直接呼び出しが含まれていない場合にのみ同等です。

残りのテストは、次のような関数に基づく割り当てテストです。

function fn(x) {
    // Assignment to x, arguments[0] or fn.arguments[0]
    console.log(x, arguments[0], fn.arguments[0]);
    return; // make sure eval is not actually called
    // No eval, eval(""), or (1,eval)("")
}

以下は、テストされた各ブラウザの結果です。

Internet Explorer 9.0.8112.16421

                             | no eval                   | direct eval call          | indirect eval call
-----------------------------+---------------------------+---------------------------+--------------------------
arguments[0] = 'changed';    | changed, changed, changed | changed, changed, changed | changed, changed, changed
x = 'changed';               | changed, changed, changed | changed, changed, changed | changed, changed, changed
fn.arguments[0] = 'changed'; | changed, changed, changed | changed, changed, changed | changed, changed, changed

まず第一に、私の IE テストでは、質問に記載されている結果とは異なる結果が得られたようです。私は常にIEで「変更」されます。異なる IE ビルドを使用したのでしょうか。とにかく、上記の結果が示しているのは、IE が最も一貫性のあるブラウザーであることです。IEarguments === fn.argumentsと同じように、常に true 、xarguments[0]またはfunction.arguments[0]すべてが同じ値を指します。それらのいずれかを変更すると、3 つすべてが同じ変更された値を出力します。

Firefox 16.0.2

                             | no eval                      | direct eval call          | indirect eval call
-----------------------------+------------------------------+---------------------------+-----------------------------
arguments[0] = 'changed';    | changed, changed, original   | changed, changed, changed | changed, changed, original
x = 'changed';               | changed, changed, original   | changed, changed, changed | changed, changed, original
fn.arguments[0] = 'changed'; | original, original, original | changed, changed, changed | original, original, original

Firefox 16.0.2 はあまり一貫性がargumentsありません === fn.arguments: Firefox にはありませんevalが、割り当てに影響があります。を直接呼び出さないとeval、変更arguments[0]も変更されますが、変更されxませんfn.arguments[0]。変更しても も も変更fn.arguments[0]されませxarguments[0]fn.arguments[0]変えても変わらないというのは全くの驚きでした!

eval("")導入されると、動作が異なります。 の 1 つを変更するか、x他の 2 つに影響を与え始めます。そうでないことを除いて、Firefox はまだそれがであると言います。代わりに間接呼び出しを使用すると、Firefox は が存在しないかのように動作します。arguments[0]function.arguments[0]arguments=== function.argumentsarguments === function.argumentsfalseevaleval

クロム 22.0.1229.94

                             | no eval                    | direct eval call             | indirect eval call
-----------------------------+----------------------------+------------------------------+--------------------------
arguments[0] = 'changed';    | changed, changed, changed  | changed, changed, original   | changed, changed, changed
x = 'changed';               | changed, changed, changed  | changed, changed, original   | changed, changed, changed
fn.arguments[0] = 'changed'; | changed, changed, changed  | original, original, original | changed, changed, changed

Chrome の動作は Firefox の動作と似ています。つまり、呼び出しがないeval場合、または間接eval呼び出しがある場合、一貫して動作します。直接eval呼び出しでは、 と の間のリンクがarguments壊れているように見えます (これは、がいつ存在fn.argumentsするかを考えると理にかなっています)。Chromeでは、代入後も存在するという奇妙なケースが示されますが、 が存在する場合に発生します (Firefox では、 がない場合、または間接呼び出しがある場合に発生します)。arguments === fn.argumentsfalseeval("")fn.arguments[0]originaleval("")eval

誰かがそれらを実行したい場合は、テストの完全なコードを次に示します。jsfiddle にもライブ バージョンがあります。

function t1(x) {
    console.log("no eval: ", arguments === t1.arguments);
}
function t2(x) {
    console.log("direct eval call: ", arguments === t2.arguments);
    return;
    eval("");
}
function t3(x) {
    console.log("indirect eval call: ", arguments === t3.arguments);
    return;
    (1, eval)("");
}
    
// ------------
    
function t4(x) {
    arguments[0] = 'changed';
    console.log(x, arguments[0], t4.arguments[0]);
}
    
function t5(x) {
    x = 'changed';
    console.log(x, arguments[0], t5.arguments[0]);
}
    
function t6(x) {
    t6.arguments[0] = 'changed';
    console.log(x, arguments[0], t6.arguments[0]);
}
    
// ------------
    
function t7(x) {
    arguments[0] = 'changed';
    console.log(x, arguments[0], t7.arguments[0]);
    return;
    eval("");
}
    
function t8(x) {
    x = 'changed';
    console.log(x, arguments[0], t8.arguments[0]);
    return;
    eval("");
}
    
function t9(x) {
    t9.arguments[0] = 'changed';
    console.log(x, arguments[0], t9.arguments[0]);
    return;
    eval("");
}
    
// ------------
    
function t10(x) {
    arguments[0] = 'changed';
    console.log(x, arguments[0], t10.arguments[0]);
    return;
    (1, eval)("");
}
    
function t11(x) {
    x = 'changed';
    console.log(x, arguments[0], t11.arguments[0]);
    return;
    (1, eval)("");
}
    
function t12(x) {
    t12.arguments[0] = 'changed';
    console.log(x, arguments[0], t12.arguments[0]);
    return;
    (1, eval)("");
}
    
// ------------
    
console.log("--------------");
console.log("Equality tests");
console.log("--------------");
t1('original');
t2('original');
t3('original');
    
console.log("----------------");
console.log("Assignment tests");
console.log("----------------");
console.log('no eval');
t4('original');
t5('original');
t6('original');
console.log('direct call to eval');
t7('original');
t8('original');
t9('original');
console.log('indirect call to eval');
t10('original');
t11('original');
t12('original');
于 2012-11-07T16:53:04.720 に答える
3

遊んでみると、配列f.の値からを削除し、 を使用するだけで、 の後に何が来ても動作は同じであることがわかりました。f.argumentsargumentsreturn

function f(x) {
    return [x, change(arguments), x];
}
function g(x) {
    return [x, change(arguments), x];
    eval("");
}
function h(x) {
    return [x, change(arguments), x];
    arguments;
}

を使用した 3 つのケースすべてx = "original"で、出力は次のようになります。

["original", " => ", "changed"]
["original", " => ", "changed"] 
["original", " => ", "changed"]

この場合、配列が参照によって渡されたchange()かのように値が変更されます。arguments「オリジナル」を変更argumentsせずに保つために、最初にオブジェクトを実際の配列に変換することをお勧めします(したがって、arguments要素を値で渡します):

function h(x) {
    var argsByValue = Array.prototype.slice.call(arguments, 0);
    return [x, change(argsByValue), x];
}

上記の例でxは、 は の前後で「オリジナル」のままです。これは、オリジナルではなく のchange()コピーが変更されたためです。x

eval("");orを持っていることの影響はまだわかりませんarguments;が、結果と同様に、あなたの質問は興味深いものです。

本当に奇妙なのは、これchange()が、関数引数のコピーを使用して独自の関数スコープに配置することにも影響することです。

function f(x) {
    return ((function(args) {             
        return [x, change(args), x];
    })(f.arguments));
    // the presence of the line below still alters the behavior
    arguments; 
}

この場合、への参照f.argumentsまだ保持されているようです。奇妙なもの。

アップデート

MDNから:

オブジェクトは、argumentsすべての関数内で使用できるローカル変数です。argumentsのプロパティとして、Function使用できなくなりました。

少なくとも Firefox では、argumentsプロパティ (例: function foo() { var bar = foo.arguments; }) として使用すべきではないようですが、理由はわかりません。

于 2012-08-13T18:36:33.673 に答える
1

有効になるいくつかの優れたJavascriptのニュアンスは次のとおりです。

change(f.arguments)
change(x)

前者は、引数リストを参照としてchange()に渡します。配列はJavascriptの参照になる傾向があります。つまり、配列の要素を別の場所で変更した場合、それらの変更は、同じ配列を使用するすべての場所に適用されます。

後者は引数xを値として渡します。これは、コピーを渡すようなものです。変更すると変更される可能性があり、ローカル変数にのみ影響します。xは文字列であり、文字列は不変であるため、change()関数のargs [0]="changed"は何もしません。コンソールで次のことを試してください。

var x = "asdf";
x[0] = "foo";
console.log(x); // should print "asdf"

f、h、g関数では、arguments [0]の値は、返されたリストの2番目のインデックスで変更されます。3番目のインデックスは「変更済み」を返します。

理論的には。ただし、一部のブラウザはJavascriptをコンパイルするため、競合状態や命令が入力した順序で実行されない場合があります。特に、同じ行にあり、スタックを変更して同じ行からアクセスしている場合はそうです。

return [x, change(f.arguments), x];

...引数変数の変更とx(引数)へのアクセスを同時に試みます。たとえば、Chromeでは、f.argumentsをchange()に渡すと["original"、 "=>"、 "original"]になり、引数だけを渡すと["original"、 "=>"、"changed"]になります。 。これは、スコープの問題やJavascriptが値と参照型を処理する方法でもある可能性がありますが、その動作はブラウザーによって異なります。

私が説明したことを考えると、eval()で奇妙な動作は見られませんでしたが、戻り値の後にh()関数で引数を指定すると、ChromeによるJavascriptのコンパイルが原因であると思われる副作用が発生するようです。本当に興味深いのは、シェルが内部的にその値を返すことによって変数を実行することですが、それはどこにも書き込まれておらず、おそらくキャッシュを期待しています。Javascriptのスタックで何が起こっているのかを知るのは難しいですが、あなたがしていることは確かに型破りであり、ブラウザ間でコンパイラを混乱させることは間違いありません。

編集:

さらに良い:console.log(h.arguments); return [x、change(arguments)、x]; 引数

ログに記録されます

["changed"]
["original", " => ", "changed"]

確かに、競合状態、または関数内の引数配列への参照のいくつかの奇妙な受け渡しのように見えます!

于 2012-08-13T18:33:08.187 に答える