A small JavaScript function based on the advice on MDN that additionally allows an expression to be evualated in the presence of some passed context.
Introduction
The eval()
function has been available in JavaScript since the beginning as a way of running code from dynamic input. However, it has well documented problems and the general advice is that it should not be used if there is any possibility of third parties putting content into the expressions.
You can see the discussion on the Mozilla Developer Network (MDN) here.
The MDN article also shows you a great technique to achieve similar results in a safer manner. However, I had a lightly different need that needed a different approach.
Background
The problem I was trying to resolve was to make a super lightweight rules evaluation based on some passed context data. For example, take a look at this simple object:
let projectData = {
id: 'abc',
name: 'New hotel wing',
finance: {
EstimateAtCompletion: 2735500,
costToDate: 1735500,
contingency: 250000
},
risk: {
open80PctCostImpact: 275000
}
}
It represents some project mid way through its lifecycle with a chunk of money for contingency purposes. All very standard data. It also has some data from a Risk analysis. In this example, the 80% likely cost seems to be more than we have in the contingency pot. This would need addressing.
I might have a whole bunch of record like this for a program of projects. So, it would be really useful if I could use some flexible expressions to determine which records I might want to keep in some view.
let aAllProjects = [p1, p2, p3]
Obviously, I could code up a filter rule on this list like this:
let aProblemProjects = aAllProjects.filter
(p => p.finance.contingency < p.risk.open80PctCostImpact)
However, what if I want to be able to use a dynamic filter, perhaps from some configuration file or user supplied rule. I would need something more elegant. This is a modification of the example from MDN that helps me do just that.
function wrappedEval(textExpression, contextData){
let fn = Function(`"use strict"; var $data = this;return (${textExpression})`)
return fn.bind(contextData)();
}
Using the Code
Taking the example about projects above, we could use an expression like this:
let filterExpression = '$data.finance.contingency < $data.risk.open80PctCostImpact'
let aFilteredProjects = aProjects.filter(p => wrappedEval(filterExpression, p) )
The filterExpression
variable could easily come from some configuration file, etc. giving you greater flexibility than just coding the filter into the system.
Points of Interest
What is interesting here is that this simple two line function is using two really powerful features of JavaScript.
The Function Constructor
The keyword Function is used to take arbitrary text and parse it into a regular JavaScript function. This is exactly the same as the advice from the MDN article and is used to isolate the expression evaluation.
Function Binding
The main requirement of this technique is to pass arbitrary data along with the expression that can be used by the expression. Every function in JavaScript is an object in its own right and has some standard methods passed along from its prototype.
In this case, I am using bind()
. What this does is take an initial parameter to be the this
value inside the function and then it returns a new function variable that you can call. But, it can get better! If you pass more arguments to bind
, what you get is a new function with less arguments and those items already passed will be fixed. This is sometimes referred to as a partially applied function. This ability can be used to improve the code above like this:
let filterExpression = '$data.finance.contingency < $data.risk.open80PctCostImpact'
let fnFilterProjects = wrappedEval.bind(this, filterExpression)
let aFilteredProjects = aProjects.filter(p => fnFilterProjects(p) )
The big difference here is that the call to wrappedEval
only happens once. The expression we are going to use for every item of the array is partially applied. So, if we had a large list of projects, we should gain a considerable performance improvement because the JS engine is not repeatedly passing that expression text to the same function over and over again.
History
- 15th April, 2021: First draft