在 JavaScript 面试中,手动实现 call、apply 和 bind 是高频考点。这不仅考察你对函数调用、this 绑定的理解,更检验你对 JavaScript 底层机制的掌握程度。
本文将带你一步步实现这三个方法,不仅写出代码,更要讲清原理。
一、实现 call 方法
✅ 核心思路
- 将函数作为上下文对象的临时方法;
- 调用该方法,此时
this自动指向该对象; - 调用完毕后删除临时属性;
- 返回结果。
✅ 实现代码
Function.prototype.myCall = function(context, ...args) {
// 1. 检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.call called on non-function');
}
// 2. 处理 context,null/undefined 指向全局对象
context = context || globalThis; // 浏览器中为 window,Node 中为 global
// 3. 为 context 添加唯一属性,避免覆盖原有方法
const fnKey = Symbol('tempFn');
context[fnKey] = this; // this 指向调用 myCall 的函数
// 4. 调用函数并保存结果
const result = context[fnKey](...args);
// 5. 删除临时属性
delete context[fnKey];
// 6. 返回结果
return result;
};
✅ 使用示例
function greet(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
const person = { name: 'Alice' };
console.log(greet.myCall(person, 'Hello', '!'));
// 输出: Hello, I'm Alice!
二、实现 apply 方法
✅ 与 call 的区别
apply的第二个参数是数组或类数组;- 其余逻辑几乎完全相同。
✅ 实现代码
Function.prototype.myApply = function(context, argsArray) {
// 1. 检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.apply called on non-function');
}
// 2. 处理 context
context = context || globalThis;
// 3. 处理参数:必须是数组或类数组
if (argsArray == null) {
argsArray = [];
} else if (!Array.isArray(argsArray) && !(typeof argsArray === 'object' && 'length' in argsArray)) {
throw new TypeError('CreateListFromArrayLike called on non-object');
}
// 4. 添加临时方法
const fnKey = Symbol('tempFn');
context[fnKey] = this;
// 5. 调用函数(使用展开运算符传参)
const result = context[fnKey](...argsArray);
// 6. 删除临时属性
delete context[fnKey];
// 7. 返回结果
return result;
};
✅ 使用示例
function sum(a, b, c) {
return a + b + c;
}
console.log(sum.myApply(null, [1, 2, 3])); // 6
console.log(sum.myApply(null, {0: 1, 1: 2, 2: 3, length: 3})); // 6(类数组)
三、实现 bind 方法
✅ 核心难点
bind 返回一个新函数,且需处理:
- 普通调用:
this指向绑定对象; new调用:this指向新创建的实例(此时忽略绑定的this);- 参数预设:支持柯里化(currying)。
✅ 实现代码
Function.prototype.myBind = function(context, ...bindArgs) {
// 1. 检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind called on non-function');
}
const originalFunc = this;
// 2. 创建绑定函数
const boundFunc = function(...callArgs) {
// 3. 判断是否通过 new 调用
const isNew = this instanceof boundFunc;
// 4. 决定 this 指向
const thisArg = isNew ? this : (context || globalThis);
// 5. 合并参数:绑定时的参数 + 调用时的参数
const args = bindArgs.concat(callArgs);
// 6. 使用 apply 调用原函数
return originalFunc.apply(thisArg, args);
};
// 7. 维护原型链(关键!)
// 如果原函数有 prototype,boundFunc 也应继承
if (originalFunc.prototype) {
// 创建一个空函数作为中转,避免直接修改原 prototype
function Empty() {}
Empty.prototype = originalFunc.prototype;
boundFunc.prototype = new Empty();
}
// 8. 返回绑定函数
return boundFunc;
};
✅ 使用示例
场景1:普通绑定
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const person = { name: 'Bob' };
const boundGreet = greet.myBind(person);
console.log(boundGreet('Hi')); // Hi, Bob
场景2:new 调用
function Person(name, age) {
this.name = name;
this.age = age;
}
const boundPerson = Person.myBind(null, 'Charlie');
const p = new boundPerson(25);
console.log(p); // Person { name: 'Charlie', age: 25 }
// 注意:new 调用时,this 指向新实例,而非 null
场景3:柯里化
function add(a, b, c) {
return a + b + c;
}
const add5 = add.myBind(null, 5);
const add5And10 = add5.myBind(null, 10);
console.log(add5And10(3)); // 18 (5 + 10 + 3)
四、关键细节解析
🔍 1. 为什么用 Symbol 作为临时键?
const fnKey = Symbol('tempFn');
- 避免与对象原有属性冲突;
Symbol是唯一的,最安全。
⚠️ 旧实现常用
context.fn = this,但可能覆盖fn属性。
🔍 2. 为什么 bind 要维护原型链?
boundFunc.prototype = new Empty();
- 如果原函数是构造函数,
new boundFunc()应能正常工作; - 直接
boundFunc.prototype = originalFunc.prototype会导致修改boundFunc.prototype影响原函数; - 使用空构造函数中转是经典模式。
🔍 3. 如何判断 new 调用?
const isNew = this instanceof boundFunc;
- 当
new boundFunc()时,this是新创建的实例,且instanceof为true; - 此时
this的原型链包含boundFunc.prototype。
五、测试与验证
// 测试 call
console.assert((function() { return this.value; }).myCall({ value: 42 }) === 42);
// 测试 apply
console.assert((function(a,b,c) { return a+b+c; }).myApply(null, [1,2,3]) === 6);
// 测试 bind
const obj = { value: 100 };
const bound = (function() { return this.value; }).myBind(obj);
console.assert(bound() === 100);
六、总结:三大方法实现要点
| 方法 | 关键实现点 |
|---|---|
call | 临时挂载 → 调用 → 删除 → 返回 |
apply | 参数为数组,用 ...argsArray 传参 |
bind | 返回函数,处理 new 调用,维护原型链 |
💡 结语
“实现
call、apply、bind,就是实现 JavaScript 的函数调用控制权。”
通过手动实现,你将深刻理解:
this是如何被绑定的;- 函数是如何被“借调”的;
- 原型链是如何被继承的。