Spread trải một đối tượng vào đối tượng khác
Cú pháp object spread, tức ba dấu chấm đặt trước một đối tượng, sao chép các thuộc tính enumerable của chính đối tượng đó vào object literal bao quanh. Đây là cách ngắn nhất để sao chép, gộp và tạo bản sao đã chỉnh sửa mà không động vào đối tượng gốc.
copy có cùng khóa và giá trị với user, nhưng là một đối tượng khác. Sửa đối tượng này sẽ không ảnh hưởng tới đối tượng kia, ít nhất là ở cấp ngoài cùng. Chúng ta sẽ quay lại lưu ý này ngay sau đây.
Cách nghĩ dễ nhớ: { ...obj } nghĩa là "đổ các thuộc tính của obj vào object literal mới này". Mọi thứ bạn viết cạnh spread đều trở thành một phần của kết quả.
Sao chép và ghi đè trong một bước
Cách dùng phổ biến nhất là spread một đối tượng rồi thêm hoặc ghi đè vài thuộc tính. Khóa đứng sau luôn thắng, nên bạn spread trước rồi ghi đè sau:
user không bị động tới. updated là một đối tượng mới với role đã được thay. Cách cập nhật bất biến này xuất hiện ở khắp nơi trong JavaScript hiện đại: hàm cập nhật state trong React, reducer trong Redux, bất kỳ đoạn code nào không muốn sửa trực tiếp dữ liệu.
Đảo thứ tự lại, bạn sẽ có hành vi ngược lại:
Ở đây role: "guest" đứng trước, nên user.role ghi đè nó. Rất tiện khi bạn muốn có giá trị mặc định mà đối tượng được spread có thể ghi đè.
Gộp đối tượng
Spread hai (hoặc nhiều) đối tượng vào một literal mới để gộp chúng. Với các khóa trùng nhau, đối tượng đứng sau ghi đè đối tượng đứng trước:
theme và fontSize lấy từ userPrefs; debug được giữ lại từ defaults. Ba đối tượng? Bốn? Cùng một quy tắc: đọc từ trái sang phải, lần ghi cuối cùng thắng.
Đây là cách viết hiện đại thay cho Object.assign({}, defaults, userPrefs). Cả hai làm cùng một việc, nhưng phiên bản spread dễ đọc hơn và không khiến bạn mắc lỗi phổ biến là viết Object.assign(defaults, userPrefs), vốn làm thay đổi defaults.
Spread là sao chép nông
Đây là phần khiến nhiều người vấp. Spread sao chép các thuộc tính cấp ngoài cùng của đối tượng. Nếu giá trị của một thuộc tính lại là đối tượng hoặc mảng, thứ được sao chép là tham chiếu chứ không phải nội dung.
Thay đổi copy.address.city cũng làm thay đổi user.address.city, vì cả hai đối tượng dùng chung một đối tượng address. Spread chỉ cho chúng ta một lớp vỏ ngoài mới.
Khi cần sửa thứ gì đó lồng bên trong, hãy spread từng cấp mà bạn muốn thay đổi:
Để sao chép sâu thực sự với dữ liệu bất kỳ, hãy dùng structuredClone(obj). Hàm này xử lý được đối tượng lồng nhau, mảng, ngày tháng, Map và Set, và có sẵn trong mọi môi trường chạy hiện đại.
Spread và rest
Chúng dùng chung ba dấu chấm nhưng làm hai việc ngược nhau. Spread trải ra; rest gom lại.
Quy tắc dễ nhớ: nếu ... nằm ở vế trái của dấu = (phân rã, destructuring), đó là rest. Nếu nó nằm bên trong một object hoặc array literal ở vế phải, đó là spread.
Xóa một thuộc tính mà không làm thay đổi đối tượng
Kết hợp rest khi phân rã với spread cho bạn một cách gọn gàng để tạo bản sao của đối tượng đã bỏ đi một khóa: không cần delete, không thay đổi dữ liệu gốc:
tempToken được tách ra thành một biến riêng (mà bạn bỏ qua), còn mọi thứ khác nằm trong safe. Đối tượng user gốc không bị động tới.
Những gì spread không sao chép
Vài điểm tinh tế nên biết:
- Thuộc tính non-enumerable không được sao chép. Phần lớn thuộc tính bạn tạo ra mặc định là enumerable, nhưng các thuộc tính định nghĩa bằng
Object.definePropertyvà một số thuộc tính có sẵn thì không. - Prototype không được sao chép.
{ ...instance }cho bạn một đối tượng thường, không phải instance của lớp ban đầu. Các phương thức định nghĩa trên prototype của lớp sẽ không có trên bản sao. - Getter được gọi. Spread một đối tượng có getter sẽ gọi getter đó một lần và lưu giá trị trả về thành một thuộc tính bình thường trên đối tượng mới.
copy có x và y, nhưng nó là một đối tượng thường: distance nằm trên Point.prototype, nơi spread không chạm tới. Nếu cần sao chép một instance của lớp, thường thì lớp đó phải tự cung cấp phương thức clone của riêng nó.
Tiếp theo: các phương thức mảng
Spread là một mảnh trong bộ công cụ làm việc với dữ liệu bất biến. Mảnh lớn còn lại là các phương thức mảng: map, filter, reduce và những phương thức tương tự, vốn tạo ra mảng mới thay vì sửa mảng gốc. Đó là nội dung của trang tiếp theo.
Câu hỏi thường gặp
...obj làm gì trong JavaScript?
Bên trong một object literal, ...obj sao chép mọi thuộc tính enumerable của chính đối tượng đó vào đối tượng mới. { ...user } tạo ra một đối tượng mới có cùng khóa và giá trị với user. Đây là cách chuẩn để sao chép hoặc gộp đối tượng mà không làm thay đổi đối tượng gốc.
Làm sao gộp hai đối tượng trong JavaScript?
Spread cả hai vào một object literal mới: const merged = { ...a, ...b }. Thuộc tính của b ghi đè các khóa trùng của a vì thuộc tính đứng sau luôn thắng. Cách này tương đương Object.assign({}, a, b) nhưng dễ đọc hơn.
Object spread có phải là sao chép sâu không?
Không. Object spread là sao chép nông: nó sao chép các thuộc tính cấp ngoài cùng, còn đối tượng và mảng lồng bên trong vẫn được dùng chung qua tham chiếu. Sửa copy.address.city cũng sẽ sửa original.address.city. Muốn sao chép sâu thật sự, hãy dùng structuredClone(obj).
Spread và rest khác nhau thế nào?
Chúng trông giống hệt nhau (...) nhưng làm hai việc ngược nhau. Spread trải một đối tượng hoặc mảng ra thành từng thuộc tính hoặc phần tử: { ...user }. Rest gom phần còn lại vào một biến: const { name, ...others } = user. Ngữ cảnh cho bạn biết mình đang nhìn thấy loại nào.