🔥 手撕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适配。
相关推荐
大佬桑2 小时前
Talend API Tester 接口测试插件 Google Chrome 浏览器的轻量级 API 测试插件
前端·chrome
阿西谈科技2 小时前
利用 AI 写前端:从辅助编码到智能化开发的完整实践指南
前端
爱喝麻油的小哆2 小时前
前端html导出pdf,(不完美)解决文字被切割的BUG,记录一下
前端
@大迁世界2 小时前
React 以惨痛的方式重新吸取了 25 年前 RCE 的一个教训
前端·javascript·react.js·前端框架·ecmascript
晴殇i2 小时前
【拿来就用】Uniapp路由守卫终极方案:1个文件搞定全站权限控制,老板看了都点赞!
前端·javascript·面试
嘿siri2 小时前
uniapp enter回车键不触发消息发送,已解决
前端·前端框架·uni-app·vue
CodeCraft Studio2 小时前
Excel处理控件Aspose.Cells教程:使用C#在Excel中创建树状图
前端·c#·excel·aspose·c# excel库·excel树状图·excel sdk
咬人喵喵2 小时前
CSS Flexbox:拥有魔法的排版盒子
前端·css
LYFlied2 小时前
TS-Loader 源码解析与自定义 Webpack Loader 开发指南
前端·webpack·node.js·编译·打包