《你不知道的javascript》學習總結I - this關鍵字

javascript中的this關鍵字

上一張初略版this分析導圖先:

this在英文中是一個指示代詞,指示將要表達中的名詞。

js王國中的this有會是指向誰了,先看看this常有的誤解認識

  • 誤解1: this指向函數本身

    function fun(){
        this.count++
        console.log('invoke fun')
    }
    fun.count=0
    fun()
    console.log(fun.count)
    
    輸出: 
    invoke fun
    0
    

    上面的代碼并沒有輸出1, 可見this并非指向函數本身

  • 誤解2: this指向當前作用域

    function f1(){
        var a = 'inner.f1'
        this.f2()
    }
    
    function f2(){
        console.log('a='+this.a)
    }
    
    var a = 'global'
    f1()
    輸出:
    a=global
    

    同樣上面的代碼然而并沒有輸出'a=inner.f1',這樣this也并非指向當前作用域。

上面已經描述this不是什么了,那么this到底該指向什么了。 其實在js中一個函數被調用時,引擎會創建一個執行上下文記錄,該記錄包含函數調用位置,函數調用方式,傳入的參數信息。this就是執行上下文記錄中的一個屬性。 this是動態綁定的,并非編寫代碼時決定的。通過分析函數的調用位置,分析出this的綁定對象。這其中有四大綁定規則。

一、綁定規則:

  1. 默認綁定

    函數作為獨立調用方式時,其采用默認綁定,或者無法應用其他綁定規則時的使用的默認綁定規則。默認綁定時,this指向全局對象。
    例如:

    function fun(){
        console.log('name:'+this.name)
    }
    var name = 'global'
    fun()
    輸出: name:global
    

    fun函數在上面代碼中使用獨立調用方式,this指向了全局對象,name作為全局定義的對象。
    *** 當上述代碼fun內部運行在strict mode下時,全局定義的對象不能用于默認綁定,同時會報錯TypeError: this is undefined。

  2. 隱式綁定
    一個對象中含有指向函數的引用屬性,并通過這個屬性間接引用函數,從而把this隱式綁定到這個對象。這就是隱式綁定。簡單點說:
    當函數有被對象包含著時,這是函數內的this便指向被包含對的對象,上代碼示例:

    function fun(){
        console.log('fun name:'+this.name)
    }
    var obj = {
        name: 'obj',
        fun: fun,
        funX: function(){
            console.log('funX name:'+this.name)
        }
    }
    
    obj.fun()
    obj.funX()
    輸出:
    fun name:obj
    funX name:obj
    

    可以看到上面代碼中調用方式:obj.fun/obj.funX, 這是函數內部的this指向了obj對象,這樣this.name就會使用了obj.name

  3. 顯示綁定
    如果按照隱式綁定中的方法調用,但不想要this綁定到默認對象,可以通過js中另外兩種調用方式apply/call,這兩種方法中第一個參數都是為顯示為函數內部this指向的對象,上代碼示例:

    var objA={
        name: 'objA'
    }
    var objB={
        name: 'objB',
        fun: function(){
            console.log('name:' + this.name)
        }
    }
    
    objB.fun()
    objB.fun.call(objA)
    objB.fun.apply(objA)
    輸出:
    name:objB
    name:objA
    name:objA
    

    上面代碼中,objB.fun()使用了隱式綁定,輸出了objB中的name屬性值,另外兩種調用方式則改變了函數中的this對象使其綁定到了對象objA.

    ***同時還有一種顯示綁定方式bind,將其稱為硬綁定。

    var objA={
        name: 'objA'
    }
    var objB={
        name: 'objB',
        fun: function(){
            console.log('name:' + this.name)
        }
    }
    
    var fun = objB.fun.bind(objA)
    fun()
    輸出:
    name:objA
    

    上面代碼中通過bind方式定義了一個函數引用fun,后續調用時,其中的this指向了定義時的對象,故而輸出了objA,name值

  4. new綁定
    在函數作為構造函數方式調用時,函數內的this指向了新創建的對象。
    例如:

    function Fun(name){
        this.name = name
    }
    
    var fun = new Fun('fun')
    console.log(fun.name)
    輸出:
    fun
    

    上述函數Fun作為構造函數方式調用(其實Fun也是js中一名普通的函數),其實此時發生了如下幾件事:

    1. 創建一個對象
    2. 將新創建的對象綁定到函數中的this上
    3. 新創建的對象的[[prototype]]指向了Fun.prototype
    4. 如果函數內部沒有顯示返回值,那么new表達式執行完后默認返回該新創建的對象

    所以如上顯示的那樣,輸出了'fun'

