完全搞懂JavaScript中的this關鍵字

本文對JavaScript中的this關鍵字進行全方位的解析,看完本篇文章,但願讀者們可以徹底理解this的綁定問題。app

開篇:對於那些沒有投入時間去學習this機制的JavaScript開發者來講,this的綁定是一件使人困惑的事。(包括曾經的本身)。函數

誤區:學習this的第一步是明白this既不指向函數自己也不指向函數的詞法做用域,你是否被相似這樣的解釋所誤導?但其實這種說法都是錯誤的。學習

歸納:this實際是在函數被調用時發生的綁定,它所指向的位置徹底取決於函數被調用的位置。this

1、調用位置

在理解this的綁定過程以前,首先要理解調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。code

因此說,尋找調用位置就是尋找「函數被調用的位置」,這裏最重要的點是要分析調用棧(存放當前正在執行的函數的位置)對象

什麼是調用棧和調用位置?排序

關係:調用位置就在當前正在執行的函數(調用棧)的前一個位置繼承

function func1() {
  // 當前調用棧:func1
  // 當前調用位置是全局做用域(調用棧的前一個位置)
  console.log('func1')
  func2() // 這裏是:func2的調用位置
}
function func2() {
  // 當前調用棧:func1 -> func2
  // 當前調用位置是在func1(調用棧的前一個位置)
  console.log('func2')
  func3() // 這裏是:func3的調用位置
}
function func3() {
  // 當前調用棧:func1 -> func2 -> func3
  // 當前調用位置是在func2(調用棧的前一個位置)
  console.log('func3')
}
func1() // 這裏是:func1的調用位置

關注點:咱們是如何從調用棧中分析出真正的調用位置的,由於這決定了this的綁定ip

2、綁定規則

  • 默認綁定

最經常使用的函數調用類型:獨立函數調用作用域

function getName() {
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'kyrie'

當調用getName()時,this.name拿到了全局對象的name。由於getName()是直接調用的,不帶任何修飾符,使用的是默認綁定,所以this指向全局對象(非嚴格模式)。

若是使用嚴格模式('strict mode')呢?

function getName() {
  'use strict';
  console.log(this.name)
}
var name = 'kyrie'
getName() // 'TypeError: this is undefined'

那麼全局對象沒法使用默認綁定,所以this會綁定到undefined

  • 隱式綁定

調用位置是否有上下文對象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
person.getName() // 'kyrie'

當getName()被調用時,它的落腳點指向person對象,當函數引用有上下文對象時,隱式綁定會把函數調用中的this綁定到這個上下文對象,所以調用getName()時this被綁定到person,所以this.name跟person.name是同樣的

常見問題:隱式丟失?

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie',
  getName: getName
}
var getName2 = person.getName() // 函數別名
var name = 'wen' // name是全局對象的屬性
getName2() // 'wen' 這裏拿到的是全局對象的name

解釋:雖然getName2是person.getName的一個函數引用,但它引用的getName函數的自己,所以getName2()調用時不帶任何修飾符,使用的是默認綁定,所以this綁定了全局對象

  • 顯式綁定

使用call() / apply() / bind() 指定this的綁定對象

function getName() {
  console.log(this.name)
}
var person = {
  name: 'kyrie'
}
getName.call(person) // 'kyrie'
getName.apply(person) // 'kyrie'

經過getName.call()/ getName.apply() 調用強制把它的this綁定到person上。

  • new綁定

全部函數均可以用new來調用,這種函數調用稱爲構造函數調用

重點:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」

使用new來調用函數,或者說發生構造函數調用時,會自動執行如下的四步操做:
  1. 建立(或者構造)一個新的對象
  2. 這個新對象會被執行[[原型]]鏈接(暫時忽略,屬於原型內容,後面再介紹它)
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,則new表達式中的函數會自動返回這個新的對象
function setName(name) {
  this.name = name
}
var person = new setName('kyrie')
console.log(person.name) // 'kyrie'

使用new調用setName()時,會建立一個新對象並把這個新對象綁定到setName()調用的this上,並把這個對象返回

