eval()
警告:从字符串执行 JavaScript 存在巨大的安全风险。当您使用 eval()
时,恶意攻击者运行任意代码过于容易。请参阅下面的 切勿使用直接 eval()!。
eval()
函数评估表示为字符串的 JavaScript 代码并返回其完成值。源代码被解析为脚本。
试一试
语法
eval(script)
参数
返回值
评估给定代码的完成值。如果完成值为空,则返回 undefined
。如果 script
不是字符串基元,则 eval()
会返回未更改的参数。
异常
抛出代码评估期间发生的任何异常,包括如果 script
无法解析为脚本的 SyntaxError
。
描述
eval()
是全局对象的函数属性。
eval()
函数的参数是一个字符串。它会将源字符串评估为脚本体,这意味着允许使用语句和表达式。它返回代码的完成值。对于表达式,它是表达式计算到的值。许多语句和声明也有完成值,但结果可能会令人惊讶(例如,赋值的完成值是赋值的值,但 let
的完成值是未定义的),因此建议不要依赖语句的完成值。
在严格模式下,声明名为 eval
的变量或重新赋值 eval
是 SyntaxError
。
"use strict";
const eval = 1; // SyntaxError: Unexpected eval or arguments in strict mode
如果 eval()
的参数不是字符串,则 eval()
会返回未更改的参数。在以下示例中,传递 String
对象而不是基元会导致 eval()
返回 String
对象,而不是评估字符串。
eval(new String("2 + 2")); // returns a String object containing "2 + 2"
eval("2 + 2"); // returns 4
要以通用方式解决此问题,您可以在将参数传递给 eval()
之前 自行将其强制转换为字符串。
const expression = new String("2 + 2");
eval(String(expression)); // returns 4
直接和间接 eval
eval()
调用有两种模式:直接 eval 和间接 eval。顾名思义,直接 eval 指的是直接使用 eval(...)
调用全局 eval
函数。其他所有内容,包括通过别名变量、通过成员访问或其他表达式或通过可选链 ?.
运算符调用它,都是间接的。
// 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"
指令。另一方面,直接 eval 继承调用上下文的严格性。jsfunction 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
jsfunction 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
let
和const
声明在被评估的字符串中始终作用域于该脚本。 - 直接 eval 可能会访问其他上下文表达式。例如,在函数体中,可以使用
new.target
jsfunction 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()
考虑以下代码
function looseJsonParse(obj) {
return eval(`(${obj})`);
}
console.log(looseJsonParse("{ a: 4 - 1, b: function () {}, c: new Date() }"));
只需使用间接 eval 并强制执行严格模式即可使代码变得更好
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()
。因此,在代码的jsfunction 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()
构造函数很有用。
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()
来完成
const obj = { a: 20, b: 30 };
const propName = getPropName(); // returns "a" or "b"
const result = eval(`obj.${propName}`);
但是,这里不需要eval()
——事实上,它更容易出错,因为如果propName
不是有效的标识符,则会导致语法错误。此外,如果getPropName
不是您控制的函数,则可能导致执行任意代码。相反,使用属性访问器,它们更快也更安全
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()
,这将如下所示
const obj = { a: { b: { c: 0 } } };
const propPath = getPropPath(); // suppose it returns "a.b.c"
const result = eval(`obj.${propPath}`); // 0
避免在此处使用eval()
可以通过拆分属性路径并循环遍历不同的属性来完成
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
以这种方式设置属性的工作方式类似
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都是考虑到这一点而设计的,因此您可以(也应该)编写
// 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"
。
const x = 2;
const y = 39;
const z = "42";
eval("x + y + 1"); // 42
eval(z); // 42
eval() 返回语句的完成值
eval()
返回语句的完成值。对于if
,它将是最后评估的表达式或语句。
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
的值,因为赋值的完成值是赋值的值。
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
如果您分配多个值,则返回最后一个值。
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() 作为定义函数的字符串需要“(”和“)”作为前缀和后缀
// 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 表格仅在浏览器中加载