this指向與call,apply,bind

前端南玖發表於2021-10-19

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
}

相關文章