JavaScript中如何指定eval()的范围?

34

有没有办法在特定的作用域中执行eval() (但不是全局)

例如,以下代码不能正常工作(第二个语句中未定义a),因为它们位于不同的作用域中:

eval(var a = 1); 
eval(alert(a));
如果可能的话,我想临时创建一个作用域。例如(语法肯定是错的,但只是为了说明这个想法)。
var scope1;
var scope2;
with scope1{
    eval(var a = 1); eval(alert(a));  // this will alert 1
}
with scope2{
    eval(var a = 1); eval(a++); eval(alert(a));  // this will alert 2
}
with scope1{
    eval(a += 2); eval(alert(a)); // this will alert 3 because a is already defined in scope1
}

有什么想法可以实现类似这样的效果吗?谢谢!


如果有人仍然感兴趣,我刚刚在这里发布了一个答案https://dev59.com/q3RB5IYBdhLWcg3wtZIV#43306962 - Ge Liu
你在 eval() 函数的参数周围缺少引号。 - Barmar
14个回答

24

你可以使用"use strict"让eval函数执行的代码局限在eval函数本身中。

其次,在严格模式下,eval执行的代码不会引入新的变量到周围的作用域中。在普通模式下,eval("var x;")会将变量x引入到周围的函数或全局作用域中。这意味着,一般情况下,在一个包含调用eval的函数中,除了参数或者局部变量之外的任何名称都必须在运行时映射到特定的定义(因为那个eval可能会引入一个新的变量,从而隐藏外部变量)。在严格模式下,eval只为被评估的代码创建变量,因此eval不能影响名称是指向外部变量还是某些局部变量

var x = 17;                                       //a local variable
var evalX = eval("'use strict'; var x = 42; x");  //eval an x internally
assert(x === 17);                                 //x is still 17 here
assert(evalX === 42);                             //evalX takes 42 from eval'ed x
如果一个函数使用了"use strict"声明,那么该函数内的所有内容都将在严格模式下执行。以下代码的作用与上面的代码相同:
function foo(){
    "use strict";

     var x = 17;
     var evalX = eval("var x = 42; x");
     assert(x === 17);
     assert(evalX === 42);
}

1
顺便提一下,使用'use strict'时,with语句会导致异常。引用:启用严格模式后,with(){}语句已经死亡 - 实际上它甚至出现了语法错误。来源:http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ - T. Junghans
@Joseph the dreamer,这个对我不起作用。我在Chrome上尝试了一下。哪个浏览器应该支持这个? - Zo72
6
这段代码似乎没有完成原帖作者要求的功能。它似乎是防止调用eval函数改变调用它时所在的范围,但提问者想允许eval调用者控制eval所操作的范围,而不是防止修改(他举的例子中包含了此类修改,因此显然这是他想要的)。 - Periata Breatta

15

在一个函数中创建你想要存在于作用域中的变量,将它们声明为局部变量。然后,从该函数中返回一个具有单个参数并调用eval的本地定义函数。该实例的eval将使用其包含函数的作用域,该函数嵌套在你的顶层函数的作用域内。每次调用顶层函数都会创建一个新的作用域,并生成一个新的eval函数实例。为了保持所有内容的动态性,甚至可以在顶层函数中使用调用eval来声明将在该作用域中使用的变量。

示例代码:

function makeEvalContext (declarations)
{
    eval(declarations);
    return function (str) { eval(str); }
}

eval1 = makeEvalContext ("var x;");
eval2 = makeEvalContext ("var x;");

eval1("x = 'first context';");
eval2("x = 'second context';");
eval1("window.alert(x);");
eval2("window.alert(x);");

https://jsfiddle.net/zgs73ret/


但是在"use strict"模式下,这并不起作用。 - Jianwu Chen

14

对我来说,这是最好的解决方案:

const scopedEval = (scope, script) => Function(`"use strict"; ${script}`).bind(scope)();

使用方法:

scopedEval({a:1,b:2},"return this.a+this.b")

1
太棒了!这段神奇的代码片段是我发现的唯一一种可以在ShadowRoot中执行注入脚本并能够在该范围内进行交互的方法。 - Besworks
你真是个混蛋! - undefined

8
< p > 简单易懂。 < /p >

// Courtesy of Hypersoft-Systems: U.-S.-A.
function scopeEval(scope, script) {
  return Function('"use strict";return (' + script + ')').bind(scope)();
}

scopeEval(document, 'alert(this)');


Function.bind(scope) 解决了我的问题。非常感谢! - retromuz
以下代码将不会被作用域限定:scopeEval('在作用域内求值', "new Function('alert(this)')()") - hldev
1
bind方法绑定函数的接收者(this),而不是作用域。这意味着您必须将所有变量引用为this.varname - corwin.amber
运行代码片段,看起来在某些情况下可以访问外部变量:https://dev59.com/w2kw5IYBdhLWcg3wlroa#75085984 - bristweb

3
这里的方法是允许上下文对象来参数化表达式的评估。
首先,使用Function()构造函数创建一个函数,该函数接受上下文的每个键以及要评估的表达式;函数体返回评估后的表达式。然后,使用上下文的所有值和要评估的表达式调用该函数。
function scopedEval(context, expr) {
    const evaluator = Function.apply(null, [...Object.keys(context), 'expr', "return eval('expr = undefined;' + expr)"]);
    return evaluator.apply(null, [...Object.values(context), expr]);
}

// Usage
const context = {a: 1, b: 2, c: {d: 3}};
scopedEval(context, "a+b+c.d");  // 6

通过使用 Function.prototype.apply,不需要预先知道参数数量和名称。因为参数被限定在 evaluator 函数范围内,它们可以直接从表达式中访问(而无需使用 this.a)。

