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