# call、apply 与 bind

Source: https://codewiki.com/zh/javascript/call-apply-bind/

> - **what**: `call()` 和 `apply()` 立即调用函数，并显式提供 `this`；`bind()` 保存 `this` 和可选的前置实参，返回稍后调用的新函数。
> - **trap**: `apply()` 接受类数组对象，展开语法要求可迭代对象；每次 `bind()` 还会创建不同的函数标识，不能在注册与移除时各绑定一次。
> - **fix**: 根据实参来源选择 `call()`、`apply()` 或 `Reflect.apply()`；需要长期回调时只绑定一次并保存结果，同时单独测试普通调用与构造调用。

## 是什么，为什么存在

`call()`、`apply()` 和 `bind()` 都是 `Function.prototype` 上的方法，用来控制一个可调用值怎样接收 `this` 和实参。前两个方法立即执行目标函数，最后一个方法返回新函数。这是三者最先要分清的边界。

普通方法调用从点号或方括号左侧取得接收者，例如 `account.close()` 中的 `this` 是 `account`。函数被赋值、解构或作为回调传递后，只剩函数值，原对象不会自动跟随。显式 `this` 绑定（explicit `this` binding）让调用方在不修改对象的情况下指定接收者。

`call()` 适合实参数量和位置已经写在调用点的情况。`apply()` 适合实参已经装在数组或类数组对象中的情况。`bind()` 适合稍后调用、稳定回调标识，或用偏应用（partial application）预先填入前几个实参。

这三个方法不是 `this` 的全部规则。箭头函数没有自己的 `this`，类构造函数不能作为普通函数调用，而严格函数与非严格函数对 `null`、`undefined` 和原始值接收者的处理也不同。学习这些 API 的重点是识别它们能控制什么，以及目标函数仍保留哪些语义。

| 方法 | 是否立即执行 | 实参形式 | 结果 |
| --- | --- | --- | --- |
| `fn.call(receiver, a, b)` | 是 | 逐个实参 | 目标函数的返回值 |
| `fn.apply(receiver, args)` | 是 | 数组或类数组对象 | 目标函数的返回值 |
| `fn.bind(receiver, a, b)` | 否 | 可选前置实参 | 新的绑定函数 |

这三种操作都要求目标可调用。对数字或普通对象借用 `Function.prototype.call` 时，接收者规则还没机会生效，运行时就会因目标不可调用而失败。

它们都不会检查接收者是否拥有目标需要的字段。这项契约仍由目标函数负责，因此显式接收者可能在语法上合法，在业务语义上却错误。

## 工作原理

调用 `fn.call(receiver, ...args)` 时，`call()` 先确认 `fn` 可调用，再以 `receiver` 作为目标函数的 `this`，并按原顺序传入其余实参。`call()` 的返回值和异常都直接来自目标函数。它不会把目标函数永久附加到 `receiver` 上。

调用 `fn.apply(receiver, args)` 时，接收者规则与 `call()` 相同，但第二个参数先被转换为实参列表。这个值可以是数组，也可以是具有 `length` 和从 `0` 开始的索引属性的类数组对象（array-like object）。传入 `null` 或 `undefined` 表示没有实参；其他非对象值会抛出 `TypeError`。

调用 `fn.bind(receiver, ...leadingArgs)` 时，目标函数不会执行。运行时创建一个绑定函数，内部保存目标函数、`receiver` 和前置实参。以后调用绑定函数时，前置实参排在本次实参之前；再次对它调用 `call()` 或 `apply()` 不能替换已保存的 `this`。

每次 `bind()` 都创建新的函数对象，即使目标、接收者和实参完全相同。这个回调标识（callback identity）差异会影响事件移除、订阅取消、缓存键和去重。需要对应的注册与清理操作时，绑定结果本身就是必须保存的资源。

### 接收者与实参是两个维度

`thisArg` 不会成为第一个普通形参，普通实参也不会自动成为 `this`。`bind(null, currency)` 常用于只预置实参的函数；如果目标函数从不读取 `this`，`null` 只是一个明确表示「不使用接收者」的占位值。

