JS 下的 eval() 函数
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() {}
标准
https://tc39.es/ecma262/multipage/global-object.html#sec-eval-x
Comments
✍️ Reply by email