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

```javascript
"use strict"
eval("2 + 3")
// Uncaught EvalError: call to eval() blocked by CSP
```

```javascript
"use strict"
const eval = 1
// Uncaught SyntaxError: 'eval' can't be defined or assigned to in strict mode code
```

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

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

## 直接 eval 和非直接 eval

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

```javascript
(0, eval)("x + y")

eval?.("x + y")

const geval = eval
geval("x + y")

const obj = { eval }
obj.eval("x + y")
```

非直接 eval 看起来似乎是在 `&lt;script&gt;` 中执行。这意味着：

- 非直接 eval 工作在全局作用域，而非局部作用域。所执行代码无法访问所调用作用域的局部变量。

```javascript
function test() {
  const x = 2
  const y = 2
  console.log(eval("x + y"))
  console.log(eval?.("x + y"))
}
test()
```

- 非直接 eval 不会继承所在上下文的严格状态，只有 eval 内部有 `"use strict"` 字样时才会进入严格状态。

```javascript
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"` ：

```javascript
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` 声明的变量和函数声明将不会进入周围的作用域：

```javascript
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` 中：

```javascript
eval("var a = 12")
eval("let b = 13")
eval("const c = 10")
console.log(a)
console.log(b)
console.log(c)
```

- 直接 eval 有可能访问额外的上下文表达式。比如，在函数中，可有以下代码状态：

```javascript
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()

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

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

```javascript
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()` ：

```javascript
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()` 构造函数非常有用。

```javascript
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()` 来完成：

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

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

```javascript
const obj = { a: 20, b: 30 }
const propName = getPropName()
const result = obj[propName]
```

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

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

不使用 eval：

```javascript
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)
```

设置属性：

```javascript
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。

```javascript
// Not setTimeout("...", 1000)
setTimeout(() => {
  // ...
}, 1000)

// Not element.setAttribute("onclick", "...")
element.addEventListener("click", () => {
  // ...
})
```

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

### 使用 JSON

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

## 例子

### 使用 eval

```javascript
const x = 2
const y = 3
const z = 4
eval("x + y + z")
eval(z)
```

### eval 返回语句的完成值

```javascript
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
```

```javascript
const x = 5
const str = `if (x === 5) {
  console.log("z is 42")
  z = 42
} else {
  z = 0
}`

console.log(eval(str))
```

```javascript
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 作用字符串参数定义函数

```javascript
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>
