以 VSCode dubug 模式來看,經典的觀念:作用域(Scope)、this、閉包(Closure)

回憶

昨天提到了用 debug 模式玩ES6的基本語法。

目標

以 VSCode dubug 模式來看,經典的觀念:作用域(Scope)、this、閉包(Closure)

函數(function)

函數可看成一群程式碼的集合,可以幫我們包裝 routine 的工作 (可以重複呼叫),命名後可以增加程式碼可讀性。 函數也引發了變數作用域、閉包、this 的問題。

基本宣告

有兩種方法可以宣告函數

  1. 函數物件
    1function sayHi() {
    2    console.log('Hi!)');
    3}
    
  2. 箭頭函數arrow function
    1const sayHi = () => {
    2    console.log('Hi!');
    3}
    

var/let/const 作用域(Scope): 變數生存的空間

接下來會用 debug 模式,觀察 var/let 的特性。

在 ES6 出來以前只有 var 可以用,這是指在宣告在函數內的變數,在這函數的執行過程中會一直在,不管包幾層區塊。

在區塊 ({…}) 內宣告的變數可以在區塊外使用嗎?

我們觀察以下程式碼:

 1// var/let in global
 2const runIf = true;
 3if(runIf) {
 4    var ifVar = 'ifVar'; // if 執行完會留下
 5    let ifLet = 'ifLet';
 6}
 7console.log(ifVar); // 執行到這行,可存取到 ifVar,因為 ifVar 是在主程式函數中宣告的
 8// console.log(ifLet); // ReferenceError: ifLet is not defined
 9
10
11// var/let in function
12function fun1() {
13    var innerVar = 'fun1Let'; // fun1()執行完不會留下
14    let innerLet = 'fun1Let'; // fun1()執行完不會留下
15}
16fun1();
17// console.log(innerVar); // ReferenceError: innerVar is not defined
18// console.log(innerLet); // ReferenceError: innerLet is not defined
19console.log('bye');

在第2,5,13,19行下中斷點,執行 debug,如下圖: Screen Shot 2018-10-05 at 4.48.08 PM.png 停在第2行後,看看 CALL STACK ,目前執行在匿名函數中 (anonymous function)中,也就是,當程式執行時,我們可以假想它們被包在某個函數中且立刻被執行,像是:

1(anonymousFunction() {
2  // …上面程式碼…
3})()

此外, VARIABLES->Local 中有在函數內可以存取的變數,但會發現 沒有 ifLet ,也就是第8行不能讀到ifLet的原因。

再往下執行到第5行, Screen Shot 2018-10-05 at 4.48.15 PM.png 多出 VARIABLES->Block ,裡面有ifLet,而 VARIABLES->Local 還留著。

再往下執行到第13行, Screen Shot 2018-10-05 at 4.48.22 PM.png CALL STACK 現在進入到 fun1 中, CALL STACK 自然就只剩下 innerLetinnerVar ( this 晚點說)

再往下執行到第19行, Screen Shot 2018-10-05 at 4.48.39 PM.png 離開 fun1()innerLetinnerVar就會被消毀,當再次回到「進來前的函數空間」, innerLetinnerVar 當然就存取不到了,也就是第17,18行不能讀到他們。

可以試試把第 8, 17, 18註解拿掉,會丟出例外

我們整理結論:

  1. var 是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local
  2. let 是屬於區塊作用域(block scope),活在 {…} (curly brackets),出現在 VARIABLES->Block

那…const 呢? 它跟 let 一樣,只是變數不能再次被賦值(=)。 所以結論:

  1. var 是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local
  2. let/const 是屬於區塊作用域(block scope),活在 {…} (curly brackets),出現在 VARIABLES->Block

要怎麼選用它們?

我的做法是變儘量限制它們的 scope,以降低無法預期的效果,像是:var 變數生存太久佔用記憶體、本應該是常數的東西不小心執行時被改到、存取到本不應該存在的變數…等

  1. 能用 const 儘量用
  2. 可能要改值,就用 let
  3. 最後才用 var

this:呼叫函數的人

this 在OO(物件導向)技術被用來當做實例(instance)的代理變數。在 javascript 也有類似的功用,但 this 是可以被我們動態替換的,所以可以做的更多,見:JavaScript - call,apply,bind。現階段只要了解:this就是呼叫函數的人

看以下的程式,下中斷點觀察

 1const funA = () => {
 2    console.log(this);
 3};
 4function funF () {
 5    console.log(this);
 6};
 7
 8const obj = {
 9    funA: funA,
10    funF: funF,
11}
12
13funA();
14funF();
15
16obj.funA();
17obj.funF();

下面這張圖,程式是從第14行進入至第3行,呼就叫的人是誰?因為沒有指明人,就會拿最上層的人(物件),所以就叫 global,此時 thisglobal Screen Shot 2018-10-05 at 5.23.30 PM.png

下面這張圖,程式是從第17行進入至第3行,呼就叫的人是誰?是 obj,所以 thisobj 且它的型別是 Object,打開來看看真的是它 Screen Shot 2018-10-05 at 5.26.24 PM.png

閉包(Closure):把外面的變數關在函數中使用

我們考慮以下問題:

  1. 函數外宣告的變數,能不能在函數內使用?
  2. 函數內如何使用外部變數的值?
  3. 閉包域中的外部變數值可以被修改嗎?

回答這些問題

  1. 可以,每個建立一個函數物件時,會會有一個閉包域產生。當函數物件執行時可以存取外部變數
  2. 一般有兩個方法
    1. 用參數,把值傳入
      1const outer = 'outer';
      2function fun(a){
      3  console.log(a);
      4}
      5fun(outer);
      
    2. 透過閉包,把外部變數包入閉包域
      1const outer = 'outer';
      2function fun(){
      3  console.log(outer); // 引用到外部變數,所以會放到 fun()函數中的閉包域
      4}
      5fun();
      
  3. 可以,因為閉包域中的變數和外部變數是相同的記憶體位置,所以可以被修改。見下面說明。

執行以下程式,並下中斷點,為了看的更清楚我們很刻意的放到 main() 中執行,

 1function main() {
 2    let outer = 'outer'; // 外部變數
 3    function funA() {
 4        console.log(outer); // 讀取到外部變數
 5    };
 6
 7    function funB() {
 8        const inner = outer; // 內部變數,指向 outer 的值
 9        outer = outer + '-fix'; // 修改 outer 的值
10        console.log(inner, outer);
11    };
12
13    funA();
14    funB(); // outer 值被修改
15    funA();
16};
17
18main();

下圖中,outer 放入 VARIABLES->Closure 閉包域中,使我們可以存取它的值。 Screen Shot 2018-10-05 at 8.57.12 PM.png

下圖中,因為宣告了 const inner,它是屬於 VARIABLES->Local ,並設定成outer的值,所以 inner = 'outer'。然而,outer = outer + '-fix',把 outer 改成了 outer-fix的值。此外,outer 也被放入 VARIABLES->Closure 閉包域中。 Screen Shot 2018-10-05 at 8.57.19 PM.png 用記憶體圖示來說,就會很清楚了,白正方形是記憶體空間,裡面會放字串值。 Untitled Diagram.png

最後在呼叫一次 funA(),得到 outer 被修改後的結果。 Screen Shot 2018-10-05 at 8.57.33 PM.png

什麼時候用?

可以用閉包包入定值,但很煩,ES6 引入的 let 可以簡化不少。

 1// i, j loop 完,變成定值
 2let funs = [];
 3for (var i = 0; i < 3; i++) {
 4    var j = i;
 5    funs.push(function () {
 6        console.log(i, j); // loop 完才用到 i, j
 7    });
 8}
 9funs.forEach(fun => fun());
10
11// 用閉包
12funs = [];
13for (var i = 0; i < 3; i++) {
14    (function () {
15        var j = i; // 把值存入一個匿名函數的閉包
16        funs.push(function () {
17            console.log(i, j);
18        });
19    })();
20}
21funs.forEach(fun => fun());
22
23// 用 let
24funs = [];
25for (var i = 0; i < 3; i++) {
26    let j = i;
27    funs.push(function () {
28        console.log(i, j);
29    });
30}
31funs.forEach(fun => fun());

結果:

13 2
23 2
33 2
43 0
53 1
63 2
73 0
83 1
93 2

只有後面兩種寫法會正確。

復雜習題

可以猜看看下面的結果:

 1const funs = [];
 2for (var i = 0; i < 3; i++) {
 3    const j = i;
 4    funs.push(function () {
 5        const inner = i;
 6        console.log(inner, i, j);
 7    });
 8}
 9
10funs.forEach(fun => fun());

總結

今天用 debug 模式,觀察作用域(Scope)、this、閉包(Closure)的例子,並發現下面的關連性。 VARIABLES->Local - var VARIABLES->Block - let/const VARIABLES->Closure - 閉包

參考連結