Подтвердить что ты не робот

Почему неисследованный eval влияет на поведение в некоторых браузерах?

Предположим, что у меня есть две функции:

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.

Я вообще не понимаю этого поведения. Если функция возвращается до выполнения этих операторов, как эти утверждения могут влиять на возвращаемое значение? Даже если инструкции выполнялись, почему они могут повлиять на мутацию аргумента?

4b9b3361

Ответ 1

TL; DR

Вероятная причина - взаимодействие нестандартного function.arguments с оптимизацией браузера для кода функции, содержащего eval и/или arguments. Тем не менее, только люди, знакомые с деталями реализации в каждом браузере, смогут объяснить, почему в глубину.

Основная проблема здесь заключается в использовании нестандартного Function.prototype.arguments. Когда вы его не используете, странное поведение уходит.

В спецификации упоминается объект arguments и никогда не говорит, что его можно рассматривать как свойство с префиксом [funcName].. Я не уверен, откуда это взялось, но, вероятно, что-то пред-ES3, хранится в браузерах для обратной совместимости. Как сказано в ответе Кори, использование теперь обескуражено в MDN. MSDN, однако ничего не говорит против него. Я также нашел в нем упомянутую * которая, как представляется, не выполняется последовательно поставщиков (браузер не пропускает все tests). Кроме того, использование arguments в качестве свойства функции не допускается в строгом режиме (опять же, это не в спецификациях ECMA, а IE9, похоже, игнорирует ограничение).

Затем наберите eval и arguments. Как вам известно, спецификация ECMAScript требует некоторого extra операции, чтобы эти языковые конструкции могли использоваться (в случае eval операция отличается в зависимости от того, что вызов direct или нет). Поскольку эти операции могут влиять на производительность, некоторые (некоторые?) JavaScript-движки выполняют оптимизации, чтобы избежать их, если eval или arguments не используются. Эти оптимизации, в сочетании с использованием нестандартного свойства объекта Function, по-видимому, вызывают у вас странные результаты. К сожалению, я не знаю детали реализации для каждого браузера, поэтому я не могу дать вам точный ответ о том, почему мы видим эти побочные эффекты.

(*) Spec, написанный пользователем SO, кстати.

Испытания

Я провел несколько тестов, чтобы увидеть, как eval (прямые и косвенные вызовы), arguments и fn.arguments взаимодействуют в IE, Firefox и Chrome. Не удивительно, что результаты зависят от каждого браузера, поскольку мы имеем дело с нестандартным 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. Однако в 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 является самым последовательным браузером. Как и в IE arguments === fn.arguments всегда верно, x, arguments[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 никогда не === fn.arguments в Firefox, eval влияет на назначения. Без прямого вызова eval изменение arguments[0] также меняет x, но не меняет fn.arguments[0]. Изменение fn.arguments[0] не изменяет ни x, ни arguments[0]. Было совершенно неожиданно, что изменение fn.arguments[0] не меняет себя!

Когда вводится eval(""), поведение отличается: изменение одного из x, arguments[0] или function.arguments[0] начинает влиять на другие два. Так что, как arguments становится === function.arguments – кроме того, что это не так, Firefox все еще говорит, что arguments === function.arguments - false. Когда вместо этого используется косвенный вызов 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 связь между arguments и fn.arguments кажется нарушенной (что имеет смысл, учитывая, что arguments === fn.arguments есть false, когда присутствует eval("")). Chrome также представляет странный случай fn.arguments[0] original даже после назначения, но это происходит, когда присутствует eval("") (в то время как в Firefox это происходит, когда нет 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');

Ответ 2

Просто играя, я обнаружил, что вы удаляете 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; }), хотя они не говорят, почему.

Ответ 3

Вот некоторые отличные javascript нюансы, вступающие в силу:

change(f.arguments)
change(x)

Первый передает список аргументов в change() как ссылку. Массивы, как правило, являются ссылками в Javascript. Это означает, что если вы измените элемент массива где-то еще, эти изменения будут применяться везде, где вы используете тот же массив.

Последний передает аргумент x в качестве значения. Это, как передача копии, может изменить ее, и это затронет только локальную переменную. Поскольку x является строкой, а строки неизменяемы, args [0] = "changed" в функции change() ничего не делает. Попробуйте выполнить следующие действия в консоли:

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

В функциях f, h, g значение аргументов [0] изменяется во втором индексе в возвращаемом списке. Третий индекс вернется "изменен".

Теоретически. Тем не менее, некоторые браузеры компилируют Javascript, что приводит к тому, что условия гонки и инструкции не могут выполняться в том порядке, в котором вы их вводите, особенно если они находятся на одной строке, и вы меняете стек и получаете доступ к нему из той же строки.

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

... пытается изменить переменную аргументов и получить доступ к x (который является аргументом) в одно и то же время. Например, в Chrome передача f.arguments для изменения() приводит к [ "original" , "= > ", "original" ] при передаче только аргументов, приводит к [ "original" , "= > ", "changed" ], Это также может быть проблема определения и как Javascript обрабатывает значения и ссылочные типы, но это поведение различно в браузерах.

Я не видел какого-либо нечетного поведения с eval(), учитывая то, что я описал, но кажется, что указание аргументов в функции h() после возврата создает побочный эффект, который, как я подозреваю, вызван компиляцией Chrome Javascript. Что действительно интересно, так это внутренне, оболочка выполняет переменную, возвращая ее значение, но она нигде не записывается, возможно, ожидает кэш. Трудно сказать, что происходит в стеке Javascript, но то, что вы делаете, безусловно, нетрадиционное, и это наверняка испортит компилятор в браузерах.

EDIT:

Еще лучше:   console.log(h.arguments);   return [x, change (arguments), x];   Аргументы

будет записываться

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

Конечно, это выглядит как условие гонки или некоторая неуловимая передача ссылок на массив аргументов внутри функций!