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

瀏覽器相容性

另見