JavaScript #02: ในมิติ Imperative Programming

เกริ่นนำ

บทความนี้เป็นบทความแรกในชุด ECMAScript ซึ่งผมตั้งเป้าเอาไว้สำหรับผู้อ่านที่ไม่เคยเขียนภาษา ECMAScript มาก่อน แต่ก็พอมีความรู้ในการเขียนโปรแกรมตระกูล C มาก่อน จะเป็น C/C++/Java/C# ก็ได้ ไม่จำเป็นต้องรู้ภาษาเหล่านี้ลึกซึ่ง แค่พออ่านออกเขียนได้ก็พอ เนื้อหาในบทนี้จะพิจารณา ECMAScript เฉพาะในมุมมอง imperative หรือ Procedural Programming ซึ่งถือว่าเป็นแบบพื้นฐานที่สุด ทุกคนที่หัดเขียนโปรแกรมก็น่าจะผ่านแนวทางการเขียนโปรแกรมแบบนี้มาแล้วทั้งนั้น

แนวทางการนำเสนอของผมในบทความนี้ คงไม่ใช่เขียนแบบอ้างอิง (reference) นะครับ มีเว็บหลายเว็บที่ทำ reference ดีๆ อยู่แล้ว ที่ผมชอบมากก็เช่นที่ http://www.w3schools.com/js/  เว็บนี้นอกจากเป็น reference ที่ดีแล้ว ยังเป็น tutorial ที่ใช้ได้อีกด้วย ผมเลยจะใช้วิธี link เข้าหาเว็บนี้ในแต่ละส่วน แทนที่จะเขียน reference เอง และเพื่อไม่ให้สิ่งที่ผมเขียนออกอ่าวเกินไปนัก ผมก็จะสอบทานกับตัวมาตรฐาน ECMA-262 ไปด้วยครับ

พื้นฐาน ECMAScript

ECMAScript ชื่อก็บอกแล้วนะครับว่าเป็นภาษาแนว script ธรรมชาติของภาษาแนว script นี้ ก็เน้นสั้นกระชับ ภาษานี้ได้แนวคิดหลักมาจากภาษา C และยังเป็นภาษา script อีกด้วย ดังนั้นจึงไม่ต้องแปลกใจในความสั้น กระชับ ใครมาจากภาษา C ก็น่าจะชอบนะครับ มาลองดู Hello, World กันก่อนครับ

บรรทัดเดียวเสร็จเลยครับ ไม่ต้องรำมวยนาน อยากให้สังเกต ; ท้ายสุดนะครับ ภาษานี้จะใส่หรือไม่ใส่ก็ได้ ใส่บ้างไม่ใส่บ้างก็ได้เช่นกัน

; มีเอาไว้แยกคำสั่ง ถ้าคำสั่งละบรรทัด ก็ไม่ต้องใช้ แต่ถ้าอยากเขียนบรรทัดเดียวกัน ก็ทำได้ ดังนี้

วิธีนี้ไม่ดี bad practice ควรหลีกเลี่ยง

flow control

flow ของโปรแกรมนี่แทบจะลอกภาษา C มาเลยครับ ลองดูตัวอย่างนี้ครับ

ถ้าไม่นับคำสั่งในการแสดงผลแล้ว ที่เหลือก็แทบจะเหมือน C เลยครับ ต่างกันแค่ตอนนิยามตัวแปร ถ้าเป็นภาษา C นั้นต้องระบุชนิดของตัวแปร เช่น int i = 0 เป็นต้น แต่ ECMAScript นั้นไม่กำหนดชนิดตัวแปรครับ ไม่ว่าตัวแปรชนิดอะไรก็ตาม จะนิยามโดยใช้ var ทั้งหมด ดังตัวอย่างข้างต้น var i = 0 ซึ่งเวลาทำงานมันจะรู้ชนิดของตัวแปรจากค่าที่กำหนดเข้ามาจากด้านขวาของเครื่องหมาย = และเมื่อกำหนดค่าใหม่ ก็ไม่จำเป็นต้องเป็นชนิดเดิม เปลี่ยนชนิดตัวแปรได้ตลอดเวลา

คำสั่ง if ก็เหมือนกับ C แต่ไม่มี elseif เขียนติดกัน ต้องเขียนแยก switch-case ternary while do-while ก็เหมือนภาษา C ทั้งสิ้น

Gotcha: semicolon

ผมเคยอ่านหนังสืออยู่เล่มหนึ่งชื่อว่า Verilog and SystemVerilog Gotchas: 101 Common Coding Errors and How to Avoid Them แล้วชอบมาก ชอบที่วิธีการเขียนที่แตกต่าง หนังสือเล่มนี้ใช้วิธีการแสดงโปรแกรมตัวอย่าง จากนั้นให้เราเดาผลลัพธ์ว่าจะได้อะไร ซึ่งเมื่อเทียบกับคำตอบที่ได้จริง มักทำให้เราอุทานว่า “เฮ้ย!” มันได้อย่างงั้นได้ยังไงฟะ แต่สำเนียงฝรั่งที่กินขนมปังกินชีสมากๆ จะอุทานไม่เหมือนเรา เขาอุทานว่า ‘gotcha!’ หนังสือเล่มดังกล่าวทำให้ผมเรียนรู้ความเป็นตัวตนของได้ลึกขึ้นและรวดเร็วขึ้น ผมก็เลยคิดว่าจะเอาวิธีเดียวกันนี้มาใช้กับบทความชุด ECMAScript เพราะมันมีอะไรที่ gotcha! อยู่ไม่น้อย เรามาเริ่มที่ gotcha แรกกันดีกว่าครับ 

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

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

คำตอบที่ได้คือ undefined ครับ ทำไม? ใครเดาเอาว่าปัญหาอยู่ที่ฟังก์ชัน ก็ผิดครับ ไม่เกี่ยวเลย ปัญหามันอยู่ที่ semicolon อย่างที่ผมจั่วหัวนั่นแหละ มันยังไงกันนะ

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

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

ส่วนในโค้ดเดิมที่ตอนที่นิยาม a นั้นไม่มีปัญหา ก็เพราะในบรรทัดแรกมันคาเครื่องหมาย = เอาไว้  var a =  ระบบจะลองเอา ; เติมเข้าไป แต่ก็ไม่ทำให้ประโยคสมบูรณ์ มันจึงตัดทิ้งและอ่านต่อ  ถ้าอยากให้มันมีปัญหา ลองย้าย = ไปอีกบรรทัดหนึ่งดูครับ ดังนั้นในบรรทัดแรกจะเหลือเพียง var a  เมื่อเติม ; เข้าไป จะกลายเป็นประโยคสมบูรณ์

เหตุการณ์นี้ไม่เกิดกับภาษาตระกูล C อื่นนะครับ เพราะภาษาตระกูล C โดยปกติแล้วจะเป็นลักษณะ free form คือประกันได้ว่าเขียนบรรทัดเดียวกัน หรือคนละบรรทัด  เว้นมากเว้นน้อย ได้ทั้งนั้น ผลลัพธ์ไม่แตกต่างกัน มันจะอ่านไปเรื่อยๆ จนเจอ ; เป็นอันรู้ว่าจบคำสั่ง แต่ ECMAScript นั้น เป็น free form เหมือนกัน ที่มีข้อยกเว้นเล็กน้อย ดังที่กล่าวไว้ข้างต้น

