一篇搞懂this指向,趕超70%的前端人

2022-09-06 18:02:34
同事因為this指向的問題卡住的bugvue2的this指向問題,使用了箭頭函數,導致拿不到對應的props。當我給他介紹的時候他竟然不知道,隨後也刻意的看了一下前端交流群,至今最起碼還有70%以上的前端程式設計師搞不明白,今天給大家分享一下this指向,如果啥都沒學會,請給我一個大嘴巴子。

1. 呼叫位置

  • 作用域跟在哪裡定義有關,與在哪裡執行無關
  • this指向跟在哪裡定義無關,跟如何呼叫,通過什麼樣的形式呼叫有關
  • this(這個) 這個函數如何被呼叫(方便記憶)
  • 為了方便理解,預設情況下不開啟嚴格模式

2. 繫結規則

  上面我們介紹了,this的指向主要跟通過什麼樣的形式呼叫有關。接下來我就給大家介紹一下呼叫規則,沒有規矩不成方圓,大家把這幾種呼叫規則牢記於心就行了,沒有什麼難的地方。

  • 你必須找到呼叫位置,然後判斷是下面四種的哪一種繫結規則
  • 其次你要也要曉得,這四種系結規則的優先順序
  • 這兩點你都知道了 知道this的指向對於你來說 易如反掌

2.1 預設繫結

   函數最常用的呼叫方式,呼叫函數的型別:獨立函數呼叫

function bar() {
  console.log(this) // window
}
  • bar是不帶任何修飾符的直接呼叫 所以為預設繫結 為window
  • 在嚴格模式下 這裡的thisundefined

2.2 隱式繫結

  用最通俗的話表示就是:物件擁有某個方法,通過這個物件存取方法且直接呼叫(注:箭頭函數特殊,下面會講解)

const info = {
  fullName: 'ice',
  getName: function() {
    console.log(this.fullName)
  }
}

info.getName() // 'ice'
  • 這個函數被info發起呼叫,進行了隱式繫結,所以當前的thisinfo,通過this.fullName毫無疑問的就存取值為ice

隱式丟失 普通

  有些情況下會進行隱式丟失,被隱式繫結的函數會丟失繫結物件,也就是說它為變為預設繫結,預設繫結的this值,為window還是undefined取決於您當前所處的環境,是否為嚴格模式。

const info = {
  fullName: 'ice',
  getName: function() {
    console.log(this.fullName)
  }
}

const fn = info.getName

fn() //undefined

  這種情況下就進行了隱式丟失,丟失了繫結的物件,為什麼會產生這樣的問題呢?如果熟悉記憶體的小夥伴,就會很容易理解。

  • 這裡並沒有直接呼叫,而是通過info找到了對應getName的記憶體地址,賦值給變數fn
  • 然後通過fn 直接進行了呼叫
  • 其實這裡的本質 就是獨立函數呼叫 也就是為window,從window中取出fullName屬性,必定為undefined

隱式丟失 進階
這裡大家首先要理解什麼是回撥函數。其實可以這樣理解,就是我現在不呼叫它,把他通過引數的形式傳入到其他地方,在別的地方呼叫它。

//申明變數關鍵字必須為var
var fullName = 'panpan'

const info = {
  fullName: 'ice',
  getName: function() {
    console.log(this.fullName)
  }
}

function bar(fn) {
  //fn = info.getName
  fn() // panpan
}

bar(info.getName)
  • 首先bar中的fn為一個回撥函數
  • fn = info.getName 引數傳遞就是一種隱式賦值,其實跟上面的隱式丟失是一個意思,他們都是指向的fn = info.getName參照,也就是它們的記憶體地址
  • 因為他們的this丟失,也就是函數獨立呼叫,預設繫結規則,this為全域性的window物件
  • 注意: 為什麼申明必須為var呢?
    • 因為只有var申明的變數才會加入到全域性window物件上
    • 如果採用let\const 則不是,具體的後續介紹一下這兩個申明變數的關鍵字
  • 但是有些場景,我不想讓隱式丟失怎麼辦,下面就來給大家介紹一下顯示繫結,也就是固定呼叫。

