eval()

警告:从字符串执行 JavaScript 存在巨大的安全风险。当您使用 eval() 时,恶意攻击者运行任意代码过于容易。请参阅下面的 切勿使用直接 eval()!

eval() 函数评估表示为字符串的 JavaScript 代码并返回其完成值。源代码被解析为脚本。

试一试

语法

js
eval(script)

参数

脚本

表示 JavaScript 表达式、语句或语句序列的字符串。表达式可以包含变量和现有对象的属性。它将被解析为脚本,因此 import 声明(只能存在于模块中)是不允许的。

返回值

评估给定代码的完成值。如果完成值为空,则返回 undefined。如果 script 不是字符串基元,则 eval() 会返回未更改的参数。

异常

抛出代码评估期间发生的任何异常,包括如果 script 无法解析为脚本的 SyntaxError

描述

eval() 是全局对象的函数属性。

eval() 函数的参数是一个字符串。它会将源字符串评估为脚本体,这意味着允许使用语句和表达式。它返回代码的完成值。对于表达式,它是表达式计算到的值。许多语句和声明也有完成值,但结果可能会令人惊讶(例如,赋值的完成值是赋值的值,但 let 的完成值是未定义的),因此建议不要依赖语句的完成值。

在严格模式下,声明名为 eval 的变量或重新赋值 evalSyntaxError

js
"use strict";

const eval = 1; // SyntaxError: Unexpected eval or arguments in strict mode

如果 eval() 的参数不是字符串,则 eval() 会返回未更改的参数。在以下示例中,传递 String 对象而不是基元会导致 eval() 返回 String 对象,而不是评估字符串。

js
eval(new String("2 + 2")); // returns a String object containing "2 + 2"
eval("2 + 2"); // returns 4

要以通用方式解决此问题,您可以在将参数传递给 eval() 之前 自行将其强制转换为字符串

js
const expression = new String("2 + 2");
eval(String(expression)); // returns 4

直接和间接 eval

eval() 调用有两种模式:直接 eval 和间接 eval。顾名思义,直接 eval 指的是直接使用 eval(...) 调用全局 eval 函数。其他所有内容,包括通过别名变量、通过成员访问或其他表达式或通过可选链 ?. 运算符调用它,都是间接的。

js
// Direct call
eval("x + y");

// Indirect call using the comma operator to return eval
(0, eval)("x + y");

// Indirect call through optional chaining
eval?.("x + y");

// Indirect call using a variable to store and return eval
const geval = eval;
geval("x + y");

// Indirect call through member access
const obj = { eval };
obj.eval("x + y");

间接 eval 可以看作是在单独的 <script> 标签中评估代码。这意味着

  • 间接 eval 在全局作用域而不是局部作用域中工作,并且被评估的代码无法访问调用它的作用域内的局部变量。
    js
    function test() {
      const x = 2;
      const y = 4;
      // Direct call, uses local scope
      console.log(eval("x + y")); // Result is 6
      // Indirect call, uses global scope
      console.log(eval?.("x + y")); // Throws because x is not defined in global scope
    }
    
  • 间接 eval 不会继承周围上下文的严格性,并且仅在 严格模式 下,如果源字符串本身具有 "use strict" 指令。
    js
    function nonStrictContext() {
      eval?.(`with (Math) console.log(PI);`);
    }
    function strictContext() {
      "use strict";
      eval?.(`with (Math) console.log(PI);`);
    }
    function strictContextStrictEval() {
      "use strict";
      eval?.(`"use strict"; with (Math) console.log(PI);`);
    }
    nonStrictContext(); // Logs 3.141592653589793
    strictContext(); // Logs 3.141592653589793
    strictContextStrictEval(); // Uncaught SyntaxError: Strict mode code may not include a with statement
    
    另一方面,直接 eval 继承调用上下文的严格性。
    js
    function nonStrictContext() {
      eval(`with (Math) console.log(PI);`);
    }
    function strictContext() {
      "use strict";
      eval(`with (Math) console.log(PI);`);
    }
    function strictContextStrictEval() {
      "use strict";
      eval(`"use strict"; with (Math) console.log(PI);`);
    }
    nonStrictContext(); // Logs 3.141592653589793
    strictContext(); // Uncaught SyntaxError: Strict mode code may not include a with statement
    strictContextStrictEval(); // Uncaught SyntaxError: Strict mode code may not include a with statement
    
  • 如果源字符串未在严格模式下解释,则 var 声明的变量和 函数声明 将进入周围的作用域——对于间接 eval,它们成为全局变量。如果它是严格模式上下文中的直接 eval,或者如果 eval 源字符串本身位于严格模式下,则 var 和函数声明不会“泄漏”到周围的作用域。
    js
    // Neither context nor source string is strict,
    // so var creates a variable in the surrounding scope
    eval("var a = 1;");
    console.log(a); // 1
    // Context is not strict, but eval source is strict,
    // so b is scoped to the evaluated script
    eval("'use strict'; var b = 1;");
    console.log(b); // ReferenceError: b is not defined
    
    function strictContext() {
      "use strict";
      // Context is strict, but this is indirect and the source
      // string is not strict, so c is still global
      eval?.("var c = 1;");
      // Direct eval in a strict context, so d is scoped
      eval("var d = 1;");
    }
    strictContext();
    console.log(c); // 1
    console.log(d); // ReferenceError: d is not defined
    
    letconst 声明在被评估的字符串中始终作用域于该脚本。
  • 直接 eval 可能会访问其他上下文表达式。例如,在函数体中,可以使用 new.target
    js
    function Ctor() {
      eval("console.log(new.target)");
    }
    new Ctor(); // [Function: Ctor]
    

