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。