Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative to 'with' in the mini javascript spreadsheet system

Tags:

javascript

I've just come across this elegant javascript spreadsheet code which I'd not seen before: http://jsfiddle.net/ondras/hYfN3/

It uses cell reference named getter objects as properties on a DATA object and 'with' to scope evaluation of the cell values.


//elm.id is the cell reference, DATA is an object whose properties are these getter wrappers
Object.defineProperty(DATA, elm.id, {get:getter});

Magic happens in the getter:

    //My comments but jsfiddle code from Ondřej Žára's fiddle 
    //Cell value getter function..  
    var getter = function() {
        var value = localStorage[elm.id] || ""; //Direct cell contents
        if (value.charAt(0) == "=") { //Got a formula, work it out
            //strip the '=' and evaluate recursively with this getter func
            with (DATA) return eval(value.substring(1));
        } else { // Else just send back the cell contents
            return isNaN(parseFloat(value)) ? value : parseFloat(value); 
        }
    };

It is a beautiful thing but given that 'with' is on the way out I wondered if there is a way to replace the usage succinctly without broadening the scope of the DATA object?

like image 604
Andrew Avatar asked May 20 '19 15:05

Andrew


1 Answers

I would avoid with and eval using the Function constructor:

I have done something similar in the past:

function compile(lexicalScope) {
  const params = Object.keys(lexicalScope).join(',');
  const values = Object.values(lexicalScope);

  return function (expr) {
    const compiledFn = new Function(params, '"use strict"; return ' + expr);
    return compiledFn.apply(null, values);
  };
}

const variables = {A1: 100, B1: 200};

const evaluator = compile(variables);

console.log(evaluator("A1 + B1"));    // 300
console.log(evaluator("B1 * 100")); // 20000

Basically we pass an object that will act as the lexical scope of the function, we extract the property names and pass them to the first argument of the Function constructor.

Then in the body of the constructed function, I like to start by defining the function as strict, since by default functions created by the Function constructor run on sloppy mode.

And at the end, we simply return the expression. Afterwards, we execute the dynamic function using apply.

EDIT: I was able to give a look to the spreadsheet, and I made some modifications to my approach to make it work with this implementation.

First, the DATA object has all the getters as non enumerable, this means Object.keys returns no properties, instead of using all the available properties, I parse the possible identifiers that come in the expression, and use them as the possible arguments of the dynamic function:

function compile(expr) {
  // possible parameters for dynamic function
  const identifiers = [...new Set(expr.match(/[A-Z]+\d/g))]
  const compiledFn = new Function(identifiers, '"use strict"; return ' + expr)
  return function(context) {
    // extract values
    const values = identifiers.map(id => context[id])
    return compiledFn(...values)
  }
}

This way the dynamic function will receive only the arguments that are actually used in the expression.

How does it work ?

The compile function is actually a factory of functions, when we call it with an expression, for example compile('A1+B2*C3'), it will generate dynamically a function, using the Function constructor with the following form:

function anonymous(A1,B2,C3) {
  "use strict";
  return A1+B2*C3
} 

This function will be stored in the scope of the closure of the compile function. Another function is returned, and this one is the one that receives the object as argument, where the properties are stored, depending on what identifiers where used in the expression it extracts them and uses them to apply the dynamic function we created at first place.

You can see a working example here.

This could be further optimized if we memoize the compile function.

If you are interested, you can read this article about the differences between using eval vs new Function.

like image 82
Christian C. Salvadó Avatar answered Sep 18 '22 18:09

Christian C. Salvadó