前端手写源码系列(二)——手写call、apply、bind

news2024/11/17 0:27:54

image-20240826172816275

手写源码系列目录

    • 一、作用
    • 二、手写call方法
    • 三、手写apply方法
    • 四、手写bind方法
    • 五、三者区别
      • apply
      • call
      • bind
      • 小结

一、作用

callapplybind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

那么什么情况下需要改变this的指向呢?下面举个例子

var name = "lucy";
var obj = {
    name: "martin",
    say: function () {
        console.log(this.name);
    }
};
obj.say(); // martin,this 指向 obj 对象
setTimeout(obj.say,0); // lucy,this 指向 window 对象

从上面可以看到,正常情况say方法输出martin

但是我们把say放在setTimeout方法中,在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,这时候this指向window,所以输出lucy

我们实际需要的是this指向obj对象,这时候就需要该改变this指向了

setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象

二、手写call方法

call做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向 window

分析:如何在函数执行时绑定this

  • var obj = {x:100,fn() { this.x }}
  • 执行obj.fn() ,此时fn内部的this就指向了obj 可借此来实现函数绑定this
  • 原生callapply传入的this如果是值类型,会被new Object(如fn.call('abc'))
//实现call方法

// 相当于在obj上调用fn方法,this指向obj 
// var obj = {fn: function(){console.log(this)}}
// obj.fn() fn内部的this指向obj
// call就是模拟了这个过程
// context 相当于obj

Function.prototype.myCall = function(context = window, ...args) {
  if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

  // args 传递过来的参数
  // this 表示调用call的函数fn
  // context 是call传入的this

  // 在context上加一个唯一值,不会出现属性名称的覆盖
  let fnKey = Symbol()
  // 相等于 obj[fnKey] = fn 
  context[fnKey] = this; // this 就是当前的函数

  // 绑定了this
  let result = context[fnKey](...args);// 相当于 obj.fn()执行 fn内部this指向context(obj)

  // 清理掉 fn ,防止污染(即清掉obj上的fnKey属性)
  delete context[fnKey];

  // 返回结果 
  return result;
};

调用:

//用法:f.call(this,arg1)

function f(a,b){
  console.log(a+b)
  console.log(this.name)
}
let obj={
  name:1
}
f.myCall(obj,1,2) // 不传obj,this指向window

三、手写apply方法

思路: 利用this的上下文特性。apply其实就是改一下参数的问题

Function.prototype.myApply = function(context = window, args) {  // 这里传参和call传参不一样
  if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

  // args 传递过来的参数
  // this 表示调用call的函数
  // context 是apply传入的this

  // 在context上加一个唯一值,不会出现属性名称的覆盖
  let fnKey = Symbol()
  context[fnKey] = this; // this 就是当前的函数

  // 绑定了this
  let result = context[fnKey](...args); 

  // 清理掉 fn ,防止污染
  delete context[fnKey]; 

  // 返回结果
  return result;
}

调用:

// 使用
function f(a,b){
  console.log(a,b)
  console.log(this.name)
}
let obj={
  name:'张三'
}
f.myApply(obj,[1,2])

四、手写bind方法

bind 的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现:

  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来
  • 最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
  • 箭头函数的底层是bind,无法改变this,只能改变参数

简洁版本:
对于普通函数,绑定this指向
对于构造函数,要保证原函数的原型对象上的属性不能丢失

Function.prototype.myBind = function(context = window, ...args) {
  // context 是 bind 传入的 this
  // args 是 bind 传入的各个参数
  // this表示调用bind的函数
  let self = this; // fn.bind(obj) self就是fn

  //返回了一个函数,...innerArgs为实际调用时传入的参数
  let fBound = function(...innerArgs) { 
    //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)
    // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
    // 当作为普通函数时,this 默认指向 window,此时结果为 false,将绑定函数的 this 指向 context
    return self.apply( // 函数执行
      this instanceof fBound ? this : context, 
      args.concat(innerArgs) // 拼接参数
    );
  }

  // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失
  // 实现继承的方式: 使用Object.create
  fBound.prototype = Object.create(this.prototype);
  return fBound;
}

调用:

// 测试用例

