Babel Coder

พื้นฐาน ES2015 (ES6) สำหรับการเขียน JavaScript สมัยใหม่

beginner

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

ลาก่อน semicolon เพื่อนยาก

สำหรับ ES2015 นั้นไม่มีความจำเป็นที่คุณต้องใส่ semicolon หรือ ; อีกต่อไป ที่ผมพูดว่าไม่ต้องใส่นี้ไม่ได้หมายความว่า JavaScript เป็นภาษาที่สลัด semicolon ทิ้งเสียแล้ว แท้ที่จริงนั้น JavaScript ยังต้องการ semicolon อยู่ เพียงแต่ ES2015 นั้นฉลาดพอที่จะเติม semicolon ให้เราอัตโนมัติ (automatic semicolon insertion) ไร้ซึ่ง semicolon โค๊ดของเราจึงดูสวยขึ้น นี่ไม่ใช่สิ่งมหัศจรรย์ถ้าคุณไปเห็นโค๊ดของไลบรารี่ของใครซักคนในโลก React ที่ไม่ปิดท้ายด้วย ;

// โค๊ดที่เปลือยเปล่าไร้ semicolon ของฟังก์ชัน applyMiddleware ใน Redux
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

จาก var สู่ let และ const

ผมว่าเพื่อนๆคงเริ่มใช้ทั้ง let และ const กันแล้ว แต่ทราบหรือไม่ว่า let/const นั้นมีขอบเขตการมีตัวตนหรือ scope ที่แตกต่างจาก var กล่าวคือ var นั้นเป็น function-scoped หรือพูดง่ายๆคือเราใช้ var ประกาศตัวแปรตรงจุดไหนก็ตาม แต่เมื่อ JavaScript เริ่มทำงาน การประกาศตัวแปรด้วย var นั้นจะกระดึ๊บๆตัวเองไปอยู่ใกล้กับจุดประกาศฟังก์ชันที่ใกล้ที่สุดหรือที่เราเรียกกันว่า hoisting

function foo(isValid) {
  if(isValid) {
    var x = 1
    return x
  }
  
  return x
}

// มีค่าเท่ากับ
function foo(isValid) {
  var x; // ดีดตัวเองมาอยู่ใกล้จุดประกาศฟังก์ชัน
  if(isValid) {
    var x = 1
    return x
  }
  
  return x
}

สำหรับ let และ const นั้นเป็น block-scoped หมายถึงประกาศตัวแปรอยู่ในบลอคไหนก็จะอยู่แค่ในบลอคนั้น ไม่เสนอหน้าดีดตัวเองออกไปใกล้ฟังก์ชันแบบที่ var ทำ

// พิมพ์ 2 ออกมาทั้งสองครั้ง
for(var i = 0; i < 2; i++) {
  // เนื่องจากในจังหวะที่ console.log ทำงาน loop ได้วนไปจนครบแล้ว
  // ทำให้ในขณะนั้่นค่า i มีค่าเป็น 2
  // อย่าลืมว่า i ประกาศโดยใช้ var มันจึงผลักตัวเองออกจาก for
  // หรือพูดง่ายๆคือ i นั้นเป็นตัวแปรตัวเดียวทุกครั้งที่วนลูปก็เพิ่มค่าใส่ตัวแปรเดิม
  setTimeout(function() { console.log(i) }, 100)
}

// ผลลัพธ์ได้ 0 และ 1
// ใช้ let ประกาศตัวแปรทำให้ตัวแปรเป็น block-scoped
// พูดง่ายๆคือ i จะเกิดขึ้นทุกครั้งที่วนลูป
// และไม่ซ้ำกับ i ก่อนหน้า
// เนื่องจากเป็น block-scoped มันจึงมีช่วงชีวิตอยู่แค่ใน {}
for(let i = 0; i < 2; i++) {
  setTimeout(function() { console.log(i) }, 100)
}

ความต่างของ let และ const คือ let นั้นหลังประกาศตัวแปรแล้วสามารถเปลี่ยนค่าได้ แต่ const ใช้สำหรับประกาศค่าคงที่ ทำให้หลังประกาศแล้วไม่สามารถเปลี่ยนแปลงค่าได้ การใช้ const นั้นมีข้อพึงระวังลองดูตัวอย่างครับ

let a = 0
a = 1

const PI = 3.14
PI = 1 // "PI" is read-only เปลี่ยนค่าไม่ได้อีกแล้วนะ

