เจาะลึกความลับ Shallow และ Deep Copies ใน JavaScript (ตอนที่ 1)
Table of contents
Intro
สวัสดีครับ บทความนี้มีหัวข้อเกี่ยวกับ เจาะลึกความลับ Shallow และ Deep copies ใน JavaScript
ที่ผมได้พยายามอ่านและรวบรวมความเข้าใจเขียนเป็นบทความไว้ โดยมีรายละเอียดความคล้าย และความแตกต่างกันพอสมควร ซึ่งบทความมี 2 ตอนดังนี้
- เจาะลึกความลับ Shallow และ Deep Copies ใน JavaScript (ตอนที่ 1)
- เจาะลึกความลับ References ใน Shallow, Deep, Shadow Copies ใน JavaScript (ตอนที่ 2) (กำลังเขียน)
ในบทความนี้คุณจะได้เรียนรู้เกี่ยวกับความแตกต่างระหว่าง deep
และ shallow copies
ใน JavaScript
ทำไมเราจึงควรใช้วิธีสร้างแบบใด และเมื่อใดควรใช้อย่างใด เพื่อหลีกเลี่ยงปัญหาที่อาจเกิดขึ้นที่เราอาจจะเข้าใจผิด เพราะความไม่รู้ และเรื่องประสิทธิภาพ
ก่อนเริ่ม
ก่อนเริ่มเข้าสู่เนื้อหาจริง ๆ ที่พูดถึงการ copies เราควรเข้าใจก่อนว่ามีใน JavaScript มี 2 Data types
ที่มีความแตกต่าง
กันของ Value types คือ Primitive
และ Non-primitive
พื้นฐานประเภทตัวแปร
Key ที่สำคัญคือในทุก Programming language
- Primitives จะ passed by value
- Non-primitives จะ passed by reference
Primitive
Primitive เป็น Immutable
(ไม่มี
methods หรือ properties ที่สามารถเปลี่ยนแปลงได้
)
- string
- number
- booleans
- null
- undefined
- bigint
- symbol
Non-Primitive
Non-Primitive เป็น Mutable
(มี methods หรือ properties ที่สามารถเปลี่ยนแปลงได้
)
- object
- arrays
- function
Primitive vs. Non-Primitive
Primitive
- Primitive values มีคุณสมบ ัติ Immutable
- Primitive สามารถใช้ values compared ด้วย value
- Primitive Data types เป็น predefined
- Primitive Data types จะมีการกำหนด values
Non-Primitive
- Non-Primitive values มีคุณสมบัติ Mutable
- Non-Primitive เก็บ values ด้วย Address
- Non-Primitive สามารถ compare ด้วย reference
พิสูจน์ Non-primitives vs. Primitive
พิสูจน์ว่า String เป็น Immutable
const pokemon = 'Pikachu'
pokemon[0] = 'L'
console.log(pokemon) // Pikachu
จะเห็นว่าตัวแปร pokemon
เมื่อเราพยายามเปลี่ยนแปลงค่าตัวอักษรแรกจาก "P" เป็น "L" แต่ค่าเดิมก็ ไม่เปลี่ยนแปลง
❌
ทีนี้ลองมากำหนดเป็น strict mode
ดูว่าจะได้รับข้อความผิดพลาด (Errors) ออกมาแบบไหน 🔥
'use strict'
const pokemon = 'Pikachu'
pokemon[0] = 'L'
console.log(pokemon)
// TypeError: Cannot assign to read only property '0' of string 'Pikachu'
พิสูจน์ว่า Object เป็น Mutable
const pokemon = ['P', 'i', 'k', 'a', 'c', 'h', 'u']
pokemon[0] = 'L'
console.log(pokemon) // ['L', 'i', 'k', 'a', 'c', 'h', 'u']
เพราะ pokemon
เป็นประเภท Non-Primitive
ผลคือ สามารถเปลี่ยนค่าได้
✅
ปัญหาการ Copy
Primitive
ทีนี้มาลอง Copy ตัวแปรประเภท Primitive
แบบท่าเบสิก ๆ โดยเราไม่ต้องทำอะไรมาก มาดูผลลัพธ์
const pokemon = 'Pikachu'
const _pokemon = pokemon
console.log(language) // Pikachu
console.log(_language) // Pikachu
ผลลัพธ์โอเค ✅ และควรจะเป็นอย่างงั้น ไม่มีปัญหาอะไร 😄
Non-primitive
ทีนี้ลอง Copy ตัวแปรประเภท Non-primitive
และมาดูผลลัพธ์
const pokemon = { name: 'Pikachu', age: 3 }
const _pokemon = pokemon
console.log(pokemon) // { name: 'Pikachu', age: 3 }
console.log(_pokemon) // { name: 'Pikachu', age: 3 }
ก็ปกติ! ✅ จากด้านบนเป็นตัวแปรชนิด Object จากนั้นก็ Copy ค่าให้ตัวแปรใหม่ ทุกอย่างก็ดูจะโอเค แต่ในความเป็นจริงมันไม่ใช่อย่างนั้นเสมอไป ทำไมละ ?
มาดูปัญหาจากตัวอย่างด้านล่างนี้ ⚠️
const pokemon = { name: 'Pikachu', age: 3 }
const _pokemon = pokemon
_pokemon.age = 5
console.log(pokemon) // { name: 'Pikachu', age: 5 }
console.log(_pokemon) // { name: 'Pikachu', age: 5 }
จากโค้ดด้านบน และดูที่ผลลัพธ์ที่ได้ pokemon
ทำไมเปลี่ยนค่าได้ ? ทำไมมันเป็นอย่างนั้น ? 😅
ค่า age
ที่กำหนดให้กับ _pokemon
ส่งผลกระทบกับ pokemon
ด้วยมันเป็นอย่างนั้นได้อย่างไร ?
จะบอกว่านี่คือปัญหาของ Non-primitive ก็ไม่แฟร์ ❌ แต่อยากให้มองว่ามันเป็นจุดเด่นมากกว่า 😎 เพราะมันเป็นเรื่อง stack vs. heap
stack vs. heap
ก่อนจะไปทำความเข้าใจ ก่อนอื่นคุณต้องเข้าใจเรื่องพื้นฐานของ Computer science
ก่อน
- จากที่เราเคยเรียนมา ในคอมพิวเตอร์จะมีตัวแปรอยู่ชนิดหนึ่งชื่อ
Pointer
เป็นตัวแปรที่ทำหน้าที่ชี้ไปที่ Value และเก็บ Address - ในภาษา Programming languages มี 2 สิ่งที่เก็บข้อมูล Computer memory คือ
stack
และheap
- โดย
stack
คือTemporary memory
ที่เก็บข้อมูลPrimitive Local variables
และReferences to objects
- และ
heap
คือตัวที่ทำหน้าที่เก็บข้อมูลGlobal variables
ซึ่งheap
และstacks
จะเชื่อมโยงหากันด้วย (pointers) ดังรูปภาพด้านล่าง
- จากรูปภาพด้านบนตัวแปร pokemon และ _pokemon โดยใช้ค่าเดียวกัน
- ทั้ง 2 ตัวแปรได้
แยกการจัดเก็บข้อมูล
เป็น 2 ชุดบน stack - และทั้ง 2 ตัวแปร
ไม่มีการเชื่อมโยงหาระหว่างกัน
- ทั้ง 2 ตัวแปร คือ Primitives เป็นแบบ
"Passed by value"
- ทั้ง 2 ตัวแปรได้
- ต่อมาได้สร้างตัวแปร 2 Objects ด้วยค่าที่เหมือนกัน จะเห็นว่ามี 2 pointers (references) ใน
stack
- pointers ชี้ไปที่ address เดียวกัน
- จะมี 1 ค่าเท่านั้นที่อยู่บน
heap
- ซึ่งเราสรุปได้ว่ามันคือ
"passed by reference"
ลองคิดต่อสมมุติว่า ถ้าเราต้องกา รเปลี่ยนแปลงค่า แค่บาง Properties ของ Object แต่ไม่ให้มันกระทบกับ Object อีกตัว มันไม่สามารถทำได้! ❌
นั่นเป็นเพราะปัญหาก็คือ การใช้ heap ร่วมกัน
นี่ก็เป็นเหตุผลว่าทำไมต้องทำ Deep copy
และ Shallow copy
💡
Shallow copy
ก่อนจะใช้ Shallow copy มาเข้าใจก่อนว่า
- Shallow copy เป็นการใช้สำหรับ "flat" objects
- Deep copy เป็นการใช้สำหรับ "nested" objects
โดยคำว่า "flat" objects ก็หมายความว่า object ที่ประกอบไปด้วย primitive values
เช่น
const numbers = [ 1, 2, 3, 4, 5, 6 ]
คำว่า "nested" objects ก็หมายความว่า object ที่ประกอบไปด้วย non-primitive values
เช่น
const pokemon = [ "Pikachu", { type: "Electric type" } ]
ทีนี้มาดู Methods ต่าง ๆ ที่ไว้สร้าง Shallow copy
Spread syntax
วิธีการสร้าง Shallow copy ด้วย Spread syntax
const numbers = [ 1, 2, 3, 4, 5, 6 ]
const _numbers = [ ...numbers ]
_numbers[0] = 7
console.log(numbers) //[ 1, 2, 3, 4, 5, 6 ]
console.log(_numbers) //[ 7, 2, 3, 4, 5, 6 ]
const pokemon = { name: 'Pikachu', age: 3 }
const _pokemon = { ...pokemon }
_pokemon.age = 5
console.log(pokemon) // { name: 'Pikachu', age: 3 }
console.log(_pokemon) // { name: 'Pikachu', age: 5 }
{...} Spread syntax สามารถอ่านเพิ่มเติมได้ที่นี่ Rest Parameters vs. Spread Operators ใน JavaScript
Array.prototype.concat
นอกจากนี้ยังมี Method ตัวหนึ่งที่คล้าย ๆ กับ Spread syntax คือ concat() method
(แต่ใช้ได้แค่กับ Array) ตัวอย่าง
const number1 = [ 1, 2, 3 ]
const number2 = [ 4, 5, 6 ]
const numberAll = number1.concat(number2)
console.log(number1) // [ 1, 2, 3 ]
console.log(numberAll) // [ 1, 2, 3, 4, 5, 6 ]
//
const pokemon1 = { name: 'Pikachu', age: 3 }
const pokemon2 = { name: 'Venusaur', age: 4 }
const pokemonArr1 = [ pokemon1 ]
const pokemonArr2 = [ pokemon2 ]
// by Concat method
const pokemonAll1 = pokemonArr1.concat(pokemonArr2)
console.log(pokemonAll1) // [ { name: 'Pikachu', age: 3 }, { name: 'Venusaur', age: 4 } ]
// by Spread syntax
const pokemonAll2 = [ ...pokemon1, ...pokemon2 ]
console.log(pokemonAll2) // [ { name: 'Pikachu', age: 3 }, { name: 'Venusaur', age: 4 } ]
// Spread syntax (merge)
const pokemonAll3 = { ...pokemon1, ...pokemon2 }
console.log(pokemonAll3) // { name: 'Venusaur', age: 4 }
console.log(pokemon1) // { name: 'Pikachu', age: 3 }
ก็จะเห็นว่ามีความคล้าย ๆ กันแต่ ต้องระวังการใช้งานให้ดี
ใช้ด้วยความเข้าใจ
ข้อดีการใช้ concat
คือเมื่อทำงานกับ Array ขนาดใหญ่ concat นั้นมีประสิทธิภาพดีกว่า Spread syntax
เนื่องจากบางทีก็จะเกิด Errors จากการใช้งาน Spread syntax
กับ Array ขนาดใหญ่ เช่น Maximum call stack size excited
และในทางกลับกัน Spread syntax
ก็มีประสิทธิภาพที่ดีกับ Array ขนาดเล็ก