[แปล] Explaining Value vs. Reference in Javascript

Sayamrat Kaewta
dumpsayamrat
Published in
5 min readJul 17, 2017

แปลบทความจาก 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 จะหายไปเมื่อฟังก์ชันทำงานเสร็จ

จบแล้ว…ไปเขียนโค้ดสิ

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

--

--