3、優先級

毫無疑問,默認綁定的優先級是四條規則中最低的,因此暫不考慮它。

  1. 隱式綁定和顯式綁定哪一個優先級高?
function getName() {
  console.log(this.name)
}
var p1 = {
  name: 'kyrie',
  getName: getName
}
var p2 = {
  name: 'wen',
  getName: getName
}
p1.getName() // 'kyrie'
p2.getName() // 'wen'
p1.getName.call(p2) // 'wen'
p2.getName.call(p1) // 'kyrie'

結果,顯式綁定的優先級比隱式綁定高。

  1. 隱式綁定和new綁定哪一個優先級高?
function setName(name) {
  this.name = name
}
var p1 = {
  setName: setName
}
var p2 = {}
p1.setName('kyrie')
console.log(p1.name) // 'kyrie'
p1.setName.call(p2, 'wen')
console.log(p2.name) // 'wen'
var p3 = new p1.setName('zbw')
console.log(p1.name) // 'kyrie'
console.log(p3.name) // 'zbw'

結果,new綁定的優先級比隱式綁定高

  1. 顯式綁定和new綁定的哪一個優先級高?
function setName(name) {
  this.name = name
}
var p1 = {}
// bind會返回一個新的函數
var setP1Name = setName.bind(p1)
setP1Name('kyrie')
console.log(p1.name) // 'kyrie'
var p2 = new setP1Name('wen')
console.log(p1.name) // 'kyrie'
console.log(p2.name) // 'wen'

結果,new綁定的優先級比顯示綁定高

綜上,優先級的正確排序:

從高到低: new > 顯示 > 隱式 > 默認

  • 判斷this的指向

如今咱們能夠根據優先級來判斷函數在某個位置調用this的指向。

  1. 函數是否經過new來調用(new綁定)?若是是,則this指向新建立的對象
var p1 = new Person()
  1. 函數是否經過call/apply/bind調用(顯式綁定)?若是是,則this指向第一個參數
var p1 = setName.call(p2)
  1. 函數是否在某個上下文對象中調用(隱式綁定)?若是是,則this指向該上下文對象
var p2 = p1.setName()
  1. 若是以上三個條件都不知足,則使用默認綁定。若是是在嚴格模式中,this指向undefined,不然指向全局對象。
var p1 = setName()

4、箭頭函數的this

以上上提到判斷this指向的四條規則包含全部正常的函數,除了ES6中的箭頭函數

歸納:箭頭函數不像普通函數那樣使用function關鍵字定義,而是用 「胖箭頭」 => 定義 。並且箭頭函數並不適用以上的四條規則,它的this綁定徹底是根據 外層做用域(函數或者全局) 來決定的。

function getName() {
  // 箭頭函數的this指向外層做用域
  return (name) => {
    console.log(this.name)
  }
}
var p1 = {
  name: 'kyrie'
}
var p2 = {
  name: 'wen'
}
var func = getName.call(p1)
func.call(p2) // 'kyrie'

getName()內部建立的箭頭函數會捕獲調用時外層做用域(getName)的this,因爲getName的this經過顯示綁定到p1上,因此getName裏建立的箭頭函數也會指向p1,最重要的一點:箭頭函數的this沒法被修改(即便是優先級最高的new綁定也不行

總結

要判斷一個運行中的函數的this綁定,須要找到該函數的調用位置(結合調用棧),接着根據優先級得出的四條規則來判斷this的綁定對象。

  1. 函數由new調用?綁定到新建立的對象
  2. 由call/apply/bind調用?綁定到指定對象
  3. 由上下文對象調用?綁定到上下文對象
  4. 默認:嚴格模式下綁定到undefined,不然綁定到全局對象

ES6的箭頭函數不適用以上四條規則,而是根據當前的詞法做用域來決定this綁定,也就是說,箭頭函數會繼承外層函數調用的this綁定(不管綁定到什麼),並且箭頭函數的this綁定沒法被修改

相關文章
相關標籤/搜索