JavaScript: มีอะไรใหม่บ้างใน ES2022

Nuttavut Thongjor

ES2022 มาตราฐานใหม่ของ JavaScript ได้ปล่อยออกมาแล้ว หลายฟีเจอร์เราอาจคุ้นเคยผ่านการใช้งาน Node.js หรือ TypeScript มาบ้างแล้ว เพื่อไม่ให้เป็นการเสียเวลาเรามาดูกันดีกว่าว่า ES2022 นั้นมีฟีเจอร์ใหม่อะไรบ้าง

Error Cause

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

JavaScript
1class FetchArticleError extends Error {
2 constructor(message, cause) {
3 super(message);
4 this.cause = cause;
5 }
6}
7
8// ตัวอย่างการเรียกใช้ FetchArticleError
9async function fetchArticle() {
10 try {
11 const res = await fetch('/not/found');
12 const data = await res.json();
13
14 return data;
15 } catch (err) {
16 // สาเหตุของ Error ใหม่คือ err
17 throw new FetchArticleError('Fetch article failed', err);
18 }
19}

เราเรียกการดำเนินการเพื่อห่อหุ่ม error เก่าด้วย error ใหม่ในลักษณะนี้ว่า Chaining Errors สำหรับ ES2022 ได้เพิ่มพารามิเตอร์ตัวที่สองของ Error() พารามิเตอร์นี้เป็นออบเจ็กต์แบบ Optional คือส่งค่านี้หรือไม่ก็ได้ หนึ่งในพร็อพเพอร์ตี้คือ cause เป็นส่วนที่ใช้ระบุว่า Error ใหม่ที่สร้างเกิดมาจากสาเหตุใดด้วยการส่ง Error เก่าไปเป็น cause

การเปลี่ยนรูปแบบโค้ดจากที่ต้องสร้าง FetchArticleError เป็นการใช้ cause แทน จะได้โค้ดใหม่ดังนี้

