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?

8
  • 1
    Split it by dot then iterate and access attributes one by one. Commented Mar 5, 2015 at 22:49
  • I can't do that, because it must evaluate different kinds of code. Commented Mar 5, 2015 at 22:50
  • See javascript test for existence of nested object key. Specifically this answer. Commented Mar 5, 2015 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. Commented Mar 5, 2015 at 22:56
  • 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. Commented Mar 5, 2015 at 23:23

2 Answers 2

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))();
    }
};
Sign up to request clarification or add additional context in comments.

8 Comments

Thank you, this is a great solution (for POJOs).
Is it possible to achieve this with functions? I know that JSON.stringify will not convert functions to strings.
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 ;)
I just updated my answer. It was a fun little project. Let me know how it works.
Thanks for the help. I've created a fairly robust function out of this. jsfiddle.net/4qgqa4ay
|
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'))

13 Comments

@RobG I could've if I'd search for it first, instead I just wrote my tiny version :p
I am using eval, but I also must not make the user use the this keyword.
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.
No, it works fine with a with statement, but I cannot use it, because the code should run in strict mode.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.