Trong JavaScript, có hai loại kiểu dữ liệu: kiểu tham trị và kiểu tham chiếu, hiểu được sự khác biệt giữa hai loại này rất quan trọng để viết code hiệu quả và tránh những lỗi không đáng có. Trong bài viết này sẽ giải thích sâu về tham trị và tham chiếu trong:
Tham trị (Value/Primitive Types)
Tham trị trong JavaScript bao gồm các loại dữ liệu nguyên thủy như number, string, boolean, null hoặc undefined. Khi bạn khai báo một biến có các kiểu dữ liệu trên, giá trị đó được lưu trữ trực tiếp trong biến, nó sẽ được lưu trong một ô nhớ. Khi bạn gán một loại giá trị cho một biến khác hoặc chuyển nó làm đối số của hàm, một bản sao của giá trị sẽ được tạo.
let x = 10;
let y = x;
y = 20;
console.log(x); // Output: 10
console.log(y); // Output: 20
Trong ví dụ này, x là một biến có kiểu dữ liệu là Number và giá trị là 10, khi chúng ta gán y = x, một bản sao của giá trị 10 được tạo và lưu trữ trong y (lúc này y sẽ được lưu trong ô nhớ mới). Khi chúng ta cập nhật giá trị của y, nó không ảnh hưởng đến giá trị của x, ví dụ gán y = 20 như ví dụ trên thì giá trị của x vẫn là 10.
Tham chiếu (Reference Types)
Các kiểu tham chiếu trong JavaScript bao gồm các kiểu dữ liệu Object, Array, khi bạn khai báo một biến có kiểu tham chiếu, biến đó sẽ lưu trữ tham chiếu đến một vị trí trong bộ nhớ nơi dữ liệu thực được lưu trữ. Khi bạn gán một biến mới cho một biến tham chiếu, nếu bạn thay đổi biến mới, thì biến tham chiếu sẽ bị thay đổi theo, để hiểu rõ hơn bạn xem ví dụ sau
let a = [1, 2, 3];
let b = a;
b.push(4);
console.log(a); // Output: [1, 2, 3, 4]
console.log(b); // Output: [1, 2, 3, 4]
Trong ví dụ này, a là một kiểu tham chiếu (array), ta gán a cho b, cả hai biến đều trỏ đến cùng một vị trí trong bộ nhớ nơi lưu trữ dữ liệu mảng, sau đó ta cập nhật mảng bằng b, nó cũng ảnh hưởng đến mảng được tham chiếu bởi a, do đó cả 2 biến đều nhận được giá trị mới.
Xử lý các vấn đề của tham chiếu
Trong các bài toán thực tế, nhu cầu gán một biến mới mà không làm thay đổi giá trị của biến cũ rất phổ biến, để làm được triệt để điều này bạn dùng phương pháp sau
let a = [1, 2, 3];
let b = JSON.parse(JSON.stringify(a));
Nguyên tắc của câu lệnh này là biến a thành một chuỗi JSON, hay nó chính là string, lúc này JSON.stringify(b) đã là 1 tham trị được lưu trên ô nhớ mới, ta tiếp tục dùng JSON.parse để biến chuỗi JSON trên trở lại thành mảng. Lúc này bạn thay đổi b thoải mái mà không lo a bị thay đổi.
Ngoài ra còn một cách không triệt để bạn có thể tham khảo là dùng spread operator để clone thành object mới, ta có ví dụ sau:
let a = {
name: 'NGUYEN AN',
address: 'Hanoi'
}
let b = {...a}
b.address = 'Saigon';
console.log(a) // output {name: 'NGUYEN AN', address: 'Hanoi'}
console.log(b) // output {name: 'NGUYEN AN', address: 'Saigon'}
Trường hợp này hoạt động rất ok, ta tiếp tục trường hợp 2:
const a = [[1], [2], [3]];
const b = [...a];
b.shift().shift();
console.log(a);
console.log(b);
Trường hợp này thì lỗi rồi! Nguyên nhân là do spread operator chỉ sao chép được một cấp của mảng, không phù hợp với nhiều object/array lồng nhau theo mozilla.org
Kết luận
Cần lưu ý là sự khác biệt giữa tham trị và tham chiếu khi làm việc với dữ liệu trong JavaScript. Các loại tham trị thường an toàn hơn khi sử dụng vì chúng không cho phép thay đổi ngoài ý muốn đối với giá trị ban đầu. Tuy nhiên, việc dùng object và array trong JS hay bất cứ một ngôn ngữ lập trình nào là không thể thiếu, do đó bạn nên thận trọng khi gán các biến mới cho loại này, tránh tình trạng thay đổi giá trị gốc làm sai bài toán của mình.