Babel Coder

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

intermediate

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

// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES5
import { xyz } from 'mn'

function foo() {
  xyz()
}

// หลังแปลง
'use strict';

var _mn = require('mn');

function foo() {
  (0, _mn.xyz)(); // อะไรหว่า
}

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

สารบัญ

Comma Operator คืออะไร?

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

// 4 เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา
console.log((1, 2, 3, 4)) // 4

const foo = () => 99
const bar = () => 77

// bar() เป็นค่าสุดท้ายใน (, , ,) จึงคืนค่ากลับออกมา
console.log((foo(), bar())) // 77

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

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

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

const foo = () => { return 1 }
const bar = () => { return 2 }

// ทำ foo() ก่อน ไม่สนใจผลลัพธ์ที่เกิดขึ้น
// จากนั้นจึงทำ bar()
// ผลลัพธ์ที่ได้จาก bar() จะเก็บไว้ในตัวแปร result
const result = (foo(), bar())

console.log(result) // 2

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

comma operator เป็น function call

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

const obj = {
  print() {
    console.log('Print: Method call')
  }
}

function print() {
  console.log('Print: Function call')
}

// print ที่เรียกผ่าน obj นี้เป็นการเรียกเมธอด
// เนื่องจากเป็นการเรียกผ่านอ็อบเจ็กต์ obj
// โดย this มีค่าเป็น obj
obj.print() // Print: Method call

// การเรียก print นี้เป็นการเรียกฟังก์ชัน
// เนื่องจากฟังก์ชันนี้ไม่ได้อ้างอิงกับอ็อบเจ็กต์ใด
// โดย this จะเป็น undefined (ใน strict mode)
print() // Print: Function call

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

'use strict';

const obj = {
  getThis() {
    return this
  }
}

console.log(obj.getThis() === obj) // true

// (0, obj.getThis) จะคืนค่ากลับเป็น obj.getThis
// ซึ่ง obj.getThis มันคือฟังก์ชัน
// ดังนั้นเมื่อเราใส่ () ต่อท้ายจึงเป็นการเรียกฟังก์ชัน
// (0, obj.getThis)() ได้ค่า this เป็น undefined
console.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 แบบนี้

// ES2015 ก่อน Babel จะจับแปลงร่างเป็น ES5
import { xyz } from 'mn'

function foo() {
  xyz()
}

// หลังแปลง
'use strict';

var _mn = require('mn');

function foo() {
  (0, _mn.xyz)();
}

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

comma operator กับ bind

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

(0, console.log)('Hi') // Error!

// bind เพื่อตั้ง this ให้เป็น console
(0, console.log).bind(console)('Hi') // Hi

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

const log = console.log
log('Hi') // Error!

const log = console.log.bind(console)
log('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


แสดงความคิดเห็นของคุณ


No any discussions