为什么未执行的eval会对某些浏览器中的行为产生影响?

时间:2012-08-13 18:00:36

标签: javascript

我们假设我有这两个功能:

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

它将在IE9,Safari 5和Firefox 16和17中返回["original", " => ", "changed"]

如果我将eval("")替换为arguments,则Chrome也会更改。

你可以test it on your own browser on jsFiddle.

我根本不明白这种行为。如果函数在执行这些语句之前返回,那么这些语句如何影响返回值?即使这些陈述被执行,为什么它们会对参数变异产生任何影响呢?

3 个答案:

答案 0 :(得分:9)

  

TL; DR

     

可能的原因是非标准function.arguments与包含eval和/或arguments的功能代码的浏览器优化的互动。但是,只有熟悉每个浏览器实现细节的人才能够深入解释为什么

这里的主要问题似乎是使用非标准Function.prototype.arguments。当你不使用它时,奇怪的行为就会消失。

规范仅提及arguments object,并且从不说它可能被视为属性,前缀为[funcName].。我不知道它来自哪里,但它可能是ES3之前的东西,为了向后兼容而保留在浏览器上。正如Cory的回答所述,使用的是now discouraged on MDN。但是,MSDN并没有反对它。我还发现它在specification for compatibility between browsers * 上提到,供应商似乎没有一致地实现(没有浏览器通过所有tests)。此外,在严格模式下不允许使用arguments作为函数的属性(同样,不在ECMA规范中,IE9似乎忽略了限制)。

然后来evalargumentsAs you are aware,ECMAScript规范要求执行一些extra operations,以便可以使用这些语言结构(在eval的情况下,操作因呼叫而异是direct还是没有。由于这些操作会对性能产生影响,(某些?)JavaScript引擎会执行优化,以避免在未使用evalarguments的情况下进行优化。这些优化与Function对象的非标准属性的使用相结合,似乎是导致您获得奇怪结果的原因。很遗憾,我不知道每个浏览器的实施细节,因此我无法就为什么我们看到这些附带效果给出准确的答案。

(*)顺便写一个SO user的规范。

测试

我运行了一些测试,以了解eval(直接和间接呼叫),argumentsfn.arguments如何在IE,Firefox和Chrome上进行互动。由于我们处理的是非标准fn.arguments,所以每个浏览器的结果都有所不同并不奇怪。

第一个测试只是检查fn.argumentsarguments的严格平等,以及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上永远不会相同。但是,在Chrome中,如果功能代码不包含直接eval调用,则它们仅相等。

其余测试是基于如下所示功能的分配测试:

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是最一致的浏览器。由于IE arguments === fn.arguments始终为真,xarguments[0]function.arguments[0]都指向相同的值。如果您更改其中任何一个,则所有三个都将输出相同的更改值。

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在Firefox上从不 === fn.arguments,但eval会对分配产生影响。如果没有直接调用eval,则更改arguments[0]也会更改x,但不会更改fn.arguments[0]。更改fn.arguments[0]不会更改xarguments[0]。令人惊讶的是,改变fn.arguments[0]并没有改变自己!

引入eval("")时,行为会有所不同:更改xarguments[0]function.arguments[0]中的一个会影响其他两个。因此arguments变为=== function.arguments - 除了它没有,Firefox仍然说arguments === function.argumentsfalse。当使用间接eval调用时,Firefox的行为就像没有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的行为类似于Firefox:当没有eval或间接eval调用时,它的行为一致。通过直接eval调用,argumentsfn.arguments之间的链接似乎已中断(这有意义,arguments === fn.arguments false eval("")存在)。即使在分配后,Chrome也会出现fn.arguments[0] original eval("")的奇怪情况,但是当eval出现时会发生这种情况(而在Firefox上,当没有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'); 时就会发生这种情况。 },或间接调用。)

以下是测试的完整代码,如果有人想运行它们。还有live version on jsfiddle

{{1}}

答案 1 :(得分:3)

只是玩游戏,我发现您从数组中的f.值中删除f.arguments,并且只使用arguments,无论后面发生什么,行为都是一样的return

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;
}

在使用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("");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; }),尽管他们没有说明原因。

答案 2 :(得分: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函数中,参数[0]的值在返回列表的第二个索引中更改。第三个指数将返回“已更改”。

理论上。但是,有些浏览器会编译Javascript,导致种类条件和指令可能无法按您键入的顺序执行,特别是如果它们位于同一行并且您正在更改堆栈并从同一行访问它。 / p>

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

...尝试同时更改参数变量并访问x(这是一个参数)。例如,在Chrome中,将f.arguments传递给change()会导致[“original”,“=&gt;”,“original”],而只传递参数会导致[“original”,“=&gt;”,“更改“。它也可能是一个范围问题以及Javascript如何处理值和引用类型,但这种行为在浏览器中是不同的。

根据我所描述的内容,我没有看到eval()的任何奇怪行为,但似乎在返回后在h()函数中声明参数会产生副作用,我怀疑这是由Chrome的编译引起的的Javascript。真正有趣的是,在内部,shell通过返回其值来执行变量,但它不会被写入任何地方,可能期望缓存。很难说Javascript的堆栈中发生了什么,但你正在做的事情肯定是非常规的,它肯定会在浏览器中搞乱编译器。

编辑:

更好:     的console.log(h.arguments);     return [x,change(arguments),x];     参数

将记录

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

当然看起来像竞争条件,或者在函数中对参数数组的引用有些不稳定!