JavaScript #05: ในมิติ Object-Oriented Programming

เกริ่นนำ

Object-Oriented เป็นกระบวนทัศน์หลักในการพัฒนายุคปัจจุบัน เด็กที่เรียนสายเขียนโปรแกรมโดยตรงก็น่าจะผ่านแนวคิดของ OO นี้มาแล้วเกือบทุกคน และเนื่องจากความนิยมจึงเป็นเหตุให้ ECMAScript ไม่ขอตกขบวน รองรับ OO กับเขาเหมือนกัน แต่ก็เป็น OO ในแบบเฉพาะตัวของ ECMAScript ซึ่งต่างกับ OO ของภาษาอื่นๆ ทั่วไปไม่น้อย บทความนี้ขออุทิศให้แก่ความแตกต่างเหล่านั้น

ผมคาดหวังว่าก่อนที่ท่านอ่านบทความบทนี้ ท่านน่าจะมีความรู้พื้นฐาน OO และคุ้นเคยกับภาษาทาง OO มาบ้าง จะเป็น C++/Python/Java/C#/VB.NET หรืออะไรก็แล้วแต่ที่ใช้กระบวนทัศน์ OO เป็นหลัก ได้ทั้งนั้น จะทำให้อ่านเข้าใจได้โดยง่ายครับ

พื้นฐาน Object-Oriented

ผมขอทบทวนแนวคิด OO ก่อนเสียเล็กน้อย จะได้ต่อกันติด แนวคิดของ OO นั้นเกิดขึ้นมาเพื่อกำจัดหรือลด 4 ซ. นั่นคือ ‘ซับซ้อน’ และ ‘ซ้ำซ้อน’ ถ้าจัดการปัญหาเหล่านี้ได้ก็จะเกิดผลลัพธ์คือการ reuse เกิดขึ้น แทนที่จะต้องทำอะไรใหม่เริ่มต้นจากศูนย์ เมื่อ reuse เราก็เพียงแค่ต่อยอดเท่านั้น

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

  • abstraction
  • encapsulation
  • inheritance
  • polymorphism

abstraction

abstraction หรือนามธรรม ตรงกันข้ามกับคำว่า concretion หรือรูปธรรม ถ้าพิจารณารูปภาพไม่ว่าจะเป็นภาพวาดหรือภาพถ่ายทาง abstraction เช่นภาพความหิว แทนที่จะนำเสนอมาในรูปของอาหารเต็มโต๊ะ มีคนมองด้วยสายตาอยาก น้ำลายหยดติ๋งๆ แต่ผู้วาดกลับตัดองค์ประกอบที่เกี่ยวข้องเหล่านี้ออกไปเสียเกือบทั้งหมด นำเสนอ ‘ความหิว’ ในมุมองของผู้วาดเข้าใจ ซึ่งคนอื่นจะเข้าใจหรือไม่นั่นก็เป็นอีกเรื่องหนึ่ง

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

เรื่องราวเกี่ยวกับ constructor function ผมนำเสนอเป็นที่เรียบร้อยแล้วในบทความก่อนหน้า จึงไม่ขอฉายซ้ำอีก

encapsulation

ว่ากันตามรากศัพท์ของคำว่า encapsulation ก็น่าจะได้มาจากการเอายาเข้าแคปซูล คนป่วยมีหน้าที่กินให้ถูกสีของแคปซูล และตามเวลาที่หมอกำหนดเท่านั้น ไม่จำเป็นต้องรู้ว่าตัวยาที่อยู่ในแคปซูลนั้นมีอะไรบ้าง นั่นคือหัวใจของเรื่องนี้

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

เรื่องราวเกี่ยวกับ encapsulation ผมนำเสนอเป็นที่เรียบร้อยแล้วในบทความก่อนหน้า จึงไม่ขอฉายซ้ำอีก

inheritance

‘การสืบทอด’ เป็นความหมายของมัน การสืบเชื้อสาย การได้รับมรดก ก็ล้วนเข้าด้วยเนื้อหาเรื่องนี้ด้วยกันทั้งสิ้น

ในโลกของ OO เมื่อเราต้องการอะไรใหม่ เราจะพิจารณาจากของเดิมที่มีอยู่แล้ว ถ้ามีอยู่แล้ว แต่ดีไม่พอ เราก็ต่อยอดมันขึ้นไป มันจึงเรียกว่าการสืบทอดหรือได้รับมรดกมาจากของเดิม ถ้ามองกันในอีกมุมหนึ่ง อาจจะมองได้ว่า เราสามารถ reuse ของเดิม โดยการนำเอามาปรับปรุงเสียใหม่เล็กน้อย ก็สามารถรองรับงานปัจจุบันได้แล้ว และถ้าพิจารณาให้ละเอียดขึ้น ก็จะเห็นว่าเป็นการ reuse ในส่วนของผู้ถูกเรียกใช้ เช่น library ต่างๆ นั่นเอง

Grady Booch หนึ่งในคณะสามสหาย (three amigos) ผู้คิดค้น UML และเป็นปรมาจารย์ทางด้าน OO เคยนิยามความแตกต่างระหว่าง object-based และ object-oriented เอาไว้ว่า

