call/apply/bind 详解
如何手动控制 this 绑定?bind 的“硬绑定”与“柯里化”特性
当你写下:
func.call(obj, 1, 2);你不是在“调用函数”,
而是在劫持函数的执行上下文,
主动干预 ECMAScript 引擎的 [[Call]] 协议。
我们来从规范协议、执行上下文、硬绑定机制、柯里化原理四个维度,彻底拆解:
一、前置知识:this 的本质是“动态绑定”
在 ECMA-262 §6.2.4.1 中:
this 是执行上下文(Execution Context)的一个组件,称为 ThisBinding。
它的值由调用方式决定,而非函数定义位置。
四种绑定规则:
- 默认绑定:
func()→this = global/undefined(严格模式) - 隐式绑定:
obj.method()→this = obj - 硬绑定:
func.call(obj)→this = obj new绑定:new C()→this = 新对象
而 call、apply、bind 就是手动触发“硬绑定”的 API。
二、call 与 apply:立即执行的“硬绑定”
语法对比
| 方法 | 语法 | 参数形式 |
|---|---|---|
call | func.call(thisArg, arg1, arg2, ...) | 明确参数列表 |
apply | func.apply(thisArg, [argsArray]) | 参数数组 |
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) 时,引擎执行:
// 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.namecall vs apply:性能与使用场景
| 维度 | call | apply |
|---|---|---|
| 性能 | 略快(无数组解构) | 稍慢(需处理数组) |
| 适用场景 | 参数明确 | 参数动态(如 Math.max.apply(null, arr)) |
| 现代替代 | func.bind(obj)(...args) | Math.max(...arr)(展开语法) |
推荐:
- 固定参数用
call - 数组参数优先用
...args(更现代)
三、bind:创建“硬绑定”的新函数
基本用法
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() 时:
// 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 无法再被 call 或 apply 覆盖!
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };
const bound = func.bind(obj1);
bound.call(obj2); // this 仍然是 obj1!这就是“硬绑定”的含义:this 被冻结在 [[BoundThis]] 中。
四、bind 的“柯里化”(Currying)特性
bind 的第二个能力是参数预设,实现函数柯里化。
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这等价于:
const add5 = (b, c) => add(5, b, c);但 bind 更高效,因为它直接在内部槽中存储了 5,无需闭包。
柯里化 vs 闭包:性能差异
| 方式 | 实现 | 性能 | 说明 |
|---|---|---|---|
bind | add.bind(null, 5) | ⚡ 更快 | 内部槽存储,无闭包 |
| 闭包 | (b,c) => add(5,b,c) | 稍慢 | 创建新函数 + 闭包环境 |
V8 对 bind 的柯里化有优化路径。
五、bind 的经典应用:事件处理器与 setTimeout
1. 修复 this 丢失问题
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
setTimeout(this.onClick.bind(this), 1000);否则 this 会指向 window。
六、bind 的“不可逆性”:一旦绑定,永不改变
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 也无法覆盖)bind 是 this 绑定的“终极形态”——一旦锁定,永不释放。
七、手写 bind:模拟引擎行为
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 调用的特殊处理(返回新对象,但保留预设参数)
一句话总结
call 和 apply 是“立即硬绑定”,bind 是“延迟硬绑定 + 柯里化”。
- 它们通过干预
[[Call]]协议,手动控制this绑定 bind创建的函数具有[[BoundTargetFunction]]、[[BoundThis]]、[[BoundArguments]]三个内部槽- 一旦绑定,
this无法被call、apply或new覆盖 bind是修复this丢失、实现柯里化的关键工具