// obj เก็บ address หรือที่อยู่ของ { a: 1 }
// เราเปลี่ยน address ไม่ได้ เช่น obj = {} แบบนี้คือเปลี่ยน memory address ทำไม่ได้
const obj = { a: 1 }
// แต่การเปลี่ยนค่าภายใน object ไม่ได้ทำให้ memory address เปลี่ยนไป
obj.a = 2
console.log(obj) // { a: 2 }

เพื่อนๆที่เคยใช้ Redux จะทราบว่าเรานิยมใช้ switch/case กันใน reducer

export default (state = initialState, action) => {
  switch(action.type) {
    case LOAD_PAGES_SUCCESS:
      // ลดการเรียกชื่อยาวๆด้วยการประกาศตัวแปรมารองรับ
      // มั่นใจ 100% ว่า pages นี้ไม่เปลี่ยนแปลงแน่นอนเลยใช้ const ซะเลย
      const pages = action.result.entities.pages
      return { ...state, loading: false, items: pages }
    default:
      return state
  }
}

การเขียนแบบข้างบนฟังดูดีนะครับ เราอาจจะใช้ pages ไปทำสารพัดอย่างก่อน return ออกไปข้างนอก เราเลยสร้างตัวแปรมารองรับมัน แต่ถ้ามี case เพิ่มมาอีกอันแบบนี้หละ?

export default (state = initialState, action) => {
  switch(action.type) {
    case LOAD_PAGES_SUCCESS:
      const pages = action.result.entities.pages
      return { ...state, loading: false, items: pages }
    case LOAD_PAGE_SUCCESS:
      // ประกาศแบบนี้พังทันที นั่นเป็นเพราะ const เป็น block-scoped
      // ทำให้ scope ของมันอยู่ภายใต้ {} ของ switch
      // ใน case ก่อนหน้าเราประกาศ pages ไปแล้วจึงชนกัน
      // ควรย้ายการประกาศตัวแปรที่ทำซ้อนแบบนี้ออกไปข้างนอก
      const pages = action.result.entities.pages
      return { ...state, loading: false, items: [pages[0]] }
    default:
      return state
  }
}

อย่าคิดว่าเรื่องแบบนี้ไม่เคยเกิดขึ้นนะครับ เพราะมันมีคำถามใน stackoverflow มาแล้ว

ES2015 Module

JavaScript ไม่เคยจัดการโมดูลได้ด้วยตัวเองมาก่อนต้องทำผ่านไลบรารี่อย่าง CommonJS หรือ AMD การมาของ ES2015 มาพร้อมกับการสนับสนุนการทำงานกับโมดูลในตัว ตอนนี้คุณสามารถใช้ ES2015 เพื่อ import/export ของจากไฟล์หนึ่งไปอีกไฟล์หนึ่งได้แล้ว ดังนี้

// dog.js
export const DEFAULT_COLOR = 'white'
export function walk() {
  console.log('Walking...')
}

// main.js
// เลือกนำเข้าเฉพาะ DEFAULT_COLOR
import { DEFAULT_COLOR } from './dog.js'

// main.js
// นำเข้าทุกสรรพสิ่งที่ export จาก dog
// แล้วตั้งชื่อใหม่ให้ว่า lib
import * as lib from './dog.js'

ถ้าหากโมดูลนั้นมีแค่สิ่งเดียวที่อยาก export ทำได้ดังนี้

// circle.js
// สังเกตคำว่า default
export default class Circle {
  area() {
    
  }
}

// main.js
import Circle from './circle.js'

ES2015 module นั้นฉลาด มันมีการตรวจสอบว่าเรา import อะไรเข้ามาบ้าง ถ้าสิ่งไหนไม่ได้ import มันจะไม่นำมารวม ทำให้ไฟล์มีขนาดเล็กกว่าการใช้ CommonJS module เนื่องจาก CommonJS จะ import ทุกสรรพสิ่งที่โมดูลนั้น export ออกมา

// dog.js
export const DEFAULT_COLOR = 'white'
export function walk() {
  console.log('Walking...')
}

// main.js
// เลือกนำเข้าเฉพาะ walk
import { walk } from './dog.js'
walk()

// ผลลัพธ์สุดท้ายจะเป็น...
// สังเกตว่าไม่มี DEFAULT_COLOR ติดมาด้วย
function walk() 
  console.log('Walking...')
}
walk()

