Article · programming
JavaScript Scope และ Closure
JavaScript
EP 10 ของซีรีส์ JavaScript คืออะไร? เริ่มต้นเขียนโปรแกรม เรียนรู้ Global Scope, Function Scope, Block Scope, Lexical Scope และ Closure พร้อมตัวอย่างโค้ดและแบบฝึกหัด
EP 10: Scope และ Closure เข้าใจขอบเขตตัวแปรอย่างลึกซึ้ง
ในตอนก่อนเราได้เรียนรู้การสร้างฟังก์ชันเพื่อแบ่งงานออกเป็นส่วนย่อยและนำกลับมาใช้ซ้ำได้ แต่เมื่อโปรแกรมเริ่มมีฟังก์ชันและตัวแปรจำนวนมาก คำถามสำคัญจะเกิดขึ้นว่า ตัวแปรหนึ่งสามารถถูกเรียกใช้จากตรงไหนได้บ้าง และเหตุใดฟังก์ชันบางตัวจึงยังจดจำข้อมูลที่ประกาศไว้ภายนอกได้ แม้การทำงานส่วนนั้นจะจบไปแล้วก็ตาม คำตอบของเรื่องนี้เกี่ยวข้องกับ **Scope** และ **Closure**
แนวคิดหลักของบทนี้Scope คือขอบเขตที่กำหนดว่าตำแหน่งใดสามารถเข้าถึงตัวแปรได้ ส่วน Closure คือความสามารถของฟังก์ชันในการเก็บการเข้าถึงตัวแปรจาก Lexical Scope ที่เกี่ยวข้อง แม้ฟังก์ชันจะถูกเรียกใช้งานภายหลัง
1. Scope คืออะไร?
Scope เปรียบเสมือนพื้นที่ที่ตัวแปรมีสิทธิ์ให้โปรแกรมเข้าถึงได้ ตัวแปรบางตัวประกาศไว้ภายนอกฟังก์ชันจึงอาจถูกเรียกใช้จากหลายตำแหน่ง ขณะที่ตัวแปรที่ประกาศภายในฟังก์ชันหรือบล็อกจะมีขอบเขตจำกัดกว่า การเข้าใจ Scope ช่วยลดข้อผิดพลาดจากการใช้ตัวแปรผิดตำแหน่งและทำให้โครงสร้างโปรแกรมเป็นระเบียบ
2. Global Scope: ตัวแปรระดับภายนอก
ตัวแปรที่ประกาศในระดับบนสุดของโปรแกรมหรือโมดูลสามารถมีขอบเขตระดับ Global หรือ Module ตามสภาพแวดล้อมที่ใช้งาน ในตัวอย่างพื้นฐานต่อไปนี้ ตัวแปร `siteName` ถูกประกาศภายนอกฟังก์ชัน จึงสามารถถูกอ่านจากภายในฟังก์ชัน `showSiteName` ได้
const siteName = 'LevelLance';
function showSiteName() {
console.log(siteName);
}
showSiteName();
console.log(siteName);
ทั้งคำสั่งภายใน `showSiteName()` และคำสั่งภายนอกฟังก์ชันสามารถอ่านค่า `siteName` ได้ในตัวอย่างนี้ อย่างไรก็ตาม ไม่ควรประกาศตัวแปรทุกอย่างเป็น Global โดยไม่จำเป็น เพราะจะทำให้ส่วนต่าง ๆ ของโปรแกรมพึ่งพาข้อมูลร่วมกันมากเกินไป
ระวังตัวแปรส่วนกลางตัวแปร Global ที่ถูกแก้ไขจากหลายตำแหน่งอาจทำให้ติดตามต้นเหตุของข้อผิดพลาดได้ยาก ควรจำกัดขอบเขตข้อมูลให้แคบที่สุดเท่าที่งานต้องการ
3. Function Scope: ขอบเขตภายในฟังก์ชัน
ตัวแปรที่ประกาศด้วย `let` หรือ `const` ภายในฟังก์ชันจะอยู่ในขอบเขตของฟังก์ชันนั้น และไม่สามารถเข้าถึงจากภายนอกได้โดยตรง หลักการนี้ช่วยให้ข้อมูลภายในไม่รั่วไหลไปยังส่วนอื่นของโปรแกรม
function calculatePrice(price, quantity) {
const total = price * quantity;
return total;
}
console.log(calculatePrice(50, 3));
// ตัวแปร total อยู่ภายใน calculatePrice
// console.log(total); จะเกิด ReferenceError
ตัวแปร `total` ถูกสร้างขึ้นภายใน `calculatePrice` จึงใช้งานได้ภายในฟังก์ชันนั้นเท่านั้น เมื่อฟังก์ชันส่งค่ากลับด้วย `return` ผู้เรียกจะได้รับค่าผลลัพธ์ แต่ไม่ได้รับสิทธิ์เข้าถึงตัวแปร `total` โดยตรง
4. Block Scope: ขอบเขตภายในบล็อก
บล็อกคำสั่งคือชุดคำสั่งที่อยู่ภายในวงเล็บปีกกา `{}` เช่น บล็อกของ `if`, `for` หรือบล็อกที่เขียนแยกไว้ ตัวแปรที่ประกาศด้วย `let` และ `const` ภายในบล็อกจะเข้าถึงได้เฉพาะในขอบเขตนั้น
const score = 85;
if (score >= 50) {
const result = 'ผ่าน';
let bonus = 10;
console.log(result);
console.log(bonus);
}
console.log(score);
// result และ bonus ไม่สามารถเรียกจากตรงนี้ได้
ตัวแปร `result` และ `bonus` อยู่ภายในบล็อก `if` ส่วน `score` ประกาศอยู่นอกบล็อกจึงยังอ่านได้หลังจบคำสั่ง `if` หากต้องการใช้ผลลัพธ์จากบล็อกภายนอก ควรออกแบบให้ส่งค่ากลับหรือกำหนดตัวแปรในขอบเขตที่เหมาะสม
แล้ว var แตกต่างอย่างไร?
`var` มี Function Scope แทน Block Scope จึงอาจเข้าถึงได้หลังออกจากบล็อก `if` หรือ `for` ภายในฟังก์ชันเดียวกัน แม้จะไม่ใช่รูปแบบที่แนะนำสำหรับโค้ดใหม่ทุกกรณี แต่การรู้ความแตกต่างนี้ช่วยให้เข้าใจโค้ด JavaScript รุ่นเก่าได้ดีขึ้น
if (true) {
var oldStyle = 'ประกาศด้วย var';
let modernStyle = 'ประกาศด้วย let';
console.log(oldStyle);
console.log(modernStyle);
}
console.log(oldStyle);
// console.log(modernStyle); จะเกิด ReferenceError
| คำประกาศ | ขอบเขตหลัก | ใช้กับโค้ดใหม่ |
|---|---|---|
| var | Function Scope | ควรระมัดระวังเรื่องขอบเขตและการประกาศซ้ำ |
| let | Block Scope | เหมาะกับค่าที่ต้องเปลี่ยนแปลง |
| const | Block Scope | เหมาะกับตัวแปรที่ไม่ต้องกำหนดค่าใหม่ |
5. Lexical Scope: ฟังก์ชันมองเห็นตัวแปรจากที่ใด?
JavaScript ใช้หลัก Lexical Scope ซึ่งพิจารณาขอบเขตจากตำแหน่งที่ประกาศฟังก์ชันในโค้ด ไม่ใช่จากตำแหน่งที่เรียกฟังก์ชันเพียงอย่างเดียว ฟังก์ชันภายในจึงสามารถเข้าถึงตัวแปรในขอบเขตภายนอกที่ครอบคลุมตำแหน่งประกาศของมันได้
const message = 'ข้อความระดับบนสุด';
function outer() {
const name = 'JavaScript';
function inner() {
console.log(message);
console.log(name);
}
inner();
}
outer();
ฟังก์ชัน `inner` ถูกประกาศอยู่ภายใน `outer` จึงเข้าถึงตัวแปร `name` ของ `outer` ได้ และยังเข้าถึง `message` จากขอบเขตที่อยู่ภายนอกขึ้นไปได้ด้วย หากฟังก์ชันไม่พบชื่อตัวแปรในขอบเขตปัจจุบัน JavaScript จะค้นหาต่อไปยังขอบเขตภายนอกตามลำดับ
6. Closure คืออะไร?
Closure เกิดขึ้นเมื่อฟังก์ชันยังคงเข้าถึงตัวแปรจาก Lexical Scope ที่เกี่ยวข้องได้ แม้ฟังก์ชันภายนอกที่สร้างมันขึ้นมาจะทำงานเสร็จแล้วก็ตาม กล่าวอีกแบบคือ ฟังก์ชันไม่ได้เก็บเพียงชุดคำสั่ง แต่ยังคงเชื่อมโยงกับสภาพแวดล้อมของตัวแปรที่มันต้องใช้งาน
function createGreeting(name) {
return function () {
return `สวัสดี ${name}`;
};
}
const greetMali = createGreeting('มะลิ');
const greetNiran = createGreeting('นิรันดร์');
console.log(greetMali());
console.log(greetNiran());
เมื่อเรียก `createGreeting('มะลิ')` ฟังก์ชันภายนอกจะคืนฟังก์ชันด้านในออกมา แม้การทำงานของ `createGreeting` จะจบแล้ว ฟังก์ชันที่คืนมายังคงเข้าถึงค่า `name` ที่สัมพันธ์กับการเรียกครั้งนั้นได้ การเรียกอีกครั้งด้วยชื่ออื่นจะมีสภาพแวดล้อมของตัวเอง
จุดที่ควรจำClosure ไม่ได้หมายความว่าตัวแปรทุกตัวจะกลายเป็น Global แต่หมายถึงฟังก์ชันยังสามารถเข้าถึงข้อมูลจากขอบเขต Lexical ที่มันปิดล้อมอยู่ได้
7. ใช้ Closure สร้างตัวนับที่เก็บสถานะ
ตัวอย่างที่เห็นประโยชน์ของ Closure ได้ชัดเจนคือการสร้างตัวนับ เราต้องการให้ตัวแปรเก็บจำนวนครั้งที่ถูกเรียก แต่ไม่ต้องการเปิดให้โค้ดส่วนอื่นแก้ไขตัวแปรนั้นโดยตรง
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA());
console.log(counterA());
console.log(counterB());
console.log(counterA());
ตัวแปร `count` ถูกประกาศภายใน `createCounter` แต่ฟังก์ชันด้านในยังเข้าถึงและปรับค่าได้ แต่ละการเรียก `createCounter()` จะสร้างสถานะของตัวนับแยกกัน ดังนั้น `counterA` และ `counterB` จึงไม่ใช้ค่าตัวนับร่วมกัน
8. Closure กับการซ่อนข้อมูล
Closure สามารถช่วยจำกัดวิธีเข้าถึงข้อมูลภายใน โดยเปิดให้ผู้เรียกใช้งานผ่านฟังก์ชันที่กำหนดไว้แทนการเข้าถึงตัวแปรโดยตรง แนวทางนี้มีประโยชน์เมื่อสร้างตัวนับ สถานะของโมดูล หรือฟังก์ชันที่ต้องเก็บค่าบางอย่างระหว่างการเรียกหลายครั้ง
function createWallet(initialBalance = 0) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) {
return false;
}
balance += amount;
return true;
},
getBalance() {
return balance;
}
};
}
const wallet = createWallet(100);
wallet.deposit(50);
console.log(wallet.getBalance());
console.log(wallet.balance);
ในตัวอย่างนี้ `balance` ไม่ใช่พร็อพเพอร์ตีที่เปิดเผยบนออบเจ็กต์ `wallet` แต่เป็นตัวแปรที่ฟังก์ชัน `deposit` และ `getBalance` เข้าถึงผ่าน Closure ได้ การเรียก `wallet.getBalance()` จึงคืนยอดเงินปัจจุบัน ส่วน `wallet.balance` จะได้ `undefined` เพราะไม่มีพร็อพเพอร์ตีชื่อนี้อยู่บนออบเจ็กต์
Closure ไม่ใช่ระบบรักษาความปลอดภัยครบวงจรการซ่อนตัวแปรด้วย Closure ช่วยจัดการการเข้าถึงข้อมูลในระดับโค้ด แต่ไม่ได้ทดแทนการตรวจสอบสิทธิ์ การตรวจสอบข้อมูล หรือมาตรการความปลอดภัยของระบบที่ต้องทำในส่วนอื่นด้วย
9. Closure ที่ใช้ใน Callback และ Event-driven Code
Callback คือฟังก์ชันที่ส่งไปให้โค้ดอีกส่วนเรียกใช้ภายหลัง Closure ช่วยให้ Callback สามารถอ้างถึงข้อมูลจากบริบทที่สร้างมันขึ้นมาได้ แม้การเรียกใช้งานจะเกิดขึ้นในภายหลัง เช่น การประมวลผลรายการข้อมูล การตอบสนองต่อเหตุการณ์ หรือการทำงานแบบไม่พร้อมกัน
function createMultiplier(multiplier) {
return function (number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const numbers = [2, 4, 6];
console.log(numbers.map(double));
console.log(numbers.map(triple));
ฟังก์ชัน `double` และ `triple` ต่างเก็บค่า `multiplier` ที่แตกต่างกันไว้ผ่าน Closure เมื่อนำไปใช้เป็น Callback ของ `map()` แต่ละฟังก์ชันจึงคูณตัวเลขด้วยค่าของตัวเองได้ โดยไม่ต้องส่งตัวคูณซ้ำทุกครั้งที่ประมวลผลแต่ละรายการ
10. เปรียบเทียบ Scope และ Closure
| แนวคิด | คำถามที่ตอบ | ตัวอย่างการใช้งาน |
|---|---|---|
| Global Scope | ตัวแปรระดับบนสุดเข้าถึงได้จากที่ใด? | ค่าตั้งต้นที่หลายฟังก์ชันอ่านร่วมกัน |
| Function Scope | ตัวแปรภายในฟังก์ชันเข้าถึงได้จากที่ใด? | ตัวแปรชั่วคราวระหว่างคำนวณ |
| Block Scope | ตัวแปรในบล็อกจำกัดอยู่ตรงไหน? | ตัวแปรภายใน if หรือ for |
| Lexical Scope | ฟังก์ชันมองเห็นตัวแปรตามตำแหน่งประกาศอย่างไร? | ฟังก์ชันภายในอ่านตัวแปรของฟังก์ชันภายนอก |
| Closure | เหตุใดฟังก์ชันจึงยังเข้าถึงตัวแปรเดิมภายหลังได้? | ตัวนับหรือฟังก์ชันที่จดจำค่ากำหนดไว้ |
11. ข้อผิดพลาดที่พบบ่อย
เรียกตัวแปรนอกขอบเขต
หากประกาศตัวแปรด้วย `let` หรือ `const` ภายในฟังก์ชันหรือบล็อก แล้วพยายามอ่านจากภายนอก อาจเกิด `ReferenceError` ควรตรวจสอบตำแหน่งประกาศ หรือส่งค่าที่ต้องการออกมาผ่าน `return`
คิดว่า Closure คัดลอกค่าทุกอย่างในเวลาสร้าง
Closure เก็บการเข้าถึงสภาพแวดล้อมของตัวแปร ไม่ใช่การคัดลอกค่าทุกตัวเป็นภาพนิ่ง หากตัวแปรที่ถูกปิดล้อมเปลี่ยนแปลง ฟังก์ชันอาจเห็นค่าปัจจุบันเมื่อถูกเรียก
ใช้ตัวแปร Global มากเกินไป
การแชร์สถานะผ่านตัวแปร Global จำนวนมากทำให้ส่วนต่าง ๆ ของโปรแกรมเชื่อมโยงกันแน่นเกินไป ควรเลือกใช้พารามิเตอร์ ค่าที่ส่งกลับ หรือ Closure ตามความเหมาะสม
เก็บข้อมูลใน Closure โดยไม่คำนึงถึงอายุการใช้งาน
หากฟังก์ชันที่เก็บข้อมูลขนาดใหญ่ยังถูกอ้างอิงอยู่ ข้อมูลที่เกี่ยวข้องอาจยังไม่ถูกเก็บกวาดโดยระบบจัดการหน่วยความจำ ควรระวังการเก็บ Callback หรือสถานะที่ไม่จำเป็นไว้นานเกินไป
12. แบบฝึกหัดท้ายบท
ลองทำนายผลลัพธ์ก่อนรันโค้ด แล้วอธิบายเหตุผลโดยใช้แนวคิด Scope และ Closure การฝึกอธิบายว่าแต่ละตัวแปรอยู่ในขอบเขตใดจะช่วยให้เข้าใจมากกว่าการจำคำตอบเพียงอย่างเดียว
โจทย์ที่ 1: ตัวแปรนี้เข้าถึงได้หรือไม่?
สร้างฟังก์ชัน `showScore()` ที่ประกาศตัวแปร `score` ไว้ภายใน แล้วทดลองอ่านค่าจากภายนอก อธิบายว่าเหตุใดการอ่านจากภายนอกจึงเกิดข้อผิดพลาด และแก้ไขโดยใช้ `return`
function showScore() {
const score = 95;
return score;
}
const result = showScore();
console.log(result);
โจทย์ที่ 2: สร้างตัวนับเพิ่มทีละหนึ่ง
สร้างฟังก์ชัน `createCounter()` ที่คืนฟังก์ชันด้านใน เมื่อเรียกฟังก์ชันด้านในแต่ละครั้งให้เพิ่มค่าตัวนับหนึ่งหน่วยและส่งค่าปัจจุบันกลับมา ทดสอบโดยสร้างตัวนับสองชุดและตรวจสอบว่าสถานะแยกจากกัน
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const first = createCounter();
const second = createCounter();
console.log(first());
console.log(first());
console.log(second());
console.log(first());
โจทย์ที่ 3: สร้างฟังก์ชันคูณด้วยค่าที่กำหนด
สร้าง `createMultiplier(factor)` ให้คืนฟังก์ชันที่รับตัวเลขหนึ่งตัวแล้วคูณด้วย `factor` จากนั้นสร้างฟังก์ชันคูณสองและคูณห้า แล้วทดลองใช้กับ Array ของตัวเลข
function createMultiplier(factor) {
return number => number * factor;
}
const timesTwo = createMultiplier(2);
const timesFive = createMultiplier(5);
const values = [1, 2, 3, 4];
console.log(values.map(timesTwo));
console.log(values.map(timesFive));
13. สรุปบทเรียน
Scope กำหนดขอบเขตการเข้าถึงตัวแปร โดย `let` และ `const` มี Block Scope ส่วน `var` มี Function Scope หลัก Lexical Scope ทำให้ฟังก์ชันเข้าถึงตัวแปรตามตำแหน่งที่ประกาศ และ Closure ทำให้ฟังก์ชันยังคงเข้าถึงข้อมูลจากขอบเขตที่เกี่ยวข้องได้เมื่อนำไปเรียกใช้ภายหลัง
เมื่อเข้าใจว่าตัวแปรอยู่ที่ไหนและฟังก์ชันจดจำอะไรได้ เราจะออกแบบโปรแกรมที่มีสถานะชัดเจนและคาดเดาพฤติกรรมได้ง่ายขึ้น
ตอนถัดไป: Array Methods ขั้นสูงตอนต่อไปจะต่อยอดความรู้เรื่องฟังก์ชันและ Callback ด้วยเมธอดของ Array เช่น map, filter, reduce, find และ some เพื่อจัดการชุดข้อมูลได้อย่างกระชับ อ่านง่าย และเป็นระบบ
คำแนะนำก่อนจบบท: อย่าหยุดเพียงแค่รันโค้ดให้ได้ผลลัพธ์ ลองอธิบายด้วยว่าตัวแปรแต่ละตัวอยู่ใน Scope ใด ฟังก์ชันเข้าถึงข้อมูลจากตำแหน่งไหน และเหตุใด Closure จึงยังใช้ข้อมูลเดิมได้ การตอบคำถามเหล่านี้ได้จะเป็นพื้นฐานสำคัญสำหรับ JavaScript ระดับต่อไป
อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง
⏳ กำลังจับเวลาอ่าน...