ข้ามไปยังเนื้อหา

Scope และ Closures

JavaScript มี keyword การประกาศสามแบบ แต่ละแบบมีกฎ scoping ที่แตกต่างกัน:

KeywordScopeHoistedReassignableRe-declarable
varFunctionใช่ (เป็น undefined)ใช่ใช่
letBlockใช่ (TDZ)ใช่ไม่
constBlockใช่ (TDZ)ไม่ไม่

Block scope หมายความว่าตัวแปรมีอยู่เฉพาะระหว่างเครื่องหมาย {} ที่ใกล้ที่สุด Function scope หมายความว่าตัวแปรมีอยู่ตลอด function body ไม่ว่า block ด้านในจะเป็นอย่างไร

function example() {
if (true) {
var x = 1; // function-scoped — รั่วออกนอก if
let y = 2; // block-scoped — อยู่แค่ใน if
const z = 3; // block-scoped
}
console.log(x); // 1 — มองเห็นที่นี่
console.log(y); // ReferenceError: y is not defined
}

การประกาศตัวแปรถูก hoist — เลื่อนไปที่ด้านบนของ scope โดย engine ก่อนการ execution

การ hoist ของ var จะ initialise ตัวแปรเป็น undefined ทันที ส่วน let/const ถูก hoist แต่อยู่ใน Temporal Dead Zone (TDZ) — การเข้าถึงก่อนบรรทัดที่ประกาศจะ throw ReferenceError

console.log(a); // undefined — var ถูก hoist และ initialised
var a = 5;
console.log(b); // ReferenceError — let อยู่ใน TDZ
let b = 10;

Function declarations ก็ถูก hoist อย่างสมบูรณ์ — สามารถเรียกก่อนบรรทัดที่เขียนได้ แต่ Function expressions (ที่กำหนดให้กับตัวแปร) ไม่ถูก hoist

Scope ใน JavaScript เป็น lexical (หรือ static): scope ของฟังก์ชันถูกกำหนดโดยตำแหน่งที่เขียนใน source code ไม่ใช่ตำแหน่งที่เรียก ฟังก์ชันด้านในสามารถอ่านตัวแปรจาก scope รอบนอกได้เสมอ

const prefix = 'LOG';
function outer() {
const level = 'INFO';
function inner(msg) {
// inner อ่านได้ทั้ง prefix (module scope) และ level (outer scope)
console.log('[' + prefix + ':' + level + '] ' + msg);
}
inner('started');
}
outer(); // [LOG:INFO] started

Closure คือฟังก์ชันที่เก็บการเข้าถึงตัวแปรของ scope รอบนอกไว้แม้ว่าฟังก์ชันนั้นจะ return ไปแล้ว ฟังก์ชันด้านในไม่ได้คัดลอกค่า — แต่ถือ reference ที่มีชีวิตอยู่ไปยังตัวแปรนั้น

use case คลาสสิกคือ counter factory:

function makeCounter(start) {
let count = start;
return {
increment: function() { count += 1; },
decrement: function() { count -= 1; },
value: function() { return count; },
};
}
const counter = makeCounter(0);
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.value()); // 2

count อาศัยอยู่ใน scope ของ makeCounter ทุกฟังก์ชันที่ return จาก makeCounter ถือ reference ไปยัง count ตัวเดียวกัน

JavaScript

สังเกตว่า c1 และ c2 เป็นอิสระจากกันอย่างสมบูรณ์ — การเรียก makeCounter แต่ละครั้งสร้างตัวแปร count ใหม่ใน scope ใหม่ และแต่ละ object ที่ return กลับมาจะ close over ตัวแปรของตัวเอง

ความแตกต่างหลักระหว่าง `let` และ `var` ในเรื่อง scope คืออะไร?
Closure คืออะไร?
เกิดอะไรขึ้นเมื่อเข้าถึงตัวแปร `let` ก่อนบรรทัดที่ประกาศ?