Comma Operator เครื่องหมายใน JavaScript ที่หลายคนยังไม่รู้จัก
เพื่อนๆที่ใช้ Babel อยู่เคยแอบลองแงะโค๊ดที่ Babel แปลงเป็น ES5 ดูไหมครับ เมื่อเราเขียนประโยค import ของ ES2015 แล้ว Babel จะแปลงเป็น CommonJS ดังนี้
1// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES52import { xyz } from 'mn'34function foo() {5 xyz()6}78// หลังแปลง9;('use strict')1011var _mn = require('mn')1213function foo() {14 ;(0, _mn.xyz)() // อะไรหว่า15}
ช้าก่อน (0, _mn.xyz)()
คืออะไร? มาจากไหน? ทำเพื่อใคร? ร่วมหาคำตอบไปกับเราในบทความนี้ครับ
Comma Operator คืออะไร?
ตามชื่อเลยครับ มันคือเครื่องหมายลูกน้ำนั่นเอง โดยลักษณะพื้นฐานของมันคือเมื่อใช้เครื่องหมายนี้สิ่งสุดท้ายที่คั่นด้วยลูกน้ำจะคืนค่ากลับออกมาเสมอ
1// 4 เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา2console.log((1, 2, 3, 4)) // 434const foo = () => 995const bar = () => 7767// bar() เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา8console.log((foo(), bar())) // 77
จะเห็นว่าการใช้ comma operator นั้นต้องอยู่ในรูป (x1, x2, ..., xn) โดยค่า xn จะคืนกลับออกมาเสมอ
ประโยชน์เบื้องต้นของ comma operator
comma operator นั้นสามารถใช้เพื่อเขียนลำดับการทำงานต่างๆและคืนค่ากลับจากลูกน้ำตัวสุดท้าย
1const foo = () => {2 return 13}4const bar = () => {5 return 26}78// ทำ foo() ก่อน ไม่สนใจผลลัพธ์ที่เกิดขึ้น9// จากนั้นจึงทำ bar()10// ผลลัพธ์ที่ได้จาก bar() จะเก็บไว้ในตัวแปร result11const result = (foo(), bar())1213console.log(result) // 2
จากตัวอย่างข้างต้นเป็นการเรียกฟังก์ชันสองตัว โดยตัวสุดท้ายจะคืนค่าออกมาแล้วเก็บผลลัพธ์ลงในตัวแปร result รูปแบบการใช้งาน comma operator แบบนี้ยังมีผลต่อการลดการใช้หน่วยความจำอีกด้วย สามารถศึกษารายละเอียดเพิ่มเติมจาก Tail Call Optimization คืออะไร? มารู้จักวิธีประหยัดหน่วยความจำใน ES2015 ด้วย Tail Call Optimization
comma operator เป็น function call
ก่อนที่จะเริ่มอธิบายว่า comma operator เป็นการเรียกฟังก์ชันไม่ใช่การเรียกเมธอดอย่างไร ขอทบทวนความหมายของ method call และ function call กันก่อนครับ
1const obj = {2 print() {3 console.log('Print: Method call')4 },5}67function print() {8 console.log('Print: Function call')9}1011// print ที่เรียกผ่าน obj นี้เป็นการเรียกเมธอด12// เนื่องจากเป็นการเรียกผ่านอ็อบเจ็กต์ obj13// โดย this มีค่าเป็น obj14obj.print() // Print: Method call1516// การเรียก print นี้เป็นการเรียกฟังก์ชัน17// เนื่องจากฟังก์ชันนี้ไม่ได้อ้างอิงกับอ็อบเจ็กต์ใด18// โดย this จะเป็น undefined (ใน strict mode)19print() // Print: Function call
เอาหละเรามาลองเล่นกับ comma operator กัน
1'use strict'23const obj = {4 getThis() {5 return this6 },7}89console.log(obj.getThis() === obj) // true1011// (0, obj.getThis) จะคืนค่ากลับเป็น obj.getThis12// ซึ่ง obj.getThis มันคือฟังก์ชัน13// ดังนั้นเมื่อเราใส่ () ต่อท้ายจึงเป็นการเรียกฟังก์ชัน14// (0, obj.getThis)() ได้ค่า this เป็น undefined15console.log((0, obj.getThis)() === undefined) // true
ก่อนอื่นเลย 0 ที่เราใส่ใน (0, obj.getThis)() คืออะไร? มันเป็นขยะตัวหนึ่งที่เราใส่เข้าไปครับเพื่อให้สามารถใส่เครื่องหมายลูกน้ำได้ ลองจินตนาการดูถ้าไม่ใส่อะไรเข้าไปซักอย่างมันจะเหลือแค่ (obj.getThis)() ที่ไม่ใช่ comma operator เพราะไม่มีลูกน้ำซักตัว เราจะใช้ตัวเลขอื่นหรืออักษรข้อความอะไรก็ได้ครับ แต่ในที่นี้ผมใช้ 0 เพราะมันสั้นประหยัดพื้นที่
จากตัวอย่างจะพบว่าเมื่อเราเรียก (0, obj.getThis)() ค่าของ this จะเป็น undefined พูดง่ายๆก็คือ this ตัวนี้ไม่ได้อ้างอิงไปถึง obj จึงไม่เป็น method call แต่เป็น function call นั่นเอง
ย้อนกลับไปที่คำถามตั้งต้นของเรากันครับ ทำไม Babel ถึงแปลง import โดยใช้ comma operator แบบนี้
1// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES52import { xyz } from 'mn'34function foo() {5 xyz()6}78// หลังแปลง9;('use strict')1011var _mn = require('mn')1213function foo() {14 ;(0, _mn.xyz)()15}
ที่ต้องใช้ comma operator เพื่อรีเซ็ตค่า this ให้เป็น undefined นั่นเองครับ
comma operator กับ bind
จากหัวข้อที่ผ่านมา comma operator ทำให้เกิดการเรียกฟังก์ชันแทนที่จะเป็นการเรียกเมธอด นั่นคือ this ที่ชี้ไปที่อ็อบเจ็กต์นั้นจะหายไป ฉะนั้นแล้วการเรียกต่อไปนี้จะเกิดปัญหา
1;(0, console.log)('Hi')(2 // Error!34 // bind เพื่อตั้ง this ให้เป็น console5 0,6 console.log7).bind(console)('Hi') // Hi
เกร็ดความรู้เพิ่มเติมคือไม่ใช่แค่ comma operator ที่ทำให้ this หายไปครับ แต่การประกาศตัวแปรพร้อมกำหนดค่าก็ให้ผลเช่นเดียวกัน
1const log = console.log2log('Hi') // Error!34const log = console.log.bind(console)5log('Hi') // Hi
เพื่อนๆที่ยังไม่เข้าใจเรื่อง bind สามารถอ่านเพิ่มเติมได้ที่ ข้อแตกต่างของ bind, apply และ call ใน JavaScript กับการใช้งาน
ทั้งหมดนี้เป็นผลจากการที่ ECMAScript มีชนิดข้อมูลพิเศษคือ Reference ที่ไม่ปรากฎเป็นรูปธรรม แต่ยังคงหลอกหลอนไม่ไปผุดไปเกิดซะที โดยแอบแฝงมาในรูปของการทำ dereference และ this ครับ เราจะไม่กล่าวถึงกันในบทความนี้แต่ขอให้เพื่อนๆทราบไว้ว่า comma operator และการเก็บค่าในตัวแปรนั้นเป็น dereference ของชนิดข้อมูล Reference ใน ECMAScript
comma operator เป็นอีกหนึ่งเครื่องหมายที่ทำให้รู้สึกว่า JavaScript เป็นภาษาที่ง่ายนิดเดียว (นิดเดียวจริงๆ ที่เหลือจะโคตรยากไปไหน) เพื่อนๆคนไหนมีข้อสงสัยเพิ่มเติมพิมพ์ไว้ใต้บทความได้เลยครับ
เอกสารอ้างอิง
Dr. Axel Rauschmayer (2015). Babel and CommonJS modules. Retrieved June, 20, 2016, from http://www.2ality.com/2015/12/babel-commonjs.html
Dmitry Soshnikov (2010). ECMA-262-3 in detail. Chapter 3. This.. Retrieved June, 20, 2016, from http://dmitrysoshnikov.com/ecmascript/chapter-3-this/
Dr. Axel Rauschmayer (2015). Why is (0,obj.prop)() not a method call?. Retrieved June, 20, 2016, from http://www.2ality.com/2015/12/references.html
สารบัญ
- Comma Operator คืออะไร?
- ประโยชน์เบื้องต้นของ comma operator
- comma operator เป็น function call
- comma operator กับ bind
- เอกสารอ้างอิง