ทำความเข้าใจเรื่อง Closure ใน JavaScript – ตอนที่ 1: Closure คืออะไร?


ต้องออกตัวก่อนว่าผมเองไม่ใช่ผู้เชี่ยวชาญภาษา JavaScript แต่อย่างใด เคยเขียนบ้างนิดๆหน่อยๆ แต่ด้วยความที่ไวยากรณ์ (Syntax) ของมันคล้ายกับภาษา C, Java ที่ผมพอจะคุ้นเคยอยู่ เวลาผมเห็นโค้ด JavaScript ก็เลยไม่รู้สึกลำบากอะไร และสามารถทำความเข้าใจมันได้ระดับนึง
วันก่อนผมไปอ่านบทความของฝรั่งที่อธิบายเรื่อง Closure ใน JavaScript ซึ่งว่ากันว่าเป็นเรื่องที่เข้าใจยากเรื่องหนึ่งของภาษานี้ ผมเห็นว่าน่าสนใจดี ก็เลยอยากจะนำมาถ่ายทอดให้เข้าใจง่ายๆตามสไตล์ของผมบ้าง จึงเป็นที่มาของบทความนี้ครับ

รู้จักกับนิพจน์ฟังก์ชั่น (Function Expression)

ก่อนจะอธิบายเรื่อง Closure คุณต้องเข้าใจสิ่งที่เรียกว่า “นิพจน์ฟังก์ชั่น” (Function Expression) ใน JavaScript เสียก่อน
โดยทั่วไปเวลาเราต้องการสร้างฟังก์ชั่นไว้เรียกใช้งาน เราจะกำหนด (define) ฟังก์ชั่นในลักษณะนี้ใช่ไหมครับ
การกำหนดฟังก์ชั่นดังโค้ดข้างต้นเป็นรูปแบบที่เรียกว่า “การประกาศฟังก์ชั่น” (Function Declaration) ซึ่งเวลาเรียกใช้จะระบุชื่อฟังก์ชั่นแล้วตามด้วยวงเล็บเปิด-ปิด ดังนี้ (ถ้าฟังก์ชั่นมีพารามิเตอร์ก็ระบุค่าที่จะส่งให้ฟังก์ชั่นลงในวงเล็บ แต่ในที่นี้ฟังก์ชั่น sayHello ไม่มีพารามิเตอร์จึงใส่วงเล็บว่างๆ)
ทีนี้มาดูกรณีของนิพจน์ฟังก์ชั่นบ้าง นิพจน์ฟังก์ชั่นคือฟังก์ชั่นที่เรากำหนด (define) ขึ้นมาเป็นส่วนหนึ่งของนิพจน์ที่ใหญ่ขึ้นอีกที เช่น
หรือเขียนแยกบรรทัดแบบนี้ก็ได้เช่นกัน
จากโค้ด เราสร้างนิพจน์ฟังก์ชั่นชื่อ sayHello2 แล้วนำไปกำหนดให้ตัวแปร f กล่าวคือ ฟังก์ชั่น sayHello2 ทั้งอันจะเสมือนเป็นค่า 1 ค่าหรือออบเจ็ค 1 ออบเจ็คที่เราสามารถกำหนดให้ตัวแปรได้
หลังจากกำหนดฟังก์ชั่น sayHello2 ลงในตัวแปร f แล้ว เวลาเรียกใช้ฟังก์ชั่นก็จะเรียกผ่านตัวแปร f แบบนี้
ถ้าพูดให้ถูกต้อง จริงๆตัวแปร f ไม่ได้เก็บนิพจน์ฟังก์ชั่น sayHello2 ไว้ แต่มันคือตัวชี้หรือพอยเตอร์ (pointer) ที่ชี้/อ้างอิงไปยังนิพจน์ฟังก์ชั่น sayHello2 นั่นเอง
และเนื่องจากการเรียกใช้นิพจน์ฟังก์ชั่น เราไม่ได้เรียกจากชื่อของมัน แต่เรียกจากตัวแปรที่อ้างอิงมัน โดยทั่วไปเราจึงมักไม่ตั้งชื่อให้มัน
สังเกตนะครับว่านิพจน์ฟังก์ชั่นข้างต้นไม่มีชื่อแล้ว เพราะหลังคีย์เวิร์ด function เราใส่วงเล็บเปิด-ปิดทันที ฟังก์ชั่นนี้จึงเป็นฟังก์ชั่นที่ไม่มีชื่อ ภาษาอังกฤษเรียกว่า Anonymous Function แต่เรายังสามารถเรียกใช้มันผ่านตัวแปร f ได้ตามปกติเหมือน sayHello2 ก่อนหน้านี้
แถมเรื่องนิพจน์ฟังก์ชั่นอีกนิด (อันนี้ค่อนข้างนอกเรื่อง Closure ไปหน่อยนะครับ) ถ้าคุณต้องการสร้างนิพจน์ฟังก์ชั่นแล้วเรียกใช้มันตรงนั้นทันที แทนที่จะเก็บลงตัวแปรก่อนแล้วเรียกผ่านตัวแปร ก็ให้เอาวงเล็บมาคลุมนิพจน์ฟังก์ชั่นทั้งหมดไว้ แล้วตามด้วยวงเล็บเปิด-เปิด เช่น
โค้ดข้างต้นจะสร้างนิพจน์ฟังก์ชั่นที่ไม่มีชื่อ (anonymous) แล้วเรียกให้ทำงานทันที ดังนั้นเมื่อรันจะมีไดอะล็อกที่แสดงข้อความว่า “สวัสดี” ปรากฏขึ้นมา
ถ้าหากนิพจน์ฟังก์ชั่นมีพารามิเตอร์ การสร้างและเรียกใช้ทันทีจะเขียนได้ดังนี้
ก็จะปรากฏไดอะล็อกที่แสดงข้อความว่า “สวัสดี พร้อมเลิศ”