严格普通函数会原样收到 `call()`、`apply()` 或 `bind()` 提供的 `thisArg`。非严格普通函数会把 `null` 和 `undefined` 替换为 `globalThis`，并包装原始值。业务代码不应依赖这种转换，因为模块和类方法使用严格语义。

箭头函数会忽略这些 API 提供的 `thisArg`，因为它从创建位置读取词法 `this`。这些方法仍能调用箭头函数并传递实参，`bind()` 也仍能预置实参。绑定不会把箭头函数变成可构造函数。

### `apply()` 与展开语法

`fn.apply(receiver, values)` 和 `fn.call(receiver, ...values)` 在 `values` 为数组时常得到相同结果，但它们的输入协议不同。`apply()` 读取 `length` 与索引属性；展开语法读取 `Symbol.iterator`。一个值可以只满足其中一种协议。

`Reflect.apply(fn, receiver, values)` 把目标函数作为显式参数，而不是从 `fn.apply` 属性取得方法。它适合代理、包装器和通用调用工具，也能避开目标自身名为 `apply` 的属性。它和 `Function.prototype.apply()` 一样要求第三个参数是类数组对象，但不能省略该参数。

## 示例

下面四个示例依次展示立即调用与延迟调用、两种实参协议、稳定回调标识和绑定构造函数。所有输出都由本地 Node 24 运行对应文件得到。

### 同一个函数的三种调用方式

目标函数同时读取接收者和普通实参。`call()` 与 `apply()` 立即返回字符串，`bind()` 则保存接收者以及前两个实参，等待最后一个实参。

<!-- quick -->

```javascript
// file: three_modes.js
'use strict';

function price(currency, amount, fee) {
  return `${this.region} ${currency}${amount + fee}`;
}

const store = { region: 'EU' };

console.log(price.call(store, 'EUR ', 20, 2));
console.log(price.apply(store, ['EUR ', 20, 2]));

const addFee = price.bind(store, 'EUR ', 20);
console.log(addFee(2));
console.log(addFee === price);
```

```text
EU EUR 22
EU EUR 22
EU EUR 22
false
```


<!-- /quick -->

三个调用得到相同业务字符串，但过程不同。前两个表达式已经完成调用；`addFee` 是可复用的新函数，还需要接收 `fee`。最后一行证实它和 `price` 不是同一个函数对象。

`bind()` 只预置前置实参，不能留下中间空位。若接口需要先固定 `amount`、以后再提供 `currency`，应改写形参顺序，使用命名选项对象，或写一个明确的包装函数。

### 区分类数组对象与可迭代对象

自定义 `arrayLike` 有索引和 `length`，但没有迭代器；`Set` 有迭代器，但没有数组式 `length`。这两个值把 `apply()` 与展开语法的协议差异直接暴露出来。

```javascript
// file: argument_protocols.js
'use strict';

function label(first, second) {
  return `${this.prefix}:${first}|${second}`;
}

const context = { prefix: 'queue' };
const arrayLike = { 0: 'fast', 1: 'bulk', length: 2 };

console.log(label.apply(context, arrayLike));

try {
  console.log(label.call(context, ...arrayLike));
} catch (error) {
  console.log(error.name);
}

const iterable = new Set(['audit', 'mail']);
console.log(label.apply(context, iterable));
console.log(label.call(context, ...iterable));
```

```text
queue:fast|bulk
TypeError
queue:undefined|undefined
queue:audit|mail
```

`arrayLike` 可供 `apply()` 按索引读取，却不能被展开，因此第二次调用在目标函数执行前抛出 `TypeError`。`Set` 的情况正好相反：`apply()` 看不到 `length`，于是提供零个实参；展开语法则从迭代器取得两个值。

把 `apply()` 机械替换为展开语法前，应先确定调用方承诺的是数组、类数组对象还是可迭代对象。若 API 由自己设计，使用真正的数组通常能减少这种协议歧义。

### 保存一次绑定后的回调

订阅表按函数对象标识添加和移除监听器。`Reporter` 在构造时绑定一次，因此注册与移除能使用同一个引用；临时再绑定一次则无法匹配已有监听器。

```javascript
// file: stable_callback.js
'use strict';

class TopicBus {
  #listeners = new Set();
  on(listener) {
    this.#listeners.add(listener);
  }
  off(listener) {
    this.#listeners.delete(listener);
  }
  emit(value) {
    for (const listener of this.#listeners) listener(value);
  }
  get size() {
    return this.#listeners.size;
  }
}

class Reporter {
  constructor(label) {
    this.label = label;
    this.onValue = this.onValue.bind(this);
  }
  onValue(value) {
    console.log(`${this.label}:${value}`);
  }
}

const bus = new TopicBus();
const reporter = new Reporter('R');

bus.on(reporter.onValue);
bus.emit('open');
bus.off(reporter.onValue);
console.log(bus.size);

bus.on(reporter.onValue);
bus.off(reporter.onValue.bind(reporter));
console.log(bus.size);
```

```text
R:open
0
1
```

第一次 `off()` 删除了准确的引用，所以监听器数量归零。第二次 `bind()` 产生一个包装在已绑定函数外的新函数，接收者仍然是 `reporter`，但标识已经不同，因此原监听器留在集合中。

浏览器的 `removeEventListener()`、Node 的 `EventEmitter.off()` 以及许多订阅库都有相同要求。还要保存注册选项或取消函数等 API 特有信息；只保留目标方法通常不够。

### 用绑定函数进行构造

目标函数可构造时，它的绑定函数也可构造。普通调用使用保存的对象，`new` 调用创建自己的接收者，但两种调用都会收到预置的 `'eu'` 实参。

```javascript
// file: bound_constructor.js
'use strict';

function Session(region, id) {
  this.key = `${region}-${id}`;
}

const fallback = { key: 'unset' };
const BoundSession = Session.bind(fallback, 'eu');

BoundSession('ops');
const session = new BoundSession('42');

console.log(fallback.key);
console.log(session.key);
console.log(session instanceof Session);
console.log(session instanceof BoundSession);
console.log(BoundSession.name);
console.log(BoundSession.length);
console.log(Object.hasOwn(BoundSession, 'prototype'));
```

```text
eu-ops
eu-42
true
true
bound Session
1
false
```

`new BoundSession('42')` 忽略 `fallback`，却保留前置实参。`instanceof` 会通过绑定目标检查 `Session.prototype`。绑定函数没有自己的 `prototype` 属性，所以它能被 `new` 调用，却不能直接作为 `extends` 的基类。

这里用普通函数同时演示普通调用和构造调用，不代表一个公开 API 应同时支持两者。生产代码通常应以 `class`、命名工厂或明确文档固定一种调用形式。

## 陷阱

### 用 `call()` 或 `apply()` 串联类构造

> **陷阱:** `Parent.call(this, value)` 只进行普通函数调用，不会保留完整的构造语义。JavaScript 类不能在没有 `new` 的情况下调用，`new.target` 也会与真正构造不同。

**修复方法：** 类继承使用 `extends` 和 `super()`。元编程确实需要转发构造时使用 `Reflect.construct()`，并分别验证原型、返回对象和 `new.target`。

### 把 `apply()` 输入一律当作可展开值

> **陷阱:** 类数组对象不一定可迭代，可迭代对象也不一定具有 `length`。把 `fn.apply(receiver, input)` 自动改成 `fn.call(receiver, ...input)` 可能抛错或改变实参数量。

**修复方法：** 写清输入协议，并在边界转换成真正数组。实参数量可能很大时，不要把整个集合一次展开为函数实参；改用循环、`reduce()` 或专门接收集合的 API。

### 在注册与清理位置分别绑定

> **陷阱:** `on(this.handle.bind(this))` 与稍后的 `off(this.handle.bind(this))` 得到两个不同函数。清理操作无法匹配注册项，还可能让绑定函数及其接收者长期保持可达。

**修复方法：** 在所有者初始化时绑定一次，把结果保存到语义明确的字段，并在完整的「注册、触发、清理、再次触发」测试中复用这个引用。

### 认为再次绑定会替换 `this`

> **陷阱:** 已绑定函数再次 `bind(other, extra)` 时，`other` 被忽略，但 `extra` 会追加到已有前置实参之后。箭头函数也会忽略新的 `thisArg`，但仍接收预置实参。