2.3 顯示繫結

  但是在某些場景下,this的改變都是意想不到的,實際上我們無法控制回撥函數的執行方式,因此沒有辦法控制呼叫位置已得到期望的繫結即this指向。

接下來的顯示繫結就可以用來解決這一隱式丟失問題。

2.3.1 call/apply/bind

  js中的 」所有「函數都有一些有用的特性,這個跟它的原型鏈有關係,後續我會在原型介紹,通過原型鏈js中變相實現繼承的方法,其中call/apply/bind這三個方法就是函數原型鏈上的方法,可以在函數中呼叫它們。

2.3.2 call

  • call() 方法使用一個指定的 this 值和單獨給出的一個或多個引數來呼叫一個函數。
    • 第一個引數為固定繫結的this物件
    • 第二個引數以及二以後的引數,都是作為引數進行傳遞給所呼叫的函數
  • 備註
    • 該方法的語法和作用與 apply() 方法類似,只有一個區別,就是 call() 方法接受的是一個參數列,而 apply() 方法接受的是一個包含多個引數的陣列
var fullName = 'panpan'

const info = {
  fullName: 'ice',
  getName: function(age, height) {
    console.log(this.fullName, age, height)
  }
}

function bar(fn) {
  fn.call(info, 20, 1.88) //ice 20 1.88
}

bar(info.getName)

2.3.3 apply

  • call的方法類似,只是參數列有所不同
    • 引數
      • call 引數為單個傳遞
      • apply 引數為陣列傳遞
var fullName = 'panpan'

const info = {
  fullName: 'ice',
  getName: function(age, height) {
    console.log(this.fullName, age, height)
  }
}

function bar(fn) {
  fn.apply(info, [20, 1.88]) //ice 20 1.88
}

bar(info.getName)

2.3.4 bind

  • bindapply/call之間有所不同,bind傳入this,則是返回一個this繫結後的函數,呼叫返回後的函數,就可以拿到期望的this。
  • 引數傳遞則是
    • 呼叫bind時,可以傳入引數
    • 呼叫bind返回的引數也可以進行傳參
var fullName = 'panpan'

const info = {
  fullName: 'ice',
  getName: function(age, height) {
    console.log(this.fullName, age, height) //ice 20 1.88
  }
}

function bar(fn) {
  let newFn = fn.bind(info, 20)
  newFn(1.88)
}

bar(info.getName)

2.4 new繫結

  談到new關鍵字,就不得不談建構函式,也就是JS中的 "類",後續原型篇章在跟大家繼續探討這個new關鍵字,首先要明白以下幾點,new Fn()的時候發生了什麼,有利於我們理解this的指向。

  • 建立了一個空物件

  • 將this指向所建立出來的物件

  • 把這個物件的[[prototype]] 指向了建構函式的prototype屬性

  • 執行程式碼塊程式碼

  • 如果沒有明確返回一個非空物件,那麼返回的物件就是這個建立出來的物件

function Person(name, age) {
  this.name = name
  this.age = age

}

const p1 = new Person('ice', 20)

console.log(p1) // {name:'ice', age:20}
  • 當我呼叫new Person()的時候,那個this所指向的其實就是p1物件

3. 繫結優先順序

3.1 隱式繫結 > 預設繫結

function bar() {
  console.log(this) //info
}

const info = {
  bar: bar
}

info.bar()
  • 雖然這邊比較有些勉強,有些開發者會認為這是預設繫結的規則不能直接的顯示誰的優先順序高
  • 但是從另外一個角度來看,隱式繫結,的this丟失以後this才會指向widonw或者undefined,變相的可以認為隱式繫結 > 預設繫結

