Trong JavaScript, biến được chia thành hai loại chính: kiểu dữ liệu nguyên thủy (primitive) và kiểu dữ liệu tham chiếu (reference).
Kiểu dữ liệu nguyên thủy được lưu trữ trong bộ nhớ stack. Khi bạn gán một biến nguyên thủy cho biến khác, một bản sao giá trị sẽ được tạo ra. Hai biến này sẽ có giá trị giống nhau nhưng nằm ở các vị trí bộ nhớ khác nhau. Do đó, việc thay đổi giá trị của một biến sẽ không ảnh hưởng đến biến còn lại.
Ngược lại, kiểu dữ liệu tham chiếu (như đối tượng, mảng) được lưu trữ trong bộ nhớ heap. Khi bạn gán một biến tham chiếu cho biến khác, thực chất là bạn đang sao chép con trỏ đến cùng một vùng nhớ. Điều này có nghĩa là cả hai biến sẽ cùng trỏ đến một đối tượng hoặc mảng duy nhất. Nếu bạn thay đổi thuộc tính của đối tượng thông qua một biến, sự thay đổi đó sẽ phản ánh ở cả biến kia.
Ví dụ về kiểu dữ liệu tham chiếu:
var objGoc = { ten: 'Alice' };
var objSaoChep = objGoc;
objSaoChep.tuoi = 30;
console.log(objGoc); // Kết quả: { ten: 'Alice', tuoi: 30 }
console.log(objSaoChep); // Kết quả: { ten: 'Alice', tuoi: 30 }
Trong ví dụ này, `objSaoChep` và `objGoc` cùng trỏ đến một đối tượng. Khi thuộc tính `tuoi` được thêm vào `objSaoChep`, `objGoc` cũng hiển thị thuộc tính này vì chúng là cùng một đối tượng.
1. Copy nông (Shallow Copy)
Như ví dụ trên, việc gán trực tiếp một biến tham chiếu cho biến khác dẫn đến copy nông. Khi đó, thay đổi trên một biến sẽ ảnh hưởng đến biến kia.
2. Copy sâu (Deep Copy)
Copy sâu tạo ra một bản sao độc lập hoàn toàn của biến gốc. Mọi thay đổi trên bản sao sẽ không ảnh hưởng đến biến gốc và ngược lại. Đây là giải pháp để khắc phục vấn đề của copy nông.
2.1. Đối với Mảng (Array)
2.1.1. Mảng chỉ chứa kiểu dữ liệu nguyên thủy
Đối với mảng chỉ chứa các giá trị nguyên thủy, bạn có thể sử dụng phương thức slice() hoặc concat() để tạo bản sao nông.
var mangGoc = [1, 2, 3];
var mangSaoChep = mangGoc.slice(0); // Hoặc: var mangSaoChep = [].concat(mangGoc);
arrayObj.slice(start, [end]): Trả về một mảng mới chứa các phần tử được chỉ định từ mảng gốc. Phương thức này không làm thay đổi mảng gốc.arrayObj.concat(): Nối hai hoặc nhiều mảng lại với nhau và trả về một mảng mới. Mảng gốc không bị thay đổi.
2.1.2. Mảng chứa các mảng con (kiểu dữ liệu tham chiếu)
Nếu mảng có chứa các mảng con, slice() và concat() chỉ thực hiện copy nông cho các mảng con.
var mangGoc = [1, 3, 4, [46, 23]];
var mangSaoChep = mangGoc.slice(0);
mangSaoChep[3][0] = 34; // Thay đổi phần tử trong mảng con của bản sao
console.log(mangGoc); // Kết quả: [1, 3, 4, [34, 23]]
console.log(mangSaoChep); // Kết quả: [1, 3, 4, [34, 23]]
Để giải quyết vấn đề này, bạn cần một hàm copy sâu:
function deepCopyMang(mangGoc) {
var ketQua = [];
for (var i = 0; i < mangGoc.length; i++) {
if (Array.isArray(mangGoc[i])) {
ketQua[i] = deepCopyMang(mangGoc[i]); // Đệ quy nếu là mảng con
} else {
ketQua[i] = mangGoc[i];
}
}
return ketQua;
}
2.2. Đối với Đối tượng (Object)
Nguyên tắc cơ bản để copy sâu đối tượng là tạo một đối tượng mới, sau đó lặp qua các thuộc tính của đối tượng gốc và gán chúng cho đối tượng mới.
Nếu giá trị thuộc tính chỉ là kiểu nguyên thủy, bạn chỉ cần gán trực tiếp. Nếu giá trị thuộc tính là một đối tượng khác, bạn cần sử dụng đệ quy.
function deepCopyDoiTuong(doiTuongGoc) {
var ketQua = {};
for (var thuocTinh in doiTuongGoc) {
if (doiTuongGoc.hasOwnProperty(thuocTinh)) { // Kiểm tra thuộc tính của chính đối tượng
if (typeof doiTuongGoc[thuocTinh] === 'object' && doiTuongGoc[thuocTinh] !== null) {
// Nếu là object (và không phải null), gọi đệ quy
ketQua[thuocTinh] = deepCopyDoiTuong(doiTuongGoc[thuocTinh]);
} else {
// Ngược lại, gán trực tiếp
ketQua[thuocTinh] = doiTuongGoc[thuocTinh];
}
}
}
return ketQua;
}
2.3. Hàm Copy Sâu Đa Năng (Hỗ trợ cả Mảng và Đối tượng lồng nhau)
Hàm này có thể xử lý cả mảng và đối tượng, bao gồm cả các cấu trúc lồng nhau.
function deepCopy(input) {
// Xử lý null và kiểu nguyên thủy
if (input === null || typeof input !== "object") {
return input;
}
// Xử lý Mảng
if (Array.isArray(input)) {
var output = [];
for (var i = 0; i < input.length; i++) {
output[i] = deepCopy(input[i]); // Đệ quy cho từng phần tử
}
return output;
}
// Xử lý Đối tượng
if (input instanceof Object) {
var output = {};
for (var key in input) {
if (input.hasOwnProperty(key)) {
output[key] = deepCopy(input[key]); // Đệ quy cho từng thuộc tính
}
}
return output;
}
// Trường hợp khác (không mong đợi, nhưng để an toàn)
return input;
}
Kiểm tra với các trường hợp khác nhau:
2.3.1. Mảng chứa mảng
var mangGoc1 = [1, 3, 4, [46, 23]];
var mangSao1 = deepCopy(mangGoc1);
mangSao1[3][0] = 34;
console.log("Mảng gốc 1:", mangGoc1); // [1, 3, 4, [46, 23]]
console.log("Mảng sao 1:", mangSao1); // [1, 3, 4, [34, 23]]
2.3.2. Mảng chứa mảng và đối tượng
var mangGoc2 = [1, 3, 4, [46, 23], { ten: 'Alice' }];
var mangSao2 = deepCopy(mangGoc2);
mangSao2[3][0] = 34;
mangSao2[4].ten = 'Bob';
console.log("Mảng gốc 2:", mangGoc2); // [1, 3, 4, [46, 23], { ten: 'Alice' }]
console.log("Mảng sao 2:", mangSao2); // [1, 3, 4, [34, 23], { ten: 'Bob' }]
2.3.3. Đối tượng chứa đối tượng
var doiTuongGoc1 = { ten: 'Alice', danhSach: { so: [1, 2, 3, 4] } };
var doiTuongSao1 = deepCopy(doiTuongGoc1);
doiTuongSao1.danhSach.so[2] = 34;
doiTuongSao1.ten = 'Bob';
console.log("Đối tượng gốc 1:", doiTuongGoc1); // { ten: 'Alice', danhSach: { so: [1, 2, 3, 4] } }
console.log("Đối tượng sao 1:", doiTuongSao1); // { ten: 'Bob', danhSach: { so: [1, 2, 34, 4] } }
2.3.4. Đối tượng chứa mảng
var doiTuongGoc2 = { ten: 'Alice', danhSach: [1, 2, 3, 4] };
var doiTuongSao2 = deepCopy(doiTuongGoc2);
doiTuongSao2.danhSach[0] = 34;
doiTuongSao2.ten = 'Bob';
console.log("Đối tượng gốc 2:", doiTuongGoc2); // { ten: 'Alice', danhSach: [1, 2, 3, 4] }
console.log("Đối tượng sao 2:", doiTuongSao2); // { ten: 'Bob', danhSach: [34, 2, 3, 4] }