以 VSCode dubug 模式來看,經典的觀念:作用域(Scope)、this、閉包(Closure)
回憶
昨天提到了用 debug 模式玩ES6的基本語法。
目標
以 VSCode dubug 模式來看,經典的觀念:作用域(Scope)、this、閉包(Closure)
函數(function)
函數可看成一群程式碼的集合,可以幫我們包裝 routine 的工作 (可以重複呼叫),命名後可以增加程式碼可讀性。 函數也引發了變數作用域、閉包、this 的問題。
基本宣告
有兩種方法可以宣告函數
- 函數物件
1function sayHi() { 2 console.log('Hi!)'); 3} - 箭頭函數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,如下圖:
1(anonymousFunction() {
2 // …上面程式碼…
3})()
此外, VARIABLES->Local 中有在函數內可以存取的變數,但會發現 沒有 ifLet ,也就是第8行不能讀到ifLet的原因。
再往下執行到第5行,
ifLet,而 VARIABLES->Local 還留著。
再往下執行到第13行,
fun1 中, CALL STACK 自然就只剩下 innerLet 和 innerVar ( this 晚點說)
再往下執行到第19行,
fun1() 後 innerLet 和 innerVar就會被消毀,當再次回到「進來前的函數空間」, innerLet 和 innerVar 當然就存取不到了,也就是第17,18行不能讀到他們。
可以試試把第 8, 17, 18註解拿掉,會丟出例外
我們整理結論:
var是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local 中let是屬於區塊作用域(block scope),活在{…}(curly brackets),出現在 VARIABLES->Block 中
那…const 呢? 它跟 let 一樣,只是變數不能再次被賦值(=)。
所以結論:
var是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local 中let/const是屬於區塊作用域(block scope),活在{…}(curly brackets),出現在 VARIABLES->Block 中
要怎麼選用它們?
我的做法是變儘量限制它們的 scope,以降低無法預期的效果,像是:var 變數生存太久佔用記憶體、本應該是常數的東西不小心執行時被改到、存取到本不應該存在的變數…等
- 能用
const儘量用 - 可能要改值,就用
let - 最後才用
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,此時 this 是 global
下面這張圖,程式是從第17行進入至第3行,呼就叫的人是誰?是 obj,所以 this 是 obj 且它的型別是 Object,打開來看看真的是它
閉包(Closure):把外面的變數關在函數中使用
我們考慮以下問題:
- 函數外宣告的變數,能不能在函數內使用?
- 函數內如何使用外部變數的值?
- 閉包域中的外部變數值可以被修改嗎?
回答這些問題
- 可以,每個建立一個函數物件時,會會有一個閉包域產生。當函數物件執行時可以存取外部變數
- 一般有兩個方法
- 用參數,把值傳入
1const outer = 'outer'; 2function fun(a){ 3 console.log(a); 4} 5fun(outer); - 透過閉包,把外部變數包入閉包域
1const outer = 'outer'; 2function fun(){ 3 console.log(outer); // 引用到外部變數,所以會放到 fun()函數中的閉包域 4} 5fun();
- 用參數,把值傳入
- 可以,因為閉包域中的變數和外部變數是相同的記憶體位置,所以可以被修改。見下面說明。
執行以下程式,並下中斷點,為了看的更清楚我們很刻意的放到 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 閉包域中,使我們可以存取它的值。
下圖中,因為宣告了 const inner,它是屬於 VARIABLES->Local ,並設定成outer的值,所以 inner = 'outer'。然而,outer = outer + '-fix',把 outer 改成了 outer-fix的值。此外,outer 也被放入 VARIABLES->Closure 閉包域中。
最後在呼叫一次 funA(),得到 outer 被修改後的結果。
什麼時候用?
可以用閉包包入定值,但很煩,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 - 閉包
評論