手写实现 bind、call、apply 函数(附带逐行解析)!


在 JavaScript 中,bind、call 和 apply 是函数对象的三个重要方法,用于显式绑定函数的 this 指向(无法改变箭头函数this指向,下面会解释原因)。三个函数都绑定在Function构造函数的prototype上,每个函数又都是Function的实例,所以每个函数都可以直接调用call、apply、bind(原型链继承向上查找)。

方法调用方式结果
callfunction.call(thisArg, arg1, arg2, ...)立即执行
applyfunction.apply(thisArg, [arg1, arg2, ...])立即执行
bindfunction.bind(thisArg, arg1, arg2, ...)返回新函数,用到的时候调用

下面开始实现它们的自定义版本:

1. es6实现方式

1.实现call函数
Function.prototype.selfCall = function(context, ...args) {
  // 1. 处理 context 为 null 或 undefined 的情况
  context = context || window;

  // 2. 创建唯一的键名避免冲突
  const fnKey = Symbol('fn');

  // 3. 将当前函数设置为 context 的属性
  context[fnKey] = this;

  // 4. 执行函数
  const result = context[fnKey](...args);

  // 5. 删除临时属性
  delete context[fnKey];

  // 6. 返回结果
  return result;
};

// 测试
const person = { name: 'Alice' };
function greet(greeting) {
  return `${greeting}, ${this.name}!`;
}
        
console.log(greet.selfCall(person, 'Hello')); // "Hello, Alice!"  
2.实现apply函数

Function.prototype.selfApply = function(context, argsArray) {
  // 1. 处理 context 为 null 或 undefined 的情况
  context = context || window;
  
  // 2. 创建唯一的键名避免冲突
  const fnKey = Symbol('fn');
  
  // 3. 将当前函数设置为 context 的属性
  context[fnKey] = this;
  
  // 4. 执行函数(处理参数数组)
  const result = argsArray 
    ? context[fnKey](...argsArray)
    : context[fnKey]();
  
  // 5. 删除临时属性
  delete context[fnKey];
  
  // 6. 返回结果
  return result;
};

// 测试
const numbers = [1, 2, 3];
console.log(Math.max.selfApply(null, numbers)); // 3
3.实现bind函数
Function.prototype.selfBind = function(context, ...bindArgs) {
  // 1. 保存原始函数引用
  // this 指向调用 selfBind 的原始函数
  // 例如:fn.selfBind() 中 this = fn
  const originalFunc = this;
  
  // 2. 返回绑定函数
  // 创建闭包,捕获 context 和 bindArgs
  return function boundFn(...callArgs) {
    // 3. 判断是否作为构造函数调用
    // new.target 检测函数是否通过 new 调用
    // 如果使用 new 调用 boundFn,new.target 指向 boundFn
    const isNewCall = new.target !== undefined;
    
    // 4. 处理不同的调用场景
    if (isNewCall) {
      // 4.1 构造函数调用场景
      // 忽略绑定的 this (context)
      // 合并预设参数和调用时参数
      // 使用 new 创建原始函数实例
      return new originalFunc(...bindArgs, ...callArgs);
    }
    
    // 5. 普通函数调用场景
    // 使用绑定的 this (context)
    // 合并预设参数和调用时参数
    return originalFunc.call(context, ...bindArgs, ...callArgs);
  };
};