切勿使用直接 eval()!

使用直接 eval() 会遇到多个问题

  • eval() 以调用者的权限执行传递给它的代码。如果您使用可能受恶意方影响的字符串运行 eval(),您最终可能会在用户的计算机上以您的网页/扩展程序的权限运行恶意代码。更重要的是,允许第三方代码访问 eval() 被调用的作用域(如果它是直接 eval)会导致可能读取或更改局部变量的攻击。
  • eval() 比替代方案慢,因为它必须调用 JavaScript 解释器,而许多其他构造则由现代 JS 引擎优化。
  • 现代 JavaScript 解释器将 JavaScript 转换为机器代码。这意味着任何变量命名的概念都被消除。因此,任何使用 eval() 的情况都将迫使浏览器进行漫长且昂贵的变量名称查找,以确定变量在机器代码中存在的位置并设置其值。此外,可以通过 eval() 向该变量引入新内容,例如更改该变量的类型,迫使浏览器重新评估所有生成的机器代码以进行补偿。
  • 如果作用域被 eval() 转换依赖,则缩小器会放弃任何缩小,因为否则 eval() 无法在运行时读取正确的变量。

在许多情况下,可以使用 eval() 或相关方法进行优化或完全避免。

使用间接 eval()

考虑以下代码

js
function looseJsonParse(obj) {
  return eval(`(${obj})`);
}
console.log(looseJsonParse("{ a: 4 - 1, b: function () {}, c: new Date() }"));

只需使用间接 eval 并强制执行严格模式即可使代码变得更好

js
function looseJsonParse(obj) {
  return eval?.(`"use strict";(${obj})`);
}
console.log(looseJsonParse("{ a: 4 - 1, b: function () {}, c: new Date() }"));

上面这两个代码片段似乎以相同的方式工作,但它们并非如此;第一个使用直接 eval 的代码存在多个问题。

  • 由于需要进行更多作用域检查,因此速度要慢得多。请注意被评估字符串中的 c: new Date()。在间接 eval 版本中,对象在全局作用域中被评估,因此解释器可以安全地假设 Date 指的是全局 Date() 构造函数,而不是名为 Date 的局部变量。但是,在使用直接 eval 的代码中,解释器无法假设这一点。例如,在以下代码中,被评估字符串中的 Date 不指的是 window.Date()
    js
    function looseJsonParse(obj) {
      function Date() {}
      return eval(`(${obj})`);
    }
    console.log(looseJsonParse(`{ a: 4 - 1, b: function () {}, c: new Date() }`));
    
    因此,在代码的 eval() 版本中,浏览器被迫进行昂贵的查找调用以检查是否存在任何名为 Date() 的局部变量。
  • 如果未使用严格模式,则 eval() 源代码中的 var 声明将成为周围作用域中的变量。如果字符串是从外部输入获取的,尤其是在存在同名变量的情况下,这会导致难以调试的问题。
  • 直接 eval 可以读取和更改周围作用域中的绑定,这可能导致外部输入破坏本地数据。
  • 当使用直接的eval时,尤其是在无法证明eval源代码处于严格模式的情况下,引擎(以及构建工具)必须禁用与内联相关的所有优化,因为eval()源代码可能依赖于其周围作用域中的任何变量名。

但是,使用间接的eval()不允许传递除现有全局变量之外的其他绑定,以便被评估的源代码读取。如果您需要指定被评估的源代码应该访问的其他变量,请考虑使用Function()构造函数。

使用Function()构造函数

Function()构造函数与上面间接eval示例非常相似:它也在全局作用域中评估传递给它的JavaScript源代码,而不会读取或修改任何局部绑定,因此允许引擎执行比直接eval()更多的优化。

eval()Function()之间的区别在于,传递给Function()的源字符串被解析为函数体,而不是脚本。有一些细微差别——例如,您可以在函数体的顶层使用return语句,但不能在脚本中使用。

如果您希望通过将变量作为参数绑定来在eval源代码中创建局部绑定,则Function()构造函数很有用。

js
function Date(n) {
  return [
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday",
  ][n % 7 || 0];
}
function runCodeWithDateFunction(obj) {
  return Function("Date", `"use strict";return (${obj});`)(Date);
}
console.log(runCodeWithDateFunction("Date(5)")); // Saturday

eval()Function()都隐式地评估任意代码,并且在严格的CSP设置中是被禁止的。对于常见用例,还有其他更安全(也更快!)的eval()Function()替代方案。

