首页 > 其他分享 >this指向&call,apply,bind的用法及手写

this指向&call,apply,bind的用法及手写

时间:2024-03-27 23:58:05浏览次数:20  
标签:函数 指向 bind ctx call key apply mycall

我们知道 call,apply,bind 主要用来修改 this 指向。那么这三个方法的用法区别是什么?具体是怎么修改 this 指向的?我们该如何手写自己的 call,apply,bind 函数?

我们先从 this 指向讲起。明白了this在不同情况的指向,再来看这三个方法在操纵 this的具体情况及异同,明白其作用后再来讲解如何实现这样功能的函数。

this 指向

this 指向可分为四种场景,在这不同场景里 this 指向不同,我们以代码做演示。

function fn(){
    console.log(this);
}

// 示例1:直接调用的this-→全局对象
fn()//this指向window对象

// 示例2:构造函数创建的实例的this-→实例对象
function Fn(name){
    this.name=name
    this.fn2=function(){
        console.log(this);
        console.log(`此人名字是:${this.name}`);
    }
}
const obj2=new Fn('木鱼') //Fn中的this都指向new出来的obj2
obj2.fn2() 

// 示例3:通过对象调用的方法中的this-→调用的对象
const obj3={
    name:'我是obj3',
    fn3(){
        console.log(this);
    }
}
obj3.fn3() //fn3中的this指向调用该方法的obj3

// 示例4:使用call,apply,bind执行的this-→call,apply,bind的第一个参数
const obj4={}
fn.call(obj4) //call方法会修改fn里的this指向为obj4,并执行fn
fn.apply(obj4)//apply方法会修改fn里的this指向为obj4,并执行fn
const fn4=fn.bind(obj4)//bind方法会返回一个this指向为obj4而其它功能与fn一样的新函数,且不执行
fn4()

1.直接调用函数➡️全局对象

如果直接调用函数,则函数里的 this 指向全局变量。如示例1,其运行结果如下:

2.new 出来的实例调用➡️实例对象

构造函数中的 this 指向其 new 出来的实例对象。如示例2,其运行结果如下:

如框架中在标签 div 上定义 onclick 方法,本质也是标签 div 的构造函数创建了一个 标签实例(也就是当前的div节点),因此上面定义方法的this 指向也就是这个标签实例本身。

3.以方法形式存于对象中➡️调用该方法的对象

如果该函数是作为方法被对象调用的,则方法里的 this 指向为调用该方法的对象。如示例3,其运行结果如下:

4.调用了call,apply,bind 方法后➡️第一个参数

如果一个函数或方法调用了 call,apply,bind,则其 this 指向将被修改指向传进 call,apply,bind 的第一个实参。如示例3,其运行结果如下:

call,apply,bind的用法

明白了this在不同场景下的指向后,我们再来着重讲一下call、apply、bind这三种方法的不同点。

实际上call、apply、bind的区别很简单,而用法很相近,因此我们可以先单介绍call的用法:

call方法的作用是其实就两个:1、修改函数里的this指向,2、执行该函数

function fnc(a,b){
    console.log(this,a,b);
}
const obj={
    name:'我是obj'
}
fnc(1,2) //this为window对象
fnc.call(obj,1,2) //this变为obj

call会将调用call的函数fnc的this指向修改为传给call的第一个参数,也就是obj,然后再执行fnc这个函数(后面的参数与fnc一一对应的实参)。其运行结果如下:

call和apply的区别在于:call第二个参数往后的参数都是一个个传进去的,而apply第二个参数则是个类数组,即让fnc运行的实参都是放入一个数组里作为第二个参数,如上述例子用apply运行则代码区别仅为fnc.apply(obj,[1,2])。

fnc.apply(obj,[1,2]) //this变为obj,除了传参形式外其余都与call一样

call和bind的区别则在于:bind不直接修改func函数的this,而是创建并返回一个新函数,该函数内部的this指向bind的第一个实参,其余功能与func一样,不会运行func,也不会自动运行该创建的新函数。这意味着bind修改this指向后可以在想调用时再自行调用该函数。