เรื่องที่ต้องพึงระวังในการใช้ ES2015 module มีดังนี้

  • ให้ import เฉพาะสิ่งที่จำเป็นต้องใช้จริงๆ
import _ from 'lodash'

_.map([1, 2, 3], item => item * 2)

// จากตัวอย่างนี้เรา import ทุกสิ่งที่ lodash มีมาใส่ตัวแปร _
// ทั้งๆที่ความจริงเราใช้แค่ map
// วิธีนี้จะทำให้ไฟล์ผลลัพธ์ของเรามีขนาดใหญ่ เพราะอุดมไปด้วยของที่ไม่ใช้
// วิธีต่อไปนี้จึงเหมาะสมกว่า

import map from 'lodash/map'
map([1, 2, 3], item => item * 2)
  • นอกจากนี้เพื่อนๆควรตรวจสอบให้แน่ใจว่าไลบรารี่ที่เรานำมาใช้นั้นมีอะไรใน JavaScript ที่ใช้ทดแทนได้หรือไม่ เพื่อจะได้ไม่ทำให้ไฟล์ผลลัพธ์มีขนาดใหญ่จากการใช้ไลบรารี่เหล่านั้น เช่น จากตัวอย่างข้างบนพบว่าสามารถใช้ Array#map ใน JavaScript แทนได้โดยไม่ต้องพึ่ง lodash

  • ตรวจสอบให้แน่ใจว่าขั้นตอนการทำงานของ build tool ของคุณ แอบแปลงโค๊ดคุณเป็น CommonJS ก่อนหรือไม่ ถ้ามีการแปลงนั่นหมายความว่า แม้คุณจะ import บางสิ่งเข้ามา แต่ด้วยความเป็น CommonJS มันจะ import ทุกสรรพสิ่งแม้คุณไม่ต้องการ ตัวอย่างเช่นการใช้ import/export ใน Webpack1

// ถ้าเพื่อนๆใช้ Webpack1 แม้เราจะเลือกเฉพาะสามตัวนี้ให้ import เข้ามา
// แต่ด้วยความที่ Webpack1 แปลงเป็น CommonJS ก่อน
// import ที่เราทำจึงกลายเป็นการ import ทุกสิ่งเข้ามาในไฟล์อยู่ดี
// เพียงแต่มี map, filter และ reduce ที่เรียกใช้งานได้
import { map, filter, reduce } from 'lodash'

// ควรเปลี่ยนแปลงเป็นสิ่งนี้
import map from 'lodash/map'
import filter from 'lodash/filter'
import reduce from 'lodash/reduce'

สำหรับเพื่อนๆคนไหนที่เผลอเรียก import _ from ‘lodash’ ไปแล้วก็ไม่ต้องเสียใจ ผมมี babel plugin ตัวนึงมาฝาก ที่จะทำแปลงการ import ของเพื่อนๆให้เป็นตามที่ผมแนะนำ รออะไรอยู่เล่า โหลดเลยซิ

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

System.import('module_name')
  .then(module => { ... })
  // จัดการ error ในนี้
  .catch(error => ...)

Destructuring

Destructuring เป็นฟีเจอร์สำหรับการดึงส่วนของข้อมูลออกมาทำให้เราเขียนโค๊ดได้ง่ายขึ้น

let person = { 
  age: 24, 
  gender: 'male', 
  name: { 
    firstName: 'firstName', 
    lastName: 'lastName'
  } 
}

// ถ้าเราต้องการค่าเหล่านี้ออกจากอ็อบเจ็กต์ ต้องมาประกาศตัวแปรแบบนี้
let age = person.age 
let genger = person.gender
let name = person.name
let firstName = name.firstName
let lastName = name.lastName

// หากใช้ Destructuring จะเหลือแค่นี้
let { age, gender, name } = person
let { firstName, lastName } = name

// แต่ในความเป็นจริงแล้ว name เป็นเพียงแค่ทางผ่าน
// เราไม่ต้องการมัน เราต้องการแค่ firstName และ lastName
// จึงใช้ Destructuring ซ้อนเข้าไปอีกครั้ง
// เพียงเท่านี้ตัวแปร name ก็จะไม่เกิดขึ้นมาให้รำคาญใจ
let { age, gender, name: { firstName, lastName } } = person

รู้จักกับ Spread Operator

