🔥 手撕call/apply/bind:从ES6用法到手写实现,吃透this指向核心

🔥 手撕call/apply/bind:从ES6用法到手写实现,吃透this指向核心

在JavaScript中,this 指向的绑定是前端开发绕不开的核心知识点,而 callapplybind 作为改变函数执行上下文的"三剑客",既是解决 this 丢失问题的常用手段,也是面试中考察JS基础的高频题。

本文将从原生ES6用法手写实现源码核心差异深度对比三个维度,结合实战源码案例,带你彻底掌握这三个方法,看完就能手写面试题!


一、核心共性:本质都是改变this指向

!NOTE\] 核心结论 `call`、`apply`、`bind` 的**本质作用完全一致** ------修改函数执行时 `this` 的指向,让函数能在指定的上下文环境中执行。

比如对象内部的方法,this 原本指向对象本身;而全局函数的 this 默认指向 window(浏览器环境),通过这三个方法可强制改变这一规则:

javascript 复制代码
// 基础原理示例:this指向原对象
const obj = {
  name: '测试1',
  name2: '测试2',
  name3: '测试3',
  say: function() {
    this.name2 = this.name + this.name3 // this指向obj
  }
};

二、逐个拆解:ES6用法 + 手写实现 + 核心逻辑

下面针对每个方法,先展示原生ES6用法,再给出手写实现源码,最后解析核心逻辑,让你知其然也知其所以然。

1. call:参数列表传参,立即执行

① 原生ES6用法
javascript 复制代码
// ES6 原生call用法
function say() {
  console.log(this.name)
}
const obj = { name: 'Alice' }
say.call(obj) // 输出:Alice

call 接收第一个参数为绑定的this上下文 ,后续为逗号分隔的参数列表 ,调用后立即执行函数并返回结果。

② 手写实现源码
javascript 复制代码
// 手写 myCall 实现
Function.prototype.myCall = function(receive, ...args) {
  // 处理默认上下文:非严格模式下,null/undefined指向window
  receive = receive || window
  // 将原函数挂载到目标对象上(this指向原函数)
  receive.fn = this
  // 执行函数并传递参数列表,获取结果
  const result = receive.fn(...args)
  // 清理临时属性,避免污染目标对象
  delete receive.fn
  // 返回函数执行结果(与原生call行为一致)
  return result
}

// 手写myCall测试案例
function testFun(param1, param2) {
  return param1 + this.a + param2
}
const obj1 = { a: '测试1' }
console.log(testFun.myCall(obj1, '测试0', '测试2')) // 输出:测试0测试1测试2
③ 核心逻辑解析

!TIP\] 关键逻辑 * `receive = receive || window`:处理边界情况,当传入的上下文为 `null/undefined` 时,非严格模式下默认绑定 `window`; * `receive.fn = this`:核心技巧------将原函数(`this` 指向调用myCall的函数)挂载到目标对象上,通过"对象.方法"调用让 `this` 指向目标对象; * `delete receive.fn`:避免临时属性污染目标对象,执行完立即删除; * `...args`:ES6剩余参数,接收所有逗号分隔的参数列表,适配多参数场景。


2. apply:数组传参,立即执行

applycall 唯一的区别是参数传递形式,执行时机和返回值完全一致。

① 原生ES6用法

apply 接收第一个参数为绑定的this上下文 ,第二个参数为数组/类数组对象 ,调用后立即执行函数并返回结果。

!WARNING\] 原生特性 若第二个参数非数组且非undefined,会抛出TypeError,这是原生apply的核心特性。

② 手写实现源码
javascript 复制代码
// 手写 myApply 实现
Function.prototype.myApply = function(context, args) {
  // 处理默认上下文
  context = context || window
  // 将原函数挂载到目标对象
  context.fn = this
  let result
  // 核心:处理数组参数
  if (!args) {
    // 无参数时直接执行
    result = context.fn();
  } else if (Array.isArray(args)) {
    // 数组参数解构传递
    result = context.fn(...args)
  } else {
    // 非数组参数抛出类型错误(符合原生行为)
    throw new TypeError('CreateListFromArrayLike called on non-object');
  }
  // 清理临时属性
  delete context.fn;
  return result
}

