深刻JavaScript系列(四):完全搞懂this

1、函數的調用

全局環境的this指向全局對象,在瀏覽器中就是咱們熟知的window對象git

說到this的種種狀況,就離不開函數的調用,通常咱們調用函數,無外乎如下四種方式:github

  1. 普通調用,例如foo()
  2. 做爲對象方法調用,例如obj.foo()
  3. 構造函數調用,例如new foo()
  4. 使用callapplybind等方法。

除箭頭函數外的其餘函數被調用時,會在其詞法環境上綁定this的值,咱們能夠經過一些方法來指定this的值。瀏覽器

  1. 使用callapplybind等方法來顯式指定this的值。
    function foo() {
        console.log(this.a)
    }
    foo.call({a: 1}) // 輸出: 1
    foo.apply({a: 2}) // 輸出: 2
    // bind方法返回一個函數,須要手動進行調用
    foo.bind({a: 3})() // 輸出: 3
    複製代碼
  2. 當函數做爲對象的方法調用時,this的值將被隱式指定爲這個對象。
    let obj = {
        a: 4,
        foo: function() {
            console.log(this.a)
        }
    }
    obj.foo() // 輸出: 4
    複製代碼
  3. 當函數配合new操做符做爲構造函數調用時,this的值將被隱式指定新構造出來的對象。

2、ECMAScript規範解讀this

上面講了幾種比較容易記憶和理解this的狀況,咱們來根據ECMAScript規範來簡單分析一下,這裏只說重點,一些規範內具體的實現就不講了,反而容易混淆。app

其實當咱們調用函數時,內部是調用函數的一個內置[[Call]](thisArgument, argumentsList)方法,此方法接收兩個參數,第一個參數提供this的綁定值,第二個參數就是函數的參數列表。函數

ECMAScript規範: 嚴格模式時,函數內的this綁定嚴格指向傳入的thisArgument。非嚴格模式時,若傳入的thisArgument不爲undefinednull時,函數內的this綁定指向傳入的thisArgument;爲undefinednull時,函數內的this綁定指向全局的thispost

因此第一點中講的三種狀況都是顯式或隱式的傳入了thisArgument來做爲this的綁定值。咱們來用僞代碼模擬一下:ui

function foo() {
    console.log(this.a)
}

/* -------顯式指定this------- */
foo.call({a: 1})
foo.apply({a: 1})
foo.bind({a: 1})()
// 內部均執行
foo[[Call]]({a: 1})

/* -------函數構造調用------- */
new foo()
// 內部執行
let obj = {}
obj.__proto__ = foo.prototype
foo[[Call]](obj)
// 最後將這個obj返回,關於構造函數的詳細內容可翻閱我以前關於原型和原型鏈的文章

/* -------做爲對象方法調用------- */
let obj = {
    a: 4,
    foo: function() {
        console.log(this.a)
    }
}
obj.foo()
// 內部執行
foo[[Call]]({
    a: 1,
    foo: Function foo
})
複製代碼

那麼當函數普通調用時,thisArgument的值並無傳入,即爲undefined,根據上面的ECMAScript規範,若非嚴格模式,函數內this指向全局this,在瀏覽器內就是window。this

僞代碼模擬:spa

window.a = 10
function foo() {
    console.log(this.a)
}
foo() // 輸出: 10
foo.call(undefined) // 輸出: 10
// 內部均執行
foo[[Call]](undefined) // 非嚴格模式,this指向全局對象

foo.call(null) // 輸出: 10
// 內部執行
foo[[Call]](null) // 非嚴格模式,this指向全局對象
複製代碼

根據上面的ECMAScript規範,嚴格模式下,函數內的this綁定嚴格指向傳入的thisArgument。因此有如下表現。prototype

function foo() {
 'use strict'
    console.log(this)
}
foo() // 輸出:undefined
foo.call(null) // 輸出:null
複製代碼

須要注意的是,這裏所說的嚴格模式是函數被建立時是否爲嚴格模式,並不是函數被調用時是否爲嚴格模式:

window.a = 10
function foo() {
    console.log(this.a)
}
function bar() {
 'use strict'
    foo()
}
bar() // 輸出:10
複製代碼

3、箭頭函數中的this

ES6新增的箭頭函數在被調用時不會綁定this,因此它須要去詞法環境鏈上尋找this

