在写 call 之前,我们先明确它的原生功能 ——call 是所有函数都有的方法,核心作用就两个:
- 改变函数执行时的
this指向; - 给函数传递参数(逐个传递,不是数组)。
// 普通函数
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 步,每一步都有明确目的:
- 先判断:调用
myCall的是不是函数(比如有人用对象调用,就报错); - 处理
context:如果没传context(比如fn.myCall()),就默认指向window; - 处理参数:截取
call第一个参数后的所有参数(第一个是context,后面才是给函数的实参); - 绑定函数:把要执行的函数(
this,因为myCall是函数原型上的方法,this就是调用者),临时挂载到context上(比如context.fn = 要执行的函数); - 执行函数:通过
context.fn(...)调用,此时函数的this自然指向context(因为是context调用的方法); - 捕获结果:保存函数执行后的返回值(原生
call会返回函数执行结果); - 清理痕迹:删除
context上临时挂载的fn(避免污染原对象); - 返回结果:把捕获的返回值返回出去。
// 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;
};