Skip to content

call/apply/bind 详解

如何手动控制 this 绑定?bind 的“硬绑定”与“柯里化”特性

当你写下:

js
func.call(obj, 1, 2);

你不是在“调用函数”,
而是在劫持函数的执行上下文
主动干预 ECMAScript 引擎的 [[Call]] 协议。

我们来从规范协议、执行上下文、硬绑定机制、柯里化原理四个维度,彻底拆解:

一、前置知识:this 的本质是“动态绑定”

ECMA-262 §6.2.4.1 中:

this 是执行上下文(Execution Context)的一个组件,称为 ThisBinding
它的值由调用方式决定,而非函数定义位置。

四种绑定规则:

  1. 默认绑定func()this = global / undefined(严格模式)
  2. 隐式绑定obj.method()this = obj
  3. 硬绑定func.call(obj)this = obj
  4. new 绑定new C()this = 新对象

callapplybind 就是手动触发“硬绑定”的 API

二、callapply:立即执行的“硬绑定”

语法对比

方法语法参数形式
callfunc.call(thisArg, arg1, arg2, ...)明确参数列表
applyfunc.apply(thisArg, [argsArray])参数数组
js
function greet(a, b) {
  console.log(this.name, a, b);
}

const obj = { name: 'Alice' };

greet.call(obj, 1, 2);     // Alice 1 2
greet.apply(obj, [1, 2]);  // Alice 1 2

规范行为(来自 §19.2.3.3)

当调用 func.call(thisArg, ...args) 时,引擎执行:

js
// 1. 检查 func 是否有 [[Call]]
if (typeof func !== 'function') throw TypeError;

// 2. 将 thisArg 转换为实际的 this 值
let thisValue = ToThisEnvironment(thisArg);

// 3. 调用 func 的 [[Call]] 方法,传入 thisValue 和 args
return func.[[Call]](thisValue, args);

call 的本质:在调用时,临时替换 this 绑定。

图解 call 的执行上下文切换

全局上下文

func.call(obj, 1, 2)

创建 func 的执行上下文
  ├── [[ThisBinding]]: obj          ← 被 call 强制设置
  ├── [[LexicalEnvironment]]: func 的词法环境
  └── 参数: a=1, b=2

执行 func 体
  → this.name → obj.name

call vs apply:性能与使用场景

维度callapply
性能略快(无数组解构)稍慢(需处理数组)
适用场景参数明确参数动态(如 Math.max.apply(null, arr)
现代替代func.bind(obj)(...args)Math.max(...arr)(展开语法)

推荐:

  • 固定参数用 call
  • 数组参数优先用 ...args(更现代)

三、bind:创建“硬绑定”的新函数

基本用法

js
const boundFunc = func.bind(thisArg, arg1, arg2);
boundFunc(arg3, arg4); // 实际调用 func(thisArg, arg1, arg2, arg3, arg4)

bind 不立即执行,而是返回一个新函数,该函数的 this 被永久锁定。

bind 的“硬绑定”机制

bind 的核心是创建一个 Bound Function Exotic Object(§10.4.1)。

它有三个内部槽:

  • [[BoundTargetFunction]] → 原始函数
  • [[BoundThis]] → 被绑定的 this
  • [[BoundArguments]] → 预设参数

当调用 boundFunc() 时:

js
// 1. 获取原始函数
let target = boundFunc.[[BoundTargetFunction]];

// 2. 获取绑定的 this 和参数
let thisArg = boundFunc.[[BoundThis]];
let args = boundFunc.[[BoundArguments]] + 实际传入参数;

// 3. 调用 target.[[Call]](thisArg, args)
return target.[[Call]](thisArg, args);

bind 创建的函数,其 this 无法再被 callapply 覆盖!

js
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

const bound = func.bind(obj1);
bound.call(obj2); // this 仍然是 obj1!

这就是“硬绑定”的含义:this 被冻结在 [[BoundThis]]

四、bind 的“柯里化”(Currying)特性

bind 的第二个能力是参数预设,实现函数柯里化。

js
function add(a, b, c) {
  return a + b + c;
}

const add5 = add.bind(null, 5);        // 固定 a=5
const result = add5(10, 15);           // 5 + 10 + 15 = 30

这等价于:

js
const add5 = (b, c) => add(5, b, c);

bind 更高效,因为它直接在内部槽中存储了 5,无需闭包。

柯里化 vs 闭包:性能差异

方式实现性能说明
bindadd.bind(null, 5)⚡ 更快内部槽存储,无闭包
闭包(b,c) => add(5,b,c)稍慢创建新函数 + 闭包环境

V8 对 bind 的柯里化有优化路径。

五、bind 的经典应用:事件处理器与 setTimeout

1. 修复 this 丢失问题

js
class Button {
  constructor() {
    this.text = 'Click me';
    this.el = document.getElementById('btn');
    // ❌ this 指向 DOM 元素
    // this.el.addEventListener('click', this.onClick);
    
    // ✅ 用 bind 硬绑定
    this.el.addEventListener('click', this.onClick.bind(this));
  }
  
  onClick() {
    console.log(this.text); // 必须绑定,否则 undefined
  }
}

2. setTimeout 中的 this

js
setTimeout(this.onClick.bind(this), 1000);

否则 this 会指向 window

六、bind 的“不可逆性”:一旦绑定,永不改变

js
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

const bound = func.bind(obj1);
bound.call(obj2);     // this = obj1
bound.apply(obj2);    // this = obj1
new bound();          // this = obj1(即使 new 也无法覆盖)

bindthis 绑定的“终极形态”——一旦锁定,永不释放。

七、手写 bind:模拟引擎行为

js
Function.prototype.myBind = function(thisArg, ...presetArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind called on non-function');
  }

  const target = this;

  const bound = function(...args) {
    // 判断是否被 new 调用
    if (new.target) {
      // 被 new 调用:忽略 thisArg,创建新对象
      return Reflect.construct(target, [...presetArgs, ...args], bound);
    } else {
      // 普通调用:使用绑定的 this
      return target.apply(thisArg, [...presetArgs, ...args]);
    }
  };

  // 继承原型(可选,用于 new bound() 时)
  bound.prototype = Object.create(target.prototype);

  return bound;
};

这个 myBind 实现了: 硬绑定 柯里化 new 调用的特殊处理(返回新对象,但保留预设参数)

一句话总结

callapply 是“立即硬绑定”,bind 是“延迟硬绑定 + 柯里化”。

  • 它们通过干预 [[Call]] 协议,手动控制 this 绑定
  • bind 创建的函数具有 [[BoundTargetFunction]][[BoundThis]][[BoundArguments]] 三个内部槽
  • 一旦绑定,this 无法被 callapplynew 覆盖
  • bind 是修复 this 丢失、实现柯里化的关键工具