JavaScript: มีอะไรใหม่บ้างใน ES2022
ES2022 มาตราฐานใหม่ของ JavaScript ได้ปล่อยออกมาแล้ว หลายฟีเจอร์เราอาจคุ้นเคยผ่านการใช้งาน Node.js หรือ TypeScript มาบ้างแล้ว เพื่อไม่ให้เป็นการเสียเวลาเรามาดูกันดีกว่าว่า ES2022 นั้นมีฟีเจอร์ใหม่อะไรบ้าง
Error Cause
ข้อผิดพลาดที่เกิดขึ้นตอน runtime หรือที่เรียกว่า errors นั้นมักประกอบด้วยข้อมูลที่เกี่ยวข้องกับมัน เช่น error message
ทว่าหากเราสร้างข้อผิดพลาดขึ้นมาเองผ่านการเรียก throw
บางครั้งเราก็อยากใส่รายละเอียดเพิ่มเข้าไปอีกว่าทำไม
error ที่เราสร้างใหม่นี้จึงเกิดขึ้น โดยทั่วไปแล้วเราสามารถสร้างคลาสสำหรับข้อผิดพลาดขึ้นมาใหม่เพื่อตอบสนองความต้องการนี้ได้
1class FetchArticleError extends Error {2 constructor(message, cause) {3 super(message);4 this.cause = cause;5 }6}78// ตัวอย่างการเรียกใช้ FetchArticleError9async function fetchArticle() {10 try {11 const res = await fetch('/not/found');12 const data = await res.json();1314 return data;15 } catch (err) {16 // สาเหตุของ Error ใหม่คือ err17 throw new FetchArticleError('Fetch article failed', err);18 }19}
เราเรียกการดำเนินการเพื่อห่อหุ่ม error เก่าด้วย error ใหม่ในลักษณะนี้ว่า Chaining Errors
สำหรับ ES2022 ได้เพิ่มพารามิเตอร์ตัวที่สองของ Error()
พารามิเตอร์นี้เป็นออบเจ็กต์แบบ Optional คือส่งค่านี้หรือไม่ก็ได้
หนึ่งในพร็อพเพอร์ตี้คือ cause เป็นส่วนที่ใช้ระบุว่า Error ใหม่ที่สร้างเกิดมาจากสาเหตุใดด้วยการส่ง Error เก่าไปเป็น cause
การเปลี่ยนรูปแบบโค้ดจากที่ต้องสร้าง FetchArticleError เป็นการใช้ cause แทน จะได้โค้ดใหม่ดังนี้
1async function fetchArticle() {2 try {3 const res = await fetch('/not/found');4 const data = await res.json();56 return data;7 } catch (err) {8 throw new Error('Fetch article failed', { cause: err });9 }10}
RegExp Match Indices
มี Flag หลายตัวที่ถูกใช้งานร่วมกับการสร้าง RegExp เช่น g (Global) หรือ y (Sticky) เป็นต้น สำหรับ ES2022 d (Indices) เป็น Flag ใหม่ที่ถูกเพิ่มขึ้นมาเพื่อให้ผลลัพธ์ของการหาผลลัพธ์เช่นจากการเรียกเมธอด exec มีตัวบ่งชี้ว่าแต่ละกลุ่มของข้อมูลที่ค้นพบนั้นมีจุดเริ่มต้นและสิ้นสุดอยู่ที่ index หมายเลขใด
1const m = /(\w+)\s(\w+)/d.exec('>>Babel Coder<<');23// [[2, 13], [2, 7], [8, 13], (groups: undefined)];4// ข้อความที่ตรงกับ RegExp อยู่ในตำแหน่งที่ 2 - 125// ข้อความที่ตรงกลุ่มแรกอยู่ตำแหน่งที่ 2 - 66// ข้อความที่ตรงกลุ่มหลังอยู่ตำแหน่งที่ 8 - 137console.log(m.indices);
กรณีของการใช้ named groups สามารถเข้าถึง index เริ่มต้นและสิ้นสุดของแต่ละกลุ่มที่มีชื่อได้ผ่าน indices.groups
1const re = /\+(?<code>\d{2,3})\s(\d{2,3})-(\d{3})-(\d{3})/d;2const m = re.exec('+66 081-111-1111');34// [5// [ 0, 15 ],6// [ 1, 3 ],7// [ 4, 7 ],8// [ 8, 11 ],9// [ 12, 15 ],10// groups: { code: [ 1, 3 ] }11// ]12console.log(m.indices);1314// [ 1, 3 ]15console.log(m.indices.groups.code);
เมธอด at สำหรับทุกชนิดข้อมูลที่ทำ index ได้
ES2022 ได้เพิ่มเมธอด at ให้กับ Array String และ TypedArray เช่น Uint8Array เป็นต้น เมธอดนี้ใช้เพื่อเข้าถึงอีลีเมนต์ในตำแหน่ง index ที่สนใจ
1['H', 'e', 'l', 'l', 'o'].at(1); // e
สิ่งที่ at แตกต่างจากการใช้ []
คือ at สามารถเข้าถึง index นับจากท้ายสุดผ่านเลขติดลบได้
โดยตำแหน่งท้ายสุดถือเป็น -1 ตำแหน่งถัดไปในทิศทางย้อนไปด้านหน้าจะเป็น -2, -3, ... ตามลำดับ
1['H', 'e', 'l', 'l', 'o'].at(-4); // e
Object.hasOwn
ความสามารถในการตรวจสอบว่า property ที่สนใจเป็น property ของออบเจ็กต์ตนเองโดยไม่ได้รับมาจากการสืบทอด
เป็นคุณสมบัติของเมธอด Object.prototype.hasOwnProperty()
1const human = {2 species: 'H. sapiens',3};45const somchai = {6 name: 'Somchai',7 age: 24,8};910// species สืบทอดมาจาก human จึงไม่ใช่ property ของ somchai11console.log(somchai.hasOwnProperty('species')); // false12console.log(somchai.hasOwnProperty('name')); // true
เหตุเพราะ hasOwnProperty()
เป็นเมธอดบน Object.prototype
หากปราศจากซึ่ง Prototype
แล้วจึงเป็นไปไม่ได้ที่เมธอดดังกล่าวจะถูกเข้าถึงได้
1Object.create(null).hasOwnProperty('foo');2// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
ES2022 นำเสนอ Object.hasOwn เพื่อทั้งแก้ปัญหาดังกล่าวรวมถึงย่นย่อการเขียนโค้ดให้กระชับลง
ต่อไปนี้คือตัวอย่างของการแทนที่ hasOwnProperty()
ด้วย Object.hasOwn
1const human = {2 species: 'H. sapiens',3};45const somchai = {6 name: 'Somchai',7 age: 24,8};910console.log(Object.hasOwn(somchai, 'species')); // false11console.log(Object.hasOwn(somchai, 'name')); // true
Top-level await
สมมติเราต้องทำการโหลดการตั้งค่าจากเซิฟเวอร์มาก่อนการประมวลผลอื่นใดในแอพพลิเคชันของเรา จากนั้นนำค่า lang จาก setting เพื่อมากำหนดว่าจะแสดงคำทักทายด้วยภาษาปลายทางตาม lang อย่างไร เราอาจสร้างโค้ดตามความต้องการดังกล่าวได้ดังนี้
1// preferences.mjs2let setting;34async function load() {5 const res = await fetch('/api/v1/setting');6 setting = await res.json();7}89load();1011export { setting };1213// main.mjs14import i18n from 'i18n';15import { setting } from './preferences.mjs';1617const greet = () => i18n.t(setting.lang, 'greeting');1819greet();
โค้ดนี้ไม่สามารถทำงานได้จริงนั่นเพราะฟังก์ชัน load มีการทำงานแบบ Asynchonous ในขณะที่โมดูล setting ถูกโหลดโดย main
ค่าของตัวแปร setting จะยังคงเป็น undefined อยู่ เมื่อ greet ถูกเรียก setting.lang
จึงไม่สามารถทำงานได้อย่างถูกต้อง
เพื่อให้เราการันตีได้ว่า setting ต้องถูกโหลดมาก่อนเสมอ เราอาจแก้ไขปัญหานี้ได้ด้วยการใช้ Promise
1// preferences.mjs2let setting;34export default async function (load() {5 const res = await fetch('/api/v1/setting');6 setting = await res.json();7})()89export { setting };1011// main.mjs12import i18n from 'i18n';13import preferences, { setting } from './preferences.mjs';1415const greet = () => i18n.t(setting.lang, 'greeting');1617preferences.then(() => {18 greet();19});
วิธีการดังกล่าวแม้จะแก้ปัญหาได้ดีแต่ความซับซ้อนเป็นสิ่งที่รับไม่ได้ นอกจากนี้ผู้เรียกใช้งาน preferences.mjs ต้องทราบเสมอว่า การรอคอยผ่าน then เป็นสิ่งสำคัญ เหนือสิ่งอื่นใดโค้ดดังกล่าวอาจก่อให้เกิดปัญหาคือ race condition ได้
ES2022 แก้ปัญหานี้ด้วยการนำเสนอ Top-level await ด้วยวิธีการดังกล่าวจึงเป็นการใช้ระบบ modules อย่างคุ้มค่า ด้วยการผลักภาระการรอโค้ด Asynchonous ไปตัดสินใจด้วยตัวระบบ modules แทน
1// preferences.mjs2const res = await fetch('/api/v1/setting');3const setting = await res.json();45export { setting };67// main.mjs8import i18n from 'i18n';9import { setting } from './preferences.mjs';1011const greet = () => i18n.t(setting.lang, 'greeting');1213greet();
จากโค้ดข้างต้นจะไม่มีโค้ดใด ๆ ใน main.mjs ได้รับการทำงาน หากบรรดา await ทั้งหลายใน preferences.mjs ยังทำงานไม่เสร็จ
Class fields
ES2015 ประกาศไวยากรณ์สำหรับการสร้างคลาส เราจึงสามารถสร้างและกำหนดค่าของ instance variables (Fields) ได้ผ่าน constructor หรือกำหนดค่าโดยตรงไปยัง fields นั้น ๆ
1class Point {2 constructor(x, y) {3 this.x = x ?? 0;4 this.y = y ?? 0;5 }6}
ES2022 เราสามารถประกาศ Fields ให้เป็นส่วนหนึ่งของคลาสได้แล้ว
1class Point {2 x = 0;3 y = 0;45 constructor(x, y) {6 if (x) this.x = x;7 if (y) this.y = y;8 }9}
ลักษณะของ Fields ที่ประกาศด้วยวิธีดังกล่าวจะทำให้ Fields เหล่านั้นสามารถถูกเข้าถึงได้จากทั้งในและนอกคลาส
กรณีที่ต้องการให้ Fields ถูกเข้าถึงได้เฉพาะในคลาสเราต้องประกาศเป็น Private Fields ด้วยการใช้สัญลักษณ์ #
นำหน้า
1class Point {2 #x = 0;3 #y = 0;45 constructor(x, y) {6 if (x) this.#x = x;7 if (y) this.#y = y;8 }9}1011const p = new Point();12// Uncaught SyntaxError: Private field '#x' must be declared in an enclosing class13p.#x = 10;
เมธอดของคลาสก็สามารถเป็น private ได้ด้วยการใส่ #
นำหน้าเช่นเดียวกัน
1class Foo {2 #internalMethod() {}3}
ES2022 สามารถสร้าง Fields แบบ static โดยสามารถเป็นทั้งแบบ Public และ Private fields ได้
1class Foo {2 static publicField = 1;3 static #privateField = 2;45 static publicMethod() {}6 static #privateMethod() {}7}
แม้ว่าคลาสจะอนุญาตให้มี static fields อยู่แล้ว แต่ในบางสถานการณ์ที่ต้องการสร้าง fields ต่าง ๆ ด้วยการกำหนดเงื่อนไข นั้นเป็นไปได้ยากเช่นการกำหนด fields จากข้อมูลที่โหลดมาได้
1class Foo {2 static a;3 static b;4 static c;5}67const bar = getBar();8Foo.a = bar.a;9Foo.b = bar.b;10Foo.c = bar.c;
ES2022 นำเสนอ Static initialization blocks ที่ทำให้เราประกาศบลอคแบบ static ได้ ปัญหาดังกล่าวจึงสามารถแก้ไขได้ดังนี้
1class Foo {2 static a;3 static b;4 static c;56 static {7 const bar = getBar();8 this.a = bar.a9 this.b = bar.b10 this.c = bar.c11 }12}
จากโค้ดใหม่นี้ค้นพบว่าการรวมกลุ่มโค้ดเหล่านี้เข้าไว้ภายใต้ static block ของคลาสช่วยให้โค้ดดูมีความเป็นสัดส่วนและอ้างถึงความสัมพันธ์ของฟิลด์ใต้คลาสของมันเองได้
static blocks ยังช่วยให้เราเข้าถึง private fields ที่มีเพียงคลาสที่ประกาศฟิลด์นั้นเท่านั้นที่เข้าถึงได้
1let getBar;23export class Foo {4 #bar5 constructor(bar) {6 this.#bar = { data: bar };7 }89 static {10 getBar = (obj) => obj.#bar;11 }12}
Ergonomic brand checks สำหรับ Private Fields
ก่อนหน้านี้หากเราต้องการตรวจสอบว่ามี Private Field ที่สนใจอยู่ในออบเจ็กต์นั้นหรือไม่เราต้องใช้ try/catch ครอบทับ หากไม่พบ Field นั้นโค้ดของ catch จะได้รับการทำงาน
1class Foo {2 #bar;34 static isFoo(obj) {5 try {6 obj.#bar;7 return true;8 } catch {9 return false;10 }11 }12}1314Foo.isFoo({}); // false15Foo.isFoo(new Foo()); // true
สำหรับ ES2022 เราสามารถใช้ in ในการตรวจสอบโครงสร้างของออบเจ็กต์ได้ว่ามี Private Field ที่เราสนใจอยู่หรือไม่
1class Foo {2 #bar;34 static isFoo(obj) {5 return #bar in obj6 }7}8910Foo.isFoo({}); // false11Foo.isFoo(new Foo()); // true
สารบัญ
- Error Cause
- RegExp Match Indices
- เมธอด at สำหรับทุกชนิดข้อมูลที่ทำ index ได้
- Object.hasOwn
- Top-level await
- Class fields
- Ergonomic brand checks สำหรับ Private Fields