Guest Adventurer 🥉 Lv.1
🪙 50
🔷 10
← Back to library
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
คำประกาศขอบเขตหลักใช้กับโค้ดใหม่
varFunction Scopeควรระมัดระวังเรื่องขอบเขตและการประกาศซ้ำ
letBlock Scopeเหมาะกับค่าที่ต้องเปลี่ยนแปลง
constBlock 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 ระดับต่อไป
Reach within 500px of the page end and stay for at least 5 minutes to earn EXP once per day.

⏳ Reading timer is running...

🌐
Select Language
21 World Languages Supported