0

Is there a way to change the execution context of a Javascript function manually at execution time?

I am not talking about changing the this reference to point to a different object - I know this can be done with Function.prototype.call()/apply().

I am talking about changing variables accessible to the function, or to be more specific, granting access to variables that are outside of the scope the function is defined in.

Example:

var func;
(function () {
    func = function () {
        alert(test); // no variable called "test" defined, it will
                     // result in a ReferenceError when func() is called
    }
})();


function callFunction (callee) {
    var test ='foobar';
    callee(); // callee does not have access to "test"
}

callFunction(func);

In this example, I'd love the function func to be able to access the variable test defined in callFunciton when it is called. However due to how scope and execution contexts work in Javascript, it doesn't work that way.

I am looking for a way for callFunction to insert a variable called test in the execution context of the callee() call.


In this minimal example, it is of course possible to just add test as a parameter to func().

My use-case however is a framework with a plugin infrastructure, where plugins can specify callbacks and what parameters they take - along with parameter types. The framework then takes care of converting requested data into the requested types and provide them to the callback. This should happen completely transparent to the plugin.

Pseudocode:

Plugin

function callback {
    alert(user.name);
    alert(number);
};
var callbackParams = [
    {name : 'user', type : User},
    {name : 'number', type : Number}
];
defineCallback('event', callback, callbackParams);

Framework

var on = {
    event : [];
}

var User = function (name) {
    this.name = name;
}

function defineCallback (event, callback, params) {
    on[event].push({
        callback : callback;
        params   : params;
    });
}

function handleEvent(event) {
    on[event].forEach(function (handler) {
        for (param of handler.params) {
            // 1) gather data
            // 2) create Object of correct type as specified in 
            //    param.type
            // 3) insert variable called param.name into 
            //    execution context of handler.callback
        }
        handler.callback();
    });
}

So far, I found three solutions to this problem, and I like neither:

  • define parameters in callback, and only define their types in callbackParams, indexed by their order in the function declaration. Disadvantage: parameters have to be declared in two places.
  • use this.paramname instead of just paramname in callback, and use .call()/.apply() in `handleEvent(). Disadvantage: not transparent to plugins.
  • assign the variables to global scope before calling handler.callback() in handleEvent() and delete them again afterwards. Disadvantage: very ugly workaround.

Ideas?

Johannes H.
  • 5,875
  • 1
  • 20
  • 40
  • 1
    Short answer, **no**, functions don't have access to variables unless they are in scope, so either you pass the variables as arguments or you make them available to the scope of the function, or a higher scope, which should be doable if you don't wrap everything in IIFE's – adeneo Jun 04 '16 at 18:48
  • you have to re-eval() the function code to re-assess closures. – dandavis Jun 04 '16 at 18:50
  • 1
    My "idea" is that you reconsider your design. It's simply wrong. It's counter intuitive, it's unnatural to the language (and probably to any language), it's untestable (for the plugin developer), and worst of all, the simple solution (explicitly declared arguments) has everything you need without any real downside. – Amit Jun 04 '16 at 19:13
  • @adeneo The wrapping in the IIFE in the minimal example was intentional, as it reporduces the use-case where the function is defined in a plugin but called in the framework – Johannes H. Jun 04 '16 at 19:28
  • @dandavis That is actually more ugly then using global scope as a workaround... it IS certainly an option though, yes. – Johannes H. Jun 04 '16 at 19:28
  • @Amit It's most likely what I will do. – Johannes H. Jun 04 '16 at 19:29
  • Thank god **no**. [Dynamic scoping](https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynamic_scoping) is the horror. That's what parameters and argument passing is made for. – Bergi Jun 04 '16 at 19:29
  • 1
    For your use case, you should use `function callback(user, number) { … }` and just pass the values in the order in which they are given in the `callbackParams`. You might even drop the `name` and just specify the `type` if you don't need them otherwise. – Bergi Jun 04 '16 at 19:32
  • @Bergi that is basically what I thought about in the first of my 3 possible workarounds - and certainly the one I like the most. I'm just not comfortable with specifying parameters in two different places (function declaration and the `callbackParams` array), but it does seem like the cleanest solution. Thanks for confirmation! – Johannes H. Jun 04 '16 at 19:34
  • @Amit: Actually there are languages that had this "feature". They died out relatively quickly, were/are used only in small projects, or required ferocious style rules to stay maintainable. – Bergi Jun 04 '16 at 19:36
  • 1
    @JohannesH.: You could visually colocate them, e.g. `callback.types = [User, Number]; ¶ function callback(user, number) { ¶ …` – Bergi Jun 04 '16 at 19:39

1 Answers1

1

You can use a with statement. Be aware it's slow and forbidden in strict mode.

var func;
(function () {
  func = function func() {
    with(func.variables) {
      console.log(test);
    }
  }
})();
function callFunction (callee) {
  callee.variables = {test: "foobar"};
  callee();
  callee.variables = null;
}
callFunction(func);
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Thanks for the suggestion! I completely forgot about `with` as I haven't used it in years for good reason... I like the approach of assigning the variables to a property of the callback function, I didn't think about this one yet. I would have to specify the plugin infrastructure that callbacks have access to their parameters that way then - if a plugin developer uses `with` in them or not is outside of my responsibility though. – Johannes H. Jun 04 '16 at 19:32
  • 1
    Still, `callee.variables` is just a rather crude workaround of simply passing the variable object into the function as a parameter. Whether the plugin uses `with` or not isn't even your decision then. – Bergi Jun 04 '16 at 19:41