call、apply、bind 原理与实现

17 阅读6分钟

📍 第一章:先搞清楚 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, ...)

三大特性

  1. 不立即执行:返回一个新函数
  2. 永久绑定 this:bind 后的函数 this 不能再用 call/apply 改变
  3. 支持柯里化:可以预置参数

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;
};

🎓 第八章:总结对比

特性callapplybind
执行时机立即立即延迟
参数形式列表数组列表(可分批)
返回值函数结果函数结果新函数
this 永久性一次性一次性永久
柯里化不支持不支持支持
new 调用无效无效有效(原函数可被 new)
实现难点Symbol 防冲突参数数组处理new 判断 + 原型链

call 是立即执行+参数列表,apply 是立即执行+参数数组,bind 是返回新函数+永久绑定+支持柯里化