JavaScript #04: ในมิติ Object-based Programming

เกริ่นนำ

ถ้าถาม ECMAScript ว่า “ท่านเจริญรอยตามกระบวนทัศน์เป็นทางหลัก”  ถ้าท่านคาดหวังว่า ECMAScript จะตอบว่า “functional programming” เป็นคำตอบที่ไม่ถูกต้องนะครับ ตอบว่า “imperative” หรือ “object-oriented” ก็ล้วนไม่ใช่ทั้งสิ้น ถ้าเป็น ECMAScript จะตอบท่านว่า “object-based” ครับ

ท่านอาจแย้งว่าที่ท่านศึกษามาไม่เห็นเหมือนที่ผมบอกเลย ก็อย่าไปจริงจังมากครับ ผมแค่อ้างตามมาตรฐานครับ

ณ วันที่ผมเขียนบทความนี้ ก็ปลายๆ เดือนพฤษภาคม 2015 สถานะล่าสุดของ ES6 ก็ผ่าน final draft ไปแล้วตั้งแต่ช่วงสงกรานต์ที่ผ่านมา ในเดือนหน้า ทีม TC39 จะประชุมกันเป็นครั้งสุดท้ายเพื่อประกาศมาตรฐาน ES6 อย่างเป็นทางการ เชื่อว่าคงไม่มีปัญหาอะไร เวลาที่ท่านอ่านบทความนี้เชื่อว่า ES6 ก็คงประกาศใช้งานเรียบร้อยแล้ว

ผมลองอ่านมาตรฐานคร่าวๆ ดู พบว่า ทางทีม TC39 ระบุว่า ECMAScript นั้นเป็น object-oriented ในเชิงของการทำงานภายในของ engine แต่ถ้าในเชิงภาษาแล้ว TC39 ชี้ชัดว่า ECMAScript เป็นภาษาแบบ object-based ครับ การนิยามนี้มีมาตั้งแต่มาตรฐานรุ่นเก่าๆ และในรุ่นล่าสุดคือรุ่น 6 นี่ก็ยังคงนิยามนี้อยู่ครับ

ในเมื่อ ECMAScript เป็นภาษาที่ใช้กระบวนทัศน์ object-based เป็นหลัก ผมจึงขออุทิศบทความหนึ่งบทเต็มๆ ให้แก่ object-based ครับ

พื้นฐาน Object

object ใน ECMAScript นั้นเป็นประชากรเต็มขั้น (first-class) เหมือนกับฟังก์ชัน ดังนั้นจึงสามารถกำหนดเป็นตัวแปรได้ และเมื่อกำหนดเป็นตัวแปร ก็สามารถส่งเป็นพารามิเตอร์และส่งกลับออกมาจากฟังก์ชันได้

พิจารณาการสร้าง object

เราเรียนรู้ไปแล้วว่าการสร้าง object นั้นมี 2 วิธีคือ

สิ่งที่อยากให้สังเกตก็คือ ในบรรทัดแรกนั้นถ้าเป็นภาษา OO ทั่วไป จะสร้าง object ขึ้นมาตัวหนึ่งที่ค่อนข้างไร้สาระมาก ทำอะไรก็ไม่ได้ เพราะ ไม่มี methods/properties อะไร ไม่สามารถเพิ่มได้อีก ถ้าอยากได้ methods/properties อะไร ก็ต้องสร้างเป็นพิมพ์เขียวเอาไว้ โดยใช้คำสั่ง class จากนั้นก็ใช้ class นั้นปั๊ม objects ขึ้นมาอีกที

แต่ ECMAScript นั้นไม่มีแนวคิดของ class (ES6 มีแล้ว) เราสามารถสร้าง objects เปล่าๆ ขึ้นมาก่อนโดยใช้คำสั่งใดคำสั่งหนึ่งข้างต้น จากนั้นค่อยเติม properties/methods เข้าไป เช่น

สะดวกดีไหมครับ อยากเพิ่มอะไรก็เพิ่ม แต่เชื่อว่าคนส่วนใหญ่ที่อ่านบทความนี้คงไม่เห็นด้วยกับผมเป็นแน่ ถ้าปล่อยให้สร้างอิสระแล้วจะคุมมาตรฐานได้อย่างไร แล้วใน libraries ที่มี object สำเร็จรูปจะทำไม่ได้เลยเนื่องจากไม่มีแนวคิดของ class

ทำใจร่มๆ ครับ มันไม่ได้แย่อย่างที่ท่านคิดครับ ลองดูตัวอย่างนี้ดูครับ

Person เป็นฟังก์ชันที่ส่งค่ากลับออกมาเป็น object หรือพูดอีกอย่างได้ว่าฟังก์ชัน Person นั้นเป็น constructor สำหรับสร้าง object นั่นเอง เมื่อสร้าง object ผ่านฟังก์ชัน ก็ไม่ต้องกลัวว่า object ที่ได้นั้นจะหน้าตาไม่เหมือนกัน ฟังก์ชันจะบังคับให้ object ที่สร้างนั้นมี properties/methods ที่มีมาตรฐานเหมือนกัน แต่อาจจะยืดหยุ่นกว่าการใช้คำสั่ง class เสียด้วยซ้ำไป เพราะถ้าเราใช้คำสั่งเงื่อนไข เราอาจจะสร้าง object ที่มีหน้าตาต่างออกไปที่เรายังสามารถควบคุมได้

ถ้าไม่พิจารณาในส่วนฟังก์ชัน ดูโค้ดเฉพาะในส่วนการใช้งาน ท่านที่คุ้นเคยกับ OO ในภาษาอื่น จะคุ้นเคยทันทีว่า Person นั้นเหมือนกับ class เลย เพียงแต่ไม่มีคำสั่ง new เท่านั้น  จริงๆ แล้วจะใส่ new ก็ได้นะครับ ลองดู เปลี่ยนบรรทัดที่ 10 เป็น

ก็ทำงานได้เหมือนกัน คราวนี้เหมือนกับภาษา OO อื่นแล้วใช่ไหมครับ จะเห็นได้ว่า เราใช้ฟังก์ชันแทน class นั่นเอง

ส่วนเรื่อง new หรือ ไม่ new นั้นมีความแตกต่างกันเล็กน้อย ต่างกันอย่างไร ผมขอติดท่านไว้ก่อน เอาไว้กลับมาคุยกันตอนกลางๆ บทนี้ครับ

ธรรมเนียมปฏิบัติ: ฟังก์ชันที่ตัวอักษรตัวแรกเป็นตัวเล็กเป็นฟังก์ชันปกติ แต่ถ้าเป็นอักษรตัวใหญ่จะทำหน้าที่เป็น constructor

ธาตุแท้ของ methods

ท่านลองสังเกตการสร้าง method ให้ดีๆ นะครับ จากตัวอย่างข้างบนมี method hello()  ที่สร้างโดยวิธี function expression แบบ on-the-fly เป็นการสร้าง anonymous ฟังก์ชันนั่นเอง จุดที่ผมอยากให้ฉุกคิดก็คือ hello จริงๆ แล้วไม่ใช่ method/function นะครับ มันเป็นตัวแปรธรรมดา ซึ่งก็คือ property นั่นเอง เพียงแค่มันเก็บตัวชี้ไปยังฟังก์ชันเท่านั้น และฟังก์ชันเป็นแบบเต็มขั้นหรือ first-class มันจึงกำหนดให้เป็นตัวแปรได้ ว่ากันไปแล้วใน object มีแค่ properties นะครับ ไม่ได้มี methods แต่อย่างใด

นอกจากค่าตัวแปรที่เป็น primitive และฟังก์ชันแล้ว property ยังสามารถชี้ไปยัง object อื่นๆ ได้ ทำให้กลายเป็น object ซ้อน object จะซ้อนกี่ชั้นก็ได้ นั่นคือจุดเด่นของ ES แต่เพื่อความคุ้นเคย ผมเลยเขียนแยกกันระหว่าง properties/methods แต่ขอให้ตระหนักให้ชัดเจนว่าใน object มีได้เพียง properties นะครับ (ส่วน properties จะไปชี้อะไรก็สุดแล้วแต่)

constructor แบบคาย Object ตรงๆ

วิธีนี้ก็ยอดนิยม ตรงไปตรงมา เมื่อ constructor เป็นฟังก์ชันที่คาย Object ออกมา ก็คายกันแบบทื่อๆ แบบนี้ สั้นๆ ชัดเจน

ปริศนาตัวแปร this

เราคงคุ้นเคยตัวแปร this กันเป็นอย่างดีอยู่แล้ว ใครมาทางสาย OO คงรู้ดีว่า ถ้า method ต้องการเข้าถึง property ใด ก็ให้อ้างผ่าน this ถ้าเป็นภาษาอื่นบางทีอาจเป็น me เป็น self ก็ได้  แต่ this ของ ECMAScript มันมีอะไรลึกซึ้งกว่านั้น เพื่อให้เข้าใจการทำงานของ this จริงๆ ผมของอธิบายด้วย gotcha! ดีกว่าครับ

Gotcha: this

มาเล่น gotcha! กันหน่อยดีกว่า ลับสมองดี

ถามว่ารันผ่านไหมครับ ถ้าผ่าน ได้คำตอบอะไรครับ ใครตอบถูกบ้างครับ

ใครคิดว่ารันไม่ผ่านติดบรรทัดที่ 2 ยกมือขึ้นครับ ผิดนะครับ โปรแกรมนี้รันผ่าน แต่ได้ผลลัพธ์เป็น NaN  gotcha!

