如何在严格模式下使用上下文评估自定义JavaScript表达式?

3

更新

我提供了一种简洁的解决方案来解决这个问题,其行为类似于Node.js的vm模块。

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]);
    }
}

这样做可以直接使用:
var vm = new VM({ prop1: { prop2: 3 } });
console.assert(3 === vm('prop1.prop2'), 'Property access');

这个解决方案仅使用标识符arguments覆盖命名空间。

感谢Ryan Wheale的想法。

简短版本

使用JavaScript对象作为上下文,评估自定义JavaScript表达式的最佳方法是什么?

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')

它应该能在最新版本的node(0.12)和当时编写时的所有现代浏览器上运行(2015年3月6日)。

注:大多数模板引擎都支持此功能。例如,Jade

详细说明

我目前正在开发一个应用引擎,其中一个特点是将一段代码与提供的对象一起评估并返回结果。

例如,engine.evaluate('prop1.prop2', {prop1: {prop2: 3}})应返回3

这可以通过使用以下方式轻松完成:

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

然而,使用with被认为是不良实践,在ES5严格模式下将无法运行。

在看with之前,我已经编写了一个替代方案:

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

然而,这种方法需要使用this
例如:engine.evaluate('this.prop1.prop2', {prop1: {prop2: 3}}) 最终用户不应该使用任何“前缀”。
引擎还必须能够评估类似于字符串的内容。
'prop1.prop2 + 5'

并且

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

并且那些包含对提供对象中函数的调用的代码也需要处理。

因此,它不能仅依赖于将code字符串拆分为属性名称。

如何解决这个问题?


1
按点分割字符串,然后逐个迭代访问属性。 - zerkms
我不能这样做,因为它必须评估不同种类的代码。 - Synchronous
@zerkms 您是什么意思? - Synchronous
3
问如何访问属性和如何解析和评估最后一个文本块是非常不同的问题。你在这里花费大部分时间来设置的问题,在最后添加的附加条件实质上减弱了其意义。有一个很好的答案可以访问属性 - 但对于最后一部分,正如所说,你基本上是在问如何从头开始实现“eval”。 - Sam Hanley
请更新您的问题标题以表达您实际的问题。 - Felix Kling
显示剩余3条评论
2个回答

2
我不知道你的具体情况,但这应该能帮你入门: 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))();
    }
};

更新 - 我觉得有点无聊: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))();
    }
};

使用函数可以实现这个吗?我知道JSON.stringify不能将函数转换为字符串。 - Synchronous
感谢您的帮助。我已经将它们组合成了一个相当强大的函数。http://jsfiddle.net/4qgqa4ay/ - Synchronous
你的代码似乎运行得很好。我建议在处理日期、null和undefined时使用JSON.stringify,因为Date.toString()返回的结果无法被Date构造函数解析,而Date.toJSON返回的结果可以被解析,并且stringify也会将null和undefined转换成可解析的版本(更易于跨API传输...)。好问题。 - Ryan Wheale
@RyanWheale —“Date.toString()的结果无法被Date构造函数解析。” ES5规定实现必须正确解析自己从Date.prototype.toString输出的内容(参见Date.parse)。然而,一个实现的输出可能无法被另一个实现正确解析。此外,大多数实现无法解析年份0到99的自身格式(会转换为1900到1999),但通常不需要这样做。;-) - RobG
我之前以为测试过 new Date( '' + (new Date()) ),但在发表最后一条评论之前它失败了...现在它按照我的期望工作了。我的错。无论如何,我最大的观点是 toJSON 的结果(例如 2015-03-09T23:55:46.380Z)应该可以被任何东西解析,而 toString(例如 Mon Mar 09 2015 17:53:12 GMT-0600 (MDT))不太友好于跨网络传输。 - Ryan Wheale
显示剩余3条评论

0

更新 3:当我们弄清楚你真正想问什么时,问题就清晰了:你不这样做。特别是在严格模式下。

作为你的方法的可行替代,请参考关于 require.js、common.js 和其他允许你在浏览器中加载模块的库的文档。基本上,主要区别是你不使用prop1.prop2,而是使用context.prop1.prop2

如果使用context.prop1.prop2可接受,请查看 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))
更新: 关于如何使用 prop1.prop2 访问属性的问题的答案。
首先,您可以使用字典表示法访问变量,例如:
obj['prop1']['prop2'] === obj.prop1.prop2

给我几分钟时间来举例说明如何递归地完成它。

更新:这应该可以工作(这里是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'))

@Synchronous 在哪里执行,是在node.js还是浏览器?支持哪些浏览器?哪个版本的node.js? - vittore
@Synchronous 再次,将其添加到问题中。 - vittore
@vittore 谢谢你,但我不能使用任何前缀。我已经将它添加到我的答案中。抱歉。 - Synchronous
@Synchronous 在任何模板引擎中都有此功能。 - vittore
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接