3

Update

I've come up with a concise solution to this problem, that behaves similar to node's vm module.

var VM = function(o) {
    eval((function() {
        var src = '';
        for (var prop in o) {
            if (o.hasOwnProperty(prop)) {
                src += 'var ' + prop + '=o[\'' + prop + '\'];';
            }
        }
        return src;
    })());
    return function() {
        return eval(arguments[0]);
    }
}

This can then be used as such:

var vm = new VM({ prop1: { prop2: 3 } });
console.assert(3 === vm('prop1.prop2'), 'Property access');

This solution overrides the namespace with only the identifier arguments taken.

Thanks to Ryan Wheale for his idea.

Short version

What is the best way to evaluate custom javascript expression using javascript object as a context?

var context = { prop1: { prop2: 3 } }

console.assert(3 === evaluate('prop1.prop2', context), 'Simple expression')

console.assert(3 === evaluate('(function() {' +
                              ' console.log(prop1.prop2);' +
                              ' return prop1.prop2;' +
                              '})()', context), 'Complex expression')

It should run on the latest version of node (0.12) and all evergreen browsers at the time of writing (3/6/2015).

Note: Most templating engines support this functionality. For example, Jade.

Long version

I'm currently working on an application engine, and one of its features is that it takes a piece of code and evaluates it with a provided object and returns the result.

For example, engine.evaluate('prop1.prop2', {prop1: {prop2: 3}}) should return 3.

This can be easily accomplished by using:

function(code, obj) {
    with (obj) {
        return eval(code);
    }
};

However, the usage of with is known to be bad practice and will not run in ES5 strict mode.

Before looking at with, I had already written up an alternative solution:

function(code, obj) {
    return (function() {
        return eval(code);
    }).call(obj, code);
}

However, this method requires the usage of this.

As in: engine.evaluate('this.prop1.prop2', {prop1: {prop2: 3}})

The end user should not use any "prefix".

The engine must also be able to evaluate strings like

'prop1.prop2 + 5'

and

'(function() {' +
'   console.log(prop1.prop2);' +
'   return prop1.prop2;' +
'})()'

and those containing calls to functions from the provided object.

Thus, it cannot rely on splitting the code string into property names alone.

What is the best solution to this problem?