// 测试
const person = { name: 'Bob' };
function introduce(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const boundIntro = introduce.selfBind(person, 'Hi');
boundIntro('!'); // "Hi, Bob!"

// 构造函数测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const BoundPerson = Person.selfBind(null, 'Alice');
const alice = new BoundPerson(25);
console.log(alice); // { name: 'Alice', age: 25 }

2. bind 的复杂点处理

场景处理方式
普通调用使用绑定的 this 和合并后的参数
构造函数调用忽略绑定的 this,但保留参数
参数合并绑定参数 + 调用参数

关键点解释:

  1. 闭包设计

    • boundFn 函数捕获了三个关键变量:

      • originalFunc: 原始函数
      • context: 绑定的 this 值
      • bindArgs: 预设参数
    • 即使外部函数执行完毕,这些变量仍然可用
  2. new.target 检测

    • 当使用 new 调用绑定函数时:
    const Bound = fn.selfBind(obj);
    const instance = new Bound(); // new.target 指向 Bound
    • 普通调用时:new.targetundefined
  3. 构造函数处理

    • 当检测到 new 调用时,忽略绑定的 this
    • 将预设参数和调用参数合并后传递给原始函数
    • 使用 new originalFunc(...) 创建实例
  4. 普通调用处理

    • 使用 call 方法调用原始函数
    • 传入绑定的 this 和合并后的参数

3. 边界情况处理

  1. context 为空:默认指向全局对象(浏览器中为 window
  2. Symbol 使用:避免覆盖对象原有属性
  3. 构造函数场景:通过 new.target 检测是否被 new 调用
  4. 参数处理:支持剩余参数语法处理变长参数

完整实现代码(ES5 兼容版本)

// call 实现 (ES5)
Function.prototype.selfCall = function(context) {
  context = context || window;
  var fnKey = '__fn__' + Date.now();
  context[fnKey] = this;
  
  var args = [];
  for (var i = 1; i < arguments.length; i++) {
    args.push('arguments[' + i + ']');
  }
  
  var result = eval('context[fnKey](' + args + ')');
  delete context[fnKey];
  return result;
};

// apply 实现 (ES5)
Function.prototype.seflApply = function(context, argsArray) {
  context = context || window;
  var fnKey = '__fn__' + Date.now();
  context[fnKey] = this;
  
  var result;
  if (!argsArray || !argsArray.length) {
    result = context[fnKey]();
  } else {
    var args = [];
    for (var i = 0; i < argsArray.length; i++) {
      args.push('argsArray[' + i + ']');
    }
    result = eval('context[fnKey](' + args + ')');
  }
  
  delete context[fnKey];
  return result;
};

// bind 实现 (ES5)
Function.prototype.selfBind = function(context) {
  // 1. 保存原始函数
  // this 指向调用 bind 的原始函数
  var originalFunc = this;
  
  // 2. 获取预设参数
  // 将 arguments 转为数组,跳过第一个参数(context)
  // 例如:fn.selfBind(obj, 1, 2) → bindArgs = [1, 2]
  var bindArgs = Array.prototype.slice.call(arguments, 1);
  
  // 3. 创建绑定函数
  function BoundFn() {
    // 4. 获取调用时参数
    // 将当前函数的 arguments 转为数组
    var callArgs = Array.prototype.slice.call(arguments);
    
    // 5. 检测构造函数调用
    // 检查 this 是否是 BoundFn 的实例
    // 通过 new 调用时:this instanceof BoundFn 为 true
    var isNewCall = this instanceof BoundFn;
    
    // 6. 动态确定 this 值
    // 构造函数调用:使用新创建的实例 (this)
    // 普通调用:使用预设的 context
    return originalFunc.apply(
      isNewCall ? this : context,
      bindArgs.concat(callArgs) // 合并参数
    );
  }
  
  // 7. 维护原型链
  // 复制原始函数的原型,确保 instanceof 正常工作
  BoundFn.prototype = Object.create(originalFunc.prototype);
  
  // 8. 修复构造函数指向
  // 确保新实例的 constructor 属性正确指向原始函数
  BoundFn.prototype.constructor = originalFunc;
  
  // 9. 返回绑定函数
  return BoundFn;
};

测试用例

// 测试 call
const obj1 = { value: 10 };
function add(a, b) {
  return this.value + a + b;
}
console.log(add.selfCall(obj1, 5, 3)); // 18

// 测试 apply
const obj2 = { value: 20 };
console.log(add.selfApply(obj2, [5, 3])); // 28

// 测试 bind
const obj3 = { value: 30 };
const boundAdd = add.selfBind(obj3, 5);
console.log(boundAdd(3)); // 38

// 测试构造函数
function Point(x, y) {
  this.x = x;
  this.y = y;
}
const BoundPoint = Point.selfBind(null, 10);
const point = new BoundPoint(20);
console.log(point); // { x: 10, y: 20 }

实现总结

方法实现难点解决方案
call参数传递使用剩余参数或 eval
apply数组参数处理判断数组是否存在并展开
bind构造函数处理检测 new.targetinstanceof
原型链绑定函数原型使用 Object.create 继承

下面特别为es6、es5selfBind方法关键点解释:

  1. 参数处理

    • 使用 Array.prototype.slice.call(arguments, 1) 将类数组对象转为真实数组
    • 获取预设参数(除第一个 context 参数外的所有参数)
  2. 构造函数检测

    • 通过 this instanceof BoundFn 检测是否被 new 调用
    • new 调用时:this 指向新创建的 BoundFn 实例
    • 普通调用时:this 指向全局对象或 undefined(严格模式)
  3. 原型链维护
BoundFn.prototype = Object.create(originalFunc.prototype);
    • 创建新对象继承原始函数的原型
    • 确保 instanceof 操作符正常工作:
    new BoundFn() instanceof originalFunc // true
    1. 构造函数修复
    BoundFn.prototype.constructor = originalFunc;
    • 修正原型对象的 constructor 属性
    • 确保继承关系正确:
    new BoundFn().constructor === originalFunc // true
    1. 参数合并
    bindArgs.concat(callArgs)
    • 将预设参数和调用时参数合并为单个数组
    • 使用 apply 传递合并后的参数数组

    两版本核心区别

    特性ES6+ 版本ES5 版本
    参数处理剩余参数 ...bindArgsArray.prototype.slice.call
    构造函数检测new.targetthis instanceof BoundFn
    原型处理隐式处理显式维护原型链
    参数传递扩展运算符 ...apply + concat
    代码简洁性⭐⭐⭐⭐⭐⭐⭐⭐
    浏览器兼容性现代浏览器IE9+

    使用示例

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    // 测试 ES6+ 版本
    const BoundPerson6 = Person.myBind(null, "Alice");
    const alice = new BoundPerson6(25);
    console.log(alice); // { name: "Alice", age: 25 }
    
    // 测试 ES5 版本
    const BoundPerson5 = Person.myBind(null, "Bob");
    const bob = new BoundPerson5(30);
    console.log(bob); // { name: "Bob", age: 30 }
    
    // 普通调用
    function greet(greeting) {
      return `${greeting}, ${this.name}`;
    }
    
    const boundGreet = greet.myBind({ name: "Charlie" }, "Hello");
    console.log(boundGreet()); // "Hello, Charlie"

    要点总结

    1. 双重调用模式处理

      • 普通调用:使用绑定的 this
      • 构造函数调用:忽略绑定的 this,创建新实例
    2. 参数合并

      • 预设参数 + 调用时参数
      • 保持原生 bind 的参数传递顺序
    3. 原型链维护

      • 确保构造函数调用时,实例继承原始函数的原型
      • 修复 constructor 属性指向
    4. 闭包应用

      • 捕获原始函数、绑定上下文和预设参数
      • 保持函数调用的上下文信息

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

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

    转载:转载请注明原文链接 - 手写实现 bind、call、apply 函数(附带逐行解析)!


    Carpe Diem and Do what I like