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?
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With