ท่านลองเปลี่ยนคำว่า this เป็น x แล้วลองรันดูใหม่ครับ ผลลัพธ์ที่ได้ต่างกัน มันจะบอกว่า ReferenceError: x is not defined  นั่นหมายความว่าตัวแปร this นั้นมันนิยามแล้ว และมันก็มีค่าเป็น undefined นั่นเอง

จินตนาการ this

การทำงานของ this นั้น ผมอยากชวนให้ท่านคิดว่า การนิยามฟังก์ชันใดๆ นั้น ที่แท้จริงการทำงานข้างในมันมีตัวแปร this เป็นพารามิเตอร์แถมข้างหน้าดังนี้ครับ

ไม่ว่าฟังก์ชันใดๆ รวมทั้ง methods ด้วย จะมีพารามิเตอร์แถมอยู่ตัวหนึ่งเป็นตัวแรก ชื่อว่า this นี้เสมอ ดังนั้นเราจึงไม่อาจตั้งชื่อพารามิเตอร์ตัวใดก็ตามว่า this เพราะมันจะชนกับที่มันแอบแถมมาให้

คราวนี้เรามาดูการเรียกใช้บ้าง เมื่อเราเรียกใช้

ยังจำ method call() ในบทความที่แล้วได้ไหมครับ เวลาใช้งานมันจะแอบแปลงเป็น

ดังนั้นจากตัวอย่างข้างบน ระบบจะส่ง undefined เข้าสู่ this และเมื่อ undefined ไปบวกกับตัวเลขก็จะได้ NaN  นั่นเอง

คราวนี้มาถึงของสนุกแล้ว จากตัวอย่างข้างต้น เราจะเปลี่ยนการเรียกใช้ sum(2, 4) มาใช้ call()  ดังนี้

ถ้าเราเรียกใช้ call() เอง ก็จะไม่มีการแปลงใดๆ อีกแล้ว   ลองรันโปรแกรมข้างบนดู ตามคาดได้ 7  หวังว่าคงไม่งงนะครับ ถ้างงขอให้อ่านอีกรอบ ตรงนี้มันเป็นฐานจริงๆ ถ้าไม่เข้าใจก็จะไปต่อไม่ได้

คราวนี้มาดู method กันบ้าง จาก

ข้างบนนี้ก็เป็น การสร้างและเรียกใช้ object ธรรมดา แต่ที่สำคัญก็คือบรรทัดสุดท้าย ก่อนรันมันจะต้องแปลงเป็น call() มันแปลงเป็นดังนี้

คือพูดง่ายๆ มันจะโยนเอา object ที่เราใช้เรียกเป็นทางเข้าซ้ายมือนั่นแหละให้เป็นพารามิเตอร์ตัวแรกแทน  มันจีงไม่ใช่ undefined แล้วนะครับ obj1 จึงถูกส่งไปยัง method hello() และแน่นอนพารามิเตอร์ตัวแรกของฟังก์ชัน hello() คือ this  ดังนั้น this จึงหมายถึง obj1  เวลาเรียกใช้ this เมื่อใด ก็คือ obj1 นั่นเอง   แต่ถ้าเราเรียกใช้ hello() ผ่าน obj2 มันก็จะดีดเอา obj2 เป็น this การทำงานมันเป็นเช่นนี้เอง

แนวคิดข้างต้นนี้ไม่ได้เป็นเอกสิทธิ์เฉพาะ ECMAScript นะครับ ภาษา script หลายๆ ตัวก็ใช้หลักการนี้เช่น Python เป็นต้น

กลยุทธ์: ลักขื่อเปลี่ยนเสา

เพื่อเข้าใจกลยุทธ์นี้ ผมขอสมมุติตัวละครสองตัวก่อนครับ ขอตั้งชื่อว่าสมชายและมิยาบิ สมชายเป็นคนไทย มิยาบิเป็นคนญี่ปุ่น ผมเอาข้อมูลนี้มาเขียนเป็นโปรแกรมดังนี้

โค้ดนี้เป็นการสร้าง object แบบ on-the-fly หรือเรียกว่าแบบ literal สร้างเสร็จในคำสั่งเดียว ซึ่งผมเคยอธิบายให้ฟังไปแล้วในบทความก่อนหน้า ไม่มีอะไรเข้าใจยาก ลองไปทบทวนดูครับ

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

สมชายก็สมหวังไปตามระเบียบ นั่นคือส่วนของกริยา แต่เมื่อเปลี่ยนสัญชาติได้แล้ว ต่อมาก็มีส่วนของกรรม ซึ่งผมจะละเอาไว้ไม่ขอกล่าวถึง ท่านไปคิดต่อเอาเอง

จากตัวอย่างนี้ ใครคิดลึกหน่อย คงอุทานว่ามันบ้าไปแล้ว method ตัวหนึ่งแทนที่จะรับใช้ object ของตัวเอง แต่หากแอบไปรับใช้ object ตัวอื่น ซึ่งอาจจะเป็นคนละ class ด้วยซ้ำไป ECMAScript จัดให้ได้ครับ ตราบได้ที่ฟังก์ชันหรือ method เมื่อเรียกใช้ของจาก object และ object ที่ส่งไปนั้นมีของให้มันเรียกใช้

ถ้าอย่างนั้น ยกเอา method ออกมานอก object ได้ไหม ได้แน่นอนครับ เราก็ลักขื่อเปลี่ยนเสาเสียเลยดังนี้ครับ

ผมดึงเอา show() จาก method ออกมาให้กลายเป็นฟังก์ชันธรรมดา พอผมใช้ .call() ก็สามารถส่ง object ให้กลายเป็น this ได้ ตัดต่อกันสนุกเลยครับคราวนี้ บางท่านอาจบอกว่า ถ้าในเชิง OO เราสามารถใช้ inheritance จัดการปัญหานี้ได้ ซึ่งมันก็จริง แต่ inheritance นั้น แต่ละ class ย่อย ก็ต้องมาจากรากที่เป็นแม่เดียวกันจึงจะถ่ายทอดได้ การเขียนลักษณะข้างบนนี้ ไม่ได้จำกัดครับว่าจะเป็น object มาจาก class ใด ใช้งานได้หมด ขอให้ใช้ชื่อตัวแปรตัวเดียวกันก็พอ และยิ่งไปกว่านั้น ยังรองรับการใช้งานนอก object ด้วย ถ้าใน show() เรามีการตรวจสอบว่า this เป็น undefined แสดงว่าเรียกมาโดยตรงเป็นฟังก์ชัน ไม่ได้ผ่าน object ซึ่งเราก็สามารถเขียนโปรแกรมจัดการให้เหมาะสมได้

กลยุทธ์ต้นไม้ผลิดอก

ในบางครั้ง เวลาที่เราสร้าง object เราอาจต้องการกำหนดค่าเริ่มต้นให้แก่ property ถ้าใครมาทางสาย OO ก็จะนึกถึง constructor ของ class ขึ้นมาทันที แต่นี่ไม่ใช้ class เราจะสร้าง constructor อย่างไร  เรามาลองดูกลยุทธ์นี้ครับ ทางฝรั่งเขาแยกออกเป็น 2 วิธีดังนี้

init property

วิธีนี้ใช้หลักการของ IFFE ในระดับ property นั่นคือ property จะถูก init ในขั้นตอนการสร้าง object เลย ผู้ใช้ไม่ได้รับรู้เลยว่ามีการแอบแทรก IFFE แต่อย่างใด นับได้ว่าเป็นข้อดี แต่วิธีนี้ก็มีข้อเสียสองประการก็คือ ประการแรก การส่งพารามิเตอร์เข้าไปใน IFFE นั้น จะอยู่ภายใน object ซึ่งการใช้งานอาจจะไม่ยืดหยุ่น และที่สำคัญคือ ถ้าเรามี property หลายๆ ตัว โค้ดนี้จะค่อนข้างรก

init method

วิธีนี้เห็นได้ชัดว่าเราสามารถส่งพารามิเตอร์ได้ โค้ดก็อ่านเข้าใจง่ายกว่า แต่ข้อเสียก็คือเราต้องมาเรียก .init() ในตอนแรก ลืมไม่ได้ ถ้ามิฉะนั้นจะทำงานไม่ถูก ว่ากันไปแล้ว ทั้งสองวิธีนี้สู้การใช้ constructor function ไม่ได้ แต่ก็มีคนเขียนแบบนี้อยู่ไม่น้อย ก็รู้ไว้ใช่ว่าครับ ถ้าเจอจะได้เข้าใจ ไม่งง

Typing

แนวคิด OO นั้นพัฒนาต่อยอดมาจากแนวคิด abstract data type (ADT) อธิบายความง่ายๆ ก็คือเราสามารถสร้างหน่วยข้อมูลขึ้นมาใหม่ แล้วมองมันเหมือนกับชนิดตัวแปรชนิดใหม่ชนิดหนึ่ง ซึ่งในเชิง OO ก็คือการสร้าง class แล้วการใช้งาน ตัวแปรชนิดของ class นั้น ก็จะได้อารมณ์ความรู้สึกในการใช้งานคล้ายกับสร้างตัวแปรชนิด primitive เช่นตัวเลขนั่นเอง

เพื่อความยืดหยุ่น เมื่อเราเขียนฟังก์ชันขึ้นฟังก์ชันหนึ่ง จะดีไม่น้อยถ้าจะสามารถรองรับชนิดตัวแปรที่หลากหลายดังเช่นตัวอย่างข้างบนเป็นต้น ปัญหาก็คือเราจะแน่ใจได้อย่างไร ว่า object นั้นมี methods/properties ที่เราต้องการหรือไม่

