Skip to content

Files

Latest commit

Apr 12, 2019
04d0f2e · Apr 12, 2019

History

History

eval

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Apr 9, 2019
Apr 12, 2019
Apr 12, 2019
Apr 10, 2019

README.md

为什么要少用eval?

eval是 js 中一个强大的方法,它的作用是编译运行传入字符串代码。都说eval == evil等于true,这篇文章将研讨eval的几个缺点和使用注意事项。

目录

一、安全性

太明显了,暂不讨论

二、运行效率

都知道 eval 比较慢,到底慢多少,自己测测看,下面是代码(对比运行 1万次 eval("sum++") 和 500万次 sum++ 所需要的时间)

var getTime = function(){
  // return Date.now();
  return new Date().getTime()     //兼容ie8
}
var sum;
// 测试 1万次 eval("sum++")
sum = 0;
var startEval = getTime();
for(var i = 0;i<10000;i++){
  eval("sum++");
}
var durEval = getTime() - startEval;
console.log("durEval = ",durEval,"ms");
// 测试 500万次 sum++
sum = 0;
var startCode = getTime();
for(var i = 0;i<5000000;i++){
  sum++;
}
var durCode = getTime() - startCode;
console.log("durCode = ",durCode,"ms");
//输出结果
console.log('直接运行 sum++ 的速度约是 运行 eval("sum++") 的',(durEval * 500 / durCode).toFixed(0),'倍');

测试结果

在同一台PC上,测试3款浏览器和nodejs环境,结果如下:

Chrome 73

durEval =  236 ms
durCode =  14 ms
直接运行 sum++ 的速度约是 运行 eval("sum++") 的 8429 倍

Firefox 65

durEval =  766 ms
durCode =  167 ms
直接运行 sum++ 的速度约是 运行 eval("sum++") 的 2293 倍

IE8

durEval = 417ms
durCode = 572ms
直接运行 sum++ 的速度约是 运行 eval("sum++") 的365倍

Nodejs 10.15.0

durEval =  5 ms
durCode =  14 ms
直接运行 sum++ 的速度约是 运行 eval("sum++") 的 179 倍

Chrome 的 V8 果然是王者,Firefox 在运行eval的PK上输给了古董IE8,node环境中eval的表现最好(只慢100多倍)
PS:笔者测试环境有限,大家有个感性认识就好。

三、作用域

在作用域方面,eval 的表现让人费解。直接调用时:当前作用域;间接调用时:全局作用域

3.1 直接调用

eval被直接调用并且调用函数就是eval本身时,作用域为当前作用域,function中的foo被修改了,全局的foo没被修改。

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
console.log(test());    // 3
console.log(foo);       // 1

3.2 间接调用

间接调用eval时 执行的作用域为全局作用域,两个function中的foo都没有被修改,全局的foo被修改了。

var foo = 1;
(function(){
  var foo = 1;
  function test() {
      var foo = 2;
      var bar = eval;
      bar('foo = 3');
      return foo;
  }
  console.log(test());    // 2
  console.log(foo);       // 1
})();
console.log(foo);         // 3

四、内存 ▲

使用eval会导致内存的浪费,这是本文要讨论的重点。 下面用测试结果来对比,使用eval不使用eval 的情况下,以下代码内存的消耗情况。

4.1 不用eval的情况

var f1 = function(){          // 创建一个f1方法
  var data = {
    name:"data",
    data: (new Array(50000)).fill("data 111 data")
    };                        // 创建一个不会被使用到的变量
  var f = function(){         // 创建f方法然后返回
    console.log("code:hello world");
  };
  return f;
};
var F1 = f1();

测试结果

在Chrome上查看内存使用情况,开发者工具->Momery->Profiles->Take snapshot,给内存拍个快照。

为了便于查找,在过滤器中输入window,查看当前域的window

可以看到,window占用了686122%的内存。然后在其中找F1变量:

F1占用了320%的内存。

这似乎说明不了什么。没有对比就没有伤害,下面我们来伤害一下eval

4.2 使用eval的情况

修改上面的代码,把 console.log 修改为 eval 运行:

- console.log("code:hello world");
+ eval('console.log("eval:hello world");');

测试结果

方法同上。在Chrome上查看内存使用情况,开发者工具->Momery->Profiles->Take snapshot

window占用了2510484%的内存,其中F1占用了200140,相当于总量的3%的内存,F1.context.data,占用了200044,约等于F1的占用量,可见这些额外的内存开销都是来自于F1.context.data

4.3 分析

使用eval时:F1占用了2001403%的内存;
不用eval时:F1占用了320%的内存;

这样的差别来自于javascript引擎的优化。在方法f1运行时创建了data,接着创建了一个方法ff中可以访问到data,但它没有使用data,然后f被返回赋值给变量F1,经过javascript引擎优化,这时data不会被加入到闭包中,同时也没有其他指针指向datadata的内存就会被回收。然而在f中使用了eval后,情况就不同了,eval太过强大,导致javascript引擎无法分辨f会不会使用到data,从而只能将全部的环境变量(包括data),一起加入到闭包中,这样F1就间接引用了datadata的内存就不会被回收。从而导致了额外的内存开销。

我们可以进一步测试,这时在开发者工具->Console 中输入:

F1 = "Hello"  //重设F1,这样就没什么引用到data了

然后用同样的方法查看内存,可以发现 window占用的内存,从200000+下降到了60000+

说到这里,再回头看eval奇怪的作用域。直接调用时:当前作用域;间接调用时:全局作用域,也就可以理解了。当间接调用时,javascript引擎不知道它是eval,优化时就会移除不需要的变量,如果eval中用到了那些变量,就会发生意想不到的事情。这违背了闭包的原则,变得难以理解。索性把间接调用的作用域设置为了全局。

五、总结和应对方案

安全性

分析:eval是否安全主要由数据源决定,如果数据源不安全,eval只是提供了一种攻击方法而已。
方案:严格管控数据源。

运行效率

分析:eval比直接运行慢很多倍,但主要的消耗在于编译代码过程,简单项目中,不会这样高频率的运行eval
方案:低频使用时影响不大,不要高频使用,建议寻找替代方案。

作用域

分析:实际项目中直接调用都很少,间接调用更是少之又少。
方案:了解直接调用和间接调用的区别,遇到问题时不要懵逼即可。

内存

分析:实际应用中很常见,却很少有人会注意到内存管理,大项目中被重复使用会浪费较多的内存。
方案:优化编码规范,使用eval时注意那些没有被用到局部变量。

原文链接:github