How do you add variables to the scope of a pre-defined function?
Title kinda says it all. In JavaScript, how would you inject a variable to the inner-scope of a function that you do not control (a callback for example)? This is done commonly in various JavaScript frameworks, where Jest provides you with describe
, it
, beforeAll
, etc., all within the body of callbacks that Jest has no explicit knowledge of. Cucumber does this more cleanly, using the shared World
instance, and also provides controls on providing a custom constructor for that object, but if I wanted to add a logger
object to the executing scope of anything using my library, is the only answer to add it to global
, or is there a way to keep the values scoped to the called function and its callbacks?
Edit: Here's what I see as a clunky solution, but it technically achieves the goal. If this is the accepted solution, I'll move forward with it, but I thought there had to be a more sophisticated/clean way to do this.
function a(opts, callback) {
Object.assign(global, opts);
const assignedKeys = Object.keys(opts).filter(k => Object.prototype.hasOwnProperty.call(global, k));
for(const k in opts) {
console.log('%s: %s', k, opts[k]); // val: 5
callback(); // Can I see it? true
}
console.log('Before clean-up', assignedKeys.map(k => `${k}: ${global[k]}`).join(', '));
// Before clean-up val: 5
for(const k in opts) {
delete global[k];
}
console.log('After clean-up', assignedKeys.map(k => `${k}: ${global[k]}`).join(', '));
// After clean-up val: undefined
}
function b() {
console.log('Can I see it?', !!val);
// Can I see it? true
}
a({ val: 5 }, b);
Solution 1:
Probably not the most elegant, but you could toString()
a function and inject the variable, utilizing the Function
constructor.
Here's a dumbed-down version of your example:
function inject(cb, hw = "Hello, world!") {
const injection = `const hw = "${hw}";`;
const fn = new Function(`${injection} return (${cb.toString()})();`);
const result = fn();
if(result)
console.log(result);
// cb(); <-- error: hw is undefined
}
inject(() => {
console.log(hw);
});
inject(function() {
console.log(hw);
}, "Hello, StackOverflow!");
inject(function test() {
console.log(hw);
return 5;
});
Solution 2:
Yes, using global variables is the standard practice for this. There is no way to alter the scope of a function that you don't control, it's impossible to "inject" new local variables into it. The only thing you can do is to put the variables in a shared scope (and assume that the function doesn't shadow them), which typically is the global scope.
Frameworks like React or Jest get away with this practice because they are the global framework for which all the code is written, so developers know about this feature and won't get confused by "magic" variables - and it's only very few, well-known variables. Tooling still needs to be told about these globals though. It's a trade-off, but sometimes worth it where it leads to much terser code.
Notice that unlike your a
function, such global variables are not dynamic but statically declared, and they don't care about collisions and cleanup. If you use such a framework, you'll know up-front which globals there are, and won't write code that interferes with them.
If you really want to dynamically "inject" arbitrary values into a called function, there is a standard technique: use arguments! The function that you call needs to declare parameters to use them, but this is the proper clean solution and will really keep the variables scoped to where you want them.