3

As a part of my template engine I need to be able to evaluate expressions in JavaScript like: "first || second" in context of some object serving role of global namespace. So properties of the object should be seen as global variables.

So far I came up with this function:

function scopedEval(str, scope) {
   var f = new Function("scope", "with(scope) { return (" + str + "); }");
   return f(scope);  
}

Everything is fine with it and I am able to run it as:

var scope = { first:1, second:2 };
var expr1  = "first || second";
alert( scopedEval(expr1,scope) );

it alerts me 1.

The only problem with variables that were not defined in the scope object. This:

var expr2  = "third || first || second";
alert( scopedEval(expr2,scope) );

generates an error "variable third is not defined". But I would like all unknown variables to be resolved to undefined rather than throwing errors. So "third || first || second" should yield to 1 again.

As for my knowledge such thing is not possible in modern JavaScript but I might miss something so asking. Any ideas?

Here is an example to play with: http://jsfiddle.net/nCCgT/

c-smile
  • 26,734
  • 7
  • 59
  • 86
  • 1
    [`jade`](https://github.com/visionmedia/jade) does something similar but has an entire compiler build in. Yes it's possible, build a compiler. – Raynos Jun 14 '11 at 07:30
  • 3
    Wow: `eval` and `with` in one function! Mater! – KooiInc Jun 14 '11 at 08:07
  • 3
    @Koolinc: It lacks a `break – Ates Goral Jun 14 '11 at 08:20
  • 2
    @Ates: for completeness it should also use `document.write` to write a `` element to the page, and create a few popups using `window.open` etc. etc. Sometimes web development is really *sisyphean* (http://en.wikipedia.org/wiki/Sisyphus) – KooiInc Jun 14 '11 at 08:50
  • You CAN do something like this using eval and lexical scoping. I provide a Scope class in my answer, [here](http://stackoverflow.com/a/40503510/1026782) – Bill Burdick Nov 09 '16 at 09:35

3 Answers3

3

Intercepting access to non-existing properties is non-standard and not possible with most ECMAScript implementations. The Rhino version shipping with the JDK seems to provide this feature, and SpiderMonkey can catch arbitrary method calls, but not general property access.

There's a proxy proposal for ES Harmony, so the need for such a feature has been acknowledged, but right now, you're out of luck.

As a really ugly workaround, you could scan str for (potential) identifiers and add them to scope like this:

var toks = str.match(/[A-Za-z_$][\w$]*/g);
for(var i = 0; i < toks.length; ++i) {
    if(!(toks[i] in scope))
        scope[toks[i]] = undefined;
}
Christoph
  • 164,997
  • 36
  • 182
  • 240
  • Probably useful to check if the token is a valid identifier. Something like: `(/[a-z_\$]\w+/i).test(toks[i])` . And the `\b` splits up `$foo`, so if that matters you'll need to find a workaround for that... – Gijs Jun 14 '11 at 08:06
  • 1
    I already upvoted this answer. But, trying to come up with a quick workaround instead of using a full-fledged JS parser is just opening a can of worms. The edge cases that need to be considered will never end. Perhaps the right solution is to bite the bullet and not handle undefined variables/properties gracefully at all. I believe jQuery Templates also fails ungracefully. – Ates Goral Jun 14 '11 at 08:12
  • @Gijs, Ates: the new regex should be less problematic, but still won't handle unicode escapes... – Christoph Jun 14 '11 at 08:24
  • I've accepted the answer as it in principle allows to achieve the goal. But as you said it is ugly and highly non-effective indeed. `eval()` combined with the `with` with regexps on top of that are too much. In my TIScript eval function ( http://www.terrainformatica.com/tiscript/Global.whtm ) has signature `eval(str[,obj])` that allows to do exactly that - evaluate in `obj` namespace. And yet TIScript has `property undefined(name, val)` that allows to catch access to undefined properties. – c-smile Jun 14 '11 at 16:02
1

As others have noted, you cannot do this with the current JS implementations, unless you get into at least a basic amount of parsing (for identifiers).

What I'd like to add is: Maybe the right answer is not to do anything and let JS just fail as it fails right now. Any solution you'll attempt to implement will be either too complex and/or slow, or a half-baked implementation that leaves holes. So, it may not be worth the effort and complexity to make your template engine that robust.

You could simply document, "Variable/property references must be valid. Otherwise, you'll get an error." and put the onus on the programmers.

Ates Goral
  • 137,716
  • 26
  • 137
  • 190
  • 1
    Unfortunately mentioning "Variable/property references must be valid" does not work in my case as in others. Consider the Mustache or Kite templates where you can define `{{#one}}..markup..{{/one}}`. That can be implemented without `eval()` at all. And if `one` does not exist the section is just skipped without error. I am adding there conditional `if` construct: `{{? one && two}}..markup..{{/?}}`. Expression `one && two` should evaluate without throwing exceptions. – c-smile Jun 14 '11 at 16:10
0

If I understand your question correctly, this should do what you want:

function scopedEval(str, scope) {
   var toCheck = str.split(' || ');
    for(var i = 0; i < toCheck.length; ++i){
        if(scope[toCheck[i]])
            return scope[toCheck[i]];
    }
}

May need some better checking here and there, but the method works on your fiddle :) http://jsfiddle.net/nCCgT/1/

Thor Jacobsen
  • 8,621
  • 2
  • 27
  • 26
  • 3
    I believe the `||` was a generic example and he wanted any reference to undefined variables not throw an error. – Raynos Jun 14 '11 at 07:23
  • `first || last` is a generic example, unfortunately it can be any valid JS expression there. – c-smile Jun 14 '11 at 15:48