Skip to content

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 ​

需要满足四点:

  1. 返回一个新函数,this 固定为 context
  2. 支持预置参数 + 调用时参数合并
  3. 被 new 调用时,忽略绑定的 this,以原函数为构造函数创建实例
  4. 实例能通过 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。

个人面试题归档,持续更新中