JS-105 รู้หรือไม่ Import/Require มันต่างกันกว่าที่คิด !!
วันนี้จะมาพูดถึงการใช้งาน Import กับ Require กัน หลายๆคนที่เขียน JavaScript น่าจะเคยใช้สองคำสั่งนี้กันอยู่แล้ว แต่รู้หรือไม่ว่ามันต่างกันกว่าที่คิด และถ้าคุณไม่ใช้อย่างระมัดระวังละก็ มีหวัง Bugs ตรึม !!
CommonJS Modules (using “Require”)
CommonJS คือรูปแบบการเขียน JavaScript แบบแบ่ง Code ออกเป็นสัดเป็นส่วนหรือเป็น Unit ย่อยๆ เพื่อให้ง่ายต่อการพัฒนาโปรเจคขนาดใหญ่ขึ้น จุดแข็งคือการทำงานแบบ Synchronous
ตัวอย่าง
// somefile.js
exports.log = function (text) {
console.log(text)
}// anotherfile.js
const logFn = require('./somefile.js')// using
logFn('Hello World')
ES6 Modules (using “Import”)
ในสมัยก่อนนั้น JavaScript ยังไม่สามารถจัดการเรื่องของ Modules ได้ด้วยตัวเอง ทำให้ต้องใช้ Libs ต่างๆเข้ามาช่วยจัดการ เช่น CommonJS/AMD เป็นต้น ซึ่งการมาของ ES2015/ES6 นั้นได้มีการอัพเดทฟังก์ชั่น ES6 Modules เข้ามาทำให้ตัว JavaScript สามารถจัดการการเขียนแบบ Modules ได้ด้วยตัวเองแล้ว
ตัวอย่าง
// somefile.js
export const text = 'Hello World'// anotherfile.js
import { text } from 'somefile.js'console.log(text)
CommonJS vs ES6 Modules
อธิบายพื้นฐานกันไปแล้ว จริงๆหลายๆคนน่าจะรู้จักและเคยใช้กันมาบ้าง เรามาว่าถึงความแตกต่างและข้อควรระวังกันดีกว่า ผมคิดว่าหลายๆท่านอาจจะไม่เคยทราบมาก่อนก็ได้นะ มาลองดูตัวอย่างกัน
ลองเขียน JS แบบ CommonJS กันก่อน
// lib.js
var number = 0
function increaseNumber() {
number++
}
module.exports = {
number: number,
increaseNumber: increaseNumber
}
ทีนี้มาลองเขียน Code ที่ทำงานเหมือนกันในแบบ ES6 Modules บ้าง
// lib.js
export let number = 0
export function increaseNumber() {
number++
}
เพื่อนๆคิดว่า การเขียน Code ด้วย 2 วิธีข้างต้นนั้น ให้ผลลัพธ์แตกต่างกันหรือไม่ ??
ผมให้เวลาคิดสัก 2–3 นาที… ติ๊ก ต่อก .. ติ๊กกก …. ต่อก ..
….
..
…….. มาลองไปด้วยกันทั้ง 2 วิธีกันโลดดด
CommonJS Example Usage
มาลองใช้งานแบบ CommonJS กันก่อน
var libs = require('./lib')console.log(libs.number) // = 0
libs.increaseNumber()
console.log(libs.number) // = 0
จากตัวอย่างข้างต้น จะเห็นว่าค่า number นั้นไม่ได้ถูกเพิ่มค่าขึ้นด้วยฟังก์ชัน increaseNumber ! แต่ถ้าเราอยากให้มันเพิ่มล่ะต้องเขียนยังไง ?
var libs = require('./lib')console.log(libs.number) // = 0
libs.number++
console.log(libs.number) // = 1
การ Import ของ CommonJS นั้นจะเป็นการ Copy Object ออกมาใช้งาน ทำให้ค่า number นั้นถูก copy ออกมาด้วย ดังนั้นการเรียกใช้งาน function increaseNumber ไม่ส่งผลต่อค่า number ที่ถูก copy ออกมาแล้วนั่นเอง แต่อย่างไรก็ตาม libs.number ที่ถูก Copy ออกมานั้นยังคงเปลี่ยนแปลงค่าได้เสมอ ทำให้การบวกค่าเข้าไปตรงๆด้วยคำสั่ง libs.number++ ทำให้ค่าเปลี่ยนไป
ES6 Modules Example Usage
ในทางกลับกัน ES6 Modules นั้นทำงานต่างจาก CommonJS โดยสิ้นเชิง แต่ก่อนอื่นมาดูวิธีเรียกใช้กันก่อน
import { number, increaseNumber } from './lib'
console.log(number) // 0
increaseNumber()
console.log(number) // 1
จะเห็นว่าผลลัพธ์การเรียกใช้ของทั้ง 2 วิธีนั้นแตกต่างกันเพราะ ES6 Modules นั้นทำงานแบบ import bindings ดังนั้นค่าตัวแปรจึง references ถึงกันเสมอ ทำให้มีความอันตรายอยู่ หากใช้อย่างไม่ระวังหรือไม่เข้าใจมันดีพอ
จริงๆการ Bindings ของ ES6 Modules นั้นทำมาแก้ปัญหา “Cyclic Dependencies” ของ CommonJS แต่ก็ไม่ได้แก้ปัญหาได้ 100% เสียทีเดียว หากสนใจก็ไปอ่านเพิ่มเติมได้ทื่ References เลยครับ เพราะมันค่อนข้างละเอียดพอสมควร
สรุป
ไม่ว่าจะเขียนด้วยวิธีไหนสิ่งที่ต้องคำนึงถึงมากที่สุดคือการเข้าใจในสิ่งที่เราเขียน เพราะจะทำให้ระบบนั้นมี Bugs น้อยที่สุดเท่าที่จะเป็นไปได้ หรือถ้าเกิดมี Bugs ขึ้นมาเราก็จะรู้ว่าเกิดขึ้นเพราะอะไรและสามารถแก้ไขได้ทันท่วงทีครับผม
สุดท้ายก็มีข้อควรระวังนิดหน่อยสำหรับคนที่อยากจะไปลองเล่นดู คือการใช้งานบน Browser อาจจะต้อง setup Babel/Webpack มาช่วย หรือถ้าอยากลองเล่นด้วย Node.js ตัว ES6 Modules นั้นจะต้องใช้ Node version 12+ และต้องเขียนเป็น .mjs หรือระบุ type module ใน package.json ด้วย และต้องใส่ option — experimental-modules เพิ่มตอนจะ run อีก โอย… ยุ่งยากเป็นบ้า
แต่…. ถ้าใครขี้เกียจก็เข้าไปดู Code ตัวอย่างของผมผมได้ ที่นี่