简体   繁体   中英

Is there a safe way to call `call` to call a function in JavaScript?

I want to call a function with a custom thisArg .

That seems trivial, I just have to call call :

func.call(thisArg, arg1, arg2, arg3);

But wait! func.call might not be Function.prototype.call .

So I thought about using

Function.prototype.call.call(func, thisArg, arg1, arg2, arg3);

But wait! Function.prototype.call.call might not be Function.prototype.call .

So, assuming Function.prototype.call is the native one, but considering arbitrary non-internal properties might have been added to it, does ECMAScript provide a safe way in to do the following?

func.[[Call]](thisArg, argumentsList)

That's the power (and risk) of duck typing: if typeof func.call === 'function' , then you ought to treat it as if it were a normal, callable function. It's up to the provider of func to make sure their call property matches the public signature. I actually use this in a few place, since JS doesn't provide a way to overload the () operator and provide a classic functor.

If you really need to avoid using func.call , I would go with func() and require func to take thisArg as the first argument. Since func() doesn't delegate to call (ie, f(g, h) doesn't desugar to f.call(t, g, h) ) and you can use variables on the left side of parens, it will give you predictable results.

You could also cache a reference to Function.prototype.call when your library is loaded, in case it gets replaced later, and use that to invoke functions later. This is a pattern used by lodash/underscore to grab native array methods, but doesn't provide any actual guarantee you'll be getting the original native call method. It can get pretty close and isn't horribly ugly:

const call = Function.prototype.call;

export default function invokeFunctor(fn, thisArg, ...args) {
  return call.call(fn, thisArg, ...args);
}

// Later...
function func(a, b) {
  console.log(this, a, b);
}

invokeFunctor(func, {}, 1, 2);

This is a fundamental problem in any language with polymorphism. At some point, you have to trust the object or library to behave according to its contract. As with any other case, trust but verify:

if (typeof duck.call === 'function') {
  func.call(thisArg, ...args);
}

With type checking, you can do some error handling as well:

try {
  func.call(thisArg, ...args);
} catch (e) {
  if (e instanceof TypeError) { 
    // probably not actually a function
  } else {
    throw e;
  }
}

If you can sacrifice thisArg (or force it to be an actual argument), then you can type-check and invoke with parens:

if (func instanceof Function) {
  func(...args);
}

At some point you have to trust what's available on the window. It either means caching the functions you're planning on using, or attempting to sandbox your code.

The "simple" solution to calling call is to temporarily set a property:

var safeCall = (function (call, id) {
    return function (fn, ctx) {
        var ret,
            args,
            i;
        args = [];
        // The temptation is great to use Array.prototype.slice.call here
        // but we can't rely on call being available
        for (i = 2; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        // set the call function on the call function so that it can be...called
        call[id] = call;
        // call call
        ret = call[id](fn, ctx, args);
        // unset the call function from the call function
        delete call[id];
        return ret;
    };
}(Function.prototype.call, (''+Math.random()).slice(2)));

This can then be used as:

safeCall(fn, ctx, ...params);

Be aware that the parameters passed to safeCall will be lumped together into an array. You'd need apply to get that to behave correctly, and I'm just trying to simplify dependencies here.


improved version of safeCall adding a dependency to apply :

var safeCall = (function (call, apply, id) {
    return function (fn, ctx) {
        var ret,
            args,
            i;
        args = [];
        for (i = 2; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        apply[id] = call;
        ret = apply[id](fn, ctx, args);
        delete apply[id];
        return ret;
    };
}(Function.prototype.call, Function.prototype.apply, (''+Math.random()).slice(2)));

This can be used as:

safeCall(fn, ctx, ...params);

An alternative solution to safely calling call is to use functions from a different window context.

This can be done simply by creating a new iframe and grabbing functions from its window. You'll still need to assume some amount of dependency on DOM manipulation functions being available, but that happens as a setup step, so that any future changes won't affect the existing script:

var sandboxCall = (function () {
    var sandbox,
        call;
    // create a sandbox to play in
    sandbox = document.createElement('iframe');
    sandbox.src = 'about:blank';
    document.body.appendChild(sandbox);
    // grab the function you need from the sandbox
    call = sandbox.contentWindow.Function.prototype.call;
    // dump the sandbox
    document.body.removeChild(sandbox);
    return call;
}());

This can then be used as:

sandboxCall.call(fn, ctx, ...params);

Both safeCall and sandboxCall are safe from future changes to Function.prototype.call , but as you can see they rely on some existing global functions to work at runtime. If a malicious script executes before this code, your code will still be vulnerable.

If you trust Function.prototype.call , you can do something like this:

func.superSecureCallISwear = Function.prototype.call;
func.superSecureCallISwear(thisArg, arg0, arg1 /*, ... */);

If you trust Function..call but not Function..call.call , you can do this:

var evilCall = Function.prototype.call.call;
Function.prototype.call.call = Function.prototype.call;
Function.prototype.call.call(fun, thisArg, arg0, arg1 /*, ... */);
Function.prototype.call.call = evilCall;

And maybe even wrap that in a helper.

If your functions are pure and your objects serializable, you can create an iframe and via message passing ( window.postMessage ), pass it the function code and the arguments, let it do the call for you (since it's a new iframe without any 3rd party code you're pretty safe), and you're golden, something like (not tested at all, probably riddled with errors):

// inside iframe
window.addEventListener('message', (e) => {
    let { code: funcCode, thisArg, args } = e.data;
    let res = Function(code).apply(thisArg, args);

    e.source.postMessage(res, e.origin);
}, false);

Same thing can be done with Web Workers.

If that's the case though, you can take it a step further and send it over to your server. If you're running node you can run arbitrary scripts rather safely via the vm module . Under Java you have projects like Rhino and Nashorn; I'm sure .Net has its own implementations (maybe even run it as JScript!) and there're probably a bazillion broken javascript VMs implemented in php.

If you can do that , why not use a service like Runnable to on-the-fly create javascript sandboxes, maybe even set your own server-side environment for that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM