摘要:然后繼續往后看,后面你會得到答案的想馬上驗證可以拖到最后到底是什么的確定是在的創建階段,而的創建發生在瀏覽器第一次加載的時候或者調用函數的時候具體可參見之前寫過的一篇文章基礎系列執行環境與作用域鏈。
最近重溫了一遍《你不知道的JavaScript--上卷》,其中第二部分關于this的講解讓我收獲頗多,所以寫一篇讀書筆記記錄總結一番。
消除誤解--this指向自身由于this的英文釋義,許多人都會將其理解成指向函數自身(JavaScript 中的所有函數都
是對象),但是實際上this并不像我們所想的那樣指向函數自身,我們可以通過下面的栗子驗證一下~
function foo(num) { console.log( "foo: " + num ); // 記錄foo 被調用的次數 this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo 被調用了多少次? console.log( foo.count ); // 0 -- WTF?
上述栗子的本意是想記錄foo被調用的次數
假設this指向函數本身,那么this.count與foo.count應該是foo函數對象的同一個屬性,那么最終得到的foo.count應該是4;
然而實際上,最終得到的foo.count是0,也就是說foo.count初始化之后就沒有再改變過了,所以this.count與foo.count是相互獨立的,互不影響;所以結論是:this并不是指向函數本身
那么這個里面的this到底是指向什么呢?你可以思考一下,寫下你的答案。然后繼續往后看,后面你會得到答案的~~想馬上驗證可以拖到最后...
this到底是什么this的確定是在Execution Context的創建階段,而Execution Context的創建發生在瀏覽器第一次加載script的時候或者調用函數的時候----具體可參見之前寫過的一篇文章JavaScript基礎系列---執行環境與作用域鏈。
所以this 是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數調用時的各種條件,this的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式;this的指向并沒有一個固定的說法,需要分情況而論。
要想明確this指向什么,需要通過尋找函數的調用位置來判斷函數在執行過程中會如何綁定this,從而確定this的指向。
尋找調用位置尋找調用位置就是尋找“函數被調用的位置”,但是做起來并沒有這么簡單,因為某些編程模式可能會隱藏真正的調用位置,這種時候很容易出錯。
最重要的是要分析調用棧(就是為了到達當前執行位置所調用的所有函數),我們關心的調用位置就在當前正在執行的函數的前一個調用中,下面用栗子來幫助理解:
function baz() { debugger // 當前調用棧是:baz // 因此,當前調用位置是全局作用域 console.log( "baz" ); bar(); // <-- bar 的調用位置 } function bar() { debugger // 當前調用棧是baz -> bar // 因此,當前調用位置在baz 中 console.log( "bar" ); foo(); // <-- foo 的調用位置 } function foo() { debugger // 當前調用棧是baz -> bar -> foo // 因此,當前調用位置在bar 中 console.log( "foo" ); } baz(); // <-- baz 的調用位置
如果條件允許,可以使用開發者工具進行觀察,將會更加直觀。
baz函數是在全局作用域中調用的,baz函數的調用棧為baz,所以baz函數的調用位置是全局作用域
bar函數是在baz函數中調用的,bar函數的調用棧為baz -> bar,當正在執行的是bar函數時,其前一個調用是baz,所以bar函數的調用位置是baz函數中的bar();位置
foo函數是在bar函數中調用的,foo函數的調用棧為baz -> bar -> foo,當正在執行的是foo函數時,其前一個調用是bar,所以foo函數的調用位置是bar函數中的foo();位置
this的綁定規則找到調用位置后該如何確定this的指向呢?這是有規則可循的,下面我們就來看看這四條規則,了解了規則后,確定this的步驟就變成:找到調用位置,然后判斷需要應用四條規則中的哪一條,根據規則得出this的指向。
默認綁定首先要介紹的是最常用的函數調用類型:獨立函數調用。這種調用是直接使用不帶任何修飾的函數引用進行調用的,它的調用位置是全局作用域,于是this指向全局對象。可以把這條規則看作是無法應用其他規則時的默認規則。
我們看下面的代碼:
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
首先我們要知道一件事,聲明在全局作用域中的變量(比如上述代碼中的var a = 2)就是全局對象的一個同名屬性。它們本質上就是同一個東西,并不是通過復制得到的,就像一個硬幣的兩面一樣。
在代碼中,foo()是在全局作用域中直接使用不帶任何修飾的函數引用進行調用的,所以foo函數調用時應用this的默認綁定,因此this指向全局對象;既然this指向全局對象,那么this.a便是全局變量a,所以打印的結果為2。
注意:嚴格模式下,禁止this關鍵字指向全局對象,此時this會綁定到undefined;所以當函數定義在嚴格模式下或函數內的代碼運行在嚴格模式下時,其中的this綁定的是undefined;特別注意如果僅僅是函數的調用語句運行在嚴格模式下,那么不受影響,該函數內的this仍然綁定到全局對象
"use strict"; function foo() { console.log( this.a ); } var a = 2; foo(); // TypeError: Cannot read property "a" of undefined
foo函數定義在嚴格模式下,所以this綁定到了`undefined
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: Cannot read property "a" of undefined
foo函數內部為嚴格模式,所以this綁定到了undefined
function foo() { console.log( this.a ); } "use strict"; var a = 2; foo(); // 2
嚴格模式的標識在foo函數的定義之后,foo函數未定義在嚴格模式下,僅僅是foo函數的調用語句foo()運行在嚴格模式下,所以this仍然可以綁定到全局對象
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2 })()
僅僅是foo函數的調用語句foo()運行在嚴格模式下,所以this仍然可以綁定到全局對象
溫馨提示:通常來說你不應該在代碼中混合使用嚴格模式和n非嚴格模式。整個程序要么嚴格要么非嚴格。然而,有時候你可能會用到第三方庫,其嚴格程度和你的代碼有所不同,因此一定要注意這類兼容性細節。
隱式綁定第二條規則是考慮函數調用位置是否有上下文對象,或者說該函數是否被某個對象“擁有”或者“包含”(僅僅是這么理解一下),如果函數調用位置有上下文對象,那么隱式綁定規則會把該函數中的this綁定到這個上下文對象
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
首先需要注意的是foo函數的聲明方式,及其之后是如何被當作引用屬性添加到obj中的。但是無論是直接在obj中定義還是先定義再添加為引用屬性,這個函數嚴格來說都不屬于obj對象;然而,調用位置會使用obj上下文來引用函數,因此你可以說函數被調用時obj 對象“擁有”或者“包含”它。
當函數調用位置有上下文對象時,隱式綁定規則會把該函數中的this綁定到這個上下文對象。所以上面的例子中,調用foo()時this被綁定到obj,那么this.a 和obj.a 是一樣的,打印的結果便是2。
對象屬性引用鏈中只有最頂層或者說最后一層會影響調用位置,看個例子就很容易理解了:
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
上述對象引用鏈為 :obj1->obj2,只有最后一層會影響調用位置,也就是只有obj2會影響調用位置,所以foo函數的調用位置的上下文對象為obj2,this綁定到obj2
注意:有些情況下會出現隱式丟失,意思就是被隱式綁定的函數丟失綁定對象,也就是說它會應用默認綁定,從而把this綁定到全局對象或者undefined上(取決于是否是嚴格模式)
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a 是全局對象的屬性 bar(); // "oops, global"
上面例子中,雖然bar是obj.foo的一個引用,但是實際上,它引用的是foo 函數本身,相當于var bar = foo;。因此此時的bar()其實是一個不帶任何修飾的函數調用,所以會應用了默認綁定,綁定到全局對象
function foo() { console.log( this.a ); } function doFoo(fn) { // fn 其實引用的是foo fn(); // <-- 調用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a 是全局對象的屬性 doFoo( obj.foo ); // "oops, global" setTimeout( obj.foo, 100 ); // "oops, global"
參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式賦值,所以將obj.foo傳遞給doFoo函數的參數fn,相當于fn = foo,所以doFoo函數內部的fn()其實是一個不帶任何修飾的函數調用,所以會應用了默認綁定,綁定到全局對象
內置函數setTimeout的結果也是一樣的。回調函數丟失this綁定是非常常見的,之后我們會介紹如何通過固定this來修復這個問題。
顯式綁定就像我們剛才看到的那樣,在分析隱式綁定時,我們必須在一個對象內部包含一個指向函數的屬性,并通過這個屬性間接引用函數,從而把this 間接(隱式)綁定到這個對象上。那么如果我們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎么做呢?
可以使用函數的call(..) 和apply(..) 方法。嚴格來說,JavaScript 的宿主環境有時會提供一些非常特殊的函數,它們并沒有這兩個方法。但是這樣的函數非常罕見,JavaScript 提供的絕大多數函數以及你自己創建的所有函數都可以使用call(..) 和apply(..) 方法。
這兩個方法是如何工作的呢?它們的第一個參數是一個對象,它們會把這個對象綁定到this,接著在調用函數時指定這個this;因為你可以直接指定this的綁定對象,因此我們稱之為顯式綁定。(如果沒有傳遞第一個參數,也就是沒有直接指定this,那么this將綁定到全局對象或者undefined上)
function foo() { console.log( this.a ); } var obj = { a:2 }; foo.call( obj ); // 2
通過foo.call(..),我們可以在調用foo時強制把它的this綁定到obj上。
如果你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當作this的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(..)、new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”。但是在嚴格模式下this不會被強制轉換為一個對象,也就是說傳入原始值來當做this的綁定對象,那么它不會轉換為對象形式
function foo() { console.log( this ); } foo.call( "cc" ); // String?{"cc"} foo.call( 6 ); // Number?{6} foo.call( true ); // Boolean?{true} "use strict" function foo() { console.log( this ); } foo.call( "cc" ); // cc foo.call( 6 ); // 6 foo.call( true ); // true
可惜,顯式綁定仍然無法解決我們之前提出的丟失綁定問題
function foo() { console.log( this.a ); } var obj = { a:6 }; var bar = function() { foo(); }; bar.call(obj); // undefined
可以看出,雖然bar通過call(..)方法顯示綁定到了obj,但是其內部的foo()仍然是一個不帶任何修飾的函數調用,this綁定到全局對象
顯式綁定的一個變種可以解決這個丟失綁定問題,我們稱這個變種為硬綁定,下面來看看它是如何解決的:
function foo() { console.log( this.a ); } var obj = { a:6 }; var bar = function() { foo.call( obj ); }; bar(); // 6 setTimeout( bar, 100 ); // 6 // 硬綁定的bar 不可能再修改它的this bar.call( window ); // 6
我們創建了函數bar,并在它的內部手動調用了foo.call(obj),因此強制把foo的this 綁定到了obj,無論之后如何調用函數bar,this始終綁定到obj。
一般來說,可以創建一個可重復使用的硬綁定輔助函數:
function foo(something) { console.log( this.a, something ); return this.a + something; } // 簡單的輔助綁定函數 function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); }; } var obj = { a:2 }; var bar = bind( foo, obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
通過bind函數就可以將foo函數的this始終綁定為obj,由于硬綁定是一種非常常用的模式,所以在ES5中提供了內置的方法Function.prototype.bind,將上面的例子改成該方法的形式,代碼如下:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
bind(..)會返回一個硬編碼的新函數,調用這個新函數時會把原始函數的this綁定到傳入bind(..)的參數上并調用原始函數,所以foo.bind( obj )會返回一個新函數,然后被賦值給bar,調用bar時會把foo中的this綁定到obj,并且調用foo函數。
除了上面說的硬綁定可以強制給this一個綁定,第三方庫的許多函數,以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,通常被稱為“上下文”(context),其作用和bind(..) 一樣,確保你的回調函數使用指定的this。
比如說:
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" }; // 調用foo(..) 時把this綁定到obj [1, 2, 3].forEach( foo, obj ); // 1 "awesome" // 2 "awesome" // 3 "awesome"
array.forEach(function(currentValue, index, arr), thisValue)方法用于調用數組的每個元素,并將元素傳遞給回調函數,它的第二個參數thisValue就可以指定回調函數中的this(如果這個參數為空,那么this將綁定到全局對象或者undefined上);forEach內部實際上就是通過call(..) 或者apply(..) 實現了顯式綁定
其他函數還有array.map,array.filter,array.every,array.some等
new綁定最后一條this的綁定規則,在講解它之前我們首先需要澄清一個非常常見的關于JavaScript 中函數和對象的誤解。
在傳統的面向類的語言中,“構造函數”是類中的一些特殊方法,使用new初始化類時會調用類中的構造函數。通常的形式是這樣的:
something = new MyClass(..);
JavaScript也有一個new操作符,使用方法看起來也和那些面向類的語言一樣,但是,JavaScript中new的機制實際上和面向類的語言完全不同。
首先我們重新定義一下JavaScript中的“構造函數”:在JavaScript中,構造函數只是一些使用new操作符時被調用的函數,它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被new操作符調用的普通函數而已。(ES6中的Class只是語法糖而已)
自定義函數和內置對象函數(比如Number(..))都可以用new來調用,這種函數調用被稱為構造函數調用。這里有一個重要但是非常細微的區別:實際上并不存在所謂的“構造函數”,只有對于函數的“構造調用”。
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操作:
創建(或者說構造)一個全新的對象
這個新對象會被執行[[Prototype]]鏈接([[Prototype]]指向構造函數的原型對象
這個新對象會綁定到該構造函數中的this上
執行構造函數中的代碼
如果該構造函數沒有返回其他對象,那么會自動返回這個新對象
上述過程中的this綁定就被稱為new綁定,下面看個簡單的例子:
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new來調用foo(..)時,我們會構造一個新對象(賦值給了變量bar)并把它綁定到foo函數中的this上,foo函數中的this綁定的就是對象bar
綁定規則的優先級在了解了四種綁定規則后,我們需要了解一下他們之間的優先級,因為有時候會出現符合多種規則的情況。
毫無疑問,默認綁定的優先級是四條規則中最低的,所以我們可以先不考慮它。
隱式綁定和顯式綁定哪個優先級更高?我們來測試一下:
function foo() { console.log( this.a ); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call( obj2 ); // 3 obj2.foo.call( obj1 ); // 2
可以明顯看出,顯示綁定優先于隱式綁定,也就是說在判斷時應當先考慮是否可以應用顯式綁定
那么隱式綁定和new綁定哪個優先級更高?我們也來測試一下:
function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo( 2 );//隱式綁定 console.log( obj1.a ); // 2 var bar = new obj1.foo( 4 );//new綁定,相當于vra bar = new foo(4); console.log( obj1.a ); // 2 console.log( bar.a ); // 4
可以看到new綁定比隱式綁定優先級高,那么現在還需要知道new綁定和顯式綁定誰的優先級更高,由于new 和call/apply 無法一起使用,因此無法通過new foo.call(obj1) 來直接進行測試,而硬綁定是顯示綁定的一種,所以我們使用硬綁定來測試它倆的優先級:
在看代碼之前先回憶一下硬綁定是如何工作的。Function.prototype.bind(..) 會創建一個新的包裝函數,這個函數會忽略它當前的this綁定(無論綁定的對象是什么),并把我們提供的對象綁定到this上。
這樣看起來硬綁定(也是顯式綁定的一種)似乎比new 綁定的優先級更高,應該無法使用new來控制this綁定,那實際上是如何的呢?來讓代碼揭曉答案:
function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind( obj1 ); bar(2); console.log( obj1.a ); // 2 var baz = new bar(3); console.log( obj1.a ); // 2 console.log( baz.a ); // 3
bar函數中的的this被硬綁定到obj1上,但是new bar(3)并沒有像我們前面預計的那樣把obj1.a修改為3,這說明使用new來調用bar()的時候,bar函數中的this綁定的不是obj1(否則obj1.a應該被修改為3),所以使用new仍然可以控制this綁定,實際上此時bar函數中的this綁定的是一個新對象,這個新對象最后賦值給了baz,所以baz.a的值為3。
為什么與預想的不同?因為ES5 中內置的Function.prototype.bind(..)方法的內部會進行判斷,會判斷硬綁定函數是否是被new調用,如果是的話就會使用新創建的this替換硬綁定的this。
所以new綁定的優先級高于顯示綁定。
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])方法,可以傳入參數序列,當綁定函數被調用時,這些參數將置于實參之前傳遞給被綁定的函數,所以bind(..)的功能之一就是可以把除了第一個參數(第一個參數用于綁定this)之外的其他參數都傳給下層的函數(這種技術稱為“部分應用”,是“柯里化”的一種)。
正是由于bind(...)的這一功能,如果我們在new中使用硬綁定函數,那么就可以預先設置函數的一些參數,這樣在使用new進行初始化時就可以只傳入其余的參數,這也就是為什么有些時候會在new中使用硬綁定函數的原因,看個例子:
function foo(p1,p2) { this.val = p1 + p2; } // 之所以使用null 是因為在本例中我們并不關心硬綁定的this是什么 // 反正使用new的時候this會被修改 var bar = foo.bind( null, "p1" );//傳入預先設置的參數p1 var baz = new bar( "p2" );//只需傳入剩余的參數p2 baz.val; // p1p2優先級總結
綜上所述,優先級如下:
new綁定 > 顯示綁定 > 隱式綁定 > 默認綁定
那么我們在確定this的時候就可以根據下面的步驟來:
函數是否使用new調用(new綁定)?如果是的話this綁定的是新創建的對象。
var bar = new foo()
函數是否通過call、apply(顯式綁定)或者硬綁定bind調用?如果是的話,this綁定的是指定的對象。
var bar = foo.call(obj2)
函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上下文對象。
var bar = obj1.foo()
如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。
var bar = foo()
對于正常的函數調用來說,理解了這些知識就可以明白this的綁定原理了,不過……凡事總有例外!!!
綁定的特殊情況在某些場景下this的綁定行為會出乎意料,你認為應當應用其他綁定規則時,實際上應用的可能是默認綁定規則。
被忽略的this如果你把null或者undefined作為this的綁定對象傳入call、apply 或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:
function foo() { console.log( this.a ); } var a = 2; foo.call( null ); // 2
那么什么情況下你會傳入null呢?一種非常常見的做法是使用apply(..)來“展開”一個數組,并當作參數傳入一個函數(ES6中可以直接使用...操作符)。類似地,bind(..)可以對參數進行柯里化(預先設置一些參數),這種方法有時非常有用:
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); } // 把數組“展開”成參數 foo.apply( null, [2, 3] ); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3
這兩種方法都需要傳入一個參數當作this的綁定對象。如果函數并不關心this的話,你仍然需要傳入一個占位值,這時null可能是一個不錯的選擇,就像代碼所示的那樣。
然而,總是使用null來忽略this綁定可能產生一些副作用。如果某個函數確實使用了this(比如第三方庫中的一個函數),那默認綁定規則會把this綁定到全局對象(在瀏覽器中這個對象是window),這將導致不可預計的后果(比如修改全局對象。顯而易見,這種方式可能會導致許多難以分析和追蹤的bug。
一種“更安全”的做法是傳入一個特殊的對象,把this綁定到這個對象不會對你的程序產生任何副作用。就像網絡(以及軍隊)一樣,我們可以創建一個DMZ(demilitarized zone,非軍事區)對象,如果我們在忽略this綁定時總是傳入一個DMZ對象,那就什么都不用擔心了,因為任何對于this的使用都會被限制在這個空對象中,不會對全局對象產生任何影響。
由于這個DMZ對象完全是一個空對象,可以使用一個特殊的變量名來表示它,比如?(這是數學中表示空集合符號的小寫形式)。在JavaScript中創建一個空對象最簡單的方法都是Object.create(null),Object.create(null)和{}很像,但是并不會創建Object.prototype這個委托,所以它比{}“更空”,所以之前的例子可以改為:
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); } // 我們的DMZ 空對象 var ? = Object.create( null ); // 把數組展開成參數 foo.apply( ?, [2, 3] ); // a:2, b:3 // 使用bind(..) 進行柯里化 var bar = foo.bind( ?, 2 ); bar( 3 ); // a:2, b:3間接引用
另一個需要注意的是,你有可能(有意或者無意地)創建一個函數的“間接引用”,在這種情況下,調用這個函數會應用默認綁定規則。
間接引用最容易在賦值時發生:
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
賦值表達式的返回值是要賦的值,所以p.foo = o.foo的返回值是目標函數的引用,即foo函數的引用,因此調用位置是foo()而不是p.foo()或者o.foo()。根據我們之前說過的,這里會應用默認綁定。
軟綁定之前我們已經看到過,硬綁定這種方式可以把this強制綁定到指定的對象(除了使用new時),防止函數調用應用默認綁定規則。問題在于,硬綁定會大大降低函數的靈活性,使用硬綁定之后就無法使用隱式綁定或者顯式綁定來修改this。
如果可以給默認綁定指定一個全局對象和undefined以外的值,那就可以實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改this的能力。
可以通過一種被稱為軟綁定的方法來實現我們想要的效果:
if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this;//這個this是指調用softBind的函數 // 捕獲所有 curried 參數(柯里化參數) var curried = [].slice.call( arguments, 1 );//arguments指傳入softBind的參數列表 var bound = function() { return fn.apply( (!this || this === (window || global)) ? obj : this, curried.concat.apply( curried, arguments) );//這里的this是指調用bound時的this,arguments指傳入bound的參數列表 }; bound.prototype = Object.create( fn.prototype ); return bound; }; }
除了軟綁定之外,softBind(..) 的其他原理和ES5內置的bind(..) 類似。它會對指定的函數進行封裝,首先檢查調用時的this,如果this綁定到全局對象或者undefined,那就把指定的默認對象obj綁定到this,否則不會修改this。此外,這段代碼還支持可選的柯里化(詳情請查看之前和bind(..)相關的介紹),看看軟綁定的實例:
function foo() { console.log("name: " + this.name); } var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; var fooOBJ = foo.softBind( obj ); fooOBJ(); // name: obj obj2.foo = foo.softBind(obj); obj2.foo(); // name: obj2 <---- 看!!!通過上下文對象(隱式綁定)綁定到obj2 fooOBJ.call( obj3 ); // name: obj3 <---- 看!通過顯示綁定綁定到obj3 setTimeout( obj2.foo, 10 );// name: obj <---- 應用了軟綁定,this本來綁定到全局對象,通過軟綁定綁定到了obj
可以看到,軟綁定版本的foo()可以手動將this綁定到obj2或者obj3上,但如果應用默
認綁定,則會將this綁定到obj。
我們之前介紹的四條規則已經可以包含所有正常的函數。但是ES6中介紹了一種無法使用這些規則的特殊函數類型:箭頭函數。
箭頭函數并不是使用function關鍵字定義的,而是使用被稱為“胖箭頭”的操作符=>定義的。箭頭函數不使用this的四種標準規則,而是根據外層(函數或者全局)作用域來決定this。
我們來看看箭頭函數的詞法作用域:
function foo() { // 返回一個箭頭函數 return (a) => { //this 繼承自foo() console.log( this.a ); }; } var obj1 = { a:2 }; var obj2 = { a:3 }; var bar = foo.call( obj1 ); bar.call( obj2 ); // 2, 不是3 !
foo()內部創建的箭頭函數會捕獲調用foo()時的this。由于foo()的this綁定到obj1,bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定無法被修改。(new也不行!)
箭頭函數最常用于回調函數中,例如事件處理器或者定時器:
function foo() { setTimeout(() => { // 這里的this 在詞法上繼承自foo() console.log( this.a ); },100); } var obj = { a:2 }; foo.call( obj ); // 2
箭頭函數可以像bind(..)一樣確保函數的this被綁定到指定對象,此外,其重要性還體現在它用更常見的詞法作用域取代了傳統的this機制。實際上,在ES6之前我們就已經在使用一種幾乎和箭頭函數完全一樣的模式:
function foo() { var self = this; // lexical capture of this setTimeout( function(){ console.log( self.a ); }, 100 ); } var obj = { a: 2 }; foo.call( obj ); // 2
是不是非常熟悉?
雖然self = this和箭頭函數看起來都可以取代bind(..),但是從本質上來說,它們想替代的是this機制,如果你經常編寫this風格的代碼,但是絕大部分時候都會使用self = this或者箭頭函數來否定this機制,那你或許應當:
只使用詞法作用域并完全拋棄錯誤this風格的代碼;
完全采用this風格,在必要時使用bind(..),盡量避免使用self = this和箭頭函數。
當然,包含這兩種代碼風格的程序可以正常運行,但是在同一個函數或者同一個程序中混合使用這兩種風格通常會使代碼更難維護,并且可能也會更難編寫。
疑問解答先來說一下最前面的一個例子的真實情況:
function foo(num) { console.log( "foo: " + num ); // 記錄foo 被調用的次數 this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { foo( i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo 被調用了多少次? console.log( foo.count ); // 0 -- WTF?
先說結論,this指向全局對象,而this.count為NaN。
通過分析我們可以知道foo的調用位置是全局作用域,然后foo處于非嚴格模式,所以this指向全局對象,由于this.count的值一開始為undefined,然后進行this.count++;的操作,所以變成NaN
記得前面提到過下面這段話:
嚴格模式下,禁止this關鍵字指向全局對象,此時this會綁定到undefined;所以當函數定義在嚴格模式下或函數內的代碼運行在嚴格模式下時,其中的this綁定的是undefined;特別注意如果僅僅是函數的調用語句運行在嚴格模式下,那么不受影響,該函數內的this仍然綁定到全局對象
但是測試的時候遇到一種情況一開始讓我匪夷所思:
function foo(){ "use strict"; console.log(this); } setTimeout(foo,100);//Window
foo的函數體處于嚴格模式下,為什么this還是綁定到全局對象Window?于是我又測試了幾種情況:
"use strict"; function foo(){ console.log(this); } setTimeout(foo,100);//Window //---------分割線----------- function foo(){ console.log(this); } setTimeout(function(){ "use strict"; foo(); },100);//Window //---------分割線----------- function foo(){ "use strict"; console.log(this); } setTimeout(function(){ foo(); },100);//undefined
只有最后一種情況this綁定到undefined,其他情況仍然綁定到Window。
在MDN-Window.setTimeout-關于this的問題中,找到一段備注:
備注:在嚴格模式下,setTimeout( )的回調函數里面的this仍然默認指向window對象, 并不是undefined
但是這個僅僅是告訴了我們結論,并沒有給出為什么。經過思考,我給出我自己的猜想,也不知道對不對:
我們知道setTimout是掛在Window下的方法,所以調用時實際上是Window.setTimout,是通過Window對象調用的,一般認為setTimout的偽代碼是下面這樣:
function setTimeout(fn,delay) { // 等待delay 毫秒 fn(); }
但是通過前文的介紹,我們知道
直接使用不帶任何修飾的函數引用進行調用的,它的調用位置是全局作用域,非嚴格模式下綁定到全局對象,嚴格模式下綁定到undefined
根據setTimeout這種偽代碼,等待delay毫秒后,fn()就是一個不帶任何修飾的函數調用,而下面的測試確仍然指向全局對象Window
function foo(){ "use strict"; console.log(this); } Window.setTimeout(foo,100);//Window
所以我猜想,setTimout的偽代碼是下面這樣:
function setTimeout(fn,delay) { // 等待delay 毫秒 //直接執行fn內的代碼,而不是調用fn(相當于把fn中的代碼粘貼到此處) }
基于這種猜想,我們來看前面的測試代碼:
function foo(){ "use strict"; console.log(this); } Window.setTimeout(foo,100);//Window
相當于下面這樣:
Window = { setTimeout: function(){ // 等待100毫秒 "use strict"; console.log(this); } }
這樣一看,this自然就是指向Window;再看其他三個測試代碼:
"use strict"; function foo(){ console.log(this); } setTimeout(foo,100);//Window //相當于 "use strict"; Window = { setTimeout: function(){ // 等待100毫秒 console.log(this); } }//通過Window調用setTimeout,this指向Window //---------分割線----------- function foo(){ console.log(this); } setTimeout(function(){ "use strict"; foo(); },100);//Window //相當于 Window = { setTimeout: function(){ // 等待100毫秒 "use strict"; foo(); }//通過Window調用setTimeout,其內部調用了foo,而且僅僅是foo的調用處于嚴格模式,所以foo中的this指向Window } //---------分割線----------- function foo(){ "use strict"; console.log(this); } setTimeout(function(){ foo(); },100);//undefined //相當于 Window = { setTimeout: function(){ // 等待100毫秒 foo(); }//通過Window調用setTimeout,其內部調用了foo,但是foo的函數體處于嚴格模式,所以foo中的this指向undefined }
似乎一切也說的過去,不過我暫時沒有找到權威性的資料來證實,自己先這樣理解一下,如果不對,還請大家指正!
尾聲以前對this真是不清不楚,這次徹底的順了一遍之后清晰多了,每天進步一點點,加油~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95579.html
摘要:我們繼續,這次來聊聊類。,編寫代碼角色基類判斷角色是否死亡升級受到傷害攻擊普通攻擊攻擊了造成了點傷害攻擊,有概率是用必殺攻擊必殺攻擊使用必殺攻擊了造成了點傷害游戲世界權利的游戲初始化英雄怪物集合,模擬簡單的游戲關卡。 OK, 我們繼續,這次來聊聊類。 內有 Jon Snow大戰異鬼, ? 熟悉后端的朋友們對類肯定都不陌生,如下面一段PHP的代碼: class Human { pr...
摘要:先來介紹下語法官方示例代碼模塊中對象暴露只需要即可,可以是任何類型的對象。手動導入模塊下某個對象,需要和模塊中定義的名字對應,順序無關。 看一下官方介紹: Language-level support for modules for component definition. JS在ES2015開始原生支持模塊化開發,我們之前也曾借助于諸如: AMD CommonJS 等的模塊加載...
摘要:不過好消息是,在事件發生的二十四小時以后,我發現我的賬號解禁了,哈哈哈哈。 本文最初發布于我的個人博客:咀嚼之味 從昨天凌晨四點起,我的 Leetcode 賬號就無法提交任何代碼了,于是我意識到我的賬號大概是被封了…… 起因 我和我的同學 @xidui 正在維護一個項目 xidui/algorithm-training。其實就是收錄一些算法題的解答,目前主要對象就是 Leetcode。...
摘要:據調研機構數據,年第三季度,全球智能手機芯片市場占有率中,聯發科力壓高通,歷史首次登頂全球第一。年月,聯發科發布全球首款十核處理器,以及它的升級版。聯發科本月表示,其最新的旗艦芯片將于明年第一季度發布,希望在農歷新年前推出。在被喊了一年的MTK YES后,聯發科終于迎來了自己的YES時刻。據調研機構Counterpoint數據,2020年第三季度,全球智能手機芯片市場占有率中,聯發科力壓高通...
閱讀 6912·2021-09-22 15:08
閱讀 1920·2021-08-24 10:03
閱讀 2437·2021-08-20 09:36
閱讀 1315·2020-12-03 17:22
閱讀 2474·2019-08-30 15:55
閱讀 905·2019-08-29 16:13
閱讀 3053·2019-08-29 12:41
閱讀 3249·2019-08-26 12:12