Một trong những điểm khác biệt cơ bản của các đối tượng so với kiểu nguyên thủy là chúng được lưu trữ và sao chép bởi các tham chiếu.
Các giá trị nguyên thủy: chuỗi, số, booleans – được gán / sao chép trên toàn bộ giá trị.
Ví dụ:
let message = "Hello!";
let phrase = message;
Kết quả là chúng ta có hai biến độc lập, mỗi biến được lưu trữ một chuỗi "Hello!"
.
Đối tượng không như thế.
Một biến lưu trữ không phải là chính đối tượng, mà là địa chỉ của nó trong bộ nhớ, nói cách khác là một tham chiếu tới nó.
Đây là hình ảnh cho đối tượng:
let user = {
name: "David"
};
Ở đây, đối tượng được lưu trữ ở đâu đó trong bộ nhớ. Và biến user
có một tham chiếu tới nó.
Khi một biến đối tượng được sao chép thì tham chiếu của nó được sao chép, chứ không phải đối tượng đó được sao chép.
Ví dụ:
let user = { name: "David" };
let admin = user; // copy the reference
Bây giờ chúng ta có hai biến, mỗi biến có tham chiếu đến cùng một đối tượng:
Chúng ta có thể sử dụng bất kỳ biến nào để truy cập đối tượng và sửa đổi nội dung của nó:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let user = { name: 'David' };
let admin = user;
admin.name = 'Pete'; // changed by the "admin" reference
alert(user.name); // 'Pete', changes are seen from the "user" reference
Ví dụ trên chứng tỏ rằng chỉ có một đối tượng. Như thể chúng ta có một cái tủ có hai chìa khóa và ta sử dụng một trong số chúng ( admin
) để thay đổi tủ đó. Sau đó, nếu sau này chúng ta sử dụng một khóa khác ( user
), chúng ta cũng có thể thấy các thay đổi đó.
Nội dung chính
1. So sánh bằng tham chiếu
Các toán tử đẳng thức ==
và đẳng thức nghiêm ngặt ===
cho các đối tượng hoạt động giống hệt nhau.
Hai đối tượng chỉ bằng nhau nếu chúng là cùng một đối tượng.
Ở đây hai biến tham chiếu cùng một đối tượng, do đó chúng bằng nhau:
let a = {};
let b = a; // copy the reference
alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true
Và ở đây hai đối tượng độc lập không bằng nhau, mặc dù cả hai đều trống:
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
Để so sánh như obj1 > obj2
hoặc để so sánh với kiểu nguyên thủy obj == 5
, các đối tượng được chuyển đổi thành kiểu nguyên thủy. Chúng ta sẽ nghiên cứu cách chuyển đổi đối tượng sau này, nhưng nói thật, những so sánh như vậy rất hiếm khi xảy ra, thường là do lỗi mã hóa.
2. Nhân bản và hợp nhất, Object.assign(Cloning and merging, Object.assign)
Vì vậy, sao chép một biến đối tượng sẽ tạo thêm một tham chiếu đến cùng một đối tượng.
Nhưng nếu chúng ta cần sao chép một đối tượng thì sao? Tạo một bản sao độc lập với một đối tượng?
Điều đó cũng có thể thực hiện được, nhưng khó khăn hơn một chút, vì không có phương thức tích hợp sẵn cho JavaScript. Trên thực tế, điều đó hiếm khi cần thiết. Sao chép bằng cách tham thì tốt hơn.
Nhưng nếu chúng ta thực sự muốn điều đó, thì chúng ta cần tạo một đối tượng mới và sao chép cấu trúc của đối tượng hiện có bằng cách lặp lại các thuộc tính của nó và sao chép chúng ở cấp độ nguyên thủy.
Như thế này:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let user = {
name: "David",
age: 30
};
let clone = {}; // the new empty object
// let's copy all user properties into it
for (let key in user) {
clone[key] = user[key];
}
// now clone is a fully independent object with the same content
clone.name = "Pete"; // changed the data in it
alert( user.name ); // still John in the original object
Ngoài ra chúng ta có thể sử dụng phương thức Object.assign để làm điều đó.
Cú pháp là:
Object.assign(dest, [src1, src2, src3...])
- Đối số đầu tiên
dest
là một đối tượng mục tiêu. - Đối số tiếp theo
src1, ..., srcN
(có thể nhiều nếu cần thiết) là các đối tượng nguồn. - Nó sao chép các thuộc tính của tất cả các đối tượng nguồn
src1, ..., srcN
vào mục tiêudest
. Nói cách khác, các thuộc tính của tất cả các đối số bắt đầu từ thứ hai được sao chép vào đối tượng đầu tiên. - Trả trở lại cho
dest
.
Chẳng hạn, chúng ta có thể sử dụng nó để hợp nhất một số đối tượng thành một:
let user = { name: "David" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);
// now user = { name: "David", canView: true, canEdit: true }
Nếu tên thuộc tính được sao chép đã tồn tại, nó sẽ bị ghi đè:
let user = { name: "David" };
Object.assign(user, { name: "Pete" });
alert(user.name); // now user = { name: "Pete" }
Chúng tôi cũng có thể sử dụng Object.assign
để thay thế for..in
vòng lặp cho nhân bản đơn giản như ở trên:
let user = {
name: "David",
age: 30
};
let clone = Object.assign({}, user);
Nó sao chép tất cả các thuộc tính của user
vào đối tượng trống và trả về nó.
3. Nhân bản một object trong một object
Cho đến bây giờ chúng ta giả định rằng tất cả các thuộc tính của user
là kiểu nguyên thủy. Nhưng các thuộc tính có thể được tham chiếu đến các đối tượng khác. Làm gì với chúng đây?
Như thế này:
let user = {
name: "David",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
Bây giờ nó không đủ để sao chép clone.sizes = user.sizes
, vì user.sizes
nó là một đối tượng, nó sẽ được sao chép bằng tham chiếu. Vì vậy, clone
và user
sẽ chia sẻ cùng kích thước:
Như thế này:
let user = {
name: "David",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true, same object
// user and clone share sizes
user.sizes.width++; // change a property from one place
alert(clone.sizes.width); // 51, see the result from the other one
Để khắc phục điều đó, chúng ta nên sử dụng vòng lặp để nhân bản và kiểm tra từng giá trị của user[key]
và, nếu đó là một đối tượng, thì cũng sao chép cấu trúc của nó. Điều đó được gọi là một bản sao lồng bên trong.
Có một thuật toán tiêu chuẩn để nhân bản lồng để xử lý trường hợp trên và các trường hợp phức tạp hơn, được gọi là thuật toán nhân bản có cấu trúc.
Chúng ta có thể sử dụng đệ quy để thực hiện nó. Hoặc, không phát minh lại bánh xe, có một thực hiện, ví dụ _.cloneDeep (obj) từ thư viện JavaScript lodash .
4. Tóm lược
Các đối tượng được chỉ định và sao chép bằng cách tham chiếu. Nói cách khác, một biến lưu trữ không phải là giá trị đối tượng, mà là một tham chiếu tới bộ nhớ (địa chỉ trong bộ nhớ) lưu giá trị. Vì vậy, sao chép một biến như vậy hoặc chuyển nó dưới dạng đối số hàm sẽ sao chép tham chiếu đó, không phải sao chép đối tượng.
Tất cả các hoạt động từ các tham chiếu được sao chép (như thêm / xóa thuộc tính) được thực hiện trên cùng một đối tượng mà thôi.
Để tạo một bản sao, chúng ta có thể sử dụng Object.assign
(các đối tượng lồng nhau được sao chép bằng cách tham chiếu) hoặc một hàm nhân bản lồng nhau của một bản sao, như _.cloneDeep (obj).
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!