Cùng cú pháp, hai công việc
Ba dấu chấm ... xuất hiện rất nhiều trong code JavaScript hiện đại, và chúng làm hai việc ngược nhau tùy vào vị trí. Khi đã nhận ra quy luật, mọi cách dùng ... đều rơi vào một trong hai nhóm:
- Rest:
...nameở phía nhận gom nhiều giá trị vào một mảng hoặc đối tượng. - Spread:
...valueở phía cho trải một mảng hoặc đối tượng ra thành từng phần riêng lẻ.
Đó là toàn bộ mô hình tư duy. Phần còn lại của trang này chỉ là ví dụ cho từng loại, cùng những mẫu code bạn sẽ dùng đi dùng lại.
Tham số rest: gom đối số
Một tham số rest trong định nghĩa hàm gom bất kỳ số lượng đối số (argument) nào thành một mảng thật:
nums là một mảng bình thường. Bạn có thể .map, .filter, xem .length của nó, truyền nó cho hàm khác: mọi thứ mảng làm được.
Tham số rest có thể đi cùng tham số thường, nhưng tham số rest phải đứng cuối:
label nhận đối số đầu tiên; mọi thứ sau đó rơi vào items. Đặt tham số rest ở bất kỳ đâu khác ngoài vị trí cuối là lỗi cú pháp.
Rest và đối tượng arguments cũ
Code JavaScript cũ dùng một biến đặc biệt tên là arguments bên trong hàm thường. Nó trông giống mảng nhưng không phải mảng, nghĩa là các phương thức mảng không dùng trực tiếp được trên nó. Tham số rest thay thế nó một cách gọn gàng:
Arrow function thậm chí không có đối tượng arguments, nên tham số rest là cách duy nhất để chúng nhận số lượng đối số thay đổi. Trong code mới, hãy ưu tiên ...args.
Spread trong lời gọi hàm
Spread làm điều ngược lại: nó lấy một mảng và mở ra thành từng đối số riêng ngay tại chỗ gọi.
Math.max nhận từng số riêng lẻ, không nhận mảng. Trước khi có spread, bạn phải viết Math.max.apply(null, nums). Giờ chỉ cần một ... là xong.
Để ý rằng cùng một ... là rest trong định nghĩa hàm và là spread trong lời gọi hàm: vị trí cho bạn biết đó là loại nào.
Spread trong literal mảng
Trải vào một literal mảng giúp sao chép hoặc gộp mảng:
[...a] cho bạn một mảng mới với cùng các phần tử, hữu ích khi bạn muốn sắp xếp hay thay đổi mà không đụng tới mảng gốc:
scores vẫn nguyên vẹn vì .sort chạy trên bản sao. Thói quen nhỏ nhưng lợi ích lớn khi bạn viết code không được có tác dụng phụ bất ngờ.
Spread trong literal đối tượng
Spread cũng dùng được với đối tượng thường, gộp các thuộc tính của chúng vào một đối tượng mới:
Khóa đứng sau thắng. updates.age ghi đè user.age, còn city được mang theo luôn. Thứ tự các lần spread quyết định kết quả, nên hãy nhớ điều này khi xếp chồng giá trị mặc định và giá trị ghi đè:
Giá trị mặc định đặt trước, lựa chọn của người dùng đặt sau. Người dùng thắng ở fontSize và kế thừa theme.
Cái bẫy sao chép nông
Spread chỉ sao chép một tầng. Các đối tượng và mảng lồng nhau vẫn được dùng chung giữa bản gốc và bản sao:
Cả hai mảng đều hiện tag mới vì copy.tags và original.tags là cùng một mảng. Spread không nhân bản danh sách lồng bên trong mà chỉ sao chép tham chiếu.
Để sao chép sâu thật sự dữ liệu thuần, hãy dùng structuredClone:
Giờ hai mảng đã độc lập với nhau. structuredClone có sẵn trong trình duyệt hiện đại và Node, xử lý được cấu trúc lồng nhau, và là lựa chọn đúng mỗi khi sao chép "nông" là không đủ.
Rest trong destructuring
Rest cũng hoạt động trong destructuring, nơi nó gom các phần tử hoặc thuộc tính còn lại:
Lấy ra vài trường và giữ phần còn lại trong một đối tượng là mẫu code phổ biến khi chuyển tiếp props, loại bỏ trường nhạy cảm hoặc tạo phiên bản dữ liệu đã chỉnh sửa:
password được tách ra (và bỏ qua); safe chứa mọi thứ còn lại. Không thay đổi dữ liệu gốc, không phải sao chép thủ công.
Tóm tắt nhanh
...nametrong danh sách tham số hoặc mẫu destructuring là rest: nó gom lại....valuetrong lời gọi hàm, literal mảng hoặc literal đối tượng là spread: nó trải ra.- Bản sao bằng spread là nông. Cấu trúc lồng nhau vẫn dùng chung. Dùng
structuredCloneđể sao chép sâu. - Tham số rest là mảng thật: hãy dùng chúng thay cho
arguments. - Trong literal đối tượng, spread đứng sau ghi đè spread đứng trước, đó là cách tạo giá trị mặc định kèm giá trị ghi đè.
Tiếp theo: closure
Hàm trong JavaScript không chỉ nhận đầu vào và trả về đầu ra: chúng còn ghi nhớ phạm vi nơi chúng được định nghĩa. Sự ghi nhớ đó gọi là closure, và nó là cơ chế đứng sau callback, hàm factory và phần lớn các mẫu code bạn sẽ gặp ở trang tiếp theo.
Câu hỏi thường gặp
Rest và spread trong JavaScript khác nhau thế nào?
Chúng dùng chung cú pháp ... nhưng làm hai việc ngược nhau. Rest gom nhiều giá trị vào một mảng duy nhất, xuất hiện trong danh sách tham số của hàm và trong destructuring. Spread trải một iterable hoặc đối tượng ra thành từng phần, xuất hiện trong lời gọi hàm, literal mảng và literal đối tượng. Nếu ... nằm ở phía nhận thì đó là rest; nếu nằm ở phía cho thì đó là spread.
Tham số rest hoạt động thế nào trong hàm?
Một tham số rest như function sum(...nums) gom mọi đối số truyền vào hàm thành một mảng thật tên là nums. Nó phải là tham số cuối cùng trong danh sách. Khác với đối tượng arguments cũ, tham số rest là mảng thật, nên .map, .filter và .reduce dùng trực tiếp được trên nó.
Toán tử spread có tạo bản sao sâu không?
Không. Spread chỉ sao chép một tầng, tức là bản sao nông. { ...user } cho bạn một đối tượng mới với cùng các khóa ở tầng trên cùng, nhưng các đối tượng và mảng lồng bên trong vẫn là tham chiếu dùng chung. Để sao chép sâu, hãy dùng structuredClone(value) hoặc tuần tự hóa qua JSON với dữ liệu thuần.