The ability of a language to support this kind of inheritance distinguishes object-oriented from object-based programming languages.

และ

programming without inheritance is distinctly not object-oriented.

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

เรื่องของ inheritance มาในระยะหลังมีการใช้งานกันแบบเลอะเทอะมาก จนต้องมีการลดการใช้งาน และพัฒนาต่อมาเป็น interface-based programming จนแม้แต่ Grady Booch เองยังออกปากเองอยู่บ่อยครั้งว่า

Inheritance is highly overrated.

polymorphism

poly คือหลาย morph คือหน้าหรือรูปร่าง แปลรวมความแล้วหมายความว่ามีหลายหน้า หรือหลายรูปร่าง อย่างเช่นรถเป็นต้น จำหนังดังเก่าๆ ได้ไหมครับ เรื่อง speed เร็วกว่านรก ที่นางเอกแซนด้า บูลล๊อก ต้องขับรถบัส หยุดไม่ได้ ถ้ารถหยุด ระเบิดจะทำงาน ถามว่านางเอกในเรื่องเคยขับรถบัสรึเปล่าครับ ไม่ต้องสงสัยเลยว่าไม่เคยครับ ในเรื่องก็เคยขับแต่รถเก๋งธรรมดา แล้วทำไมถึงขับรถบัสได้หละ ถ้าดูดีๆ แล้ว ใช่ว่านางเองจะรู้จักรถบัสคันนั้นอย่างแจ่มแจ้งนะครับ เธอรู้เฉพาะในส่วนที่เหมือนกับรถเก๋งของเท่านั้นเอง เช่น พวงมาลัย คันเร่ง เบรค เกี่ยร์เป็นต้น นั่นก็คือ interface พื้นฐานที่เหมือนกันในรถแต่ละคัน ตราบใดที่ยังคงใช้เพียงแค่ interface ร่วมชุดนี้ ก็จะใช้งานได้ แต่ถ้าเธออยากจะไปเร่งแอร์ คราวนี้ก็คนละเรื่องแล้วครับ อาจจะหาปุ่มไม่เจอ เรื่องนี้รถใครรถมัน แตกต่างกันไปในแต่ละรุ่น

ถามต่อว่า ทำไมหน้าตารถกระบะถึงมีหน้าตาคล้ายๆ กัน แม้ว่าจะต่างยี่ห้อกันก็ตาม คำตอบก็คือมัน inherit มาจากรถกระบะโบราณต้นแบบสมัยก่อน พูดง่ายๆ ว่ามีรากเดียวกันนั่นเอง นั่นเป็นการ reuse รูปร่างรถ ถือว่าเป็นการ inheritance  และทำไมเราจึงขับรถกระบะได้ โดยที่เราไม่เคยขับมาก่อน (แต่เคยขับรถชนิดอื่นมานะครับ) นั่นเพราะรถกระบะมีคุณสมบัติของ polymorphism ทำให้เราสามารถ reuse ตัวเราเองได้

ดังนั้น inheritance จึงเป็นการ reuse ของที่ถูกเรียกเช่น libraries เป็นต้น ส่วน polymorphism คือการ reuse ผู้เรียกนั่นเอง เช่น Windows Form อาจเกิดปี 2002 แต่  controls   ต่างๆ เกิดหลังจากนั้น แน่นอนครับ เมื่อตอนที่ Windows Form เกิดนั้น ย่อมต้องไม่รู้จัก controls เหล่านั้นเป็นแน่ แล้วทำไม Windows Form จึงสามารถ host เอา controls ใหม่ๆ เหล่านั้นมาอยู่บนจอได้หละ คำตอบก็คือ polymorphism นั่นเอง โดยที่ Windows Form นั้นมอง controls ต่างๆ เป็นเพียง controls ไม่มองในรายละเอียดเป็น textbox หรือ combobox ดังนั้นขอให้ control เหล่านั้นรองรับ interface มาตรฐานที่ Windows Form ใช้ติดต่อ เท่านี้ก็เชื่อมกันได้แล้ว ส่วนที่จะไปเพิ่มเติมอะไร ก็สุดแล้วแต่ ทาง Windows Form เข้าไม่เข้าไปยุ่ง ส่วนที่เพิ่มเติมนั้น จึงเป็น interface เพื่อให้โปรแกรมเมอร์มาใช้งาน

Gotcha: เรื่องของ prototype

มาเล่นกันเถอะๆ มาเรียนรู้การทำ inheritance ตามแบบฉบับ ES5 โดยใช้ gotcha! เป็นสื่อกัน ในการนี้เราใช้ prototype ที่ผมเคยนำเสนอในบทความที่ผ่านมา พิจารณาโค้ดนี้ดูครับ

ใน Person มี property name อยู่ จากนั้นในบรรทัดที่ 5 ผมก็กำหนด prototype ชื่อ age ขึ้นมากำหนดให้มันเป็น 20  ซึ่งแนวคิดจะคล้ายกับ inheritance นะครับ คือเมื่อเรา new Person() ขึ้นมา เราจะมีทั้ง name และ age เอาไว้ใช้งาน ซึ่งท่านลองไล่โปรแกรมดูครับ ไม่น่าจะมีอะไรเข้าใจยาก ทุกอย่างก็ตรงไปตรงมา คราวนี้มาถึงคำถามครับ  ถ้าผมเขียนอีกสองบรรทัดต่อท้ายโปรแกรมข้างบนนี้ จะได้ผลลัพธ์อะไร

ตอบกันถูกไหมครับ  ถ้าตอบไม่ถูก ผมให้ gotcha! นะ คาดว่าหลายคนคงตอบไม่ถูก คำตอบก็คือ

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

Prototype-based inheritance

ผมขอย้ำว่า OO ใน ECMAScript ไม่ใช่เป็น OO ในแบบทั่วไปที่เราคุ้นเคยกัน ถ้าถามว่ามันทำ inheritance ได้ไหม ก็จะบอกว่าไม่ได้มันก็คงไม่ใช่ แต่ถ้าบอกว่าได้มันก็กระไรอยู่ การ inheritance ใน ES5 นั้นทำผ่าน prototype หรือเรียกว่า Prototypal inheritance ซึ่งเป็นการ inherit ในระดับ object ไม่ใช่ระดับ class เรามาลองดูตัวอย่างนี้ครับ

อันนี้ก็ตรงไปตรงมาใช่ไหมครับ คือใน C1 เอง มีการสร้าง properties 3 ตัวคือ one, two, three และก็ inherit มาจาก prototype อีก 3 ตัว รวมแล้วมี 6 ตัว ซึ่งดูแล้วก็คล้ายๆ การ inheritance แบบทั่วไป แต่เรามาลองดูการตรวจสอบหา properties เหล่านี้ดูครับ

โค้ดข้างบนน่าสนใจนะครับ ถ้าท่านใช้ in ในการตรวจสอบ ก็จะพบทั้ง 6 ตัว แต่ถ้าใช้ method hasOwnProperty() ในการตรวจสอบกลับไม่พบที่ทำ prototype มา ท่านเดาได้รึยังครับว่าเรื่องราวมันเป็นยังไง

ความลับของเรื่องนี้มันอยู่ที่ตัว method ที่ใช้ในการตรวจสอบใช้คำว่า hasOwnProperty()  ใช่แล้วครับ ตัว runtime/virtual machine มันไม่ได้เอา properties จาก prototype มายัดใส่ object ของเรา แต่หาก object ของเราจะมี property ซ่อนอยู่ตัวหนึ่ง (มีในทุก object) ชื่อว่า [[Prototype]] ซึ่งตัวชี้ตัวนี้ เรารู้จักกันในชื่อ __proto__ นั่นเอง ทำหน้าที่ชี้ไปยังอีก object หนึ่งที่เก็บ prototype object นั่นเอง ภายในนั้นจึงจะมี p1  p2 และ p3 เมื่อรันโปรแกรมข้างบนเสร็จ จะได้โครงสร้างข้อมูลภายในดังกระดานดำข้างล่างนี้ครับ

prototype1

 

จากกระดานดำจะเห็นว่า เมื่อสร้าง object c จะได้ __proto__ ขึ้นมา มันจะชี้ที่เดียวกันกับ [[Prototype]] (หรือในชื่อ property prototype) ของฟังก์ชัน C1 ทันทีเมื่อสร้าง objct c นี่คือหัวใจของเรื่องทั้งหมด ทำให้การ inheritance เกิดขึ้นได้ และเพื่อให้เข้าใจกระจ่างจิตกันไป ผมของเพิ่มโค้ดคำสั่งดังนี้

ลองดูนะครับว่า หลังจากคำสั่งนี้แล้ว จะได้ผลลัพธ์เป็นดังกระดานดำนี้ครับ

prototype2

นี่แหละครับความลับของมัน ใครติดเรื่อง gotcha! ข้างบนดูกระดานดำหน้านี้ก็น่าจะกระจ่างขึ้น แต่ถ้ายังไม่กระจ่างก็ลองพิจารณาดูตัว object ที่อยู่ตรงกลาง เป็น object ของ prototype  ตัว p1, p2, p3 มันอยู่ตรงนี้เอง เมื่อสร้าง object c, cc ไม่มีการคัดลอกตัวแปรทั้งสามมาสู่ c และ cc แต่อย่างใด แต่หากเป็นการชี้ไปเท่านั้น ดังนั้น ถ้า

อันนี้ก็ตรงๆ นะครับ เพราะ c และ c1 ไม่มี property p1 ดังนั้น เมื่อหาไม่พบใน object ตัวเองมันก็จะไต่ลงไปใน __proto__  ซึ่งก็พบ จึงนำค่ามาแสดง คราวนี้มาถึงจุดหน้าสนใจ แล้วถ้า c เกิดกำหนดค่า p1 ดังนี้

อะไรจะเกิดขึ้น ลองดูกระดานดำนี้ครับ ว่าตรงกับที่ท่านคิดหรือไม่

prototype3

 

เมื่อเราเพิ่ม p1 ให้แก่ c มันก็ไปอยู่ที่ c ดังนั้นถ้าถามหา p1 จาก c มันมีในตัวของมันเองแล้ว จึงดึงมาตอบได้เลย มันจะไม่มีการวิ่งเข้าไปใน __proto__ เพื่อหาอีกต่อไป ถ้าใครเคยอ่านบทความก่อนหน้าเรื่อง JavaScript #03: ในมิติ Functional Programming คงจะคุ้นๆ ว่าแนวคิดการทำงานภายในของ prototype นี่ช่างคล้ายกับแนวคิดภายในของ closure เสียนี่กระไร

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

ทำแบบนี้เพื่อ !?

แนวคิด prototype-based ข้างบนนี้ถือว่าเป็นหมู่บ้านกระสุนตกแห่งหนึ่งของ ECMAScript ครับ มีเสียงบ่นจากผู้ใช้ไม่น้อยว่า แนวคิดช่างประหลาดดีแท้ กว่าจะเข้าใจได้ก็งงกันหลายตลบ  ไม่รู้ว่าทีม ECMA-262 มีอะไรเข้าสิงถึงได้ออกแบบมาได้ประหลาดอย่างนี้ เพื่อตอบปัญหานี้ ผมขอนำเสนอโค้ดภาษา C++ ทำเลียนแบบสิ่งที่ผมนำเสนอมาก่อนหน้า ดังนี้

จากโปรแกรมข้างบนนี้ ผมขอแปลงตัวแปรให้มันกลายเป็นตัวเลขทั้งหมด จะได้คำนวณขนาดได้ง่ายๆ  ผมอยากพิจารณาขนาดของ object ที่สร้างจาก c1 จะเห็นว่ามีขนาด 24 bytes หรือ int 6 ตัวนั่นเอง จะเห็นว่า เมื่อท่านสร้าง object มาหนึ่งตัวจาก class ใดก็ตาม ไม่ว่า class นั้นจะ inherit มากี่ชั้นก็ตาม ตัวแปร fields ทั้งหมดของทุกชั้นที่ inherit มา จะถูกสร้างขึ้น ในส่วนของ methods ก็เช่นกันนะครับ แม้ว่าเนื้อโค้ดของมันมีเพืยงที่เดียวก็จริง ไม่ได้ถูกสร้างตามจำนวนการที่ new object แต่ตัวชี้ไปยังฟังก์ชันก็ต้องเกิดตามจำนวนครั้งของการ new objects อยู่ดี เป็นเรื่องที่สิ้นเปลืองมาก และภาษา OO เกือบทั้งหมด ก็เป็นเช่นนี้ครับ

สิ่งที่ ECMAScript ทำนั้นแทนที่สร้างตัวแปรขึ้นมามากมาย แต่กลับใช้วิธีการ “แชร์” ซึ่งจากตัวอย่าง ในแต่ละ object ที่สร้างใหม่ได้แก่ c, cc จะมีตัวแปรเริ่มต้นเพียง 3 ตัวเท่านั้น (ไม่นับตัวชี้ต่างๆ ที่ซ่อนอยู่นะครับ) ส่วนอีก 3 ตัวก็ “แชร์” ลองนึกภาพว่า ถ้าท่าน object มาหลายร้อยตัว และแต่ละตัว มี “แชร์” properties/methods อยู่เป็นจำนวนมาก ท่านจะประหยัดหน่วยความจำได้สักแค่ไหนหนอ แต่ในมุมกลับ การออกแบบอย่างนี้มีผลข้างเคียง อย่างที่ท่านเห็นใน gotcha! ซึ่งท่านต้องเข้าใจการทำงานของมันเป็นอย่างดี จะได้ไม่ผิดพลาด

ในภาษา OO มาตรฐานก็มีการใช้ตัวแปร shared หรือ static ที่ให้ผลลัพธ์ใกล้เคียงกับ prototypal

Inheritance แบบ classic

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

เมื่อศาสตร์หมด ที่เหลือก็คือศิลป์ ชีวิตยังไม่สิ้นก็ดิ้นกันไป เรามาลองดูกันว่า จะทำได้อย่างไร ผมขอนำเสนอเป็น Python ก่อน ถือเป็นตัวแทนภาษา OO ทั่วไป  เชื่อว่าท่านคงคุ้นเคยดีอยู่แล้ว จากนั้นจะลองดัดให้เป็น ECMAScript ดู วิธีนี้น่าจะทำให้การเรียนรู้เป็นไปได้อย่างรวดเร็วขึ้น

เริ่มจาก class เดียวก่อน

จากตัวอย่างข้างบนภาษา Python ผมก็แปลงมาเป็น ECMAScript ถ้าต้องการสร้าง class แบบนี้เราก็ใช้ฟังก์ชัน constructor function ดังนี้

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

  • methods ควรอยู่ใน prototype object ยกเว้นเป็น methods เฉพาะของ class ใดๆ
  • properties ควรอยู่ใน constructor function ยกเว้นค่าคงที่ที่อ่านอย่างเดียว

constructor property

prototype object นั้นเป็นของที่เกิดขึ้นคู่กับฟังก์ชันอยู่แล้ว เมื่อเรานิยามฟังก์ชัน ก็จะมีตัวชี้ [[Prototype]] เพื่อชี้ไปยัง prototype object ที่เกิดขึ้นมาใหม่ ถามว่า prototype object นั้นภายในมีอะไรบรรจุอยู่ หรือเป็น object เปล่าๆ  แน่นอนครับไม่เปล่าแน่นอน มี properties ซ่อนอยู่หลายตัวมาก แต่ในหัวข้อนี้ผมขอพูดถึง constructor และ __proto__  มาดูรูปกันก่อนครับจะได้รู้ว่ามันอยู่ตรงไหนในโครงสร้างข้อมูล

constructor

 

รูปนี้แสดงกลไกการทำ inheritance ของ ECMAScript ในหัวข้อนี้ผมยังไม่แสดงให้เห็นนะครับว่าเขียนโค้ดยังไงถึงทำให้ได้โครงสร้างนี้ โค้ดเอาไว้ในหัวข้อต่อไป ในหัวข้อนี้ถือว่าเรามีโครงสร้างข้อมูลตัวนี้ก่อนก็แล้วกัน เอาหละครับตามผมมา ผมจะพาไล่โครงสร้างตามรูปข้างบนนี้กัน

เริ่มจาก Dog และ Animal ในที่นี้เป็นฟังก์ชัน ตามรูปนี้ผมแสดงด้วยกรอบสีชมพู (เรื่องสีอย่าเอาอะไรกับผมมาก สีชมพูผมไม่ได้หมายถึงฟังก์ชันเป็นสรณะนะครับ การเลือกสีมันอยู่ที่อารมณ์ตอนวาด ไม่ได้กติกาอะไรตายตัว) จริงๆ แล้วใน object ของฟังก์ชันที่สร้างขึ้นจะมีตัวชี้ตัวหนึ่งชื่อว่า [[ECMAScriptCode]] ผมเคยแสดงให้เห็นในตอนต้นของบทความนี้ไปแล้ว แต่เพื่อความเรียบง่าย ผมขอละไว้ไม่แสดงให้เห็นครับ

เมื่อมีการนิยามฟังก์ชัน ก็จะเกิด prototype object ขึ้นมาคู่กัน หนึ่ง object ต่อหนึ่งฟังก์ชัน ในที่นี้แสดงให้เห็นเป็นกรอบสีน้ำเงิน  object คู่นี้จะมีตัวชี้ซึ่งกันและกัน ในฟังก์ชันมี [[Prototype]] หรือ prototype เพื่อชี้มายัง prototype object ในทางกลับกัน ใน prototype object ก็จะมีตัวชี้ชื่อว่า constructor เพื่อชี้กลับมายังฟังก์ชัน

คราวนี้เรามาดูการทำงานกันครับ สมมุติว่าท่านเรียกใช้

เริ่มต้นจากทางเข้า object jim ถ้าใน jim มี method cry() ก็จะเรียกใช้งานได้เลยทุกอย่างก็จบ แต่ถ้าไม่มี มันจะมุดเข้าไปยัง __proto__ เพื่อไปยัง prototype object ของ Dog ถ้าใน object นี้มีก็จะเรียกใช้งาน แล้วก็จบ แต่ถ้าไม่มีก็จะไล่ลงไปใน __proto__ อีก ไล่กันเป็นชั้นๆ จนกว่าจะเจอหรือสิ้นสุด ในส่วนก็การเรียกใช้ properties ก็เช่นเดียวกันนะครับ ไล่เป็นชั้นๆ แบบนี้เหมือนกัน นี่คือกลไกการจัดการเรื่อง inhertance ของ ECMAScript นั่นเอง

ถ้าสมมุติว่าใน prototype object ของ Animal มี cry() อยู่ แต่เราไปเพิ่ม cry() ใน jim ก็ย่อมทำได้ ถ้าเป็นภาษา OO ทั่วไป เรียกว่า override นั่นเอง เข้าใจไม่ยากนะครับ ตรงไปตรงมา แต่การ override นั้นบ่อยครั้งไม่ใช่ทดแทนของเดิม แต่หากเสริมเพิ่มเติมความสามารถเดิมขึ้นไป โดยการเรียกใช้ของเดิมก่อน จากนั้นค่อยเสริมในส่วนที่เราต้องการ ทำได้ดังตัวอย่างนี้

เมื่อเราเรียกใช้ jim.cry() ในบรรทัดแรกมันจะไปเรียก cry() method ที่อยู่ใน prototype object ซึ่งอาจจะไม่เจอก็ได้ ถ้าไม่เจอ มันก็จะวิ่งไล่ลงไปตาม __proto__ ชั้นอื่นไล่จนเจอแล้วสั่งทำงาน จากนั้นก็ค่อยมาทำโค้ดเสริมของเรา ซึ่งการทำลักษณะนี้จะให้ผลลัพธ์ไม่ต่างจาก OO ภาษาอื่นเลย

คราวนี้เรามาประยุกต์ใช้ constructor property ดูบ้าง ถ้าเราต้องการรู้ว่า object ของเราสร้างจากฟังก์ชัน constructor ใด ก็ดูจาก constructor property นั่นเอง ดังโค้ดนี้ครับ

ที่เป็นดังข้างบนก็เพราะเมื่อวิ่งไล่เข้าไปใน __proto__ จะพบ constructor property ที่ชี้ไปยัง Dog ก่อนเสมอ

ถามว่า เราสามารถเปลี่ยน [[Prototype]] และ constructor property ให้ไปชี้ที่อื่นได้หรือไม่ คำตอบคือได้แน่นอนครับ ถ้าเรียนกันลึกๆ แล้วเราอาจเปลี่ยนเป็นชี้ที่อื่นเพื่อให้ได้ลูกเล่นบางอย่างในระดับก้าวหน้าได้ แต่ในบทความนี้ผมเอาแค่พื้นฐานเท่านั้น ก็เลยจะไม่เปลี่ยน และในส่วนของ constructor property ผมขอละเอาไว้แค่นี้ ในส่วนที่เหลือของบทความนี้ผมจะละเอาไว้ไม่แสดงนะครับ จะได้ไม่ดูชี้ไปชี้มาดูสับสนจนเกินไป

การเขียนโค้ด

ถึงเวลาลงโค้ดกัน ปูพื้นฐานกันมาพอสมควรแล้ว ผมจะสร้าง class ตามโครงสร้างลำดับชั้นที่แสดงในรูปแบบ UML ดังรูปข้างล่างนี้

uml_inheritance

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

จากโค้ดข้างต้น ผมกำหนดให้ cry() เป็น abstract method ของ Animal ดังนั้น class ที่ inherit ไป ก็จะต้องลงโค้ดในส่วนของ method นี้ เป็นการบังคับให้ต้องมี ดังที่เราคุ้นเคยกันดีอยู่แล้ว  จากนั้นผมสร้าง Dog และ Cat ขึ้นมา ให้ inherit จาก Animal โดยการส่งชื่อสัตว์ไปเป็น constructor ของ Dog, Cat ซึ่งทั้ง Dog, Cat จะต้องหากลยุทธ์เพื่อส่งผ่านไปยัง animal เพื่อบรรจุชื่อนี้ลงไปยังตัวแปร name และในที่สุดก็เพิ่ม method cry() เข้าไป ซึ่งเนื้อในก็แตกต่างกันออกไปตามเสียงร้อง

ผมสร้าง jim เป็น Bangkaew,  jam เป็น Cat  ทั้งคู่สามารถเรียกใช้ method เดิมของ Animal คือ showName() ได้ และเรียก method cry() ก็ไม่มีปัญหา เรียกใช้ guard() ก็ย่อมได้ถ้าเป็น jim และที่สำคัญก็คือเมื่อใช้ isinstance() เพื่อตรวจสอบชนิด พบว่า ทั้ง jim และ jam เป็น Animal และ jim เป็น Dog และ Bangkaew ไม่เป็น Cat ส่วน jam นั้นกลับกัน สอดคล้องตามสิ่งที่ควรจะเป็นทุกประการ

คราวนี้มาถึง ECMAScript แล้ว ถ้าผมต้องการให้ทำเช่นนี้ได้ ผมต้องเขียนโปรแกรมอย่างไร ดูโค้ดเลยครับ

อยากแรกที่ต้องคุยกันก่อนก็คือ ECMAScript ไม่มีแนวคิดของ abstract method ดังนั้นจึงไม่สามารถบังคับให้ class ที่ inherit ไปต้องสร้าง method ได้ ดังนั้น ข้อนี้ก็ถือว่าสอบตกไป ผมจึงขอข้ามไป ส่วนข้ออื่นนั้นพอทำได้ วิธีการเขียนก็ตรงไปตรงมา อย่างที่แสดงให้เห็น

เพื่อให้การอธิบายง่ายเข้า ผมขอแสดงแผงผังหน่วยความจำบนของโปรแกรมข้างบนหลังทำงานถึงบรรทัดสุดท้ายบนกระดานดำ ดังนี้

single interitance internal

จะเห็นได้ว่าในส่วนของ prototype กรอบสีน้ำเงินนั้น มีการใช้ร่วม มีจำนวนตัวเท่ากับจำนวนฟังก์ชัน constructor (กรอบชมพู) เมื่อเพิ่ม objects เข้าไป กรอบน้ำเงินก็จะไม่เพิ่มขึ้น ภายในมี __proto__  เพื่อเชื่อมตามลำดับการ inheritance

jim และ jam นั้นใช้ prototype ของ Animal ร่วมกัน และสมมุติว่าถ้าท่านสร้าง object Bangkaew ขึ้นมาสองตัว ทั้งสองตัวก็จะใช้ prototype ของ Bangkaew, Dog และ Animal ร่วมกัน เข้าใจตรงกันนะครับ

ส่วน properties ที่เกิดขึ้นมานั้น ใน Bangkaew ผมส่ง this ซึ่งเป็นของ Bangkaew เข้าไปสู่ constructor function ของ Dog โดยใช้การ call ดังนั้น เมื่อ dog ทำอะไรกับ this มันก็คือตัวเดียวกันกับ Bangkaew ผลลัพธ์ก็คือ ในแต่ละ constructor function จะสร้าง properties อะไรขึ้นมาก็ตาม ก็จะสะสมอยู่ใน this ตัวเดียวกันทั้งหมด นั่นคือกุญแจทั้งหมดครับ ถ้าทำได้ตามนี้ เราก็สามารถเลียนแบบการ inheritance ของภาษา OO อื่นๆ ได้แล้วครับ

Inheritance แบบร่วมสมัย

อย่างที่ผมได้กล่าวเอาไว้ตั้งแต่บทความก่อนหน้าแล้วว่า ECMAScript นั้นไม่ใช่ OO แต่หากเป็น object-based ดังนั้นการที่ทำให้ภาษารองรับ inheritance ในแบบที่ผมและท่านคุ้นเคยนั้น จึงเป็นการ ข่มเขาโคขึนให้กลืนหญ้า  ดังที่ผมเพิ่งแสดงให้ท่านเห็นไปในหัวข้อก่อนหน้า

Object-based มันก็มีวิธีการ inheritance เป็นแบบเฉพาะของมันเองเหมือนกัน เพียงแต่ว่ามันอาจจะแตกต่างกับที่เราคุ้นเคยกัน แนวคิดการทำ inheritance แบบ ECMAScript เราเรียกเป็นศัพท์เฉพาะว่า prototypal inheritance ซึ่งกุญแจสำคัญของการทำ inheritance แบบนี้ก็คือการไม่ใช้ class ดังนั้นจึงไม่มีการ inherit ผ่านแนวคิดของ class เลย พื้นฐานของแนวคิดนี้อยู่ที่การ inherit ในระดับ object เป็นหลัก ลองดูตัวอย่างนี้

มาดู object somchai กันก่อน somchai ก็เป็น object ธรรมดาไม่มีสาระอะไรมาก somjit ก็เป็น object อยากจะ inherit มาจาก somchai พูดง่ายๆ ก็คือกำหนดให้ somchai เป็น prototype ของ somjit แค่นี้นั่นเอง หรืออาจจะเขียนแบบนี้ก็ได้ครับ

เราสามารถประยุกต์ใช้ Object.create() ที่มีมาใน ES5 เพื่อเขียนให้กระชับขึ้นได้ ดังนี้

ไม่ยากใช่ไหมครับกับการ inherit ในระดับ object พื้นฐานของมันก็มีง่ายๆ แค่นี้เองครับ

กลยุทธ์: มหาเวทดูดดาว

การ inheritance ในรูปแบบที่ผ่านมานั้นเป็นการชี้ prototype ไปยัง object ต้นฉบับ ซึ่งมีข้อดีคือประหยัดหน่วยความจำเพราะใช้การอ้างอิง ถ้าเทียบกับ OO ทั่วไปก็คือเป็น property แบบ shared หรือ static นั่นเอง แต่ถ้าพิจารณาในมุมกลับ การใช้ร่วมกันในลักษณะนี้ทำให้เสียอิสระ เสียความยืดหยุ่น ถ้ามีการแก้ที่หนึ่งก็จะเกิดผลข้างเคียงกระทบไปยัง object อื่นๆ อย่างหลีกเลี่ยงไม่ได้ ลองดูตัวอย่างนี้ที่คล้ายกับ gotcha! ในต้นของบทความนี้ครับ

ถ้าใน base มีไว้อ่านอย่างเดียวก็คงไม่เป็นปัญหา แต่ถ้าแก้ไขค่าด้วย มันก็ย่อมส่งผลมาถึง derive ด้วย ทั้งนี้ก็เนื่องจากแนวคิด “แชร์” นั่นเอง

ถ้าเราไม่ชอบแบบผลข้างเคียงแบบนี้ เรามีทางเลือกอีกทางคือแทนที่จะเชื่อม prototype เราใช้วิธีการดูด properties จาก object อื่นมาเป็นของตัวเองเลย เราจะปรับโด้ดเป็นดังนี้ครับ

extend() ไม่ใช้ฟังก์ชันมาตรฐานนะครับ  เราต้องเขียนเองโดยยึดหลัก มหาเวทดูดดาว  แตะโดนเป็นถูกดูดพลังภายในทั้งหมดมาเป็นของตัวเอง วิทยายุทธนี้เขียนเป็นภาษาคอมพิวเตอร์ได้ง่ายๆ ดังนี้

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

มหาเวทดูดดาวนี้ยังไม่ครบถ้วนนะครับ เพราะการดูดพลังภายในนั้นดูดได้แค่ชั้นเดียว ดังนั้นถ้า property ใดเป็นตัวชี้ไปยัง object อื่น มันก็ไม่ได้ดูด object นั้นมาเป็นตัวใหม่นะครับ object นั้นคงยังใช้ร่วมกันอยู่ ในวงการคอมพิวเตอร์เราเรียกการคัดลอกในระดับผิวชั้นเดียวแบบนี้ว่า การคัดลอกแบบตื้นเขิน (shallow copy) ถ้าต้องการดูดหมดเอาให้เกลี้ยงต้องเป็นแบบลึกซึ้ง (deep copy) ดังโค้ดนี้ครับ

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

กลยุทธ์: แม่น้ำร้อยสาย

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

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

กลยุทธ์: ตัดต่อพันธุกรรม

ในแนวทางของการ inheritance แบบร่วมสมัยนี้ เราไม่จำเป็นดูดมาหมดแบบยกเข่งอย่างที่เคยๆ ทำกันมา ชอบอะไรก็ค่อยดึงเอามา เอามาเฉพาะที่ชอบ มาลองดูวิธีการยืมพื้นฐานที่ผมเคยนำเสนอมาก่อนแล้วดังนี้

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

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

Polymorphism

แนวคิดของ Polymorphism ใน ECMAScript นั้นแตกต่างจาก OO ทั่วไปค่อนข้างมาก เนื่องจากไม่มีแนวคิดของ abstract class หรือ interface ดังนั้นมันจึงมีแนวทางเฉพาะตัวในการทำ polymorphism ซึ่งจะใช้หลักการทำ polymorphism แบบเป็ดๆ (duck type) ซึ่งผมขอยกยอดเรื่องนี้ไปคุยใน OO ตามแบบฉบับฉบับ Python ซึ่งแนวคิดคล้ายๆ กัน ลองติดตามอ่านกันครับ

ES6: inheritance

อันนี้ก็คล้ายๆ OO ที่เราคุ้นเคยอยู่แล้ว ผมคงไม่ต้องอธิบายมากนะครับ แต่สิ่งที่น่าสังเกตก็คือ แนวคิดของการ inheritance นั้น ใส้ในมันก็คือการทำ prototypal inheritance นั่นเอง ลองดูโค้ดนี้

ดังนั้นถ้าท่านต้องการทำความเข้าใจของ inheritance ใน ES6 ให้ถ่องแท้ ท่านก็ต้องทำความเข้าใจเนื้อหาของบทความนี้ทั้งหมดครับ มิฉะนั้นอาจเจอ gotcha! ที่นึกไม่ถึงครับ

TS: Inheritance / Interface

TS จะต่างกับ ES6 อย่างชัดเจนตรงที่ TS สามารถนิยามตัวแปรใน class ได้โดยตรง กำหนดได้เลยว่าเป็น public/prototected/private ไม่ต้องไปนิยามตัวแปรใน constructor block และที่สำคัญ TS ยังรองรับ interface อีกด้วย จากตัวอย่างจะเห็นได้ว่า TS นั้นไม่รองรับ multiple inheritance นั่นก็เหมือนกันภาษา OO ส่วนใหญ่ ทางออกของภาษาเหล่านั้นก็ใช้แนวคิดของ interface แน่นอนครับดังที่ท่านเห็น TS รองรับแนวคิดของ interface

เฉกเช่นเดียวกัน interface ในภาษาอื่น interface สามารถ inherit ได้ ดังนี้

หรือเราอาจจะสร้างตัวแปรเป็นแบบ interface แล้วบังคับให้มอง object ผ่านมุมมอง interface นั้นก็ได้เช่นกัน ดังนี้

จะเห็นได้ว่าโครงสร้างทางภาษาค่อนข้างสมบูรณ์ รองรับกระบวนทัศน์ interface-based programming ได้ แต่เนื้อหาในส่วนนี้ผมขอละเอาไว้นะครับ ไปอ่านจากบทความเก่าๆ ของผมเมื่อสิบปีที่แล้วได้ครับ

แต่ถ้าท่านไปแนวของ TS กลยุทธ์ทั้งหลายที่ผมนำเสนอไปจะถูก TS บล๊อกเอาไว้ แนวคิด TS จะยึดแนวคิดแบบ static-typed เป็นหลักซึ่งตรงข้ามกับ ECMAScript ที่ยึด dynamic-typed เป็นสรณะ

ทิ้งท้าย

ES5 นั้นใช้แนวคิด prototypal inheritance ซึ่งก็ได้รับสืบทอดมาเป็น ES6 แม้ว่าจะมีแนวคิดของ class เพิ่มขึ้นมาก็ตาม แต่ TS นั้นดัดแนวคิดให้ให้กลายเป็นแบบ OO ที่เราคุ้นเคย หรือเรียกว่าแบบ classic inheritance ซึ่งการ inheritance ทั้งสองแบบนี้มีข้อดีข้อเสียด้วยกันทั้งคู่ สุดแต่ท่านจะเลือกใช้ แต่มีจุดหนึ่งน่าสนใจ ถ้าท่านยึดหลัก prototypal inheritance ท่านสามารถดัดโค้ดเล็กน้อย ปรับเปลี่ยนโครงสร้างข้อมูลภายในอีกหน่อย ท่านจะได้ความสามารถของ multiple inheritance ถ้ายังไงถ้าท่านนึกสนุก ก็ฝากเป็นการบ้านให้ลองหาวิธีดูครับ

 

[Total: 1    Average: 5/5]

You may also like...

Leave a Reply

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