本文對JavaScript中的this關鍵字進行全方位的解析,看完本篇文章,但願讀者們可以徹底理解this的綁定問題。app
開篇:對於那些沒有投入時間去學習this機制的JavaScript開發者來講,this的綁定是一件使人困惑的事。(包括曾經的本身)。函數
誤區:學習this的第一步是明白this既不指向函數自己也不指向函數的詞法做用域,你是否被相似這樣的解釋所誤導?但其實這種說法都是錯誤的。學習
歸納:this實際是在函數被調用時發生的綁定,它所指向的位置徹底取決於函數被調用的位置。this
在理解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
最經常使用的函數調用類型:獨立函數調用作用域
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來調用,這種函數調用稱爲構造函數調用。
重點:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。
function setName(name) { this.name = name } var person = new setName('kyrie') console.log(person.name) // 'kyrie'
使用new調用setName()時,會建立一個新對象並把這個新對象綁定到setName()調用的this上,並把這個對象返回。
毫無疑問,默認綁定的優先級是四條規則中最低的,因此暫不考慮它。
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'
結果,顯式綁定的優先級比隱式綁定高。
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綁定的優先級比隱式綁定高
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的指向。
var p1 = new Person()
var p1 = setName.call(p2)
var p2 = p1.setName()
var p1 = setName()
以上上提到判斷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的綁定對象。
ES6的箭頭函數不適用以上四條規則,而是根據當前的詞法做用域來決定this綁定,也就是說,箭頭函數會繼承外層函數調用的this綁定(不管綁定到什麼),並且箭頭函數的this綁定沒法被修改。