有没有安全的方法调用 `call` 来调用 JavaScript 中的函数?

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

我想用自定义 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)

这就是 duck typing 的威力(和风险):if typeof func.call === 'function',那么您应该将其视为一个普通的可调用函数。 func 的提供者需要确保他们的 call 属性 与 public 签名匹配。我实际上在几个地方使用了它,因为 JS 没有提供重载 () 运算符和提供经典仿函数的方法。

如果你真的需要避免使用 func.call,我会选择 func() 并要求 functhisArg 作为第一个参数。由于 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);
}

在某些时候,您必须相信 window 上可用的内容。它要么意味着缓存您计划使用的功能,要么试图将您的代码沙盒化。

调用call的"simple"解决方法是临时设置一个属性:

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 的另一种解决方案是使用来自不同 window 上下文的函数。

这可以简单地通过创建一个新的 iframe 并从其 window 中获取函数来完成。您仍然需要假设对可用的 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,但是如您所见,它们依赖于一些现有的全局函数在运行时工作。如果恶意脚本在 之前 执行此代码,您的代码仍然容易受到攻击。

如果你信任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对你来说(因为它是一个没有任何第 3 方代码的新 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 Worker 完成。

如果是这种情况,您可以更进一步,将其发送到您的服务器。如果您是 运行ning 节点,您可以通过 vm module 相当安全地 运行 任意脚本。在 Java 下,您有 Rhino 和 Nashorn 等项目;我确定 .Net 有自己的实现(甚至可能 运行 它作为 JScript!)并且可能有大量损坏的 javascript 虚拟机在 php.

中实现

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