JavaScript bind 遇到 new:接收者已经固定,为什么字段却写进了新实例

前天 3阅读

先把Ticket绑定到一个已有对象receiver,再通过new调用绑定结果,this上的字段却没有写进receiver,而是出现在新实例里。若把bind理解成“无论怎么调用都只用这个对象”,工厂函数或测试替身就容易在这里出现状态归属错误。

普通调用绑定函数时,保存的接收者会参与调用;可构造的绑定函数经new调用时,走的是构造路径,预先绑定的this不被采用。预设参数仍会放在后续实参前面,因此同一次bind保存的接收者与参数,在两种调用方式中并不是完全一起生效。

把下面程序保存为demo.mjs,运行node demo.mjs。已在Node.js v24.19.0实跑,只创建内存对象。Ticket没有显式返回另一个对象,便于直接观察常规实例创建;同一BoundTicket会先普通调用一次,再构造一次。

JavaScript bind 遇到 new:接收者已经固定,为什么字段却写进了新实例

AI模型生成的概念插图:模具产出新的票形块,旁边原有票形块仍留在自己的托盘,提示构造时的新实例与已有接收者分开;不是内存截图。

完整可运行程序

import assert from 'node:assert/strict';

function Ticket(prefix, number) {
  this.code = `${prefix}-${number}`;
  this.targetIsTicket = new.target === Ticket;
}
const receiver = {code: 'original'};
const BoundTicket = Ticket.bind(receiver, 'R');
BoundTicket(1);
assert.equal(receiver.code, 'R-1');
assert.equal(receiver.targetIsTicket, false);
console.log('ordinary call:', receiver.code, receiver.targetIsTicket);

const created = new BoundTicket(2);
assert.equal(created.code, 'R-2');
assert.equal(created.targetIsTicket, true);
assert.equal(receiver.code, 'R-1');
assert.notEqual(created, receiver);
assert.equal(Object.getPrototypeOf(created), Ticket.prototype);
assert.equal(created instanceof Ticket, true);
assert.equal(created instanceof BoundTicket, true);
console.log('constructed:', created.code, created.targetIsTicket);
console.log('saved receiver:', receiver.code);
console.log('instance checks:', created instanceof Ticket,
  created instanceof BoundTicket);

const boundArrow = (() => 1).bind(null);
assert.throws(() => new boundArrow(), TypeError);
console.log('bound arrow remains non-constructible');

本次实际输出

ordinary call: R-1 false
constructed: R-2 true
saved receiver: R-1
instance checks: true true
bound arrow remains non-constructible

普通调用先证明绑定确实成立

ordinary call输出R-1和false。预先绑定的R进入prefix,调用时提供的一进入number;this就是receiver,所以它的code变为R-1。此时没有使用new,new.target不是Ticket,记录下来的比较结果为false。

如果只测试这一行,确实容易以为这个包装函数以后永远修改receiver。但接下来的new BoundTicket(2)改变了调用方式,不能继续沿用普通调用时的接收者推断。排查时要查看最终入口,而不只查看bind出现的那一行。

构造调用保住参数,创建新对象

constructed输出R-2和true,saved receiver仍为R-1。断言确认created与receiver不是同一个对象,说明二被用于新实例的编号,之前保存的receiver没有被这次构造改成R-2。R依然保留,证明预设参数没有随绑定this一起被忽略。

本例直接以BoundTicket作为new目标。绑定函数的构造规则把这个new目标转回原始Ticket,因此Ticket内部看到new.target === Ticket为true。这个观察限定在示例的直接构造方式;不要把复杂的Reflect.construct或继承情形也简化为永远相同。

原型与instanceof也要跟着构造路径看

created的原型是Ticket.prototype,两个instanceof检查都为true。对普通绑定函数,instanceof会沿绑定目标处理,不能因为变量名是BoundTicket,就认定实例必须来自另一份独立的类型定义。绑定包装和新建一个类不是同一种操作。

最后把箭头函数绑定后尝试new,仍得到TypeError。bind只在原目标具备可构造能力时提供相应构造路径,不会把任何可调用函数都变成构造函数。这条测试可以拦住“为了支持new,先给函数bind一下”的错误修补。

实际接口最好明确接收普通回调还是构造器。若只需要固定参数的对象工厂,可以暴露一个清楚的创建函数;若必须交给要求构造器的框架,就要分别验证新实例字段、原型和外部对象是否被意外修改。

本例用相同前缀、不同编号对照两次调用,避免把结果差异归因于传错参数。验收时保留普通调用与new调用两条路径,再加一个不可构造函数样本,就能把接收者规则、参数预填和可构造性三个问题分开定位。

官方资料核验日期:2026年10月2日。上述输出来自本文完整程序的本地执行,全部断言通过,退出状态为零。

参考资料

ECMAScript规范:绑定函数的Construct规则

ECMAScript规范:绑定函数创建与可构造性

ECMAScript规范:OrdinaryHasInstance对绑定函数的处理

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