1. 程式人生 > 實用技巧 >別再為了this發愁了------JS中的this機制

別再為了this發愁了------JS中的this機制

別再為了this發愁了------JS中的this機制

  題記:JavaScript中有很多令人困惑的地方,或者叫做機制。但是,就是這些東西讓JavaScript顯得那麼美好而與眾不同。比方說函式也是物件、閉包、原型鏈繼承等等,而這其中就包括頗讓人費解的this機制。不管是新手還是老手,不仔細深摳一下還真鬧不明白this倒地咋回事捏。今天,我們就一起看一下this倒地咋回事,別再為了this發愁了。


  1、this是啥?

  簡言之,this是JavaScript語言中定義的眾多關鍵字之一,它的特殊在於它自動定義於每一個函式域內,但是this倒地指引啥東西卻讓很多人張二摸不著頭腦。這裡我們留個小懸念,希望看完這篇文章了你能回答出來this到底指引個甚。


  2、this有啥用?

  那邊觀眾又該問了,既然this這麼難以理解,那麼為個甚還要用它呢?我們來看個例子:

 1 function identify() {
 2     return this.name.toUpperCase();
 3 }
 4 function sayHello() {
 5     var greeting = "Hello, I'm " + identify.call( this );
 6     console.log( greeting );
 7 }
 8 var person1= {
 9     name: "Kyle"
10 };
11 var person2= {
12     name: "Reader"
13 };
14 identify.call( person1); // KYLE
15 identify.call( person2); // READER
16 sayHello.call( person1); // Hello, I'm KYLE
17 sayHello.call( person2); // Hello, I'm READER

  這段程式碼很簡單,我們定義了兩個函式,分別為identify和sayHello。並且在不同的物件環境下執行了它們,達到了複用的效果,而不用為了在不同的物件環境下執行而必須針對不同的物件環境寫對應的函數了。簡言之,this給函式帶來了複用。那邊客官又問了,我不用this一樣可以實現,如:

 1 function identify(context) {
 2     return context.name.toUpperCase();
 3 }
 4 function sayHello(context) {
 5     var greeting = "Hello, I'm " + identify( context);
 6     console.log( greeting );
 7 }
 8 var person1= {
 9     name: "Kyle"
10 };
11 var person2= {
12     name: "Reader"
13 };
14 identify( person1); // KYLE
15 identify( person2); // READER
16 sayHello( person1); // Hello, I'm KYLE
17 sayHello( person2); // Hello, I'm READER

  仔細一看,這位客官給出的解決方法的確也達到了類似的效果。贊一個!我想說的是,隨著程式碼的增加,函式巢狀、各級呼叫等變得越來越複雜,那麼傳遞一個物件的引用將變得越來越不明智,它會把你的程式碼弄得非常亂,甚至你自己都無法理解清楚。而this機制提供了一個更加優雅而靈便的方案,傳遞一個隱式的物件引用讓程式碼變得更加簡潔和複用。好了,知道了this的用處,那麼再看看我們對它的誤解。

  3、關於this的誤解


  相信很多童鞋是學過其它語言的,在很多程式語言中都有this的機制,慣性思維把其它語言裡對它的理解帶到了JavaScript中。同時,由於this這個單詞的理解導致了我們產生了對它各種各樣的誤解。所以,開始前,我們先澄清下對它的誤解。

  3.1 誤解一:this引用function本身

  我們都知道,在函式裡引用函式可以達到遞迴和給函式屬性賦值的效果。而這在很多應用場景下顯得非常有用。所以,很多人都誤以為this就是指引function本身。例如:

 1 function fn(num) {
 2     console.log( "fn: " + num );
 3     // count用於記錄fn的被呼叫次數
 4     this.count++;
 5 }
 6 fn.count = 0;
 7 var i;
 8 for (i=0; i<10; i++) {
 9     if (i > 5) {
10         fn( i );
11     }
12 }
13 // fn: 6
14 // fn: 7
15 // fn: 8
16 // fn: 9
17 
18 console.log( fn.count ); // 0 -- 耶?咋不是4捏?  

  上面我們想要記錄fn被呼叫的次數,可是明顯fn被呼叫了四次但count仍然為0。咋回事捏?這裡簡單解釋下,fn裡第4行的自增隱式的建立了一個全域性變數count,由於初始值為undefined,所以每一次自增其實依然不是一個數字,你在全域性環境下列印count(window.count)輸出的應該是NaN。而第6行定義的函式熟悉變數count依然沒變,還是0。如果對這個執行結果不清楚的,歡迎去看我前些天的那篇博文(聊一下JS中的作用域scope和閉包closure scope和closure),在這裡你只需要知道,this引用的是function這種理解是錯誤的就行。

  這邊就會又有人問了,既然this不是引用function,那麼我要實現遞迴函式,該咋引用呢?這裡簡單回答下介個問題,兩種方法:①函式體內用函式名來引用函式本身②函式體內使用arguments.callee來引用函式(不推薦)。那麼既然第二種方法不推薦,匿名函式咋引用呢?用第一種,並且給匿名函式一個函式名即可(推薦)。
  3.2 誤解二:this引用的是function的詞法作用域

  這種誤解欺騙的人可能更多一些。首先,澄清一下,this並沒有引用function的詞法作用域。的確JS的引擎內對詞法作用域的實現的確像是一個物件,擁有屬性和函式,但是這僅僅是JS引擎的一種實現,對程式碼來說是不可見的,也就是說詞法作用域“物件”在JS程式碼中取不到。(關於詞法作用域,如果不理解,可以參考之前的一篇博文《聊一下JS中的作用域scope和閉包closure scope和closure》)。看個錯誤的例子:

1 function fn1() {
2     var a = 2;
3     this.fn2();//以為this引用的是fn1的詞法作用域
4 }
5 function fn2() {
6     console.log( this.a );
7 }
8 fn1(); //ReferenceError

  上面的程式碼明顯沒有執行出想要的結果,從而可以看到this並沒有引用函式的詞法作用域。甚至,可以肯定的說,這個例子裡fn2可以在fn1里正確執行都是偶然的(理解了詞法作用域你就知道為什麼這裡執行不報錯了)。


  4、this到底跟啥有關?

  好了,扯了那麼多都沒上乾貨,有的觀眾都開始關閉當前頁開始離席了。這裡,我們鄭重宣告:this跟函式在哪裡定義沒有半毛錢關係,函式在哪裡呼叫才決定了this到底引用的是啥。也就是說this跟函式的定義沒關係,跟函式的執行有大大的關係。所以,記住,“函式在哪裡呼叫才決定了this到底引用的是啥”。


  5、this機制的四種規則

  this到底繫結或者引用的是哪個物件環境決定於函式被呼叫的地方。而函式的呼叫有不同的方式,在不同的方式中呼叫決定this引用的是哪個物件是由四種規則確定的。我們一個個來看。

  5.1 預設繫結全域性變數

  這條規則是最常見的,也是預設的。當函式被單獨定義和呼叫的時候,應用的規則就是繫結全域性變數。如下: 

1 function fn() {
2     console.log( this.a );
3 }
4 var a = 2;
5 fn(); // 2 -- fn單獨呼叫,this引用window

  5.2 隱式繫結

  隱式呼叫的意思是,函式呼叫時擁有一個上下文物件,就好像這個函式是屬於該物件的一樣。例如:

1 function fn() {
2     console.log( this.a );
3 }
4 var obj = {
5     a: 2,
6     fn: fn
7 };
8 obj.fn(); // 2 -- this引用obj。

  需要說明的一點是,最後一個呼叫該函式的物件是傳到函式的上下文物件(繞懵了)。如:

 1 function fn() {
 2     console.log( this.a );
 3 }
 4 var obj2 = {
 5     a: 42,
 6     fn: fn
 7 };
 8 var obj1 = {
 9     a: 2,
10     obj2: obj2
11 };
12 obj1.obj2.fn(); // 42 -- this引用的是obj2.

  還有一點要說明的是,失去隱式繫結的情況,如下:

 1 function fn() {
 2     console.log( this.a );
 3 }
 4 var obj = {
 5     a: 2,
 6     fn: fn
 7 };
 8 var bar = obj.fn; // 函式引用傳遞
 9 var a = "全域性"; // 定義全域性變數
10 bar(); // "全域性"

  如上,第8行雖然有隱式繫結,但是它執行的效果明顯是把fn賦給bar。這樣bar執行的時候,依然是預設繫結全域性變數,所以輸出結果如上。

 5.3 顯示繫結

  學過bind()\apply()\call()函式的都應該知道,它接收的第一個引數即是上下文物件並將其賦給this。看下面的例子:

1 function fn() {
2     console.log( this.a );
3 }
4 var obj = {
5     a: 2
6 };
7 fn.call( obj ); // 2

  如果我們傳遞第一個值為簡單值,那麼後臺會自動轉換為對應的封裝物件。如果傳遞為null,那麼結果就是在繫結預設全域性變數,如:

1 function fn() {
2      console.log( this.a );
3  }
4  var obj = {
5      a: 2
6  };
7 var a = 10;
8 fn.call( null); // 10

  5.4 new新物件繫結

  如果是一個建構函式,那麼用new來呼叫,那麼繫結的將是新建立的物件。如:

1 function fn(a) {
2     this.a = a;
3 }
4 var bar = new fn( 2 );
5 console.log( bar.a );// 2

  注意,一般建構函式名首字母大寫,這裡沒有大寫的原因是想提醒讀者,建構函式也是一般的函式而已。


  6、結束語

  讀到現在,1中問的問題你該自己能回答上來了。上面介紹的四種關於this繫結的4中情況和規則,現實寫程式碼的過程中肯定比這要多和複雜,但是無論多複雜多亂,它們都是混合應用上面的幾個規則和情況而已。只要你的思路和理解是清晰的,那肯定沒問題的。