-3

This code works optimally and is easily comprehensible:

function evalInScope(js, contextAsScope) {
    //# Return the results of the in-line anonymous function we .call with the passed context
    return function() {
        with(this) {
            return eval(js);
        };
    }.call(contextAsScope);
}
evalInScope("a + b", {a: 1, b: 2}); // 3 obviously, but fails in strict mode!

However, the "smart" brains decided to remove the with statement without a proper replacement.

Question: how to make it work again in ES6, which is automatically in strict mode?

kungfooman
  • 4,473
  • 1
  • 44
  • 33
  • 1
    https://stackoverflow.com/q/9781285/1427878 and https://stackoverflow.com/q/543533/1427878 have some approaches, maybe one of those works for your situation. – CBroe Aug 28 '23 at 09:17
  • 5
    First off, ES6 is *not* automatically in strict mode, only inside ESM modules and class bodies. Second, `with` was removed from strict mode for a good reason. Your example seems like an XY problem to me. What do you want to achieve with that function? What are you evaluating? (if it's hardcoded, don't use `eval`; if it's user input, `eval`ing it is **dangerous** and you should parse and interpret it instead) – FZs Aug 28 '23 at 09:22
  • @kungfooman A REPL is a different story. Do you want to give the user full access to everything the JS code can access? – FZs Aug 28 '23 at 09:28
  • @FZs I have a JSDOM context in node and want to make that the context, instead of giving the default `globalThis` to eval. – kungfooman Aug 28 '23 at 09:30
  • @kungfooman OK, but do you want to allow the user to access Node APIs as well, or do you want a complete sandbox? – FZs Aug 28 '23 at 09:32
  • @FZs It doesn't need to be a sandbox, it's not safety related, just my own code, so I'm fine if the `JSDOM context` simply augments `globalThis`. – kungfooman Aug 28 '23 at 09:34
  • 2
    @kungfooman Ok, then. You have several ways to bypass this. 1. You can implement this function in a CommonJS module (you can then use that module from ESMs, too), where there is a non-strict environment. 2. Node has [some interesting APIs in the `vm` module](https://nodejs.org/api/vm.html), which may allow you something like this. 3. You can use the `Function` constructor instead of `eval`, which allows you to pass in dynamically named parameters. – FZs Aug 28 '23 at 09:41
  • 1
    @FZs Thank you very much, I just went with @Bergi's answer. The `vm` module API looks very interesting and I may experiment a bit to learn more about it aswell. The `Function` approach seems to be the easiest and working out-of-the-box in most environments *thumbs up* – kungfooman Aug 28 '23 at 09:49
  • 1
    @kungfooman Wow, Bergi's `Function`-constructor approach is different from what I had in mind. – FZs Aug 28 '23 at 09:57
  • it might help to describe with more clarity what this code is supposed to achieve. I know you wrote a comment in the code block, but further elaboration would help, I think. – starball Aug 28 '23 at 20:35

2 Answers2

5

Don't use eval, create a new Function instead. It won't inherit lexical strict mode - and even better, it won't inherit all your function-scoped and module-scoped variables:

"use strict";

function evalInScope(js, contextAsScope) {
  return new Function(`with (this) { return (${js}); }`).call(contextAsScope);
}

console.log(evalInScope("a + b", { a: 1, b: 2 })); // 3

Also you don't get the weird "(last) statement result" return value that eval uses, but can either confine the js code to be an expression or include a return statement in the code itself.


Alternatively, if you don't actually need to use a with statement with all its intricacies but just want to make a dynamic set of constant variables available to the evaled code, just generate the code for those constants dynamically. This allows to eval the code in strict mode even:

"use strict";

function evalInScope(js, contextAsScope) {
  return new Function(
    `"use strict";
    const {${Object.keys(contextAsScope).join(', ')}} = this;
    return (${js});`
  ).call(contextAsScope);
}

console.log(evalInScope("a + b", { a: 1, b: 2 })); // 3

Or if the code doesn't use the this keyword itself, maybe also

"use strict";

function evalInScope(js, contextAsScope) {
  return new Function(
    '{' + Object.keys(contextAsScope).join(', ') + '}',
    `return (${js});`
  )(contextAsScope);
}

console.log(evalInScope("a + b", { a: 1, b: 2 })); // 3
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you so much, your answers are always based! Just tested and it works perfectly. To make it even shorter, isn't it possible to remove the first arg (which is `''` and the `new` before `Function`) or did you have a bad experience with that? Just like `Error` also doesn't require `new Error` – kungfooman Aug 28 '23 at 09:43
  • 1
    It's not required. I didn't bother to read the mdn page I had linked :-) I remembered something like this, but I always use the two-argument version and pass a comma-separate string for the parameter declarations. – Bergi Aug 28 '23 at 09:47
  • So, the answer is to basically break out of strict mode, and use `with` any way... I see how that works, but that's... Funky. – Cerbrus Aug 28 '23 at 09:53
  • @Cerbrus Yes, if you really need to use `with` (e.g. with a proxy to intercept all non-local variable accesses) then you don't get around it. But there are actually [better approaches](https://stackoverflow.com/a/24032179/1048572) that work in strict mode, see update – Bergi Aug 28 '23 at 11:56
  • Those are a lot better, indeed! – Cerbrus Aug 28 '23 at 12:01
  • The `Object.keys` approach is worse and easily fails, for example StackOverflow for some reason exposes `window["0"]`. Suddenly you are exposing a number as a destructure target, which causes a `SyntaxError`. The `with` version is way more stable and error proof. Mini error test: `console.log(evalInScope("a + b", { a: 1, b: 2, 0: "happens" }));` – kungfooman Aug 31 '23 at 08:28
  • I had expected that the object you are passing as `contextAsScope` contains only properties with names that are valid identifiers, as it is controlled by your repl. You can of course `.filter()` the keys if that's not guaranteed. – Bergi Aug 31 '23 at 11:52
0

The problem is that you're trying to do something that strict mode specifically blocks.

So, basically, what you want to do isn't possible.

You're either going to have to choose not to use use strict mode, or you're going to have to work around the use of eval (I'd personally recommend never using eval...).

Cerbrus
  • 70,800
  • 18
  • 132
  • 147