Spread Operator หรือผมขอเรียกมันง่ายๆว่าเครื่องหมายแตกตัวแล้วกัน เป็นจุดไข่ปลาสามจุด (…) ที่เอาไปวางหน้าอาร์เรย์หรืออ็อบเจ็กต์แล้วมีผลทำให้เครื่องหมายที่ครอบมันอยู่หลุดออก ดูตัวอย่างกันเลย

let obj1 = { a: 1, b: 2 }
let obj2 = { c: 3, d: 4 }
console.log({ ...obj1, ...obj2 }) // {"a":1,"b":2,"c":3,"d":4}

let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
console.log([...arr1, ...arr2]) // [1,2,3,4,5,6]

สารพัดวิธีแบบใหม่ในการจัดการกับฟังก์ชัน

ES2015 มาพร้อมกับความสามารถในการจัดการฟังก์ชันที่มากขึ้นดังนี้

Arrow Function

จากเดิมที่เราต้องประกาศฟังก์ชันด้วยการใช้คีย์เวิร์ด function ใน ES2015 เราลดรูปการประกาศให้เหลือเพียงลูกศรสองเส้นหรือ fat arrow เหมือนในภาษา CoffeeScript ดังนี้

// ES5
function(arguments) {

}

// ES2015
(arguments) => {

}

Arrow function ไม่ได้ต่างเพียงแค่ไวยากรณ์ที่เปลี่ยนไป แต่ arrow function นั้นยังเข้าถึง this จาก scope ที่ครอบมันอยู่ (Lexical binding) ดังนี้

function Dog() {
  this.color = 'white'
  
  setTimeout(function() { 
    // this ตัวนี้หมายถึง this ใน context ของฟังก์ชันนี้
    // จึงไม่มีการพิมพ์อะไรออกไป เพราะในฟังก์ชันนี้ this ไม่มีค่าของ color
    console.log(this.color) 
  }, 100)
}

new Dog()

// ถ้าต้องการให้พิมพ์ค่า color ออกมาต้องแก้ไขใหม่เป็น
function Dog() {
  this.color = 'white'
  let self = this
  
  setTimeout(function() { 
    // เรียกผ่านตัวแปร self แทน
    console.log(self.color) 
  }, 100)
}

// หรือใช้ arrow function ดังนี้
function Dog() {
  this.color = 'white'
  
  setTimeout(() => { 
    // this ของ arrow function นี้จะหมายถึง
    // this ตัวบน
    console.log(this.color) 
  }, 100)
}

กรณีของ Arrow Function ถ้าตัว body ของฟังก์ชันไม่ครอบด้วย {} และมี statement เดียว จะถือว่าค่านั้นคือค่าที่ return ออกจากฟังก์ชัน

const fn = () => 3

console.log(fn()) // 3

นอกจากนี้เราสามารถละ () ได้ถ้าพารามิเตอร์นั้นมีเพียงตัวเดียว

const arr = [1, 2, 3]
// มีค่าเท่ากับ arr.map((x) => x * x)
arr.map(x => x * x)

Default Values

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

// ES5
function foo(genger) {
  gender = gender || 'male;
}

// ES2015
function foo(gender = 'male') {

}

Named Parameters

ใน ES5 บางครั้งเราส่ง object เข้าไปในฐานะของ options พร้อมทั้งมีการตรวจสอบค่าต่างๆในอ็อบเจ็กต์ ถ้าค่าไหนไม่มีก็จะกำหนดค่าเริ่มต้นให้ ใน ES2015 เราสามารถใช้ destructuring เพื่อทำสิ่งเดียวกันกับที่ทำใน ES5 ได้ดังนี้

// ES5
function request(options) {
  var method = options.method || 'GET'
  var ssl = options.ssl || false
  console.log(method)
}
request({})

// ES2015
function request({ method='GET', ssl=false }) {
  console.log(method)
}
request({})

นอกจากนี้เราอาจต้องการตรวจสอบค่าที่อ็อบเจ็กต์ที่ส่งเข้ามาก่อนด้วยว่ามีค่าหรือไม่ ถ้าไม่มีให้กำหนดค่าเริ่มต้นเป็น {} ดังนี้

// ES5
function request(options) {
  options = options || {}
  var method = options.method || 'GET'
  var ssl = options.ssl || false
  console.log(method)
}
request()

// ES2015
// กำหนดค่าเริ่มต้นเป็น {}
function request({ method='GET', ssl=false } = {}) {
  console.log(method)
}

