iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 10
0
自我挑戰組

花三十天找到 JavaScript 沙漠中的綠洲系列 第 10

10 This

全域與區域變數

昨天我們提到了 this ,但礙於篇幅無法仔細描述,今天讓我們深入來看看 this 到底是什麼。

要講 this 就必須先介紹全域與區域變數的概念。其實,在 02 var、let、const 與 ES6 簡介中,我們有稍微提過這個概念,只是並沒有仔細描述。所謂的全域變數,就是直接 window 底下宣告,沒有放進函式裡宣告的變數。而區域變數,則是在函式裡頭宣告的變數,只有函式內可以使用,所以稱為區域。

廢話不多說,先來一題前菜:

var a = 1;

function b(){
    var c = 2;
}

console.log(a);
console.log(c);

上面哪個是區域變數?哪個是全域變數?

答案是 a 是全域變數, c 是區域變數。所以如果我在 function b 外面要去運用 c 這個值,就會無法使用。舉例來說,上面的 console.log 最後只能得到 a 的答案,而 c 則會顯示錯誤訊息 c is not defined 。

讓我們看看下一題:

var a = 1; 

function bar() {
  console.log(a); 
  var a = 2; 
  console.log(a); 
  
  function bar_child () {
    console.log(a); 
  }
  
  function bar_child_2() {
    var a = 3; 
    console.log(a);  
  }
  
  bar_child();
  bar_child_2();
}

function bar_2() {
  console.log(a); 
}

bar(); 
bar_2(); 

你覺得上面的 bar() 和 bar_2() 會得到什麼答案呢?別被這麼長的式子嚇著了,讓我們來分析一下整個結構。

首先是比較簡單的 bar_2() 。

function bar_2() {
  console.log(a);  // 印出 a
}

a 是多少呢? bar_2 本身區域中並沒有宣告,所以跳出來往上一看,發現最上面宣告了 var a = 1; 因為 bar_2 不能去拿別人家的 a ,只能拿外面全域的,因此這裡的 bar_2() 會得到 1 的答案。這題應該沒什麼問題,只要知道全域跟區域的概念就能回答了。讓我們來看看 bar() 。

var a = 1; 

function bar() {
  console.log(a); 
  var a = 2; 
  console.log(a); 
  
  function bar_child () {
    console.log(a); 
  }
  
  function bar_child_2() {
    var a = 3; 
    console.log(a);  
  }
  
  bar_child();
  bar_child_2();
}

這裡總共有 4 次 console.log 。第一次,在它上面沒有宣告過 a 是誰,因此印出 undefined 。你可能會覺得很奇怪,欸不是,剛剛 function bar_2() 裡 console.log 上面也沒有宣告過 a 是誰啊,為什麼可以直接拿外面全域的 a ,這裡卻不能呢?

這是因為要優先取用區域變數裡的東西。而就在第一次的 console.log 下方,出現了宣告 a 是誰的句子。代表區域裡是有宣告的,所以不會去取全域的東西。當重複宣告時,就算先取用也只會顯示 undefined 。

第二次 console.log 答案是 2 ,這應該就沒什麼問題。上面宣告了 a = 2 ,這裡自然得到 2 的答案。

第三次跟 function bar_2() 這題有點像。在 function bar_child () 裡,並沒有宣告過 foo 是誰,所以就到他的父層,也就是 function bar() 裏頭去尋找,發現裡面宣告過 a = 2 ,因此第三次也會得到 2 的答案。

最後第四次應該不用我多說,答案會是 3 。

如果上面的例題,對你來說輕而易舉,那我們就可以進一步進入到 this 的世界。

利用 This 節省打重複程式的時間

先來寫一段程式:

function getGender(){
  return this.gender;
};

let people1 = {
  gender: 'female',
  getGender: getGender
};

let people2 = {
  gender: 'male',
  getGender: getGender
};

console.log(people1.getGender());
console.log(people2.getGender());

This 跟英文一樣,假設你講一段話,每次都要講一次這是阿明的同事的外婆的項鍊,那舌頭可能講完三次就直接打結起水泡。當上面的 function ,要隨下方不同人跳出不同結果時,為避免每次 return 都要逐一寫 return peoplexx.gender ,可利用 this 來解決。

但是要注意全域變數的問題。上面的範例中, gender 存在在物件(也就是區域變數)中,因此在 getGender 時,會取到正確的數字。但如果你要呼叫的東西,不在物件中,而是本身就是個物件呢?在下面的例子中,你會發現這將導致取到全域變數(也就是window)的值,而顯示不出原先設計時想跑出的效果:

var foo = function() {
  this.count++;
};

foo.count = 0;

for(var i = 0; i < 5; i++) {
  foo();
}

上面的例子中,原本是希望讓 for 迴圈跑完以後,把 foo 的值送出去。但 foo 本身就是函式,宣告的時候放在全域中,因此是全域變數。導致下面呼叫 foo 時,實際上是讓 this 呼叫到 window ,偏偏 window 中從頭到尾,沒有寫一行 window.count 。 undefined 加了 5 次,怎麼會有 undefined 加法呢?最後跑出的結果當然會是 NaN 。

foo.count 則會一直維持在 0 的狀態了。

這個例子我花了很久的時間才參透其中意義。舉另一個較簡單的例子來說:

var foo = 1;
console.log(window.foo);

以上的程式會跑出 1 的結果。整段的重點是 foo 在全域中宣告,因此 console.log 的括號中要寫 window.foo 。相信熟悉全域區域觀念的你,可以立刻發現,同理,上面第一個例子也隱藏類似觀念。

綜合以上可知,要用 this 呼叫的東西必須存放在物件(也就是區域變數)中。 this 代表的是 function 執行時所屬的物件,不是 function 本身。

所以當程式這樣寫:

var foo = function() {
  this.count++;
};

this 指的不是 function foo 本身,而是 function foo 執行時所屬的物件。也就是 window ,因為 foo 放在全域中,所以隸屬在 window 下面。如果你看懂了這句,再回去看上面的例題也許會更加明白其中的奧義。

學習與參考資料


上一篇
09 事件處理與監聽
下一篇
11 物件與陣列
系列文
花三十天找到 JavaScript 沙漠中的綠洲35

尚未有邦友留言

立即登入留言