JavaScript bind 固定接收者:方法传给回调后,this 为什么会丢

10-01 3阅读

保存了方法,不代表保存了调用方式

对象上的方法可以正常读取 this,但把方法单独交给任务队列以后,同一段代码却开始报错。原因往往不在对象消失,而在调用形式改变:原先是通过对象调用,现在只剩下一个普通函数引用。函数被取出来时,并不会自动记住它是从哪个对象属性中取得的。

Function.prototype.bind 会创建一个新函数,保存指定的接收者,以及需要提前固定的若干参数。以后普通调用这个新函数时,参数会接在预设参数之后,原函数则用保存的接收者执行。这适合把依赖实例状态的方法交给回调接口。

先复现脱离对象,再比较绑定后的结果

下面保存为 JavaScript 文件,用 Node.js 运行,本文在二十四版验证。代码显式启用严格模式,避免普通函数独立调用时的接收者行为受到宽松模式影响。示例只有内存对象和同步调用,输出顺序不依赖定时器。

对象方法接收一个前缀和一个增量。绑定时预设前缀,调用时只传增量;随后尝试用另一个对象再次绑定,观察状态究竟改在哪里。最后修改原对象的名称,确认 bind 保存的是对象引用,而不是对象当时所有字段的快照。

JavaScript bind 固定接收者:方法传给回调后,this 为什么会丢

AI概念配图,非真实界面

'use strict';
const assert = require('node:assert/strict');

const stock = {
  name: 'Depot',
  count: 1,
  add(prefix, amount) {
    this.count += amount;
    return `${prefix}:${this.name}:${this.count}`;
  }
};
const extracted = stock.add;
assert.throws(() => extracted('stock', 2), TypeError);
console.log('detached method: rejected');

const addStock = stock.add.bind(stock, 'stock');
console.log('bound:', addStock(2));
const other = {name: 'Other', count: 100};
const rebound = addStock.bind(other);
console.log('rebound:', rebound(3));
assert.equal(other.count, 100);
stock.name = 'Depot-B';
console.log('live receiver:', addStock(1));
assert.notEqual(stock.add.bind(stock), stock.add.bind(stock));

function makeArrow() {
  return () => this.name;
}
const arrow = makeArrow.call(stock);
console.log('arrow after bind:', arrow.bind(other)());
assert.equal(stock.count, 7);
console.log('all bind checks passed');

再绑定不能替换已经固定的接收者

第一次绑定调用把库存从一增加到三。再次绑定到另一个对象后,结果仍来自原仓库,数量变成六,另一个对象保持一百。外层绑定函数最终仍会调用内层绑定函数,内层已有的接收者决定真正使用哪个对象,因此不能靠反复 bind 给旧回调换主人。

如果确实要绑定另一个实例,应从原始未绑定方法重新创建回调,并检查该方法是否适合用于新对象。方法可能依赖私有字段或其他实例约束,形状相似的普通对象不一定能够代替原实例。固定接收者只解决调用规则,不证明业务对象之间可以互换。

预设参数会排在后续实参前面。示例固定了第一个参数,后来的数字进入第二个位置。如果误把增量先绑定进去,实际顺序就会与函数声明不符。设计回调时先列出原函数参数和框架将提供的参数,通常比运行出错后逐个交换更可靠。

绑定引用与冻结数据是不同操作

对象改名后,新调用输出新名称,数量也继续累计。这说明绑定不会冻结对象状态。对于延迟执行任务,如果需要使用提交任务那一刻的数据,应显式保存必要字段,而不是假设绑定已经生成快照;反过来,需要实时状态时也不要提前复制后误以为仍是原对象。

每次调用 bind 都会产生新函数,所以示例中的两个绑定结果并不相等。如果回调接口通过函数身份来移除订阅,就应保存最初注册的那个绑定函数,再使用同一个引用解除。重新执行一遍看似相同的 bind,得到的仍然是另一个对象。

箭头函数的 this 来自定义时的词法环境,不能被 bind 的接收者参数改写。示例先在仓库对象的上下文中创建箭头函数,再绑定到另一个对象,结果仍来自仓库。决定使用普通方法还是箭头函数时,应根据是否需要动态接收者选择。

本文演示的是普通调用。可构造函数经绑定后若用于 new,实例创建有另外的规则,不能把这里固定接收者的结论直接搬过去。排查回调故障时,先确认函数类型、提取位置和最终调用方式,再检查参数顺序,就能把大多数 this 问题缩小到清楚的边界。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。