National Yang Ming Chiao Tung University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

陽明交大 AQI 69 24°C PM2.5 26
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 12

作用域與閉包

全域與區域變數、區塊作用域與 closure
Interaction實作 25 minute(s)

Learning Objectives

  • 說明靜態作用域:函式在宣告的位置就決定了看得到哪些變數
  • 分辨全域變數、函式作用域與區塊作用域,以及 var 與 let、const 的差別
  • 依作用域鏈判斷一個變數名稱會找到哪一個變數
  • 說明閉包如何讓函式記住外層的變數,並用閉包把資料藏起來

作用域:變數在哪裡看得到

作用域(scope,也稱變數領域)是「一個變數在程式的哪些地方可以使用」。離開了這個範圍,就讀不到這個變數。

JavaScript 採用靜態作用域(static scope,也叫詞法作用域 lexical scope):函式看得到哪些變數,在寫程式的位置就決定了, 不會因為「在哪裡被呼叫」而改變。下面這個例子裡,function2 有自己的 x,但它呼叫的 function1 寫在外面, 所以 function1 看到的是外面的 x。

靜態作用域:看宣告的位置,不看呼叫的位置

一步一步播放,注意第 5 行執行時,右邊有兩個框架:function2 裡的 x 是「內部變數」,但 function1 的框架裡沒有 x,它往外找,找到的是全域的 x。

全域、函式與區塊作用域

作用域 怎麼產生 誰看得到
全域(global) 寫在所有函式與區塊外面的宣告 整份程式都看得到
函式作用域(function scope) 每呼叫一次函式就產生一個 只有這個函式裡面(包含裡面的內層函式)
區塊作用域(block scope) { } 區塊,例如 if、for、while 的本體 只有這個區塊裡面;只對 let、const 有效

var 不認得區塊,只認得函式:在 if 區塊裡用 var 宣告,整個函式都看得到;let、const 離開區塊就不見了。 這也是現在建議一律用 let、const,不再用 var 的原因之一。

在瀏覽器裡,最外層用 var 宣告的變數與函式宣告會變成 window 物件的屬性(window.x), 最外層的 let、const 雖然也是全域變數,但不會掛到 window 上。

var 與 let:區塊結束之後

看第 3 行:函式一開始,a 就已經出現在框架裡,值是 undefined,這是 var 的提升(hoisting)。執行到第 8 行時,if 區塊已經結束,b 從框架裡消失了,a 還在。

作用域鏈:一層一層往外找

程式用到一個變數名稱時,JavaScript 依照下面的順序找:

  1. 先找目前所在的作用域(目前的區塊、目前的函式)
  2. 找不到,就往宣告這個函式的外層找
  3. 一路找到全域;全域也沒有,就是 ReferenceError: x is not defined

只要在某一層找到,就不再往外找。所以內層宣告了與外層同名的變數時,內層會「遮住」外層的(shadowing)。

三層巢狀函式的作用域鏈

small 裡沒有宣告任何變數,卻能印出 x、y、z。執行到第 9 行時看右邊:small 的框架裡標著「外層」的 y、z,是它從外層函式找到的變數。

同名變數:內層遮住外層

全域與函式裡都有 a。函式裡的 return a 找到的是哪一個?函式結束之後,全域的 a 有沒有被改掉?

想一想:作用域

1.

下面的程式會印出什麼?

let n = 1;
function show() {
  console.log(n);
}
function run() {
  let n = 2;
  show();
}
run();
2.

下面的程式會印出什麼?

function test() {
  if (true) {
    var a = 1;
    let b = 2;
  }
  console.log(typeof a, typeof b);
}
test();
3.

在函式裡讀取一個變數,這個函式自己沒有宣告它,JavaScript 接下來會怎麼做?

第 1 次作答

閉包:函式記得它出生的地方

閉包(closure)是「函式」加上「它被建立時所在的作用域」。

一般來說,函式執行完,它的區域變數就用不到了。但如果函式裡面回傳了另一個函式,而這個內層函式用到了外層的變數, 外層的變數就會跟著內層函式一起留下來,即使外層函式已經執行完畢。

下面的 makeCounter 每呼叫一次,就產生一個新的作用域與一個新的 count,回傳的函式記住的是自己那一份 count。

計數器:兩個閉包、兩份 count

注意第 4 行回傳之後,makeCounter 的框架消失了,但呼叫 c1() 時,右邊的框架裡又出現了標著「外層」的 count,而且值是上一次留下來的。c2 有自己的 count,從 0 開始。

迴圈裡的閉包:var 與 let 的差別

兩個迴圈都把「回傳迴圈變數」的箭頭函式放進陣列。用 var 的迴圈從頭到尾只有一個 i,迴圈結束時是 3;用 let 的迴圈每一圈都建立一個新的 j,每個函式記得各自那一圈的值。

閉包的用途:把資料藏起來

閉包讓「資料」和「使用資料的函式」綁在一起,外面的程式只能透過這些函式操作資料,不能直接改。 這和物件導向裡的私有(private)屬性類似。下面的帳戶例子裡,balance 藏在 createAccount 的作用域裡, 外面只拿得到 deposit 和 getBalance 兩個方法,acc.balance 根本不存在。

用閉包保護帳戶餘額

看第 6 行:deposit 的框架裡,balance 標著「外層」,它是 createAccount 留下來的變數。最後一行讀 acc.balance 得到 undefined:回傳的物件上沒有這個屬性。

想一想:閉包

1.

下面的程式會印出什麼?

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}
const add5 = makeAdder(5);
console.log(add5(3));
2.

下面的程式會印出什麼?

const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log(fns[0]());
3.

關於閉包,下列哪一個說法正確?

第 1 次作答

實作:用閉包做出乘法器

完成 makeMultiplier(n):它要回傳一個函式,這個函式收到 x 之後回傳 x * n。完成後 double(4) 應該是 8、triple(4) 應該是 12。

你的輸出
預期輸出

重點精華回顧

  • JavaScript 是靜態作用域:函式看得到哪些變數,由它宣告的位置決定,不是由呼叫的位置決定。
  • 作用域有三種:全域、函式作用域、區塊作用域;let、const 認得區塊,var 只認得函式,而且會被提升到函式開頭。
  • 找變數時沿著作用域鏈從內往外找,找到就停;內層同名變數會遮住外層的。
  • 閉包=函式+它被建立時的作用域:內層函式用到的外層變數,會在外層函式結束後繼續存在。
  • 每次呼叫外層函式都會產生新的作用域,所以每個閉包記得的是自己那一份變數;迴圈裡建立函式時,用 let 每一圈才會有自己的變數。
  • 閉包可以把資料藏起來,只開放特定函式操作,達到類似私有屬性的效果。
這一課完成了!
Next lesson:Math 物件
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。