ทางแก้ก็คือเราต้องไม่ทำให้บรรทัดนั้นสมบูรณ์ครับ โดยการย้ายเอา { ไปต่อท้ายคำสั่ง จากตัวอย่างแรกข้างบนทั้ง for() และ if() จะเห็นว่าผมใช้วิธีการเยื้องไม่ตรงกัน รูปแบบนี้เรียกว่า Indian hill ซึ่งเป็นรูปแบบที่ใช้ในภาษา C มาตั้งแต่ยุคแรก ระยะหลังมานี้มีการใช้อีกรูปแบบหนึ่งนั่นคือแบบ สมมาตร (symmetry) ภาษาอย่าง C# จะใช้รูปแบบที่ว่านี้เป็นมาตรฐาน แต่สำหรับ ECMAScript แล้ว ขอให้ใช้แบบ Indian hill เรามาดูกันว่า Indian hill นั้นแก้ปัญหานี้ได้อย่างไร

ลองดูบรรทัดที่ 2 ที่เป็นบรรทัด return จะเห็นว่า เมื่อจบบรรทัด ระบบมันก็จะลองใส่ ; ดู ปรากฏว่า ใส่เข้าไปแล้วไม่สำเร็จ เกิดวงเล็บปีกกาเปิดค้างอยู่ คำสั่งไม่สมบูรณ์ ระบบเลยถอดเอา ; ออก แล้วอ่านบรรทัดต่อไป เช่นเดิมครับ บรรทัดที่ 3 อ่านเก็บเพิ่มเติม แต่เมื่อลองใส่ ; ดู ก็ปรากฏว่ายังไม่สมบูรณ์ก็ถอดทิ้ง ; ทิ้ง อ่านบรรทัดที่ 4 ต่อไป ในบรรทัดที่ 4 นี้เราใส่ ; ในตอนจบ ระบบก็จะไม่ใส่เพิ่มให้อีก และเมื่อเป็นวงเล็บปีกกาปิด คำสั่งก็สมบูรณ์ ถือว่าจบคำสั่ง  ซึ่งในบรรทัดที่ 4 นี้เอง จะใส่ ; หรือไม่ใส่ก็ได้ เพราะถ้าไม่ใส่ ระบบจะทดลองเติมให้ (มันก็ทดลองทุกบรรทัดที่เราไม่ใส่อยู่แล้ว)

ดังนั้นเราจึงใช้ Indian hill ในการจัดรูปแบบของตัวโค๊ดภาษา ECMAScript ในทุกๆ การใช้ { } ปํญหา ; ก็จะหมดไป

ชนิดตัวแปร

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

ชนิดตัวแปรภาษา ECMAScript นั้นเรียบง่ายครับ มีไม่กี่ชนิดเอง จึงเรียนรู้ได้อย่างรวดเร็ว ECMA แบ่งชนิดตัวออกเป็นสองกลุ่มคือ  primitive และ object ดังนี้ครับ

ES Typing

 

ก่อนที่เราจะลงรายละเอียดของตัวแปรแต่ละชนิด ผมว่าเรามาคุยกันเรื่องพื้นฐานของชนิดตัวแปรก่อนจะดีกว่า จะได้เข้าใจตัวตนของมัน ใน ECMAScript นั้น ใช้ระบบชนิดตัวแปรที่เรียกว่า dynamic typing อธิบายเป็นภาษาไทยง่ายๆ ได้ว่า ชนิดของตัวเปรแต่ละตัวนั้น ตัว runtime จะรู้เวลารัน ภาษา script ส่วนมากจะใช้ระบบชนิดตัวแปรแบบนี้ เช่น Python หรือ Ruby เป็นต้น ระบบชนิดตัวแปรแบบนี้แตกต่างกับ ภาษาหลักๆ อย่างเช่น C, C++, Java, C# ที่ใช้ระบบชนิดตัวแปรแบบ static typing ระบบนี้จะกำหนดชนิดตัวแปรเอาไว้ตั้งแต่ตอนเขียนโปรแกรม และสามารถตรวจสอบได้เวลาคอมไพล์เลย เรามาลองดูตัวอย่างเปรียบเทียบดังนี้ครับ

ตัวอย่างข้อบนนี้เป็น ECMAScript ซึ่งเป็นแบบ dynamic typing ให้ท่านสังเกตการนิยามตัวแปร v1 ในบรรทัดแรก ถามว่าเวลาคอมไพล์รู้ชนิดตัวแปรหรือยังครับ มันยังไม่รู้นะครับ ชนิดตัวแปรจะไปรู้เอาตอนรันในบรรทัดที่ 2 เมื่อเรากำหนดค่า 15 เข้าไป มันจะรู้เลยว่าอยากให้เป็นตัวตัวเลข ดังนั้นเมื่อแสดงชนิดตัวแปรในบรรทัดที่ 3 ก็ได้เป็นตัวเลข

แต่ที่น่าสนใจคือบรรทัดที่ 4 และ 5 ครับ เราเปลี่ยนใจ ไปกำหนดค่าเป็น string จากเดิมที่เป็น number เรื่องนี้ไม่ใช่เรื่องของ dynamic typing โดยตรงนะครับ แต่ภาษา script ส่วนมากก็จะรองรับการเปลี่ยนชนิดระหว่างทำงานแบบนี้ได้ ซึ่งแน่นอนครับ ทางค่าย Microsoft มองว่านี่ไม่ใช่ความสามารถ แต่เป็นเรื่องอันตรายที่ต้องหลีกเลี่ยง  ตัวแปรสร้างมาเป็นชนิดอะไร ก็ต้องเป็นชนิดนั้นไปจนหมด scope ดังนั้นถ้าเขียนโปรแกรมเดียวกันนี้ด้วย TypeScript จะได้ผลลัพธ์ดังนี้ครับ

โปรแกรมนี้ทรานไพล์ไม่ผ่านนะครับ ติดบรรทัดที่ 4 แสดงข้อความข้อผิดพลาดว่า “error TS2011: Cannot convert ‘string’ to ‘number’.”  ต่อให้เปลี่ยน ‘Hello, World’ เป็น ’20’ ก็ไม่ผ่านอยู่ดีครับ ในบรรทัด ที่ 1 มีการกำหนดชนิด v1 เอาไว้ บอกว่าเป็นตัวเลขเท่านั้น ดังนั้นเมื่อกำหนดค่า string ใส่เข้าไปใน v1 ที่บรรทัดที่ 4 จึงไปไม่รอดครับ ถ้าตัดบรรทัดที่ 4, 5 ออกไป ก็จะผ่าน ไม่มีปัญหา นี่แหละครับ static typing จะรู้ชนิดเวลาคอมไพล์/ทรานไพล์ และเป็นคุณสมบัติหลักเลยก็คือ เมื่อกำหนดชนิดแล้วเปลี่ยนไปไม่ได้ มิฉะนั้นก็ไม่รู้ว่าจะกำหนดชนิดบังคับไปเพื่ออะไร

static typing ว่าไปแล้วก็ดูเคร่งเกิดไป ใช้แล้วอาจจะอึดอัด ดังนั้น ภาษาที่ใช้แนวคิดนี้จึงหาทางทำให้ยืดหยุ่นขึ้นโดยให้สามารถเปลี่ยนชนิดได้บ้างโดยยอมให้เปลี่ยนชนิดอยู่ในวงแคบๆ ควบคุมได้ ผลลัพธ์จึงออกมาเป็นแนวคิด polymorphism และ generics programming ในที่สุด ซึ่งผมจะอธิบายต่อไปในส่วนของ TypeScript

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

หมายเหตุ: ชนิดตัวแปร any ไม่มีในภาษา ECMAScript นะครับ ทาง TypeScript กำหนดขึ้นมาเพื่อให้เข้ากันได้กับภาษา ECMAScript เดิมนั่นเอง

ชนิดตัวแปรกลุ่ม Primitive

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

primitive: number

เป็นตัวเลข double-precision 64 bits ซึ่งเทียบกับภาษาอื่นก็คือ double นั่นเอง ใช้ตามมาตรฐาน IEEE 754 ดังนั้นจึงมีค่า NaN, Infinity และ -Infinity ได้ ดังนี้

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

.toFixed() นั้นจะได้คำตอบออกมาเป็น String ด้งนั้นจึงต้องเอา Number() มาคร่อมเพื่อแปลงกลับเป็นตัวเลข ส่วนค่าพารามิเตอร์ 0 ที่ใส่นั้นคือจำนวนหลังจุดทศนิยมที่ต้องการ ใช้หลักการการปัดที่เลข 5 ตามปกติ ในเมื่อเป็น 0 ไม่ใส่ก็ได้ผลลัพธ์ไม่ต่างกันครับ

หรืออาจจะใช้ parseInt() ก็ได้เช่น

ผลลัพธ์จะแต่ต่างจาก Number() ตรงที่มันตัดเศษทิ้งครับกลายเป็นตัวเลขจำนวนเต็ม ใน parseInt() ยังรองรับเลขฐานอื่นด้วยเช่น

แต่ถ้าเราไม่ระบุ 0x ข้างหน้า เราเขียนอย่างนี้ก็ได้ครับ

หรือถ้าต้องการแปลงอย่างง่ายสะดวกรวดเร็วใช้ + ก็ได้ครับ ดังนี้

ซึ่งวิธีนี้เหมาะใช้เป็นส่วนหนึ่งของเงื่อนไขเช่น

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

จริงๆ แล้ว opearators ที่ใช้ในการคำนวณค่า พวก + – * / % ++ —  นั้นเหมือนภาษา C และมี precedence เหมือนกันเลยครับ ดังนั้นจึงไม่ต้องพูดอะไรมาก รายละเอียดได้ได้จาก ที่นี่

เลขจำนวนเต็ม

ถ้าใช้ตัวแปลชนิด number เก็บเลขจำนวนเต็ม จะสามารถใช้งานได้ถึง 53 bits ไม่รวมเครื่องหมาย นั่นก็คือสามารถนำเสนอตัวเลขได้ระหว่าง -2^53 ถึง 2^53 เลยทีเดียว ซึ่งระบบตัวเลขจะเปลี่ยนเป็นระบบจำนวนเต็ม ไม่ใช่ IEEE-754 นะครับ

เพียงพอสำหรับงานที่ใช้ตัวเลขขนาดใหญ่ทั่วไปได้อย่างสบาย ส่วนถ้าไปใช้กับงานบางประเภทเช่น การเข้ารหัสที่ต้องใช้ bitwise operators เช่น & | ~ << >> นั้น  เมื่อใช้ operator เหล่านี้ ตัวเลขก็จะกลับกลายมีขนาดเพียง 32 bits แบบ 2-complements ตามแบบ 32 bits integer ของภาษาคอมพิวเตอร์ทั่วไปโดยอัตโนมัติ ดังนั้นชนิด number จึงเป็นชนิดตัวเลขแบบครอบจักรวาล

ศึกษาเพิ่มเติม

ฟังก์ชันต่างๆ หาอ่านได้จาก Math Object ฟังก์ชันเหล่านี้ช่วยเหลือเราคำนวณด้านคณิตศาสตร์ไม่น้อยหน้าภาษาอื่น ผมขอละเอาไว้ ท่านไปอ่านรายละเอียดได้เองตาม link ที่ให้ครับ หรือจะอ่านพื้นฐานการใช้ Number ได้จากที่นี่

มีลูกเล่นเล็กน้อย ถ้าเราต้องการแปลง string ให้กลายเป็นตัวเลข แทนที่จะใช้ Number(a) เราอาจใช้ +a  แทนได้ครับ แต่ถ้า a นั้นไม่ใช่ตัวเลข ก็ไม่เกิด error แต่อย่างใดนะครับ ผลลัพธ์จะได้เป็น NaN เฉกเช่นเดียวกันกับเมื่อแปลงด้วย Number() หรือ parseInt()

primitive: string

เป็นตัวแปรที่เก็บสายวลี (ศัพท์อะไรกันนี่) หรือเรียกภาษาฝรั่งเข้าใจง่ายกว่าว่า string ซึ่งค่าคงที่ก็ตรงไปตรงมาเหมือนภาษาคอมพิวเตอร์ทั่วไป แต่ใช้ได้ทั้ง ‘ ‘ และ ” ”  และรองรับ escape sequences เหมือนภาษา C เช่นถ้าเราอยากใช้ ‘ ใน ‘ ‘  ก็สามารถใช้

ถามว่ามีให้เลือกทั้ง ‘ ‘ และ ” ” จะใช้อะไรดี ตามธรรมเนียมแล้วเราจะใช้ ‘ ‘ เป็นหลักครับ พยายามหลีกเลี่ยงการใช้ ” ” เพราะบ่อยครั้งเราต้องฝัง ECMAScript ลงใน attributes บางตัวในหน้า HTML ซึ่งโดยทั่วไปใน HTML เขาจะใช้ ” ” ครับ จะได้ไม่ไปชนกัน

นบรรทัดที่ 2 นั้น เราได้ผลลัพธ์ว่า str = ‘abcdef’  แต่มันก็ไม่ได้เอาไปต่อท้ายของเดิมนะครับ  มีการสร้าง ‘abcdef’ ขึ้นใหม่ของเดิมที่เป็น ‘abc’ ก็ยกเลิกไป ในบรรทัดที่ 3 เราดึงค่าตัวอักษรตัวที่ 2 ออกมา ภาษานี้เป็นภาษาตระกูล C จึงนับตัวแรกที่ 0

string นั้นเป็น immutable เปลี่ยนค่าไม่ได้ ดังนั้นในบรรทัดที่ 2 แม้ว่าพยายามเปลี่ยนค่า แต่ก็ผลลัพธ์เหมือนเดิมเพราะเป็น immutable

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

ถ้าท่านมีตัวแปรชนิดอื่น ต้องการแปลงเป็น String อาจจะใช้ method หรืออาจจะใช้ String คร่อมก็ได้เช่น

ท่านศึกษาวิธีการใช้ string ได้จาก ที่นี่ และ methods ต่างๆ ได้จาก ที่นี่

primitive: boolean

ใช้เก็บค่าอยู่แค่สองค่าอย่างที่รู้กันคือ true / false เรารู้กันอยู่แล้วว่า operators ที่อยู่ในชุดการเปรียบเทียบนั้น เช่น > >= เป็นต้น ผลลัพธ์จะให้ค่าความจริงออกมาว่า true / false ซึ่งก็ไม่ได้แตกต่างจากภาษา C เลย แต่ภาษานี้มี operator ชวนฉงนก็ตอนที่เปรียบเทียบความเท่ากันหรือไม่เท่านี่แหละครับ มีตัวแปรียบเทียบเหมือนกับภาษา C คือ == และ !=  แต่ภาษานี้มีเพิ่มเติมคือ === และ !== แล้วมันต่างกันอย่างไร

ถ้าเราใช้ == หรือ !=  ตัว runtime จะพยายามแปลงตัวแปรให้เป็นชนิดเดียวกันก่อนค่อยเปรียบเทียบ เช่น

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

boolean นั้นเหมือนภาษา C ครับ ถ้าเทียบกับตัวเลขแล้ว จะได้ค่า 0 นอกนั้นค่าอื่นมีค่าเป็น true ครับ

primitive: null และ undefined

สองชนิดนี้ใช้ง่ายมาก ไม่มี method อะไรให้ยุ่งยาก แต่คนมักสับสน เมื่อไหร่จะใช้ตัวไหน ง่ายๆ ครับ undefined เราไม่ได้กำหนดค่า แต่ null เราเป็นคนกำหนดค่า (สังเกตว่าเป็นอักษรตัวเล็กทั้งหมด)

undefined นั้นหมายความว่า ตัวแปรตัวนั้นนิยามมาแล้ว แต่ยังไม่ได้กำหนดค่าเช่น

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

ส่วน null นั้นเราตั้งใจกำหนดค่าให้แก่ตัวแปร กำหนดให้มันไม่มีค่า จะไม่มีเพศ ไม่มีราคา ไม่ระบุ หรือไม่ได้ชี้ไปยัง object ใด

ถ้าเราเอา ฺBoolean() คร่อม null หรือ undefined จะได้ผลลัพธ์เป็น false  และเมื่อเอา Number() คร่อม ก็จะได้ 0

ชนิดตัวแปรกลุ่ม Object

ในกลุ่มนี้ ก็จะเป็นกลุ่มที่เก็บ object เหมือนกับภาษาทั่วๆ ไป มีตัวชี้ (pointer) และ ตัว object ที่ชี้ไป ซึ่ง object โดยพื้นฐานแล้วจะประกอบด้วยของสองสิ่งนั่นคือ

  • properties
  • methods

และโดยปกติแล้วเราจะใช้ new operator เพื่อสร้าง object ใหม่ ที่น่าสนใจก็คือเมื่อเราสร้าง object มาแล้ว เราอาจสร้าง properties/methods เพิ่มให้แก่ object นั้นได้เช่น

หนังสือแต่ละเล่มก็นิยามชื่อของตัวแปรกลุ่มนี้ไม่เหมือนกัน เช่นทาง Microsoft เรียกกลุ่มนี้ว่า complex types แต่ผมขอยึดเอาตามนิยามของ ECMA-262 ซึ่งอาจจะดูงงหน่อย แต่ก็ถือว่าเป็นมาตรฐานที่สุด

ตัวแปรในกลุ่มนี้  เราไม่สามารถใช้ typeof มาตรวจสอบได้ (ยกเว้น function) มันจะบอกเป็น object ทั้งหมด ไม่สามารถแยกเป็นชนิดย่อยได้ เราต้องใช้ operator instanceof มาใช้ รายละเอียดการใช้งานจะแสดงให้ดูในแต่ละชนิดครับ อย่างเช่นจากตัวอย่างล่าสุด ถ้าใช้

Object: Array

จากตัวอย่างข้างบน ในบรรทัดแรกเป็นการสร้าง array เปล่าครับ จากนั้นก็บรรจุค่าเข้าไป มีข้อสังเกตหลายประการดังนี้

  • index ของ array คือ 0  ตามแบบภาษา C
  • เราไม่ต้องจองขนาดของ array ล่วงหน้า มันจะใหญ่ขึ้นได้ตามข้อมูลที่เราใส่
  • ขนาดของ array จะใหญ่เท่ากับเลข index ที่สูงที่สุดที่เราใช้
  • index ที่เราข้าม ค่าจะเป็น undefined
  • ชนิดของตัวแปร ไม่จำกัดผสมอย่างไรก็ได้ แม้กระทั่งเอา object อื่นซ้อนเข้าไปก็ยังได้ ผมว่าน่าจะเรียกว่า list มากกว่า array

ถ้าเราต้องการบรรจุข้อมูลเข้าไปใน array แต่ไม่ต้องการวุ่นวายยุ่งกับ เลข index เอง เราอาจใช้ method push() ได้ดังนี้

ในบรรทัดแรก เป็นการสร้าง array เปล่าแบบเขียนสั้นๆ ผลลัพธ์ไม่ต่างกับ var a = new Array(); ครับ คนส่วนมากชอบวิธีสั้นๆ มากกว่า ซึ่งผมก็จะใช้วิธีเขียนสั้นๆ นี้ไปตลอด

ส่วน method push() นั้น ระบบจะไล่ดูเองครับว่าค่าใดเป็นค่าล่าสุดและจะเพิ่มค่าและต่อให้เอง สะดวกกว่ามาก ปกติแล้วเราจะไม่นับเองให้เสี่ยงต่อความผิดพลาดครับ และในบรรทัดสุดท้ายเป็นการใช้ instanceof เพื่อตรวจสอบว่า a นั้นเป็น Array หรือไม่

ถ้าสี่บรรทัดข้างบนยังไม่สั้นพอ เราอาจจะยุบให้เหลือบรรทัดเดียวได้โดย

ส่วนการดึงข้อมูลนั้นนอกจาก การดึงเป็นราย index ที่ระบุแล้ว เรามักจะวนรอบเพื่อดึงค่าทั้งหมด ซึ่งมีหลายวิธีครับ เอาวิธีที่ง่ายที่สุด ดังนี้ครับ

คำสั่งนี้เป็นแบบเดียวกันกับภาษา C ถ้าท่านไม่คุ้นเคย ลองหาข้อมูลเพิ่มเติมเองนะครับ ผมขอละเอาไว้ เรามาดู property length ซึ่งตรงไปตรงมา จะให้ค่าจำนวนข้อมูลใน array นั้น มีข้อน่าพึงสังเกตว่า วิธีนี้ไม่ค่อยดีนัก เนื่องจาก ในการวนรอบทุกรอบนั้นก็จะต้องหาค่า property length ทุกครั้ง ซึ่งเสียเวลา เราอาจจะปรับให้ดีขึ้นได้เป็น

วิธีนี้จะไปเรียกใช้ proprty length เพียงครั้งเดียว แล้วไปเก็บไว้ในตัวแปร max จากนั้นก็ใช้ตัวแปร max ในการเปรียบเทียบตลอด ซึ่งทำงานเร็วกว่า

หรือเราอาจใช้ functional programming ก็ได้เช่นกัน ดังนี้

ผมใช้ method forEach() ซึ่งรับ parameter เป็นฟังก์ชัน ซึ่งเราจะเห็นได้ว่า เราสามารถส่งฟังก์ชันเป็นพารามิเตอร์ของฟังก์ชันอื่นได้ ซึ่งรายละเอียดจะอยู่ในบทความต่อไปครับ ในส่วนนี้ผมขอข้ามไปก่อน

รายละเอียดการใช้งาน array อ่านเพิ่มเติมได้ที่ http://www.w3schools.com/js/js_arrays.asp และลงรายละเอียดได้ที่ http://www.w3schools.com/jsref/jsref_obj_array.asp

ES6: for .. of

ใน ES6 มีการเพิ่มคำสั่งให้เราใช้งานง่ายขึ้น เร็วและไม่ต้องยุ่งกับ index ดังนี้

TS: ชนิดตัวแปร Array

TS เน้นให้เรากำหนดชนิดตัวแปร ตัวแปรชนิด Array ของตัวเลข กำหนดเป็น number[] ดังนี้

แน่นอนครับว่าเมื่อเราบังคับชนิดตัวแปรเป็น number[] ค่าใน array ก็ต้องเป็นตัวเลขทั้งหมด ผสมอย่างอื่นไม่ได้ ยกเว้นเราจะนิยามให้มันเป็นชนิด any[]

ใน TypeScript นี้เราอาจนิยาม Array โดยใช้ generics ก็ได้ โดยการเปลี่ยนบรรทัดแรกให้เป็น

ชอบแบบไหนก็ตามอัธยาศัยครับ

TS: ชนิดตัวแปร enum

TS เพิ่มชนิด enum ให้เราใช้งานง่ายๆ ดังนี้

ผมคงไม่อธิบายนะครับว่า enum คืออะไร ท่านรู้ดีอยู่แล้ว ในกรณีข้างบน ถ้าท่านไม่กำหนดค่าให้แก่ element แต่ละตัวใน enum มันจะเป็นตัวเลขไล่ตั้งแต่ 0   ซึ่งหลักการเหมือนกับภาษา C นั่นเอง ถ้าท่านกำหนด action เป็น 20 และไม่กำหนดค่าให้กับตัวอื่น Comedy จะเป็น 21 และไล่เลขเรียงลงมาเหมือนภาษา C

เวลาตอนใช้งาน ค่า enum จาก element แต่ละตัวจะเป็นตามชนิดที่เราระบุเอาไว้ (แน่นอน ถ้าไม่ใส่ก็จะเป็นตัวเลขดังที่กล่าวไว้ข้างต้น)

Object: Object

ในบรรทัดแรกนั้น เราสร้าง Object เปล่าขึ้นมา โดยใช้ obj ชี้ไปยัง Object ที่สร้างขึ้นมาใหม่ สิ่งที่ภาษาที่เป็น dynamic typing มักมีร่วมกันก็คือ เราสามารถเพิ่มเติม properties และ methods เข้าไปหลังจากที่สร้าง object เสร็จแล้ว

ดูจาก code ดูเหมือนว่าไม่มีอะไรในก่อไผ่ แต่ถ้าคิดให้ดีแล้วมันก็มีประเด็นที่น่าสนใจครับ ประเด็นที่สำคัญเลยก็คือ เราสร้าง Object ได้โดยไม่ต้องอาศัย class ครับ ใน ES5 ไม่มีแนวคิดของเรื่อง class เลย อยากเติมอะไรเข้าไปใน object ก็เติมเลยครับ ดังนั้นภาษา ECMAScript จึงไม่อาจพูดได้ว่าเป็น object-oriented ถ้าว่ากันให้ถูกแล้วมันก็เป็น object-based รายละเอียดเรื่องนี้เอาไว้คุยกันในบทความเฉพาะเรื่อง object-based ดีกว่าครับ

ผมขอนำท่านมาดูโครงสร้างข้อมูลอีกตัวหนึ่งครับ เป็นแบบ dictionary ลองดูตัวอย่างดังนี้

dictionary นั้น ประกอบด้วยข้อมูลสองส่วนคือ key และ value  ในส่วนของ key นั้น ในที่นี้มี 2 ตัวคือ  ‘name’ และ ‘age’ ซึ่งผมผูก key ‘name’ เข้ากับ value ‘somchai’ และ key ‘age’ เข้ากับ value 30  ตัว dictionary นี้เป็นโครงสร้างข้อมูลที่ค้นหาข้อมูลเร็วมากครับ โดยมากใช้หลักการ hashing ในการค้นหาข้อมูล ซึ่งแทบจะให้คำตอบทันทีที่หา เราสามารถกำหนดค่าเริ่มต้นให้แก่ dictionary  ทำให้เรายุบสามบรรทัดแรกให้เหลือเพียงบรรทัดเดียวได้ดังนี้

สังเกตว่า ตัว key นั้น ในเวลานิยามเช่นนี้ จะมี ‘ ‘ คร่อมเป็น string หรือไม่ก็ไม่มีปัญหาครับ แต่เวลาใช้งานนั้น อย่างเช่นในบรรทัดที่ 5 ต้องเอา ‘ ‘ คร่อมชื่อ key

ท่านอาจจะงงว่า ผมพูดเรื่อง Object อยู่ดีๆ ผมหักพวงมาลัยเลี้ยวหา dictionary เฉยเลย เมารึเปล่า (นั่นหนะสิ) ไม่เมาครับ ผมขอแจ้งให้ท่านทราบว่าสองเรื่องที่ผมกล่าวมานั้นคือเรื่องเดียวกัน พูดง่ายๆ Object ใน ECMAScript นั้นมีความสามารถของ dictionary อยู่ด้วย  ผมลองผสมให้ดูครับ

Object คือ dictionary และ dictionary ก็คือ Object นั่นเอง

ถ้าเราต้องการวนรอบเพื่อดึงข้อมูลทั้งหมดมาแสดง เราสามารถใช้ for … in ดังนี้

Object ของ ECMAScript นั้นสามารถส่งข้ามเครื่องไปยัง network เพื่อให้มันมีชีวิตอยู่ในเครื่องอื่นได้ การส่งข้ามเครื่องเราคุ้นเคยกันดีอยู่แล้ว เราเรียกการทำเช่นนี้ว่า serialization ที่ยอดนิยม ถือว่าเป็นรุ่นบุกเบิกเลยก็คือภาษา Java เราสามารถทำการ serialization object ให้มันกลายเป็น XML  จากนั้นก็ส่ง XML ไปยัง อีกเครื่องหนึ่ง และเมื่อได้รับแล้ว ก็ทำการ deserialization กลับมาเป็น object อีกครั้ง ด้วยแนวคิดนี้ทำให้เกิดการประยุกต์มากมาย เช่นเกิดแนวคิด web services ที่ทำให้เราเรียกใช้บริการข้ามเครื่องได้ อย่างที่เราคุ้นเคยกันดีอยู่แล้ว

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

ทางค่าย ECMAScript คิดค้นรูปแบบของข้อมูลที่ใช้ส่งข้ามเครื่อง เรียกว่า JSON (๋JavaScript Object Notation) เพื่อทดแทน XML ดังตัวอย่างนี้

จะเห็นได้ว่าเรียบง่ายกว่า XML พอสมควร และถ้าท่านสังเกตให้ดีรูปแบบ JSON ก็คือโค๊ดภาษา ECMAScript ตามปกตินั่นเอง ถ้าเราเขียน

ตัวอย่างข้างบน ผมเพียงแค่เติม var world = ในตอนต้น และเติม ; ในตอนท้าย (ไม่เติมก็ได้) เท่านี้ผมก็สามารถแปลง JSON ให้กลายเป็น ECMAScript แล้วครับ จริงๆ แล้ว ผมไม่ได้สร้าง text ข้างบนเองนะครับ ผมใช้เขียนโปรแกรมเอา ดังนี้ครับ

JSON.stringify()  ใช้แปลง Object ชนิดใดๆ ให้กลายเป็น JSON ครับ เมื่อรันโปรแกรมแล้วจะได้ผลลัพธ์ดังนี้

ถ้าท่านใช้ Brackets เป็น IDE เหมือนผม และลงโปรแกรมตามที่ผมบอก ท่านสามารถคัดลอกผลลัพธ์นี้ไปยังหน้าเปล่า แล้วเลือกเมนู Edit -> Pretty JSON เพื่อปรับข้อมูลให้สวยงามดังที่ผมแสดงให้ท่านดูในตอนแรกนั่นเอง

ในทางกลับกัน ถ้าเรามี JSON อยู่ในตัวแปร txt เราสามารถแปลงกลับมาเป็น Object ได้โดย

ง่ายๆ เท่านี้เองครับ จากตัวอย่างผมแสดงให้เห็นถึงการผสมกันระหว่าง Object และ Array ครับ

หมายเหตุ:  JSON นั้นยุ่งแต่กับข้อมูล ไม่ได้ยุ่งกับ code นะครับ ดังนั้น JSON.stringify() นั้น จะไม่สนใจในส่วนของ methods นะครับ มันจะแปลงเฉพาะ properties เท่านั้น

Date

Date นั้นตรงไปตรงมาเก็บวันเวลานั่นเอง แต่เรื่องของวันเวลานั้นไม่ง่ายอย่างที่มือใหม่คิดครับ เอาเรื่องที่เกิดขึ้นล่าสุด แผ่นดินไหวรุนแรง 7.8 แมกนิจูด วันที่ 25 เมษายน 2015 เวลา 13.11 น. ถ้าท่านมีเพื่อนอยู่อเมริกา แล้วบอกว่าเหตุเกิดเวลา 13.11 น. รับรองได้ว่าเข้าใจกันคลาดเคลื่อนแน่ และถ้าท่านไม่รู้ว่าเพื่อนท่านอยู่รัฐไหนก็ไม่อาจจะคำนวณให้ถูกต้องได้ครับ เพราะลำพังสหรัฐอเมริกาเองก็มีเวลาถึง 6 timezones แต่ประเด็นมันอยู่ที่ว่า ไม่ว่าท่านจะอยู่ส่วนไหนของโลกก็ตาม เวลาที่เกิดแผ่นดินไหวนั้นเป็นเวลาเดียวกัน เพียงแค่แต่ละส่วนของโลกเรียกเป็นเวลาต่างกันเท่านั้นเอง

ดังนั้นจึงมีการคิดเวลามาตรฐานขึ้น ไม่ใช่ของใหม่นะครับ มันเกิดมาพร้อมกับการแบ่งเขตเวลาโลกแล้ว ผู้แบ่งก็ย่อมนับจุดเริ่มต้นที่ประเทศตัวเองอยู่แล้ว ผู้แบ่งก็ไม่ใช่ใครอื่นครับ ประเทศที่ตะวันไม่ตกดิน อังกฤษนั่นเอง โดยนับเวลาตั้งต้น 0 ที่ตำบล Greenwich และตั้งเป็น timezone GMT ขึ้นมา ประเทศไทยนั้น อยู่ห่างจาก GMT ไปทางขวาไปอีก 7 timezones  จึงเป็น GMT+7 หรือมีชื่อเฉพาะว่า ICT (Indochina Time)

เมื่อก่อนก็ไม่มีปัญหาครับ ประเทศใครประเทศมัน ต่างคนต่างอยู่ แต่ปัจจุบันโลกเริ่มไร้พรมแดน ไม่ต้องอื่นไกล โครงการ opensources หลายโครงการมีคนช่วยกันเขียนหลายคนที่มาจากคนละซีกโลก ไม่รู้กันด้วยซ้ำไปว่าใครอยู่ประเทศไหน ถ้าเขานัดเวลาแชทถกกัน เขาจะบอกเวลากันอย่างไร จะให้ผมบอกเวลาไทยไป คนอื่นก็ปวดหัวแปลงอีก ในทางกลับกันก็เช่นกัน ทำไปทำมาอาจจะมั่วนัดผิดเวลาก็ได้ ดังนั้นจึงมีการคิดเวลาสากลขึ้นมา ไม่ว่าท่านจะอยู่ส่วนใดของโลกก็ตาม เวลาสากลมีหนึ่งเดียว เวลานัดก็เข้าใจตรงกันครับ แล้วเวลาที่ว่านั้นไปเอามาตรฐานมาจากไหน ก็ไม่ใช่ที่ไหนหรอกครับ GMT เดิมนั่นเอง เอามาเรียกใหม่ว่าเป็น UTC (Coordinated Universal Time)  พูดง่ายๆ เอาเวลาอังกฤษมาเป็นมาตรฐานนั่นเอง เวลา ICT ของเราก็เลยเรียกได้ว่าเป็น UTC+7 นั่นเอง และลดบทบาท GMT ให้เป็นเพียงชื่อเรียก timezone อังกฤษเท่านั้นครับ

ใน ECMAScript นั้นชนิดตัวแปร Date ภายในจึงเก็บเวลาเป็น UTC นั่นเอง แต่การเก็บนั้นมันไม่ได้เก็บเป็น string นะครับ มันเก็บตัวเลขใหญ่ๆ ตัวนึง โดยใช้วิธีสร้างตัวเลขแบบนี้ครับ คือเขาตั้งต้นที่เที่ยงคืนวันปีใหม่ปี 1970 ทุกๆ มิลลิวินาทีที่ผ่านไปเขาจะเพิ่มค่าไป 1  ดังนั้น เวลาเที่ยงคืนหนึ่งวินาทีของวันดังกล่าว ตัวเลขก็จะเป็น 1000 นั่นเอง ตัวเลขเวลาก็จะสะสมตามเวลาที่เปลี่ยนไปครับ เรามาลองใช้งานดูครับ

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

ในบรรทัดที่สองนั้นเป็นตัวเลขสะสมที่มีหน่วยมิลลิวินาทีอย่างที่กล่าวเอาไว้แล้วข้างต้น พอมาบรรทัดที่ 3 ก็ให้แสดงเป็นเวลาท้องถิ่น (ประเทศไทย) และบรรทัดที่ 4 แสดงเป็น เวลา UTC

คราวนี้เรามาดู JSON บ้าง อย่างที่กล่าวไปแล้ว JSON เน้นให้มนุษย์อ่านรู้เรื่อง ดังนั้นเราอาจส่งวันเวลาข้ามไปเป็นตัวเลขอย่างในบรรทัดที่ 2 ก็ได้ แต่มนุษย์จะอ่านไม่รู้เรื่อง ดังนั้นในการส่ง JSON จึงนิยมใช้รูปแบบการบอกเวลามาตรฐาน ISO 8601 ดังสองบรรทัดข้างล่าง

ถ้าเราได้ตัวเลขเวลาสะสมมิลลิวินาทีมา เราสามารถแปลงกลับเป็นวันที่ได้โดย

หรือเราสร้างโดยใช้รูปแบบนี้ได้ครับ

methods ที่ดึงค่าหรือกำหนดค่าเฉพาะส่วน ก็มีให้ใช้ เช่น

ในทางกลับกัน ถ้าต้องการกำหนดค่าให้ใช้ ชุด set แทน

สังเกตว่า เดือนแรกนับจาก 0 นะครับ ดังนั้นเดือนกุมภาพันธ์ จะมีค่า 1 ไม่ใช่ 2

ทั้งหมดนี้ อ้างอิงเวลาเป็นเวลาท้องถิ่น ถ้าต้องการใช้งาน UTC ให้แทรก UTC เข้าไปในชื่อ methods เช่น จาก getDate(), setDate() ให้กลายเป็น getUTCDate(), setUTCDate() ตามลำดับ

เนื่องจากภายในของ Date นั้นก็คือตัวเลขตัวหนึ่ง ดังนั้นเราสามารถเอา date 2 ตัวนำเอามาลบกันได้ ผลลัพธ์จะกลายเป็นตัวเลข ที่มีหน่วยมิลลิวินาที เราเอาความรู้นี้ไปประยุกต์สร้างเป็นตัวจับเวลาได้ดังนี้

ผมต้องการเวลาที่ได้เป็นหน่วยวินาที ดังนั้นจึงต้องหารด้วย 1000 ง่ายๆ แค่นี้เอง

รายละเอียดเพิ่มเติมการใช้งาน Date อ่านได้จาก http://www.w3schools.com/js/js_dates.asp และ http://www.w3schools.com/jsref/jsref_obj_date.asp

RegExp

ชนิดตัวแปรนี้มีเอาไว้จัดการ regular expression (regexp) มีเอาไว้เพื่อค้นหาและเปลี่ยนแปลงข้อมูลใน string แต่ไม่ได้เป็นแบบตรงๆ ตรงมานะครับ เป็นแบบ patterns ซึ่งรายละเอียดเรื่องนี้คุยก็ได้เป็นบทความแยกต่างหากหลายบทเลย ในบทความนี้ผมขอนำเสนอแค่กลิ่นของมันเท่านั้น มิฉะนั้นจะยาวไม่จบครับ

จากตัวอย่างข้างบน ผมตัดข่าวหุ้นเมื่อเช้านี้ (30 Apr 15) มาจาก Bangkok Post และตั้งโจทย์เล่นๆ ว่า ผมอยากแกะตัวเลขทุกตัวออกมาจาก text ข้างบน ก็เลยเขียน code ดังที่เห็นครับ  สังเกตชนิดตัวแปร re มันเป็นชนิด RegExp ครับ

เช่นเคยครับท่านสามารถศึกษา RegExp เพิ่มเติมได้จาก http://www.w3schools.com/js/js_regexp.asp และ http://www.w3schools.com/jsref/jsref_obj_regexp.asp

Object: Error

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

ฟังก์ชัน myIO() จะทำงานได้เมื่อพารามิเตอร์ port มีค่าไม่ติดลบ ในตัวอย่างนี้ผู้เรียกใช้ ใส่ค่าติดลบเข้าไป ระบบทำงานไม่ได้แสดงข้อความผิดพลาดขึ้นมา แต่ผู้เรียกใช้ก็นิ่งเฉย ไม่อ่านค่าที่ส่งกลับมา ทำงานต่อไป ซึ่งจริงๆ แล้วควรหยุดทำงานครับ บรรทัดที่ 9 ควรเขียนดังนี้

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

เมื่อรันแล้ว โปรแกรมจะ throw (ปา) “port error” ขึ้นไปข้างบน โปรแกรมนี้ไม่มีการตั้งรับเอาไว้ ทำเป็นไม่สนใจ โปรแกรมก็แสดงข้อผิดพลาดแล้วหยุดเลยครับ  ดังนั้นจึงทำงานไมถึงบรรทัดที่แสดง ‘done’ ครับ

throw อะไรได้บ้าง

throw ได้หมด ทุกชนิดตัวแปรทั้ง primitive และ object ที่ผมกล่าวในบทความนี้ แต่ถ้าคิดจะเขียนโปรแกรมขนาดใหญ่ ก็ควรจัดการชนิดการ throw ให้เป็นระบบ ECMAScript โดยใช้ชนิดตัวแปร Error เช่น

หรือถ้าต้องการให้ละเอียดขึ้น ก็สามารถระบุชนิดย่อยได้เลย ทาง engine ของ ECMAScript แต่ละตัวก็จะมีชนิด Error ย่อยที่ทำให้ไว้แตกต่างกัน แต่ที่พื้นๆ ก็เช่น RangeError, TypeError เป็นต้น เช่น

ถ้าชนิด Error ที่ระบบเตรียมให้ไว้นั้นไม่ตรงใจ ก็สามารถสร้างชนิด Error ได้เอง ในที่นี้ผมจะสร้าง PortError ขึ้นมา ดังนี้

ถ้าจะทำความเข้าใจโค๊ดส่วนนี้ ก็ต้องเรียนรู้ความเป็น object-based ของ ECMAScript ก่อนครับ ตอนนี้ก็ใช้วิธีคัดลอกไปก่อนครับ ทำตาม code ข้างบนเราก็จะมีชนิดตัวแปร PortError มาใช้ ซึ่งก็ใช้งานได้เหมือนกันชนิดอื่นดังนี้

การดักจับ

ในบรรทัดที่ 9 เมื่อเรียกแล้วมีการ throw เกิดขึ้น แต่เราก็เอา try block เป็นตัวกำหนดว่ามีการตัดจับอยู่ในโค๊ดนี้ การเขียนโค๊ดใน try block นั้นอาจมีหลายบรรทัดก็ได้ เมื่อมีการ throw เกิดขึ้น ก็จะเข้าสู่การ catch ในที่นี้ ผมระบุตัวแปร e ขึ้นมา แต่ไม่มีเงื่อนไข ดังนั้นไม่ว่าเกิด throw อะไร ก็จะเข้าสู่ catch block ตัวนี้ ซึ่งก็ส่งตัวแปรที่ throw นั้นมายังยัง e นั่นเอง

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

แต่ถ้าการ throw เป็นชนิด Error หรือชนิดที่แผลงมาจาก Error เช่น RangeError หรือแม้แต่ PortError ที่เราสร้างนั้น เราก็จะมี properties สองตัว คือ name และ message ให้ใช้ครับ ลองดูตัวอย่างนี้

e.name ก็ตามชนิดของ error ส่วน e.message ก็คือพารามิเตอร์ที่เราส่งเข้ามาตอนสร้างนั่นเอง สังเกตว่าผมไม่ได้เรียกใช้ฟังก์ชัน แต่หากบังคับให้ใน try block เกิด Error  ทั้งนี้ก็ไม่มีอะไรมาก ต้องการให้มันสั้นๆ เท่านั้น

ถ้าเราสร้างชนิด Error เป็นระบบ เราสามารถใช้ instanceof เพื่อตรวจสอบชนิดที่เกิด Error และจัดการเขียนโค๊ดเพื่อดัก Error ให้เหมาะสมได้ดังตัวอย่างข้างล่างนี้

จะเห็นได้ว่าใน code นี้สามารถจัดการ TypeError และ RangeError ได้ นอกเหนือจากนั้นก็ throw e;  เพื่อส่งขึ้นไปชั้นที่บนกว่า ให้ข้างบนจัดการ การทำเช่นนี้เราเรียกว่าการ rethrow

finally

ลองดูตัวอย่างนี้ครับ

ถ้าแฟ้มเปิดไม่สำเร็จ ก็ไม่มีปัญหาครับ จะเข้า catch แล้วก็ rethrow  แต่ถ้าเกิดเปิดสำเร็จหละ แต่เขียนแฟ้มได้ครึ่งเดียว เกิดพื้นที่เต็ม มันก็จะเข้าสู่ catch เหมือนกัน แล้วก็ rethrow ปัญหาก็คือ แฟ้มมันเปิดค้าง มีการ lock เกิดขึ้น ทางแก้วิธีแรก ก็คือ ใน catch ก่อนจะ rethow ก็ต้องปิดแฟ้มให้ ซึ่งดูยุ่งยาก เขียนการปิดแฟ้มซ้ำซ้อนกับกรณีทำงานถูกต้องตามปกติ เลยมีการเพิ่ม finally เข้าไป ดังนี้

finally นั้น เป็นการบังคับให้ต้องทำงาน มีสามกรณีด้วยกัน

  • try ทำงานปกติ เสร็จเรียบร้อย ก็จะทำ finally
  • try ทำงานผิดปกติ เข้าสู่ catch ทำงานใน catch ปกติ แล้วก็จะทำ finally
  • try ทำงานผิดปกติ เข้าสู่ catch เกิด rethrow ก่อน rethow จะทำ จะทำ finally ก่อน

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

Object: function

ECMAScript เป็นภาษาที่รองรับแนวคิด functional programming ได้ค่อนข้างดีมาก ลูกเล่นของฟังก์ชันนั้นจึงมีมาก ผมจึงขอแยกเอาไว้ไปอีกบทความหนึ่งโดยเฉพาะ ส่วนในบทความนี้ ผมเลือกที่จะมองการใช้งานฟังก์ชันในระดับพื้นฐานเท่านั้น เรามาดูกัน

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

แบบนี้ทาง ECMAScript เรียกว่า function declaration ครับ หรืออาจจะเขียนแบบข้างล่างนี้ก็ได้

แบบนี้เรียกว่า function expression ซึ่งแตกต่างจากแบบข้างบนเล็กน้อย เดี๋ยวจะมีรายละเอียดต่อไป เอาเป็นว่าตอนนี้มองว่าทั้งสองแบบเหมือนกันไปก่อน

ปกติแล้ว ถ้าเป็น Object ก็ต้องใช้ instanceof เพื่อในการทดสอบชนิดตัวแปร แต่ function นั้นมีความพิเศษเพิ่มเติมตรงที่สามารถใช้ typeof เพื่อแสดงชนิดตัวแปรเหมือนในกลุ่ม primitive ได้

คราวนี้มาลองดู code นี้ครับ ท่านว่ามันจะแสดงผลอะไร

มันจะแสดงสองบรรทัดนะครับ บรรทัดแรกแสดง Hello, World ซึ่งเป็นผลมาจาก console.log() ข้างใน และ console.log() ตัวข้างในนี้ ไม่มีคำสั่ง return ดังนั้นมันจะส่งค่า undefined ออกมา ดังนั้น console.log() ตัวข้างนอกจึงได้ค่า undefined เพื่อนำไปแสดงเป็นบรรทัดที่สองครับ

ใน ES5 นั้น ในเชิง imperative แล้ว scope ของตัวแปรจะมีอยู่เพียงสองระดับเท่านั้น นั่นคือ global และ function scope ไม่มีระดับ block หรือระดับแฟ้ม ดังนั้น ES5 จึงค่อนข้างมีปัญหากับการจัดการ scope ของตัวแปร ซึ่งผมจะอธิบายในบทความต่อไป แต่ในหัวข้อนี้ ขอให้ท่านเข้าใจก่อนว่า เราสามารถสร้างตัวแปรระดับฟังก์ชันได้ มันจะเกิดและตายในฟังก์ชัน ไม่หลุดออกไปเพ่นพ่านที่อื่น ดังนี้ครับ

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

การเรียกใช้งานฟังก์ชัน

ก็อย่างที่เห็นครับ เราสามารถเรียกใช้งานฟังก์ชัน เหมือนกันภาษาอื่นได้ เช่น

แต่จริงๆ แล้ว function ก็เป็น Object ประเภทหนึ่ง ดังนั้นการทำงานจริงๆ มันเรียกผ่าน methods ดังนี้ครับ

ท่านยังไม่ต้องสนใจว่า ทำไมถึงมี parameter ตัวแรกขึ้นมาที่กำหนดเป็น undefined เอาไว้ผมจะขยายความในบท object-based ครับ  ในหัวข้อนี้เรามาเรียกเพิ่มกันอีกตัวครับ คู่บุญมากับ method call() เลย ลองดูครับ

apply() จะต่างกับ call() ตรงที่ apply จะมีพารามิเตอร์จำกัดแค่สองตัวครับ ตัวแรกในที่นี้เป็น undefined ส่วนตัวที่สองจะยุบเอาพารามิเตอร์ที่เราใช้งาน มีกี่ตัวก็ตามเป็น Array เพียงตัวเดียว ผิดกับ call() จะมีจำนวนพารามิเตอร์เท่ากับที่เราใช้งานบวกด้วยหนึ่งก็คือ undefined ดังที่กล่าวมาครับ

default values ของพารามิเตอร์

ภาษานี้ไม่รองรับ การกำหนด default values สำหรับพารมิเตอร์ นะครับ แต่เรามีลูกเล่นดังนี้ครับ

เวลาใช้งาน เราจะใส่ age หรือไม่ก็ได้ (จริงๆ แล้ว name จะระบุหรือไม่ก็ได้เช่นกัน อย่างไรเสียก็ไม่ error เพียงแต่มันจะแสดงเป็น undefined เท่านั้น) ลูกเล่นนั้นอยู่ที่บรรทัดที่ 3 นั่นก็คือ  || คือการ or ดังนั้น  ถ้า age มีค่า ก็จะกำหนดค่า age เป็นค่าเดิม แต่ถ้า age ไม่มีค่า ในที่นี้คือ undefined เทียบเป็น boolean ก็คือ false นั่นเอง มันจึงไปเอา 20 ไปบรรจุลงใน age

function overload และ variable-length arguments

function overload คือการที่เรามีหลายฟังก์ชัน แต่อาศัยชื่อฟังก์ชันเดียวกัน โดยการที่จะเลือกว่าจะไปเรียกใช้ฟังก์ชันตัวไหนนั้นอยู่ที่พารามิเตอร์ที่เรียกใช้ครับ หรือเราอาจจะเรียกแต่ละชุดของพารามิเตอร์ว่า prototype ก็ได้ ก็เลยอาจกล่าวได้ว่าการเลือกใช้ฟังก์ชันขึ้นอยู่กับ prototype นั่นเอง ที่กล่าวมาทั้งหมดนั้น ECMAScript ไม่รองรับครับ (แล้วพูดทำไมนี่)

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

arguments เป็นตัวแปรพิเศษที่สร้างมาให้ในฟังก์ชันครับ ซึ่งจะเก็บพารามิเตอร์ทุกตัวอยู่ในรูป Object นั่นเอง โดยใช้ เลขตำแหน่ง (เป็น string) เป็น key ในการค้นหา ลองดูการเรียกใช้คำสั่งแรกบรรทัดที่ 5 ดูครับ เราส่งไปตัวเดียว ดังนั้นฝั่งรับก็จะรับเป็น a ส่วน b, c ก็เป็น undefined ส่วนในคำสั่งที่ 2 บรรทัดที่ 6 เราส่งไปเท่าจำนวนตัว  a, b, c จึงมีค่า 1, 2, 3  ส่วนใน arguments ก็มีค่าครบถ้วน และในบรรทัดสุดท้าย เราส่งเกิน แน่นอนครับ 4, 5 ไม่มีตัวแปรรองรับ แต่ก็ยังไปปรากฏใน arguments

ดังนั้น เราอาจไม่ต้องรับพารามิเตอร์แม้แต่ตัวเดียวให้ยุ่งยากครับ เขียนอย่างนี้ก็ได้ผลลัพธ์เดียวกัน

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

และเมื่อจำนวน parameters มีความยืดหยุ่น ผมก็สามารถประยุกต์แบบนี้ได้ครับ

อันนี้ถ้าเป็นภาษา C จะเรียกว่า varargs หรือ variable-length arguments ซึ่งรองรับจำนวนพารามิเตอร์ที่หลากหลายได้

Gotcha!: นิยามฟังก์ชันและตัวแปรไว้ที่ไหนดี

เริ่มจากการนิยาม function declaration ดังนี้ก่อน

อันนี้เป็น code ปกติเมื่อรันแล้วก็แสดง ‘Hello, World’ ออกมา คราวนี้ถ้าผมสลับตำแหน่งกันเป็นแบบนี้ จะทำงานได้เหมือนเดิมหรือไม่

ยังได้ ‘Hello, World’ ไหมครับ คิดดูก่อนครับ

ว่ากันตามพื้นฐานแล้ว ไม่น่าจะทำงานได้ เพราะฟังก์ชันยังไม่ได้นิยาม แต่ดันใช้งานเสียก่อน แต่ผลปรากฏว่า ใช้ได้เฉยเลย  gotcha! ถ้าเป็นภาษา script ที่เป็นพวกเดียวกันอย่าง Python โค๊ดนี้รันไม่ได้ครับ  ถ้างั้นเรามาดูต่อ ผมลองเปลี่ยนเป็น function expression ดังนี้

รันได้ไหมครับ

ไม่กี้รันได้ โค๊ดนี้ก็เหมือนกัน ไม่น่าจะมีปัญหา แต่ gotcha! รันไม่ได้ครับ งงไหมครับ ก็เป็นฟังก์ชันเหมือนกันแท้ๆ แต่คราวนี้กลับรันไม่ได้ มันอะไรกันนี่ รันไม่ได้ไม่เป็นไร มาดูว่ามันแสดง error ว่าอย่างไร ปรากฏว่า ระบบบอกว่า ‘TypeError: undefined is not a function’   ไม่เป็นไร แสดงว่า f1 ไม่รู้จักมันก็เลย undefined แน่นอน undefined ไม่ใช่ฟังก์ชันอยู่แล้ว มาใส่วงเล็บทำตัวเป็นฟังก์ชัน ก็ย่อมไม่ได้

แล้วถ้างั้น ผมเขียนอย่างนี้หละ

ผมลองเปลี่ยนการเรียกให้เป็น f2() ดู รันได้รึเปล่า คงไม่ gotcha! รันได้นะครับ มันไม่อินดี้ขนาดนั้น รันไม่ได้ถูกแล้วครับ แต่ที่จะมีปัญหาก็คือ error ระบบแสดง มันแสดงว่า ‘ReferenceError: f2 is not defined’  ทำไมมันไม่แสดง error เหมือนกับตัวอย่างที่ผ่านมา gotcha! ไปอีกดอก

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

hoist car

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

เมื่อ hoisted นิยามของ f1() และ f2() ขึ้นไปบนสุดแล้ว เราจึงเรียกใช้ f1() ได้ จึงเห็นผลลัพธ์ ‘Hello, World’ ลองดูกลับกันดูครับ

แบบนี้ก็รันได้นะครับ แม้ว่า f2() จะนิยามหลังจาก f1() ก็ตาม แต่ระบบมันจัดการให้ครับ เรื่องนี้ไม่ต้องไปกังวล คราวนี้เรามาดูบ้างครับ ว่าทำไมนิยามฟังก์ชันแบบ expression มันกลับไม่ hoisted ให้เรา ที่จริงแล้วมันก็ hoisted นะครับ แต่ function expression มันนิยามเป็นตัวแปร ไม่ใช่ฟังก์ชัน ดังนั้นมันจึงใช้กฏการ hoisted ของตัวแปร ไม่ใช่ฟังก์ชัน

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

ถ้าเป็นภาษาอื่นจะ error ตั้งแต่บรรทัดแรกแล้ว เพราะไม่รู้จักตัวแปร hello แต่ ECMAScript นั้นจะ hoisted ตัวแปร hello ขึ้นบนสุด โดยการแอบเพิ่มบรรทัดแรกเข้าไปดังนี้

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

แต่ code ที่ใช้ f2() นั้น ระบบไม่รู้จัก f2 เลย ไม่ได้มีการนิยามตัวแปรเอาไว้เลย ดังนั้นจึงไม่เกิดการ hoisted พอเอามาใช้งาน มันก็แสดง error ว่าไม่รู้จักนั่นเอง

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

TS: function

เรานิยามฟังก์ชัน ระบุได้ทั้งชนิดของพารามิเตอร์และค่าส่งกลับ ดังนี้ครับ

ถ้าฟังก์ชันนั้น ไม่มีการส่งค่ากลับ ให้กำหนดชนิดส่งกลับเป็น void ดังนี้ครับ

หรือถ้าจะใช้เป็นแบบ function expression ก็เขียนแบบนี้ได้ครับ

แต่ที่เห็นไปก็ยังไม่เต็มยศนะครับ เพราะ ตัวแปร sum ยังไม่มีชนิด ชนิดของตัวแปร sum ต้องอยู่ทางซ้ายมือของเครื่องหมาย = อย่างเช่น

การนิยามให้เต็มยศ ใช้แบบนี้ครับ

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

เรื่องความยืดหยุ่นก็หายไปครับ ตรวจเข้มกว่าเดิม ดังนี้

เมื่อฟังก์ชันบอกว่ามีพารามิเตอร์ 2 ตัวก็ต้องส่งมาสองตัวครับ ถ้าส่งไม่ครบ ก็จะทรานไพล์ไม่ผ่านครับ ทาง TypeScript ก็ประณีประนอมยอมให้ตัวแปรบางตัวเป็น optional ได้โดยการใส่เครื่องหมายคำถามต่อท้ายตัวแปรเวลานิยาม ดัง code ข้างล่างครับ

และ TypeScript รองรับ การนิยาม default value แล้วครับ ทำได้โดย

ถ้าถามว่าแล้ว varargs หละรองรับหรือไม่ รองรับครับโดยการ

จะสังเกตได้ว่า arr นั้นต่างจาก arguments นะครับ เพราะ arguments นั้น อมพารามิเตอร์ทุกตัวเอาไว้ แต่ arr จะอมเฉพาะตัวที่ล้นเท่านั้น

ฟังก์ชันนั้นยังรองรับ generics เหมือนกับภาษา object-oriented ชั้นนำทั่วไป ลองดูตัวอย่างนี้ครับ

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

กลยุทธ: จักจั่นลอกคราบ

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

เมื่อทดลองให้หา fib(15) ก็จะได้ค่า 610 code นี้เป็นแบบ recursion คือการเรียกตัวเองนะครับ ภาษา ECMAScript รองรับการทำงานแบบ recursion ได้อย่างสมบูรณ์ครับ คราวนี้มาพิจารณาประสิทธิภาพกันหน่อยครับ ไหนๆ เว็บนี้ก็เกี่ยวข้องกับเรื่องการเขียนโปรแกรมประสิทธิภาพสูง ก็เลยหาโอกาสแทรกซะเลย จะได้เรียนรู้ควบคู่กันไปครับ ผมลองเขียนโปรแกรมจับเวลาดู ดังนี้ครับ

ท่านลองรันโปรแกรมดูนะครับ ทำใจร่มๆ นะครับ กว่าผลลัพธ์จะออก มันนานนะครับ ก็ผมเล่นตั้ง fib(50) เครื่องผมใช้เวลาไป 172 วินาที คำถามว่าทำให้เร็วก็นี้ได้ไหม ผมเคยเขียนบทความ ก้าวแรก HPC #05: อัลกอริทึมอย่างง่ายและการปรับปรุงประสิทธิภาพ ในการปรับปรุงครั้งที่ 3 ผมทำ caching ในหัวข้อนี้ผมก็ใช้วิธีเดียวกันนี้ ผมเลยปรับโปรแกรมมาเป็น

การใช้งานนั้น ผมต้องให้ผู้ใช้ สร้าง cache_fib ขึ้นมาโดย

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

แต่ถ้าไม่มีใน cache_fib ก็จะคำนวณ เมื่อได้คำตอบแล้ว ก่อนส่งกลับก็บันทึกใน cache_fib เสียก่อน ทุกอย่างดูเหมือนดีครับ แต่มันมีก้างตำคออยู่สองประการครับคือหนึ่ง ตัวแปร cache_fib มันลอยออกมา กลายเป็นตัวแปรอยู่ใน global scope ซึ่งไม่ดีครับ ใช้ฟังก์ชันนี้ ก็ต้องผูกเอา cache_fib ไปด้วย เหมือนกับคนไข้ในโรงพยาบาลที่เดินไปก็ต้องลากเอาเสาน้ำเกลือไปด้วย หรือเอาทันสมัยหน่อย ก็เหมือนกับโทรศัพท์มือถือรุ่นล่าสุดราคาสูง เบาและบางสุดยอด แต่ถือไปไหนก็ต้องต่อสายน้ำเกลือเชื่อมกัน powerbank ตลอดเวลา ดูแล้วชวนพิศวงไม่น้อย

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

ผมอาศัยหลักที่ว่า Object นั้นสามารถเพิ่ม methods/properties เข้าไปได้ ก็ function ก็เป็น Object อย่างหนึ่ง ดังนั้นผมจึงสามารถเพิ่ม cache_fib ให้ไปอยู่ใน object ของ function fib เลย และการเรียกใช้ครั้งแรกนั้น fib.cache_fib ยังไม่เกิด ผมเลยต้องใช้เงื่อนไขตรวจสอบดู ถ้ายังไม่เกิดก็สร้างขึ้นมาครับ ทดลองดูครับ code นี้ก็ให้คำตอบที่ถูกต้องเหมือนเดิม

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

อาจจะดูงงๆ หน่อยนะครับ ผมจะให้ดูนะครับ เริ่มต้นเมื่อมีการเรียกใช้ fib() เป็นครั้งแรก จะส่ง n มาเป็นค่าอะไรก็ช่างครับ ก็จะมาถึงบรรทัดที่ 2 เป็นการนิยามฟังก์ชัน fib() ใหม่ อย่างที่บอกนะครับว่า เรานิยามซ้ำได้ แต่นิยามแล้วของใหม่จะทับของเก่า เราอาศัยจุดนี้เอามาใช้งาน ดังนั้นในบรรทัดนี้ เราก็นิยามฟังก์ชัน fib() ใหม่ ยังไม่ต้องสนใจเนื้อหาภายในก็แล้วกัน เอาเป็นว่า fib กลายเป็น code ใหม่แล้ว แต่ code ปัจจุบันก็ยังไม่ตายนะครับ ยังทำงานอยู่ แต่ไม่ได้ชื่อ fib แล้ว และหลังจากที่รันฟังก์ชันนี้จนจบ มันก็จะสูญสลายไป แต่ตอนนี้มันยังอยู่ จากบรรทัดที่สอง บรรทัดต่อมาคือบรรทัดที่ 13 เป็นการสร้างตัวแปร cache_fib ให้อยู่ในฟังก์ชัน ซึ่งเป็นฟังก์ชันตัวใหม่นะครับ ไม่ใช่ตัวที่กำลังรัน และบรรทัดที่ 14 ก็สั่งให้ fib ตัวใหม่นี้คำนวณค่า

ในฟังก์ชันตัวใหม่ก็ทำการคำนวณตามปกติครับ แต่ไม่ต้องยุ่งกับการจัดการ cache_fib ในตอนเริ่มต้น ดังนั้นจึงไม่ต้องเสียเวลาไปตรวจสอบเงื่อนไข cache_fib ครับ แนวคิดที่นิยามฟังก์ชันให้มีการทำงานใหม่โดยที่ผู้นิยามคือฟังก์ชันนั้นเอง ผมเรียกว่า จักจั่นลอกคราบ หรือถ้าเป็นภาษาฝรั่งก็จะเรียกว่า self-defining function และมีการที่แอบเก็บ cache เพื่อให้เรียกใช้ได้ภายหลังโดยไม่ต้องคำนวณใหม่ ก็เป็น pattern ตัวหนึ่งที่เรียกว่า Memorization pattern

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

ทิ้งท้าย

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

[Total: 11    Average: 5/5]

You may also like...