[แปล] Explaining Value vs. Reference in Javascript
แปลบทความจาก Explaining Value vs. Reference in Javascript โดย Arnav Aggarwal
Javascript มี 5 ชนิดข้อมูล ที่ passed by value ได้แก่ Boolean
null
undefine
String
และNumber
เราเรียกข้อมูลพวกนี้ว่าชนิดข้อมูลแบบ primitive
และ Javascript มีอีก 3 ชนิดข้อมูล ที่ passed by reference ได้แก่ Array
Function
และ Object
ทั้งหมดนี้ในทางเทคนิคแล้วเป็น Object
Primitives
ถ้า primitive type ถูกกำหนดค่าไปยังตัวแปรเราจะสามารถบอกได้ว่าตัวแปรนั้นเก็บค่าที่เป็น primitive อยู่
var x = 10;
var y = 'abc';
var z = null;
จากโค้ดข้างบน x
เก็บค่า 10
y
เก็บค่า 'abc'
เราจะทำการจำลองการเก็บค่าและตัวแปรเหล่านี้ตามรูปด้านล่าง
เมื่อเรากำหนดตัวแปรไปยังตัวแปรอื่นๆ โดยใช้เครื่องหมาย =
มันคือเราคัดลอกค่าของตัวแปรนั้นไปยังตัวแปรใหม่
var x = 10;
var y = 'abc';var a = x;
var b = y;console.log(x, y, a, b); // -> 10, 'abc', 10, 'abc'
ทั้ง x
และ a
เก็บค่า 10
ทั้งคู่ y
และ b
เก็บค่า 'abc'
ทั้งคู่ ตัวแปรเหล่านี้ถูกแยกการเก็บค่าออกจากกันหรือก็คือตัวแปรเหล่านี้มีค่าเป็นของตัวเอง
เมื่อเปลื่ยนค่าของตัวแปรใดก็ตามไม่ได้หมายความว่าอีกตัวแปรจะเปลื่ยนด้วย นั้นหมายความว่าแต่ละตัวแปรไม่มีความสัมพันธ์ต่อกัน
var x = 10;
var y = 'abc';var a = x;
var b = y;a = 5;
b = 'def';console.log(x, y, a, b); // -> 10, 'abc', 5, 'def'
Objects
มันอาจสับสนหน่อย แต่ลองอ่านดูก่อนได้มันไม่ได้ยากขนาดนั้น
ตัวแปรที่ถูกกำหนดค่าที่ไม่ใช่ Primitive มันคือการเก็บที่อยู่ที่ชี้ไปยังค่าที่อยู่ในหน่วยความจำ (Memory) ซึ่งตัวแปรนั้นไม่ได้เก็บค่านั้นไว้จริงๆ แต่เก็บที่อยู่ของ Object นั้นแทน
Object ที่ถูกสร้างขึ้นโดยเก็บไว้ที่ไหนสักแห่งในหน่วยความจำของคอมพิวเตอร์ของคุณ เมื่อเราเขียน arr = []
คือการสร้างอาเรย์ (Array)ในหน่วยความจำ และสิ่งที่ตัวแปร arr
เก็บไว้คือที่อยู่ (Address) ที่ชี้ไปยังอาเรย์นั้น
ลองสมมุติให้ว่า ที่อยู่
เป็นชนิดข้อมูลแบบใหม่ที่เป็นแบบ passed by value เหมือนกับ String
หรือ Number
แล้วที่อยู่
ใช้เก็บที่อยู่ที่ชี้ไปยังหน่วยความจำของค่าที่เป็น passed by reference และเก็บอยู่ภายใต้เครื่องหมาย <>
เหมือนกับ String
ที่เก็บค่าอยู่ภายใต้ ''
หรือ ""
เมื่อเรากำหนดค่าที่เป็นแบบ Reference ให้กับตัวแปร เราสามารถเขียนได้ดังนี้
1) var arr = [];
2) arr.push(1);
สิ่งที่เกิดขึ้นในหน่วยความจำของบรรทัดที่ 1 และ 2 จะเป็นดังนี้
บรรทัดที่ 1
บรรทัดที่ 2
จะสังเกตได้ว่าตัวแปร arr
เก็บที่อยู่ที่ชี้ไปยังหน่วยความจำมันไม่เปลื่ยน สิ่งที่เปลื่ยนคืออาเรย์ที่อยู่ในหน่วยความจำ เมื่อเราใช้ arr
ทำอะไรสักอย่างเช่นการใส่ค่า (Pushing) Javascript engine จะไปยังที่อยู่ของตัวแปร arr
ในหน่วยความจำและทำการเปลื่ยนแปลงค่าที่อยู่ในหน่วยความจำนั้น
Assigning by Reference
เมื่อชนิดข้อมูลแบบ Reference ถูกคัดลอกโดยใช้เครื่องหมาย =
สิ่งที่ถูกคัดลอกจริงๆคือที่อยู่แทนที่จะคัดลอกค่า เหมือนกับที่อยู่นั้นเป็นชนิดข้อมูลแบบ Primitive
var reference = [1];
var refCopy = reference;
สิ่งที่เกิดขึ้นในหน่วยความจำจากโค้ดข้างบน แสดงได้ดังนี้
แต่ละตัวแปรเก็บค่าที่อยู่ที่ชี้ไปยังอาเรย์เดียวกัน นั้นหมายความว่าเมื่อเราแก้ไขค่าของ reference
แล้ว refCopy
ก็จะเปลื่ยนด้วย
reference.push(2);
console.log(reference, refCopy); // -> [1, 2], [1, 2]
เราได้ทำการเพิ่มค่า 2
ไปในอาเรย์ที่อยู่ในหน่วยความจำ เมื่อเราใช้ reference
และ refCopy
มันก็คือการใช้อาเรย์เดียวกัน
Reassigning a Reference
การกำหนดค่าใหม่ของตัวแปรแทนที่อันเก่า
var obj = { first: 'reference' };
ในหน่วยความจำ
เมื่อเราเพิ่มบรรทัดที่สองเข้ามาดังนี้
var obj = { first: 'reference' };
obj = { second: 'ref2' }
ที่อยู่ของตัวแปร obj
จะถูกเปลื่ยนและที่อยู่นั้น ชี้ไปยัง Object อันใหม่ส่วน Object อันแรกก็ยังคงถูกเก็บไว้ในหน่วยความจำ
เมื่อไม่มีตัวแปรไหนอ้างอิงหรือชี้ไปยัง Object แรกแล้ว ที่อยู่#234
จะถูก Javascript Engine จัดการโดย Garbage Collection นั้นคือ Javascript Engine จะทำการลบ Object แรกทิ้ง ดังนั้นเราจะไม่สามารถใช้ Object { first: 'reference' }
นั้นได้อีกต่อไป
== และ ===
เมื่อเครื่องหมาย ==
และ ===
ถูกใช้กับตัวแปรที่ทีชนิดข้อมูลแบบ reference มันจะเปรียบเทียบโดยที่อยู่ เช่น ถ้าตัวแปรชี้ไปยังค่าเดียวกันในหน่วยความจำผลลัพธ์ที่ได้จากการเปรียบเทียบจะมีค่าเป็น true
var arrRef = [’Hi!’];
var arrRef2 = arrRef;console.log(arrRef === arrRef2); // -> true
แต่ถ้ามันเป็นคนละ Object ถึงแม้ค่าที่กำหนดในตัวแปร เป็นค่าที่เหมือนกันก็ตามผลลัพธ์ที่ได้จากการเปรียบเทียบจะมีค่าเป็น false
var arr1 = ['Hi!'];
var arr2 = ['Hi!'];console.log(arr1 === arr2); // -> false
ถ้าเรามีสอง Object ที่ต่างกันแล้วเราต้องการเปรียบเทียบว่าสองค่านั้นเหมือนกันหรือไม่ มีวิธีที่ง่ายๆ ที่จะเปรียบเทียบคือแปลงทั้งสองค่าเป็น string
ก่อนแล้วค่อยเปรียบเทียบ เมื่อเครื่องหมายเท่ากับเปรียบเทียบข้อมูลที่มีชนิดเป็น Primitive มันจะเปรียบเทียวโดยดูที่ค่าว่าเหมือนกันหรือไม่
var arr1str = JSON.stringify(arr1);
var arr2str = JSON.stringify(arr2);console.log(arr1str === arr2str); // true
อีกวิธีในการเปรียบเทียบก็คือการใช้ Recursive โดยลูปเปรียบว่าแต่ละ Properties นั้นมีค่าเหมือนกัน
Passing Parameters through Functions
เมื่อเราส่งค่าแบบ Primitive ไปยังฟังก์ชัน ฟังก์ชันจะคัดลอกค่านั้น เหมือนกับการใช้เครื่องหมาย =
var hundred = 100;
var two = 2;function multiply(x, y) {
// PAUSE
return x * y;
}var twoHundred = multiply(hundred, two);
จากโค้ดข้างบนเรากำหนดให้ตัวแปร hundred
มีค่าเป็น 100
เมื่อเราส่งตัวแปรนั้นไปยังไปยังฟังก์ชัน multiply
ตัวแปร x ก็จะได้รับค่า 100
โดยการคัดลอกเหมือนกับการใช้เครื่องหมาย =
ทีนี้เครื่องหมายตัวแปร hundred
จะไม่มีผลกระทบใดในฟังก์ชันอีก และนี้คือสิ่งที่เกิดขึ้นในหน่วยความจำเมื่อโค้ดถึงบรรทัด // PAUSE
Pure Functions
ฟังก์ชันที่ไม่มีผลกระทบใดๆ เลยที่อยู่นอกเหนือฟังก์ชันคือ Pure Function เป็นเหมือนฟังก์ชันที่รับค่าแบบ Primitive จาก Parameter และไม่เรียกใช้ตัวแปรใดเลยที่อยู่นอกขอบเขตฟังก์ชันและมันไม่สามารถมีผลกระทบใดๆ เลยที่อยู่นอกเหนือฟังก์ชัน ดังนั้นเมื่อฟังก์ชันรีเทิร์น ตัวแปรทั้งหมดที่ถูกสร้างขึ้นภายในฟังก์ชันจะถูกจัดการโดย Garbage Collection
ฟังก์ชันที่รับตัวแปรที่เป็น Object สามารถเปลื่ยนแปลงค่าที่อยู่นอกขอบเขตฟังก์ชันได้ เมื่อฟังก์ชันรับตัวแปรที่เป็นอาเรย์เข้ามาในฟังก์ชัน และทำการแก้ไขหรือเพิ่มค่า ตัวแปรที่อยู่นอกขอบเขตฟังก์ชันนี้จะมองเห็นค่าถูกแก้ไขหรือเพิ่มเข้ามาด้วย ดังนั้นเมื่อฟังก์ชันนี้ถูก Return ออกไปการเปลื่ยนแปลงก็ยังคงอยู่ เป็นเหตุให้เกิด Side Effects และยากที่จะตรวจสอบ
อาเรย์ในหลายๆ ภาษามักจะพ่วงมาด้วย Array.map
และ Array.filter
ที่เป็น Pure Function ที่รับอาเรย์เข้ามาและคัดลอกมันเป็นอาเรย์อันใหม่ ก่อนที่จะทำการเปลื่ยนแปลง แทนที่จะเปลื่ยนแปลงอาเรย์อันเดิม นี้จะทำให้ไม่ต้องแตะในอาเรย์อันเดิมเลย ดังนั้นก็จะไม่มีผลกระทบกับตัวแปรที่อยู่นอกฟังก์เลย เมื่อฟังก์ชันรีเทิร์น ก็จะรีเทิร์นอาเรย์อันใหม่ออกไป (Returned a reference)
มาดูตัวอย่างฟังก์ชันแบบ pure และ impure
function changeAgeImpure(person) {
person.age = 25;
return person;
}var alex = {
name: 'Alex',
age: 30
};var changedAlex = changeAgeImpure(alex);console.log(alex); // -> { name: 'Alex', age: 25 }
console.log(changedAlex); // -> { name: 'Alex', age: 25 }
ฟังก์ชันนี้รับ Object มาและเปลื่ยนแปลง property age
ให้เป็น 25
เพราะฟังก์ชันนี้มันรับชนิดข้อมูลที่เป็น Reference เข้ามามันเลยไปแก้ไขค่าเดียวกันกับที่อยู่ใน alex
ด้วย ดังนั้นเมื่อรีเทิร์นตัวแปร person
ออกไปมันคือการรีเทิร์น Object เดียวกันกับตัวแปร alex
ทั้ง alex
และ changedAlex
เก็บที่อยู่เหมือนกันที่ชี้ไปที่ Object เดียวกัน
มาดูตัวอย่างฟังก์ชันที่เป็น pure
function changeAgePure(person) {
var newPersonObj = JSON.parse(JSON.stringify(person));
newPersonObj.age = 25;
return newPersonObj;
}var alex = {
name: 'Alex',
age: 30
};var alexChanged = changeAgePure(alex);console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
ในฟังก์ชันนี้เราใช้ JSON.stringify
เพื่อแปลงค่าให้มันเป็น String
ก่อนแล้วแปลงค่า String
นั้นกลับไปเป็น Object โดยใช้ JSON.parse
และเก็บไว้ในตัวแปรใหม่ มีหลายวิธีที่จะทำแบบนี้เช่นการวนลูปและกำหนดค่าให้แต่ละตัวๆ ไปยังตัวแปรใหม่แต่วิธีนี้เป็นวิธีที่ง่ายที่สุด ตัวแปรใหม่มีค่าที่เหมือนกันกับอันเดิมแต่เก็บค่าที่อยู่ในหน่วยความจำที่แยกจากกัน
เมื่อเราแก้ไข Property age
ของตัวแปรใหม่ ก็จะไม่มีผลกับตัวแปรเดิม ดังนั้นฟังก์นี้จึง Pure ไม่สร้างผลกระทบใดๆ นอกขอบเขตฟังก์ชัน ไม่แม้กระทั้ง Object ที่ส่งเข้ามา ตัวแปรที่ถูกสร้างขึ้นในฟังก์ชันจะถูกรีเทิร์นออกไปหรือไม่ก็ถูกจัดการด้วย Garbage Collection เมื่อฟังก์ชันทำงานเสร็จ
ทดสอบตัวเอง (Test Yourself)
Value กับ Reference มักจะถูกถามประจำตอนสัมภาษณ์งาน ลองหาคำตอบด้วยตัวเองดูสิว่าจะ Log ออกมาได้อะไร
function changeAgeAndReference(person) {
person.age = 25;
person = {
name: 'John',
age: 50
};
return person;
}var personObj1 = {
name: 'Alex',
age: 30
};var personObj2 = changeAgeAndReference(personObj1);console.log(personObj1); // -> ?
console.log(personObj2); // -> ?
ฟังก์ชันนี้ทำการเปลื่ยนแปลง Property age
ของตัวแปรที่ส่งเข้ามา กำหนด Object ใหม่ให้กับมันและรีเทิร์นออกไป และนี้คือสิ่งที่ได้จากการ log จริงๆ
console.log(personObj1); // -> { name: 'Alex', age: 25 }
console.log(personObj2); // -> { name: 'John', age: 50 }
จำได้ไหมว่าเมื่อเราส่งค่าผ่านฟังก์ชันมันเหมือนกันการใช้เครื่องหมาย =
ดังนั้นตัวแปร person
เก็บที่อยู่เหมือนกันกับ personObj1
และที่อยู่นั้นชี้ไปยัง Object เดียวกัน และเมื่อทำการกำหนด Object ใหม่ ไปให้กับตัวแปร person
มันก็ได้ที่อยู่ใหม่ที่ชี้ไปยัง Object นั้นและไม่ส่งผลอะไรกับอันเดิมอีก
การกำหนดใหม่นี้ไม่ใช่การเปลื่ยนแปลงตัวแปร personObj1
ตัวแปร person
ได้ที่อยู่ใหม่เพราะมันถูกกำหนดใหม่และไม่เปลื่ยนแปลง personObj1
โค้ดข้างบนจะถูกแปลงได้เหมือนกันแบบนี้
var personObj1 = {
name: 'Alex',
age: 30
};var person = personObj1;
person.age = 25;person = {
name: 'john',
age: 50
};var personObj2 = person;console.log(personObj1); // -> { name: 'Alex', age: 25 }
console.log(personObj2); // -> { name: 'John', age: '50' }
มีข้อแตกต่างนิดหน่อยเมื่อเราใช้แบบฟังก์ชันคือตัวแปร person
จะหายไปเมื่อฟังก์ชันทำงานเสร็จ
จบแล้ว…ไปเขียนโค้ดสิ
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —