en.javascript.info/1-js/7-js-misc/4-eval/article.md
2015-03-10 14:43:25 +03:00

13 KiB
Raw Blame History

Запуск кода из строки: eval

Функция eval(code) позволяет выполнить код, переданный ей в виде строки.

Этот код будет выполнен в текущей области видимости. [cut]

Использование eval

В простейшем случае eval всего лишь выполняет код, например:

//+ run no-beautify
var a = 1;

(function() {

  var a = 2; 
  
*!*
  eval(' alert(a) '); // 2
*/!*

})()

Но он может не только выполнить код, но и вернуть результат.

Вызов eval возвращает последнее вычисленное выражение:

Например:

//+ run
alert( eval('1+1') ); // 2

При вызове eval имеет полный доступ к локальным переменным.

Это означает, что текущие переменные могут быть изменены или дополнены:

//+ untrusted refresh run
var x = 5;
eval(" alert( x ); x = 10"); // 5, доступ к старому значению
alert( x ); // 10, значение изменено внутри eval

[smart header="В строгом режиме eval имеет свою область видимости "] В строгом режиме функционал eval чуть-чуть меняется.

При use strict код внутри eval по-прежнему сможет читать и менять внешние переменные, однако переменные и функции, объявленные внутри eval, не попадут наружу.

//+ untrusted refresh run
"use strict";

*!*
eval("var a = 5; function f() { }");
*/!*
alert( a ); // ошибка, переменная не определена
// функция f тоже не видна снаружи

Иными словами, в новом стандарте eval имеет свою область видимости, а к внешним переменным обращается через замыкание, аналогично тому, как работают обычные функции. [/smart]

Неграмотное использование eval

Начнём с того, что eval применяется очень редко. Действительно редко. Есть даже такое выражение "eval is evil" (eval -- зло).

Причина проста: когда-то JavaScript был гораздо более слабым языком, чем сейчас, и некоторые вещи без eval было сделать невозможно. Но те времена давно прошли. И теперь найти тот случай, когда действительно надо выполнить код из строки -- это надо постараться.

Но если вы действительно знаете, что это именно тот случай и вам необходим eval -- есть ряд вещей, которые нужно иметь в виду.

Доступ к локальным переменным -- худшее, что можно сделать при eval.

Дело в том, что локальные переменные могут быть легко переименованы:

function sayHi() {
  var phrase = "Привет";
  eval(str);
}

Переменная phrase может быть переименована в hello, и если строка str обращается к ней -- будет ошибка.

Современные средства сжатия JavaScript переименовывают локальные переменные автоматически. Это считается безопасным, так как локальная переменная видна лишь внутри функции и если в ней везде поменять phrase на p, то никто этого не заметит.

До сжатия:

function sayHi() {
  var phrase = "Привет";
  alert( phrase );
}

После сжатия:

function sayHi() {
  var a = "Привет";
  alert( a );
}

На самом деле всё ещё проще -- в данном случае утилита сжатия автоматически уберёт переменную a и код станет таким:

function sayHi() {
  alert( "Привет" );
}

Итак, если где-то в функции есть eval, то его взаимодействие с локальными переменными будет нарушено с непредсказуемыми побочными эффектами.

Некоторые инструменты сжатия предупреждают, когда видят eval или стараются вообще не сжимать такой код вместе с его внешними функциями, но всё это борьба с последствиями кривого кода.

Как правило, eval не нужен, именно поэтому говорят, "eval is evil".

Запуск скрипта в глобальной области

Ок, взаимодействовать с локальными переменными нельзя.

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