const newFun=func.bind(obj,1,2)//用法与call一样,但不会执行该函数,而是返回一个新函数
newFun()//该新函数可在想调用时再调用,执行结果与call调用的结果一致

手写call、apply、bind

明白了这三种修改this方法的用法及异同,那么我们再次从介绍如何手写call,明白了call的手写,apply和bind的手写也就仅仅只在call的手写基础上做轻微改动即可。

1、手写call

由前面call的用法已知,call方法就做了两件事:1、修改函数里的this指向,2、执行该函数。

自己手写一个call功能的函数mycall,思路如下:

  • 要使每个函数想修改this时都能调用mycall,则mycall方法需挂载在构造函数Function的原型上,因此我们可以通过 Function.prototype.mycall 定义一个自己的call方法。
  • mycall的参数,第一个参数是我们要修改this指向的另一个对象(我们将该形参命名为ctx),而后面可能没有参数,也可能有一个或多个参数,这取决于调用函数的参数,因此第二个参数我们可以以剩余参数...arg的形式传递。即 Function.prototype.mycall=function(ctx,...arg){ } 
  • 我们要修改this,那么我们先思考这时候我们定义的函数里的this是谁?Function.prototype.mycall=function(ctx,...arg){ console.log(this) } 由前面介绍的this指向知识我们可以知道,谁调用了mycall,mycall函数里的this就指向谁。因此此时的this将指向将来调用mycall的函数,换个角度而言,这里的this代表了将来调用mycall的函数。
  • 那么如何修改this指向ctx呢?可以这么看,Function构造函数内部会处理将this指向为其new出来的示例。我们可以暂用伪代码来表示辅助理解:Function.prototype.mycall=function(ctx,...arg){ const this=将来调用mycall的函数 } ,那么我们结合前面this指向知识,可以利用对象的方法this指向调用该方法的对象的知识点,将调用该方法的函数添加作为ctx对象的一个方法(如添加为ctx的temp方法),通过ctx调用自身的方法(ctx.temp()),则temp里的this指向即为ctx。因此可以通过Function.prototype.mycall=function(ctx,...arg){ ctx.temp=将来调用mycall的函数 } 来隐式修改this的指向,那么这个“将来调用该方法的函数”在这个mycall里应该怎么表示?结合前面这两个蓝色块伪代码来看,即用this来表示即可。即:Function.prototype.mycall=function(ctx,...arg){ ctx.temp=this } 。
  • 由于temp是临时定义的,为防止ctx本身就有该temp方法,我们应该将this放在ctx一个独一无二的方法名上,怎么做到不重名呢?借助symbol。因此以上代码可以改进为Function.prototype.mycall=function(ctx,...arg){ const key=symbol('temp') ;  ctx[key]=this } 。而如果读取时读到这个ctx本不存在而被新定义的方法key是不是也很奇怪,因此我们可以设置该方法不显示,怎么不显示呢,只要设置该key属性不被枚举就行了,那么新增key的方式ctx[key]=this可改进为通过defineProperty()定义→Object.defineProperty(ctx,key,{enumerable:false,value:this}),即最终改进为:Function.prototype.mycall=function(ctx,...arg){ const key=symbol('temp') ;  Object.defineProperty(ctx,key,{enumerable:false,value:this}) }
  • 那么接下来只需要调用ctx的key方法,即能隐式修改被挂在key方法上的函数的this指向了。即运行 ctx[key](...arg) 即可,当然了,该函数可能有返回值,我们也要记得给它返回。即 const result=ctx[key](...arg) ;  return result。这样不仅修改了this指向,也运行了这个函数,call的功能基本实现。
  • 实现了call的功能后,别忘了最后一步,还原ctx本身的样子。我们需要把这个过程中新定义在ctx上的方法key删除。即 delete ctx[key] (这一步放在return前),由于defineProperty的configurable属性默认为false,表示默认该属性不可删除,因此需补充配置将其改为true才可以成功删除该属性。Object.defineProperty(ctx,key,{enumerable:false,configurable:true,value:this})。
  • 补充完善:我们前面的方法是基于将调用的函数挂到ctx的方法上,那么如果ctx是null呢?或者ctx不是一个对象呢?因此我们应该做一个特殊情况的判断:如果ctx是null或者undefined,那么this应该指向谁?应指向全局对象。全局对象有一个关键字globalThis,它能适配所有的环境(浏览器的全局对象是Window,在node环境里面全局对象是global,我们直接统一用globalThis关键字表示即可)。如果ctx不是null或undefined,无论是number类型string类型还是object对象,我们直接统一将其转为对象形式即可--Object(ctx),即可将各种类型都转换为对象形式(对象被Object包裹后还是对象本身)。因此执行上述代码前可先对ctx进行如下判断: ctx=ctx===undefined||ctx===null?globalThis:Object(ctx) ,如此,ctx就一定是对象了。
