手动实现 JavaScript 的 call、apply 和 bind 方法

1. 实现 call 方法

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

实现思路:

  1. 将函数设置为对象的属性
  2. 执行该函数
  3. 删除该属性(避免污染对象)

实现代码:

javascript 复制代码
Function.prototype.myCall = function(context, ...args) {
  // 如果context为null或undefined,则指向全局对象(浏览器中是window)
  context = context || window;
  
  // 创建一个唯一的属性名,避免覆盖原有属性
  const fnSymbol = Symbol('fn');
  
  // 将当前函数(this)赋值给context的fnSymbol属性
  context[fnSymbol] = this;
  
  // 执行函数
  const result = context[fnSymbol](...args);
  
  // 删除临时属性
  delete context[fnSymbol];
  
  return result;
};

// 测试用例
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'John' };
greet.myCall(person, 'Hello', '!'); // 输出: "Hello, John!"

2. 实现 apply 方法

apply() 方法与 call() 类似,只是参数以数组(或类数组对象)的形式传递。

实现代码:

javascript 复制代码
Function.prototype.myApply = function(context, argsArray) {
  // 如果context为null或undefined,则指向全局对象
  context = context || window;
  
  // 创建一个唯一的属性名
  const fnSymbol = Symbol('fn');
  
  // 将当前函数赋值给context的fnSymbol属性
  context[fnSymbol] = this;
  
  // 执行函数,处理argsArray为null或undefined的情况
  const result = argsArray ? context[fnSymbol](...argsArray) : context[fnSymbol]();
  
  // 删除临时属性
  delete context[fnSymbol];
  
  return result;
};

// 测试用例
function introduce(language, years) {
  console.log(`I'm ${this.name}, I code in ${language} for ${years} years.`);
}

const dev = { name: 'Alice' };
introduce.myApply(dev, ['JavaScript', 5]); // 输出: "I'm Alice, I code in JavaScript for 5 years."

3. 实现 bind 方法

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,其余参数将作为新函数的参数供调用时使用。

实现思路:

  1. 返回一个新函数
  2. 新函数执行时,使用指定的 this
  3. 支持柯里化(参数分次传递)

实现代码:

javascript 复制代码
Function.prototype.myBind = function(context, ...bindArgs) {
  const originalFunc = this;
  
  return function(...callArgs) {
    // 判断是否作为构造函数调用(使用new操作符)
    if (new.target) {
      return new originalFunc(...bindArgs, ...callArgs);
    }
    
    // 普通函数调用
    const fnSymbol = Symbol('fn');
    context = context || window;
    context[fnSymbol] = originalFunc;
    
    const result = context[fnSymbol](...bindArgs, ...callArgs);
    delete context[fnSymbol];
    
    return result;
  };
};

// 测试用例
function printInfo(role, department) {
  console.log(`${this.name} is ${role} in ${department}`);
}

const employee = { name: 'Bob' };
const boundFunc = printInfo.myBind(employee, 'developer');
boundFunc('IT'); // 输出: "Bob is developer in IT"

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

const BoundPerson = Person.myBind(null, 'DefaultName');
const p = new BoundPerson(30);
console.log(p); // 输出: Person { name: 'DefaultName', age: 30 }

4. 完整实现与边界情况处理

完整实现(处理更多边界情况):

javascript 复制代码
Function.prototype.myFullBind = function(context, ...bindArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('Bind must be called on a function');
  }
  
  const originalFunc = this;
  const boundFunc = function(...callArgs) {
    // 判断是否作为构造函数调用
    const isConstructorCall = new.target;
    
    return originalFunc.apply(
      isConstructorCall ? this : (context || globalThis),
      bindArgs.concat(callArgs)
    );
  };
  
  // 维护原型关系
  if (originalFunc.prototype) {
    boundFunc.prototype = Object.create(originalFunc.prototype);
  }
  
  return boundFunc;
};

边界情况说明:

  1. 构造函数调用 :当使用 new 操作符调用绑定函数时,this 应该指向新创建的对象,而不是绑定的 context
  2. 原型链维护:确保绑定后的函数能正确继承原函数的原型链
  3. 严格模式 :在严格模式下,contextnullundefined 时,this 不会自动转为全局对象
  4. 参数合并:正确处理绑定时参数和调用时参数的合并

5. 三种方法的对比总结

方法 调用方式 参数形式 立即执行 返回新函数
call func.call(ctx, arg1, arg2) 参数列表
apply func.apply(ctx, [arg1, arg2]) 数组形式参数
bind func.bind(ctx, arg1, arg2) 参数列表

6. 实际应用场景

call/apply 使用场景:

  1. 借用方法
javascript 复制代码
// 类数组对象使用数组方法
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.push.myCall(arrayLike, 'c');
console.log(arrayLike); // { 0: 'a', 1: 'b', 2: 'c', length: 3 }
  1. 确定函数上下文
javascript 复制代码
function logThis() {
  console.log(this);
}
logThis.myCall({ id: 42 }); // 输出: { id: 42 }

bind 使用场景:

  1. 事件处理函数
javascript 复制代码
class Button {
  constructor() {
    this.text = 'Click me';
    this.handleClick = this.handleClick.myBind(this);
  }
  
  handleClick() {
    console.log(this.text);
  }
}

const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
  1. 参数预设(柯里化)
javascript 复制代码
function multiply(a, b) {
  return a * b;
}
const double = multiply.myBind(null, 2);
console.log(double(5)); // 10
相关推荐
xingba1 分钟前
重写IE的showModalDialog模态框以兼容现代浏览器
前端·javascript·google
前端小巷子2 分钟前
Promise 静态方法:轻松处理多个异步任务
前端·面试·promise
梨子同志7 分钟前
JavaScript Set 和 Map 数据结构
前端·javascript
令狐寻欢8 分钟前
JavaScript中常用的数据结构与算法
javascript
初辰ge11 分钟前
做个大屏既要不留白又要不变形还要没滚动条,我直接怒斥领导,大屏适配就这四种模式
前端·javascript
Face14 分钟前
路由Vue-router 及 异步组件
前端·javascript·vue.js
Nano15 分钟前
Axios 进阶指南:掌握请求取消与进度监控的艺术
前端
工呈士15 分钟前
Context API 应用与局限性
前端·react.js·面试
ArcX15 分钟前
从 JS 到 Rust 的旅程
前端·javascript·rust