ถ้าเป็น OO ปกติ มันจะมีกลไก polymorphism เพื่อรับประกันว่า ถ้าเราเรียกใช้ methods/properties ของ class แม่แล้ว ลูกทุกตัวจะต้องมี แต่ก็มีอีกวิธีหนึ่งครับก็คือการตรวจสอบชนิดตัวแปรก่อนทำงาน ถ้ามีหรือถูกต้องเราจึงสั่งทำงาน ซึ่งการตรวจสอบมีสองระดับ คือคือระดับ ตัวแปรและระดับ object

การตรวจสอบการมีอยู่ของ properties/methods

ปกติแล้วแล้วจะใช้ in ดังนี้

การตรวจสอบระดับ object

ในภาษา OO ทั่วไป อย่างที่กล่าวไปแล้วว่ามันแนวคิดของเรื่อง ADT จึงสามารถตรวจสอบชนิดตัวแปรได้โดยใช้ is หรือ instanceof เป็นต้น ใน ECMAScript เองก็มี instanceof ให้ใช้ ว่าแต่ว่ามันเอาชนิดตัวแปรจากไหนมาให้ตรวจสอบกันนะ ไขข้อข้องใจกันง่ายๆ ดูโค้ดเลยดีกว่า

อย่างที่ผมบอกนะครับ ใน ECMAScript เรามองฟังก์ชันที่ขึ้นด้วยตัวอักษรตัวใหญ่ว่าเป็น constructor function มีหน้าที่คาย object ออกมา ทำตัวเป็น class เลย แต่ในที่นี้ผมสร้างมันแบบง่ายๆ ก่อน ไม่มีเนื้อ ไม่มีการคาย object แต่อย่างใด

ในเมื่อเรามอง Person เป็น class ในบรรทัดที่ 7 ผมสามารถใช้ instanceof ในการตรวจสอบชนิดตัวแปร โดยที่ทางด้านขวาของ instanceof จะเป็นชื่อของฟังก์ชัน (เรามองเป็น class ก็ได้) ส่วนด้านซ้ายก็เป็น object ในที่นี้คือ obj1 นั่นเอง

การทำงานภายในจริงๆ ของ instanceof เป็นดังบรรทัดที่ 8 ครับ จากบรรทัดที่ 7 จะถูกแปลงเป็นบรรทัดที่ 8 คือการเปรียบเทียบโดยใช้ prototype นั่นเอง  ในส่วนของ object ก็จะใช้ property ชื่อว่า __proto__ ส่วน ในฟังก์ชันจะชื่อว่า prototype  ดังนั้นในบรรทัดที่ 6 ผมทำให้ obj1.__proto__ ไปชี้ที่เดียวกันกับ Person.prototype ดังนั้นในสองบรรทัดล่างสุดจึงเป็น true

น่าปวดหัวใช่ไหมครับ แต่ถ้าเรายัดใส่เข้าไปข้างในฟังก์ชันจะเข้าใจได้ง่ายขึ้นและเป็นธรรมชาติกว่ามากครับ

จากตัวอย่างผมสร้างให้ฟังก์ชัน Person ทำหน้าที่เป็น constructor function  มีการคาย object ออกมาจริง ภายในผมสร้าง that เป็น object เปล่าขึ้นมา แล้วใส่ properties งานเข้าไป และหัวใจเลยครับคือบรรทัดที่ 3 มีการกำหนด that.__proto__ ให้เท่ากับ Person.prototype ซึ่ง Person.prototype ถ้าเราไม่ได้กำหนดอะไรมันก็เป็น object เปล่าหรือ {} นั่นเอง  แต่ไม่ว่ามันเป็นอะไรก็ตาม ทั้ง that.__proto__ และ Person.prototype จะชี้ที่เดียวกัน ดังนั้น เมื่อเวลาเปรียบเทียบ จึงได้เป็น true ทั้งคู่

มีข้อสังเกตว่า ใน method1() ผมเรียกใช้ this.name ทำไมถึงไม่เป็น that.name ฝากเอาไปคิดหน่อยครับ ถ้าคิดไม่ออกยังไง ก็ย้อนกลับไปอ่านบทความนี้ตั้งแต่ต้นนะครับ

new

การเขียน constructor function แบบข้างบนดูแล้วมันจะยืดยาวไปหน่อย ดังนั้น ECMAScript จึงทำให้มันสั้นลงแถมยังได้อารมณ์เหมือนภาษา OO อื่น คือการใช้ new นั่นเอง ลองดูตัวอย่างเดียวกันแต่มาใช้ new นะครับ ดังนี้

สิ่งที่เปลี่ยนไปอย่างแรกก็คือ เราใช้ this ครับ ตัวแปร this นั้นไม่ต้องสร้างอยู่แล้ว มันส่งเปล่าๆ มาจากข้างนอก แต่ในที่นี้เพื่อความง่าย ผมขอจินตนาการว่ามีการสร้าง this ในฟังก์ชันนี้เป็น object เปล่า แล้วก็แอบมีการกำหนด prototype ให้ตรงกัน และในบรรทัดสุดท้ายก็มีการ return this ตัวนี้ออกไป ดังนั้น จึงเขียนเป็นโครงได้ว่า

