js基礎:this你到底指向哪裡?

cynthiazha發表於2018-07-24

this是javascript中最常用也是比較難理解的一個關鍵字了,看了很多相關知識,以為理解的差不多了,然後偶然刷到一題又感覺自己不懂了。一直在這種懂與不懂之間徘徊了。
最近在看javascript基礎知識,有了一些自己的總結與體會。最開始是看到下面一題:

var age = 100;
var test = {
    age: 120,
    sayAge: function(){
        console.log(this.age)
        function go(){
            console.log(this.age)
        }
        go.prototype.age = 60;
        return go
    }
}
var p = test.sayAge()  // ->120
p() // ->100
複製程式碼
  • 如果將上面的程式碼最後加上一句new (test.sayAge())()
    第一步:test.sayAge() 列印120,返回go
    第二步:new go() 列印60
  • 如果將go.prototype.age = 60;這句話註釋了,執行new (test.sayAge())()
    列印結果:120,undefined(this指向new出來的物件例項,不再是window,而this這個物件上沒有age這個屬性)

之前判斷this的指向就是使用口訣:“誰呼叫它,它就指向誰”,但是有時候這個ta很讓人困惑,最後就分不清this指向誰了;有時候又錯誤的理解this指向它的作用域。比如上面的第二個問題,this沒有age,他的全域性作用域上有age的,就會錯誤的認為會列印全域性的age。例如下面的程式碼也常會錯誤將this=作用域;

var a = 100;
function car(){
    console.log(this.a)
}
var myCar = {
    car: car
}
myCar.car() //->undefined而不是100
複製程式碼

在ES6之前,每個函式的this都是在呼叫時才被繫結,所以理解函式的呼叫位置很重要(不是宣告位置)

在《你不知道的javascript》上卷中對this的指向總結了4條規則:

  • 預設繫結(函式在不帶任何修飾情況下進行呼叫時,此時函式中的this在非嚴格模式下指向window )

    function test(){
        bar() //->bar的呼叫位置
    }
    function bar(){
        foo() // ->foo的呼叫位置
    }
    function foo(){
        console.log('end')
    }
    test() // ->test的呼叫位置
複製程式碼

上面呼叫順序是test->bar->foo,找到了函式的呼叫位置,我們來分析它的this繫結,上面的三個函式都是作為獨立函式呼叫的,相當於在全域性環境下呼叫,預設的全域性環境中(非嚴格模式下),this指向window(嚴格模式下this指向undefined)。

  • 隱式繫結(呼叫位置是否有上下文或者說被某個物件擁有,此時this指向被擁有的物件)

var a = 101;
function foo(){
    console.log(this.a)
}
var obj = {
    a: 100,
    foo: foo
}
obj.foo() // ->100
複製程式碼

在上述程式碼中foo函式作為obj物件foo屬性的一個引用來呼叫,因此可以說obj擁有foo這個函式,所以此時foo函式中的this指向的是obj這個物件。
但是物件引用鏈只有最後一層會影響呼叫位置: obj.obj2.foo()此時foo中的this只會指向obj2

在隱式繫結中最容易弄混的是下面這種情況:

var a = 1
function foo(){
    console.log(this.a)
}
var obj = {
    a:2,
    foo: foo
}
var temp = obj.foo
temp();
複製程式碼

在上面的那段程式碼中有時會錯誤的理解為最後列印的結果是:2,實際控制檯列印的是: 1
在上述程式碼中的倒數第二步中我們將obj.foo這個引用賦值給temp這個物件,這裡相當於將物件的賦值(this是不會被複制過來了,this只有在呼叫的時候才會被制定)此時就temp這個物件指向foo函式,最後在呼叫temp()這個函式的時候,可以使用第一條預設繫結規則,此時this就指向window了.

到這裡就能明白最上面的那道題中的執行結果了

var p = test.sayAge()  // ->120
p() // ->100
複製程式碼

第一個使用隱式繫結規則this指向test,第二條中this指向window(宣告在全域性作用域中的變數就是全域性物件的一個同名屬性,不是在作用域中查詢的),所以window上有age這個屬性。

  • 顯示繫結(使用call、apply、bind,此時this指向前面3個方法所傳第一個引數值)

var a = 1
function foo(){
    console.log(this.a)
}
var obj = {
    a:2,
    foo: foo
}
var temp = obj.foo.bind(obj)
temp(); // ->2
// temp.call(obj)  ->2
// temp.apply(obj) ->2
複製程式碼

這裡顯示的將temp中this強制繫結到obj這個物件上(bind函式相當於返回一個新函式,不會執行),call/apply會執行函式,兩者只是所傳的引數不同,在傳的引數為null、undefined時this指向window。

  • new繫結

當將一個函式作為建構函式來呼叫時,使用new來建立一個例項時,會自動執行下面4步

var mycar = new Car()
function Car(){
    var this;//建立一個物件
    this = {
        __proto__: Car.prototype
    }
    return this
}
複製程式碼
  • 建立一個全新的物件
  • 這個新物件預設有一個__proto__屬性指向原型
  • 這個物件繫結到函式呼叫的this
  • 如果函式沒有返回其他物件,那麼返回這個新物件
function Car(color){
    this.color = color
}
var mycar = new Car('red')
mycar.color //->red
複製程式碼

回到最初的題目

new (test.sayAge())()
複製程式碼

執行test.sayAge()時採用隱式繫結規則,此時this指向test這個物件,所以列印120。這個執行結果會返回一個go函式,後面繼續執行new go()這時就使用最後一條規則,this指向建構函式go建立的這個例項物件,雖然這個例項物件上沒有age這個屬性,但是他在建立的時候就從原型上繼承了一個age屬性。所以在他的原型上找到了,列印60。
後續的註釋掉go.prototype.age = 60;這句話,這句導致建立的這個例項物件自己本身沒有,原型上也沒有,則只能列印undefined。

所以判斷一個執行中函式的this,首先需要找到這個函式的呼叫位置,然後就使用上述4條規則來判斷this繫結的物件。但是ES6的箭頭函式不適用上述4條規則。

相關文章