// 手写myApply测试案例
const obj = { name1: 'ceshi1' }
function testFun(param1, param2) {
  return param1 + this.name1 + param2
}
console.log(testFun.myApply(obj, ['测试2', '测试3'])) // 输出:测试2ceshi1测试3
③ 核心逻辑解析
  • 与myCall的核心差异:args 必须是数组,通过 Array.isArray() 校验,符合原生apply的参数规则;
  • context.fn(...args):将数组参数解构为参数列表,本质是借用call的参数传递逻辑;
  • 错误抛出:严格校验参数类型,保证手写实现与原生行为一致。

3. bind:参数分批传,返回新函数(不立即执行)

bind 是三者中最特殊的一个,核心差异是不立即执行函数,而是返回绑定了this的新函数。

① 原生ES6用法

bind 接收第一个参数为绑定的this上下文 ,后续为可选的提前绑定参数,调用后返回一个新函数,只有执行新函数时才会触发原函数执行,且支持参数分批传递(柯里化特性)。

② 手写实现源码
javascript 复制代码
// 手写 myBind 实现
Function.prototype.myBind = function(context, ...bindArgs) {
  const self = this; // 保存原函数引用,避免this丢失

  // 返回新函数(核心:不立即执行)
  function boundFn(...callArgs) {
    // 关键判断:是否作为构造函数调用
    // new调用时,this instanceof boundFn 为 true,this 指向实例
    // 否则,this 指向绑定的 context
    const thisArg = this instanceof boundFn ? this : context;
    // 执行原函数:合并绑定参数+调用参数,通过apply传递
    return self.apply(thisArg, bindArgs.concat(callArgs));
  }

  // 继承原函数的prototype,保证new实例能访问原原型方法
  boundFn.prototype = Object.create(self.prototype);
  return boundFn;
};

// 手写myBind测试案例
function say(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
// 绑定this和第一个参数,返回新函数(不执行)
const boundSay = say.myBind(person, 'Hello');
boundSay('!'); // 执行新函数,输出:Hello, Alice!

// 特殊场景:作为构造函数使用
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function() {
  console.log(this.name);
};
const BoundPerson = Person.myBind({ name: 'Bob' });
const p = new BoundPerson('Charlie');
p.sayName(); // 输出:Charlie(this指向实例,而非绑定的对象)
③ 核心逻辑解析

!TIP\] 核心难点 * `const self = this`:保存原函数引用,避免后续嵌套函数中this指向丢失; * 返回新函数 `boundFn`:核心差异------不立即执行,而是返回绑定后的新函数; * `bindArgs.concat(callArgs)`:支持参数分批传递(绑定阶段传一部分,调用阶段传一部分); * `this instanceof boundFn`:关键判断------当新函数被`new`调用时,this指向实例而非绑定的context,符合原生bind的构造函数特性; * `boundFn.prototype = Object.create(self.prototype)`:继承原函数的原型链,保证new实例能访问原函数的原型方法。


三、深度对比:三者核心差异(补充版)

特性 call apply bind
参数形式 逗号分隔的参数列表 单个数组/类数组参数 支持分批传参(绑定+调用阶段)
执行时机 立即执行 立即执行 不立即执行,返回新函数
返回值 函数执行结果 函数执行结果 绑定this的新函数
构造函数场景 无特殊处理(极少用) 无特殊处理(极少用) 自动适配,this指向实例
手写源码核心差异 直接解构剩余参数传参 校验数组参数后解构传参 需返回新函数、合并参数、适配new
原生场景适配 多独立参数且需立即执行 参数为数组且需立即执行 需提前绑定this,延迟执行

补充:源码层面的核心区别

  1. call vs apply :手写源码的唯一差异在参数处理逻辑 ------call直接用剩余参数...args接收列表,apply需校验数组参数后解构;
  2. bind vs call/apply :bind的手写源码更复杂,多了三个核心逻辑:
    • 返回新函数而非立即执行;
    • 合并"绑定阶段参数"和"调用阶段参数";
    • 适配构造函数场景,保证new调用时this指向实例。


总结 📝

  1. callapply 核心差异仅在参数形式,均立即执行并返回函数结果;
  2. bind 核心差异是不立即执行,返回新函数,支持分批传参且适配构造函数场景;
  3. 手写实现的核心逻辑是:通过"函数挂载到目标对象→调用函数→清理挂载"改变this,bind需额外处理新函数、参数合并、new适配。
相关推荐
恋猫de小郭4 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端