Kodokon kodokon.com

Closure และขอบเขตแบบเล็กซิคัล

เชี่ยวชาญวิธีที่ closure จับตัวแปรเอาไว้ เพื่อให้คุณห่อหุ้มสถานะและหลีกเลี่ยงกับดักเรื่องขอบเขตได้

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

closure ไม่ใช่ลูกเล่นแปลกประหลาดอะไร มันคือกลไกที่ทำให้ฟังก์ชันยังคงเข้าถึงขอบเขตแบบเล็กซิคัลที่มันถูก นิยาม ไว้ได้ ไม่ใช่ที่ที่มันถูกเรียกใช้ เอนจินไม่ได้คัดลอกค่า แต่เก็บ การอ้างอิงที่มีชีวิต ไปยังสภาพแวดล้อมเล็กซิคัล ดังนั้นการเรียกโรงงาน (factory) เดียวกันสองครั้งจึงสร้างสภาพแวดล้อมที่แยกจากกันสองชุด ซึ่งทำให้ closure เป็นเครื่องมือห่อหุ้มที่เบาที่สุดในภาษานี้

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
สถานะส่วนตัว: count เข้าถึงได้ผ่านเมทอดที่คืนกลับมาเท่านั้น

กับดักคลาสสิกในโลกจริง: var ผูกอยู่กับฟังก์ชันที่ครอบมันไว้ ไม่ใช่กับบล็อก ภายในลูป closure ทุกตัวจึงจับตัวแปร ตัวเดียวกัน ไว้ ด้วย let การวนแต่ละรอบจะสร้างสภาพแวดล้อมเล็กซิคัลใหม่ ดังนั้นการจับแต่ละครั้งจึงแยกจากกัน เพิ่มเข้าไปด้วย temporal dead zone (TDZ): ตัวแปร let หรือ const มีอยู่ทันทีที่เข้าสู่บล็อก แต่จะโยน ReferenceError หากคุณอ่านมันก่อนการประกาศ

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
var ใช้การผูกร่วมกันเพียงตัวเดียว ส่วน let สร้างตัวใหม่ในแต่ละรอบ

ในแง่ของแพทเทิร์น closure คือรากฐานของ memoization และ module pattern เมื่อเทียบกับคลาส closure ให้ความเป็นส่วนตัวอย่างแท้จริงโดยไม่ต้องมี this หรือโปรโตไทป์ แต่แลกมาด้วยการใช้หน่วยความจำที่สูงขึ้นเล็กน้อยเมื่อคุณสร้างอินสแตนซ์นับพัน: แต่ละอินสแตนซ์พกฟังก์ชันของตัวเองมาด้วย

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
แคชอยู่ภายใน closure มองไม่เห็นจากภายนอก

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ในลูป for (var i = ...) คอลแบ็ก setTimeout สามตัวที่สร้างภายในตัวลูปจะแสดงผลอะไร?
    • 0, 1, 2 เพราะแต่ละคอลแบ็กจับค่าของตัวเองไว้
    • 3, 3, 3 เพราะทั้งหมดใช้การผูกของ i ตัวเดียวกันร่วมกัน
    • undefined สามครั้ง เพราะ i ถูกทำลายหลังจบลูป
  2. closure เก็บอะไรจากสภาพแวดล้อมที่มันถูกนิยามไว้กันแน่?
    • สำเนาค่าที่ถูกแช่แข็ง ณ เวลาที่สร้าง
    • การอ้างอิงที่มีชีวิตไปยังตัวแปรที่ถูกจับไว้
    • เฉพาะพารามิเตอร์ของฟังก์ชันที่ครอบมันไว้เท่านั้น
    • ไม่มีอะไร: ขอบเขตถูกทำลายเมื่อฟังก์ชันคืนค่ากลับ
  3. ทำไม createCounter ถึงรับประกันว่า count ยังคงเป็นส่วนตัว?
    • เพราะ let ทำให้ตัวแปรเปลี่ยนแปลงไม่ได้
    • เพราะ count เข้าถึงได้ผ่านฟังก์ชันที่คืนกลับมาเท่านั้น ซึ่งเป็นฟังก์ชันเดียวที่จับขอบเขตของมันไว้
    • เพราะเอนจินลบ count ทิ้งหลังจากโรงงานคืนค่ากลับ