Tianhe Gao

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() 是被限制使用的。

1"use strict"
2eval("2 + 3")
3// Uncaught EvalError: call to eval() blocked by CSP
1"use strict"
2const eval = 1
3// Uncaught SyntaxError: 'eval' can't be defined or assigned to in strict mode code

如果 eval() 的参数不是字符串,其返回参数本身。用通用的方式解决问题:

1const expression = new String("2 + 1")
2eval(String(expression))

直接 eval 和非直接 eval

直接 eval 只有 eval(script) ,其他所有调用 eval 执行脚本的代码都是非直接:

1(0, eval)("x + y")
2
3eval?.("x + y")
4
5const geval = eval
6geval("x + y")
7
8const obj = { eval }
9obj.eval("x + y")

非直接 eval 看起来似乎是在 <script> 中执行。这意味着:

  • 非直接 eval 工作在全局作用域,而非局部作用域。所执行代码无法访问所调用作用域的局部变量。
1function test() {
2  const x = 2
3  const y = 2
4  console.log(eval("x + y"))
5  console.log(eval?.("x + y"))
6}
7test()
  • 非直接 eval 不会继承所在上下文的严格状态,只有 eval 内部有 "use strict" 字样时才会进入严格状态。
 1function strictContext() {
 2  "use strict"
 3  eval?.(`with (Math) console.log(PI)`)
 4}
 5function strictContextStrictEval() {
 6  "use strict"
 7  eval?.(`"use strict"; with (Math) console.log(PI)`)
 8}
 9strictContext()
10strictContextStrictEval()

因此直接 eval 会继承 "use strict"

1function nonStrictContext() {
2  eval(`with (Math) console.log(PI)`)
3}
4function strictContext() {
5  "use strict"
6  eval(`with (Math) console.log(PI)`)
7}
8nonStrictContext()
9strictContext()
  • 如果源代码不在严格模式下, var 声明的变量和函数声明将进入周围的作用域——对于非直接 eval 来说,它们会变成全局变量。如果是处于严格模式下的直接 eval,或者 eval 内部的代码带有 "use strict"var 声明的变量和函数声明将不会进入周围的作用域:
 1eval("var a = 1")
 2console.log(a)
 3
 4eval("'use strict'; var b = 1")
 5console.log(b)
 6
 7function strictContext() {
 8  "use strict"
 9  eval?.("var c = 1")
10  eval("var d = 1")
11}
12strictContext()
13console.log(c)
14console.log(d)

letconst 声明的代码一直被限制在 eval 中:

1eval("var a = 12")
2eval("let b = 13")
3eval("const c = 10")
4console.log(a)
5console.log(b)
6console.log(c)
  • 直接 eval 有可能访问额外的上下文表达式。比如,在函数中,可有以下代码状态:
1function Ctor() {
2  eval("console.log(new.target)")
3}
4new Ctor()

不要用 eval()

使用直接 eval 有几个问题:

  • eval() 使用调用方的特权执行它传递的代码。如果您使用可能受到恶意方影响的字符串运行 eval() ,可能最终会用网页/扩展的权限在用户的机器上运行恶意代码。更重要的是,允许第三方代码访问调用 eval() 的作用域(如果是直接 eval)可能导致读取或更改本地变量的攻击。
  • eval() 比其他替代方案要慢,因为它必须调用 JavaScript 解释器,而许多其他构造是由现代 JS 引擎优化的。
  • 现代 JavaScript 解释器将 JavaScript 转换为机器代码。这意味着变量命名的任何概念都会被抹去。因此,任何 eval() 的使用都会迫使浏览器执行长时间的代价高昂的变量名查找,以确定变量在机器代码中的位置并设置其值。此外,可以通过 eval() 向该变量引入新内容,例如更改该变量的类型,强制浏览器重新计算所有生成的机器代码以进行补偿。
  • 如果作用域传递依赖于 eval() ,则缩减符放弃任何缩减,否则 eval() 无法在运行时读取正确的变量。

在许多情况下,可以完全优化或避免使用 eval() 或相关方法。

使用非直接 eval()

1function looseJsonParse(obj) {
2  return eval(`(${obj})`);
3}
4console.log(looseJsonParse("{ a: 4 - 1, b: function () {}, c: new Date() }"));

简单地使用间接 eval 和强制严格模式可以使代码变得更好:

1function looseJsonParse(obj) {
2  return eval?.(`"use strict";(${obj})`);
3}
4console.log(looseJsonParse("{ a: 4 - 1, b: function () {}, c: new Date() }"));