ดังนั้นสรุปง่ายๆ ก็คือ ถ้าเราใช้ new ก็จะมีการใช้โครงประมาณนี้ และบรรทัดที่เราเขียนก็จะแทรกอยู่ในบรรทัดที่ 4 นั่นเอง บางสำนักกลัวผู้ใช้จะลืม new ก็เลยออกแบบให้ฟังก์ชันทำหน้าที่เป็น constructor ได้ถูกต้องทั้งที่ new หรือไม่ได้ new ก็ตาม วิธีการไม่มีอะไรมากเลยครับ ก็ใช้ that อย่างวิธีข้างบนนั่นเอง เพราะเรารู้อยู่แล้วว่า การใช้ that ข้างต้นนั้น ทำงานได้ถูกต้องถ้าไม่ได้ new แล้วถ้าเกิด new ขึ้นมา จะเกิดอะไรขึ้น

ก็ไม่มีอะไรเข้าใจยากครับ ถ้าเรา new มันก็จะเอาที่เราเขียนเข้าไปแทรกในโครงกลายเป็น

เห็นได้ว่า โครงก็มี ที่เราแทรกก็มา มันแอบสร้างและกำหนดค่า this ก็ช่างมัน เราไม่ได้ใช้ เราใช้ that เวลา return ตัวของ that ส่งกลับก่อน ดังนั้น return this; จึงไม่มีโอกาสได้ทำ เท่านี้เองครับ  constructor ฟังก์ชันที่ทำงานได้ทั้ง new และไม่ new

ท่านอาจจะใช้วิธีการคาย Object ออกมาตรงๆ ก็ได้ ก็จะรองรับ new หรือไม่ new ก็ได้ทั้งสองเช่นกัน

เก็บตกการจัดการระดับ object

การสร้าง Object เปล่า

เราสร้าง Object โดยการ {} หรือ new Object() เราคิดว่าเป็น Object เปล่าๆ ที่จริงแล้วไม่ใช้ มันมีของแถม ดังตัวอย่างโปรแกรมนี้

เมื่อไม่ error แสดงว่ามี มันแอบเติมมาให้เราแล้ว รวมทั้ง toString() ด้วย  ถ้าเราไม่อยากได้ เราอยากได้แค่ object เปล่าๆ  เราสามารถใช้

อันนี้ได้ object เปล่าแท้ๆ เลย แต่เรื่องนี้มันมีอะไรลึกกว่าที่เห็น ผมขอยกยอดไปในบทความหน้าก็แล้วกัน

Conversion Methods

บ่อยครั้งที่เราอยากจะให้ object ของเรา ไปทำงานร่วมกับ string หรือ ตัวเลข เช่น

จากข้างบน ถ้าเอา object miyabi ไปกระทำกับ string จะได้คำว่า ‘miyabi’ ออกมา แต่ถ้าไปกระทำกับตัวเลข จะได้อายุ 29 ออกมา ทำได้ไม่ยากดังนี้ครับ

การป้องกัน object

อย่างที่ทราบว่า object นั้นมีความยืดหยุ่นสูง สามารถแก้ไขค่า properties ได้อย่างอิสระ แม้กระทั่งเพิ่มหรือลบ property ใดๆ ก็ได้ บางทีมันอาจดูหละหลวมเกินไป ทาง ECMAScript จึงออกแบบตัวควบคุมกว้างๆ ให้เราดังนี้

เพิ่ม / ลบ property ไม่ได้ก็จริง แต่ก็ยังสามารถกำหนดค่าให้แก่ properties ได้อยู่ ในกรณีที่ต้องการขั้นเด็ดขาด ใช้อย่างเดียว ห้าม เพิ่ม / ลบ properties หรือแม้กระทั่งห้ามแก้ค่า เราใช้ freeze() ครับ ดังนี้

ภาษา ECMAScript ดูจะเป็นผู้ดีหน่อยครับ อะไรที่ห้ามในข้างต้น ถ้าเราฝืนทำมันจะไม่แสดงข้อผิดพลาดใดๆ ให้เห็น เพียงแต่มันดื้อ ไม่เปลี่ยนแปลงใดๆ

เก็บตกการจัดการระดับ property

การลบ property/method

สังเกตได้ว่า เราสามารถพยายามลบได้ทุกอย่าง มีตัวตนหรือไม่มีตัวตนก็ไม่เกิด error แต่ถ้าเป็นของที่สืบทอดมาอย่าง toString() มันจะดื้อไม่ตาย ซึ่งเอาไว้คุยกันในบทหน้าครับ มีอะไรชวนงงเยอะเหมือนกัน

Accessor properties

