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

相关推荐
Fly7 分钟前
我来提需求,你带着 Codex 做项目:AI 全栈课程开始实战了
前端
集智飞行26 分钟前
解决mavros2 ros2版本cpu占用高的问题
java·服务器·前端
HSunR1 小时前
ruoyi 若依 自定义注解 参数校验
java·前端·数据库
拆房老料2 小时前
ONLYOFFICE也能像Microsoft Word和WPS一样分别设置中西文字体
前端·html·word·开源软件·wps
Solis程序员3 小时前
Stripe 幂等表:把「不确定的重试」变成「查表返回」
前端·安全·bootstrap·php·agent
李纲明3 小时前
WordPress 站点变慢:按 TTFB → 缓存 → 查询 → 前端 分层排查(含配置注意)
前端·缓存·性能优化·wordpress·后端开发
程序人生8883 小时前
PDF 转 Word 版式错乱、表格丢失?DocConverter Web 格式互转引擎的保真实践
前端·人工智能·opencv·机器学习·pdf·word
粥里有勺糖4 小时前
安利一下最近用的桌面“Agent” | T3 Code
前端·github·ai编程
xy34534 小时前
Axure 9.0 动态面板基本操作
前端·ui·html·axure·原型·产品设计
不停喝水4 小时前
【前端转全栈java速通课】 项目实战④7-11节 操作数据库-登录-注册-修改密码-注销-mubatis-plus简化crud-
java·前端·数据库