Rest Parameters

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

// ไม่ว่าจะส่งตัวเลขเข้ามากี่ตัว ตัวแรกจะเป็น initial 
// ส่วนตัวอื่นจะเก็บอยู่ในอาร์เรย์ชื่อ rest
function sum(initial, ...rest) {
  return rest.reduce((prev, cur) => prev + cur, initial)
}

console.log(sum(10, 1, 2, 3)) // 16

ลดความซ้ำซ้อนด้วยการเขียนให้สั้นลง

ใน ES2015 นั้นเพื่อนๆสามารถลดรูปการประกาศฟังก์ชันในอ็อบเจ็กต์ โดยละคีย์เวิร์ด function ดังนี้

// ES5
const obj = {
  foo: function() {
  
  }
}

// ES2015
const obj = {
  foo() {
  
  }
}

นอกจากนี้ถ้า key ของอ็อบเจ็กต์มีชื่อตรงกับตัวแปลที่จะใส่เข้าไปเป็น value แล้ว เพื่อนๆสามารถละรูปได้เช่นกันดังนี้

// ES5
const foo = 'foo'
const bar = 'bar'

const obj = {
  foo: foo,
  bar: bar
}

// ES2015
const foo = 'foo'
const bar = 'bar'

const obj = {
  foo,
  bar
}

Template String

เมื่อก่อนเราใช้ + เพื่อต่อข้อความ แต่สำหรับ ES2015 นั้น Template String ทำให้การต่อข้อความเป็นเรื่องง่ายขึ้น ใช้ `` ครอบข้อความที่จะทำเป็น template string จากนั้นใช้ ${} สำหรับส่วนที่ต้องการแทรกส่วนของ JavaScript

const name = 'Nuttavut Thongjor'
console.log(`สวัสดีชาวโลก ผมชื่อ${name}`)

// นอกจากนี้ template string ยังเอื้อต่อการทำ multiline string ด้วย
const longString = `
  Lorem Ipsum is simply dummy text of the printing 
  and typesetting industry. 
  Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, 
  when an unknown printer took a galley of type and scrambled 
  it to make a type specimen book. 
`

การใช้งานคลาสใน ES2015

เป็นที่ทราบกันดีว่า JavaScript นั้นเป็นภาษาแบบ prototype-based เราจำลองฟังก์ชันให้เป็นคลาสและสร้างเมธอดผ่าน prototype นั่นเป็นสิ่งที่เราทำมากันเสมอ ใน ES2015 ได้นำการสร้างคลาสเข้ามาสู่ไวยากรณ์ทางภาษา แต่นั่นไม่ได้หมายความว่า prototype-based แบบเดิมๆจะหายไป

// ประกาศคลาสผ่านคีย์เวิร์ด class
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  
  static species = 'Homo sapiens sapiens'
  
  walk() {
    console.log("I'm walking...")
  }
  
  print() {
    console.log(`My name is ${this.name}`)
    console.log(`I'm ${this.age}'`)
  }
}

const person = new Person('MyName', 99)
person.walk() // I'm walking...
person.print() // My name is MyName \n I'm 99'

// static method เรียกตรงผ่านคลาสได้เลย
console.log(Person.species) // Homo sapiens sapiens

// สำหรับการทำ inheritance สามารถใช้คีย์เวิร์ด extends ดังนี้
class Female extends Person {

}

Promise

เพื่อไม่ให้เป็นการเขียนบทความทับซ้อน เชิญเพื่อนๆอ่านบทความที่ผมเคยเขียนแล้วในเรื่องกำจัด Callback Hell ด้วย Promise และ Async/Await

Lodash และ ES2015

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

[1, 2, 3].includes(1)
'Hello World'.startsWith('Hello')

บทความนี้ต้องการนำเสนอ ES2015 ที่น้อยที่สุดที่ควรรู้ ความจริงแล้ว ES2015 เป็นเรื่องค่อนข้างใหญ่ มีหลายหัวข้อที่เปิดมิติการเรียนรู้ใหม่ๆในโลก JavaScript เพื่อนๆที่สนใจสามารถอ่านเพิ่มเติมได้ครับ


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


Nuttavut Thongjor10 เดือนที่ผ่านมา

😃


ไม่ระบุตัวตน10 เดือนที่ผ่านมา

กราบงามๆหนึ่งครั้ง ขอบคุณครับ