ถ้าเรามี property ชื่อว่า birthYear ตัวหนึ่งคือปีเกิดเป็นปี ค.ศ.  ถ้าเราให้ใครก็ได้มากำหนดค่า อาจจะเข้าใจคลาดเคลื่อนกัน ไปใส่เป็นปี พ.ศ. คราวนี้วุ่นแน่ครับ หาบั๊กกันหูตาเหลือก ทางแก้ก็คือเราต้องมีการป้องกันตัวแปรของเราไม่ให้ภายนอกเห็น ไม่ให้เขาแก้ได้โดยตรง ถ้าเป็นภาษา Java ก็ต้องใช้ method getBirthYear() และ setBirthYear() แล้วก็ปิด birthYear ให้เป็น private แต่ถ้าเป็น C++, C# หรือ VB.Net ก็จะมี accessor properties ทำให้ใช้งานสะดวกขึ้น มองเห็นเป็นเหมือนตัวแปรได้เลย  ECMAScript ก็มีคล้ายๆ กันครับ ดังนี้

จะเห็นว่าตัวแปรที่ใช้เก็บจริงนั้นเราเลี่ยงไปใช้ _birthYear ส่วน birthYear นั้นกลายเป็น accessor property ซึ่งผมให้ความสำคัญเวลาการกำหนดค่า ถ้าไม่อยู่ในช่วง ใครใส่ผิดมาก็ Error มันตรงนั้นเลย ไม่ใช่เข้าไปเก็บสำเร็จใน database ได้ แต่ระบบรวนไปหมด หาที่มาไม่ได้ แต่วิธีนี้ยังมีจุดอ่อนอยู่จุดหนึ่งครับ ตรงที่ _birthYear นั้น ถ้าไปเจอพวกแผลงๆ มันอาจจะแอบเข้าไปแก้โดยตรงได้ โดยไม่ผ่าน accessor property เราต้องใช้ความรู้ในหัวข้อต่อไปในการจัดการครับ

Optional keys ของ properties

property ทุกตัว (จำได้ไหมครับว่ารวม methods ด้วย เพราะ methods อาศัยตัวชี้ที่เป็น properties อยู่นั่นเอง) มี Optional keys หรือถ้าเป็นภาษาของผมก็เรียกว่า attributes อยู่ 4 ตัว เป็นเป็นการกำหนดคุณสมบัติ 4 อย่างให้แก่ property  ซึ่ง attributes เหล่านี้เป็นแบบ true/false และ default ก็เป็น true ทั้งสิ้น ถ้าสร้างด้วยวิธีปกติ แต่ถ้าสร้างด้วย วิธีที่นำเสนอในหัวข้อนี้ default จะเป็น false ครับ เรามาดูทั้ง 4 ตัวกันเลย

 configurable

ถ้าเป็น true จะใช้คำสั่ง delete ลบได้ มิฉะนั้นจะลบไม่ได้

enumerable

ลองดูคำสั่งนี้ครับ

แต่ละรอบเราได้ key ออกมา นั่นก็คือ property นั่นเอง ถ้าเราไม่อยากให้ property ใดๆ มองเห็นจากการ for in แบบนี้ เราก็กำหนดให้ enumerable เป็น false  ซึ่งมีผลต่อ JSON.stringify() ด้วยครับ ถ้า enumerable เป็น false JSON.stringify() จะข้าม property นั้นไป ไม่สร้างเป็น string ให้

writeable

ตัวนี้เข้าใจง่ายครับ ถ้า writeable เป็น false ก็จะอ่านค่าได้อย่างเดียว แก้ไขไม่ได้

value

ก็คือค่าของ properties นั่นเอง  default เป็น undefined

การกำหนดค่า

เมื่อเรารู้แล้ว property แต่ละตัวมี Option keys 4 ตัวนี้ คราวนี้เรามาลองกำหนดค่าดูครับ

โค้ดนี้แสดงให้เห็นแบบเต็มรูปเลยนะครับ ที่เราสร้าง _name ขึ้นมาเหมือนกับในภาษา OO ทั่วไป แต่เราต้องการควบคุมข้อมูลเข้าออก เราจึงใช้ get/set มีจุดหน้าสังเกตอยู่ว่า _name นั้นจะ writable: false ไม่ได้ เพราะตัว set ของ name ก็จะเขียนไม่เข้าเหมือนกัน แต่สิ่งที่ช่วยได้ก็คือเราซ่อนเสีย โดยการกำหนดให้ enumerable: false และเมื่อดูบรรทัดสุดท้ายแสดง object ทั้งหมด จะมองเห็นแต่ name ครับ _name จะถูกซ่อน

เราอาจกำหนดจะสร้าง/แก้ไขทีละ property ก็ได้ โดยใช้

ES6: class

อย่างที่ผมบอกท่านแล้วว่า ใน ECMAScript นั้นเรายืมเอาฟังก์ชันมาใช้เป็นการนิยาม class แต่คนที่อพยพมาจากสาย OO ซึ่งมีจำนวนมากนั้น เขาไม่คุ้นชินกับแนวคิดนี้ ทาง TC39 จึงตัดสินใจเพิ่ม class เข้าไปในนิยามของภาษา ดังนี้

