JavaScript 一面:call / apply / bind 用法与区别

在 JavaScript 一面里,call / apply / bind 基本属于「必考清单」。
这一篇不打算背参数签名,而是想搞清楚几件事:这三个方法到底在解决什么问题、它们之间具体有什么差异,以及在手写题和场景题里各自怎么用。

可以先把 call / apply / bind 放在同一个背景下理解:

  • JavaScript 里的 this 不是在定义阶段就决定好的,而是在调用时由调用方式决定
  • 很多时候,我们希望:
    • 临时把某个函数「借给」另一个对象用;
    • 或者手动把 this 绑定到一个固定对象上,而不是由调用点自然决定。

Function.prototype.call / apply / bind 做的事,本质上只有一件:

  • 显式指定函数执行时的 this,并控制参数传递方式和调用时机。

在这个视角下,再看它们三个的区别,会清晰很多。

先给一个面试时可以快速说出来的对比:

  • fn.call(thisArg, arg1, arg2, ...)
    • 立即调用 fnthis 指向 thisArg,参数「按顺序逐个列出」。
  • fn.apply(thisArg, [arg1, arg2, ...])
    • 立即调用 fnthis 指向 thisArg,参数「以数组形式传入」。
  • fn.bind(thisArg, arg1, arg2, ...)
    • 不立即调用,返回一个新的函数:
      • 这个新函数内部的 this 永远指向 thisArg
      • 前置参数会被「预先绑定」,后续调用时再补充剩余参数。

用一句简化版总结:

  • call / apply:改 this + 立刻执行
  • bind:改 this + 返回一个「稍后再执行」的新函数

签名大致可以理解为:

1func.call(thisArg, arg1, arg2, ...);

典型场景有两个:

  • 1)函数借用
1function greet(greeting) {
2  console.log(greeting + ', ' + this.name);
3}
4
5const user = { name: 'Alice' };
6greet.call(user, 'Hello'); // Hello, Alice
  • 2)类数组借用数组方法(ES6 之前常见)
1function demo() {
2  const args = Array.prototype.slice.call(arguments);
3  console.log(args);
4}
  • 「如何用 call 改变函数执行时的 this?」
  • 「怎么用 call 借用数组方法处理 arguments?」

答题时,给出一句解释 + 一个简短示例即可,不需要堆很多 API 细节。

签名大致可以理解为:

1func.apply(thisArg, [arg1, arg2, ...]);

call 的唯一区别在于 参数的传递方式

  • call:一个个列出来;
  • apply:把参数放进一个数组里。
  • 1)参数本来就在数组里
1const nums = [1, 2, 3];
2
3function sum(a, b, c) {
4  return a + b + c;
5}
6
7const result = sum.apply(null, nums); // 6
  • 2)和内置函数配合(如 Math.max)
1const arr = [3, 9, 1];
2const max = Math.max.apply(null, arr); // 9

(在有展开运算符之后,Math.max(...arr) 更常见,但一面题里仍会以 apply 为切入点考 this 和参数传递。)

高频问题通常是:

  • callapply 的区别是什么?」
  • 答:都是修改 this 并立即执行,区别只在于参数是「逐个传」还是「数组传」。

签名可以理解为:

1const newFn = func.bind(thisArg, arg1, arg2, ...);
2// 之后再通过 newFn(...) 执行

特点:

  • 立刻返回一个新函数,不立即执行。
  • 这个新函数内部的 this 已经被绑定到 thisArg,后续不会被普通调用方式改变。
  • 传给 bind 的参数会作为前置参数,后续调用时再补充剩余参数。
  • 1)配合事件监听,固定 this 和部分参数
1const obj = {
2  x: 42,
3  handleClick(y) {
4    console.log(this.x, y);
5  },
6};
7
8const btn = document.querySelector('button');
9btn.addEventListener('click', obj.handleClick.bind(obj, 'clicked'));
  • 2)模拟函数柯里化效果
1function add(a, b) {
2  return a + b;
3}
4
5const addFive = add.bind(null, 5);
6console.log(addFive(3)); // 8

可以用一句话概括:

  • call/apply 是「当场执行」,bind 是「生成一个预设 this 和部分参数的新函数,等以后再执行」。

在一面里,面试官更看重你能不能说出「返回新函数」这个关键点,以及知道常见的事件绑定和柯里化场景。

很多公司会要求你「简单实现一个 bind」,或者「实现一个简化版 call / apply」。这里不展开完整代码,只讲关键思路,方便你在面试时组织答案。

  • 如果 thisArg 不是对象(或者是 null / undefined),要把它转成对应的包装对象或默认指向全局(严格模式有差异)。
  • 暂时在 thisArg 上挂一个函数属性,指向当前要调用的函数。
  • 用传入的参数调用这个临时方法,拿到返回值。
  • 删除临时属性,返回结果。

答题时,可以用口头版描述:

  • 「我会把目标函数临时挂到 thisArg 上执行一次,然后删除这个属性,这样 this 指向的就是 thisArg 了。」

bind 的考点略多一些,关键点包括:

  • 返回的是一个新函数,而不是直接执行。
  • 需要保存:
    • 绑定的 this(thisArg);
    • 绑定时传入的「前置参数」。
  • 新函数被调用时:
    • 再次接收一批「后续参数」;
    • 把前置参数 + 后续参数拼接起来,一起用 callapply 调用原函数。
  • 更完整的实现还会处理 new 调用时的情况,但一面里不一定要求到这个深度。

除了直接问区别,call/apply/bind 还常作为 this 题的配角出现。

可以给出几种做法,然后重点讲 bind

  • 传统做法:const that = this;,在内部用 that
  • 箭头函数:箭头函数不绑定自己的 this,继承自定义时环境。
  • bind:在传入回调前,用 fn.bind(obj) 固定 this。

示例性回答:

  • 「如果希望在回调中始终指向某个对象,我会在传入回调时就用 bind 绑定 this,比如 btn.addEventListener('click', this.handleClick.bind(this))。」

典型就是:

  • Array.prototype.forEach.call(nodeList, callback) 之类的写法。

答题时可以说:

  • 「我会直接用 Array.prototype.someMethod.call(obj, ...),把 obj 当成 this 来借用数组方法。」

可以准备一个自己顺口的回答模板,大致包含这几个点:

  • 先用一句话说明它们的共同点:
    • 「都是用来显式指定函数执行时的 this。」
  • 再用一句话分别说区别:
    • callapply 都是修改 this 后立即执行,区别在于参数是一个个给还是用数组给;bind 则是不执行,返回一个绑定了 this 和部分参数的新函数。」
  • 最后补上 1–2 个场景:
    • 「比如借用数组方法处理类数组,可以用 call/apply;在事件回调或异步回调里需要固定 this 时,我会用 bind 或箭头函数。」

按照这个框架去答,大多数关于 call/apply/bind 的一面问题都能比较完整地覆盖到。