Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Safely Evaluating JavaScript with Context Data

5.00/5 (1 vote)
15 Apr 2021CPOL3 min read 5.3K  
A simple function to use instead of eval() that allows an expression based on some passed data scope
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:

JavaScript
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.

JavaScript
// assume that the list was loaded from some server call
let aAllProjects = [p1, p2, p3]

Obviously, I could code up a filter rule on this list like this:

JavaScript
// 
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.

JavaScript
/*
* @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)();
}

Using the Code

Taking the example about projects above, we could use an expression like this:

JavaScript
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:

JavaScript
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)