JavaScript
1async function fetchArticle() {
2 try {
3 const res = await fetch('/not/found');
4 const data = await res.json();
5
6 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 หมายเลขใด

JavaScript
1const m = /(\w+)\s(\w+)/d.exec('>>Babel Coder<<');
2
3// [[2, 13], [2, 7], [8, 13], (groups: undefined)];
4// ข้อความที่ตรงกับ RegExp อยู่ในตำแหน่งที่ 2 - 12
5// ข้อความที่ตรงกลุ่มแรกอยู่ตำแหน่งที่ 2 - 6
6// ข้อความที่ตรงกลุ่มหลังอยู่ตำแหน่งที่ 8 - 13
7console.log(m.indices);

กรณีของการใช้ named groups สามารถเข้าถึง index เริ่มต้นและสิ้นสุดของแต่ละกลุ่มที่มีชื่อได้ผ่าน indices.groups

JavaScript
1const re = /\+(?<code>\d{2,3})\s(\d{2,3})-(\d{3})-(\d{3})/d;
2const m = re.exec('+66 081-111-1111');
3
4// [
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);
13
14// [ 1, 3 ]
15console.log(m.indices.groups.code);

เมธอด at สำหรับทุกชนิดข้อมูลที่ทำ index ได้

ES2022 ได้เพิ่มเมธอด at ให้กับ Array String และ TypedArray เช่น Uint8Array เป็นต้น เมธอดนี้ใช้เพื่อเข้าถึงอีลีเมนต์ในตำแหน่ง index ที่สนใจ

JavaScript
1['H', 'e', 'l', 'l', 'o'].at(1); // e

สิ่งที่ at แตกต่างจากการใช้ [] คือ at สามารถเข้าถึง index นับจากท้ายสุดผ่านเลขติดลบได้ โดยตำแหน่งท้ายสุดถือเป็น -1 ตำแหน่งถัดไปในทิศทางย้อนไปด้านหน้าจะเป็น -2, -3, ... ตามลำดับ

JavaScript
1['H', 'e', 'l', 'l', 'o'].at(-4); // e

Object.hasOwn

ความสามารถในการตรวจสอบว่า property ที่สนใจเป็น property ของออบเจ็กต์ตนเองโดยไม่ได้รับมาจากการสืบทอด เป็นคุณสมบัติของเมธอด Object.prototype.hasOwnProperty()

JavaScript
1const human = {
2 species: 'H. sapiens',
3};
4
5const somchai = {
6 name: 'Somchai',
7 age: 24,
8};
9
10// species สืบทอดมาจาก human จึงไม่ใช่ property ของ somchai
11console.log(somchai.hasOwnProperty('species')); // false
12console.log(somchai.hasOwnProperty('name')); // true

เหตุเพราะ hasOwnProperty() เป็นเมธอดบน Object.prototype หากปราศจากซึ่ง Prototype แล้วจึงเป็นไปไม่ได้ที่เมธอดดังกล่าวจะถูกเข้าถึงได้

JavaScript
1Object.create(null).hasOwnProperty('foo');
2// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function

ES2022 นำเสนอ Object.hasOwn เพื่อทั้งแก้ปัญหาดังกล่าวรวมถึงย่นย่อการเขียนโค้ดให้กระชับลง ต่อไปนี้คือตัวอย่างของการแทนที่ hasOwnProperty() ด้วย Object.hasOwn

JavaScript
1const human = {
2 species: 'H. sapiens',
3};
4
5const somchai = {
6 name: 'Somchai',
7 age: 24,
8};
9
10console.log(Object.hasOwn(somchai, 'species')); // false
11console.log(Object.hasOwn(somchai, 'name')); // true

Top-level await

สมมติเราต้องทำการโหลดการตั้งค่าจากเซิฟเวอร์มาก่อนการประมวลผลอื่นใดในแอพพลิเคชันของเรา จากนั้นนำค่า lang จาก setting เพื่อมากำหนดว่าจะแสดงคำทักทายด้วยภาษาปลายทางตาม lang อย่างไร เราอาจสร้างโค้ดตามความต้องการดังกล่าวได้ดังนี้

JavaScript
1// preferences.mjs
2let setting;
3
4async function load() {
5 const res = await fetch('/api/v1/setting');
6 setting = await res.json();
7}
8
9load();
10
11export { setting };
12
13// main.mjs
14import i18n from 'i18n';
15import { setting } from './preferences.mjs';
16
17const greet = () => i18n.t(setting.lang, 'greeting');
18
19greet();

โค้ดนี้ไม่สามารถทำงานได้จริงนั่นเพราะฟังก์ชัน load มีการทำงานแบบ Asynchonous ในขณะที่โมดูล setting ถูกโหลดโดย main ค่าของตัวแปร setting จะยังคงเป็น undefined อยู่ เมื่อ greet ถูกเรียก setting.lang จึงไม่สามารถทำงานได้อย่างถูกต้อง

เพื่อให้เราการันตีได้ว่า setting ต้องถูกโหลดมาก่อนเสมอ เราอาจแก้ไขปัญหานี้ได้ด้วยการใช้ Promise

JavaScript
1// preferences.mjs
2let setting;
3
4export default async function (load() {
5 const res = await fetch('/api/v1/setting');
6 setting = await res.json();
7})()
8
9export { setting };
10
11// main.mjs
12import i18n from 'i18n';
13import preferences, { setting } from './preferences.mjs';
14
15const greet = () => i18n.t(setting.lang, 'greeting');
16
17preferences.then(() => {
18 greet();
19});

วิธีการดังกล่าวแม้จะแก้ปัญหาได้ดีแต่ความซับซ้อนเป็นสิ่งที่รับไม่ได้ นอกจากนี้ผู้เรียกใช้งาน preferences.mjs ต้องทราบเสมอว่า การรอคอยผ่าน then เป็นสิ่งสำคัญ เหนือสิ่งอื่นใดโค้ดดังกล่าวอาจก่อให้เกิดปัญหาคือ race condition ได้

ES2022 แก้ปัญหานี้ด้วยการนำเสนอ Top-level await ด้วยวิธีการดังกล่าวจึงเป็นการใช้ระบบ modules อย่างคุ้มค่า ด้วยการผลักภาระการรอโค้ด Asynchonous ไปตัดสินใจด้วยตัวระบบ modules แทน

JavaScript
1// preferences.mjs
2const res = await fetch('/api/v1/setting');
3const setting = await res.json();
4
5export { setting };
6
7// main.mjs
8import i18n from 'i18n';
9import { setting } from './preferences.mjs';
10
11const greet = () => i18n.t(setting.lang, 'greeting');
12
13greet();

จากโค้ดข้างต้นจะไม่มีโค้ดใด ๆ ใน main.mjs ได้รับการทำงาน หากบรรดา await ทั้งหลายใน preferences.mjs ยังทำงานไม่เสร็จ

Class fields

ES2015 ประกาศไวยากรณ์สำหรับการสร้างคลาส เราจึงสามารถสร้างและกำหนดค่าของ instance variables (Fields) ได้ผ่าน constructor หรือกำหนดค่าโดยตรงไปยัง fields นั้น ๆ

JavaScript
1class Point {
2 constructor(x, y) {
3 this.x = x ?? 0;
4 this.y = y ?? 0;
5 }
6}

ES2022 เราสามารถประกาศ Fields ให้เป็นส่วนหนึ่งของคลาสได้แล้ว

JavaScript
1class Point {
2 x = 0;
3 y = 0;
4
5 constructor(x, y) {
6 if (x) this.x = x;
7 if (y) this.y = y;
8 }
9}

ลักษณะของ Fields ที่ประกาศด้วยวิธีดังกล่าวจะทำให้ Fields เหล่านั้นสามารถถูกเข้าถึงได้จากทั้งในและนอกคลาส กรณีที่ต้องการให้ Fields ถูกเข้าถึงได้เฉพาะในคลาสเราต้องประกาศเป็น Private Fields ด้วยการใช้สัญลักษณ์ # นำหน้า

JavaScript
1class Point {
2 #x = 0;
3 #y = 0;
4
5 constructor(x, y) {
6 if (x) this.#x = x;
7 if (y) this.#y = y;
8 }
9}
10
11const p = new Point();
12// Uncaught SyntaxError: Private field '#x' must be declared in an enclosing class
13p.#x = 10;

เมธอดของคลาสก็สามารถเป็น private ได้ด้วยการใส่ # นำหน้าเช่นเดียวกัน

JavaScript
1class Foo {
2 #internalMethod() {}
3}

ES2022 สามารถสร้าง Fields แบบ static โดยสามารถเป็นทั้งแบบ Public และ Private fields ได้

JavaScript
1class Foo {
2 static publicField = 1;
3 static #privateField = 2;
4
5 static publicMethod() {}
6 static #privateMethod() {}
7}

แม้ว่าคลาสจะอนุญาตให้มี static fields อยู่แล้ว แต่ในบางสถานการณ์ที่ต้องการสร้าง fields ต่าง ๆ ด้วยการกำหนดเงื่อนไข นั้นเป็นไปได้ยากเช่นการกำหนด fields จากข้อมูลที่โหลดมาได้

JavaScript
1class Foo {
2 static a;
3 static b;
4 static c;
5}
6
7const bar = getBar();
8Foo.a = bar.a;
9Foo.b = bar.b;
10Foo.c = bar.c;

ES2022 นำเสนอ Static initialization blocks ที่ทำให้เราประกาศบลอคแบบ static ได้ ปัญหาดังกล่าวจึงสามารถแก้ไขได้ดังนี้

JavaScript
1class Foo {
2 static a;
3 static b;
4 static c;
5
6 static {
7 const bar = getBar();
8 this.a = bar.a
9 this.b = bar.b
10 this.c = bar.c
11 }
12}

จากโค้ดใหม่นี้ค้นพบว่าการรวมกลุ่มโค้ดเหล่านี้เข้าไว้ภายใต้ static block ของคลาสช่วยให้โค้ดดูมีความเป็นสัดส่วนและอ้างถึงความสัมพันธ์ของฟิลด์ใต้คลาสของมันเองได้

static blocks ยังช่วยให้เราเข้าถึง private fields ที่มีเพียงคลาสที่ประกาศฟิลด์นั้นเท่านั้นที่เข้าถึงได้

JavaScript
1let getBar;
2
3export class Foo {
4 #bar
5 constructor(bar) {
6 this.#bar = { data: bar };
7 }
8
9 static {
10 getBar = (obj) => obj.#bar;
11 }
12}

Ergonomic brand checks สำหรับ Private Fields

ก่อนหน้านี้หากเราต้องการตรวจสอบว่ามี Private Field ที่สนใจอยู่ในออบเจ็กต์นั้นหรือไม่เราต้องใช้ try/catch ครอบทับ หากไม่พบ Field นั้นโค้ดของ catch จะได้รับการทำงาน

JavaScript
1class Foo {
2 #bar;
3
4 static isFoo(obj) {
5 try {
6 obj.#bar;
7 return true;
8 } catch {
9 return false;
10 }
11 }
12}
13
14Foo.isFoo({}); // false
15Foo.isFoo(new Foo()); // true

สำหรับ ES2022 เราสามารถใช้ in ในการตรวจสอบโครงสร้างของออบเจ็กต์ได้ว่ามี Private Field ที่เราสนใจอยู่หรือไม่

JavaScript
1class Foo {
2 #bar;
3
4 static isFoo(obj) {
5 return #bar in obj
6 }
7}
8
9
10Foo.isFoo({}); // false
11Foo.isFoo(new Foo()); // true
สารบัญ

สารบัญ

  • Error Cause
  • RegExp Match Indices
  • เมธอด at สำหรับทุกชนิดข้อมูลที่ทำ index ได้
  • Object.hasOwn
  • Top-level await
  • Class fields
  • Ergonomic brand checks สำหรับ Private Fields