手写 call 函数:10 分钟吃透原理 + 实现(面试高频)

26 阅读3分钟

在写 call 之前,我们先明确它的原生功能 ——call 是所有函数都有的方法,核心作用就两个:

  1. 改变函数执行时的 this 指向;
  2. 给函数传递参数(逐个传递,不是数组)。
// 普通函数
function sayHi(name) {
  console.log(`你好,${name}!我是${this.name}`);
  return `结果:${this.name}-${name}`; // 有返回值
}

// 测试对象
const person1 = { name: "张三" };
const person2 = { name: "李四" };

// 原生 call 用法:sayHi.call(要绑定的this, 参数1, 参数2...)
sayHi.call(person1, "小明"); // 输出:你好,小明!我是张三(this指向person1)
const res = sayHi.call(person2, "小红"); // 输出:你好,小红!我是李四(this指向person2)
console.log(res); // 结果:李四-小红(捕获返回值)

通过 call,我们让同一个 sayHi 函数的 this 灵活指向不同对象,还能传参、拿返回值 —— 这就是我们要手写实现的核心目标。

要手写 call,我们得顺着它的功能反推实现逻辑,一共 7 步,每一步都有明确目的:

  1. 先判断:调用 myCall 的是不是函数(比如有人用对象调用,就报错);
  2. 处理 context:如果没传 context(比如 fn.myCall()),就默认指向 window
  3. 处理参数:截取 call 第一个参数后的所有参数(第一个是 context,后面才是给函数的实参);
  4. 绑定函数:把要执行的函数(this,因为 myCall 是函数原型上的方法,this 就是调用者),临时挂载到 context 上(比如 context.fn = 要执行的函数);
  5. 执行函数:通过 context.fn(...) 调用,此时函数的 this 自然指向 context(因为是 context 调用的方法);
  6. 捕获结果:保存函数执行后的返回值(原生 call 会返回函数执行结果);
  7. 清理痕迹:删除 context 上临时挂载的 fn(避免污染原对象);
  8. 返回结果:把捕获的返回值返回出去。
// 1. 挂载到 Function.prototype,所有函数都能调用
Function.prototype.myCall = function(context) {
  // 步骤2:判断调用者是不是函数(比如 obj.myCall() 就报错)
  if (typeof this !== "function") {
    throw new TypeError("type error"); // 规范报错类型,和原生一致
  }

  // 步骤3:处理参数——截取第一个参数后的所有参数(arguments是伪数组,转成数组后slice(1))
  const args = [...arguments].slice(1); // 比如 arguments是[person1, "小明"],slice(1)后是["小明"]
  let result = null; // 用来存函数执行后的返回值

  // 步骤4:处理 context——没传就指向 window(非严格模式)
  context = context || window; // 注意:严格模式下不能默认window,这里先按常用场景来

  // 步骤5:绑定函数——把当前调用myCall的函数(this),临时挂到context上
  // 为什么要挂?因为 context.fn() 执行时,fn的this就指向context,刚好实现改变this的目的
  context.fn = this;
  //因为context.fn = this;,
  //所以context.fn的功能和this一样了,
  //context触发了fn方法,所以this指向了context
  // 步骤6:执行函数——传入参数,捕获返回值
  result = context.fn(...args); // 等价于 context.fn("小明"),this指向context

  // 步骤7:清理痕迹——删除临时挂载的fn,不污染原context对象
  delete context.fn;

  // 步骤8:返回结果——和原生call一致,返回函数执行后的结果
  return result;
};