https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
eval()
函数能将内部字符串看作脚本并执行。所以它很危险,能够在任意位置执行。其内部的 script
可以是 JS 表达式、语句或语句序列的字符串。表达式可以包含现有对象的变量和属性。因为是解析成脚本,所以不能包含只能用于模块的 import
。
它能计算给定脚本的完成值,如果为空返回 undefined
。如果脚本不是字符串原语,直接返回脚本本身。
如果执行过程中出现任何异常,都会报错,例如 SyntaxError
。
eval()
是全局对象的属性。 eval()
的参数是字符串。它可以是语句或表达式。返回代码的运行结果。
- 表达式,返回执行结果
- 赋值,返回赋值结果
- let,返回
undefined
因为返回结果的不确定性,所以建议不要依赖语句的完成值。
在严格模式中, eval()
是被限制使用的。
"use strict"
eval("2 + 3")
// Uncaught EvalError: call to eval() blocked by CSP
"use strict"
const eval = 1
// Uncaught SyntaxError: 'eval' can't be defined or assigned to in strict mode code
如果 eval() 的参数不是字符串,其返回参数本身。用通用的方式解决问题:
const expression = new String("2 + 1")
eval(String(expression))
直接 eval 和非直接 eval
直接 eval 只有 eval(script)
,其他所有调用 eval 执行脚本的代码都是非直接:
(0, eval)("x + y")
eval?.("x + y")
const geval = eval
geval("x + y")
const obj = { eval }
obj.eval("x + y")
非直接 eval 看起来似乎是在 <script>
中执行。这意味着:
- 非直接 eval 工作在全局作用域,而非局部作用域。所执行代码无法访问所调用作用域的局部变量。
function test() {
const x = 2
const y = 2
console.log(eval("x + y"))
console.log(eval?.("x + y"))
}
test()
- 非直接 eval 不会继承所在上下文的严格状态,只有 eval 内部有
"use strict"
字样时才会进入严格状态。
function strictContext() {
"use strict"
eval?.(`with (Math) console.log(PI)`)
}
function strictContextStrictEval() {
"use strict"
eval?.(`"use strict"; with (Math) console.log(PI)`)
}
strictContext()
strictContextStrictEval()
因此直接 eval 会继承 "use strict"
:
function nonStrictContext() {
eval(`with (Math) console.log(PI)`)
}
function strictContext() {
"use strict"
eval(`with (Math) console.log(PI)`)
}
nonStrictContext()
strictContext()
- 如果源代码不在严格模式下,
var
声明的变量和函数声明将进入周围的作用域——对于非直接 eval 来说,它们会变成全局变量。如果是处于严格模式下的直接 eval,或者 eval 内部的代码带有"use strict"
,var
声明的变量和函数声明将不会进入周围的作用域:
eval("var a = 1")
console.log(a)
eval("'use strict'; var b = 1")
console.log(b)
function strictContext() {
"use strict"
eval?.("var c = 1")
eval("var d = 1")
}
strictContext()
console.log(c)
console.log(d)
let
和 const
声明的代码一直被限制在 eval
中:
eval("var a = 12")
eval("let b = 13")
eval("const c = 10")
console.log(a)
console.log(b)
console.log(c)
- 直接 eval 有可能访问额外的上下文表达式。比如,在函数中,可有以下代码状态:
function Ctor() {
eval("console.log(new.target)")
}
new 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()
:
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()
不允许传递除现有全局变量之外的其他绑定,以供计算的源读取。如果需要指定计算的源应具有访问权限的其他变量,请考虑使用 Function()
构造函数。
使用 Function() 构造器
Function()
构造函数非常类似于上面的间接计算示例:它还在全局范围内计算传递给它的 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)"))
eval()
和 Function()
都隐式计算任意代码,并且在严格的 CSP 设置中是禁止的。还有额外的安全(和更快!)用于常见用例的 eval()
或 Function()
的替代方案。
使用括号访问符
不应使用 eval()
动态访问属性。考虑下面的示例,其中要访问的对象的属性在执行代码之前是不知道的。这可以用 eval()
来完成:
const obj = { a: 20, b: 30 }
const propName = getPropName()
const result = eval(`obj.${propName}`)
但是这里并不需要 eval,如果 propName
不是一个有效的标识符,执行就会报错。而且,如果 getPropName
不是你能控制的函数,任意代码都可以通过这样执行。使用属性访问器,更快更安全:
const obj = { a: 20, b: 30 }
const propName = getPropName()
const result = obj[propName]
还可以用这种方式访问子代属性。使用 eval:
const obj = { a: { b: { c: 0 } } }
const propPath = getPropPath() // return a.b.c
const result = eval(`obj.${propPath}`)
不使用 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()
const result = getDescendantProp(obj, propPath)
设置属性:
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()
const result = setDescendantProp(obj, propPath, 1)
注意,使用带有无限制输入的方括号访问器也是不安全的。
使用回调函数
在 JS 中,函数也可以被视为变量的一种,这表示可以将函数作为参数传递给其他 APIs。
// Not setTimeout("...", 1000)
setTimeout(() => {
// ...
}, 1000)
// Not element.setAttribute("onclick", "...")
element.addEventListener("click", () => {
// ...
})
闭包也是一种参数化函数的手段。
使用 JSON
如果要在 eval 中包含某类数据,应该考虑使用 JSON。
例子
使用 eval
const x = 2
const y = 3
const z = 4
eval("x + y + z")
eval(z)
eval 返回语句的完成值
const str = "if (a) { 1 + 1 } else { 1 + 2 }"
let a = true
let b = eval(str)
console.log(`b is: ${b}`) // 2
a = false
b = eval(str)
console.log(`b is: ${b}`) // 3
const x = 5
const str = `if (x === 5) {
console.log("z is 42")
z = 42
} else {
z = 0
}`
console.log(eval(str))
let x = 5
const str = `if (x === 5) {
console.log("z is 42")
z = 42
x = 420
} else {
z = 0
}`
console.log(eval(str))
将 eval 作用字符串参数定义函数
const functionString1 = "function a() {}" // 函数声明
const functionString2 = "(function b() {})" // 函数表达式
const function1 = eval(functionString1) // undefined
const function2 = eval(functionString2) // function b() {}