算法 - 手写 call / apply / bind

1. 题目

Function.prototype 上实现三个方法(挂到自定义名字以免污染):

  • myCall(thisArg, ...args):以 thisArgthis、逐个传参,立即调用函数。
  • myApply(thisArg, argsArray):同上,但参数以数组形式传入。
  • myBind(thisArg, ...partialArgs):返回一个新函数,绑定 this 并可柯里化预置部分参数;新函数被 new 时,this 绑定失效(指向新建实例),但预置参数仍生效。

要求处理 thisArg 为原始值(应装箱/或在全局环境忽略)、null/undefined(指向全局)等边界。

2. 解题思路

三者核心都是一句话:让函数以某个对象作为 this 来执行。技巧是把函数临时设为该对象的属性,通过 obj.fn() 调用,此时 this 自然指向 obj,调用完删除临时属性。

2.1 call / apply

  1. thisArgnull/undefined 时用全局对象(浏览器 globalThis)。
  2. 把原始值 Object(thisArg) 装箱成对象(严格模式下 call 传入原始值 this 仍是原始值,这里按非严格近似)。
  3. 用唯一的 Symbol 作为临时属性名,避免与已有 key 冲突。
  4. 调用后 delete 掉临时属性。
  5. apply 只是把数组参数展开传入。

2.2 bind

bind 不立即执行,而是返回一个 boundFunction

  • 普通调用时,this 固定为绑定对象,且把预置参数和调用时参数拼接(柯里化)。

  • new 调用 boundFunction 时,ES 规范要求忽略绑定的 this,创建全新实例,但预置参数仍前置。实现关键:new bind(Fn) 时原型链上 boundFunction.prototype === Fn.prototype,因此判断 this instanceof boundFunction 来决定用 thisArg 还是新建对象。

  • 时间复杂度:调用时才确定,均 O(参数)

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// myCall
Function.prototype.myCall = function (this: Function, thisArg?: any, ...args: any[]): any {
const ctx = thisArg == null ? globalThis : Object(thisArg);
const key = Symbol("fn");
(ctx as any)[key] = this; // this 即调用者函数
const result = (ctx as any)[key](...args);
delete (ctx as any)[key];
return result;
};

// myApply
Function.prototype.myApply = function (this: Function, thisArg?: any, argsList?: any[]): any {
const ctx = thisArg == null ? globalThis : Object(thisArg);
const key = Symbol("fn");
(ctx as any)[key] = this;
const result = argsList
? (ctx as any)[key](...argsList)
: (ctx as any)[key]();
delete (ctx as any)[key];
return result;
};

// myBind
Function.prototype.myBind = function (this: Function, thisArg?: any, ...boundArgs: any[]): Function {
const original = this;

// 用一个空函数承接原型链,保证 new bound() 能拿到 original.prototype
const bound = function (this: any, ...callArgs: any[]): any {
const allArgs = [...boundArgs, ...callArgs]; // 柯里化:预置参数在前
// 被 new 调用时 this 是新建实例,忽略绑定的 thisArg
const wasNew = new.target !== undefined;
if (wasNew) {
return new (original as any)(...allArgs);
}
return original.apply(thisArg, allArgs);
};

if (original.prototype) {
bound.prototype = Object.create(original.prototype);
}
return bound;
};

// 测试
const obj = { name: "jack" };
function greet(this: any, a: string, b: string) {
return `${a} ${this.name} ${b}`;
}

console.log(greet.myCall(obj, "hi", "!")); // "hi jack !"
console.log(greet.myApply(obj, ["hello", "?"])); // "hello jack ?"

const boundGreet = greet.myBind(obj, "hey");
console.log(boundGreet("there")); // "hey jack there"

4. 面试延伸

  • bind 的 new 语义是本题最大的区分点:很多人 bind 只会 apply,答不出 new.target 判断与 boundFunction.prototype 的桥接,原型链一断 instanceof 就错。
  • Symbol 临时属性名:用 Symbol()fn_${Date.now()} 避免覆盖对象上同名方法,比直接写 ctx.fn 严谨。
  • 严格模式差异:严格模式下 call(1)this 就是 1 而非装箱对象,Object(thisArg) 是近似;能点出这条即显深度。
  • 三者关系:call/apply 只传参方式不同、都立即执行;bind 返回新函数且能柯里化,底层常复用 apply
  • 关联:instanceof 手写见下一篇 [手写 instanceof],同属「理解原型与 this」的必考组合。

难度:中等 | 手写原型三件套 | this 绑定机制