這又是一個面試經(jīng)典問題~/(ㄒoㄒ)/~~也是 ES5中眾多坑中的一個,在 ES6 中可能會極大避免 this 產(chǎn)生的錯誤,但是為了一些老代碼的維護,最好還是了解一下 this 的指向和 call、apply、bind 三者的區(qū)別。
this 的指向
在 ES5 中,其實 this 的指向,始終堅持一個原理:this 永遠指向最后調(diào)用它的那個對象,來,跟著我朗讀三遍:this 永遠指向最后調(diào)用它的那個對象,this 永遠指向最后調(diào)用它的那個對象,this 永遠指向最后調(diào)用它的那個對象。記住這句話,this 你已經(jīng)了解一半了。
下面我們來看一個最簡單的例子:
例 1:
|
|
這個相信大家都知道為什么 log 的是 windowsName,因為根據(jù)剛剛的那句話“this 永遠指向最后調(diào)用它的那個對象”,我們看最后調(diào)用 a 的地方 a();,前面沒有調(diào)用的對象那么就是全局對象 window,這就相當于是 window.a();注意,這里我們沒有使用嚴格模式,如果使用嚴格模式的話,全局對象就是 undefined,那么就會報錯 Uncaught TypeError: Cannot read property 'name' of undefined。
再看下這個例子:
例 2:
|
|
在這個例子中,函數(shù) fn 是對象 a 調(diào)用的,所以打印的值就是 a 中的 name 的值。是不是有一點清晰了呢~
我們做一個小小的改動:
例 3:
|
|
這里打印 Cherry 的原因也是因為剛剛那句話“this 永遠指向最后調(diào)用它的那個對象”,最后調(diào)用它的對象仍然是對象 a。
我們再來看一下這個例子:
例 4:
|
|
這里為什么會打印 undefined 呢?這是因為正如剛剛所描述的那樣,調(diào)用 fn 的是 a 對象,也就是說 fn 的內(nèi)部的 this 是對象 a,而對象 a 中并沒有對 name 進行定義,所以 log 的 this.name 的值是 undefined。
這個例子還是說明了:this 永遠指向最后調(diào)用它的那個對象,因為最后調(diào)用 fn 的對象是 a,所以就算 a 中沒有 name 這個屬性,也不會繼續(xù)向上一個對象尋找 this.name,而是直接輸出 undefined。
再來看一個比較坑的例子:
例 5:
|
|
這里你可能會有疑問,為什么不是 Cherry,這是因為雖然將 a 對象的 fn 方法賦值給變量 f 了,但是沒有調(diào)用,再接著跟我念這一句話:“this 永遠指向最后調(diào)用它的那個對象”,由于剛剛的 f 并沒有調(diào)用,所以 fn() 最后仍然是被 window 調(diào)用的。所以 this 指向的也就是 window。
由以上五個例子我們可以看出,this 的指向并不是在創(chuàng)建的時候就可以確定的,在 es5 中,永遠是this 永遠指向最后調(diào)用它的那個對象。
再來看一個例子:
例 6:
|
|
讀到現(xiàn)在了應(yīng)該能夠理解這是為什么了吧(o゚▽゚)o。
怎么改變 this 的指向
改變 this 的指向我總結(jié)有以下幾種方法:
- 使用 ES6 的箭頭函數(shù)
- 在函數(shù)內(nèi)部使用 _this = this
- 使用 apply、call、bind
- new 實例化一個對象
例 7:
|
|
在不使用箭頭函數(shù)的情況下,是會報錯的,因為最后調(diào)用 setTimeout 的對象是 window,但是在 window 中并沒有 func1 函數(shù)。
我們在改變 this 指向這一節(jié)將把這個例子作為 demo 進行改造。
箭頭函數(shù)
眾所周知,ES6 的箭頭函數(shù)是可以避免 ES5 中使用 this 的坑的。箭頭函數(shù)的 this 始終指向函數(shù)定義時的 this,而非執(zhí)行時。,箭頭函數(shù)需要記著這句話:“箭頭函數(shù)中沒有 this 綁定,必須通過查找作用域鏈來決定其值,如果箭頭函數(shù)被非箭頭函數(shù)包含,則 this 綁定的是最近一層非箭頭函數(shù)的 this,否則,this 為 undefined”。
例 8 :
|
|
在函數(shù)內(nèi)部使用 _this = this
如果不使用 ES6,那么這種方式應(yīng)該是最簡單的不會出錯的方式了,我們是先將調(diào)用這個函數(shù)的對象保存在變量 _this 中,然后在函數(shù)中都使用這個 _this,這樣 _this 就不會改變了。
例 9:
|
|
這個例子中,在 func2 中,首先設(shè)置 var _this = this;,這里的 this 是調(diào)用 func2 的對象 a,為了防止在 func2 中的 setTimeout 被 window 調(diào)用而導(dǎo)致的在 setTimeout 中的 this 為 window。我們將 this(指向變量 a) 賦值給一個變量 _this,這樣,在 func2 中我們使用 _this 就是指向?qū)ο?a 了。
使用 apply、call、bind
使用 apply、call、bind 函數(shù)也是可以改變 this 的指向的,原理稍后再講,我們先來看一下是怎么實現(xiàn)的:
使用 apply
例 10:
|
|
使用 call
例 11:
|
|
使用 bind
例 12:
|
|
apply、call、bind 區(qū)別
剛剛我們已經(jīng)介紹了 apply、call、bind 都是可以改變 this 的指向的,但是這三個函數(shù)稍有不同。
在 MDN 中定義 apply 如下;
apply() 方法調(diào)用一個函數(shù), 其具有一個指定的this值,以及作為一個數(shù)組(或類似數(shù)組的對象)提供的參數(shù)
語法:
fun.apply(thisArg, [argsArray])
- thisArg:在 fun 函數(shù)運行時指定的 this 值。需要注意的是,指定的 this 值并不一定是該函數(shù)執(zhí)行時真正的 this 值,如果這個函數(shù)處于非嚴格模式下,則指定為 null 或 undefined 時會自動指向全局對象(瀏覽器中就是window對象),同時值為原始值(數(shù)字,字符串,布爾值)的 this 會指向該原始值的自動包裝對象。
- argsArray:一個數(shù)組或者類數(shù)組對象,其中的數(shù)組元素將作為單獨的參數(shù)傳給 fun 函數(shù)。如果該參數(shù)的值為null 或 undefined,則表示不需要傳入任何參數(shù)。從ECMAScript 5 開始可以使用類數(shù)組對象。瀏覽器兼容性請參閱本文底部內(nèi)容。
apply 和 call 的區(qū)別
其實 apply 和 call 基本類似,他們的區(qū)別只是傳入的參數(shù)不同。
call 的語法為:
|
|
所以 apply 和 call 的區(qū)別是 call 方法接受的是若干個參數(shù)列表,而 apply 接收的是一個包含多個參數(shù)的數(shù)組。
例 13:
|
|
例 14:
|
|
bind 和 apply、call 區(qū)別
我們先來將剛剛的例子使用 bind 試一下
|
|
我們會發(fā)現(xiàn)并沒有輸出,這是為什么呢,我們來看一下 MDN 上的文檔說明:
bind()方法創(chuàng)建一個新的函數(shù), 當被調(diào)用時,將其this關(guān)鍵字設(shè)置為提供的值,在調(diào)用新函數(shù)時,在任何提供之前提供一個給定的參數(shù)序列。
所以我們可以看出,bind 是創(chuàng)建一個新的函數(shù),我們必須要手動去調(diào)用:
|
|
==================================== 更新==============================
JS 中的函數(shù)調(diào)用
看到留言說,很多童靴不理解為什么 例 6 的 innerFunction 和 例 7 的 this 是指向 window 的,所以我就來補充一下 JS 中的函數(shù)調(diào)用。
例 6:
|
|
例 7:
|
|
函數(shù)調(diào)用的方法一共有 4 種
- 作為一個函數(shù)調(diào)用
- 函數(shù)作為方法調(diào)用
- 使用構(gòu)造函數(shù)調(diào)用函數(shù)
- 作為函數(shù)方法調(diào)用函數(shù)(call、apply)
作為一個函數(shù)調(diào)用
比如上面的 例 1:
例 1:
|
|
這樣一個最簡單的函數(shù),不屬于任何一個對象,就是一個函數(shù),這樣的情況在 JavaScript 的在瀏覽器中的非嚴格模式默認是屬于全局對象 window 的,在嚴格模式,就是 undefined。
但這是一個全局的函數(shù),很容易產(chǎn)生命名沖突,所以不建議這樣使用。
函數(shù)作為方法調(diào)用
所以說更多的情況是將函數(shù)作為對象的方法使用。比如例 2:
例 2:
|
|
這里定義一個對象 a,對象 a 有一個屬性(name)和一個方法(fn)。
然后對象 a 通過 . 方法調(diào)用了其中的 fn 方法。
然后我們一直記住的那句話“this 永遠指向最后調(diào)用它的那個對象”,所以在 fn 中的 this 就是指向 a 的。
使用構(gòu)造函數(shù)調(diào)用函數(shù)
如果函數(shù)調(diào)用前使用了 new 關(guān)鍵字, 則是調(diào)用了構(gòu)造函數(shù)。
這看起來就像創(chuàng)建了新的函數(shù),但實際上 JavaScript 函數(shù)是重新創(chuàng)建的對象:
|
|
這就有要說另一個面試經(jīng)典問題:new 的過程了,(ಥ_ಥ)
這里就簡單的來看一下 new 的過程吧:
偽代碼表示:
|
|
- 創(chuàng)建一個空對象 obj;
- 將新創(chuàng)建的空對象的隱式原型指向其構(gòu)造函數(shù)的顯示原型。
- 使用 call 改變 this 的指向
- 如果無返回值或者返回一個非對象值,則將 obj 返回作為新對象;如果返回值是一個新對象的話那么直接直接返回該對象。
所以我們可以看到,在 new 的過程中,我們是使用 call 改變了 this 的指向。
作為函數(shù)方法調(diào)用函數(shù)
在 JavaScript 中, 函數(shù)是對象。
JavaScript 函數(shù)有它的屬性和方法。
call() 和 apply() 是預(yù)定義的函數(shù)方法。 兩個方法可用于調(diào)用函數(shù),兩個方法的第一個參數(shù)必須是對象本身在 JavaScript 嚴格模式(strict mode)下, 在調(diào)用函數(shù)時第一個參數(shù)會成為 this 的值, 即使該參數(shù)不是一個對象。
在 JavaScript 非嚴格模式(non-strict mode)下, 如果第一個參數(shù)的值是 null 或 undefined, 它將使用全局對象替代。
這個時候我們再來看例 6:
例 6:
|
|
這里的 innerFunction() 的調(diào)用是不是屬于第一種調(diào)用方式:作為一個函數(shù)調(diào)用(它就是作為一個函數(shù)調(diào)用的,沒有掛載在任何對象上,所以對于沒有掛載在任何對象上的函數(shù),在非嚴格模式下就是 window 調(diào)用的)
然后再看一下 例 7:
例 7:
|
|
這個簡單一點的理解可以理解為“匿名函數(shù)的 this 永遠指向 window”,你可以這樣想,還是那句話this 永遠指向最后調(diào)用它的那個對象,那么我們就來找最后調(diào)用匿名函數(shù)的對象,這就很尷尬了,因為匿名函數(shù)名字啊,笑哭,所以我們是沒有辦法被其他對象調(diào)用匿名函數(shù)的。所以說 匿名函數(shù)的 this 永遠指向 window。
如果這個時候你要問,那匿名函數(shù)都是怎么定義的,首先,我們通常寫的匿名函數(shù)都是自執(zhí)行的,就是在匿名函數(shù)后面加 () 讓其自執(zhí)行。其次就是雖然匿名函數(shù)不能被其他對象調(diào)用,但是可以被其他函數(shù)調(diào)用啊,比如例 7 中的 setTimeout。