6

I'm reading a textbook of the JavaScript language. And while I'm studying the closure topic, I came up with the following question.

Considering this function:

function foo() {
    extractPropsToCurrentContext({'prop1' : 'hello', 'prop2' : 123});
}

I want the result of the above code, equals to:

function foo() {
    var prop1 = 'hello';
    var prop2 = 123;
}

So, my question is, how to implement the function extractPropsToCurrentContext(/* Object */) ?

Just to clarify, I want to extract those properties of the object into the execution context, not under the 'this' pointer. (So, those extracted props should be private inside that function).

Another thing to clarify, you can't assume the foo will be invoked with 'new'. (like new foo())

Update:

I mean, is there any possibility that we could use any hacky tricks to detour the restriction of browser, to get closer to the result we want? Like, years ago, we invented JSONP for cross-domain, long-pulling for message pushing, etc?

user1481096
  • 317
  • 1
  • 8
  • 5
    Do you have any reason to suspect that it's actually possible? There are times when I'd like the very same thing. But I don't think it can be done. – Scott Sauyet Oct 01 '13 at 22:50
  • I don't know yet. So, that's why I ask the question here. And maybe someone had any thought about this interesting question. @ScottSauyet – user1481096 Oct 01 '13 at 22:52
  • 6
    JavaScript doesn't expose [lexical environments](http://es5.github.io/#x10.2) to your code (beyond the [global environment](http://es5.github.io/#x10.2.3) in some cases, such as `window` in browsers and `global` in Node.js). So, it isn't possible to pass a reference to it to another function for modification or even to create local variables without constructing the declaration statement and `eval()`'ing it within the scope it's destined for. – Jonathan Lonowski Oct 01 '13 at 23:00
  • But is there any possibility that we could use any hacky tricks to detour the restriction, to get closer to the result? Like, jsonp for cross-domain, long-pulling for message pushing? @JonathanLonowski – user1481096 Oct 01 '13 at 23:17
  • Not sure if it answers your question, but you could declare in all your functions a local object var CurrentContext={}, then attach all your properties to this object. – Christophe Oct 01 '13 at 23:43

2 Answers2

4

I want to extract those properties of the object into the execution context

An execution context actually consists of three things:

  • the ThisBinding, which you said you didn't want to alter or extend.

  • the VariableEnvironment, which holds the declared variables and functions. This is what your equal code would change. You can alter it with the following hack:

    function getVariableDeclaration(obj) {
        return "var " + Object.keys(obj).map(function(name) {
            return name + " = " + JSON.stringify(obj[name]);
        }).join(",\n    ") + ";";
    }
    function foo() {
        eval(getVariableDeclaration({'prop1' : 'hello', 'prop2' : 123}));
        debugger;
    }
    foo();
    

    However, this works only in non-strict mode. Check §10.4.2 for details. Also, this hack currently is restricted to JSON-serialisable values, it would get a lot uglier if you needed to assign arbitrary values - eval must be used in the environment that you want to alter.

  • The LexicalEnvironment, which holds the current identifier bindings (and might change during execution in contrast to the VariableEnvironment). This is not exactly what you might want, but it can be modified very easily via the with Statement:

    function foo() {
        with ({'prop1' : 'hello', 'prop2' : 123}) {
            debugger;
        }
    }
    foo();
    

    Again, it does not work in strict mode. See §12.10 and §10.2.2.3 on how it works and shadows other bindings.

As you can see, strict mode prohibits any changes to a (non-global) execution context, as this makes bindings non-static and non-optimisable. Most code becomes harder to understand as well, so this is in general considered a bad practise.

It's fine to ponder about such hacks from an academic viewpoint (to understand the workings of the language), but you should never use them in production. Whatever you have in mind that would require such techniques, there are better solutions.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Not to scare people. But `eval is evil`, when the inputs originate from a non-trusted source. – thefourtheye Oct 02 '13 at 06:51
  • @thefourtheye: Well, `getVariableDeclaration` is definitely trusted, and the JSON cannot contain executable code :-) – Bergi Oct 02 '13 at 11:44
1

Just wanted to share my awkward solution. The idea is to construct new function from the rest of the parent function and call it in eval.

function cc(fnname, ctxname) {
    'use strict';
    var __evalfn = function () {
        'use strict';
        var __fn = __fnname,
            __ctx = __ctxname,
            __fnStr = __fn.toString(),
            __cccall = 'return eval(cc("'+__sfnname+'", "'+__sctxname+'"));',
            __cccallPos = __fnStr.indexOf(__cccall),
            __newFn = '(function () { var ',
            __decl = [], __restBody;
        if (__cccallPos === -1) { throw new Error("Can't find cc call"); }
        __restBody = __fnStr.slice(__cccallPos+__cccall.length);
        for (var __k in __ctx) {
            __decl.push(__k + '=' + __sctxname + '['+JSON.stringify(__k)+']');
        }
        __newFn += __decl.join(',') + ';';
        __newFn += __restBody;
        __newFn += ')()';
        return eval(__newFn);
    },
    __evalStr = __evalfn.toString()
                      .replace(/__fnname/g, fnname)
                      .replace(/__sfnname/g, JSON.stringify(fnname))
                      .replace(/__ctxname/g, ctxname)
                      .replace(/__sctxname/g, JSON.stringify(ctxname));
    return '('+__evalStr+')()';
}

var fn = function () {
    var a=10, ctx = {'b':10, 'c':'hello'};
    return eval(cc("fn", "ctx"));
    console.log(a,b,c);
    return a+b;
};

> fn();
> 10 10 "hello"
> <- 20

It will require you to put return eval(cc("fn", "ctx")); in your function. fn is a name of a variable holding function in which you use cc. ctx is a name of object holding new bindings. One advantage of cc is that it allows any values in ctx, from numbers and strings to objects created using user-defined constructors.

zaquest
  • 2,030
  • 17
  • 25
  • Interesting idea with the "undead" code after the `return` (not that I really like it, though) :-) – Bergi Oct 02 '13 at 02:16
  • Some room for improvements: You could just pass the name of `__d` as another parameter, just as you did with `fn` - no need for a fixed name. If you did use `indexOf` instead of `search` you wouldn't need to construct a regex in `cccall`. `k` is always a string, you don't need to worry. Yet you should escape it in the property accessor, e.g. by `JSON.stringify`. And isn't that `__ret` variable unnecessary? – Bergi Oct 02 '13 at 02:22
  • 1
    @Bergi thank you, I amended. Now it uses two nesting eval (-_-) – zaquest Oct 02 '13 at 06:52
  • Oh, delightful! There's only a little drawback that the variable declarations and `restBody` are `eval`uated in the scope of the (substituted) `__evalFn` (check the scope chain with a `debugger;` statement). Maybe you could make it `return` the `__newFn` (maybe even without the IEFE?), and then `return '(eval('+__evalStr+'))';`? – Bergi Oct 02 '13 at 12:02