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

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

ก่อนหน้านี้ คุณต้องใช้วิธีแก้ปัญหาชั่วคราวและไลบรารีเพื่อสร้างสำเนาเชิงลึกของค่า 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

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

สำเนาเชิงลึก

ส่วนที่ตรงกันข้ามกับสำเนาแบบตื้นๆ ก็คือการคัดลอกแบบ Deep อัลกอริทึมการคัดลอกแบบลึกจะคัดลอกพร็อพเพอร์ตี้ของออบเจ็กต์ทีละรายการด้วย แต่เรียกใช้ตัวเองแบบซ้ำซ้อนเมื่อพบการอ้างอิงถึงออบเจ็กต์อื่น ซึ่งจะสร้างสำเนาของออบเจ็กต์นั้นด้วย ซึ่งอาจมีความสำคัญมากในการตรวจสอบว่าโค้ด 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
  • ไม่สามารถโคลนได้: ค่าบางค่าไม่สามารถโคลนแบบมีโครงสร้างได้ ที่สำคัญคือ Error และโหนด DOM ซึ่งจะทําให้ structuredClone() แสดงข้อผิดพลาด

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

ประสิทธิภาพ

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

บทสรุป

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