news 2026/5/25 21:08:01

手撕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/26 7:22:10

区块链相关知识

一、区块链的简介 区块链可视为一种特殊的分布式数据库。 首先,区块链的主要作用是存储信息,任何需要保存的信息,都可以写入区块链,也可以从中读取信息,所以视它为数据库。 其次,任何人都可以架设服务器,加入区块链网络,成为一个节点。区块链的世界中没有中心节点,…

作者头像 李华
网站建设 2026/5/26 8:31:10

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

一、背景意义 随着全球人口的不断增长和生活水平的提高,食品安全与营养健康问题日益受到关注。食品种类繁多,消费者在选择食品时不仅关注其营养成分,还对食品的来源、品质和安全性提出了更高的要求。在此背景下,食品分类与检测技术…

作者头像 李华
网站建设 2026/5/24 4:19:44

【英飞凌 CY8CKIT-062S2-AI评测】-开发环境搭建与开发

过21IC网,申请到了英飞凌 CY8CKIT-062S2-AI开发板,该开发板是英飞凌的PSOC6系列的人工智能评估套件,它有一套创新工具用来原型制作和收集真实数据,以快速构建机器学习模型。硬件尺寸很小巧35mm*45mm,基于它可以建构边缘…

作者头像 李华
网站建设 2026/5/26 6:53:10

基于SpringBoot2+Vue2的企业合作与活动管理平台

企业合作与活动管理平台 演示视频 https://www.bilibili.com/video/BV1E4qpB9E8b/ 角色 管理员、普通用户、企业用户 技术 后端:Spring Boot 2、MySQL 前端:Vue.js 核心功能 本系统是一个企业合作与活动管理平台,旨在连接企业和普通用…

作者头像 李华
网站建设 2026/5/26 8:14:44

稀土抑烟剂在PVC材料中的防火与抑烟作用

PVC(聚氯乙烯)因耐用、易加工、成本低,被广泛应用于建筑管材、电线护套、地板和卷材等领域。但在火灾条件下,PVC燃烧容易产生大量烟雾和刺激性气体,不仅影响逃生,也增加了火灾危害。一、什么是稀土抑烟剂&a…

作者头像 李华
网站建设 2026/5/26 8:14:46

别让AI抢了你的饭碗:学会让它替你打工,才是未来的生存法则

朋友们,你有没有想过,未来的世界可能会被简单地分成两种人?一种是让AI替自己干活的人,另一种是活被AI抢走的人。这句话听起来有点残酷,但趋势已经摆在我们眼前。从写报告、做设计,到分析数据、客服应答&…

作者头像 李华