DrillLab
第 38 / 105 道38 / 105 · #304

call、apply、bind 的区别

What are the differences between call, apply & bind

先自己答,再往下看Answer it yourself first

一句话:三个都是改this。call 和 apply立即执行,bind 返回一个新函数;call 参数一个个传,apply 传数组。

是否立即执行参数形式
fn.call(ctx, a, b)立即逐个(Comma)
fn.apply(ctx, [a, b])立即数组(Array)
fn.bind(ctx, a)不执行,返回新函数逐个,且可以只绑一部分

记法:Apply 收 Array,Call 用 Comma。

bind 的两个额外性质(追问点):

  • 能预置参数—— 所以它就是偏函数(见 #299)。
  • 绑过一次就锁死了—— 再 bind 或 call都改不回来。但 new 能突破它,因为 new 优先级最高。

还会追问:「apply 现在还有用吗?」—— 展开语法出来后大部分被fn(...args) 取代了。但转发不定参数时还常用:fn.apply(this, args)(防抖里就是这么写的,因为要同时转发this 和 args)。

In one line: all three change this. call and apply run the function right away, bind hands you back a new one; call takes its arguments one by one, apply takes an array.

Runs immediately?Argument form
fn.call(ctx, a, b)YesOne by one (Comma)
fn.apply(ctx, [a, b])YesAn array (Array)
fn.bind(ctx, a)No — you get a new functionOne by one, and you may bind only some of them

How to remember it: Apply takes an Array, Call takes Commas.

Two extra properties of bind they will probe:

  • It can preset arguments — which makes it partial application (see #299).
  • Bind once and it is locked — another bind or a call cannot change it back. But new breaks through, because new has the highest priority.

Another follow-up: “Is apply still useful?” — spread syntax replaced most of it with fn(...args). It is still the normal way to forward an unknown argument list: fn.apply(this, args) — that is how debounce is written, because you have to forward this and args at the same time.

JavaScript三者对比与手写 bindThe three compared, and bind written by hand示意Illustrative
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // "你好, 小明!"
7greet.apply(who, ["你好", "!"]); // 同上,参数是数组
8const hi = greet.bind(who, "你好"); // 返回新函数,还预置了第一个参数
9hi("?"); // "你好, 小明?"
10
11// 手写一个 bind(高频现场题)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 function bound(...args) {
15 // new 调用时 this 是新对象,此时不该用 ctx —— 这是 bind 的规范行为
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 }
19 // 让 new 出来的实例继承原函数的 prototype(箭头函数没有 prototype,跳过)
20 if (fn.prototype) bound.prototype = Object.create(fn.prototype);
21 return bound;
22};
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // the greeting, then the name, then the mark
7greet.apply(who, ["你好", "!"]); // the same, but the arguments arrive as an array
8const hi = greet.bind(who, "你好"); // returns a new function with the first argument preset
9hi("?"); // the same string, ending in a question mark
10
11// Write bind by hand (a very common live-coding question)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 function bound(...args) {
15 // With new, this is the new object and ctx must be ignored — the spec requires it
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 }
19 // instances made with new inherit from the original prototype (arrow functions have none)
20 if (fn.prototype) bound.prototype = Object.create(fn.prototype);
21 return bound;
22};
手写 bind 的加分项是 new 的两件事:new 调用时忽略绑定的 this(isNew 判断),并且让实例继承原函数的 prototype(bound.prototype 那一行)。多数人会漏。What earns extra credit when you write bind by hand is the two things new needs: ignore the bound this under new (the isNew check), and make the instance inherit from the original function's prototype (the bound.prototype line). Most people leave them out.