上面的两个代码片段看起来工作方式相同,但实际上并非如此; 第一个使用直接 eval 的代码存在多个问题。

  • 由于进行了更多的范围检查,这个过程要慢得多。注意计算字符串中的 c: new Date() 。在间接 eval 版本中,对象是在全局作用域中求值的,因此解释器可以安全地假设 Date 引用全局 Date() 构造函数而不是称为 Date 的局部变量。但是,在使用直接 eval 的代码中,解释器不能假定这一点。例如,在下面的代码中,计算字符串中的 Date 不引用 window.Date()
1function looseJsonParse(obj) {
2  function Date() {}
3  return eval(`(${obj})`);
4}
5console.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() 构造函数非常有用。

 1function Date(n) {
 2  return [
 3    "Monday",
 4    "Tuesday",
 5    "Wednesday",
 6    "Thursday",
 7    "Friday",
 8    "Saturday",
 9    "Sunday",
10  ][n % 7 || 0]
11}
12function runCodeWithDateFunction(obj) {
13  return Function("Date", `"use strict"; return (${obj})`)(Date)
14}
15console.log(runCodeWithDateFunction("Date(5)"))

eval()Function() 都隐式计算任意代码,并且在严格的 CSP 设置中是禁止的。还有额外的安全(和更快!)用于常见用例的 eval()Function() 的替代方案。

使用括号访问符

不应使用 eval() 动态访问属性。考虑下面的示例,其中要访问的对象的属性在执行代码之前是不知道的。这可以用 eval() 来完成:

1const obj = { a: 20, b: 30 }
2const propName = getPropName()
3const result = eval(`obj.${propName}`)

但是这里并不需要 eval,如果 propName 不是一个有效的标识符,执行就会报错。而且,如果 getPropName 不是你能控制的函数,任意代码都可以通过这样执行。使用属性访问器,更快更安全:

1const obj = { a: 20, b: 30 }
2const propName = getPropName()
3const result = obj[propName]

还可以用这种方式访问子代属性。使用 eval:

1const obj = { a: { b: { c: 0 } } }
2const propPath = getPropPath() // return a.b.c
3const result = eval(`obj.${propPath}`)

不使用 eval:

 1function getDescendantProp(obj, desc) {
 2  const arr = desc.split(".")
 3  while (arr.length) {
 4    obj = obj[arr.shift()]
 5  }
 6  return obj
 7}
 8
 9const obj = { a: { b: { c: 0 } } }
10const propPath = getPropPath()
11const result = getDescendantProp(obj, propPath)

设置属性:

 1function setDescendantProp(obj, desc, value) {
 2  const arr = desc.split(".")
 3  while (arr.length > 1) {
 4    obj = obj[arr.shift()]
 5  }
 6  return (obj[arr[0]] = value)
 7}
 8
 9const obj = { a: { b: { c: 0 } } }
10const propPath = getPropPath()
11const result = setDescendantProp(obj, propPath, 1)

注意,使用带有无限制输入的方括号访问器也是不安全的。

使用回调函数

在 JS 中,函数也可以被视为变量的一种,这表示可以将函数作为参数传递给其他 APIs。

1// Not setTimeout("...", 1000)
2setTimeout(() => {
3  // ...
4}, 1000)
5
6// Not element.setAttribute("onclick", "...")
7element.addEventListener("click", () => {
8  // ...
9})

闭包也是一种参数化函数的手段。

使用 JSON

如果要在 eval 中包含某类数据,应该考虑使用 JSON。

例子

使用 eval

1const x = 2
2const y = 3
3const z = 4
4eval("x + y + z")
5eval(z)

eval 返回语句的完成值

1const str = "if (a) { 1 + 1 } else { 1 + 2 }"
2let a = true
3let b = eval(str)
4console.log(`b is: ${b}`) // 2
5
6a = false
7b = eval(str)
8console.log(`b is: ${b}`) // 3
1const x = 5
2const str = `if (x === 5) {
3  console.log("z is 42")
4  z = 42
5} else {
6  z = 0
7}`
8
9console.log(eval(str))
 1let x = 5
 2const str = `if (x === 5) {
 3  console.log("z is 42")
 4  z = 42
 5  x = 420
 6} else {
 7  z = 0
 8}`
 9
10console.log(eval(str))

将 eval 作用字符串参数定义函数

1const functionString1 = "function a() {}" // 函数声明
2const functionString2 = "(function b() {})" // 函数表达式
3const function1 = eval(functionString1) // undefined
4const function2 = eval(functionString2) // function b() {}

Welcome to tell me your thoughts via "email"
UP