你不知道的JavaScript——this全面解析
之前一直不知道this
到底代表啥,只知道它和一般面向物件程式語言中的this
一定不同。
JavaScript中的this
可以在函式中使用,是編譯器通過一些條件在函式被呼叫時繫結進對應作用域的的一個變數,可以明確知道的是,這個變數一定是一個物件,所以你可以用this.xxx
的方式訪問一個屬性。
可以看到this被自動繫結進了作用域中,所以我們可以把它看成普通變數一樣對待即可,沒啥大不了的。
圖中的this
指向了Window
物件,也就是瀏覽器環境下的全域性作用域,實際上this
可以指向任意位置,具體的規則可以分四種情況。
預設繫結
預設情況下,this
指向全域性作用域。
function foo(){ console.log(this.a); } var a = 10; foo(); // 10
在瀏覽器執行這段程式碼,可以得到10
的結果。
但是,在NodeJS環境下,你會得到一個undefined
,這個理論失效了......難道是Node下的this和瀏覽器上的還不一樣??那xx還得學兩遍?
NodeJS和瀏覽器的不同
別太擔心,只是NodeJS下的全域性作用域和瀏覽器的有些區別。
如上是瀏覽器的全域性作用域,Window
物件直接作為所有js檔案的全域性作用域,所以每個js檔案中使用var
定義變數時,是直接定義在Window
下的,這樣容易造成命名衝突。
如上是NodeJs的做法,它使用了一個global
作為全域性作用域,為所有檔案通用,但每個js檔案又有一個單獨的作用域,它們使用var
回過頭來看程式碼
function foo(){
console.log(this.a);
}
var a = 10;
foo(); // undefined
咱說了,預設情況下this
被繫結到全域性作用域,那就是global
,而a
現在在哪兒啊?在檔案單獨的作用域裡,那怎麼找啊?
function foo(){
console.log(this.a);
}
global.a = 10;
foo(); // 10
這樣是可以的,但如非必要,請不要影響global
作用域。
嘶,,你說不要影響全域性作用域,那...你使用this不很容易影響全域性作用域嗎???
對了,注意,嚴格模式下這個預設行為會被禁止,也就是說,嚴格模式下,全域性變數不會被繫結給this,預設情況下的this是undefined
隱式繫結
隱式綁定出現在呼叫位置有上下文物件的情況,this
指向上下文物件。
function foo(){
console.log(this.a);
}
var obj = {
a: 10,
foo: foo
}
var a = 20; // Turn to a comment in node env
// global.a = 20 // Release in node env
foo(); // 20
obj.foo(); // 10
第一個foo
呼叫,按照剛剛的預設繫結,毫無疑問會尋找全域性作用域中的20
列印,但第二個obj.foo()
,由於我們呼叫的位置是在obj
的上下文中,所以this
指向obj
,列印10
。
這說明,隱式繫結優先順序 > 預設繫結,這顯然是句廢話,但先記著。
看下面的程式碼
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
這應該不難理解,因為呼叫動作實際是obj2
發出的,上下文理應是obj2
,所以this
也是obj2
。
再看
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "oops, global";
bar(); // "oops, global"
這...列印的結果是全域性作用域中的a
,而不是obj
中的。
因為var bar = obj.foo
,這句,只是把foo函式賦值給bar了,這和你直接呼叫foo
又有啥區別捏??嘿嘿。
再看
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";
doFoo( obj.foo ); // "oops, global"
這個和上面的原理一樣,傳參本就是一種隱式賦值。這些都是日常編寫程式碼時容易犯的錯誤。
所以,這些問題讓我感覺預設繫結和隱式繫結是如此的不可靠,一丁點兒的不小心就可能會釀成大錯。
顯式繫結
JS中的函式物件有兩個預設方法,apply
和call
,實際上都是呼叫這個函式,只是它倆可以在呼叫的同時傳遞要繫結的this
物件。(這和Java裡的invoke需要傳遞一個上下文例項不差不多嗎)。
function foo(){
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj);
上面我們就把物件obj
繫結給本次呼叫的this
了。
前面說了,this
一定是個物件型別,所以你這裡如果傳遞基本型別,則會被轉換成包裝型別。
但這好像仍然解決不了前面方法一被賦值this
就會被改變的問題,你不能指望你的使用者每次呼叫你的方法時都使用call
或者apply
並且自己繫結物件。
一個最簡單的辦法是提供一個函式幫助使用者去完成呼叫call
,繫結this
的操作
function bar(){
foo.call(obj)
}
bar();
把bar
暴露給使用者,無論它的作用域是由於疏忽的賦值操作被修改還是被顯式的惡意修改了,都不會影響實際內層foo
所繫結的this
物件。
可以考慮提供一個通用的方法來做這步操作,而不是把每一個函式都包裝一層。
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments);
}
}
function sayHello(name){
console.log(this.prefix + ',' + name);
}
var speaker = {
prefix: 'Hello',
}
speaker.sayHello = bind(sayHello,speaker);
speaker.sayHello('Julia'); // Hello,Julia
bind
方法用於提供將一個物件obj
繫結給函式fn
的能力,它返回一個新的方法,這個方法和我們之前的包裝方法無異。
這樣寫程式碼會更優雅一點,以後我們的每個方法都可以直接使用bind
來繫結this
,而不用單獨提供一個包裝方法。
ES5
提供了預設的bind
方法。
function sayHello(name){
console.log(this.prefix + ',' + name);
}
var speaker = {
prefix: 'Hello',
}
speaker.sayHello = sayHello.bind(speaker);
speaker.sayHello('Julia'); // Hello,Julia
效果和我們所寫的一樣,但更加符合面向物件的程式設計模式。
使用call
和apply
來繫結的this
的優先順序毫無疑問要高於隱式繫結和預設繫結。
改寫我們之前的程式碼
function sayHello(name){
console.log(this.prefix + ',' + name);
}
var speaker = {
prefix: 'Hello',
sayHello: sayHello
}
var anotherSpeaker = {
prefix: 'Hi',
}
speaker.sayHello('Julia'); // Hello,Julia
speaker.sayHello.call(anotherSpeaker,'Julia'); // Hi,Julia
正常呼叫speaker.sayHello
使用的就是當前speaker
的上下文物件,這是隱式呼叫。而當你使用call
或apply
給它繫結一個其他的物件時,this
物件會被繫結成你指定的物件。
這也不用故意去記,這是很自然的事。
截至目前,優先順序排序為:預設繫結 < 隱式繫結 < 顯式繫結
new繫結
JS中的new
和其他語言的不同。
new
後面可以跟一個函式,然後會自動執行這個函式,並在執行之前做幾件事情:
- 建立一個新物件
- 這個新物件被執行[[原型]]連結 (先不管他是啥)
- 這個新物件會被繫結到後面所跟的函式的
this
上 - 如果函式沒返回其他物件,這個新物件會被預設返回
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
嘿,即使js中new
的執行原理看似和傳統面嚮物件語言並不搭邊兒,但出來的效果還真和建構函式挺像呢哈。準確點來說應該是構造呼叫。
因為new
建立了一個新物件,所有操作在這個新物件上進行,所以它的優先順序理應是最高的,注意這裡說它優先順序最高的意思是它不會被其他繫結操作所影響,因為這個新物件和之前的老物件已經完全沒關係了。
最終的優先順序排序為:預設繫結 < 隱式繫結 < 顯式繫結 < new繫結
未完。。。