我们在开发中经常会使用配置来生成内容的情况,使用 js 代码保存配置会比较方便。但是如果希望把配置以 json 的形式保存在服务器,难免会遇到保存函数的问题。受某位大神提示,使用 eval 可以将函数保存在 json 中,便深入了解了一下 eval,夯实了 js 基础。下面把我的理解分享给大家。

基本概念

eval是全局函数,其接收一个字符串为参数,会执行字符串内指定的代码。这段代码可以是表达式,也可以是任何执行语句或语句的组合。

在执行完指定的代码后,如果最后执行的语句具有返回值,则 eval 会返回该结果。

let a1 = eval('let sum = 4 + 5');
console.log(a1); // undefined,因为变量声明语句没有返回值

let a2 = eval('sum = 4 + 5');
console.log(a2); // 9, 赋值语句具有返回值

let a3 = eval(`if (false) { 5 } else { 6 }`);
console.log(a3); // 6, 最后执行的表达式为 6

作用域

运行在局部作用域

默认情况下,在 eval 内运行代码,相当于在一个类似于 if 语句的大括号内运行代码。代码允许的作用域也就是 eval 语句所在的作用域,其可以获取到两个地方定义的变量:当前运行作用域定义的变量和全局作用域定义的变量。

globalName = 'global';
function funcA() {
    let localName = 'local';
    eval(`
        console.log('globalName:', globalName);
        console.log('localName:', localName);
    `);
}
funcA()
// globalName: global
// localName: local

上述代码可以看做下面这样:

globalName = 'global';
function funcA() {
    let localName = 'local';
    {
        console.log('globalName:', globalName);
        console.log('localName:', localName);
    }
}
funcA()

eval 语句中声明变量,也与在其它大括号内声明一样。对于 var 类型的变量,不存在块级作用域:

function funcB() {
    var name = 'parent'
  	eval(`var name = 'local'`)
    console.log(name)
}
funcB()
// local

上述代码输出 local, 因为在 eval 中声明的变量覆盖了之前的变量。

对于 letconst 类型的变量,会遵循 ES6 的块级作用域原则:

function funcC() {
    var name = 'parent'
  	eval(`let name = 'local'`)
    console.log(name)
}
funcC()
// parent

运行在全局作用域

如果以引用的方式运行 eval, 例如将 eval 赋值给另一个变量,那么 eval 内的代码会执行在全局作用域,也就是只能获取到全局作用域的变量,而无法获取本地作用域的变量。

globalName = 'global';
function funcD() {
    let localName = 'local';
    let newEval = eval;
    newEval(`
        console.log('globalName:', globalName);
        console.log('localName:', localName);
    `);
}
funcD()
// globalName: global
// Uncaught ReferenceError: localName is not defined

这种情况下,eval 代码内声明的 var 类型变量也属于全局作用域:

function funcE() {
    let newEval = eval;
    newEval(`var globalName = 'global';`);
}
funcE()
console.log(globalName);
// global

不过需要注意的是,即使使用引用的方式,在 eval 内声明的 let/const 类型的变量依然属于 eval 的块级作用域内的。所以这种方式可以相当于把一段在大括号中的代码放到了全局作用域去执行:

{
   var globalName = 'global'; 
}

严格模式

非严格模式下,在 eval 中运行代码,相当于在 eval 当时所处的作用域内的大括号里面运行代码。

function funcF() {
    var f = 'parent'
  	eval(`var f = 'localEval'`)
    console.log(f)
}
funcF()
// localEval

而在严格模式下,eval 中的语句会有自己独特的作用域,所以在 eval 中声明的变量,是无法在 eval 语句以外的地方获取到的,即使是 var 类型的变量:

function funcG(){
    "use strict";
    eval(`var g = 'local'`)
    console.log(g)
 }
funcG()
// Uncaught ReferenceError: g is not defined

缺点

  • 安全问题 eval 的一个缺点是不安全。 eval 内的代码可以获取当前作用域和全局作用域的一些变量和方法,如果参数字符串内包含有恶意代码,直接执行就会引发安全问题。

  • 性能问题 eval 将需要调用 javascript 解释器来将字符串代码转为机器码。由于之前所有的代码都已经转成了机器码,之前定义的变量名字已经不存在了,所以对于 eval 内代码对变量的引用,需要去当前作用域及全局作用域去寻找这个变量是否存在。进一步的,由于 eval 内的代码还会对当前上下文做一些更改,所以浏览器必须重新运行当前生成的机器码,来使这些更改生效。
  • 调试问题

实践中使用

基于上述缺点,大家普遍的观点是不要使用 eval 来运行字符串代码。 因为在很多情况下真的是没有必要😆。

对于本文开头说的使用 json 去存储函数,为了方便倒是可以使用一下 eval,不过这种方式也有替代方案,就是使用 Function 去执行这段代码。

根据上面所述,使用 eval 时,最好遵循下面的原则:

  1. 使用严格模式,避免变量污染,减少安全问题。
  2. 使用 try...catch, 避免执行出错。
  3. 不要执行用户输入的代码,小心 evalevil

以上就是我对 eval 的理解,有不对的地方恳请指正~