使用上下文数据安全地评估JavaScript
eval()从一开始就在JavaScript中提供了该功能,作为从动态输入运行代码的一种方式。但是,它存在有据可查的问题,并且一般的建议是,如果有第三方将内容放入表达式中的任何可能性,则不应使用它。
您可以在此处查看Mozilla开发人员网络(MDN)上的讨论。
该MDN文章还向您展示了一种以更安全的方式获得相似结果的出色技术。但是,我的需求略有不同,需要使用不同的方法。
背景
我试图解决的问题是基于一些传递的上下文数据进行超轻量级规则评估。例如,看一下这个简单的对象:
let projectData = { id: 'abc', name: 'New hotel wing', finance: { EstimateAtCompletion: 2735500, costToDate: 1735500, contingency: 250000 }, risk: { open80PctCostImpact: 275000 }}123456789101112复制代码类型:[html]
它代表了整个生命周期中的某个项目,其中有大量资金用于应急目的。所有非常标准的数据。它还具有来自风险分析的一些数据。在此示例中,80%的可能成本似乎比应急罐中的成本还高。这将需要解决。
对于一个项目计划,我可能会有很多这样的记录。因此,如果我可以使用一些灵活的表达式来确定要保留在某些视图中的记录,那将非常有用。
// assume that the list was loaded from some server calllet aAllProjects = [p1, p2, p3]12复制代码类型:[html]
显然,我可以在此列表上编写一个过滤规则,如下所示:
// let aProblemProjects = aAllProjects.filter(p => p.finance.contingency < p.risk.open80PctCostImpact)123复制代码类型:[html]
但是,如果我希望能够使用动态过滤器(可能来自某些配置文件或用户提供的规则)该怎么办。我需要一些更优雅的东西。这是对MDN示例的修改,可以帮助我做到这一点。
/** @param {string} textExpression - code to evaluate passed as plain text* @param {object} contextData - some JavaScript object * that can be referred to as $data in the textExpression* @returns {*} depend on the tagetExpression*/function wrappedEval(textExpression, contextData){ let fn = Function(`"use strict"; var $data = this;return (${textExpression})`) return fn.bind(contextData)();}12345678910复制代码类型:[html]
使用代码
以上面有关项目的示例为例,我们可以使用类似以下的表达式:
let filterExpression = '$data.finance.contingency < $data.risk.open80PctCostImpact'let aFilteredProjects = aProjects.filter(p => wrappedEval(filterExpression, p) )12复制代码类型:[html]
该filterExpression变量可以很容易地从某些配置文件等中获得,这比将过滤器编码到系统中更为灵活。
兴趣点
这里有趣的是,这个简单的两行函数正在使用JavaScript的两个非常强大的功能。
函数构造器
关键字Function用于获取任意文本并将其解析为常规JavaScript函数。这与MDN文章中的建议完全相同,用于隔离表达式评估。
功能绑定
该技术的主要要求是将任意数据以及可被表达式使用的表达式一起传递。JavaScript中的每个函数本身就是一个对象,并且从其原型传递了一些标准方法。
在这种情况下,我正在使用bind()。这样做是将一个初始参数this作为函数内部的值,然后它返回一个可以调用的新函数变量。但是,它会变得更好!如果将更多的参数传递给bind您,则得到的是一个具有更少参数的新函数,并且已经传递的那些项将被修复。有时将其称为部分应用功能。此功能可用于改进上面的代码,如下所示:
let filterExpression = '$data.finance.contingency < $data.risk.open80PctCostImpact'let fnFilterProjects = wrappedEval.bind(this, filterExpression)let aFilteredProjects = aProjects.filter(p => fnFilterProjects(p) )123复制代码类型:[html]
此处的最大区别是对的调用wrappedEval仅发生一次。我们将对数组的每个项目使用的表达式已部分应用。因此,如果我们有大量项目,则应该获得可观的性能改进,因为JS引擎不会反复将表达式文本反复传递给相同的函数。