ES5 版本 bind 实现中维护构造函数的原因详解


ES5 版本 bind 实现中维护构造函数的原因详解

在 ES5 版本的 bind 实现中,需要显式维护构造函数关系,这是为了确保使用 new 操作符调用绑定函数时,能够正确处理原型链和继承关系。

核心问题:原型链的断裂

当使用 new 操作符调用绑定函数时,JavaScript 引擎会创建一个新对象,并将该对象的原型链接到绑定函数的 prototype 属性:

const boundFn = originalFunc.bind(context);
const instance = new boundFn();

在原生 bind 实现中,boundFnprototype 属性会自动设置为与 originalFunc 相同的原型链。但在自定义实现中,我们需要手动维护这个关系。

ES5 实现中的关键代码
// 维护原型链
BoundFn.prototype = Object.create(originalFunc.prototype);

// 修复构造函数指向
BoundFn.prototype.constructor = originalFunc;

为什么需要这两步?

1. 确保 instanceof 操作符正常工作
function Person(name) {
  this.name = name;
}

// 使用原生 bind
const BoundPersonNative = Person.bind(null);
const alice = new BoundPersonNative('Alice');
console.log(alice instanceof Person); // true ✅

// 使用自定义 bind(无原型处理)
function brokenBind(fn, context) {
  return function() {
    return fn.apply(context, arguments);
  };
}

const BoundPersonBroken = brokenBind(Person, null);
const bob = new BoundPersonBroken('Bob');
console.log(bob instanceof Person); // false ❌

原因分析

  • instanceof 检查右侧构造函数的 prototype 是否出现在左侧对象的原型链上
  • 没有设置 BoundFn.prototype 时,新对象的原型是默认的 Object.prototype
  • 我们需要手动建立 BoundFnoriginalFunc 的原型连接
2. 保持正确的原型继承
Person.prototype.sayHello = function() {
  return `Hello, ${this.name}`;
};

// 正确实现
const BoundPerson = Person.myBind(null);
const carol = new BoundPerson('Carol');
console.log(carol.sayHello()); // "Hello, Carol" ✅

// 缺少原型处理
const BrokenPerson = brokenBind(Person, null);
const dave = new BrokenPerson('Dave');
console.log(dave.sayHello()); // TypeError: dave.sayHello is not a function ❌

原因分析

  • 原型方法定义在 Person.prototype
  • 新创建的对象需要继承 Person.prototype
  • 通过 Object.create(originalFunc.prototype) 建立原型链

3. 修复 constructor 属性

// 正确实现
console.log(carol.constructor === Person); // true ✅

// 没有修复 constructor 的情况
BoundFn.prototype = Object.create(Person.prototype);
console.log(new BoundFn().constructor); // Person ✅ (自动继承)

// 但如果重写整个 prototype
function Animal() {}
BoundFn.prototype = Object.create(Animal.prototype);
console.log(new BoundFn().constructor); // Animal ❌ (错误指向)

原因分析

  • 默认情况下,函数的 prototype.constructor 指向自身
  • 当我们使用 Object.create() 时,会创建一个新对象,其 constructor 属性继承自原始函数的原型
  • 显式设置 BoundFn.prototype.constructor = originalFunc 确保:

    1. 实例的 constructor 属性正确指向原始构造函数
    2. 避免意外的继承关系

原型链关系图解

ES5 实现的原型链

ES5 实现的原型链.png

ES6+ 版本的原型链

ES6+ 版本的原型链.png

为什么 ES6+ 版本不需要这些?

在 ES6+ 版本的实现中:

if (isNewCall) {
  return new originalFunc(...bindArgs, ...callArgs);
}

这里直接使用 new originalFunc() 创建实例,绕过了绑定函数的原型链:

  1. new originalFunc() 直接创建原始构造函数的实例
  2. 实例的原型直接链接到 originalFunc.prototype
  3. 不涉及绑定函数 (boundFn) 的原型

因此 ES6+ 版本不需要维护绑定函数的原型关系。

完整原型链对比

ES5 实现的原型链流程

ES5 实现的原型链流程.png

ES6+ 实现的原型链流程

ES6+ 实现的原型链流程.png

实际应用场景

需要维护构造函数的场景

  1. 类库开发:创建兼容旧浏览器的 polyfill
  2. 框架实现:需要支持 IE11 等老式浏览器
  3. 自定义继承系统:实现复杂的原型继承逻辑

不需要维护的场景

  1. 现代浏览器环境:使用原生 bind
  2. Babel 转译环境:转译器会自动处理
  3. 无继承的简单函数:不涉及原型链的函数

边界情况处理

1. 原始函数没有 prototype

const arrowFn = () => {};
const boundArrow = arrowFn.myBind(null);

// 应避免设置原型
if (typeof originalFunc.prototype !== 'undefined') {
  BoundFn.prototype = Object.create(originalFunc.prototype);
  BoundFn.prototype.constructor = originalFunc;
}

2. 原始函数是内置函数

const boundSlice = [].slice.myBind([1,2,3]);
console.log(boundSlice(1)); // [2,3]

// 内置函数通常没有可用的 prototype
// 但仍需正确处理

实现总结

ES5 版本的 bind 实现需要维护构造函数关系,主要是因为:

  1. 原型链继承:确保新实例继承原始构造函数的原型方法
  2. instanceof 支持:使 instanceof 操作符能正确识别实例类型
  3. 构造函数标识:保持实例的 constructor 属性指向正确
  4. 兼容原生行为:模拟原生 bind 的完整功能

bindcallapply 能否改变箭头函数的 this 指向?

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - ES5 版本 bind 实现中维护构造函数的原因详解


Carpe Diem and Do what I like