20

假设我有这两个功能:

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;博士

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

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

规范只提到了argumentsobject,并没有说它可以被视为一个属性,前缀为[funcName].. 我不确定它来自哪里,但它可能是 ES3 之前的东西,为了向后兼容而保留在浏览器上。正如 Cory 的回答所说,现在不鼓励在 MDN 上使用这种方法。然而,MSDN并没有对此表示任何反对。我还发现本规范中提到了浏览器之间的兼容性*,供应商似乎并没有一致地实现它(没有浏览器通过所有测试)。此外,arguments在严格模式下不允许用作函数的属性(同样,这不在 ECMA 规范中,IE9 似乎忽略了限制)。

然后来evalarguments如您所知,ECMAScript 规范需要执行一些额外的 操作,以便可以使用这些语言结构(在 的情况下,操作因调用是否直接eval而异)。由于这些操作可能会对性能产生影响,因此(有些?)JavaScript 引擎会执行优化以避免使用或不使用它们。这些优化,加上对象的非标准属性的使用,似乎是导致你得到奇怪结果的原因。不幸的是,我不知道每个浏览器的实现细节,所以我不能给你一个确切的答案为什么evalargumentsFunction我们看到了这些附带影响。

(*)顺便说一下,由SO 用户编写的规范。

测试

我进行了一些测试以了解eval(直接和间接调用)如何在 IE、Firefoxargumentsfn.argumentsChrome 上进行交互。每个浏览器的结果不同并不奇怪,因为我们正在处理非标准的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)("")
}

以下是每个测试浏览器的结果。

互联网浏览器 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上始终为真,xarguments[0]function.arguments[0]指向相同的值。如果您更改其中任何一个,所有三个都将输出相同的更改值。

火狐 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.argumentsFirefox 上,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.

铬 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中断了(这是有道理的,考虑到何时arguments === fn.arguments存在)。Chrome 还呈现了即使在分配之后仍然存在的奇怪情况,但它会在存在时发生(而在 Firefox 上,它会在没有或间接调用时发生)。falseeval("")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.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; }),尽管他们没有说明原因。

于 2012-08-13T18:36:33.673 回答
1

这里有一些优秀的 Javascript 细微差别开始生效:

change(f.arguments)
change(x)

前者将参数列表作为参考传递给 change() 。数组往往是 Javascript 中的引用。这意味着如果您在其他地方更改数组的元素,这些更改将应用​​到您使用相同数组的任何地方。

后者将参数 x作为 value传递。这就像交出一份副本 - 更改可以改变它,它只会影响局部变量。因为 x 是一个字符串,并且字符串是不可变的,所以 change() 函数中的 args[0] = "changed" 不会做任何事情。在控制台中尝试以下操作:

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

在 f、h、g 函数中,arguments[0] 的值在返回列表的第二个索引中发生变化。第三个索引将返回“已更改”。

理论上。但是,某些浏览器编译 Javascript,这会导致某种竞争条件,并且指令可能不会按照您键入它们的顺序执行,特别是如果它们在同一行并且您正在更改堆栈并从同一行访问它。

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

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

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

编辑:

更好的是:console.log(h.arguments); 返回[x,更改(参数),x];论据

将记录

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

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

于 2012-08-13T18:33:08.187 回答