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)

letconst 声明的代码一直被限制在 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() {}
欢迎通过「邮件」或者点击「这里」告诉我你的想法
Welcome to tell me your thoughts via "email" or click "here"