**修复方法：** 沿绑定链分别记录目标、首次绑定的接收者、每一批前置实参和调用时实参。需要另一个接收者时，从未绑定的原始函数创建新函数。

### 依赖非严格模式的全局替换

> **陷阱:** 非严格函数会把 `null` 或 `undefined` 接收者替换为 `globalThis`，这可能把缺失上下文变成静默的全局读写。模块和类方法使用严格语义，同一缺陷往往直接变成 `TypeError`。

**修复方法：** 用严格语义测试，并显式传递业务对象。确实需要全局对象时直接写 `globalThis`，不要让 `call(null)` 或 `bind(null)` 暗中决定它。

### 把简化实现当作原生替代品

> **陷阱:** 常见手写版本会把函数临时挂到接收者上。它会错误转换严格函数的接收者，在冻结对象或代理上失败，目标抛错时还可能漏掉清理；简化 `bind()` 也常漏掉构造与元数据语义。

**修复方法：** 面试练习必须明确列出没有实现的语义，生产代码使用原生方法或 `Reflect.apply()`。不要修改 `Function.prototype` 来发布自定义版本，也不要把通过几个样例等同于规范兼容。

<!-- deep -->

## 绑定函数不只是包装器

规范把绑定函数定义为特殊函数对象。它保存 `[[BoundTargetFunction]]`、`[[BoundThis]]` 和 `[[BoundArguments]]` 三项内部状态。普通调用先拼接已保存实参与本次实参，再以已保存接收者调用目标；这是可观察语义，不要求引擎真的创建一个源码可见的闭包。

再次绑定会以现有绑定函数为新目标。因此最外层的 `thisArg` 到达已有绑定层时被忽略，而每一层的实参仍按从内到外的顺序拼接。这个行为解释了为什么「不能重新绑定 `this`」与「可以继续偏应用实参」能够同时成立。

### `name`、`length` 与自有属性

原生 `bind()` 会根据目标设置新函数的 `name` 与 `length`。名称以 `"bound "` 为前缀；长度通常是目标声明形参数量减去已绑定实参数量，并且最低为 `0`。这些值适合诊断，但默认参数、剩余参数和多层绑定会让它们不等于业务上的必需实参数量。

绑定函数沿用目标函数对象的内部原型链，却不会复制目标的自有静态属性。给构造函数设置 `Session.kind = 'stateful'` 后，`Session.bind(...)` 的结果不会自动拥有 `kind`。公共 API 若依赖函数对象属性，必须明确转发或避免隐藏在绑定结果后面。

绑定函数也没有普通可构造函数那样的自有 `prototype` 属性。若目标可构造，绑定函数仍有内部构造能力，`new` 会转发到目标；但 `class Child extends BoundBase {}` 会因为基类的 `prototype` 无效而抛出 `TypeError`。

### 构造与 `instanceof`

构造绑定函数时，新实例替代已保存的 `this`，已保存实参仍然位于最前。直接通过绑定函数构造时，目标观察到的 `new.target` 是原目标，绑定层对该值透明。若目标显式返回对象，该对象仍可替代自动创建的实例。

`instanceof` 以绑定函数为右操作数时，会转向其绑定目标并读取目标的 `prototype`。这就是示例中的 `session instanceof BoundSession` 和 `session instanceof Session` 都为 `true` 的原因。自定义 `Symbol.hasInstance` 或代理仍可能影响最终行为，因此不要把这个结果推广到任意包装器。

## 通用调用与 API 设计

`call()` 本身也是依赖接收者的方法：`Function.prototype.call` 的 `this` 是它要调用的函数。`Function.prototype.call.bind(Array.prototype.slice)` 利用两层绑定创建「未绑定方法」工具，让调用方写 `slice(arrayLike)`。这种技巧有时适合封装兼容接口，但名字必须说明接收者已经变成普通实参。

现代内置 API 经常提供更直接的替代形式，例如自有属性检查可使用 `Object.hasOwn(object, key)`。直接 API 能避免读者同时追踪两层 `this`。没有合适替代形式时，应把通用调用封装一次，并用普通调用和异常输入测试其契约。

### `Reflect.apply()` 的边界

`Reflect.apply(target, thisArgument, argumentsList)` 适合目标函数本身可能定义 `apply` 属性的情况，也适合把调用作为数据处理的元编程代码。它首先显式检查 `target` 是否可调用，并要求 `argumentsList` 是对象；传入 `null`、`undefined` 或原始值都会抛出 `TypeError`。

与 `target.apply(receiver, args)` 相比，`Reflect.apply()` 不会读取可被覆盖的 `target.apply`。与 `Function.prototype.apply.call(target, receiver, args)` 相比，它更直接地表达目标、接收者和实参列表。但它不会验证领域参数，也不会让一次超大实参列表变得安全。

### 求值顺序与异常

在目标函数开始执行前，JavaScript 已经求值目标表达式、接收者表达式和实参表达式。`apply()` 与 `Reflect.apply()` 还要从类数组对象读取 `length` 和各个索引属性。getter、代理陷阱或迭代器因此可能在目标函数之前产生副作用或抛出异常。

稀疏类数组对象中的缺失索引会成为 `undefined` 实参，而不会被跳过。展开可迭代对象则由迭代器决定产生多少个值；它没有「索引缺失」的概念。审查调用包装器时，需要把实参收集阶段与目标执行阶段分开。

| 失败位置 | 目标是否开始执行 | 典型原因 |
| --- | --- | --- |
| 取得或调用被覆盖的 `target.apply` | 否 | 属性不是函数或 getter 抛错 |
| 收集调用实参 | 否 | 非对象类数组输入、getter 抛错或迭代器失败 |
| 目标函数体 | 是 | 参数校验、业务错误或显式抛出 |

`call()`、`apply()`、`bind()` 和 `Reflect.apply()` 都不会吞掉目标抛出的异常。包装器若捕获异常，应根据自己的边界决定是否补充上下文并用 `cause` 保留原错误；不要把失败转换成看似成功的 `undefined`。

### 偏应用与接口形状

`bind()` 只能从左侧预置连续实参。它适合把 `format(locale, options, value)` 变成只接收 `value` 的函数，前提是形参顺序本来就支持这种使用方式。需要占位符、按名称填值或改变实参顺序时，明确的包装函数通常更容易维护。

偏应用和柯里化不是同义词。`bind()` 一次预置零个或多个实参，返回的函数在调用时可以接收其余全部实参；柯里化通常把多参数函数系统地转换为一连串调用。需要后者时，应把约定留给专门工具或相关主题。

### 标识、可达性与清理

绑定函数会保持目标函数、绑定接收者和所有前置实参可达。长期存在的事件源如果保存了绑定回调，也就可能延长整个服务实例、请求上下文或大型配置对象的生命周期。问题不在 `bind()` 本身，而在注册关系没有明确所有者和终止点。

清理应由建立注册关系的组件负责，并保留 API 要求的完整匹配信息。除了函数标识，某些 API 还要求事件名称、捕获选项或订阅令牌一致。测试应在清理后再次触发来源，确认没有调用，而不是依赖垃圾回收时机。

### 测试调用包装器

测试不能只断言返回值。还要让目标记录实际 `this`、实参数量和顺序，才能发现接收者与首个普通实参被混淆，或类数组对象被错误截断。

至少加入一次目标抛错和一次实参收集抛错。两者都应保留原异常，同时只有前者能观察到目标函数已经开始执行。

绑定回调需要生命周期测试。连续注册两次、触发一次、清理一次再触发，能够同时暴露重复工作和函数标识不匹配。

若绑定目标可以构造，再增加一组 `new` 调用。普通调用通过并不能证明绑定接收者、原型关系和显式对象返回在构造路径上正确。

<!-- /deep -->

[检查点: javascript/call-apply-bind](https://codewiki.com/zh/javascript/call-apply-bind/#checkpoint)

## 延伸阅读

- [MDN：`Function.prototype.call()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call)
- [MDN：`Function.prototype.apply()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)
- [MDN：`Function.prototype.bind()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
- [MDN：`Reflect.apply()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply)
- [ECMAScript 规范：Bound Function Exotic Objects](https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-bound-function-exotic-objects)
