eval()

Baseline 已广泛支持

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

警告: 从字符串执行 JavaScript 存在巨大的安全风险。恶意行为者在使用 eval() 时运行任意代码太容易了。请参阅下方的 切勿直接使用 eval()!

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

试一试

console.log(eval("2 + 2"));
// Expected output: 4

console.log(eval(new String("2 + 2")));
// Expected output: 2 + 2

console.log(eval("2 + 2") === eval("4"));
// Expected output: true

console.log(eval("2 + 2") === eval(new String("2 + 2")));
// Expected output: false

语法

js
eval(script)

参数

script

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

返回值

评估给定代码的完成值。如果完成值为空,则返回 undefined。如果 script 不是字符串原始值,eval() 将不变地返回该参数。

异常

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

描述

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

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

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

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 myEval = eval;
myEval("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 Map() }"));

简单地使用间接 eval 并强制严格模式可以使代码好得多

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

上面两段代码可能看起来工作方式相同,但它们并非如此;第一个使用直接 eval 的代码存在多个问题。

  • 由于更多的作用域检查,它要慢得多。请注意评估字符串中的 c: new Map()。在间接 eval 版本中,对象是在全局作用域中评估的,因此解释器可以安全地假定 Map 指的是全局 Map() 构造函数而不是名为 Map 的局部变量。然而,在使用直接 eval 的代码中,解释器无法假定这一点。例如,在以下代码中,评估字符串中的 Map 并不指 window.Map()

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

    因此,在 eval() 版本的代码中,浏览器被迫进行昂贵的查找调用,以检查是否存在任何名为 Map() 的局部变量。

  • 如果不使用严格模式,eval() 源中的 var 声明会成为周围作用域中的变量。如果字符串是从外部输入获取的,这会导致难以调试的问题,特别是当存在同名变量时。

  • 直接 eval 可以读取和修改周围作用域中的绑定,这可能导致外部输入损坏局部数据。

  • 当使用直接 eval 时,特别是当 eval 源无法被证明是严格模式时,引擎和构建工具必须禁用与内联相关的所有优化,因为 eval() 源可能依赖其周围作用域中的任何变量名。

然而,使用间接 eval() 不允许传递除现有全局变量以外的额外绑定供被评估的源读取。如果您需要指定被评估源应有权访问的附加变量,请考虑使用 Function() 构造函数。

使用 Function() 构造函数

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

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

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

js
function add(a, b) {
  return a + b;
}
function runCodeWithAddFunction(obj) {
  return Function("add", `"use strict";return (${obj});`)(add);
}
console.log(runCodeWithAddFunction("add(5, 7)")); // 12

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 语法的子集来表示数据。

请注意,由于 JSON 语法相对于 JavaScript 语法是有限的,因此许多有效的 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 为五,则将 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® 2026 语言规范
# sec-eval-x

浏览器兼容性

另见