【JavaScript】改变函数的this指向——call函数、apply函数、bind函数

JavaScript 为我们专门提供了一些函数方法来帮我们处理函数内部 this 的指向问题,常用的有 bind(),call(),apply() 三种方法

一、call函数

call()方法可以立即调用一个函数,并可以改变该函数的this指向

使用方法: fun.call(thisArg,arg1,arg2,…)

  • thisArg: 在 fun 函数运行时指定的 this 值
  • arg1,arg2,…: 其他参数
  • 返回值: 就是fun函数的返回值,因为它就是调用函数

因此当我们想改变 this 指向,同时想调用这个函数的时候,可以使用 call,比如继承

function Father(uname, age) {
    this.uname = uname;
    this.age = age;
}

function Son(uname, age) {
    Father.call(this, uname, age);
}

var son = new Son('xiaoming', 18);
console.log(son.uname); //xiaoming

Son构造函数想继承Father构造函数的属性,在Son构造函数中,调用Father构造函数,同时改变this指向,让其指向Son构造函数。这个时候用到了call函数。

实现继承的过程: 创建一个Son的实例化对象son,这时把‘xiaoming’,18分别传给function Son(uname, age)中的uname,age,此时形参uname=‘xiaoming’; age=18; 然后function Son(uname, age)中有一个函数Father.call(this, uname, age),这里的uname和age就是形参,所以他们此时也有了对应的值,然后第一个参数改为this,Son构造函数里面的this应该指向它的实例化对象,也就是这里的son。带着这三个参数去调用Father函数,也就是son的uname属性赋值为形参uname的值‘xiaoming’,son的属性赋值为形参age的值18。实现了继承。

二、apply函数

apply()方法可以立即调用一个函数,并可以改变该函数的this指向

使用方法: fun.apply(thisArg,[argsArray])

  • thisArg: 在 fun 函数运行时指定的 this 值
  • [argsArray]: 传递的值,数组或伪数组形式
  • 返回值: 就是fun函数的返回值,因为它就是调用函数

因为它传入的参数是一个数组,所以我们可以用它处理一些和数组有关的问题,比如借用Math.max()求数组的最大值/最小值。

var arr = [15, 6, 9, 33];
   	var max = Math.max.apply(Math, arr); //max = 33;
   	var min = Math.min.apply(Math, arr); //min = 6;

Math.max是一个方法(函数),对其使用apply方法,这里不需要改变this指向,所以第一个参数让它指回Math,第2个参数传入要处理的数组即可。这样就可以借用Math方法处理数组了。

apply方法会自动将传入的数组处理为对应的数据类型。

三、bind函数

bind函数不会直接调用函数,但是可以改变函数内部的this指向,应用非常广泛。

使用方法: fun.bind(thisArg,arg1,arg2,…)

  • thisArg: 在 fun 函数运行时指定的 this 值
  • arg1,arg2,…: 其他参数
  • 返回值: 由指定的 this值和初始化参数改造的原函数拷贝,也就是说返回值是一个函数

因此当我们只是想改变 this 指向,并不想立即调用这个函数的时候,可以使用bind,比如改变定时器的this指向

比如有一个需求,点击某个按钮后,按钮被禁用,2秒后恢复正常。这里定时器里面的this指向的是window,而我们想让它指向当前被点击的button,因此需要修改this指向,但是我们又不希望它立即调用,而是等到满足定时器的条件后才执行,所以要用到bind函数

这里有一个很巧妙的点,所以function () { this.disabled = false; }.bind(this) 中this已经在定时器的外面了,所以他不再是指向window,而定时器外面的环境是按钮点击事件,也就是说this指向的是当前点击的btn,而通过bind函数,巧妙的把定时器函数的this指向改成了当前点击的btn,就可以实现当前功能。

<body>
    <button>点击button>
    <button>点击button>
    <button>点击button>
    <script>
        var btns = document.querySelectorAll('button');
        for (var i = 0; i < btns.length; i++) {
            btns[i].addEventListener('click', function () {
                this.disabled = true;
                setTimeout(function () {
                    this.disabled = false;
                }.bind(this), 2000);
            })
        }
    script>
body>

再比如我们之前写过的面向对象Tab栏【JavaScript】面向对象的Tab栏分页切换案例,当时为了使用全局this,是在class外面定义了一个that,把this先存起来,方便后面使用,这样不太好,需要随时记得定义一个that。我们可以试着用bind函数优化一下,不在定义新变量that,以toggleTab函数举例。

原代码:

var that;
class Tab {
    //传入要做Tab栏的元素的id
    constructor(id){
    that = this;
        //获取元素
        this.main = document.getElementById(id);
        this.ul=this.main.querySelector('.tab_list ul:first-child');
        this.con=this.main.querySelector('.tab_con');
        this.add=this.main.querySelector('.tabAdd');
        this.init();
    }
    //初始化函数
    init(){
        this.update();
        //绑定事件
        this.add.addEventListener('click',this.addTab);
    }
    //切换功能
    toggleTab(){
        that.clearStyle();
        this.className='current';
        that.sections[this.dataIndex].className='item';
    }
    //清除样式 排他思想
    clearStyle(){
        for(var i=0;i<this.lis.length;i++){
            this.lis[i].className='';
            this.sections[i].className='';
        }
    }
    //更新li和section
    update(){
        //重新获取lis和sections和guanbiBtns
        this.lis = this.ul.querySelectorAll('li');
        this.sections=this.con.querySelectorAll('section');
        this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
        this.spans=this.ul.querySelectorAll('li span:first-child');

        //重新绑定事件
        for(var i = 0;i<this.lis.length; i++){
            this.lis[i].dataIndex=i;
            this.lis[i].addEventListener('click', this.toggleTab);
            this.spans[i].addEventListener('dblclick',this.editTab);
            this.sections[i].addEventListener('dblclick',this.editTab);
            this.guanbiBtns[i].addEventListener('click',this.removeTab);
        }
    }
}

toggleTab函数的调用者是某个li,因此toggleTab函数里面的this指向调用的li,而我们又需要用到整个tab元素的lis,考虑更改this指向,而且这个函数不是立即执行,而是做了点击事件才执行,所以使用bind函数。但是这里也需要用到它本身的this,所以不能直接更改this指向,这样this.className=‘current’; 就有问题了,怎么样把两个不同的this都保留呢?

bind函数可以传多个参数,第一个参数是this指向,我们保持不变,(因为要用到),而第二个参数传入指向整个tab元素的this。而toggleTab函数用一个that把第二个参数接过来,就可以使用指向整个tab元素的this了。

其他的添加函数、删除函数与之类似,作出相应更改即可。

优化后的代码:

class Tab {
    //传入要做Tab栏的元素的id
    constructor(id){
        //获取元素
        this.main = document.getElementById(id);
        this.ul=this.main.querySelector('.tab_list ul:first-child');
        this.con=this.main.querySelector('.tab_con');
        this.add=this.main.querySelector('.tabAdd');
        this.init();
    }
    //初始化函数
    init(){
        this.update();
        //绑定事件
        this.add.addEventListener('click',this.addTab.bind(this.add,this));
    }
    //切换功能
    toggleTab(that){
        that.clearStyle();
        this.className='current';
        that.sections[this.dataIndex].className='item';
    }
    //清除样式 排他思想
    clearStyle(){
        for(var i=0;i<this.lis.length;i++){
            this.lis[i].className='';
            this.sections[i].className='';
        }
    }
    //更新li和section
    update(){
        //重新获取lis和sections和guanbiBtns
        this.lis = this.ul.querySelectorAll('li');
        this.sections=this.con.querySelectorAll('section');
        this.guanbiBtns=this.main.querySelectorAll('.tab_list .guanbi-btn');
        this.spans=this.ul.querySelectorAll('li span:first-child');

        //重新绑定事件
        for(var i = 0;i<this.lis.length; i++){
            this.lis[i].dataIndex=i;
            //bind函数优化///
            this.lis[i].addEventListener('click', this.toggleTab.bind(this.lis[i],this));
            this.spans[i].addEventListener('dblclick',this.editTab);
            this.sections[i].addEventListener('dblclick',this.editTab);
            this.guanbiBtns[i].addEventListener('click',this.removeTab.bind(this.guanbiBtns[i],this));
        }
    }
}

四、总结

相同点:

  • 都可以改变函数内部的this指向

不同点:

  • call 和 apply 会立即调用函数,返回值就是函数执行后的返回值
  • bind 不会立即调用函数,返回值是一个函数,改造后的原函数拷贝
  • call 和 bind 传递的参数为新的this指向和原函数的其他参数,apply 传递的参数为新的this指向和一个数组

主要应用场景

  • call() 经常实现继承
  • apply() 经常处理与数组有关的问题,比如借助于Math对象求数组的最大值或最小值
  • bind() 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向

你可能感兴趣的:(JavaScript,javascript,前端)