zoukankan      html  css  js  c++  java
  • this指向与call,apply,bind

    this问题对于每个前端同学来说相信都不陌生,在平时开发中也经常能碰到,有时候因为this还踩过不少坑,并且this问题在面试题中出现的概率也非常高,我们一起来了解一下this的指向与callapplybind

    this的指向

    ES5中的this

    在ES5中,this一般指向函数调用时所在的执行环境,与函数定义的位置无关。也可以理解成this永远指向最后调用它的对象

    • 在普通函数中的this总是指向它的直接调用者,默认情况下指向全局对象(浏览器为window)
    • 在严格模式下,没有直接调用者的函数中的thisundefined
    • Call, apply,bind函数,this指向的是绑定的对象
    • 对象函数调用,this指向调用它的那个对象
    • 构造函数中的this,指向该构造函数new出来的实例对象
    var obj = {
        a:function(){
            console.log(this)
            console.log(this.b)
            console.log(this.c)
            console.log(this.a)
        },
        b:2,
        c:3
    }
    var b = obj.a
    b()
    // 结果:window,f(){...},undefined,undefined
    obj.a()
    // 结果:{a:..,b:2,c:3},2,3,f(){...}
    
    /**
     * 解析:
     * b()调用,此时b函数所处的执行环境是全局环境,this指向window
     * obj.a()调用,此时a是作为对象方法进行调用,this指向调用对象obj
     */
    
    ES6中的this

    在ES6中新增了一种箭头函数,箭头函数的this始终指向它定义时的this,而非执行时

    • 箭头函数没有自己的this,它的this是继承来的,默认指向它定义时所在的对象,即箭头函数中的this指向外层代码的this
    • 不可以当作构造函数,也就是说,不可以用new命令调用,否则会抛出一个错误
    • 箭头函数内没有arguments对象,可以用rest参数代替
    • 不可以使用yield命令,因此箭头函数不能用作generator函数
    • 箭头函数没有自己的this,所以不能用callapplybind这些方法改变this指向
    var obj = {
                hi: function(){
                    console.log(this);
                    return ()=>{
                        console.log(this);
                    }
                },
                sayHi: function(){
                    return function() {
                        console.log(this);
                        return ()=>{
                            console.log(this);
                        }
                    }
                },
                say: ()=>{
                    console.log(this);
                }
            }
            const hi = obj.hi()   //this->obj对象
            hi()   // this->obj对象
            const sayHi = obj.sayHi()
            const sayHiBack = sayHi()  //this->window
            sayHiBack() //this->window
            obj.say() //this->window
    

    输出结果依次为obj对象,obj对象,window,window,window

    解析:

    1.第一个obj.hi()很好理解,hi为普通函数,this指向调用它的那个对象,即obj

    2.第二个执行hi(),它其实是上一个执行后返回的函数,并且是箭头函数,箭头函数本身没有this,我们往他的上一级去查找,我们刚刚得出上一级的this为obj,所以这里的this也指向obj

    3.第三个执行obj.sayHi(),这里没有打印this,而是返回了一个普通函数

    4.第四个执行sayHi(),其实执行的是刚刚返回的那个普通函数,这里的this则指向调用它的那个对象,没有则指向window

    5.第五个执行sa yHiBack(),指向的是刚刚第四次执行返回的箭头函数,OK,箭头函数我们往上一层找,也是window

    5.第六个执行obj.say(),这里这个say()是一个箭头函数,当前代码块obj不存在this,只能往上一层查找,指向window

    call, apply,bind的区别

    我们都知道call,apply,bind都可以用来改变this指向,但这三个函数稍稍有些不同。

    • call与apply唯一的区别就是它们的传参方式不同,call从第二个参数开始都是传给函数的,apply只有两个参数,第二个参数是一个数组,传给函数的参数都写在这个数组里面
    • call与apply改变了函数的this指向后会立即执行,而bind是改变函数的this指向并返回这个这个函数,不会立即执行
    • call与apply的返回值是函数的执行结果,bind的返回值是改变了this指向的函数的拷贝
    call

    call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。(来自MDN)

    语法: fun.call(thisArg,arg1[,arg2,arg3...])

    解释: call方法用来为一个函数指定this对象,第一个参数是你想要指定的那个对象,后面都是传给该函数的参数,之间用逗号隔开

    var person = {
      name:'南玖',
      gender: 'boy',
    }
    var speak = function(age,hobbit){
      console.log(`我是${this.name},今年${age}岁,爱好${hobbit},欢迎优秀的你关注~`)
    }
    speak.call(person,18,'前端开发') // 我是南玖,今年18岁,爱好前端开发,欢迎优秀的你关注~
    
    apply

    apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。(来自MDN)

    语法: fun.apply(thisArg,[arg1,arg2,arg3...])
    解释: apply方法与call方法基本类似,不同的是,两者的参数形式,apply方法传递的是一个由若干个参数组成的数组。

    var person = {
      name:'南玖',
      gender: 'boy',
    }
    function speak(age,hobbit){
      console.log(`我是${this.name},今年${age}岁,爱好${hobbit},欢迎优秀的你关注~`)
    }
    speak.apply(person,[18,'打篮球⛹️']) //我是南玖,今年18岁,爱好打篮球⛹️,欢迎优秀的你关注~
    
    bind

    bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

    语法: fun.bind(thisArg,arg1[,arg2,arg3...])()
    解释: bind方法用来为方法指定this对象并返回一个新的函数,它的参数与call函数一样。它本身是不会调用的,需要自己手动调用。

    var person = {
      name:'南玖',
      gender: 'boy',
    }
    function speak(age,hobbit){
      console.log(`我是${this.name},今年${age}岁,爱好${hobbit},欢迎优秀的你关注~`)
    }
    speak.bind(person,18,'旅游⛱️')() //我是南玖,今年18岁,爱好旅游⛱️,欢迎优秀的你关注~
    

    注意这里需要自己再调用一次,因为bind只会返回这个改变了this指向的函数,并不会自己执行

    call,apply该用哪个?

    • 参数数量,顺序确定就用call,参数数量,顺序不确定就用apply
    • 参数数量少用call,参数数量多用apply
    • 参数集合已经是一个数组的情况,最好用apply

    bind的应用场景

    1.保存参数

    我们先来看一道经典面试题

    for(var i=1;i<6;i++){
      setTimeout(()=>{
        console.log(i)  // 6,6,6,6,6
      },i*1000)
    }
    

    相信大家都知道这里会打印出五个6,因为在执行settimeout回调函数时,i已经变成了6

    那么如何让它打印出1,2,3,4,5呢?

    当然方法有很多,比如闭包、将var改成let使它形成块级作用域,这里先不讲,后面单独讲闭包会提出来

    我们也可以用bind来解决

    for(var i=1;i<6;i++){
      setTimeout(function(i){
        console.log(i) // 1,2,3,4,5
      }.bind(null,i),i*1000)
    }
    
    2.回调函数this丢失问题
    var student = {
      subject:['JS','VUE','REACT'],
      study: function(){
        setTimeout(function(){
          console.log(`我是南玖,我在学习${this.subject.join('、')}`)
        }.bind(this),0)
      }
    }
    student.study() //我是南玖,我在学习JS、VUE、REACT
    

    想一想这里settimeout的回调如果不用bind绑定this,结果会怎样?

    结果是报错,因为不给settimeout回调函数绑定this的话,那它的this应该指向的是全局window,全局没有subject,调用join会报错

    模拟call

    思路:

    1. 根据call的规则设置上下文对象,也就是this的指向。
    2. 通过设置context的属性,将函数的this指向到context上
    3. 通过隐式绑定执行函数并传递参数。
    4. 删除临时属性,返回函数执行结果
    Function.prototype.myCall = function(context){
        // context指的是那个想要借方法的对象,并为它指定默认值,没传就是window
        var context = context || window
        // 将要借用的那个方法绑定在当前要使用该方法的对象的fn属性上
        context.fn = this
        // 这里的this指向你想要借用的那个方法也就是.myCall前面的调用者(这里的this指的是一个函数)
        console.log(this)
        //获取参数,也就是相当于call的参数列表
        var args = [...arguments].slice(1)
        // 将参数传给该函数并执行
        var res = context.fn(...args)
        // 删除该方法
        delete context.fn
        // 返回执行结果
        return res
    }
    

    模拟apply

    思路:

    • 与call类似,主要区别是参数的处理
    /* 
    实现原理与call类似,主要是参数不同,apply接受一个参数数组
    */
    Function.prototype.myApply = function (context){
        var context = context || window
        context.fn = this
        // 判断第二个参数是否为数组,不为数组需提示用户(报错提示)
        console.log(arguments.length)
        if(arguments.length > 2){
            throw new Error('只能传递两个参数')
        }else if(!(arguments[1] instanceof Array)){
            throw new Error('第二个参数需要是数组类型')
        }
        var res = context.fn(...arguments[1])
        delete context.fn
        return res
    }
    

    模拟bind

    Function.prototype.myBind = function(context){
        var context = context || window
        var _this = this
        var args = [...arguments].slice(1)
    //    这里返回的是一个函数
        var res = function(){
            return _this.apply(context,...args)
        }
        return res
    }
    
    作者:前端南玖

    -------------------------------------------

    个性签名:智者创造机会,强者把握机会,弱者坐等机会。做一个灵魂有趣的人!

    如果觉得这篇文章对你有小小的帮助的话,可以关注下方公众号,在该公众号同样会推送技术文章给大家,谢谢~

    欢迎加入前端技术交流群:928029210

    逐梦wx
  • 相关阅读:
    Nowcoder9981A.串(排列组合)
    267D.Fedor and Essay(强连通分量缩点+DAG上DP)
    1290C. Prefix Enlightenment(带权并查集)
    LeetCode1. 两数之和
    LeetCode451. 根据字符出现频率排序
    LeetCode205. 同构字符串
    LeetCode290. 单词规律
    LeetCode202. 快乐数
    LeetCode242. 有效的字母异位词
    LeetCode136. 只出现一次的数字
  • 原文地址:https://www.cnblogs.com/songyao666/p/15426068.html
Copyright © 2011-2022 走看看