Appearance
call、apply、bind 的区别与手写 bind
难度等级:🟡 中级 分类:JavaScript 归档日期:2026-10-10
call、apply、bind 有什么区别?手写一个 bind。
难度与考察点
难度: 初中级基础题,手写 bind 属于中级。对 8 年经验的候选人,面试官主要看三点:
- 能否讲清
this的绑定规则,而不只是背 API - 手写
bind时有没有处理new调用(这是区分度最大的一点) - 能否说出原生
bind和手写版的差异,以及在实际项目里的取舍
核心原理
三者的区别
| 调用时机 | 参数形式 | 返回值 | |
|---|---|---|---|
call | 立即执行 | 逐个传:fn.call(ctx, a, b) | 函数执行结果 |
apply | 立即执行 | 数组/类数组:fn.apply(ctx, [a, b]) | 函数执行结果 |
bind | 不执行,返回新函数 | 逐个传,支持预置参数(柯里化) | 绑定后的新函数 |
几个容易被追问的细节:
ctx传null/undefined:非严格模式下this指向globalThis,严格模式下就是null/undefined。传原始值(如1)在非严格模式下会被包装成对象。bind只生效一次:fn.bind(a).bind(b),this仍是a。第二次 bind 改的是外层包装函数的this,而内层已经固定了,但参数可以继续追加。- 箭头函数:没有自己的
this,call/apply/bind都无法改变它的this,只能传参。 new优先级高于bind:对绑定函数使用new时,绑定的this会被忽略,this指向新创建的实例,但预置参数依然生效。
this 绑定优先级:new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定(obj.fn())> 默认绑定。详见 执行上下文、调用栈与 this。
手写 bind
需要满足四点:
- 返回一个新函数,
this固定为context - 支持预置参数 + 调用时参数合并
- 被
new调用时,忽略绑定的this,以原函数为构造函数创建实例 - 实例能通过
instanceof识别原函数
版本一:现代写法(推荐)
js
Function.prototype.myBind = function (context, ...presetArgs) {
if (typeof this !== 'function') {
throw new TypeError('myBind must be called on a function');
}
const target = this;
function bound(...args) {
const finalArgs = [...presetArgs, ...args];
// 被 new 调用:new.target 有值
if (new.target) {
// 对齐规范:new bound 时,以 target 作为 newTarget,
// 实例原型直接是 target.prototype
return Reflect.construct(
target,
finalArgs,
new.target === bound ? target : new.target
);
}
// 普通调用:固定 this
return target.apply(context, finalArgs);
}
// 让 instance instanceof bound 也成立
if (target.prototype) {
bound.prototype = target.prototype;
}
return bound;
};new.target 能准确判断“是否被 new 调用”,Reflect.construct 也能正确处理 class(class 不能用 apply 调用,会报错)。
版本二:经典 ES5 写法(面试官常期望看到的)
js
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('myBind must be called on a function');
}
var self = this;
var presetArgs = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var args = presetArgs.concat(Array.prototype.slice.call(arguments));
// this instanceof fBound 为 true,说明是 new 调用
return self.apply(this instanceof fBound ? this : context, args);
};
// 通过空函数中转,避免直接 fBound.prototype = self.prototype
// 导致修改 fBound.prototype 时污染原函数原型
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};验证:
js
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () { return this.name; };
const obj = { tag: 'ctx' };
const Bound = Person.myBind(obj, 'Tom');
const p = new Bound(18);
console.log(p.name, p.age); // Tom 18 —— 预置参数生效
console.log(p instanceof Person); // true —— 原型链正确
console.log(p.say()); // Tom
console.log(obj.name); // undefined —— new 时忽略了绑定的 this
function show(a, b) { return [this.tag, a, b]; }
console.log(show.myBind(obj, 1)(2)); // ['ctx', 1, 2]常见错误
- 漏掉
new的处理,只写return self.apply(context, args)。这是最常见的扣分点。 fBound.prototype = self.prototype直接赋值(ES5 版本里),之后往fBound.prototype上加方法会污染原函数。- 只合并调用时的参数,忘了预置参数,或者合并顺序写反。
- 把
this instanceof fBound当成完全可靠的判断:如果把绑定函数挂到它自己的实例上调用(inst.m = fBound; inst.m()),会被误判成new调用。用new.target可以避免这个问题。
口语化答案(约 2 分钟)
call、apply、bind 都是用来显式指定函数 this 的。区别主要有两点:一是执行时机,call 和 apply 是立即执行,bind 不执行,而是返回一个绑定好 this 的新函数;二是传参方式,call 是逐个传参,apply 是传一个数组,bind 也是逐个传,并且支持预置一部分参数,后面调用时再补剩下的,相当于偏函数。
另外有几个细节:bind 只能绑一次,多次 bind 以第一次为准;箭头函数没有自己的 this,这三个方法都改不了它;还有 new 的优先级比 bind 高,对绑定函数 new 的时候,绑定的 this 会失效,this 指向新实例,但预置参数还在。
手写 bind 的话,核心是返回一个闭包函数,里面把预置参数和调用参数合并,再用 apply 调用原函数。难点在于处理 new:如果绑定函数被 new 调用,就不能用绑定的 context,而要让原函数作为构造函数执行。ES5 的写法是用
this instanceof fBound判断,同时用一个空函数中转来继承原型,避免直接共享 prototype 互相污染。现在更推荐用new.target判断、Reflect.construct构造,这样判断更准确,也能兼容 class。实际项目里我基本不会自己实现 bind,原生的更标准,性能也更好;手写主要是为了理解 this 绑定和原型链。
面试官可能的追问
1. 手写版和原生 bind 还有什么差别?
- 原生绑定函数没有
prototype属性,instanceof由引擎通过内部槽[[BoundTargetFunction]]转发判断;手写版只能靠设置prototype模拟。 - 原生的
name是"bound fn",length是max(0, 原函数形参数 - 预置参数个数);手写版需要Object.defineProperty才能对齐。 - 这些差异通常不影响业务,但能说出来,说明你读过规范而不只是背代码。
2. 顺便手写一下 call?
js
Function.prototype.myCall = function (context, ...args) {
// 1. 判断调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError('myCall must be called on a function');
}
// 2. 处理 context:null/undefined 指向全局对象,原始值包装成对象
context = context == null ? globalThis : Object(context);
// 3. 用 Symbol 生成唯一属性名,避免覆盖 context 上已有的属性
const key = Symbol('fn');
// 4. 把原函数(this)临时挂到 context 上
context[key] = this;
// 5. 以 context.方法() 的形式调用,函数内部的 this 就指向 context
const result = context[key](...args);
// 6. 删除临时属性,返回执行结果
delete context[key];
return result;
};验证:
js
const obj = { name: 'Tom' };
function greet(greeting) {
return greeting + ', ' + this.name;
}
greet.myCall(obj, 'Hello'); // "Hello, Tom"口述思路(约 1 分钟):
call 的作用是改变函数的 this 并立即执行。实现的核心是利用隐式绑定规则:一个函数作为对象的方法调用时,比如
obj.fn(),函数里的 this 就是这个对象。所以思路是:先判断调用者是不是函数。然后处理传进来的 context,如果是 null 或 undefined,就指向全局对象;如果是原始值,就用 Object 包装成对象,因为原始值上挂不了属性。
接着把当前函数,也就是 this,临时挂到 context 上作为它的一个属性。属性名用 Symbol 生成,保证唯一,不会覆盖对象原有的属性。然后用
context[key](...args)的方式调用,这时函数内部的 this 就指向了 context,参数用剩余参数收集再展开传入。最后把这个临时属性删掉,避免污染对象,再把执行结果返回。
apply 的实现和它基本一样,区别只是参数是以数组形式传入的。
再追问时可以补充(手写版与原生 call 的差异): 原生 call 是引擎直接设置 this,手写版靠"挂属性再调用"模拟,所以有几处对不齐:
- 严格模式下,原生 call 传原始值时 this 就是原始值本身,手写版会被包装成对象;传
null/undefined时原生 this 保持null/undefined,手写版却指向了globalThis - context 是冻结对象(
Object.freeze)时,属性挂不上去,手写版会报错,原生 call 正常执行 - 调用期间函数内部能通过
Object.getOwnPropertySymbols(this)看到这个临时属性
3. 实际工程中 bind 用在哪里?有什么坑?
- React class 组件里在 constructor 中
this.handleClick = this.handleClick.bind(this),后来多被 class fields 箭头函数替代。要注意不要在 render 里 bind,每次渲染都生成新函数,会让子组件的memo/PureComponent浅比较失效。 addEventListener时用 bind,之后removeEventListener必须传同一个引用,否则移除不掉,造成内存泄漏。这是线上很常见的问题,要先把绑定后的函数存起来。- 回调、定时器里
this丢失:setTimeout(obj.fn, 0)的this不是obj,用 bind 或箭头函数解决。
4. call 和 apply 哪个性能好?
早期引擎里 call 略快,因为 apply 要展开数组。现在差异可以忽略,而且 ES6 之后 fn.apply(ctx, arr) 基本可以被 fn.call(ctx, ...arr) 替代。需要注意的是 apply 传超大数组(如 Math.max.apply(null, hugeArr))可能超出参数个数上限导致栈溢出,大数据量要用循环或 reduce。