Synchronous
  • 101
  • 9
  • 1
    Split it by dot then iterate and access attributes one by one. – zerkms Mar 05 '15 at 22:49
  • I can't do that, because it must evaluate different kinds of code. – Synchronous Mar 05 '15 at 22:50
  • See [javascript test for existence of nested object key](http://stackoverflow.com/q/2631001/1529630). Specifically [this answer](http://stackoverflow.com/a/2631521/1529630). – Oriol Mar 05 '15 at 22:55
  • If you are going to evaluate random code snippets, then you can either use *eval* or write your own parser and evaluation engine. *eval* is probably more efficient and easier to use. – RobG Mar 05 '15 at 22:56
  • @Synchronous: to solve the problem you explained you don't need to evaluate anything. – zerkms Mar 05 '15 at 23:05
  • @zerkms What do you mean? – Synchronous Mar 05 '15 at 23:15
  • 3
    Asking how to access properties, and then asking how to parse and evaluate the last block of text are very different questions. The question you spent most of your post setting up here is significantly nullified by the extra conditions you tacked on at the very end. There's a good answer for how to access properties - but for the last bit, as said, you're essentially asking how to implement `eval` from scratch. – Sam Hanley Mar 05 '15 at 23:23
  • Please update the title of your question to express your actual issue. – Felix Kling Mar 05 '15 at 23:25

2 Answers2

2

I don't know all of your scenarios, but this should give you a head start:

http://jsfiddle.net/ryanwheale/e8aaa8ny/

var engine = {
    evaluate: function(strInput, obj) {
        var fnBody = '';
        for(var prop in obj) {
            fnBody += "var " + prop + "=" + JSON.stringify(obj[prop]) + ";";
        }
        return (new Function(fnBody + 'return ' + strInput))();
    }
};

UPDATE - I got bored: http://jsfiddle.net/ryanwheale/e8aaa8ny/3/

var engine = {
    toSourceString: function(obj, recursion) {
        var strout = "";

        recursion = recursion || 0;
        for(var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                strout += recursion ? "    " + prop + ": " : "var " + prop + " = ";
                switch (typeof obj[prop]) {
                    case "string":
                    case "number":
                    case "boolean":
                    case "undefined":
                        strout += JSON.stringify(obj[prop]);
                        break;

                    case "function":
                        // won't work in older browsers
                        strout += obj[prop].toString();
                        break;

                    case "object":
                        if (!obj[prop])
                            strout += JSON.stringify(obj[prop]);
                        else if (obj[prop] instanceof RegExp)
                            strout += obj[prop].toString();
                        else if (obj[prop] instanceof Date)
                            strout += "new Date(" + JSON.stringify(obj[prop]) + ")";
                        else if (obj[prop] instanceof Array)
                            strout += "Array.prototype.slice.call({\n "
                                + this.toSourceString(obj[prop], recursion + 1)
                                + "    length: " + obj[prop].length
                            + "\n })";
                        else
                            strout += "{\n "
                                + this.toSourceString(obj[prop], recursion + 1).replace(/\,\s*$/, '')
                            + "\n }";
                        break;
                }

                strout += recursion ? ",\n " : ";\n ";
            }
        }
        return strout;
    },
    evaluate: function(strInput, obj) {
        var str = this.toSourceString(obj);
        return (new Function(str + 'return ' + strInput))();
    }
};
Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
  • Thank you, this is a great solution (for POJOs). – Synchronous Mar 06 '15 at 03:15
  • Is it possible to achieve this with functions? I know that `JSON.stringify` will not convert functions to strings. – Synchronous Mar 06 '15 at 03:31
  • In newer browsers, and I think IE8 even, support `Function.prototype.toString`. Instead of `JSON.stringify` you would need to recursively iterate over each property and handle each type of data separately. For example, lets say `prop1` was a Date object, you would want the generated code to be `var prop1 = new Date( JSON.stringify(obj[prop1]) );`. If it was a function, you'd want the code to render as `var prop1 = obj[prop1].toString();` - but that's a fun project for you to figure out ;) – Ryan Wheale Mar 06 '15 at 18:13
  • I just updated my answer. It was a fun little project. Let me know how it works. – Ryan Wheale Mar 06 '15 at 21:42
  • Thanks for the help. I've created a fairly robust function out of this. http://jsfiddle.net/4qgqa4ay/ – Synchronous Mar 07 '15 at 04:14
  • Yours seems to work pretty well. I recommend using JSON.stringify for dates, null, and undefined, as the result of Date.toString() is not parseable by the Date constructor whereas the result of Date.toJSON is, and stringify will convert nulls and undefines to parseable versions as well (and more transportable across APIs and all that...). Great question. – Ryan Wheale Mar 08 '15 at 01:31
  • @RyanWheale—"*the result of Date.toString() is not parseable by the Date constructor*" ES5 says that implementations **must** correctly parse their own output from *Date.prototype.toString* (see [*Date.parse*](http://ecma-international.org/ecma-262/5.1/#sec-15.9.4.2)). However, the output from one implementation may not be correctly parsed by another. Also, most fail to parse their own format for years 0 to 99 inclusive (it's converted to 1900 to 1999), but it's not common to want to do that. ;-) – RobG Mar 09 '15 at 00:40
  • Weird, I thought I tested `new Date( '' + (new Date()) )` before posting that last comment and it failed... now it's working as I would expect. My bad. Either way, my biggest point was that the result of toJSON (eg. `2015-03-09T23:55:46.380Z`) should be parseable by just about anything, whereas toString (eg. `Mon Mar 09 2015 17:53:12 GMT-0600 (MDT)`) is not as cross-the-wire friendly. – Ryan Wheale Mar 10 '15 at 00:03
0

UPDATE 3: Once we figured out what you are really asking, the question is clear: you do not do that. Especially in the strict mode.

As an viable alternative to your approach please refer to the documentation on require.js, common.js and other libraries allowing you to load modules in the browser. basically the main difference is that you do not do prop1.prop2 and you do context.prop1.prop2 instead.

If using context.prop1.prop2 is acceptable, see jsfiddle: http://jsfiddle.net/vittore/5rse4jto/

"use strict";

var obj = { prop1 : { prop2: 'a' } }

function evaluate(code, context) {
  var f = new Function('ctx', 'return ' + code);
  return f(context)
}

alert(evaluate('ctx.prop1.prop2', obj))

alert(evaluate(
'(function() {' +
'   console.log(ctx.prop1.prop2);' +
'   return ctx.prop1.prop2;' +
'}) ()', obj))

UPDATE: Answer to original question on how to access properties with prop1.prop2

First of all, you can access your variable using dictionary notation, ie:

obj['prop1']['prop2'] === obj.prop1.prop2

Give me several minutes to come up with example of how to do it recursively

UPDATED:This should work (here is gist):

 function jpath_(o, props) { 
    if (props.length == 1) 
         return o[props[0]];  
    return jpath_(o[props.shift()], props) 
 }

 function jpath(o, path) { 
    return jpath_(o, path.split('.')) 
 }

 console.log(jpath(obj, 'prop1.prop2'))
vittore
  • 17,449
  • 6
  • 44
  • 82
  • You could have just referenced [*In javascript how can I dynamically get a nested property of an object*](http://stackoverflow.com/questions/6906108/in-javascript-how-can-i-dynamically-get-a-nested-property-of-an-object). ;-) – RobG Mar 05 '15 at 22:59
  • @RobG I could've if I'd search for it first, instead I just wrote my tiny version :p – vittore Mar 05 '15 at 23:01
  • I _am_ using eval, but I also must not make the user use the `this` keyword. – Synchronous Mar 05 '15 at 23:05
  • Make a wrapper for eval which accept your context (this) as an argument, and alter every string to be eval'ed into `with` statement you already now how to use. – vittore Mar 05 '15 at 23:09
  • No, it works fine with a `with` statement, but I cannot use it, because the code should run in strict mode. – Synchronous Mar 05 '15 at 23:11
  • @Synchronous You really should've start with that when writing your question, you know. "I have to have my own javascript engine running in strict mode, with the context I pass to it. " – vittore Mar 05 '15 at 23:18
  • @Synchronous where it has to be executed, node.js or browser? which browsers? which node? – vittore Mar 05 '15 at 23:18
  • @vittore Latest node as of writing, really, and all evergreen browsers. It should run everywhere relatively modern. – Synchronous Mar 05 '15 at 23:35
  • @Synchronous again, add it to the question. – vittore Mar 05 '15 at 23:36
  • @vittore Thank you, but I can't use any kind of prefix. I've add it to my answer. Sorry. – Synchronous Mar 06 '15 at 00:07
  • @Synchronous pretty much any template engine has this functionality. – vittore Mar 06 '15 at 00:45
  • @vittore So how would I implement it? – Synchronous Mar 06 '15 at 00:50
  • @Synchronous you basically need expression parser, compiler and executor. Just take a look at amount of code Jade has. You better reuse one if it mostly suits you instead of writing new one. – vittore Mar 06 '15 at 06:22