简体   繁体   English

是否有一种安全的方法来调用`call`来调用JavaScript中的函数?

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

I want to call a function with a custom thisArg . 我想用自定义thisArg调用一个函数。

That seems trivial, I just have to call call : 这似乎微不足道,我只需要call

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

But wait! 可是等等! func.call might not be Function.prototype.call . func.call可能不是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 . Function.prototype.call.call可能不是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? 因此,假设Function.prototype.call是本机的,但考虑到任意非内部属性可能已添加到它,ECMAScript是否提供了一个安全的方法来执行以下操作?

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. 这就是鸭子打字的力量(和风险): if typeof func.call === 'function' ,那么你应该把它看作是一个正常的,可调用的函数。 It's up to the provider of func to make sure their call property matches the public signature. func提供者来确保他们的call属性与公共签名匹配。 I actually use this in a few place, since JS doesn't provide a way to overload the () operator and provide a classic functor. 我实际上在一些地方使用它,因为JS没有提供重载()运算符并提供经典仿函数的方法。

If you really need to avoid using func.call , I would go with func() and require func to take thisArg as the first argument. 如果你真的需要避免使用func.call ,我会使用func()并要求functhisArg作为第一个参数。 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. 因为func()不委托给call (即f(g, h)不会去f.call(t, g, h) )并且你可以在parens的左侧使用变量,它会给出你可预测的结果。

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. 您还可以在加载库时缓存对Function.prototype.call的引用,以防以后更换它,并在以后使用它来调用函数。 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. 这是lodash / underscore用于获取本机数组方法的模式,但不提供任何实际保证,您将获得原始本机调用方法。 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: 如果你可以牺牲thisArg (或强制它成为一个实际的参数),那么你可以使用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: 呼叫call的“简单”解决方案是临时设置属性:

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. 请注意,传递给safeCall的参数将集中在一个数组中。 You'd need apply to get that to behave correctly, and I'm just trying to simplify dependencies here. 您需要apply才能使其正常运行,我只是想在这里简化依赖关系。


improved version of safeCall adding a dependency to apply : 改进版本的safeCall添加依赖项以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. 安全地调用call的另一种解决方案是使用来自不同窗口上下文的函数。

This can be done simply by creating a new iframe and grabbing functions from its window. 这可以通过创建一个新的iframe并从其窗口中获取函数来完成。 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: 您仍然需要假设对可用的DOM操作函数有一定程度的依赖性,但这是作为设置步骤发生的,因此任何未来的更改都不会影响现有脚本:

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. safeCallsandboxCallsandboxCall受到对Function.prototype.call 未来更改的影响,但正如您所看到的,它们依赖于某些现有的全局函数来在运行时工作。 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: 如果您信任Function.prototype.call ,则可以执行以下操作:

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

If you trust Function..call but not Function..call.call , you can do this: 如果您信任Function..call但不Function..call.call ,你可以这样做:

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): 如果你的函数是纯的并且你的对象是可序列化的,你可以创建一个iframe并通过消息传递( window.postMessage ),传递它的函数代码和参数,让它为你做call (因为它是一个没有任何第三个的新iframe)派对代码,你很安全),你是金色的,像(没有经过测试,可能充满错误):

// 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. 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 . 如果您正在运行节点,则可以通过vm模块安全地运行任意脚本。 Under Java you have projects like Rhino and Nashorn; 在Java下你有像Rhino和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. 我确定.Net有自己的实现(甚至可以像JScript一样运行它!)并且可能在php中实现了大量破解的javascript VM。

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. 如果你能做到这一点 ,为什么不使用像Runnable这样的服务来动态创建javascript沙箱,甚至可以设置你自己的服务器端环境。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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