有了上面描述的四大綁定規則,那么在實際的函數方法中,該使用哪種綁定規則判斷this了,此時還需要了解這其中的優先級關系。

二、優先級判斷

唔有疑問,默認綁定無疑是四種優先級最低的,先擱置一邊,探索下其余三種的優先級。
隱式綁定 VS 顯示綁定


var objA={
    name: 'objA'
}
var objB={
    name: 'objB',
    fun: function(){
        console.log('name:' + this.name)
    }
}

objB.fun()
objB.fun.call(objA)
name:objB
name:objA

上述代碼可以看出顯示綁定優先級高于隱式綁定規則: 顯示綁定 > 隱式綁定

接下來在看下隱式綁定與new綁定的優先級關系:
隱式綁定 VS new綁定


var obj={
    name: null,
    setName: function(name){
        this.name = name
    }
}

obj.setName('obj')
console.log('obj.name:' + obj.name)
var obj2 = new obj.setName('obj2')
console.log('obj.name:' + obj.name)
console.log('obj2.name:' + obj2.name)
輸出:
obj.name:obj
obj.name:obj
obj2.name:obj2

上述代碼中new obj.setName('obj2'), 并非更改了obj對象中的name屬性,可見new綁定的優先級高于隱式綁定: new綁定 > 隱式綁定

最后看下顯示綁定與new綁定方式優先級別:
顯示綁定 VS new綁定方式


apply/call方式不能與new連用,此處使用硬綁定方式bind與new連用

var obj={
    name:null
}

function setName(name){
    this.name = name
}

var newSetName = setName.bind(obj)
var newObj = new newSetName('new')

console.log('obj.name:'+obj.name)
console.log('newObj.name:'+newObj.name)
輸出:
obj.name:null
newObj.name:new

上面代碼看出及時setName顯示硬綁定this為obj,但是new方式調用時還是更改了setName中的this引用指向,由此可見new綁定優先級高于顯示綁定:new綁定 > 顯示綁定

那么總結上面的優先級規則:

new綁定 > 顯示綁定 > 隱式綁定 > 默認綁定

平常情況按照如上規則判斷均會有效,但凡事均會有例外,再看下例外的規則

例外1. 顯示綁定apply/call/bind時傳入了null/undefined時,此時this并非指向了null/undefined,而是使用了默認綁定規則

function fun(){
    console.log(this.name)
}

var name = 'outer'

fun.call(null)
輸出
outer

上述this.name輸出了全局變量name值。
當然你會問為什么需要傳入null值,有一種case如需要使用bind預設值一些參數值,如:

function multiple(mul, number){
    console.log('result:' + (mul * number))
}

var fun = multiple.bind(null, 5)

fun(2)
輸出:
result:10

例外2. 函數賦值方式形成的間接調用,此時this也是使用了默認綁定規則

function print(){
    console.log(this.name)
}

var name = 'outer'
var obj={
    name: 'inner',
    print: print
}

var fun
(fun = obj.print)()
輸出:
outer

寫在最后

ES6中的箭頭函數,this的規則比較特殊,它是繼承了外層的this對象,由外層的作用域來決定。
上代碼:

function fun(){
    return () => {
        console.log(this.name)
    }
}

var obj1={
    name:'obj1'
}

var obj2={
    name:'obj2'
}

var reFun = fun.call(obj1)
reFun.call(obj2)
輸出:
obj1

上述代碼可以看出并沒有輸出obj2,而是輸出了fun函數調用時this指向的對象obj1對應的name屬性,可以看出,箭頭函數中的this指向了外層函數中的this對象。可以看出箭頭函數有些反this機制,而是采用了“靜態”綁定。

參考資料:[美]KYLE SIMPSON著 趙望野 梁杰 譯 《你不知道的javascript》上卷

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容