news 2026/6/9 2:11:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手撕call/apply/bind:从ES6用法到手写实现,吃透this指向核心

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

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

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


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

[!NOTE] 核心结论
callapplybind本质作用完全一致——修改函数执行时this的指向,让函数能在指定的上下文环境中执行。

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

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

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

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

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

① 原生ES6用法
// ES6 原生call用法functionsay(){console.log(this.name)}constobj={name:'Alice'}say.call(obj)// 输出:Alice

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

② 手写实现源码
// 手写 myCall 实现Function.prototype.myCall=function(receive,...args){// 处理默认上下文:非严格模式下,null/undefined指向windowreceive=receive||window// 将原函数挂载到目标对象上(this指向原函数)receive.fn=this// 执行函数并传递参数列表,获取结果constresult=receive.fn(...args)// 清理临时属性,避免污染目标对象deletereceive.fn// 返回函数执行结果(与原生call行为一致)returnresult}// 手写myCall测试案例functiontestFun(param1,param2){returnparam1+this.a+param2}constobj1={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的核心特性。

② 手写实现源码
// 手写 myApply 实现Function.prototype.myApply=function(context,args){// 处理默认上下文context=context||window// 将原函数挂载到目标对象context.fn=thisletresult// 核心:处理数组参数if(!args){// 无参数时直接执行result=context.fn();}elseif(Array.isArray(args)){// 数组参数解构传递result=context.fn(...args)}else{// 非数组参数抛出类型错误(符合原生行为)thrownewTypeError('CreateListFromArrayLike called on non-object');}// 清理临时属性deletecontext.fn;returnresult}// 手写myApply测试案例constobj={name1:'ceshi1'}functiontestFun(param1,param2){returnparam1+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上下文,后续为可选的提前绑定参数,调用后返回一个新函数,只有执行新函数时才会触发原函数执行,且支持参数分批传递(柯里化特性)。

② 手写实现源码
// 手写 myBind 实现Function.prototype.myBind=function(context,...bindArgs){constself=this;// 保存原函数引用,避免this丢失// 返回新函数(核心:不立即执行)functionboundFn(...callArgs){// 关键判断:是否作为构造函数调用// new调用时,this instanceof boundFn 为 true,this 指向实例// 否则,this 指向绑定的 contextconstthisArg=thisinstanceofboundFn?this:context;// 执行原函数:合并绑定参数+调用参数,通过apply传递returnself.apply(thisArg,bindArgs.concat(callArgs));}// 继承原函数的prototype,保证new实例能访问原原型方法boundFn.prototype=Object.create(self.prototype);returnboundFn;};// 手写myBind测试案例functionsay(greeting,punctuation){console.log(greeting+', '+this.name+punctuation);}constperson={name:'Alice'};// 绑定this和第一个参数,返回新函数(不执行)constboundSay=say.myBind(person,'Hello');boundSay('!');// 执行新函数,输出:Hello, Alice!// 特殊场景:作为构造函数使用functionPerson(name){this.name=name;}Person.prototype.sayName=function(){console.log(this.name);};constBoundPerson=Person.myBind({name:'Bob'});constp=newBoundPerson('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实例能访问原函数的原型方法。

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

特性callapplybind
参数形式逗号分隔的参数列表单个数组/类数组参数支持分批传参(绑定+调用阶段)
执行时机立即执行立即执行不立即执行,返回新函数
返回值函数执行结果函数执行结果绑定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适配。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/30 13:34:55

HTR3355 I2C/SMBus IO扩展器全方位解析

在电子系统集成化、小型化的发展趋势下,GPIO端口资源紧张已成为众多硬件设计的共性难题。HTR3355作为一款专为I2C和SMBus总线设计的高性能IO扩展器,凭借其宽电压适配、灵活配置、功能完备等核心优势,能够高效弥补主控芯片IO端口不足的短板&am…

作者头像 李华
网站建设 2026/6/8 9:25:54

【完整源码+数据集+部署教程】食品包装识别系统源码分享[一条龙教学YOLOV8标注好的数据集一键训练_70+全套改进创新点发刊_Web前端展示]

一、背景意义 随着全球经济的快速发展和生活水平的提高,食品消费市场日益繁荣,食品包装的多样性和复杂性也随之增加。食品包装不仅是保护食品的物理屏障,更是传递品牌形象和消费者信息的重要载体。在这一背景下,食品包装的识别与…

作者头像 李华
网站建设 2026/6/6 18:04:39

PageHelper插件

介绍 PageHelper是第三方提供的Mybatis框架中的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。 官网:Mybatis-PageHelper/wikis/zh/HowToUse.md at master ruiqianyi/Mybatis-PageHelper GitHub 代码实现 在pom.xml引入依…

作者头像 李华
网站建设 2026/6/9 13:32:00

苹果在日本突然“服软”,背后藏着一步大棋!

📌 目录17年封闭高墙被凿开!日本用户可自由装软件,苹果仍抽5%-21%:库克的合规躺赚套路玩疯了一、法律利剑高悬:20%营收罚款倒逼苹果妥协(一)法律硬约束:违者最高罚全年营收20%&#…

作者头像 李华