Call-this operator S1
中文标题:调用此操作符
- 阶段: Stage 1
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案引入了一种新的 ~> 操作符,以简化函数调用的 this 绑定更改,将诸如 fn.call(rec, arg0) 这样的冗长模式替换为 rec~>fn(arg0)。这是旧绑定操作符提案的重生,并直接与扩展提案竞争。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
JavaScript 的 Call-this 操作符
ECMAScript Stage-1 提案。J. S. Choi,2021 年。
- 正式规范
- Babel 插件:尚未实现
本提案是旧 Stage-0 绑定操作符提案 的 复兴。它也是 Stage-1 扩展提案 的一个替代、 竞争性 提案。更多信息,请参阅 § 相关提案。
语法
语法正在 issue #10 中讨论。
暂定语法
receiver-
一个成员表达式、调用表达式、可选表达式、带参数的
new表达式、另一个 call-this 表达式或括号表达式。该表达式解析到的值将作为右侧函数对象的this接收者绑定到该函数对象。 fn-
一个必须解析为函数对象的变量。
ns-
右侧可以是命名空间对象的变量,后跟一系列属性标识符,而不是单个变量。该链必须解析为函数对象。
expr-
括号内的任意表达式,必须解析为函数对象。
arg0, arg1等-
一系列参数表达式,可以包含展开
...语法。
描述
语法正在 issue #10 中讨论。
暂定描述
(有 正式规范 可用。)
call-this 操作符 ~> 是一个 左结合 的二元操作符。它以与 Function.prototype.call 相同的方式,将其右侧(一个 函数)的 this 值绑定到其左侧(一个 接收者 值),以及任何给定的参数。
例如,receiver~>fn(arg0, arg1) 将等同于 fn.call(receiver, arg0, arg1)(只不过即使其他代码重新赋值全局方法 Function.prototype.call,其行为也不会改变)。
同样,receiver~>(createFn())(arg0, arg1) 将大致等同于 createFn().call(receiver, arg0, arg1)。
如果操作符的右侧在运行时没有计算为函数,则程序会抛出 TypeError 异常。
操作符的 左侧 与成员表达式、调用表达式、带参数的 new 表达式和可选表达式具有相同的 优先级。与这些操作符一样,call-this 操作符也可以被左侧的可选表达式短路。
操作符的 右侧,与装饰器一样,可以是以下之一:
- 单个 标识符 或 私有字段(如
fn或#field)。 - 标识符和/或私有字段的 链(如
ns.fn或this.ns.#field)。 - 括号内的 表达式(如
(createFn()))。
例如,receiver~>ns.ns.ns.fn 组合为 receiver~>(ns.ns.ns.fn)。
与 . 和 ?. 操作符类似,call-this 操作符可以有或没有空白填充。
例如,receiver~>fn
等同于 receiver~>fn,
并且 receiver~>(createFn())
等同于 receiver~>(createFn())。
call-this 操作符可以可选地与 ?. 链式使用(即 ?.~>):
receiver~>fn 总是会产生一个绑定的函数,
无论 receiver 是否为 nullish。
receiver ?.~>fn 如果 receiver 为 null 或 undefined,其结果将为 null 或 undefined。
receiver ?.~>fn(arg) 如果 receiver 是 nullish,也会照常短路,在 arg 求值之前。
new 表达式 不能 包含不带括号的 call-this 表达式。new x~>fn() 是 SyntaxError。
否则,new x~>fn() 会在视觉上产生歧义,既可能是
(new x)~>fn() 也可能是 new (x~>fn())。
为什么需要 call-this 操作符
简而言之:
.call在 JavaScript 代码库中非常有用且非常常见。- 但
.call笨拙且不符合人体工程学。
.call 非常常见
动态的 this 绑定是当今 JavaScript 设计和实践的基本部分。因此,开发人员经常需要更改 this 绑定。.call 可以说是整个 JavaScript 中最常用的函数之一。
我们可以使用 Node Gzemnid 来估计 .call 的普遍性。虽然 Gzemnid 可能具有欺骗性,但我们只是寻求粗略的估计。
以下结果来自下载量排名前 1000 的 NPM 包的已检入 Git 的源代码。
这些结果表明,.call 的使用频率与其他常用标准函数相当。在此数据集中,其使用频率甚至超过了 console.log。
显然,此方法存在许多陷阱,但我们只是相对于其他基线函数寻找粗略的数量级估计。Gzemnid 只计算每个库的代码库一次;它不会重复计算依赖项。
这些结果是使用什么方法获得的?
首先,我们下载 2019-06-04 的 预构建 Gzemnid 数据集,用于下载量排名前 1000 的 NPM 包。我们还需要位于同一活动目录中的 Gzemnid 的 search.topcode.sh 脚本,它需要 lz4 命令套件。search.topcode.sh 将输出前 1000 个包中符合给定正则表达式的代码行。
我们使用 awk 计算这些匹配的代码行数,并将它们与 call 和其他几个常用函数的数量进行比较。
请注意,对于 .call,我们使用 grep 排除 .call 在注释中或由转译代码产生的一些不相关的出现。我们倾向于过度排除。
这些排除的模式由一位独立调查员(Scott Jamison)在 手动审查数据集中前 10,000 个 .call 出现 用于确定每个出现的原因后确定。
排除的 [^a-zA-Z][A-Z][a-zA-Z0-9_$]*\.call\( *this 模式值得特别注意。此模式匹配任何大写标识符后跟 .call(this。我们排除任何此类出现,因为任何大写标识符可能指的是构造函数,并且对构造函数使用 .call 已经在很大程度上被 class 和 super 语法所取代。此模式可能会错误地从我们的计数中排除许多合法使用 .call 的情况,但这种对 .call 的偏见对于粗略比较的目的是可以接受的。
开发人员使用 .call 的原因有很多。这些包括:
包装接收者的方法,然后调用它:
在两种方法之间条件切换:
在猴子补丁的对象上重用原始方法:
保护方法调用免受 原型污染:
…以及其他原因。开发人员使用 .call 完成所有这些操作,这些用途的总和使 .call 成为整个语言中最常用的操作之一。
.call 很笨拙
尽管它很常见,但 .call 既笨拙又难以阅读。它用样板代码将函数与其接收者和参数分开,并且颠倒了“自然”的词序,导致动词 .call-主语-宾语词序:
JavaScript 开发人员习惯于使用类似于英语和其他 SVO 人类语言 的 主语-动词-宾语词序 的方法。这种模式在 JavaScript 中作为点方法调用无处不在:
考虑以下使用 .call 的真实代码,并将它们与使用 call-this 操作符的版本进行比较。当你大声朗读时,差异尤其明显。
简而言之:
非常常见 × 非常笨拙 = 值得用语法改进。
关于生态系统分裂的担忧
多种方式或语法做事是否有害,关键取决于重复对 API 的影响及其病毒式传播的程度。
假设我们正在考虑使用两种语法 𝘟 和 𝘠 来使用 API。如果模块或个人 𝘈 使用语法 𝘟,它比语法 𝘠 与语法 𝘟 的互操作性更好,并且这迫使模块或个人 𝘉 在其新 API 中使用语法 𝘟,以与个人 𝘈 的 API 互操作,这种病毒式传播会鼓励生态系统分裂和 API 战争。在语言中引入多种这样的方式是糟糕的。
“另一方面,如果个人 𝘈 的语法选择 [即 𝘟] 对个人 𝘉 [的语法选择,𝘠] 没有影响,并且他们可以轻松地互操作,那么这通常是良性的。”
- 𝘟:某些 API(如“函数式”API)使用不基于
this的 ƒ。 - 𝘠:某些 API(如“面向对象”API)使用基于
this的 ƒ。
𝘟 API 和 𝘠 API 之间的这种分裂已经内置于语言中。这种分裂非常明显,以至于像 Firebase JS SDK 已经切换 这样的知名 API 从 𝘠 切换到了 𝘟(例如,为了模块拆分)。
但是 call-this 操作符,连同 管道操作符 |>,将使 𝘟 和 𝘠 之间的互操作性更加流畅 – 并且会使 𝘟 和 𝘠 之间的选择变得更不具有病毒式传播性 – 弥合分裂:
非目标
本提案的目标是 简单。因此,本提案有意 不 处理以下用例:
函数绑定和方法提取不是本提案的目标。
更改函数的 this 接收者比函数绑定更常见,如前面的统计所示。一些 TC39 代表表示担心函数绑定可能与诸如 PFA(部分函数应用)语法 之类的提案重复。因此,我们将这两个功能推迟到未来的提案中。
提取属性访问器(即 getter 和 setter)也不是本提案的目标。Get/set 访问器 不像 方法。方法是 属性(恰好是函数)。访问器本身 不是 属性;它们是在获取或设置属性时激活的函数。
可以使用 Object.getOwnPropertyDescriptor 提取 getter/setter;它们没有特殊处理。这种冗长可能被认为是可取的 语法盐:它使开发人员的意图(提取 getter/setter - 而不是方法)更加明确。
函数/表达式应用,即将深度嵌套的函数调用和其他表达式展开为线性管道,非常重要,但本提案未涉及。相反,它由 管道操作符 处理,本提案的语法可以很好地与其配合使用。
相关提案
旧绑定操作符
本提案是旧 Stage-0 绑定操作符提案 的 复兴。(旧提案的拥护者 建议用新提案重新开始,而不是使用旧提案。)
新提案与旧提案基本相同。唯一大的区别是,在方法提取期间,没有用于隐式绑定接收者的一元形式。(另请参阅 非目标。)
扩展
扩展系统是 Stage-1 扩展提案 的一个替代、 竞争性 提案。
还提供了 深度比较。具体差异简要如下:
- Call-this 没有特殊的变量命名空间。
- Call-this 没有属性访问器的隐式语法处理。
- Call-this 没有多态的
const ::{ … } from …;语法。 - Call-this 没有多态的
…::…:…语法。 - Call-this 没有
Symbol.extension元编程系统。
管道操作符
管道操作符 是一个 互补 的提案,可用于将深度嵌套的表达式(如 f(0, g([h()], 1), 2))线性化为 h() |> g(^, 1) |> f(0, ^, 2)。
这与 call-this 操作符的目的根本不同,后者更接近属性访问 .。
确实,属性访问 .、call-this 和管道操作符都可以用于线性化代码。但这对于前两个操作符来说只是一个令人愉快的副作用:
- 属性访问与对象成员关系紧密耦合。
- Call-this 只是改变函数调用的
this绑定。
相比之下,管道操作符旨在普遍线性化所有其他类型的表达式。
|> 不能改善 .call 的笨拙性。这是笨拙(且频繁)的现状:
引入 管道操作符 |> 解决了 词序 问题,但结果 更不 可读。过多的样板代码将函数与其接收者和参数分开:
只有单独的操作符才能在保持可读性的同时改善词序:
管道冠军组一直在研究是否可以修改管道操作符来解决 .call 的笨拙性,同时仍然解决管道的其他用例(例如,不基于 this 的 n-ary 函数调用;异步函数调用)。它仍然没有找到除了单独操作符之外的任何解决方案。
就像管道操作符与属性访问共存一样:
…它也可以与 call-this 一起工作:
PFA 语法
PFA(部分函数应用)语法 ~() 将简洁地创建部分应用的函数。
PFA 语法 ~() 和 call-this ~> 也是互补的,处理不同的用例。
例如,obj.method~() 将处理带隐式绑定的方法提取,这是 call-this 未解决的。换句话说,当接收者对象本身包含我们想要绑定的函数时,我们需要使用 call-this 重复一次接收者。PFA 语法将允许我们避免重复接收者。
相比之下,call-this 改变函数调用的接收者。receiver~>fn()。(这个未绑定的函数可能已经从另一个对象中提取出来。)PFA 语法不能解决这个用例。