function Person(name, age) {
  console.log('Person name:', name);
  console.log('Person age:', age);
  console.log('Person this:', this); // 构造函数this指向实例对象
}

// 构造函数原型的方法
Person.prototype.say = function() {
  console.log('person say');
}

// 普通函数
function normalFun(name, age) {
  console.log('普通函数 name:', name); 
  console.log('普通函数 age:', age); 
  console.log('普通函数 this:', this);  // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj
}


var obj = {
  name: 'poetries',
  age: 18
}

// 先测试作为构造函数调用
var bindFun = Person.myBind(obj, 'poetry1') // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say

// 再测试作为普通函数调用
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined
bindNormalFun(12) 
// 普通函数name: poetry2 
// 普通函数 age: 12 
// 普通函数 this: {name: 'poetries', age: 18}

注意: bind之后不能再次修改this的指向(箭头函数的底层实现原理依赖bind绑定this后不能再次修改this的特性),bind多次后执行,函数this还是指向第一次bind的对象

五、三者区别

apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入

改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

call

call方法的第一个参数也是this的指向,后面传入的是一个参数列表

apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

同样的,当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window

小结

从上面可以看到,applycallbind三者的区别在于:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2076202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Boot启用GZIP压缩

1.为什么是需要gzip压缩? 经常我们都会与服务端进行大数据量的文本传输,例如 JSON 就是常见的一种格式。通过 REST API 接口进行 GET 和 POST 请求,可能会有大量的文本格式数据提交、返回。然后对于文本,它有很高的压缩率&#x…

systemserver进程监控者--watchdog

戳蓝字“牛晓伟”关注我哦! 用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章,技术文章也可以有温度。 本文摘要 本文同样采用自述的方式来介绍systemserver进程的监控者watchdog,通过本文您将了解watchdog的作用,它…

【机器学习】梯度下降算法

梯度下降算法 这篇博客更加详细,以下只是我个人的理解 梯度下降算法原理讲解——机器学习-CSDN博客 梯度下降算法是一种优化算法,通过梯度下降找到函数最小值时的自变量值。 其基本思想是沿着梯度方向的反方向更新参数,直到逼近函数的极值…

【LLM大模型】生成式人工智能大型语言模型的安全性:概述

生成性AI大型语言模型(LLMs)的安全性:概述 具有生成性AI能力的大型语言模型(如ChatGPT)正面临加速采用和创新。生成性AI(GAI)的日益普及不可避免地引发了对这些模型相关风险和安全性的担忧。本…

Linux 下 gdb 的使用

目录 一、概述1、安装和启动 GDB 二、 GDB 常用命令1、查看源码2、断点2.1 设置断点2.2 查看断点信息2.3 删除断点2.4 激活/禁用断点2.5 观察断点2.6 捕获信号2.7 线程中断 3、查看信息3.1 查看数据3.2 查看内存3.3 查看栈信息3.4 查看栈帧信息 4、运行、调试5、编辑和搜索 一、…

MacOS安装 Python 和 PyCharm

MacOS安装 Python3.12.5 和 PyCharm 小阿呜有话说一、MacOS安装PythonPython官网下载 二、MacOS安装PyCharmPyCharm官网下载 叮嘟!这里是小啊呜的学习课程资料整理。好记性不如烂笔头,今天也是努力进步的一天。一起加油进阶吧! 小阿呜有话说 …

发完朋友圈就“退款”?黑神话的玩家是否都是“忠实粉丝”?

​声明:此篇为 ai123.cn 原创文章,转载请标明出处链接:https://ai123.cn/2228.html 《黑神话:悟空》自上线以来,便引发了玩家社区的广泛讨论。游戏的退款现象主要受到了一些技术问题和个人体验差异的影响。部分玩家因遇…

容器的ip地址不稳定问题、联合文件系统、核对时间、制作基础镜像

在docker中部署线上考试系统 1、部署前端服务器 # 上传本地下载的dist文件,因为上传的是目录,加-r选项 scp -r D:\云计算\压缩包\项目\dist root192.168.2.50:/root/ # 创建基础容器 [rootdocker ~]# docker run -it --name c0 centos:latest /bi…

SolidityFoundry BitMap

