评估是邪恶的,但不是您为什么会想到的

到现在为止,我已经听到多年了“以恶为恶”的口头禅,直到最近,没有任何论据支持这种说法。 让我们探索评估的神话,揭示其背后的真正邪恶,并找到在没有邪恶的情况下获得评估收益的另一种方法。

下面的所有代码示例在eval-is-evil存储库中可用。

误解1:评估对性能不利

该语句通常由一些简单的示例支持,例如:

  var foo = eval('bar。'+ x); 

此代码可用于访问名称存储在变量x中的对象属性。 相反,作者认为,您应该使用以下代码访问此类属性:

  var foo = bar [x]; 

尽管这种使用eval的方式确实会降低执行速度,但我无法想象有任何JavaScript开发人员(无论是经验不足的人)会想到在这种情况下使用eval

让我们考虑另一个示例,该示例也涉及属性访问,但是使用eval可以显着提高性能

我们有一些嵌套的对象,我们需要定义该对象到浅对象的某种转换。 我们想使用配置而不是仅仅编写代码来定义这种转换。 如果我们希望能够动态更改此类配置以及在许多其他情况下,则可能更可取。 因此,如果我们的嵌套对象看起来像这样:

  var source = { 
服务: {
D b: {
主持人:“ db.example.com”
},
审核:{
主持人:“ audit.example.com”
}
}
};

我们想要的浅对象应该看起来像这样:

  var结果= { 
db:“ db.example.com”,
审核:“ audit.example.com”
};

从嵌套对象到浅层对象的转换可能如下所示:

  var Transformation = { 
db:“ / services / db / host”,
审核:“ / services / audit / host”
};