Closure คืออะไร?

เอาล่ะครับ คราวนี้ก็มาที่ประเด็นหลักที่ผมจะอธิบายในบทความนี้ นั่นคือเรื่อง Closure คุณอาจสงสัยว่ามันคืออะไร และอยากให้ผมอธิบายความหมายของมันเดี๋ยวนี้เลย
แต่ช้าก่อนครับ ถึงผมจะบอกความหมายตอนนี้ เชื่อเถอะว่าคุณก็อาจไม่เข้าใจอยู่ดี เรื่อง Closure นั้นคุณต้องดูตัวอย่างโค้ดไปเรื่อยๆครับ จึงจะเข้าใจอย่างแท้จริงว่ามันคืออะไร
จุดเริ่มต้นของ Closure มาจากการที่ภาษา JavaScript อนุญาตให้เราสร้างฟังก์ชั่นซ้อนฟังก์ชั่นได้ ดังตัวอย่าง
จากโค้ด ฟังก์ชั่น inner ซ้อนอยู่ภายในฟังก์ชั่น outer มันจึงสามารถเข้าถึงตัวแปร x ซึ่งเป็นตัวแปรโลคอล (Local Variable) ของฟังก์ชั่น outer ได้ ในที่นี้ฟังก์ชั่น inner อ่านค่าของ x มาแสดงในไดอะล็อก
อย่างไรก็ตาม การสร้างฟังก์ชั่นซ้อนกันในลักษณะนี้ยังไม่ใช่ Closure (จริงๆต้องบอกว่ายังไม่เกิด Closure) แต่ Closure จะเกิดขึ้นเมื่อเราสร้างนิพจน์ฟังก์ชั่นซ้อนไว้ข้างในฟังก์ชั่นอื่น แล้วมีการ return นิพจน์ฟังก์ชั่นนั้นออกไป ลองดูตัวอย่างครับ
ในตัวอย่างนี้ เราสร้างนิพจน์ฟังก์ชั่นชื่อ inner ไว้ภายในฟังก์ชั่น outer (ความจริงนิพจน์ฟังก์ชั่นไม่ต้องมีชื่อก็ได้ แต่ผมตั้งชื่อก็เพื่อความสะดวกในการอธิบาย) โดยฟังก์ชั่น inner จะอ่านค่าของ x มาแสดงในไดอะล็อกเช่นเดียวกับตัวอย่างที่แล้ว
ภายในฟังก์ชั่น outer เรากำหนดฟังก์ชั่น inner ลงในตัวแปร f (กำหนดให้ตัวแปร f อ้างอิงไปยังฟังก์ชั่น inner) แล้ว return ค่าของตัวแปร f นี้ออกไป ดังนั้นการเรียกใช้ฟังก์ชั่น outer ในบรรทัดที่ 11 จึงได้ผลลัพธ์เป็นพอยเตอร์ที่อ้างอิงไปยังฟังก์ชั่น inner ซึ่งพอยเตอร์นี้ถูกเก็บลงตัวแปร g และทำให้ g() ในบรรทัดสุดท้ายเป็นการเรียกใช้ฟังก์ชั่น inner ที่อยู่ภายในฟังก์ชั่น outer นั่นเอง
คุณคงเดาได้ว่า ถ้ารันโค้ดข้างต้นจะปรากฏไดอะล็อกที่แสดงข้อความว่า “value of x is 123” ออกมา ใช่ครับ ได้ผลอย่างนั้นจริงๆ แต่คุณไม่สงสัยบ้างหรือว่า ตอนที่เราเรียกฟังก์ชั่น inner ผ่านตัวแปร g ในบรรทัดสุดท้ายนั้น ฟังก์ชั่น outer มันทำงานจบไปเรียบร้อยแล้ว แล้วตัวแปร x จะมีค่า 123 ได้อย่างไร เพราะ x เป็นตัวแปรโลคอลของฟังก์ชั่น outer ซึ่งควรจะต้องถูกทำลายไปเมื่อพ้นจากขอบเขต (Scope) ของ outer
ครับ สาเหตุที่ตัวแปร x ยังคงมีตัวตนอยู่ และยังเก็บค่า 123 ไว้ แม้ว่าฟังก์ชั่น outer จะจบการทำงานไปแล้ว ก็เพราะว่าตัวแปร x ถูกเก็บเอาไว้ใน Closure ครับ
ทีนี้ลองเพิ่มโค้ดอีกนิดหน่อย
จากโค้ด ทุกครั้งที่เรียกฟังก์ชั่น inner เราจะเพิ่มค่าของ x ขึ้น 1 ดังนั้นในตอนท้ายที่เราเรียก g() 3 ครั้งก็จะทำให้ปรากฏไดอะล็อก 3 ครั้งที่แสดงค่าของ x เป็น 123, 124 และ 125 ตามลำดับ สิ่งนี้แสดงให้เห็นว่า Closure ไม่ได้เก็บแค่ “สำเนา” ของค่า x ไว้ แต่มันเก็บตัวแปร x ที่เราสามารถเปลี่ยนแปลงแก้ไขค่าของมันได้จากภายนอกฟังก์ชั่น outer เสมือนว่าเรากำลังทำงานอยู่ภายในฟังก์ชั่น outer เลย และนอกจากนี้การเรียก g() ทั้ง 3 ครั้งก็เป็นการเข้าถึงตัวแปร x ใน Closure เดียวกันด้วย (แต่หากเรียกฟังก์ชั่น outer อีกครั้งจะทำให้มีการสร้าง Closure ขึ้นมาใหม่อีกชุดหนึ่ง)
เท่าที่อธิบายมาถึงตรงนี้ คุณคงเข้าใจแล้วว่า Closure คืออะไร แต่ยังไม่จบนะครับ ผมยังมีตัวอย่างเรื่อง Closure ที่น่าสนใจ (และน่าปวดหัว) อีกหลายตัวอย่างเลย รวมทั้งประโยชน์ของการใช้ Closure ด้วย แต่สำหรับบทความนี้คงเอาไว้แค่นี้ก่อน ถ้ามีคำแนะนำหรือข้อสงสัยใดๆก็ทิ้งคอมเมนต์ไว้ได้เลยครับ ^_^

http://promlert.com/2013/03/javascript_closure/
Previous
Next Post »