การคัดลอกเนื้อหาใน JavaScript โดยใช้ StructuredClone

ตอนนี้แพลตฟอร์มมาพร้อมกับ structuredClone() ซึ่งเป็นฟังก์ชันในตัวสำหรับการทำสำเนาแบบเจาะลึก

ก่อนหน้านี้ คุณต้องใช้วิธีแก้ปัญหาชั่วคราวและไลบรารีเพื่อสร้างสำเนาเชิงลึกของค่า JavaScript ตอนนี้แพลตฟอร์มมาพร้อมกับ structuredClone() ซึ่งเป็นฟังก์ชันในตัวสําหรับการคัดลอกระดับลึก

การรองรับเบราว์เซอร์

  • Chrome: 98
  • Edge: 98
  • Firefox: 94
  • Safari: 15.4

แหล่งที่มา

การคัดลอกค่าใน JavaScript เกือบจะแบบตื้นเสมอ ต่างจากแบบลึก ซึ่งหมายความว่าการเปลี่ยนแปลงค่าที่ฝังอยู่ลึกๆ จะปรากฏทั้งในสำเนาและต้นฉบับ

วิธีหนึ่งในการสร้างสำเนาระดับตื้นใน JavaScript โดยใช้โอเปอเรเตอร์การกระจายออบเจ็กต์ ...

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

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

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

อย่างไรก็ตาม การเพิ่มหรือเปลี่ยนแปลงพร็อพเพอร์ตี้ที่ฝังลึกจะส่งผลต่อทั้งสําเนาและต้นฉบับ ดังนี้

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

นิพจน์ {...myOriginal} จะวนซ้ำพร็อพเพอร์ตี้ (ที่นับได้) ของ myOriginal โดยใช้โอเปอเรเตอร์การกระจาย โดยจะใช้ชื่อและค่าของพร็อพเพอร์ตี้ และกำหนดค่าเหล่านั้นทีละรายการให้กับออบเจ็กต์ว่างที่สร้างขึ้นใหม่ ดังนั้น ออบเจ็กต์ที่ได้จะมีรูปร่างเหมือนกัน แต่จะมีรายการพร็อพเพอร์ตี้และค่าเป็นของตัวเอง ระบบจะคัดลอกค่าด้วย แต่ค่า JavaScript จะจัดการค่าที่เรียกว่าค่าพื้นฐานแตกต่างจากค่าที่ไม่ใช่ค่าพื้นฐาน อ้างอิงจาก MDN

ใน JavaScript ข้อมูลพื้นฐาน (ค่าพื้นฐาน ประเภทข้อมูลพื้นฐาน) คือข้อมูลที่ไม่ใช่ออบเจ็กต์และไม่มีเมธอด ประเภทข้อมูลพื้นฐานมี 7 ประเภท ได้แก่ สตริง ตัวเลข bigint บูลีน ไม่ได้ระบุ สัญลักษณ์ และค่า Null

MDN — Primitive

ระบบจะจัดการค่าที่ไม่ใช่แบบพื้นฐานเป็นการอ้างอิง ซึ่งหมายความว่าการคัดลอกค่านั้นเป็นเพียงการคัดลอกการอ้างอิงไปยังออบเจ็กต์พื้นฐานเดียวกันเท่านั้น ซึ่งส่งผลให้เกิดลักษณะการคัดลอกแบบตื้น

สำเนาที่ลึก

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

ก่อนหน้านี้ยังไม่มีวิธีง่ายๆ หรือสะดวกในการสร้างการคัดลอกค่าแบบเจาะลึกใน JavaScript ผู้ใช้จํานวนมากใช้ไลบรารีของบุคคลที่สาม เช่น ฟังก์ชัน cloneDeep() ของ Lodash วิธีที่พบบ่อยที่สุดในการแก้ปัญหานี้คือการแฮ็กโดยใช้ JSON

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

อันที่จริง การแก้ปัญหานี้ได้รับความนิยมมากจน V8 เพิ่มประสิทธิภาพ JSON.parse() อย่างจริงจัง โดยเฉพาะรูปแบบข้างต้นเพื่อให้ทำงานได้เร็วที่สุด แม้ว่าจะรวดเร็ว แต่ก็มีจุดอ่อนและข้อควรระวังอยู่ 2-3 ข้อดังนี้

  • โครงสร้างข้อมูลที่เรียกซ้ำ: JSON.stringify() จะแสดงข้อผิดพลาดเมื่อคุณส่งโครงสร้างข้อมูลที่เรียกซ้ำ กรณีนี้เกิดขึ้นได้ค่อนข้างง่ายเมื่อทำงานกับลิงค์ลิสต์หรือต้นไม้
  • ประเภทในตัว: JSON.stringify() จะแสดงข้อผิดพลาดหากค่ามี JS ในตัวอื่นๆ เช่น Map, Set, Date, RegExp หรือ ArrayBuffer
  • ฟังก์ชัน: JSON.stringify() จะทิ้งฟังก์ชันโดยไม่มีการแจ้งเตือน

การโคลนแบบมีโครงสร้าง