写合约的时候,记录某个账户的bool状态很常见,例如是否领取空投等,传统的写法mapping(uint256>bool)中一个slot只能存储一个账户的信息,在其他语言中,我们经常会用到bitmap来表示标志位,如果我们可以将bi…

银行卡三要素验证如何用Java进行调用

一、什么是银行卡三要素验证? 银行卡三要素验证又叫银行卡三要素核验、银行卡三要素校验、银行卡实名认证、银行卡三元素验证,即输入银行卡卡号、姓名、身份证号码,验证此三要素是否一致,该接口支持所有带银联标识的银行卡。 二…

langchain入门系列之五 初探代理

代理的核心思想是使用LLM来选择要采取的一系列动作。 在链式结构中,一系列动作是硬编码的(在代码中)。 在代理中,使用语言模型作为推理引擎来确定要采取的动作及其顺序。 代理 这是负责决定下一步采取什么动作的类。 这是由语言…

lidar3607.2 lidar360mls7.2 强大的雷达点云数据处理应用软件

1、LiDAR360是一款强大的激光雷达点云数据处理和分析平台,拥有超过10种先进的点云数据处理算法,可同时处理超过300G点云数据。平台包含丰富的编辑工具和自动航带拼接功能,可为地形、林业、矿山和电力行业(参考LiPowerline软件&…

【HarmonyOS 4.0】@BuilderParam 装饰器

1. BuilderParam 装饰器 BuilderParam 装饰器用于装饰自定义组件(struct)中的属性,其装饰的属性可作为一个UI结构的占位符,待创建该组件时,可通过参数为其传入具体的内容。参数必须满足俩个条件: 2.1 参数类型必须是个函数&#x…

前端使用canvas绘制简单工作流-react

效果图如下: 目前只做了绘制部分,绘制方式也比较简单,点击工具栏中需要绘制的图形,在画布上左键点击将会绘制一个图形出来,工具栏选中第一个,再点击其他图像,长按鼠标左键可以移动,删…

丢掉Beyond Compare吧!新款文件差异对比工具WinMerge更具性价比!

今天想和大家分享一款非常实用的免费开源文件比较工具:WinMerge。 作为一名长期从事互联网行业的人,我经常需要处理大量的文档和代码文件,文件对比工具在我的日常工作中可谓是必不可少的“左膀右臂”。 也相信很多朋友在处理多个文档内容或者…

96页PPT集团战略解码会工具与操作流程

德勤集团在战略解码过程中通常会用到以下一些具体工具: 一、平衡计分卡(Balanced Scorecard) 财务维度: 明确关键财务指标,如营业收入、利润、投资回报率等。你可以通过分析历史财务数据和行业趋势,确定…

HUSB381A:带线PD适配器的绝佳选择

HUSB381A是慧能泰半导体全新推出的一款采用SOP8封装,集成MOS的USB PD Source芯片,带CC1和CC2引脚,支持不可分离线缆(Captive Cable)PD适配器和纯PD快充充电器应用。HUSB381A支持最大功率20V5A 100W应用,支持…

单片机驱动彩屏最简方案:单片机_RA8889最小开发板驱动控制TFT彩屏介绍(一)方案架构

本文介绍使用单片机RA8889来驱动和控制彩屏的最小方案。文章从RA8889的架构功能、硬件电路设计及软件设计三个方面来说明。 小编已发布多篇文章介绍了单片机RA8889来驱动控制彩屏,但是仍有不少单片机玩家可能对驱动彩屏还不算熟悉,在此加推一个短篇介绍…

审计发现 FBI 的数据存储管理存在重大漏洞

据The Hacker News消息,美国司法部监察长办公室 (OIG) 的一项审计发现, FBI 在库存管理和处置涉及机密数据的电子存储媒体方面存在“重大漏洞”。 OIG 的审计显示,FBI 对包含敏感但未分类 (SBU&#xff09…

橙子投屏,轻松连接大屏幕

对于某腾、某爱、某酷投屏大家在熟悉不过了吧,一款非常好用的投屏软件,但是使用起来还是限制颇多,比如有犷郜,还必须同步使用手机App才能实现投屏功能,关键还得开会员,劝退不少小伙伴。但是现在手机往往占据…