顺便说一下,定义嵌套对象中数据位置的字符串格式称为[JSON指针](https://tools.ietf.org/html/rfc6901)。

好的,如果我们有这样的转换,我们将如何编写代码将嵌套对象转换为浅对象? 一种方法是简单地迭代转换中的属性并生成结果对象:

 函数transform(source){ 
var result = {};
用于(转换中的var键){
var path = Transformation [key];
var segment = path.split('/');
var data = source;
对于(var i = 1; i <segments.length; i ++){
数据=数据&&数据[段[i]];
}
result [key] =数据;
}
返回结果;
}

尽管此代码有效,但是如果必须在服务器上多次执行转换,效率不是很高。

另一种方法是生成执行转换的代码,然后使用eval (或Function构造函数)将此代码转换为函数:

  var code ='return {'; 
用于(转换中的var键){
var path = Transformation [key];
var segment = path.split('/');
var data ='source';
var expr =数据;
对于(var i = 1; i <segments.length; i ++){
数据+ ='。 +个细分[i];
expr + ='&&'+数据;
}
代码+ =键+':'+ expr +',';
}
代码+ ='};'
var transform = eval('((function(source){'+ code +'})');

最后一行可以替换为:

  var transform = new Function('source',code); 

对于我们的转换,生成的函数将是:

 功能(来源){ 
返回{
db:source && source.services && source.services.db && source.services.db.host,
审核:来源&& source.services && source.services.audit && source.services.audit.host,
};
}

这种动态生成的代码将比使用循环执行转换的第一个示例快许多倍。 创建此函数需要花费一些执行时间,但只发生一次。

此示例表明,您可以使用动态代码生成和eval来大幅提高应用程序的性能。 在Milo.js框架中使用相同的方法来生成模型访问器方法(获取,设置等)。

误解2:评估是安全风险

该语句可能假定您将在对服务器的请求中接收JavaScript代码并使用eval执行它:

  app.get('/ execute',function(req,res){ 
res.send(eval(req.body.code));
});

上面的代码确实存在一些问题。 如果req.body.code‘process.exit(1)’,则应用程序将退出并出现错误。 也可能容易发生一些更糟的事情。 但是我看不到有人怎么写这样的代码。 通常对用户输入进行清理,尤其是在此输入用于代码执行/生成的情况下。

让我们考虑另一个例子。 假设我们有一个群发邮件应用程序,它将邮件发送到收件人列表,并且我们要求用户提交的邮件是使用收件人记录中可用字段的模板:

  var收件人= [ 
{first_name:'John',last_name:'Smith'},
{first_name:'Jane',last_name:'Doe'}
];
  var messageTemplate ='你好{{first_name}} {{last_name}}!' 

我们可以循环地向所有用户创建实际消息:

  var messages = receivers.map(createMessage); 
 函数createMessage(recipient){ 
返回messageTemplate.replace(/ {{([a-z _] +)}} / ig,
功能(匹配键){
返回收件人[密钥];
}
);
}

上面的代码将创建我们需要的所有消息,但是有一种更快的方法来实现它。 我们可以生成函数createMessage 。 即使从用户收到messageTemplate,它也没有任何安全隐患:

  var code ='return“'; 
代码+ = messageTemplate.replace(/ {{([a-z _] +)}} / ig,
功能(匹配键){
返回'“ +数据。 +键+'+'';
}
);
代码+ ='“;'
var createMessage = eval('((function(data){'+ code +'})');
//或new Function('data',code);

在几行代码中,我们创建了一个超简单的“模板引擎”,可将模板编译为JavaScript函数。

模板“您好{{first_name}} {{last_name}}!” 将被编译为:

 函数(数据){ 
返回“你好” + data.first_name +“” + data.last_name +“!”;
}

执行上面的函数比不使用eval的代码快数百(如果不是数千)倍。 创建此函数始终是安全的 -如果在花括号内使用了一些不安全的代码,由于正则表达式将不匹配,因此将不会执行该函数 。 例如,模板

  “你好{{process.exit(1)}}!” 

将生成此函数:

 函数(数据){ 
返回“你好{{process.exit(1)}}!”;
}

即,不安全的插值未替换为代码。

最快,最简洁的模板引擎doT使用了将模板编译为JavaScript函数的相同方法。

误解3:评估很难进行评估

我不确定这是哪里来的。 传递给eval的代码是普通的JavaScript代码-您可以添加断点,检查变量等。传递给eval函数构造函数的调试代码与调试任何JavaScript代码没有太大不同,您只需要在代码执行期间对其进行格式化生成或使用js-beautify包。

当您想为对象定义一个超简单模式(不是JSON模式)并根据此模式对其进行验证时,让我们考虑另一个示例:

  var schema = { 
foo:“ identifier”,
酒吧:“日期”
};

在此我们假定所有属性都应为字符串,并且它们应与某些已知格式(在这种情况下为“标识符”和“日期”)匹配。

我们的格式可以定义为正则表达式:

  var格式= { 
标识符:/ ^ [a-z _ $] [a-z0-9 _ $] * $ / i,
日期:/ ^ \ d {2} \ / \ d {2} \ / \ d {4} $ /
};

我们要验证的数据:

  var validData = { 
foo:“ abc”,
酒吧:'15 / 09/2016'
};
  var invalidData = { 
foo:“ 1”,
条:'15 -09-2016'
};

我们可以通过迭代模式中的属性并根据格式检查数据属性来验证数据:

 函数validate(模式,数据){ 
对于(模式中的var prop){
var value = data [prop];
如果(typeof value!='string')返回false;
var pattern = format [schema [prop]];
如果(!pattern.test(value))返回false;
}
返回true;
}

另一种方法是从模式生成验证函数的代码,并使用eval创建此函数:

  var code =''; 
对于(模式中的var prop){
var data ='数据'。 +道具;
代码+ ='如果(typeof'+数据+'!=“字符串”)返回false;';
代码+ ='if(!formats。'+ schema [prop] +'.test('
+ data +'))返回false;';
}
代码+ ='返回true;';
var validate = eval('(function(data){'+ code +'})');

我们上面的简单模式将“编译”为以下功能:

 函数(数据){ 
如果(typeof data.foo!=“ string”)返回false;
如果(!formats.identifier.test(data.foo))返回false;
如果(typeof data.bar!=“ string”)返回false;
如果(!formats.date.test(data.bar))返回false;
返回true;
}

当几个JSON-Schema验证器使用这种将模式编译为JavaScript函数的数据验证方法,其中包括最快的一个-我创建的[Ajv](https://github.com/epoberezkin/ajv)。

那么为什么Eval是邪恶的?

Eval可以极大地提高性能,如果使用得当,它不会带来安全风险,并且可以进行调试而不会出现任何问题。 那么为什么永远不应该使用它呢?

eval的问题在于,每当eval创建函数时,它都会成为一个闭包,该闭包将保留对当前作用域和ALL父作用域中所有变量的访问,无论该闭锁是否使用了它们。 如果您从chrome inspector中的第三个示例中调试validate函数,您将看到它:

eval不同, Function构造函数没有此问题,它返回的函数是在全局范围内创建的,而不是闭包。

Vyacheslav Egorov于4年前写过有关它的信息,而今天仍然如此-在eval中,eval没有像普通闭包那样优化在eval.js和浏览器中,只保留对它们使用的作用域变量的访问。

Russ Frank指出了这个问题,他最近向Ajv提交了PR,该PR将eval替换为Function构造函数以减少内存使用率。

在除最后一个示例之外的所有示例中,我们都可以轻松地用新Function替换eval 。 在上一个示例中,尽管生成的函数应该是一个闭包-它需要访问父作用域中定义的格式 。 因此,如果我们仅以与以前相同的方式使用Function构造函数 ,它将无法正常工作。 相反,我们可以这样做:

  var createValidate = new Function('formats', 
'返回函数(数据){'+代码+'}');
var validate = createValidate(formats);

如您所见, Function构造函数用于创建一个返回闭包的函数,该闭包可以访问format ,但不能访问其他任何东西:

尽管此代码比eval更为冗长,但它没有使eval真正变得邪恶的问题- 保留对从当前到全局的所有作用域的访问

因此,尽管可以使用代码生成来获得重大的性能优势,但应避免直接调用eval 。 相反,应该使用Function构造函数(或对eval的间接调用)。