ความแตกต่างพื้นฐานประการหนึ่งของวัตถุกับวัตถุดั้งเดิมคือวัตถุจะถูกจัดเก็บและคัดลอก "โดยการอ้างอิง" ในขณะที่ค่าดั้งเดิม: สตริง ตัวเลข บูลีน ฯลฯ จะถูกคัดลอก "เป็นค่าทั้งหมด" เสมอ
นั่นเป็นเรื่องง่ายที่จะเข้าใจหากเรามองลึกลงไปเล็กน้อยว่าเกิดอะไรขึ้นเมื่อเราคัดลอกค่า
มาเริ่มกันด้วยคำดั้งเดิม เช่น สตริง
ที่นี่เราใส่สำเนา message
ลงใน phrase
:
ให้ข้อความ = "สวัสดี!"; ให้วลี = ข้อความ;
ด้วยเหตุนี้ เรามีตัวแปรอิสระสองตัว โดยแต่ละตัวจะเก็บสตริง "Hello!"
-
ค่อนข้างเห็นผลชัดเจนใช่ไหม?
วัตถุไม่เป็นเช่นนั้น
ตัวแปรที่กำหนดให้กับอ็อบเจ็กต์ไม่ได้จัดเก็บอ็อบเจ็กต์เอง แต่เป็น "ที่อยู่ในหน่วยความจำ" หรืออีกนัยหนึ่งคือ "การอ้างอิง"
ลองดูตัวอย่างตัวแปรดังกล่าว:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" -
และนี่คือวิธีการจัดเก็บจริงในหน่วยความจำ:
วัตถุถูกเก็บไว้ที่ไหนสักแห่งในหน่วยความจำ (ทางด้านขวาของภาพ) ในขณะที่ตัวแปร user
(ทางด้านซ้าย) มี "ข้อมูลอ้างอิง"
เราอาจนึกถึงตัวแปรอ็อบเจ็กต์ เช่น user
เหมือนกับแผ่นกระดาษที่มีที่อยู่ของอ็อบเจ็กต์นั้น
เมื่อเราดำเนินการกับอ็อบเจ็กต์ เช่น ใช้คุณสมบัติ user.name
เอ็นจิ้น JavaScript จะดูว่ามีอะไรอยู่ที่ที่อยู่นั้นและดำเนินการกับอ็อบเจ็กต์จริง
นี่คือเหตุผลว่าทำไมมันถึงสำคัญ
เมื่อตัวแปรวัตถุถูกคัดลอก การอ้างอิงจะถูกคัดลอก แต่ตัววัตถุเองจะไม่ถูกทำซ้ำ
ตัวอย่างเช่น:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" }; ให้ผู้ดูแลระบบ = ผู้ใช้; //คัดลอกข้อมูลอ้างอิง
ตอนนี้เรามีตัวแปรสองตัว โดยแต่ละตัวจะเก็บข้อมูลอ้างอิงไปยังวัตถุเดียวกัน:
อย่างที่คุณเห็น ยังคงมีวัตถุหนึ่งชิ้น แต่ตอนนี้มีตัวแปรสองตัวที่อ้างอิงถึงวัตถุนั้น
เราสามารถใช้ตัวแปรอย่างใดอย่างหนึ่งเพื่อเข้าถึงวัตถุและแก้ไขเนื้อหา:
ให้ผู้ใช้ = { ชื่อ: 'จอห์น' }; ให้ผู้ดูแลระบบ = ผู้ใช้; admin.name = 'พีท'; // เปลี่ยนโดยการอ้างอิง "ผู้ดูแลระบบ" การแจ้งเตือน (ชื่อผู้ใช้); // 'Pete' การเปลี่ยนแปลงจะเห็นได้จากการอ้างอิง "ผู้ใช้"
เหมือนกับว่าเรามีตู้ที่มีกุญแจสองดอกและใช้อันหนึ่ง ( admin
) เพื่อเข้าไปแก้ไข จากนั้นหากเราใช้คีย์อื่น ( user
) ในภายหลัง เรายังคงเปิด Cabinet เดิมและสามารถเข้าถึงเนื้อหาที่เปลี่ยนแปลงได้
วัตถุสองชิ้นจะเท่ากันก็ต่อเมื่อวัตถุทั้งสองเป็นวัตถุเดียวกัน
ตัวอย่างเช่น ที่นี่ a
และ b
อ้างอิงถึงวัตถุเดียวกัน ดังนั้นพวกมันจึงเท่ากัน:
ให้ = {}; ให้ ข = ก; // คัดลอกข้อมูลอ้างอิง การแจ้งเตือน (a == b); // จริง ทั้งสองตัวแปรอ้างอิงวัตถุเดียวกัน การแจ้งเตือน (ก === ข); // จริง
และที่นี่วัตถุอิสระสองชิ้นไม่เท่ากัน แม้ว่าจะดูเหมือนกัน (ทั้งคู่ว่างเปล่า):
ให้ = {}; ให้ข = {}; // สองวัตถุอิสระ การแจ้งเตือน (a == b); // เท็จ
สำหรับการเปรียบเทียบ เช่น obj1 > obj2
หรือการเปรียบเทียบกับแบบดั้งเดิม obj == 5
วัตถุจะถูกแปลงเป็นวัตถุพื้นฐาน เราจะศึกษาว่าการแปลงออบเจ็กต์ทำงานอย่างไรในเร็วๆ นี้ แต่เพื่อบอกความจริง การเปรียบเทียบดังกล่าวมีน้อยมาก ซึ่งโดยปกติแล้วจะปรากฏเป็นผลมาจากข้อผิดพลาดในการเขียนโปรแกรม
วัตถุ Const สามารถแก้ไขได้
ผลข้างเคียงที่สำคัญของการจัดเก็บอ็อบเจ็กต์เป็นข้อมูลอ้างอิงก็คือ อ็อบเจ็กต์ที่ประกาศเป็น const
สามารถ แก้ไขได้
ตัวอย่างเช่น:
ผู้ใช้ const = { ชื่อ: "จอห์น" - user.name = "พีท"; - การแจ้งเตือน (ชื่อผู้ใช้); //พีท
อาจดูเหมือนว่าบรรทัด (*)
จะทำให้เกิดข้อผิดพลาด แต่ก็ไม่เป็นเช่นนั้น ค่าของ user
เป็นค่าคงที่ โดยจะต้องอ้างอิงวัตถุเดียวกันเสมอ แต่คุณสมบัติของวัตถุนั้นสามารถเปลี่ยนแปลงได้อย่างอิสระ
กล่าวอีกนัยหนึ่ง const user
จะให้ข้อผิดพลาดเฉพาะเมื่อเราพยายามตั้งค่า user=...
โดยรวม
ที่กล่าวว่าหากเราจำเป็นต้องสร้างคุณสมบัติของวัตถุคงที่จริงๆ ก็เป็นไปได้เช่นกัน แต่ใช้วิธีการที่แตกต่างกันโดยสิ้นเชิง เราจะกล่าวถึงสิ่งนั้นในบท Property flags and descriptors
ดังนั้น การคัดลอกตัวแปรออบเจ็กต์จะสร้างการอ้างอิงไปยังออบเจ็กต์เดียวกันอีกครั้ง
แต่ถ้าเราจำเป็นต้องทำซ้ำวัตถุล่ะ?
เราสามารถสร้างวัตถุใหม่และจำลองโครงสร้างของวัตถุที่มีอยู่ได้ โดยการวนซ้ำคุณสมบัติและคัดลอกวัตถุเหล่านั้นในระดับดั้งเดิม
แบบนี้:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" อายุ: 30 - ให้โคลน = {}; // วัตถุว่างใหม่ // ลองคัดลอกคุณสมบัติผู้ใช้ทั้งหมดลงไป สำหรับ (ให้ป้อนผู้ใช้) { โคลน [คีย์] = ผู้ใช้ [คีย์]; - // ตอนนี้โคลนเป็นวัตถุอิสระที่มีเนื้อหาเดียวกัน clone.name = "พีท"; // เปลี่ยนข้อมูลในนั้น การแจ้งเตือน (ชื่อผู้ใช้.ชื่อ); // จอห์นยังคงอยู่ในวัตถุดั้งเดิม
เรายังสามารถใช้วิธี Object.assign ได้ด้วย
ไวยากรณ์คือ:
Object.มอบหมาย (ปลายทาง ... แหล่งที่มา)
dest
อาร์กิวเมนต์แรกคือวัตถุเป้าหมาย
อาร์กิวเมนต์เพิ่มเติมคือรายการของออบเจ็กต์ต้นฉบับ
โดยคัดลอกคุณสมบัติของออบเจ็กต์ต้นทางทั้งหมดไปยังเป้าหมาย dest
แล้วส่งคืนเป็นผลลัพธ์
ตัวอย่างเช่น เรามีอ็อบเจ็กต์ user
มาเพิ่มการอนุญาตสองสามอย่าง:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" }; ให้สิทธิ์ 1 = { canView: true }; ให้สิทธิ์2 = { สามารถแก้ไข: จริง }; // คัดลอกคุณสมบัติทั้งหมดจากสิทธิ์ 1 และสิทธิ์ 2 ไปยังผู้ใช้ Object.มอบหมาย (ผู้ใช้, สิทธิ์ 1, สิทธิ์ 2); // ตอนนี้ผู้ใช้ = { ชื่อ: "John", canView: true, canEdit: true } การแจ้งเตือน (ชื่อผู้ใช้); // จอห์น การแจ้งเตือน (user.canView); // จริง การแจ้งเตือน (user.canEdit); // จริง
หากมีชื่อคุณสมบัติที่คัดลอกอยู่แล้ว ชื่อนั้นจะถูกเขียนทับ:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" }; Object.มอบหมาย (ผู้ใช้ { ชื่อ: "พีท" }); การแจ้งเตือน (ชื่อผู้ใช้); // ตอนนี้ผู้ใช้ = { ชื่อ: "พีท" }
เรายังสามารถใช้ Object.assign
เพื่อทำการโคลนวัตถุอย่างง่าย:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" อายุ: 30 - ให้ clone = Object.assign({}, ผู้ใช้); การแจ้งเตือน (โคลนชื่อ); // จอห์น การแจ้งเตือน (clone.age); // 30
ที่นี่จะคัดลอกคุณสมบัติทั้งหมดของ user
ลงในวัตถุว่างและส่งคืน
นอกจากนี้ยังมีวิธีอื่นในการโคลนวัตถุ เช่น การใช้ไวยากรณ์การแพร่กระจาย clone = {...user}
ซึ่งจะกล่าวถึงในภายหลังในบทช่วยสอน
จนถึงขณะนี้เราถือว่าคุณสมบัติทั้งหมดของ user
เป็นแบบดั้งเดิม แต่คุณสมบัติสามารถอ้างอิงถึงวัตถุอื่นได้
แบบนี้:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" ขนาด: { ส่วนสูง: 182, ความกว้าง: 50 - - การแจ้งเตือน ( user.sizes.height ); // 182
ตอนนี้การคัดลอก clone.sizes = user.sizes
ยังไม่เพียงพอ เนื่องจาก user.sizes
เป็นวัตถุ และจะถูกคัดลอกโดยการอ้างอิง ดังนั้น clone
และ user
จะใช้ขนาดเดียวกัน:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" ขนาด: { ส่วนสูง: 182, ความกว้าง: 50 - - ให้ clone = Object.assign({}, ผู้ใช้); การแจ้งเตือน ( user.sizes === clone.sizes ); // จริงวัตถุเดียวกัน // ผู้ใช้และขนาดการแชร์โคลน ผู้ใช้ขนาดความกว้าง = 60; // เปลี่ยนคุณสมบัติจากที่เดียว การแจ้งเตือน (clone.sizes.width); // 60 รับผลลัพธ์จากอันอื่น
เพื่อแก้ไขปัญหานั้นและทำให้ user
และ clone
แยกอ็อบเจ็กต์ออกจากกันอย่างแท้จริง เราควรใช้การโคลนนิ่งลูปที่จะตรวจสอบแต่ละค่าของ user[key]
และหากเป็นอ็อบเจ็กต์ ให้จำลองโครงสร้างของมันด้วย นั่นเรียกว่า "การโคลนแบบลึก" หรือ "การโคลนแบบมีโครงสร้าง" มีวิธี StructuredClone ที่ใช้การโคลนแบบลึก
โครงสร้างการโทร structuredClone(object)
โคลน object
ที่มีคุณสมบัติที่ซ้อนกันทั้งหมด
นี่คือวิธีที่เราสามารถใช้มันในตัวอย่างของเรา:
ให้ผู้ใช้ = { ชื่อ: "จอห์น" ขนาด: { ส่วนสูง: 182, ความกว้าง: 50 - - ให้ clone = StructuredClone (ผู้ใช้); การแจ้งเตือน ( user.sizes === clone.sizes ); // เท็จวัตถุที่แตกต่างกัน // ผู้ใช้และโคลนไม่เกี่ยวข้องกันโดยสิ้นเชิงในขณะนี้ ผู้ใช้ขนาดความกว้าง = 60; // เปลี่ยนคุณสมบัติจากที่เดียว การแจ้งเตือน (clone.sizes.width); // 50 ไม่เกี่ยวข้อง
เมธอด structuredClone
สามารถโคลนประเภทข้อมูลส่วนใหญ่ได้ เช่น ออบเจ็กต์ อาร์เรย์ ค่าดั้งเดิม
นอกจากนี้ยังรองรับการอ้างอิงแบบวงกลม เมื่อคุณสมบัติของวัตถุอ้างอิงถึงตัววัตถุเอง (โดยตรงหรือผ่านสายโซ่หรือการอ้างอิง)
ตัวอย่างเช่น:
ให้ผู้ใช้ = {}; // มาสร้างการอ้างอิงแบบวงกลมกันดีกว่า: // user.me อ้างอิงถึงตัวผู้ใช้เอง user.me = ผู้ใช้; ให้ clone = StructuredClone (ผู้ใช้); การแจ้งเตือน (clone.me === โคลน); // จริง
อย่างที่คุณเห็น clone.me
อ้างอิงถึง clone
ไม่ใช่ user
! ดังนั้นการอ้างอิงแบบวงกลมจึงถูกโคลนอย่างถูกต้องเช่นกัน
แม้ว่าจะมีบางกรณีที่ structuredClone
ล้มเหลว
ตัวอย่างเช่น เมื่อวัตถุมีคุณสมบัติฟังก์ชัน:
// ข้อผิดพลาด โครงสร้างโคลน({ ฉ: ฟังก์ชั่น() {} -
ไม่รองรับคุณสมบัติของฟังก์ชัน
ในการจัดการกรณีที่ซับซ้อนดังกล่าว เราอาจจำเป็นต้องใช้วิธีการโคลนนิ่งร่วมกัน เขียนโค้ดที่กำหนดเอง หรือหากไม่ต้องการสร้างวงล้อขึ้นมาใหม่ ให้ดำเนินการใช้งานที่มีอยู่ เช่น _.cloneDeep(obj) จากไลบรารี JavaScript lodash
ออบเจ็กต์ถูกกำหนดและคัดลอกโดยการอ้างอิง กล่าวอีกนัยหนึ่ง ตัวแปรไม่ได้เก็บ "ค่าอ็อบเจ็กต์" แต่เป็น "ข้อมูลอ้างอิง" (ที่อยู่ในหน่วยความจำ) สำหรับค่า ดังนั้นการคัดลอกตัวแปรดังกล่าวหรือส่งผ่านเป็นอาร์กิวเมนต์ของฟังก์ชันจะคัดลอกการอ้างอิงนั้น ไม่ใช่ตัววัตถุเอง
การดำเนินการทั้งหมดผ่านการอ้างอิงที่คัดลอก (เช่น การเพิ่ม/การลบคุณสมบัติ) จะดำเนินการบนออบเจ็กต์เดียวเดียวกัน
ในการสร้าง “สำเนาจริง” (โคลน) เราสามารถใช้ Object.assign
สำหรับสิ่งที่เรียกว่า “สำเนาตื้น” (วัตถุที่ซ้อนกันจะถูกคัดลอกโดยการอ้างอิง) หรือฟังก์ชัน “การโคลนลึก” structuredClone
หรือใช้การดำเนินการโคลนแบบกำหนดเอง เช่น เป็น _.cloneDeep (obj)