Deep Copy ออบเจ็กต์ใน JavaScript ด้วย structuredClone
ออบเจ็กต์ใน JavaScript นั้นประกอบด้วยพร็อพเพอร์ตี้ที่มีส่วนของ key และ value ตามแต่การนิยามของนักพัฒนา บ่อยครั้งที่เราต้องการสร้างออบเจ็กต์ใหม่ด้วยการสำเนาพร๊อพเพอร์ตี้จากออบเจ็กต์เดิม การสำเนาข้อมูลที่ปลอดภัยต้องไม่ทำให้ออบเจ็กต์ใหม่ได้รับผลกระทบเมื่อออบเจ็กต์เดิมเปลี่ยนค่าของพร็อพเพอร์ตี้ใด ๆ การสำเนาประเภทนี้เรียกว่า Deep Copy
บทความนี้เราจะนำไปสู่การทำสำเนาแบบ Deep Copy รูปแบบต่าง ๆ รวมถึงการใช้คำสั่งใหม่ใน JavaScript คือ structuredClone
การสำเนาออบเจ็กต์แบบ Shallow Copy
สำหรับภาษา JavaScript ตัวแปรที่มีชนิดข้อมูลเป็น Primitive Data Types เช่น string, number, boolean เป็นต้น เมื่อกำหนดค่าตัวแปรที่เก็บค่านี้ไปสำเนาให้ตัวแปรใหม่ค่าข้อมูลย่อมถูกสำเนา การเปลี่ยนแปลงที่ตัวแปรเดิมย่อมไม่กระทบค่าข้อมูลในตัวแปรใหม่
1let x = 20;2let y = x;34x = 30;5console.log(y); // 20
ตัวอย่างข้างต้นเมื่อกำหนดค่า x ให้กับ y ค่าข้อมูลจะถูกสำเนา เมื่อทำการเปลี่ยนค่าของ x ค่าเดิมของ y ย่อมไม่ได้รับผลกระทบ เนื่องจากทั้งสองต่างไม่ใช่ข้อมูลชุดเดียวกันอีกต่อไป
ไม่ใช่สำหรับชนิดข้อมูลแบบออบเจ็กต์ การกำหนดข้อมูลให้กับตัวแปรใหม่ไม่ใช่การสำเนาข้อมูลหากแต่เป็นการชี้ไปยังชุดข้อมูลเดียวกัน เมื่อข้อมูลที่ถูกชี้เปลี่ยนแปลง ทั้งตัวแปรต้นฉบับและตัวแปรใหม่ย่อมเห็นผลลัพธ์ใหม่จากการเปลี่ยนแปลงนั้น ๆ
1let x = { a: 1 };2let y = x;34x.a = 2;5console.log(y.a); // 2
ผลลัพธ์จากตัวอย่างข้างต้น เมื่อ x เป็นตัวแปรที่มีชนิดข้อมูลเป็นออบเจ็กต์ การกำหนดค่า x ให้กับ y ย่อมหมายถึงการแบ่งปันให้ y
ทำการชี้ตำแหน่งข้อมูลไปยังชุดข้อมูลเดียวกับที่ x ชี้อยู่คือ { a: 1}
เมื่อ x ทำการเปลี่ยนค่าของพร็อพเพอร์ตี้ a จึงยังผลให้ y เห็นค่า a เปลี่ยนไปด้วยเช่นกัน
การทำสำเนาใด ๆ ถ้ามีพร็อพเพอร์ตี้บางส่วนที่แชร์ข้อมูลเดียวกันกับตัวแปรเก่า (เกิดขึ้นในกรณีที่พร็อพเพอร์ตี้นั้นเป็นออบเจ็กต์) การทำสำเนานั้นจะถูกเรียกว่าการทำสำเนาแบบตี้นหรือ Shallow Copy
เราสามารถใช้ Object.assign
และ Spread Operator เพื่อสร้างการสำเนาแบบตื้นได้ ดังนี้
1const person = {2 name: 'Somchai',3 age: 24,4 tels: ['0811111111', '0822222222'],5};67const somchai = { ...person };89person.age = 25;10console.log(somchai.age); // 241112person.tels[0] = '0833333333';13console.log(somchai.tels[0]); // 0833333333
จากตัวอย่างข้างต้นพร็อพเพอร์ตี้ age ของ person มีชนิดข้อมูลเป็น number ซึ่งเป็นหนึ่งในพร๊อพเพอร์ตี้แบบ Primitive การเปลี่ยนแปลงค่า age ของ person จึงไม่กระทบกับ age ของ somchai เหตุการณ์นี้จะตรงกันข้ามกับ tels ที่เป็นออบเจ็กต์ประเภทอาร์เรย์ การเปลี่ยนแปลงค่า tels บน person ย่อมกระทบกับ tels ใน somchai เหตุนี้จึงกล่าวได้ว่าการใช้ Spread Operator (เครื่องหมายจุดสามตัว) เป็นการสร้างการสำเนาแบบตื้นนั่นเอง
Deep Copy ด้วย JSON.stringify และ JSON.parse
ตรงข้ามกับการสำเนาแบบตื้นคือ Deep Copy (Deep Clone) ที่เป็นการคัดลอกชุดข้อมูลของตัวแปรเดิมมาเป็นค่าใหม่ที่แยกอิสระจากพร็อพเพอร์ตี้ของตัวแปรเก่าโดยสิ้นเชิง การเปลี่ยนแปลงใด ๆ บนตัวแปรเก่าจึงไม่กระทบค่าของตัวแปรใหม่ ในภาษา JavaScript หนึ่งในวิธีสำเนาแบบ Deep Copy ทำได้โดยการใช้ JSON.stringify ควบคู่กับ JSON.parse
1const person = {2 name: 'Somchai',3 age: 24,4 tels: ['0811111111', '0822222222'],5};67const somchai = JSON.parse(JSON.stringify(person));89person.tels[0] = '0833333333';10console.log(somchai.tels[0]); // 0811111111
อย่างไรก็ตามการใช้ JSON.stringify ควบคู่กับ JSON.parse จะไม่สำเนาพร็อพเพอร์ตี้บางอย่างเช่น ฟังก์ชัน
1const person = {2 name: 'Somchai',3 age: 24,4 tels: ['0811111111', '0822222222'],5 printDetails() {6 console.log(this.name, this.age, this.tels);7 },8};910const somchai = JSON.parse(JSON.stringify(person));1112somchai.hasOwnProperty('printDetails'); // false
กรณีที่ต้องการทำ Deep Copy อย่างสมบูรณ์สามารถใช้ไลบรารี่อย่าง Lodash ผ่านฟังก์ชัน cloneDeep ได้ ดังนี้
1const somchai = _.cloneDeep(person);23somchai.hasOwnProperty('printDetails'); // true
Structure Cloning คืออะไร
ด้วยโครงสร้างของออบเจ็กต์ใน JavaScript ที่ถูกประกาศอย่างซับซ้อน การส่งค่าออบเจ็กต์ข้ามขอบเขต (Realm)
จึงเป็นเรื่องยาก นั่นเพราะออบเจ็กต์ดั้งเดิมอาจมีพร็อพเพอร์ตี้แบบออบเจ็กต์ที่ชี้ไปยังพื้นที่เก็บค่าข้อมูล การจะให้ออบเจ็กต์ใหม่ใน Realm อื่น
เข้าถึงพื้นที่จัดเก็บเดียวกันนั้นเป็นไปไม่ได้ ตัวอย่างเช่น หากเรามีออบเจ็กต์หนึ่งและต้องการส่งค่าออบเจ็กต์นี้ไปยัง Web Workers ผ่าน postMessage
ออบเจ็กต์ต้องได้รับการการันตีว่าตัวมันเองจะไม่มีการแชร์โครงสร้างภายในร่วมกันข้าม Realm
อีกหนึ่งกรณีของความซับซ้อนในการใช้งานออบเจ็กต์คือการจัดเก็บค่าออบเจ็กต์นี้ลงพื้นที่จัดเก็บ เช่น IndexedDB กระบวนการจัดเก็บหรือ Serialization ต้องการันตีได้ว่าออบเจ็กต์ที่จัดเก็บนั้นมีโครงสร้างที่จะถูกแปลงเพื่อการจัดเก็บได้สมบูรณ์ (Serializable Objects) นอกจากนี้เมื่อต้องการดึงค่าข้อมูลกลับมาผ่านขั้นตอนของ Deserialization ข้อมูลนั้นต้องแปลงกลับเป็นข้อมูลออบเจ็กต์ได้อย่างถูกต้องด้วย
เพื่อให้ขั้นตอนการส่งออบเจ็กต์ข้าม Realm เป็นไปอย่างสมบูรณ์ เราต้องการขั้นตอนของการทำ Serialization และ Deserialization ออบเจ็กต์ให้เป็นรูปแบบที่อิสระจากโครงสร้างของออบเจ็กต์เดิม ขั้นตอนวิธีนี้เรียกว่า Structure Cloning
Structure Cloning ถูกใช้เป็นกลไกภายในของคำสั่งต่าง ๆ เช่น postMessage
เมื่อมีการส่งค่าออบเจ็กต์ผ่านคำสั่งนี้
ออบเจ็กต์ดังกล่าวจะถูก Serialize ตามกลวิธีของ Structure Cloning และถูกทำ Deserialize อีกครั้งในฝั่งของ Web Worker
คำสั่ง structuredClone
structuredClone
เป็นคำสั่งสำหรับทำสำเนาแบบลึก (deep clone) ด้วยอัลกอริทึมของ Structure Cloning มีสองรูปแบบในการใช้งาน
1structuredClone(value);2structuredClone(value, { transfer });
สำหรับบทความนี้จะนำเสนอเฉพาะรูปแบบแรกที่ไม่มีส่วนของ transfer
1const person = {2 name: 'Somchai',3 age: 24,4 tels: new Set(['0811111111', '0822222222']),5};67const somchai = structuredClone(person);8console.log(person === somchai); // false9console.log(somchai.tels === person.tels); // false
จากตัวอย่างข้างต้น structuredClone
เป็นคำสั่งสำหรับการสำเนาแบบลึกที่ออบเจ็กต์ผลลัพธ์จะไม่ใช่ออบเจ็กต์เดิมอีกต่อไป
รวมถึงค่าข้อมูลของพร็อพเพอร์ตี้ต่าง ๆ แม้จะเป็นออบเจ็กต์ก็ยังคงได้รับการทำสำเนา
ขั้นตอนวิธีของ Structure Cloning
โดยทั่วไปแล้ว structuredClone
สามารถสำเนา Primitive Values ได้ทั้งหมด
1structuredClone(true) === true; // true2structuredClone(123) === 123; // true3structuredClone('hello') === 'hello'; // true
ออบเจ็กต์แบบ Built-in ที่มาพร้อมภาษาส่วนมาก เช่น Date, RegExp, Blob, File, FileList, ArrayBuffer, ArrayBufferView,
ImageBitmap, ImageData, Array, Map และ Set ก็สนับสนุนการทำงานกับ structuredClone
1const arr = [1, 2, 3];2const clone = structuredClone(arr);34Array.isArray(clone); // true5clone.length === 3; // true
กรณีของการสำเนา RegExp ค่าของพร็อพเพอร์ตี้ lastIndex
จะถูกรีเซ็ตเป็น 0 เสมอ
แม้ว่าออบเจ็กต์แบบ Built-in ส่วนใหญ่ล้วนใช้กับ structuredClone
ได้ แต่ก็มีบางส่วนที่จะเกิดข้อผิดพลาดเมื่อนำมาใช้กับคำสั่งนี้ เช่น ฟังก์ชัน หรือ DOM nodes
โดยข้อผิดพลาดที่เกิดขึ้นจะเป็นชนิด DOMException ที่มีชื่อว่า DataCloneError
1try {2 structuredClone(() => 'arrow function');3} catch (error) {4 console.log(error instanceof DOMException); // true5 console.log(error.name === 'DataCloneError'); // true6 console.log(error.code === DOMException.DATA_CLONE_ERR); // true7}89try {10 structuredClone({11 a: 1,12 b() {13 console.log('fn');14 },15 });16} catch (error) {17 console.log(error instanceof DOMException); // true18 console.log(error.name === 'DataCloneError'); // DataCloneError19 console.log(error.code === DOMException.DATA_CLONE_ERR);20}
นอกเหนือจากข้อจำกัดข้างต้น prototype chain ของออบเจ็กต์ต้นฉบับจะไม่ถูกสำเนาด้วยเช่นกัน
ทำให้การสำเนาออบเจ็กต์ของคลาสใด ๆ ออบเจ็กต์ใหม่จะไม่ถือเป็น instance ของคลาสนั้นอีกต่อไป
กล่าวคือเมื่อสำเนา instance ออบเจ็กต์ใหม่จะเหมือนออบเจ็กต์ทั่วไปที่ถูกคัดค่าพร็อพเพอร์ตี้จาก instance เดิม
พร้อมทำการตั้งค่า prototype เป็น Object.prototype
1class Foo {}23const ori = new Foo();4console.log(ori instanceof Foo); // true56const clone = structuredClone(ori);7console.log(clone instanceof Foo); // false8console.log(Object.getPrototypeOf(clone) === Foo.prototype); // false9console.log(Object.getPrototypeOf(clone) === Object.prototype); // true
ข้อจำกัดอีกประการของ structuredClone
คือค่าของ property attributes ในออบเจ็กต์อาจไม่ได้ค่าแบบเดิมเสมอ
โดย Accessors จะเปลี่ยนเป็น data properties และ property attributes ใหม่ทุกตัวจะมีค่า default
1const obj = Object.defineProperties(2 {},3 {4 accessor: {5 get: function () {6 return 'hello';7 },8 set: undefined,9 enumerable: true,10 configurable: true,11 },12 }13);14const clone = structuredClone(obj);15const desc = Object.getOwnPropertyDescriptors(clone);16console.log(desc);1718// {19// "accessor": {20// "value": "hello",21// "writable": true,22// "enumerable": true,23// "configurable": true24// }25// }
สรุป
เราสามารถทำ Deep Copy ได้ด้วยคำสั่ง structuredClone
ที่สนับสนุนทั้งบน Node, Deno และเว็บเบราว์เซอร์หลักทั่วไป
โดยต้องคำนึงถึงว่าไม่ใช่ทุก ๆ พร็อพเพอร์ตี้ที่สามารถสำเนาค่าได้ เช่น การสำเนาฟังก์ชันจะทำให้เกิดข้อผิดพลาด เป็นต้น
กรณีที่ต้องการสำเนาแบบ Deep Copy อย่างสมบูรณ์สามารถใช้คำสั่ง cloneDeep ของ Lodash ได้เช่นกัน
เอกสารอ้างอิง
Deep-copying in JavaScript using structuredClone. Retrieved March, 9, 2022, from https://web.dev/structured-clone/
Safe passing of structured data. Retrieved March, 9, 2022, from https://html.spec.whatwg.org/#safe-passing-of-structured-data
structuredClone(). Retrieved March, 9, 2022, from https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
structuredClone(): deeply copying objects in JavaScript. Retrieved March, 9, 2022, from https://2ality.com/2022/01/structured-clone.html
The structured clone algorithm. Retrieved March, 9, 2022, from https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#see_also
สารบัญ
- การสำเนาออบเจ็กต์แบบ Shallow Copy
- Deep Copy ด้วย JSON.stringify และ JSON.parse
- Structure Cloning คืออะไร
- คำสั่ง structuredClone
- ขั้นตอนวิธีของ Structure Cloning
- สรุป
- เอกสารอ้างอิง