📍 第一章:先搞清楚 this 是什么
在讲 call/apply/bind 之前,必须理解 this 的指向规则:
// 1. 默认绑定:独立函数调用,this 指向全局(非严格模式)
function foo() { console.log(this); }
foo(); // window(浏览器)或 global(Node)
// 2. 隐式绑定:作为对象方法调用,this 指向该对象
const obj = { foo };
obj.foo(); // obj
// 3. 显式绑定:call/apply/bind 强制指定 this
foo.call(obj); // obj
// 4. new 绑定:构造函数,this 指向新创建的对象
new foo(); // foo 的实例
call/apply/bind 的作用:强行指定函数的 this,让函数执行时指向你给的对象。
🔧 第二章:call 方法深度拆解
2.1 call 的语法
fn.call(thisArg, arg1, arg2, ...)
2.2 call 的原理
一句话原理:把函数临时挂载到目标对象上执行,执行完再删除。
// 假设我们想让 fn 的 this 指向 obj
const obj = { name: 'obj' };
function fn() { console.log(this.name); }
// 1. 正常的 this 指向
fn(); // undefined(this 指向全局)
// 2. call 的魔法:obj.fn = fn; obj.fn(); delete obj.fn;
// 这就是 call 的底层原理!
2.3 手写实现
Function.prototype.myCall = function(context = window) {
// ----- 第1步:处理 context -----
// 为什么要用 Object()?
// 如果 context 是原始值(如 123、'abc'、null、undefined),需要转成对象
// null/undefined 会被转成空对象,原始值会被包装成对象
context = Object(context);
// 示例:
// myCall(123) → context = Number {123}
// myCall(null) → context = {}
// ----- 第2步:创建唯一属性名 -----
// 为什么要用 Symbol?
// 防止覆盖 context 上已有的属性
// 比如 context 本来就有 fn 属性,直接用 'fn' 会覆盖
const fnSymbol = Symbol('fn');
// ----- 第3步:把函数挂到对象上 -----
// 这里的 this 是谁?是调用 myCall 的那个函数!
// fn.myCall(obj) → this = fn
context[fnSymbol] = this;
// ----- 第4步:处理参数 -----
// arguments 是所有参数的类数组对象
// fn.myCall(obj, 1, 2, 3) → arguments = [obj, 1, 2, 3]
const args = Array.from(arguments).slice(1);
// slice(1) 去掉第一个参数(context)
// ----- 第5步:执行函数 -----
// 判断是否有参数,用扩展运算符传参
const result = args.length
? context[fnSymbol](...args) // 有参数
: context[fnSymbol](); // 无参数
// ----- 第6步:清理临时属性 -----
// 用完就删,保持对象原样
delete context[fnSymbol];
// ----- 第7步:返回结果 -----
return result;
};
2.4 执行过程可视化
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
return 'done';
}
const person = { name: '张三' };
// 调用
greet.myCall(person, 'Hello');
// 执行过程:
// 第1步:context = Object(person) → { name: '张三' }
// 第2步:fnSymbol = Symbol('fn')
// 第3步:context[fnSymbol] = greet
// person 变成了:{ name: '张三', [Symbol(fn)]: greet }
// 第4步:args = ['Hello']
// 第5步:执行 context[fnSymbol]('Hello') → this 指向 person
// 第6步:delete context[fnSymbol] → person 恢复原样:{ name: '张三' }
// 第7步:返回 'done'
2.5 边界情况处理
// 情况1:context 是原始值
function test() { console.log(this); }
test.myCall(123); // Number {123} ✅
// 情况2:context 是 null/undefined
test.myCall(null); // {} ✅(非严格模式下转成全局对象)
// 情况3:函数有返回值
function add(a, b) { return a + b; }
console.log(add.myCall(null, 1, 2)); // 3 ✅
// 情况4:无参数
function logThis() { console.log(this); }
logThis.myCall(); // window ✅
📊 第三章:apply 方法深度拆解
3.1 apply 与 call 的唯一区别
// call:参数列表
fn.call(obj, 1, 2, 3);
// apply:参数数组
fn.apply(obj, [1, 2, 3]);
3.2 手写实现
Function.prototype.myApply = function(context = window, args = []) {
// ----- 第1步:处理 context -----
context = Object(context);
// ----- 第2步:创建唯一属性名 -----
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
// ----- 第3步:执行函数(唯一区别在这里)-----
// args 是数组,直接用扩展运算符展开
const result = args.length
? context[fnSymbol](...args)
: context[fnSymbol]();
// ----- 第4步:清理 -----
delete context[fnSymbol];
return result;
};
3.3 为什么要有 apply?
// 场景1:处理类数组对象
function sum() {
// arguments 是类数组,不能直接用数组方法
// 用 apply 传参
const nums = Array.prototype.slice.apply(arguments);
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
// 场景2:配合 Math.max/min
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// ES6 可以用扩展运算符:Math.max(...numbers)
// 场景3:合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4]
🔗 第四章:bind 方法深度拆解
4.1 bind 的核心特性
fn.bind(thisArg, arg1, arg2, ...)
三大特性:
-
不立即执行:返回一个新函数
-
永久绑定 this:bind 后的函数 this 不能再用 call/apply 改变
-
支持柯里化:可以预置参数
4.2 基础版实现
Function.prototype.myBind = function(context = window, ...boundArgs) {
// 保存原函数
const originalFn = this;
// 返回新函数
return function(...callArgs) {
// 合并参数:bind 时传的 + 调用时传的
const allArgs = [...boundArgs, ...callArgs];
// 用 apply 改变 this
return originalFn.apply(context, allArgs);
};
};
// 测试
function introduce(hobby, age) {
console.log(`我是${this.name},喜欢${hobby},今年${age}岁`);
return 'done';
}
const person = { name: '李四' };
const boundIntroduce = introduce.myBind(person, '编程');
const result = boundIntroduce(18);
// 输出: 我是李四,喜欢编程,今年18岁
console.log(result); // done
4.3 进阶:考虑 new 的情况(完整版)
如果 bind 返回的函数被 new 调用,this 应该指向新创建的对象。
Function.prototype.myBind = function(context = window, ...boundArgs) {
const originalFn = this;
// 返回的函数
function boundFunction(...callArgs) {
const allArgs = [...boundArgs, ...callArgs];
// 关键判断:是否通过 new 调用
// this instanceof boundFunction 为 true 说明用了 new
const isNewCall = this instanceof boundFunction;
// 如果是 new 调用,this 指向新对象;否则指向绑定的 context
return originalFn.apply(
isNewCall ? this : context,
allArgs
);
}
// 维护原型链:让返回的函数继承原函数的原型
// 这样 new boundFunction() 创建的对象才能继承 originalFn.prototype
boundFunction.prototype = Object.create(originalFn.prototype);
return boundFunction;
};
4.4 new 场景测试
function Person(name, age) {
this.name = name;
this.age = age;
console.log('构造函数执行了');
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
// bind 预置 name
const BoundPerson = Person.myBind(null, '王五');
// 用 new 调用
const p = new BoundPerson(25);
console.log(p); // Person { name: '王五', age: 25 } ✅
p.sayHi(); // Hi, I'm 王五 ✅(原型链也保留了)
// 如果不处理 new 的情况:
// p 会是 {},name/age 都挂到了 BoundPerson 上,原型链也断了 ❌
4.5 bind 的特性验证
// 特性1:永久绑定(一旦 bind,不能再用 call/apply 改变)
function fn() { console.log(this.name); }
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const boundFn = fn.bind(obj1);
boundFn(); // obj1 ✅
// 尝试用 call 改变
boundFn.call(obj2); // 还是 obj1 ✅(bind 优先级最高)
// 特性2:支持柯里化(预置参数)
function add(a, b, c) {
return a + b + c;
}
const add5 = add.bind(null, 5); // 预置 a = 5
const add5And10 = add5.bind(null, 10); // 预置 b = 10
console.log(add5And10(15)); // 5 + 10 + 15 = 30
// 特性3:this 优先级
// new > bind > call/apply > 隐式绑定 > 默认绑定
🎯 第五章:三者的优先级关系
// this 绑定优先级(从高到低)
// 1. new 绑定
// 2. bind 绑定
// 3. call/apply 绑定
// 4. 隐式绑定(对象.方法)
// 5. 默认绑定(独立调用)
function test() { console.log(this.name); }
const obj = { name: 'obj' };
const obj2 = { name: 'obj2' };
// bind 优先级 > call/apply
const bound = test.bind(obj);
bound.call(obj2); // obj(不是 obj2) ✅
// new 优先级 > bind
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: 'bindObj' });
const p = new BoundPerson('newObj');
console.log(p.name); // newObj(不是 bindObj)✅
💡 第六章:常见题解析
实现一个可以链式调用的 call
// 题目:让 fn.call.call(obj) 这种写法生效
function fn() { console.log(this); }
// 解析:fn.call.call(obj) 等价于
// (fn.call).call(obj)
// 即 Function.prototype.call 作为函数被调用
// 理解:
// fn.call 本身是一个函数(Function.prototype.call)
// .call(obj) 把 fn.call 的 this 指向 obj
// 所以执行的是 obj 上的 call 方法
bind 之后的函数 length 属性
function fn(a, b, c) {}
console.log(fn.length); // 3
const bound = fn.bind(null, 1);
console.log(bound.length); // 2(预置了一个参数,剩余 2 个)
// 原理:bind 返回的函数的 length = 原函数 length - 预置参数个数
实现函数的柯里化
// 用 bind 实现
function curry(fn, ...args) {
return fn.length <= args.length
? fn(...args)
: curry.bind(null, fn, ...args);
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
📝 第七章:完整代码汇总
// call 完整实现
Function.prototype.myCall = function(context = window) {
context = Object(context);
const fnSymbol = Symbol();
context[fnSymbol] = this;
const args = Array.from(arguments).slice(1);
const result = args.length ? context[fnSymbol](...args) : context[fnSymbol]();
delete context[fnSymbol];
return result;
};
// apply 完整实现
Function.prototype.myApply = function(context = window, args = []) {
context = Object(context);
const fnSymbol = Symbol();
context[fnSymbol] = this;
const result = args.length ? context[fnSymbol](...args) : context[fnSymbol]();
delete context[fnSymbol];
return result;
};
// bind 完整实现(含 new 处理)
Function.prototype.myBind = function(context = window, ...boundArgs) {
const originalFn = this;
function boundFunction(...callArgs) {
const allArgs = [...boundArgs, ...callArgs];
return originalFn.apply(
this instanceof boundFunction ? this : context,
allArgs
);
}
boundFunction.prototype = Object.create(originalFn.prototype);
return boundFunction;
};
🎓 第八章:总结对比
| 特性 |
call |
apply |
bind |
| 执行时机 |
立即 |
立即 |
延迟 |
| 参数形式 |
列表 |
数组 |
列表(可分批) |
| 返回值 |
函数结果 |
函数结果 |
新函数 |
| this 永久性 |
一次性 |
一次性 |
永久 |
| 柯里化 |
不支持 |
不支持 |
支持 |
| new 调用 |
无效 |
无效 |
有效(原函数可被 new) |
| 实现难点 |
Symbol 防冲突 |
参数数组处理 |
new 判断 + 原型链 |
call 是立即执行+参数列表,apply 是立即执行+参数数组,bind 是返回新函数+永久绑定+支持柯里化