使用括号访问器

您不应该使用eval()来动态访问属性。考虑以下示例,其中要访问的对象的属性直到代码执行后才知道。这可以使用eval()来完成

js
const obj = { a: 20, b: 30 };
const propName = getPropName(); // returns "a" or "b"

const result = eval(`obj.${propName}`);

但是,这里不需要eval()——事实上,它更容易出错,因为如果propName不是有效的标识符,则会导致语法错误。此外,如果getPropName不是您控制的函数,则可能导致执行任意代码。相反,使用属性访问器,它们更快也更安全

js
const obj = { a: 20, b: 30 };
const propName = getPropName(); // returns "a" or "b"
const result = obj[propName]; // obj["a"] is the same as obj.a

您甚至可以使用此方法访问后代属性。使用eval(),这将如下所示

js
const obj = { a: { b: { c: 0 } } };
const propPath = getPropPath(); // suppose it returns "a.b.c"

const result = eval(`obj.${propPath}`); // 0

避免在此处使用eval()可以通过拆分属性路径并循环遍历不同的属性来完成

js
function getDescendantProp(obj, desc) {
  const arr = desc.split(".");
  while (arr.length) {
    obj = obj[arr.shift()];
  }
  return obj;
}

const obj = { a: { b: { c: 0 } } };
const propPath = getPropPath(); // suppose it returns "a.b.c"
const result = getDescendantProp(obj, propPath); // 0

以这种方式设置属性的工作方式类似

js
function setDescendantProp(obj, desc, value) {
  const arr = desc.split(".");
  while (arr.length > 1) {
    obj = obj[arr.shift()];
  }
  return (obj[arr[0]] = value);
}

const obj = { a: { b: { c: 0 } } };
const propPath = getPropPath(); // suppose it returns "a.b.c"
const result = setDescendantProp(obj, propPath, 1); // obj.a.b.c is now 1

但是,请注意,将括号访问器与不受约束的输入一起使用也不安全——它可能导致对象注入攻击

使用回调函数

JavaScript具有一等函数,这意味着您可以将函数作为参数传递给其他API,将它们存储在变量和对象的属性中,等等。许多DOM API都是考虑到这一点而设计的,因此您可以(也应该)编写

js
// Instead of setTimeout("…", 1000) use:
setTimeout(() => {
  // …
}, 1000);

// Instead of elt.setAttribute("onclick", "…") use:
elt.addEventListener("click", () => {
  // …
});

闭包也有助于在不连接字符串的情况下创建参数化函数。

使用JSON

如果您正在调用的eval()字符串包含数据(例如,数组:"[1, 2, 3]"),而不是代码,则应考虑切换到JSON,它允许字符串使用JavaScript语法的一个子集来表示数据。

请注意,与JavaScript语法相比,JSON语法的限制更多,因此许多有效的JavaScript字面量将无法解析为JSON。例如,JSON中不允许使用尾随逗号,并且对象字面量中的属性名称(键)必须用引号括起来。请务必使用JSON序列化器生成稍后将被解析为JSON的字符串。

通常,传递仔细约束的数据而不是任意代码是一个好主意。例如,旨在抓取网页内容的扩展程序可以使用XPath而不是JavaScript代码来定义抓取规则。

示例

使用eval()

在以下代码中,两个包含eval()的语句都返回42。第一个评估字符串"x + y + 1";第二个评估字符串"42"

js
const x = 2;
const y = 39;
const z = "42";
eval("x + y + 1"); // 42
eval(z); // 42

eval() 返回语句的完成值

eval()返回语句的完成值。对于if,它将是最后评估的表达式或语句。

js
const str = "if (a) { 1 + 1 } else { 1 + 2 }";
let a = true;
let b = eval(str);

console.log(`b is: ${b}`); // b is: 2

a = false;
b = eval(str);

console.log(`b is: ${b}`); // b is: 3

以下示例使用eval()来评估字符串str。此字符串由JavaScript语句组成,如果x为5,则将z的值设置为42,否则将0赋给z。当执行第二个语句时,eval()将导致执行这些语句,并且它还将评估语句集并返回分配给z的值,因为赋值的完成值是赋值的值。

js
const x = 5;
const str = `if (x === 5) {
  console.log("z is 42");
  z = 42;
} else {
  z = 0;
}`;

console.log("z is ", eval(str)); // z is 42  z is 42

如果您分配多个值,则返回最后一个值。

js
let x = 5;
const str = `if (x === 5) {
  console.log("z is 42");
  z = 42;
  x = 420;
} else {
  z = 0;
}`;

console.log("x is", eval(str)); // z is 42  x is 420

eval() 作为定义函数的字符串需要“(”和“)”作为前缀和后缀

js
// This is a function declaration
const fctStr1 = "function a() {}";
// This is a function expression
const fctStr2 = "(function b() {})";
const fct1 = eval(fctStr1); // return undefined, but `a` is available as a global function now
const fct2 = eval(fctStr2); // return the function `b`

规范

规范
ECMAScript 语言规范
# sec-eval-x

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