如何确定一个函数是通过`.call`还是`.apply`被调用的?

3
我创建了这个函数,以便我可以链接多个方法:
export default function bindable(fn) {
    return function boundFn(...args) {
        return this === undefined ? fn(...args) : fn(this, ...args);
    }
}

它可以与bind运算符一起使用。例如,你可以像这样定义一个新函数:

 export const flatten = bindable(arrayOfArrays => Array.prototype.concat(...arrayOfArrays));

然后像这样使用:

[[1,2,3],[4,5,6]]::flatten()

或者像这样:

flatten([[1,2,3],[4,5,6]])

Babel REPL

这个方法在一开始运行得很好,但后来我发现,如果像第二种情况那样将它作为模块导入,它会破坏上下文!

 import * as Arr from './array-helper-methods';
 Arr.flatten([1,2,3]); // `this` is now `Arr`

所以我的问题是:是否有可靠的方法来检测函数是否使用绑定运算符调用?

为什么要使用Array.prototype.concat而不仅是使用[...arrayOfArrays]呢?我还怀疑这个问题无法回答,因为它取决于绑定操作符的实现细节,而我不记得规范中有任何能够启用那种程度的内省的东西。但我很愿意被证明是错的。 - Jared Smith
@JaredSmith 这并不是问题的相关内容,而且……它不起作用。> [...arrayOfArrays] [ [ 1, 2, 3 ], [ 4, 5, 6 ] ] > Array.prototype.concat(...arrayOfArrays) [ 1, 2, 3, 4, 5, 6 ] - mpen
1
@mpen 你能详细说明一下你为什么要这样做吗?为什么你需要让你的函数既可以作为方法又可以作为纯函数工作?如果你想让任何方法都可以使用bind语法调用,那么将一个纯函数转换成一个使用this的函数不是更简单吗?然后你就可以在导入任何纯函数时调用它。如果你无论如何都要使用bind语法,为什么还需要函数在没有this上下文的情况下仍然可以工作呢? - sbking
@sbking,这本质上就是bindable的作用,不是吗?将一个纯函数变成可使用“this”调用的函数——除非“this”未定义,那么它也能像“纯”的方式一样被调用。 如果我删除了“This”检查,它将无歧义地运行,但这样我就需要两个版本的每个函数,这只会让管理变得很麻烦。 - mpen
@mpen 为什么管理起来那么痛苦呢?看起来你这样做是为了可以使用绑定语法来专门调用某个函数。你仍然可以将原始版本的函数引用传递给 bindable,因此你已经有了两个函数版本。我猜我不明白为什么你想在同一个模块文件中以两种不同的方式使用该函数。那听起来很令人困惑。 - sbking
@sbking 因为这将进入一个库。两个函数bindable和数组函数。我不想强制消费者在开始链接之前用bindable包装他们想使用的每个函数。我的库会预先导出它们。 - mpen
3个回答

2
那是绑定运算符的一个巨大而尚未解决的问题。但不,它只适用于方法。如果您想支持这两种风格,您应该提供两个版本的函数。
动态执行很困难。不,您无法检测函数是使用()、作为方法调用、被绑定、使用call或apply等方式调用的,您真的也不应该这样做。
对于您的情况,我会选择:
function bindable(fn, isContext) {
    return function boundFn(...args) {
        return isContext(this) ? fn(this, ...args) : fn(...args);
    }
}
export default bindable(bindable, f => typeof f == "function");

然后你可以使用bindable(flatten, Array.isArray)或者flatten::bindable(Array.isArray)来使用flatten。对于更复杂的情况,你也可以考虑函数的元数,即fn.length + 1 == args.length


当我编写一个操作任意对象的函数时,这会变得非常棘手,不是吗?而且,如果消费者在错误的变量类型上调用它,错误检查就会失效,因为该函数将在没有上下文的情况下被调用。 - mpen
1
是的。但是拥有一个可以处理多个签名调用的函数的整个想法很棘手,这就是为什么我建议使用两个单独的函数。 - Bergi

1
这更像是一种解决方法而非真正的解决方案,但您可以检查this是否包含一个flatten属性:
export default function bindable(fn) {
    return function boundFn(...args) {
        return (this === undefined || 'flatten' in this) ? fn(...args) : fn(this, ...args);
    }
}

1
即使那是可靠的,它也不够通用。bindable 应该适用于每个模块,而不仅仅是我的数组助手模块。实际上...我想知道嗅探 __esModule 是否会起作用? - mpen
2
@mpen 不行,因为那是转译器的产物。不要使用它。 - Bergi

1
一种更加通用(但不太好的性能)的解决方案是检查this的任何方法是否为boundFn函数:
export default function bindable(fn) {
    return function boundFn(...args) {
        return (this == null || Object.values(this).some(x => x === boundFn))
            ? fn(...args)
            : fn(this, ...args);
    }
}

但是Arr.flatten不是一个绑定函数吗?而且我也看不出来"boundFn"这个名字从哪里来。 - Bergi
1
@Bergi Arr.flatten 是一个绑定函数,因为它是将实际函数传递给 bindable 的结果。而 boundFn 是由 bindable 返回的函数名称。 - Michał Perłakowski
1
啊,我现在明白了。它不是“绑定函数”,这些是由内置的.bind()方法创建的特殊函数,这就是让我困惑的原因。要检查它是否是由bindable返回的函数,我不会检查它的名称,而是检查其身份:.every(x => x != boundFn) - Bergi

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