Why does an unexecuted eval have an effect on behavior in some browsers?

前端 未结 3 1166
死守一世寂寞
死守一世寂寞 2020-12-24 14:02

Let\'s suppose I have these two functions:

function change(args) {
    args[0] = \"changed\";
    return \" => \";
}
function f(x) {
    return [x, change         


        
3条回答
  •  谎友^
    谎友^ (楼主)
    2020-12-24 14:15

    TL;DR

    The likely cause is the interaction of the non-standard function.arguments with browser optimizations for function code containing eval and/or arguments. However, only people familiar with the implementation details on each browser would be able to explain the why in depth.

    The main problem here appears to be the use of the non-standard Function.prototype.arguments. When you don't use it, the strange behavior goes away.

    The specification only mentions the arguments object, and never says it might be treated as a property, prefixed with [funcName].. I'm not sure where that came from, but it's probably something pre-ES3, kept on browsers for backward compatibility. As Cory's answer states, that use is now discouraged on MDN. MSDN, however, doesn't say anything against it. I also found it mentioned on this specification for compatibility between browsers*, which does not appear to be implemented consistently by vendors (no browser pass all tests). Also, using arguments as a property of the function is not allowed in strict mode (again, that's not in the ECMA specs, and IE9 seems to ignore the restriction).

    Then come eval and arguments. As you are aware, the ECMAScript specification requires some extra operations to be performed so those language constructs can be used (in the case of eval, the operation is different depending on the call being direct or not). Since those operations can have an impact on performance, (some?) JavaScript engines perform optimizations to avoid them if eval or arguments are not used. Those optimizations, combined with the use of a non-standard property of the Function object, seem to be what's causing the strange results you got. Unfortunately, I don't know the implementation details for each browser, so I can't give you a precise answer on why we see those collateral effects.

    (*) Spec written by an SO user, by the way.

    Tests

    I ran some tests to see how eval (direct and indirect calls), arguments and fn.arguments interact on IE, Firefox and Chrome. It's not surprising that the results vary on each browser, since we're dealing with the non-standard fn.arguments.

    The first test just checks for strict equality of fn.arguments and arguments, and if the presence of eval affects that in any way. Inevitably, my Chrome tests are contamined by the presence of arguments, which has an effect on the results, as you said in the question. Here are the results:

                           |  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
    

    You can see IE and Firefox are more consistent: the objects are always equal on IE, and never equal on Firefox. In Chrome, however, they're only equal if the function code does not contain a direct eval call.

    The remaining tests are assignment tests based on functions that look like the following:

    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)("")
    }
    

    Below are the results for each tested browser.

    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
    

    First of all, it seems my IE tests give different results than what is stated in the question; I always get "changed" on IE. Maybe we used different IE builds? Anyway, what the results above show is that IE is the most consistent browser. As on IE arguments === fn.arguments is always true, x, arguments[0] or function.arguments[0] all point to the same value. If you change any of them, all three will output the same changed value.

    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 is less consistent: although arguments is never === fn.arguments on Firefox, eval has an effect on the assignments. Without a direct call to eval, changing arguments[0] changes x too, but does not change fn.arguments[0]. Changing fn.arguments[0] does not change either x or arguments[0]. It was a complete surprise that changing fn.arguments[0] does not change itself!

    When eval("") is introduced, the behavior is different: changing one of x, arguments[0] or function.arguments[0] starts affecting the other two. So it's like arguments becomes === function.arguments – except that it does not, Firefox still says that arguments === function.arguments is false. When an indirect eval call is used instead, Firefox behaves as if there were no eval.

    Chrome 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's behavior is similar to Firefox's: when there is no eval or an indirect eval call, it behaves consistently. With the direct eval call, the link between arguments and fn.arguments seem to break (which makes sense, considering arguments === fn.arguments is false when eval("") is present). Chrome also presents the weird case of fn.arguments[0] being original even after assignment, but it happens when eval("") is present (while on Firefox it happens when there's no eval, or with the indirect call).

    Here is the full code for the tests, if anyone wants to run them. There's also a live version on 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');
    

提交回复
热议问题