แพลตฟอร์มจําเป็นต้องสร้างสําเนาค่า JavaScript แบบเจาะลึกในบางตำแหน่งอยู่แล้ว นั่นคือ การจัดเก็บค่า JS ใน IndexedDB ต้องใช้การทําให้เป็นอนุกรมรูปแบบหนึ่งเพื่อให้จัดเก็บไว้ในดิสก์ได้ และทําให้เป็นอนุกรมอีกครั้งในภายหลังเพื่อกู้คืนค่า JS ในทํานองเดียวกัน การส่งข้อความไปยัง WebWorker ผ่าน postMessage() จะต้องโอนค่า JS จากขอบเขต JS หนึ่งไปยังอีกขอบเขตหนึ่ง อัลกอริทึมที่ใช้ในการดำเนินการนี้เรียกว่า "การโคลนแบบมีโครงสร้าง" ซึ่งนักพัฒนาแอปเข้าถึงได้ยากจนกระทั่งเมื่อไม่นานมานี้

แต่ตอนนี้ได้เปลี่ยนไปแล้ว มีการแก้ไขข้อกำหนด HTML เพื่อแสดงฟังก์ชันชื่อ structuredClone() ที่ทำงานตามอัลกอริทึมดังกล่าวอย่างตรงที่สุด เพื่อเป็นช่องทางให้นักพัฒนาซอฟต์แวร์สร้างสำเนาเชิงลึกของค่า JavaScript ได้อย่างง่ายดาย

const myDeepCopy = structuredClone(myOriginal);

เท่านี้ก็เรียบร้อย นี่คือ API ทั้งหมด หากต้องการดูรายละเอียดเพิ่มเติม โปรดดูบทความ MDN

ฟีเจอร์และข้อจำกัด

การโคลนแบบมีโครงสร้างช่วยแก้ไขข้อบกพร่องหลายประการ (แต่ไม่ใช่ทั้งหมด) ของเทคนิค JSON.stringify() การโคลนแบบมีโครงสร้างสามารถจัดการโครงสร้างข้อมูลที่วนซ้ำ รองรับประเภทข้อมูลในตัวจํานวนมาก และโดยทั่วไปจะมีประสิทธิภาพมากขึ้นและมักจะเร็วกว่า

อย่างไรก็ตาม ฟีเจอร์นี้ยังมีข้อจำกัดบางอย่างที่อาจทำให้คุณไม่ทันตั้งตัว ดังนี้

  • โปรโตไทป์: หากใช้ structuredClone() กับอินสแตนซ์ของคลาส คุณจะได้รับออบเจ็กต์ธรรมดาเป็นค่าที่แสดงผล เนื่องจากการโคลนแบบมีโครงสร้างจะทิ้งเชนโปรโตไทป์ของออบเจ็กต์
  • ฟังก์ชัน: หากออบเจ็กต์มีฟังก์ชัน structuredClone() จะแสดงข้อยกเว้น DataCloneError
  • ไม่สามารถทำซ้ำได้: ค่าบางค่าไม่ใช่ Structured Data ที่สามารถทำซ้ำได้ โดยเฉพาะอย่างยิ่ง Error และโหนด DOM ซึ่งจะทําให้ structuredClone() แสดงข้อผิดพลาด

หากข้อจำกัดเหล่านี้เป็นข้อจำกัดที่ทำให้คุณไม่สามารถใช้กรณีการใช้งานได้ ไลบรารีอย่าง Lodash ยังคงมีการใช้งานที่กำหนดเองสำหรับอัลกอริทึมการโคลนจากข้อมูลเชิงลึกอื่นๆ ที่อาจหรือไม่เหมาะกับกรณีการใช้งานของคุณ

ประสิทธิภาพ

แม้ว่าเราจะไม่ได้ทำการเปรียบเทียบการทดสอบประสิทธิภาพแบบละเอียดใหม่ แต่เราได้ทำการเปรียบเทียบเมื่อต้นปี 2018 ก่อนที่จะเปิดตัว structuredClone() สมัยนั้น JSON.parse() เป็นตัวเลือกที่เร็วที่สุดสำหรับวัตถุขนาดเล็กมาก เราคาดว่าจะยังคงเหมือนเดิม เทคนิคที่อาศัยการโคลนแบบมีโครงสร้างทำงานได้เร็วกว่า (อย่างมาก) สำหรับวัตถุขนาดใหญ่ เนื่องจาก structuredClone() เวอร์ชันใหม่ไม่มีค่าใช้จ่ายเพิ่มเติมจากการละเมิด API อื่นๆ และมีประสิทธิภาพมากกว่า JSON.parse() เราจึงขอแนะนำให้คุณใช้ structuredClone() เป็นแนวทางเริ่มต้นในการสร้างสำเนาโดยละเอียด

บทสรุป

หากต้องการสร้างการคัดลอกค่าใน JS แบบเจาะลึก ซึ่งอาจเป็นเพราะคุณใช้โครงสร้างข้อมูลที่แก้ไขไม่ได้ หรือต้องการตรวจสอบว่าฟังก์ชันสามารถจัดการออบเจ็กต์โดยไม่ส่งผลต่อออบเจ็กต์ต้นฉบับ คุณไม่จำเป็นต้องใช้วิธีแก้ปัญหาชั่วคราวหรือไลบรารีอีกต่อไป ตอนนี้ระบบนิเวศ JS มี structuredClone() เยี่ยมไปเลย