Здесь eval может пригодиться. Есть два трюка для выполнения кода в глобальной области:

  1. Везде, кроме IE8-, достаточно вызвать `eval` не напрямую, а через `window.eval`.

    Вот так:

    //+ run no-beautify
    var a = 1;
    
    (function() {
    
      var a = 2; 
    *!*  
      window.eval(' alert(a) '); // 1, выполнено глобально везде, кроме IE8-
    */!*
    })();
    
  2. В IE8- можно применить нестандартную фунцию [execScript](http://msdn.microsoft.com/en-us/library/ie/ms536420%28v=vs.85%29.aspx). Она, как и `eval`, выполняет код, но всегда в глобальной области видимости и не возвращает значение.

Оба способа можно объединить в единой функции globalEval(code), выполняющей код без доступа к локальным переменным:

//+ run no-beautify
*!*
function globalEval(code) { // объединим два способа в одну функцию
  window.execScript ? execScript(code) : window.eval(code);
}
*/!*

var a = 1;

(function() {

  var a = 2; 
  
  globalEval(' alert(a) '); // 1, во всех браузерах

})();

Внешние данные через new Function

Итак, у нас есть код, который, всё же, нужно выполнить динамически, через eval, но не просто скрипт -- а ему нужно передать какие-то значения.

Как мы говорили ранее, считать их из локальных переменных нельзя: это подвержено ошибкам при переименовании переменных и сразу ломается при сжатии JavaScript. Да и вообще, неочевидно и криво.

К счастью, существует отличная альтернатива eval, которая позволяет корректно взаимодействовать c внешним кодом: new Function.

Вызов new Function('a,b', '..тело..') создает функцию с указанными аргументами a,b и телом. Как мы помним, доступа к текущему замыканию у такой функции не будет, но можно передать параметры и получить результат.

Например:

//+ run
var a = 2,
  b = 3;

*!*
// вместо обращения к a,b через eval
// будем принимать их как аргументы динамически созданной функции
var mul = new Function('a, b', ' return a * b;');
*/!*

alert( mul(a, b) ); // 6

JSON и eval

В браузерах IE7- не было методов JSON.stringify и JSON.parse, поэтому работа с JSON происходила через eval.

Этот способ работы с JSON давно устарел, но его можно встретить кое-где в старом коде, так что для примера рассмотрим его.

Вызов eval(code) выполняет код и, если это выражение, то возвращает его значение, поэтому можно в качестве кода передать JSON.

Например:

//+ run
var str = '{ \
    "name": "Вася", \
    "age": 25 \
}';

*!*
var user = eval('(' + str + ')');
*/!*

alert( user.name ); // Вася

Зачем здесь нужны скобки eval( '(' + str + ')' ), почему не просто eval(str)?

...Всё дело в том, что в JavaScript с фигурной скобки { начинаются не только объекты, а в том числе и "блоки кода". Что имеется в виду в данном случае -- интерпретатор определяет по контексту. Если в основном потоке кода -- то блок, если в контексте выражения, то объект.

Поэтому если передать в eval объект напрямую, то интерпретатор подумает, что это на самом деле блок кода, а там внутри какие-то двоеточия...

Вот, для примера, eval без скобок, он выдаст ошибку:

//+ run
var user = eval('{  "name": "Вася",  "age": 25  }');

А если eval получает выражение в скобках ( ... ), то интерпретатор точно знает, что это не блок кода, а объект:

//+ run
var user = eval('( {  "name": "Вася",  "age": 25  } )');
alert( user.age ); // 25

[warn header="Осторожно, злой JSON!"] Если мы получаем JSON из недоверенного источника, например с чужого сервера, то разбор через eval может быть опасен.

Например, чужой сервер может быть взломан (за свой-то код мы отвечаем, а за чужой -- нет) и вместо JSON вставлен злонамеренный JavaScript-код.

Поэтому рекомендуется, всё же, использовать JSON.parse.

При разборе через JSON.parse некорректный JSON просто приведёт к ошибке, а вот при разборе через eval этот код реально выполнится, он может вывести что-то на странице, перенаправить посетителя куда-то и т.п. [/warn]

Итого

  • Функция `eval(str)` выполняет код и возвращает последнее вычисленное выражение. В современном JavaScript она используется редко.
  • Вызов `eval` может читать и менять локальные переменные. Это -- зло, которого нужно избегать.
  • Для выполнения скрипта в глобальной области используются трюк с `window.eval/execScript`. При этом локальные переменные не будут затронуты, так что такое выполнение безопасно и иногда, в редких архитектурах, может быть полезным.
  • Если нужно выполняемый код всё же должен взаимодействовать с локальными переменными -- используйте `new Function`. Создавайте функцию из строки и передавайте переменные ей, это надёжно и безопасно.

Ещё примеры использования eval вы найдёте далее, в главе .