call、apply、bind 原理与实现

📍 第一章:先搞清楚 this 是什么

在讲 call/apply/bind 之前,必须理解 this 的指向规则:

javascript 复制代码
// 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 的语法

javascript 复制代码
fn.call(thisArg, arg1, arg2, ...)

2.2 call 的原理

一句话原理:把函数临时挂载到目标对象上执行,执行完再删除。

javascript 复制代码
// 假设我们想让 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 手写实现

javascript 复制代码
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 执行过程可视化

javascript 复制代码
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 边界情况处理

javascript 复制代码
// 情况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 的唯一区别

javascript 复制代码
// call:参数列表
fn.call(obj, 1, 2, 3);

// apply:参数数组
fn.apply(obj, [1, 2, 3]);

3.2 手写实现

javascript 复制代码
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?

javascript 复制代码
// 场景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 的核心特性

javascript 复制代码
fn.bind(thisArg, arg1, arg2, ...)

三大特性

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

4.2 基础版实现

javascript 复制代码
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 应该指向新创建的对象

javascript 复制代码
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 场景测试

javascript 复制代码
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 的特性验证

javascript 复制代码
// 特性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 > 隐式绑定 > 默认绑定

🎯 第五章:三者的优先级关系

javascript 复制代码
// 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

javascript 复制代码
// 题目:让 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 属性

javascript 复制代码
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 - 预置参数个数

实现函数的柯里化

javascript 复制代码
// 用 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

📝 第七章:完整代码汇总

javascript 复制代码
// 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 是返回新函数+永久绑定+支持柯里化

相关推荐
雨落Re1 小时前
从设计到开发,过年我用十天使用AI搭建了一个完整的博客系统
前端·后端
冴羽2 小时前
100s 带你了解 Bun 为什么这么火
前端·node.js·bun
Sylvia33.2 小时前
火星数据:解构斯诺克每一杆进攻背后的数字语言
java·前端·python·数据挖掘·数据分析
Wect2 小时前
LeetCode 530. 二叉搜索树的最小绝对差:两种解法详解(迭代+递归)
前端·算法·typescript
掘金一周2 小时前
2026 春晚魔术大揭秘:作为程序员,分分钟复刻一个 | 掘金一周 2.26
前端·人工智能·后端
兴芳2 小时前
Tencent Cloud Captcha
前端
一个假的前端男2 小时前
# 从零开始创建 Flutter Web 项目(附 VS Code 插件推荐)
前端·flutter·react.js
卸任2 小时前
Windows判断是笔记本还是台式
前端·react.js·electron