Menu

Rest và spread trong JavaScript: giải thích toán tử ...

Toán tử ... hoạt động thế nào trong JavaScript: gom đối số bằng tham số rest, trải mảng và đối tượng bằng spread, và khi nào nên dùng từng cách.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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

  • ...name trong danh sách tham số hoặc mẫu destructuring là rest: nó gom lại.
  • ...value trong 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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu