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

9
我希望使用自定义的thisArg调用一个函数。
这似乎很简单,我只需要调用call:
func.call(thisArg, arg1, arg2, arg3);

等等!func.call可能不是Function.prototype.call

所以我考虑使用

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

但是等等!Function.prototype.call.call可能不是Function.prototype.call

因此,假设Function.prototype.call是原生的,但是考虑到任意非内部属性可能已经添加到其中,ECMAScript是否提供了一种安全的方法来执行以下操作?

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

4
我读了三遍...但是好问题! - James Thorpe
3个回答

4
这就是鸭子类型的威力(和风险):if typeof func.call === 'function',那么您应该将其视为普通可调用函数。提供func的人需要确保他们的call属性与公共签名匹配。我在几个地方实际使用了这种方法,因为JS没有提供一种重载()运算符并提供经典函数对象的方式。
如果您真的需要避免使用func.call,我会选择func()并要求将thisArg作为第一个参数。由于func()不委托给call(即f(g, h)不会被解析为f.call(t, g, h)),而且您可以在括号左侧使用变量,它将为您提供可预测的结果。
您还可以在加载库时缓存对Function.prototype.call的引用,以防以后替换它,并在以后使用它来调用函数。这是lodash/underscore用于获取本机数组方法的模式,但并不提供任何实际保证您将获得原始的本机调用方法。它可以非常接近,并且不会太丑陋:
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);

在任何具有多态性的语言中,这都是一个基本问题。 在某个时候,您必须相信对象或库会按照其合同行事。 与任何其他情况一样,请信任但要验证:

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

使用类型检查,您也可以进行一些错误处理:

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

如果你可以放弃thisArg(或者强制它成为实际参数),那么你可以使用括号进行类型检查和调用:

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

但是,看起来像一只可爱的鸭子的东西可能是伪装成致命蝎尾狮的生物!因此,试图用面包喂它不是安全的活动。 - Oriol
3
它肯定可能发生,这就是为什么 try { duck.feed() } catch (lostFingers) { console.log('It's a manticore!'); } 是这种语言的一部分。像任何具有多态性的语言一样,在某些时候,你必须相信对象能够正确地工作。 - ssube
是的,我想唯一安全的方法就是尽早运行一些代码并引用 Function.prototype.call。也许可以使用 sealfreeze 来保护它。 - Oriol
如果你正在使用ES6,我建议在模块加载时直接获取const prototypeCall = Function.prototype.call并使用它。虽然多态变量可能更灵活和符合惯用法。 - ssube

2

在某些时候,您必须信任窗口上可用的内容。这意味着缓存您计划使用的函数,或尝试沙盒化您的代码。

调用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)));

这可以被用作:

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

请注意,传递给safeCall的参数将被合并到一个数组中。您需要使用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)));

这可以用作:

这可以用作:

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

安全调用call的另一种解决方案是使用来自不同窗口上下文的函数。

这可以通过创建一个新的iframe并从其窗口中获取函数来简单地完成。您仍需要假设某些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;
}());

这可以用作以下内容:
sandboxCall.call(fn, ctx, ...params);

safeCallsandboxCall都可以安全地避免对Function.prototype.call未来更改,但是您可以看到它们依赖于一些现有的全局函数在运行时工作。如果恶意脚本在此代码之前执行,则您的代码仍然容易受到攻击。


1
如果您信任 Function.prototype.call,您可以像这样做:
func.superSecureCallISwear = Function.prototype.call;
func.superSecureCallISwear(thisArg, arg0, arg1 /*, ... */);

如果您信任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;

也许甚至可以将其包装在帮助程序中。

如果您的函数是纯的且对象可序列化,则可以创建一个 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);

同样的事情也可以使用Web Workers完成。

如果是这种情况,你可以更进一步地将其发送到服务器。如果你正在运行node,你可以通过vm模块相当安全地运行任意脚本。在Java下,有像Rhino和Nashorn这样的项目;我相信.Net有自己的实现(甚至可以作为JScript运行!),而在php中可能有数不清的破损javascript虚拟机。

如果你可以做到那个,为什么不使用类似Runnable的服务来即时创建javascript沙箱,甚至为此设置你自己的服务器端环境呢。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接