1
不错的解决方案。 不过,无论上下文如何,表达式的值也可以被获取。 我会确保取消设置:“return eval('expr = undefined;' + expr)” - Angel Politis
@AngelPolitis 感谢指出,已更新。 - rayscan
在你的解决方案中,你不能使用上下文中的“expr”属性。我已经修复了它。看看我的答案。 - Дмитрий Васильев

3

这里有一个大约20行的JS类,它使用词法作用域中的eval实现了可扩展的上下文:

// Scope class
//   aScope.eval(str) -- eval a string within the scope
//   aScope.newNames(name...) - adds vars to the scope
function Scope() {
  "use strict";
  this.names = [];
  this.eval = function(s) {
    return eval(s);
  };
}

Scope.prototype.newNames = function() {
  "use strict";
  var names = [].slice.call(arguments);
  var newNames = names.filter((x)=> !this.names.includes(x));

  if (newNames.length) {
    var i, len;
    var totalNames = newNames.concat(this.names);
    var code = "(function() {\n";

    for (i = 0, len = newNames.length; i < len; i++) {
      code += 'var ' + newNames[i] + ' = null;\n';
    }
    code += 'return function(str) {return eval(str)};\n})()';
    this.eval = this.eval(code);
    this.names = totalNames;
  }
}


// LOGGING FOR EXAMPLE RUN
function log(s, eval, expr) {
 s = '<span class="remark">' + String(s);
  if (expr) {
    s += ':\n<b>' + expr + '</b>   -->   ';
  }
  s += '</span>';
  if (expr) {
    try {
      s += '<span class="result">' + JSON.stringify(eval(expr)) + '</span>';
    } catch (err) {
      s += '<span class="error">' + err.message + '</span>';
    }
  }
  document.body.innerHTML += s + '\n\n';
}
document.body.innerHTML = '';


// EXAMPLE RUN
var scope = new Scope();
log("Evaluating a var statement doesn't change the scope but newNames does (should return undefined)", scope.eval, 'var x = 4')
log("X in the scope object should raise 'x not defined' error", scope.eval, 'x');
log("X in the global scope should raise 'x not defined' error", eval, 'x');
log("Adding X and Y to the scope object");
scope.newNames('x', 'y');
log("Assigning x and y", scope.eval, 'x = 3; y = 4');
log("X in the global scope should still raise 'x not defined' error", eval, 'x');
log("X + Y in the scope object should be 7", scope.eval, 'x + y');
log("X + Y in the global scope should raise 'x not defined' error", eval, 'x + y');
.remark {
  font-style: italic;
}

.result, .error {
  font-weight: bold;
}

.error {
  color: red;
}
<body style='white-space: pre'></body>


很棒的补充和良好的示例,谢谢! 我在想是否有一种方法可以将一个值“注入”到当前作用域中。例如 scope.define("x", 3) - Fadi Chamieh
1
要将"变量添加"到作用域中,您可以在旧作用域内创建一个新的带有新变量的作用域。新作用域将继承旧作用域的变量。但这不会改变旧作用域的变量集。您可以将旧作用域的eval属性分配为新作用域的eval值。 - Bill Burdick

3
你可以查看vm-browserify项目,该项目将与browserify一起使用。
它通过创建<iframe>并在该<iframe>中执行代码来工作。 实际上,代码非常简单,因此如果您不想使用库本身,可以为自己的目的调整基本思路。

1
我不确定这会增加多少内容,但我想发布我的版本的Function构造函数解决方案,其中包含现代语法糖以及我认为是避免污染作用域的内部解决方案,其中包含评估文本。 (通过牺牲this上下文,在其中属性甚至可以在use strict;代码中被delete
class ScopedEval {
    /** @param {Record<string,unknown>} scope */
    constructor(scope) {
        this.scope = scope;
    }
    eval(__script) {
        return new Function(...Object.keys(this.scope),`
                return eval(
                    '"use strict";delete this.__script;' 
                    + this.__script
                );
            `.replace(/[\n\t]|  +/g,'')
        ).bind({__script})(...Object.values(this.scope));
    }
}

个人而言,我更喜欢在添加或调整作用域以及评估某些代码时进行分离,因此可以这样使用:

const context = {
  hi: 12,
  x: () => 'this is x',
  get xGet() {
    return 'this is the xGet getter'
  }
};

const x = new ScopedEval(context)
console.log(x.eval('"hi = " + hi'));
console.log(x.eval(`
let k = x();
"x() returns " + k
`));
console.log(x.eval('xGet'));

x.scope.someId = 42;
console.log(x.eval('(() => someId)()'))


0

简单粗暴的方法:

如果你的范围不是太动态,只有一些静态和只读声明,那么就把它们放在一个字符串中,并与你想要执行的字符串连接起来,像这样:

  const scopeAll = `
    const myFunc = (a, b) => a + b + s; 
  `

  const scope1  = `
    ${scopeAll}
    const s = 'c';
  `
  const scope2  = `
    ${scopeAll}
    const s = 'd';
  `
  
  const myStringToExecute = `
    myFunc('a', 'b')
  `
  
  
  console.log(eval(scope1 + myStringToExecute));
  console.log(eval(scope2 + myStringToExecute));


0

要小心,似乎有些实例仍然可以访问外部变量。下面的代码应该会失败,但如果你运行它,你会发现它能够工作。

let exp = 'a+b';
let a = 1;
let b = 2;

function scopeEval(scope, script) {
  return Function('"use strict";return (' + script + ')').bind(scope)();
}
console.log(scopeEval({}, exp));


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