I can not figure out why the following code does not work:
var os = new Proxy(require('os'), {});
console.log( os.cpus() ); // TypeError: Illegal invocation
whereas
var os = require('os');
console.log(Reflect.apply(os.cpus, os, []));
or
var os = new Proxy(require('os'), {});
console.log( os.platform() );
works as expected.
An "illegal invocation" error is thrown when calling a function whose this keyword doesn't refer to the object where it originally did. In other words, the original "context" of the function is lost. Chromium browsers call this error an "illegal invocation."
ES6 implements intercession form of meta programming using Proxies. Similar to ReflectAPI, the Proxy API is another way of implementing meta programming in ES6. The Proxy object is used to define custom behavior for fundamental operations. A proxy object performs some operations on behalf of the real object.
A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object's property. The original object the proxy will virtualize.
Having just skim read the source for the os
package in the Node repo, it appears that the cpus()
is exported from binding.getCPUs
which is a C hook in the Node runtime environment.
cpus()
therefore has the binding
object as a function context, which is then lost through the proxy, giving you the IllegalInvocation
error because there is no context to the function when you call it — although I'm hazy on the details.
platform()
on the other hand is exported as function () { return process.platform; }
, and hence it's just a function that returns an object, and doesn't need to be run under a specific context because Node function contexts will have the process
variable specified by default (unless it has been overridden).
The following behaviour shows that applying the os
as a context to the cpus
function will work — proxies on function objects evidently lose the function context when calling properties.
const os = require('os');
const proxy = new Proxy(os, {}); // proxy of object, functions called get proxy context rather than os context
const cpus = new Proxy(os.cpus, {}); // proxy of function, still has os context
console.log(os.cpus()); // works (duh)
console.log(cpus()); // works
console.log(proxy.cpus.apply(os, [])); // works
console.log(proxy.cpus()); // fails with IllegalInvocation
Note: If someone can clear up the details on the JS function context for an answer I'd love to read it too.
How about composition:
const os = require('os');
const proxy = new Proxy(os, {});
Object.getOwnPropertyNames(os).forEach(k => {
var v = os[k];
if(typeof v === "function") proxy[k] = v.bind(os);
});
//the `!!` because I don't want the actual print
//only a `true` or an `Error`
console.log(!!os.cpus());
console.log(!!proxy.cpus());
console.log(!!proxy.cpus.apply(proxy, []));
and all this as a utility function to "replace" new Proxy()
, where handler.bindTargetFunctions
can be
the code:
function proxy(target, handler){
const _proxy = new Proxy(target, handler);
if(handler.bindTargetFunctions){
let bindTargetFunctions = handler.bindTargetFunctions;
if(!Array.isArray(bindTargetFunctions)){
bindTargetFunctions = Object.getOwnPropertyNames(target)
.filter(key => typeof target[key] === "function");
}
bindTargetFunctions.forEach(key => {
_proxy[key] = target[key].bind(target);
});
}
return _proxy;
}
const os = proxy(require('os'), { bindTargetFunctions: true });
//or
//const os = proxy(require('os'), { bindTargetFunctions: ["cpus"] });
console.log(os.cpus());
Edit:
Currently I try to bind functions directly in my get handler (see github.com/FranckFreiburger/module-invalidate/blob/master/…), the drawback of my solution is that each access to a function returns a new binding.
I entioned caching in the comments. This is how this cache could look like:
function createProxy(mod){
var cache = Object.create(null);
return new Proxy(function(){}, {
get(target, property, receiver) {
var val = Reflect.get(mod._exports, property, receiver);
if(typeof val === "function"){
if(!(property in cache) || cache[property].original !== val){
cache[property] = {
original: val,
bound: bal.bind(mod._exports)
}
}
val = cache[property].bound;
}else if(property in cache){
delete cache[property];
}
return val;
}
});
}
And No, I don't consider this cache a regular object. Not because it inherits from null, but because logically, to me this is a dictionary/map. And I don't know any reason why you would ever extend or proxy a particular dictionary.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With