具体实现代码如下:
Function.prototype.mycall=function(ctx,...args){
    // 特殊情况判断以保证ctx是对象
    ctx=ctx===undefined||ctx===null?globalThis:Object(ctx)
    const key=Symbol('temp')
    Object.defineProperty(ctx,key,{
        enumerable:false, //如果运行mycall时打印ctx,则这个key属性不用显现出来,设置不可枚举即可
        configurable:true,//默认不可以删除,因后续需要删除,需设置修改为可删除
        value:this //把将来调用mycall的函数(此示例中即为method函数)挂在到ctx的key属性上,使该函数成为ctx的方法
    })
    const result=ctx[key](...args) //通过调用ctx的key方法运行了method函数,并且method函数作为ctx的方法被ctx调用,其this指向即为ctx,并通过result拿到返回值
    delete ctx[key] //删除临时创建的方法还原ctx原本的样子
    return result //保持与method效果一致返回相应的返回值
}
function method(a,b){
    console.log(this,a,b);
    return a+b
}
const obj={
    name:'我是obj'
}
method.mycall(obj,1,2)//执行结果如下
console.log(obj);//执行结果如下,key属性会被删除保证obj经过mycall后保持原样

2、手写apply

理解了call的手写过程,apply也就照猫画虎即可,需要调整的地方仅为传参形式,apply只有两个参数,第二个参数是以数组形式传,因此仅需把...args改为args代表一个伪数组,并做好args存在与否的判断(“...args”运行的前提是args存在)

Function.prototype.myApply=function(ctx,args){//形参为args,代表一个伪数组
            ctx=ctx===undefined||ctx===null?globalThis:Object(ctx)
            const key=Symbol('temp')
            let result //因为后续result是在if逻辑判断中赋值,因此result需先声明
            Object.defineProperty(ctx,key,{
                enumerable:false, 
                configurable:true,
                value:this 
            })
            if(!args){
                result=ctx[key]() //如果函数无需传参则直接调用该函数
            }else{
                result=ctx[key](...args) //如果函数无需传参,不进行args存在与否的判断,则...args会报错,因为args不存在谈何...args
            }
           
            delete ctx[key] 
            return result 
        }
        function method(a,b){
            console.log(this,a,b);
            return a+b
        }
        const obj={
            name:'我是obj'
        }
        method.myApply(obj,[1,2])//以数组形式传参
        console.log(obj);

运行结果正常,实现了myApply。

3.手写bind

bind区别于call的地方在于bind不直接执行函数,而是返回一个可执行的函数,因此手写方式可以以mycall为前提基础,mybind返回一个新函数,在这个新函数中再来调用一下mycall方法另外,bind若传入除了第一个参数以外的参数,则bind会将新的参数一起合并进(并非替换)mycall方法的形参中。如下:

const newFun1=fnc.bind(obj,1)
newFun1(2)
//上下两段代码效果一致
const newFun2=fnc.bind(obj,1,2)
newFun2()

手写过程给出如下简洁版:

//既前面手写call的代码
Function.prototype.mybind=function(ctx,...args1){
    let that=this //注意这里的this才是调用mybind的函数
    return function newFun(...args2){
        //这里的this是调用newFun的对象,不是调用mybind的函数,需用that表示我们想要的method
        return that.mycall(ctx,...args1,...args2)
    }
}
const obj2={
    name:'我是obj2'
}
const newMethod=method.mybind(obj2,1)
newMethod(2)

运行结果无误。

标签:函数,指向,bind,ctx,call,key,apply,mycall
From: https://blog.csdn.net/didadidadidadida/article/details/137070280

相关文章

  • WARN o.a.t.util.scan.StandardJarScanner - Failed to scan [file:/D:/Mavencangku/
    1、SpringBoot项目启动突然报错2024-03-2714:57:41[restartedMain]WARNo.a.t.util.scan.StandardJarScanner-Failedtoscan[file:/D:/Mavencangku/com/sun/xml/bind/jaxb-core/2.3.0/jaxb-api.jar]fromclassloaderhierarchyjava.io.FileNotFoundException:D:\Maven......
  • Building an Automatically Scaling Web Application
    2024年春季云计算课业1:构建一个自动伸缩的Web应用程序截止日期:2024年4月15日,星期一1目标和范围在这项任务中,我们将为(非常)琐碎的Web构建一个小型的自动伸缩测试平台应用任务的目标是熟悉伸缩Web的各个方面应用程序,这将提高您对低级/基本实现的理解云系统的详细信息。正如我们在......
  • IT15527: IN SPECIFIC TIMING CONDITIONS WITH MULTIPLE DB2READLOG API CALLERS(CDC,
    IT15527:INSPECIFICTIMINGCONDITIONSWITHMULTIPLEDB2READLOGAPICALLERS(CDC,ETC),"NOROOMFORRETRIEVEDLOG"occursindb2diag.loghttps://www.ibm.com/mysupport/s/defect/aCI0z000000TOfW/dt010963?language=en_USDescription 1.  Proble......
  • PhpStrom启动报错, java.net.BindException: Address already in use: bind
    问题描述:今天启动phpstromIDE时,突然报错,报错信息如下图:问题分析1.不正确关闭应用(强制关闭):可能是之前启动了一个本地web服务占了端口,在没有停掉服务,直接关闭IDE导致的(尝试了重启电脑也没解决)2.其他应用占用端口:安装了Hyper-V导致端口被占用?显然我的是第一种情况问题解决......
  • wpf add resource dynamically in cs file
    //xaml<Windowx:Class="WpfApp12.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.mic......
  • zynq Lwip学习笔记-recv_callback函数
    文章目录前言一、概述二、函数体三调用位置前言最近在学习zynq中的lwip协议族,找不到很好的记笔记的地方,所以就用csdn记录一下自己的学习过程。现在对lwip不熟悉,只是把官方的lwipechoserver例程跑了一下,能跑通就一点点的照着学了,笔记都是根据自己的理解写的,而且部......
  • zynq Lwip学习笔记-accept_callback函数
    文章目录前言`一、概述二、函数体三、调用关系前言`最近在学习zynq中的lwip协议族,找不到很好的记笔记的地方,所以就用csdn记录一下自己的学习过程。现在对lwip不熟悉,只是把官方的lwipechoserver例程跑了一下,能跑通就一点点的照着学了,笔记都是根据自己的理解写的,而......
  • 从零开始的terraform之旅 - 3命令部分- 部署基础架构 (plan apply destroy)
    3命令部分-部署基础架构(planapply)文章目录3命令部分-部署基础架构(planapply)部署基础架构planplanningmodes**Refresh-onlymode**仅刷新模式,非常有用PlanningOptions规划选项apply命令Plan**Options**apply选项destroy命令部署基础架构terraform的......
  • vue一些基础概念,核心理念,框架和库的区别,MVC和MVVM的区别,展示数据的几种方法、v-bind、
    1、什么是vue,核心理念,为什么学习vue1(单页面应用程序)用于构建用户界面的渐进式框架,采用自底向上增量开发的设计2数据驱动视图,组件化开发3轻量级框架、简单易学、虚拟的DOM、数据视图结构分离下面展示一些内联代码片。下面是vue的代码框架分为三部分:1.引入vue.js;2......
  • 中考英语首字母快速突破012-2021上海青浦英语二模-Earth Hour: A Global Call for Env
    PDF格式公众号回复关键字:ZKSZM012原文​WhatisEarthHour?​EarthHourisorganizedbytheWorldWideFundforNature(WWF)andit’sabigeventusuallyattheendofMarcheveryyear.Onthisevening,people‘godark’-thatis,switcho......