this是每一個想要深入學習Javascript的人必過的一關,我為this看過很多書查過很多資料,雖然對this有了一定的了解並且也經常使用this,但是如果有人問我 this是什么呀? 我依舊不能給別人一個完美的解釋。最近一個小的機緣,讓我重新對this有了認識,終於覺得自己可以把我認識到的this將給別人聽了,所以現在迫不及待的來分享一下我的認識
說到this,最重要的就是this的指向了(這樣說並不准確,因為this只是函數被調用時所創建的活動對象中的一個屬性而已)。
有些人可能認為this指向的是自身,因為this這個單詞的含義就是如此嘛,不過這種認識應該是錯誤的,不信?來看代碼~
1 function foo(num) { 2 console.log("foo" + num); 3 this.count++; 4 } 5 foo.count = 0; 6 for(var i = 0; i < 5; i++) { 7 foo(i); 8 } 9 console.log(foo.count);
對於第二行的輸出肯定大家都是知道答案的
foo0
foo1
foo2
foo3
foo4
那么~第九行的輸出是什么?(如果this指向的是自身的話foo被調用5次 foo.count應該是5才對,但是!)
事實上this.count並沒有 ++ 由此可以說明,this並不是指向自身。
又會有人說,我知道他不是指向自身呀,他明明是指向作用域的嘛(好吧,很長一段時間我也是這么以為的)。不過事實上這也是不正確的,依舊,我們用一段代碼來證明
1 function foo() { 2 var a = 2; 3 this.bar(); 4 } 5 function bar() { 6 console.log(this.a); 7 } 8 foo();
這段代碼第六行的輸出會是什么呢?是2嗎?哈哈,不要太天真呦~
哈哈,this並沒有指向自身作用域呢~看到這里的你現在一定暴虐的想要知道那this到底是個什么鬼,那現在我們就開始一層層解析this是什么
想要知道函數在執行過程中是如何綁定this的,首先要知道函數的調用位置。可能很多人已經可以肯輕松的尋找出函數調用位置,那就輕輕松的看一下我的代碼和你想出的答案是不是一樣的吧
1 function baz() { 2 //當前調用棧:baz
3 //調用位置是全局作用域
4 console.log("baz"); 5 bar();//bar的調用位置
6 } 7 function bar() { 8 //當前調用棧:baz -> bar
9 //調用位置是baz中
10 console.log("bar"); 11 foo();//foo的調用位置
12 } 13 function foo() { 14 //當前調用棧:baz -> bar -> foo
15 //調用位置是bar中
16 console.log("foo"); 17 } 18 baz();//baz的調用位置
看一下上面的代碼,從 baz(); 開始,baz在全局之中,之后baz調用 bar(); 所以bar的調用位置在baz之中,再然后bar調用 foo(); foo的調用位置在baz之中。執行foo函數是的調用棧為baz -> bar -> foo 。
通過函數的互相調用找出調用棧,便可以找出函數的真正調用位置,可是如果代碼量過大可能這樣人工查找很容易出錯,所以~~我們有更好的方法。。。
上圖右側向上的箭頭所滑過的就是foo函數執行的調用棧,棧中的第二個元素就是真正的調用位置。
找到調用位置之后我們來看一下this的綁定符合哪種綁定規則
1.隱式綁定: 看調用位置是否有上下文對象,即是否被某個對象擁有或包含(這里說的包含可不是用花括號包起來呦~)。
1 var a = 3; 2 function foo() { 3 console.log(this.a); 4 } 5 var obj = { 6 a: 2, 7 foo: foo 8 }; 9 obj.foo();
上面又是一個函數調用里面輸出this指向的一個值呢,先不說答案,先看一下,這段代碼和上面指出this不是指向作用域的那段代碼的區別是什么,我想大家都發現了,這次foo函數的調用是 obj.foo() 。foo函數被用作obj對象的一個方法來調用。很明顯根據foo函數的聲明位置來看他並不屬於obj對象,但是~~哈哈,講了好多回但是~~foo函數的調用位置是obj的上下文來引用函數,也就是說他的落腳點是obj對象,那~~他的this自然也就指向他的上下文對象。這樣一分析,答案就顯而易見了,this.a等同於obj.a就是2。
(ps:如果對象屬性的引用是多層的,只有最后一層會影響調用位置)。
2.顯式綁定:
上面的隱式綁定是通過調用一個對象中綁定了函數的屬性來把this隱式的綁定在這個對象上的。所以顯示綁定就是使用一些方法強制將函數綁定在某個對象上,這些方法就是 call(...) apply(...) 這兩種方法的工作方式是類似的,所以我們就以call()來作為例子分析一下他們的工作過程
這兩個方法第一個參數是一個對象,而被綁定的函數的this就會綁定在這個對象上,看一段代碼
1 function foo() { 2 console.log(this.a); 3 } 4 var obj = { 5 a: 2
6 }; 7 foo.call(obj);
foo函數執行時使用了call函數,call函數的第一個參數是obj對象,則foo函數活動對象的this屬性就被綁定在了obj對象上,所以,輸出2.
3.new 綁定
使用 new 來調用函數,即發生構造函數調用時,會執行下面操作
- 創建一個全新的對象
- 這個對象會被執行[__proto__]的鏈接
- 這個新對象會綁定到函數調用的this上
- 如果函數沒有返回其他對象,那么new表達式中的函數調用會自動返回這個新對象
用一段代碼來解釋上面的操作( 原型的問題暫且不考慮 )
1 function Foo() { 2 this.a = a; 3 } 4 var bar = new Foo(2); 5 console.log(bar.a);
當使用new操作符執行Foo函數時,就創建了一個全新的對象並且賦值給了變量bar。進行原型鏈接之后(這里不是我們今天討論的范圍)Foo函數活動對象的this屬性被綁定在新創建的對象bar上,Foo函數並沒有返回值,所以自動返回new創建的bar對象。
4.默認綁定 默認綁定就是不符合上面任何一種規則是的默認規則
1 function foo() { 2 console.log(this.a); 3 } 4 var a = 2; 5 foo();
上面這段代碼foo函數不帶任何修飾的直接使用,不符合1-3中的任何一種綁定規則,所以只能使用默認綁定規則,而默認綁定規則就是在非嚴格模式下被綁定在全局對象上(嚴格模式下與foo函數的調用位置無關為undefined)。
好了,關於this的四種綁定規則都已經解釋清楚。那有沒有意外情況呢?
是不是意外情況就看你對this的理解有沒有到位,有沒有認真的分析了,下面放兩種特殊情況讓大家轉動一下腦筋
1.
1 function foo() { 2 console.log(this.a); 3 } 4 var obj = { 5 a: 2, 6 foo: foo 7 }; 8 var bar = obj.foo; 9 var a = "Global"; 10 bar();
這段代碼唯一的特殊點就是bar引用了obj的foo函數。而答案就變成了Global。這是為什么呢?
首先我們直接來輸出一下bar
可以看到bar就是foo()函數的一個別名而已,雖然他是通過obj引用的foo()函數,但他只是引用到了foo函數,並沒有引用到obj對象的執行上下文,所以他符合的是上面this規則的第四條默認綁定,所以this指向全局輸出Global。
2.
1 function foo() { 2 console.log(this.a); 3 } 4 function toDo (fn) { 5 fn(); 6 } 7 var obj = { 8 a: 2, 9 foo: foo 10 }; 11 var a = "Global"; 12 toDo(obj.foo);
這段代碼是傳入回掉函數。而toDO函數中fn的傳遞依舊只是傳遞了foo這個函數,並沒有傳遞obj對象的執行上下文,所以在fn()調用的位置沒有任何特殊綁定,符合this規則的默認綁定,this指向全局。不信?我們在toDO函數內輸出一下fn看是不是foo函數嘍~
嗯哼,看來我說的是對的。
一切都解決之后你肯定又會問,那萬一我遇見的函數一下子符合了兩條規則怎么辦?呃。。。我們來測一下綁定規則的優先級吧~
不用說默認綁定的優先級是最低的,那先不用理他,先看一下隱式綁定和顯示綁定誰的優先級更高
1 function foo() { 2 console.log(this.a); 3 } 4 var obj1 = { 5 a: 2, 6 foo: foo 7 }; 8 var obj2 = { 9 a: 3, 10 foo: foo 11 }; 12 obj1.foo(); 13 obj2.foo(); 14
15 obj1.foo.call(obj2); 16 obj2.foo.call(obj1);
看來顯式綁定輕松戰勝隱式綁定,那顯式綁定和new誰的優先級更高呢?好吧,由於new和call(),apply()無法同時使用,所以我沒有寫出他們之間的測試代碼,不過查了些資料之后的結論是new的優先級高於顯式綁定(那位大神可以舉一個new優先級高於現實綁定的例子呢~如果有的話請貼進評論,小女子在此謝過~~)。
那總結以上經驗就是想要判斷this綁定規則,先看new,再看顯式綁定,再看隱式綁定,都沒有則使用默認綁定。
不過凡事都有例外,不能百分百下定論,比如說顯式綁定是對象傳入null或者undefined的話,則會忽略call或apply應用默認綁定。
還有很多種例外等着大家去發覺。不過還是一般情況占據多數,所以,只要正確尋找分析函數調用棧,找到函數調用位置,在函數調用位置上判斷使用那種綁定規則,基本可以正確判斷出this的指向。