คราวนี้คุ้นเคยกันเลยใช่ไหมครับ แต่จริงๆ แล้ว syntax การใช้งานเป็นเพียง ‘ผงชูรส’ (syntactic sugar) เพื่อให้มันดูกลมกล่อมขึ้นเท่านั้น แท้ที่จริงภายในมันก็ไปสร้างเป็น constructor function แบบข้างบนอยู่ดี ส่วนเรื่องตัวแปร this ผมขออธิบายในหัวข้อต่อไปครับ

ถ้าพูดถึงการสร้าง keyword คำว่า class ในความคิดส่วนตัวของผม ผมว่ามันไม่จำเป็น และจริงๆ แล้วไม่ควรมีด้วยซ้ำไป เพราะ constructor function กับแนวคิดของ class ภาษาอื่น มันมีความไม่เหมือนกันอยู่ลึกๆ บางอย่าง  คำสั่ง class ที่ท่านใช้ใน ECMAScript กับภาษา OO อื่นๆ มีความไม่เหมือนกันในบางอย่าง ซึ่งผมค่อยนำเสนอให้ท่านได้อ่านกันเป็นลำดับไปครับ

get/set property

เพื่อความสะดวก ES6 จึงเพิ่ม get/set property เหมือน OO ภาษาอื่นดังนี้

private variable

ใน ES6 มีการคิดค้นชนิดตัวแปรใหม่ขึ้นมา เรียกว่า Symbol ซึ่ง symbol นี้ก็ตรงไปตรงมา เมื่อสร้างแล้วก็ unique เราสามารถประยุกต์ใช้ symbol เพื่อสร้างตัวแปร private ดังนี้

จะเห็นได้ว่า เมื่อเรา for in ดูใน somsri แล้ว เราไม่พบ _name เราพบแค่ age และ function ดังนั้น _name จึงเป็น private โดยสมบูรณ์ แต่มีข้อเสียตรงที่ การนิยาม _name ให้เป็น Symbol นั้น เป็นการนิยามนอก class ซึ่งดูแล้วจะหลุดเรื่อง encapsulation ไปเสียเล็กน้อย

TS: object

การสร้าง object ใน TS นั้นเราสามารถสร้าง prototype ก่อนได้ ดังนี้

TS: interface

วิธีการก่อนหน้า ดูๆ แล้วยังไม่ดี Person น่าจะเป็นชนิดตัวแปร เพื่อให้เรากำหนดให้แก่ตัวแปรอื่นๆ อีกทีทาง TS จึงมีทางเลือกอีกทางคือใช้ interface ดังโค้ดนี้ครับ

ผมจะขออธิบายส่วนที่น่าสนใจ เพราะท่านคงรู้จัก interface ดีอยู่แล้ว เริ่มจากบรรทัดที่ 3 เป็นการกำหนดว่า property ตัวใดไม่ต้องมีก็ได้ จึงทำให้บรรทัดที่ 7 นั้น เราไม่จำเป็นต้องกำหนคค่า age

แต่ในบรรทัดที่ 11 ไปไม่รอดเพราะ เราไม่สามารถเพิ่มเติม property เพิ่มเข้าไปได้อีกแล้ว และในบรรทัดที่ 12 ก็เช่นกัน ถ้าต้องการยังไม่กำหนดค่าก็ใช้แบบบรรทัดที่ 13 ซึ่งเป็นการ convert {} ให้เป็นชนิด Person ก่อน จึงจะนำไปกำหนดค่าได้

มีสิ่งที่ชวนฉงนว่า ถ้า Typescript ยอมให้กำหนด object เปล่าได้ แล้วจะคุมได้อย่างไรว่า property name นั้นต้องมี เรื่องนี้ก็ไม่มีอะไรซับซ้อน ก็คือรั่วครับ ถ้าแสดงค่า somjui.name ออกมาจะได้ undefined

TS: class

TS นั้นเพิ่มความสามารถของการกำหนดชนิดเข้าไปยัง class เรื่องนั้นเป็นของตายอยู่แล้ว แต่สิ่งที่น่าสนใจก็คือ TS เพิ่มความสามารถในการทำ private property อีกด้วย ดังตัวอย่างนี้

ถ้าเราไม่กำหนด private หน้าตัวแปร ธรรมชาติของมันจะเป็น public ในตัวอย่างในบรรทัดสุดท้ายจะคอมไพล์ไม่ผ่าน เพราะพยายามเข้าถึงตัวแปร private

ทิ้งท้าย

ในบทความนี้ผมแสดงให้เห็นมุมมองของ object-based programming ที่ ECMAScript นั้นใช้เป็นหลัก ในบทต่อไปผมจะว่าด้วยเรื่องของ object-oriented programming ซึ่งก็มีรายละเอียดอยู่พอสมควร แล้วพบกันใหม่ครับ

 

[Total: 3    Average: 3.7/5]

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *