当面试官让我们手写 call、apply、bind 时,他们真正考察的是什么?这三个方法看似简单,却隐藏着 JavaScript 函数执行上下文、原型链、参数处理等核心概念。本文将从零实现,并深入理解它们的差异和应用场景。
前言:为什么需要 call、apply、bind?
const obj = {
name: '张三',
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
const sayHelloFunc = obj.sayHello;
obj.sayHello(); // "你好,我是张三" - 正确
sayHelloFunc(); // "你好,我是undefined" - this丢失了!
上述代码,出现问题根源是:函数的 this 在调用时才确定,取决于调用方式。那如何解决呢?使用call、apply、bind 显式绑定 this 。
call 方法的实现
call 的基本使用
call 方法用于调用一个函数,并显式指定函数的 this 值和参数列表。
function greet(message) {
console.log(`${message}, ${this.name}!`);
}
const person = { name: 'zhangsan' };
// 原生 call 的使用
greet.call(person, '你好'); // "你好, zhangsan!"
call 的工作原理
- 将函数设为对象的属性
- 使用该对象调用函数
- 删除该属性
基础版本实现
Function.prototype.myCall = function (context, ...args) {
// 如果context是null或undefined,则绑定到全局对象
if (context == null) {
context = globalThis;
}
// 给context对象添加一个临时属性,值为当前函数
const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
context[fnKey] = this; // this指向调用myCall的函数
// 使用context对象调用函数
const result = context[fnKey](...args);
// 删除临时属性
delete context[fnKey];
return result;
};
处理边界情况
Function.prototype.myCallEnhanced = function (context, ...args) {
// 处理undefined和null
if (context == null) {
context = globalThis;
}
// 原始值需要转换为对象,否则不能添加属性
const contextType = typeof context;
if (contextType === 'string' ||
contextType === 'number' ||
contextType === 'boolean' ||
contextType === 'symbol' ||
contextType === 'bigint') {
context = Object(context); // 转换为包装对象
}
// 使用更安全的Symbol作为key
const fnKey = Symbol('fn');
context[fnKey] = this;
try {
const result = context[fnKey](...args);
return result;
} finally {
// 确保总是删除临时属性
delete context[fnKey];
}
};
完整实现与性能优化
Function.prototype.myCallFinal = function (context = globalThis, ...args) {
// 1. 类型检查:确保调用者是函数
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.myCallFinal called on non-function');
}
// 2. 处理Symbol和BigInt(ES6+)
const contextType = typeof context;
let finalContext = context;
// 3. 处理原始值(非严格模式下的自动装箱)
if (contextType === 'string') {
finalContext = new String(context);
} else if (contextType === 'number') {
finalContext = new Number(context);
} else if (contextType === 'boolean') {
finalContext = new Boolean(context);
} else if (contextType === 'symbol') {
// Symbol不能通过new创建,使用Object
finalContext = Object(context);
} else if (contextType === 'bigint') {
// BigInt不能通过new创建,使用Object
finalContext = Object(context);
}
// null和undefined已经通过默认参数处理
// 4. 使用Symbol创建唯一key,避免属性冲突
const fnSymbol = Symbol('callFn');
// 5. 将函数绑定到上下文对象
// 使用Object.defineProperty确保属性可配置
Object.defineProperty(finalContext, fnSymbol, {
value: this,
configurable: true,
writable: true,
enumerable: false
});
// 6. 执行函数并获取结果
let result;
try {
result = finalContext[fnSymbol](...args);
} finally {
// 7. 清理临时属性
try {
delete finalContext[fnSymbol];
} catch (error) {
// 如果上下文不可配置,忽略错误
console.warn('无法删除临时属性:', error.message);
}
}
return result;
};
apply 方法的实现
apply 的基本使用
apply 和 call 的功能基本相同,唯一的区别在于参数的传递方式:
- call 接受参数列表
- apply 接受参数数组
function sum(a, b, c) {
return a + b + c;
}
// apply:参数以数组形式传递
sum.apply(null, [1, 2, 3]);
基础版本实现
Function.prototype.myCall = function (context, args) {
// 如果context是null或undefined,则绑定到全局对象
if (context == null) {
context = globalThis;
}
// 给context对象添加一个临时属性,值为当前函数
const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
context[fnKey] = this; // this指向调用myCall的函数
// 使用context对象调用函数
const result = context[fnKey](...args);
// 删除临时属性
delete context[fnKey];
return result;
};
完整实现与性能优化
Function.prototype.myApply = function (context = globalThis, argsArray) {
// 1. 类型检查
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.myApply called on non-function');
}
// 2. 参数处理:确保argsArray是数组或类数组对象
let args = [];
if (argsArray != null) {
// 检查是否为数组或类数组
if (typeof argsArray !== 'object' ||
(typeof argsArray.length !== 'number' && argsArray.length !== undefined)) {
throw new TypeError('第二个参数必须是数组或类数组对象');
}
// 将类数组转换为真实数组
if (!Array.isArray(argsArray)) {
args = Array.from(argsArray);
} else {
args = argsArray;
}
}
// 3. 使用Symbol作为唯一key
const fnSymbol = Symbol('applyFn');
// 4. 处理原始值(与call相同)
const contextType = typeof context;
let finalContext = context;
if (contextType === 'string') {
finalContext = new String(context);
} else if (contextType === 'number') {
finalContext = new Number(context);
} else if (contextType === 'boolean') {
finalContext = new Boolean(context);
} else if (contextType === 'symbol') {
finalContext = Object(context);
} else if (contextType === 'bigint') {
finalContext = Object(context);
}
// 5. 绑定函数到上下文
Object.defineProperty(finalContext, fnSymbol, {
value: this,
configurable: true,
writable: true,
enumerable: false
});
// 6. 执行函数
let result;
try {
result = finalContext[fnSymbol](...args);
} finally {
// 7. 清理
try {
delete finalContext[fnSymbol];
} catch (error) {
// 忽略删除错误
}
}
return result;
};
bind 方法的实现
bind 的基本使用
bind 方法创建一个新的函数,当这个新函数被调用时,它的 this 值会被绑定到指定的对象,并且可以预先传入部分参数。
function greet(greeting, name) {
console.log(`${greeting}, ${name}! 我是${this.role}`);
}
const context = { role: '管理员' };
// bind:创建新函数,稍后执行
const boundGreet = greet.bind(context, '你好');
boundGreet('李四');
bind 的核心特性:
- 返回一个新函数
- 可以预设参数(柯里化)
- 绑定this值
- 支持new操作符(特殊情况)
基础版本实现
Function.prototype.myBind = function (context, ...bindArgs) {
const fn = this;
return function(...newArgs) {
return fn.apply(context, [...args, ...newArgs]);
};
};
处理 new 操作符的特殊情况
Function.prototype.myBindEnhanced = function (context = globalThis, ...bindArgs) {
const originalFunc = this;
if (typeof originalFunc !== 'function') {
throw new TypeError('Function.prototype.myBindEnhanced called on non-function');
}
// 内部函数,用于判断是否被new调用
const boundFunc = function (...callArgs) {
// 关键判断:this instanceof boundFunc
// 如果使用new调用,this会是boundFunc的实例
const isConstructorCall = this instanceof boundFunc;
// 确定最终的上下文
// 如果是构造函数调用,使用新创建的对象作为this
// 否则使用绑定的context
const finalContext = isConstructorCall ? this : Object(context);
// 合并参数
const finalArgs = bindArgs.concat(callArgs);
// 执行原函数
// 如果原函数有返回值,需要特殊处理
const result = originalFunc.apply(finalContext, finalArgs);
// 构造函数调用的特殊处理
// 如果原函数返回一个对象,则使用该对象
// 否则返回新创建的对象(this)
if (isConstructorCall) {
if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
return this;
}
return result;
};
// 维护原型链
// 方法1:直接设置prototype(有缺陷)
// boundFunc.prototype = originalFunc.prototype;
// 方法2:使用空函数中转(推荐)
const F = function () { };
F.prototype = originalFunc.prototype;
boundFunc.prototype = new F();
boundFunc.prototype.constructor = boundFunc;
// 添加一些元信息(可选)
boundFunc.originalFunc = originalFunc;
boundFunc.bindContext = context;
boundFunc.bindArgs = bindArgs;
return boundFunc;
};
完整实现与性能优化
Function.prototype.myBindFinal = (function () {
// 使用闭包保存Slice方法,提高性能
const ArraySlice = Array.prototype.slice;
// 空函数,用于原型链维护
function EmptyFunction() { }
return function myBindFinal(context = globalThis, ...bindArgs) {
const originalFunc = this;
// 严格的类型检查
if (typeof originalFunc !== 'function') {
throw new TypeError('Function.prototype.bind called on non-function');
}
// 处理原始值的上下文(非严格模式)
let boundContext = context;
const contextType = typeof boundContext;
// 原始值包装(与call/apply保持一致)
if (contextType === 'string') {
boundContext = new String(boundContext);
} else if (contextType === 'number') {
boundContext = new Number(boundContext);
} else if (contextType === 'boolean') {
boundContext = new Boolean(boundContext);
} else if (contextType === 'symbol') {
boundContext = Object(boundContext);
} else if (contextType === 'bigint') {
boundContext = Object(boundContext);
}
// 创建绑定函数
const boundFunction = function (...callArgs) {
// 判断是否被new调用
const isConstructorCall = this instanceof boundFunction;
// 确定最终上下文
let finalContext;
if (isConstructorCall) {
// new调用:忽略绑定的context,使用新实例
finalContext = this;
} else if (boundContext == null) {
// 非严格模式:使用全局对象
finalContext = globalThis;
} else {
// 普通调用:使用绑定的context
finalContext = boundContext;
}
// 合并参数
const allArgs = bindArgs.concat(callArgs);
// 调用原函数
const result = originalFunc.apply(finalContext, allArgs);
// 处理构造函数调用的返回值
if (isConstructorCall) {
// 如果原函数返回对象,则使用该对象
if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
// 否则返回新创建的实例
return this;
}
return result;
};
// 维护原型链 - 高性能版本
// 避免直接修改boundFunction.prototype,使用中间函数
if (originalFunc.prototype) {
EmptyFunction.prototype = originalFunc.prototype;
boundFunction.prototype = new EmptyFunction();
// 恢复constructor属性
boundFunction.prototype.constructor = boundFunction;
} else {
// 处理没有prototype的情况(如箭头函数)
boundFunction.prototype = undefined;
}
// 添加不可枚举的原始函数引用(用于调试)
Object.defineProperty(boundFunction, '__originalFunction__', {
value: originalFunc,
enumerable: false,
configurable: true,
writable: true
});
// 添加不可枚举的绑定信息
Object.defineProperty(boundFunction, '__bindContext__', {
value: boundContext,
enumerable: false,
configurable: true,
writable: true
});
Object.defineProperty(boundFunction, '__bindArgs__', {
value: bindArgs,
enumerable: false,
configurable: true,
writable: true
});
// 设置适当的函数属性
Object.defineProperty(boundFunction, 'length', {
value: Math.max(0, originalFunc.length - bindArgs.length),
enumerable: false,
configurable: true,
writable: false
});
Object.defineProperty(boundFunction, 'name', {
value: `bound ${originalFunc.name || ''}`.trim(),
enumerable: false,
configurable: true,
writable: false
});
return boundFunction;
};
})();
面试常见问题与解答
问题1:手写call的核心步骤是什么?
- 步骤1: 将函数设为上下文对象的属性
- 步骤2: 执行该函数
- 步骤3: 删除该属性
- 步骤4: 返回函数执行结果
- 关键点:
- 使用Symbol避免属性名冲突
- 处理null/undefined上下文
- 处理原始值上下文
- 使用展开运算符处理参数
问题2:bind如何处理new操作符?
- 通过 this instanceof boundFunction 判断是否被new调用
- 如果是new调用,忽略绑定的上下文,使用新创建的对象作为this
- 需要正确设置boundFunction的原型链,以支持instanceof
- 如果原构造函数返回对象,则使用该对象,否则返回新实例
问题3:call、apply、bind的性能差异?
- call通常比apply快,因为apply需要处理数组参数
- bind创建新函数有开销,但多次调用时比重复call/apply高效
结语
通过深入理解call、apply、bind的实现原理,我们不仅能更好地回答面试问题,还能在实际开发中编写出更优雅、更高效的JavaScript代码。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!