function foo() {
    return () => {
        console.log(this)
    }
}
const arrowFn1 = foo()
arrowFn1() // 輸出:window
           // 箭頭函數沒有this綁定,往外層詞法環境尋找
           // 在foo的詞法環境上找到this綁定,指向全局對象window
           // 在foo的詞法環境上找到,並不是是在全局找到的
const arrowFn2 = foo.call({a: 1})
arrowFn2() // 輸出 {a: 1}
複製代碼

切記,箭頭函數中不會綁定this,因爲JS採用詞法做用域,因此箭頭函數中的this只取決於其定義時的環境。

window.a = 10
const foo = () => {
    console.log(this.a)
}
foo.call({a: 20}) // 輸出: 10

let obj = {
    a: 20,
    foo: foo
}
obj.foo() // 輸出: 10

function bar() {
    foo()
}
bar.call({a: 20}) // 輸出: 10
複製代碼

4、回調函數中的this

當函數做爲回調函數時會產生一些怪異的現象:

window.a = 10
let obj = {
    a: 20,
    foo: function() {
        console.log(this.a)
    }
}

setTimeout(obj.foo, 0) // 輸出: 10
複製代碼

我以爲這麼解釋比較好理解:obj.foo做爲回調函數,咱們其實在傳遞函數的具體值,而並不是函數名,也就是說回調函數會記錄傳入的函數的函數體,達到觸發條件後進行執行,僞代碼以下:

setTimeout(obj.foo, 0)
//等同於,先將傳入回調函數記錄下來
let callback = obj.foo
// 達到觸發條件後執行回調
callback()
// 因此foo函數並不是做爲對象方法調用,而是做爲函數普通調用
複製代碼

要想避免這種狀況,有三種方法,第一種方法是使用bind返回的指定好this綁定的函數做爲回調函數傳入:

setTimeout(obj.foo.bind({a: 20}), 0) // 輸出: 20
複製代碼

第二種方法是儲存咱們想要的this值,就是常見的,具體命名視我的習慣而定。

let _this = this
let self = this
let me = this
複製代碼

第三種方法就是使用箭頭函數

window.a = 10
function foo() {
    return () => {
        console.log(this.a)
    }
}
const arrowFn = foo.call({a: 20})
arrowFn() // 輸出:20
setTimeout(arrowFn, 0) // 輸出:20
複製代碼

5、總結

  1. 箭頭函數中沒有this綁定,this的值取決於其建立時所在詞法環境鏈中最近的this綁定
  2. 非嚴格模式下,函數普通調用,this指向全局對象
  3. 嚴格模式下,函數普通調用,thisundefined
  4. 函數做爲對象方法調用,this指向該對象
  5. 函數做爲構造函數配合new調用,this指向構造出的新對象
  6. 非嚴格模式下,函數經過callapplybind等間接調用,this指向傳入的第一個參數

    這裏注意兩點:

    1. bind返回一個函數,須要手動調用,callapply會自動調用
    2. 傳入的第一個參數若爲undefinednullthis指向全局對象
  7. 嚴格模式下函數經過callapplybind等間接調用,this嚴格指向傳入的第一個參數

有時候文字的表述是蒼白無力的,真正理解以後會發現:this不過如此。

6、小練習

例子來自南波的JavaScript之例題中完全理解this

// 例1
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()  // ?
person1.show1.call(person2)  // ?

person1.show2()  // ?
person1.show2.call(person2)  // ?

person1.show3()()  // ?
person1.show3().call(person2)  // ?
person1.show3.call(person2)()  // ?

person1.show4()()  // ?
person1.show4().call(person2)  // ?
person1.show4.call(person2)()  // ?
複製代碼

選中下方查看答案:

person1 // 函數做爲對象方法調用,this指向對象

person2 // 使用call間接調用函數,this指向傳入的person2

window // 箭頭函數無this綁定,在全局環境找到this,指向window

window // 間接調用改變this指向對箭頭函數無效

window // person1.show3()返回普通函數,至關於普通函數調用,this指向window

person2 // 使用call間接調用函數,this指向傳入的person2

window // person1.show3.call(person2)仍然返回普通函數

person1 // person1.show4調用對象方法,this指向person1,返回箭頭函數,this在person1.show4調用時的詞法環境中找到,指向person1

person1 // 間接調用改變this指向對箭頭函數無效

person2 // 改變了person1.show4調用時this的指向,因此返回的箭頭函數的內this解析改變

系列文章

深刻ECMAScript系列目錄地址(持續更新中...)

歡迎前往閱讀系列文章,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

菜鳥一枚,若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤,與你們共同進步。

相關文章
相關標籤/搜索