hiukim
hiukim

Reputation: 459

Specify scope for eval() in JavaScript?

is there any way I can execute eval() on a specific scope (but NOT global)?

for example, the following code doesn't work (a is undefined on the second statement) because they are on different scope:

eval(var a = 1); 
eval(alert(a));

If possible, I would like to create a scope on the fly. for example (the syntax is definitely wrong, but just to illustrate the idea)

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
}

Any idea on how to achieve something like this? Thanks!

Upvotes: 39

Views: 33899

Answers (14)

Clif Collins
Clif Collins

Reputation: 1

var a; eval('a = 1'); eval('alert(a)');

interesting, you must understand: "eval" is run inside an anonymous function, therefore the "var" declares "a" as a variable that is unreachable. If you delete the "var", then you must declare "a" in the local calling function. If "var a" is defined in the calling function then the local variable is used, otherwise "a" is undefined. However if eval is called from a class function then an undeclared variable is assigned to "window".

function A() { eval('a=1');  eval('alert(a)');  } 
A() ;                           // (output is undefined)
new A();                        // (output  window.a = 1)

function B()  { var a; eval('a=1');  eval('alert(a)');  } 
B()                          // (local variable, output 1)
  
function C { window.eval('a=1');  eval('alert(a)');  }  
C() ;                           // (output window.a = 1)
new C();                        // (output window.a = 1)

The value of "this" of eval is "window" if called from a orphan function. However if called from a class function the "this" is the class's "this".

A = test();  alert(window.a);
B = new test();  alert(B.a);
function test() {  eval(this.a = 1); }

The following overrides the "this" to "window" in a call function. However the call, alter and bind functions of eval work, but they do not change the "this" value;

C = new wtest();  alert(window.a);
function wtest() { window.eval(this.a = 2) }

Upvotes: -1

bristweb
bristweb

Reputation: 1215

be careful; it seems some instances will still get access to outside variables. the code below should fail, but if you run it, you can see it works

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

Upvotes: 1

Jason Song
Jason Song

Reputation: 2660

script evaluation that provides script-scope, app-level-scope and app-level-scripts-this-object. it is safe and not leaking to app

it also provides a console proxy to redirect console output, it is alive in script-scope, so the script call console.log would go to the proxy.

// app level scope, to share additional function and variable that can be used without `this` keyword.
const scope = {};
// app level this obj for all scripts, use this to reference it;
// so we could share data between scripts.
const thisObj = {};

/**
 * execute scripts
 * @param thisObj 'this' is an app level obj to share data between scripts
 * @param scriptsScope app level scope to share some global predefined functions.
 * @param localScope we can setup a 'console' proxy is in this scope.
 * @param script code to execute.
 */
const scopedEval = function scopedEval(thisObj, scriptsScope, localScope, script) {
  const context = { ...scriptsScope, ...localScope };
  // create new Function with keys from context as parameters, 'script' is the last parameter.
  const evaluator = Function.apply(null, [...Object.keys(context), 'script',
  `"use strict";
  try{${script}}
  catch (e) {
    console.error(e);
  }`]);
  // call the function with values from context and 'script' as arguments.
  evaluator.apply(thisObj, [...Object.values(context), script]);
}

/**
 * create a proxy for console. that will also write to the consoleElement
 * @param consoleElement the ui element to show the console logs, i.e. a div.
 * @returns {Proxy}
 */
const consoleProxy = consoleElement => new Proxy(console, {
  get: function (target, propKey) {
    const originalMethod = target[propKey];

    return function (...args) {
      // get time with milliseconds
      const now = new Date();
      const time = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0') + '.' + now.getMilliseconds().toString().padStart(3,'0');
      // text to show
      const text = document.createTextNode(`${time}: ${args.join(' ')}\n`);
      const span = document.createElement('span');
      span.appendChild(text);
      if (propKey === 'error') {
        span.style.color = 'red';
      } else if (propKey === 'warn') {
        span.style.color = 'orange';
      } else if (propKey === 'info') {
        span.style.color = 'blue';
      } else if (propKey === 'debug') {
        span.style.color = 'gray';
      }
      consoleElement.appendChild(span);
      // original console logs, if you need
      //originalMethod.apply(target, args);
    }
  }
});

const codes = document.querySelector('.codes');
const script1 = codes.querySelector('.script1').textContent;
const script2 = codes.querySelector('.script2').textContent;

const scriptConsole1 = codes.querySelector('.script-console1');
const scriptConsole2 = codes.querySelector('.script-console2');

scopedEval(thisObj, scope, { console: consoleProxy(scriptConsole1) }, script1);
scopedEval(thisObj, scope, { console: consoleProxy(scriptConsole2) }, script2);
.code {
background: lightgray;
}
<div class="codes">
  <div>
    <pre class="code">
      <code class="script1">
        console.log('hello');
        this.a = 1
        // error
        b++
      </code>
    </pre>
    
    <pre class="script-console1">
    </pre>
  <div>
  
  <div>
    <pre class="code">
      <code class="script2">
        this.a++
        console.log(this.a);
      </code>
    </pre>
    
    <pre class="script-console2">
    </pre>
  </div>
  
<div>

Upvotes: -2

joshhemphill
joshhemphill

Reputation: 592

I'm not sure how much this adds, but I thought I'd post my version of the Function constructor based solution with modern syntactic sugar and what I think is a good solution to avoid polluting the scope with the internal one containing the evaluated text. (By sacrificing the this context, on which properties can be deleted even inside use strict; code)

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

Personally I prefer being able to separate when I add or adjust the scope and when I eval some code, so this could be used like:

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

Upvotes: 1

Dmitry Vasilev
Dmitry Vasilev

Reputation: 6508

Function

const evaluate = (context, expr) =>
        Function(Object.keys(context).join(','), `return ${expr}`)
        (...Object.values(context));

Usage

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

Example

const evaluate = (context, expr) => Function(Object.keys(context).join(','), `return ${expr}`)(...Object.values(context));

const result = evaluate({a: 1, b: 2, c: {d: 3}}, "a+b+c.d");

console.log('result is: ', result);

Upvotes: -1

rayscan
rayscan

Reputation: 321

The approach here was to allow a context object to parameterize the evaluation of the expression.

First a function is created using the Function() constructor that accepts every key of the context as well as the expression to evaluate; the body returns the evaluated expression. Then that function is called with all of the values of the context and the expression to evaluate.

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

By using Function.prototype.apply the number of arguments and the names don't need to be known beforehand. Because the arguments are scoped to the evaluator function they are directly accessible from the expression (instead of requiring this.a).

Upvotes: 5

ItsaMeTuni
ItsaMeTuni

Reputation: 475

This is the simplest way I found to do that, but it doesn't use eval.

function execInContext(code, context)
{   
    return Function(...Object.keys(context), 'return '+ code (...Object.values(context));
}

Here we create a Function object. The Function constructor accepts an array of parameters, the last one is the code to be executed by the function and all others are argument names for the function. What we're doing is creating a function that has arguments with the same names as the fields in the context object and then calling this function with the values of the fields in context. So if you call

execInContext('myVar', {myVar: 'hi!'});

it's the same as

((myVar) => { return myVar; })('hi!');

and the result will be hi!

Upvotes: -1

chickens
chickens

Reputation: 22304

This worked for me the best:

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

Usage:

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

Upvotes: 22

gazdagergo
gazdagergo

Reputation: 6691

The poor man's method:

If your scope is not too dynamic, just a couple of static and read-only declarations, simply put it in a string and concatenate with the string what you wanna execute like this:

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

Upvotes: 0

Hypersoft Systems
Hypersoft Systems

Reputation: 518

Simple as pie.

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

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

Upvotes: 10

Bill Burdick
Bill Burdick

Reputation: 975

Here is a 20-line or so JS class that implements an extensible context using eval in a lexical scope:

// 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>

Upvotes: 3

Periata Breatta
Periata Breatta

Reputation: 498

Create the variables you want to exist in your scope as local variables in a function. Then, from that function, return a locally-defined function that has a single argument and calls eval on it. That instance of eval will use the scope of its containing function, which is nested inside the scope of your top level function. Each invocation of the top level function creates a new scope with a new instance of the eval function. To keep everything dynamic, you can even use a call to eval in the top level function to declare the variables that will be local to that scope.

Example code:

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/

Upvotes: 15

Domenic
Domenic

Reputation: 112827

You can look into the vm-browserify project, which would be used in conjunction with browserify.

It works by creating <iframe>s, and evaling the code in that <iframe>. The code is actually pretty simple, so you could adapt the basic idea for your own purposes if you don't want to use the library itself.

Upvotes: 3

Joseph
Joseph

Reputation: 119837

you can use the "use strict" to contain the eval'ed code within the eval itself.

Second, eval of strict mode code does not introduce new variables into the surrounding scope. In normal code eval("var x;") introduces a variable x into the surrounding function or the global scope. This means that, in general, in a function containing a call to eval every name not referring to an argument or local variable must be mapped to a particular definition at runtime (because that eval might have introduced a new variable that would hide the outer variable). In strict mode eval creates variables only for the code being evaluated, so eval can't affect whether a name refers to an outer variable or some local variable

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

If a function is declared with "use strict", everything in it will be executed in strict mode. the following will do the same as above:

function foo(){
    "use strict";

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

Upvotes: 23

Related Questions