手动实现 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
相关推荐
lijun_xiao20091 小时前
前端最新Vue2+Vue3基础入门到实战项目全套教程
前端
90后的晨仔1 小时前
Pinia 状态管理原理与实战全解析
前端·vue.js
杰克尼1 小时前
JavaWeb_p165部门管理
java·开发语言·前端
EndingCoder1 小时前
WebSocket实时通信:Socket.io
服务器·javascript·网络·websocket·网络协议·node.js
90后的晨仔1 小时前
Vue3 状态管理完全指南:从响应式 API 到 Pinia
前端·vue.js
90后的晨仔1 小时前
Vue 内置组件全解析:提升开发效率的五大神器
前端·vue.js
我胡为喜呀1 小时前
Vue3 中的 watch 和 watchEffect:如何优雅地监听数据变化
前端·javascript·vue.js
我登哥MVP2 小时前
Ajax 详解
java·前端·ajax·javaweb
非凡ghost2 小时前
Typora(跨平台MarkDown编辑器) v1.12.2 中文绿色版
前端·windows·智能手机·编辑器·软件需求
馨谙2 小时前
/dev/null 是什么,有什么用途?
前端·chrome