3.2 顯示繫結 > 隱式繫結

var fullName = 'global ice'
const info = {
  fullName: 'ice',
  getName: function() {
    console.log(this.fullName) 
  }
}

info.getName.call(this) //global ice
info.getName.apply(this) //global ice
info.getName.bind(this)() //global ice
  • 通過隱式繫結和顯示繫結的一起使用很明顯 顯示繫結 > 隱式繫結

3.3 bind(硬繫結) > apply/call

function bar() {
  console.log(this) //123
}

const newFn = bar.bind(123)
newFn.call(456)

3.4 new繫結 > bind繫結

首先我們來說一下,為什麼是和bind比較,而不能對callapply比較,思考下面程式碼

const info = {
  height: 1.88
}

function Person(name, age) {
  this.name = name
  this.age = age
}

const p1 = new Person.call('ice', 20)

//報錯: Uncaught TypeError: Person.call is not a constructor

new繫結和bind繫結比較

const info = {
  height: 1.88
}

function Person(name, age) {
  this.name = name
  this.age = age
}

const hasBindPerson = Person.bind(info)

const p1 = new hasBindPerson('ice', 20)

console.log(info) //{height: 1.88}
  • 我們通過bindPerson進行了一次劫持,硬繫結了this為info物件
  • new 返回的固定this的函數
  • 但是我們發現 並不能干預this的指向

3.5 總結

new關鍵字 > bind > apply/call > 隱式繫結 > 預設繫結

4. 箭頭函數 (arrow function)

首先箭頭函數是ES6新增的語法

const foo = () => {}

4.1 箭頭函數this

var fullName = 'global ice'

const info = {
  fullName: 'ice',
  getName: () => {
    console.log(this.fullName)
  }
}

info.getName() //global ice
  • 你會神奇的發現? 為什麼不是預設繫結,列印結果為ice
  • 其實這是ES6的新特性,箭頭函數不繫結this,它的this是上一層作用域,上一層作用域為window
  • 所以列印的結果是 global ice

4.2 箭頭函數的應用場景 進階

  • 需求: 在getObjName通過this拿到info中的fullName (值為icefullName)
const info = {
  fullName: 'ice',
  getName: function() {
    let _this = this
    return {
      fullName: 'panpan',
      getObjName: function() {
        console.log(this) // obj
        console.log(_this.fullName)
      }
    }
  }
}

const obj = info.getName()
obj.getObjName()
  • 當我呼叫 info.getName() 返回了一個新物件

  • 當我呼叫返回物件的getObjName方法時,我想拿到最外層的fullName,我通過,getObjName的this存取,拿到的this卻是obj,不是我想要的結果

  • 我需要在呼叫info.getName() 把this儲存下來,info.getName() 是通過隱式呼叫,所以它內部的this就是info物件

  • getObjName是obj物件,因為也是隱式繫結,this必定是obj物件,繞了一大圈我只是想拿到上層作用域的this而已,恰好箭頭函數解決了這一問題

const info = {
  fullName: 'ice',
  getName: function() {
    return {
      fullName: 'panpan',
      getObjName: () => {
        console.log(this.fullName)
      }
    }
  }
}

const obj = info.getName()
obj.getObjName()

5. 總結

5.1 this的四種系結規則

  • 預設繫結

  • 隱式繫結

  • 顯示繫結 apply/call/bind(也稱硬繫結)

  • new繫結

5.2 this的優先順序 從高到低

  • new繫結

  • bind

  • call/apply

  • 隱式繫結

  • 預設繫結

6. 結語

  當一切都看起來不起作用的時候,我就會像個石匠一樣去敲打石頭,可能敲100次,石頭沒有任何反應,但是101次,石頭可能就會裂為兩半 我知道並不是第101次起了作用,而是前面積累所致。

  大家有疑惑可以在評論區留言 第一時間為大家解答。

(學習視訊分享:)

前端(vue)入門到精通課程: