Comma Operator เครื่องหมายใน JavaScript ที่หลายคนยังไม่รู้จัก

Nuttavut Thongjor

เพื่อนๆที่ใช้ Babel อยู่เคยแอบลองแงะโค๊ดที่ Babel แปลงเป็น ES5 ดูไหมครับ เมื่อเราเขียนประโยค import ของ ES2015 แล้ว Babel จะแปลงเป็น CommonJS ดังนี้

JavaScript
1// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES5
2import { xyz } from 'mn'
3
4function foo() {
5 xyz()
6}
7
8// หลังแปลง
9;('use strict')
10
11var _mn = require('mn')
12
13function foo() {
14 ;(0, _mn.xyz)() // อะไรหว่า
15}

ช้าก่อน (0, _mn.xyz)() คืออะไร? มาจากไหน? ทำเพื่อใคร? ร่วมหาคำตอบไปกับเราในบทความนี้ครับ

Comma Operator คืออะไร?

ตามชื่อเลยครับ มันคือเครื่องหมายลูกน้ำนั่นเอง โดยลักษณะพื้นฐานของมันคือเมื่อใช้เครื่องหมายนี้สิ่งสุดท้ายที่คั่นด้วยลูกน้ำจะคืนค่ากลับออกมาเสมอ

JavaScript
1// 4 เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา
2console.log((1, 2, 3, 4)) // 4
3
4const foo = () => 99
5const bar = () => 77
6
7// bar() เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา
8console.log((foo(), bar())) // 77

จะเห็นว่าการใช้ comma operator นั้นต้องอยู่ในรูป (x1, x2, ..., xn) โดยค่า xn จะคืนกลับออกมาเสมอ

ประโยชน์เบื้องต้นของ comma operator

comma operator นั้นสามารถใช้เพื่อเขียนลำดับการทำงานต่างๆและคืนค่ากลับจากลูกน้ำตัวสุดท้าย

JavaScript
1const foo = () => {
2 return 1
3}
4const bar = () => {
5 return 2
6}
7
8// ทำ foo() ก่อน ไม่สนใจผลลัพธ์ที่เกิดขึ้น
9// จากนั้นจึงทำ bar()
10// ผลลัพธ์ที่ได้จาก bar() จะเก็บไว้ในตัวแปร result
11const result = (foo(), bar())
12
13console.log(result) // 2

จากตัวอย่างข้างต้นเป็นการเรียกฟังก์ชันสองตัว โดยตัวสุดท้ายจะคืนค่าออกมาแล้วเก็บผลลัพธ์ลงในตัวแปร result รูปแบบการใช้งาน comma operator แบบนี้ยังมีผลต่อการลดการใช้หน่วยความจำอีกด้วย สามารถศึกษารายละเอียดเพิ่มเติมจาก Tail Call Optimization คืออะไร? มารู้จักวิธีประหยัดหน่วยความจำใน ES2015 ด้วย Tail Call Optimization

comma operator เป็น function call

ก่อนที่จะเริ่มอธิบายว่า comma operator เป็นการเรียกฟังก์ชันไม่ใช่การเรียกเมธอดอย่างไร ขอทบทวนความหมายของ method call และ function call กันก่อนครับ

JavaScript
1const obj = {
2 print() {
3 console.log('Print: Method call')
4 },
5}
6
7function print() {
8 console.log('Print: Function call')
9}
10
11// print ที่เรียกผ่าน obj นี้เป็นการเรียกเมธอด
12// เนื่องจากเป็นการเรียกผ่านอ็อบเจ็กต์ obj
13// โดย this มีค่าเป็น obj
14obj.print() // Print: Method call
15
16// การเรียก print นี้เป็นการเรียกฟังก์ชัน
17// เนื่องจากฟังก์ชันนี้ไม่ได้อ้างอิงกับอ็อบเจ็กต์ใด
18// โดย this จะเป็น undefined (ใน strict mode)
19print() // Print: Function call

เอาหละเรามาลองเล่นกับ comma operator กัน

JavaScript
1'use strict'
2
3const obj = {
4 getThis() {
5 return this
6 },
7}
8
9console.log(obj.getThis() === obj) // true
10
11// (0, obj.getThis) จะคืนค่ากลับเป็น obj.getThis
12// ซึ่ง obj.getThis มันคือฟังก์ชัน
13// ดังนั้นเมื่อเราใส่ () ต่อท้ายจึงเป็นการเรียกฟังก์ชัน
14// (0, obj.getThis)() ได้ค่า this เป็น undefined
15console.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 แบบนี้

JavaScript
1// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES5
2import { xyz } from 'mn'
3
4function foo() {
5 xyz()
6}
7
8// หลังแปลง
9;('use strict')
10
11var _mn = require('mn')
12
13function foo() {
14 ;(0, _mn.xyz)()
15}

ที่ต้องใช้ comma operator เพื่อรีเซ็ตค่า this ให้เป็น undefined นั่นเองครับ

comma operator กับ bind

จากหัวข้อที่ผ่านมา comma operator ทำให้เกิดการเรียกฟังก์ชันแทนที่จะเป็นการเรียกเมธอด นั่นคือ this ที่ชี้ไปที่อ็อบเจ็กต์นั้นจะหายไป ฉะนั้นแล้วการเรียกต่อไปนี้จะเกิดปัญหา

JavaScript
1;(0, console.log)('Hi')(
2 // Error!
3
4 // bind เพื่อตั้ง this ให้เป็น console
5 0,
6 console.log
7).bind(console)('Hi') // Hi

เกร็ดความรู้เพิ่มเติมคือไม่ใช่แค่ comma operator ที่ทำให้ this หายไปครับ แต่การประกาศตัวแปรพร้อมกำหนดค่าก็ให้ผลเช่นเดียวกัน

JavaScript
1const log = console.log
2log('Hi') // Error!
3
4const 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
  • เอกสารอ้างอิง