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

266 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Запуск кода из строки: eval
Функция `eval(code)` позволяет выполнить код, переданный ей в виде строки.
Этот код будет выполнен в *текущей области видимости*.
[cut]
## Использование eval
В простейшем случае `eval` всего лишь выполняет код, например:
```js
//+ run no-beautify
var a = 1;
(function() {
var a = 2;
*!*
eval(' alert(a) '); // 2
*/!*
})()
```
Но он может не только выполнить код, но и вернуть результат.
**Вызов `eval` возвращает последнее вычисленное выражение**:
Например:
```js
//+ run
alert( eval('1+1') ); // 2
```
**При вызове `eval` имеет полный доступ к локальным переменным.**
Это означает, что текущие переменные могут быть изменены или дополнены:
```js
//+ untrusted refresh run
var x = 5;
eval(" alert( x ); x = 10"); // 5, доступ к старому значению
alert( x ); // 10, значение изменено внутри eval
```
[smart header="В строгом режиме `eval` имеет свою область видимости "]
В строгом режиме функционал `eval` чуть-чуть меняется.
При `use strict` код внутри `eval` по-прежнему сможет читать и менять внешние переменные, однако переменные и функции, объявленные внутри `eval`, не попадут наружу.
```js
//+ 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`.
Дело в том, что локальные переменные могут быть легко переименованы:
```js
function sayHi() {
var phrase = "Привет";
eval(str);
}
```
Переменная `phrase` может быть переименована в `hello`, и если строка `str` обращается к ней -- будет ошибка.
Современные средства сжатия JavaScript переименовывают локальные переменные автоматически. Это считается безопасным, так как локальная переменная видна лишь внутри функции и если в ней везде поменять `phrase` на `p`, то никто этого не заметит.
До сжатия:
```js
function sayHi() {
var phrase = "Привет";
alert( phrase );
}
```
После сжатия:
```js
function sayHi() {
var a = "Привет";
alert( a );
}
```
На самом деле всё ещё проще -- в данном случае утилита сжатия автоматически уберёт переменную `a` и код станет таким:
```js
function sayHi() {
alert( "Привет" );
}
```
Итак, если где-то в функции есть `eval`, то его взаимодействие с локальными переменными будет нарушено с непредсказуемыми побочными эффектами.
Некоторые инструменты сжатия предупреждают, когда видят `eval` или стараются вообще не сжимать такой код вместе с его внешними функциями, но всё это борьба с последствиями кривого кода.
Как правило, `eval` не нужен, именно поэтому говорят, "eval is evil".
## Запуск скрипта в глобальной области
Ок, взаимодействовать с локальными переменными нельзя.
Но допустим мы загрузили с сервера или вручную сгенерировали скрипт, который нужно выполнить. Желательно, в глобальной области, вне любых функций, чтобы он уж точно к локальным переменным отношения не имел.
Здесь `eval` может пригодиться. Есть два трюка для выполнения кода в глобальной области:
<ol>
<li>Везде, кроме IE8-, достаточно вызвать `eval` не напрямую, а через `window.eval`.
Вот так:
```js
//+ run no-beautify
var a = 1;
(function() {
var a = 2;
*!*
window.eval(' alert(a) '); // 1, выполнено глобально везде, кроме IE8-
*/!*
})();
```
</li>
<li>В IE8- можно применить нестандартную фунцию [execScript](http://msdn.microsoft.com/en-us/library/ie/ms536420%28v=vs.85%29.aspx). Она, как и `eval`, выполняет код, но всегда в глобальной области видимости и не возвращает значение.</li>
</ol>
Оба способа можно объединить в единой функции `globalEval(code)`, выполняющей код без доступа к локальным переменным:
```js
//+ 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` и телом. Как мы помним, доступа к текущему замыканию у такой функции не будет, но можно передать параметры и получить результат.
Например:
```js
//+ 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.
Например:
```js
//+ run
var str = '{ \
"name": "Вася", \
"age": 25 \
}';
*!*
var user = eval('(' + str + ')');
*/!*
alert( user.name ); // Вася
```
Зачем здесь нужны скобки `eval( '(' + str + ')' )`, почему не просто `eval(str)`?
...Всё дело в том, что в JavaScript с фигурной скобки `{` начинаются не только объекты, а в том числе и "блоки кода". Что имеется в виду в данном случае -- интерпретатор определяет по контексту. Если в основном потоке кода -- то блок, если в контексте выражения, то объект.
Поэтому если передать в `eval` объект напрямую, то интерпретатор подумает, что это на самом деле блок кода, а там внутри какие-то двоеточия...
Вот, для примера, `eval` без скобок, он выдаст ошибку:
```js
//+ run
var user = eval('{ "name": "Вася", "age": 25 }');
```
А если `eval` получает выражение в скобках `( ... )`, то интерпретатор точно знает, что это не блок кода, а объект:
```js
//+ 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]
## Итого
<ul>
<li>Функция `eval(str)` выполняет код и возвращает последнее вычисленное выражение. В современном JavaScript она используется редко.</li>
<li>Вызов `eval` может читать и менять локальные переменные. Это -- зло, которого нужно избегать.</li>
<li>Для выполнения скрипта в глобальной области используются трюк с `window.eval/execScript`. При этом локальные переменные не будут затронуты, так что такое выполнение безопасно и иногда, в редких архитектурах, может быть полезным.</li>
<li>Если нужно выполняемый код всё же должен взаимодействовать с локальными переменными -- используйте `new Function`. Создавайте функцию из строки и передавайте переменные ей, это надёжно и безопасно.</li>
</ul>
Ещё примеры использования `eval` вы найдёте далее, в главе [](/json).