Mảng (array) trong JavaScript: khởi tạo và phương thức

Mảng trong JavaScript là gì? Cách khởi tạo, thêm xóa phần tử, các phương thức map, filter, reduce và duyệt mảng kèm ví dụ chạy được.

6 phút

Mảng trong JavaScript là kiểu dữ liệu dùng để lưu một danh sách giá trị có thứ tự. Bạn sẽ gặp nó trong hầu hết đoạn code front-end lẫn back-end. Bài này hướng dẫn tạo mảng, thêm xóa phần tử, các phương thức hay dùng và vài cách duyệt mảng. Mọi ví dụ đều chạy được trong console của trình duyệt.

Khởi tạo mảng

Khởi tạo mảng
Khởi tạo mảng

Cách phổ biến nhất là dùng dấu ngoặc vuông. Phần tử có thể thuộc bất kỳ kiểu nào, thậm chí trộn lẫn nhiều kiểu. Tuy vậy, trong thực tế nên giữ cùng một kiểu để dễ xử lý.

const ngonNgu = ["Java", "C", "JavaScript"];
const rong = [];
const so = Array.from({ length: 5 }, (_, i) => i * 2);
console.log(ngonNgu[0]);      // Java
console.log(ngonNgu.length);  // 3
console.log(so);              // [0, 2, 4, 6, 8]

Chỉ số bắt đầu từ 0. Truy cập chỉ số không tồn tại không gây lỗi mà trả về undefined, nên rất dễ đọc sai mà không nhận ra. Ngoài ra đừng dùng new Array(3) với một số, vì nó tạo ra mảng ba ô trống chứ không phải mảng chứa số 3.

Thêm, xóa và sửa phần tử

Thêm, xóa và sửa phần tử
Thêm, xóa và sửa phần tử
  • push thêm vào cuối, pop lấy ra từ cuối.
  • unshift thêm vào đầu, shift lấy ra từ đầu.
  • splice(vị trí, số lượng, ...phần tử mới) chèn, xóa hoặc thay thế ở giữa.
  • slice(bắt đầu, kết thúc) sao chép một đoạn mà không làm thay đổi mảng gốc.
const ds = ["a", "b", "c"];
ds.push("d");          // ["a","b","c","d"]
ds.splice(1, 1);       // xóa "b" -> ["a","c","d"]
const dau = ds.slice(0, 2);   // ["a","c"]
console.log(ds, dau);

Hãy nhớ phân biệt splice và slice. Chỉ khác một chữ cái nhưng một cái sửa mảng gốc, một cái thì không. Chúng tôi từng thấy lỗi trong dự án thật chỉ vì ai đó gõ nhầm hai tên này.

Các phương thức biến đổi: map, filter, reduce

Các phương thức biến đổi: map, filter, reduce
Các phương thức biến đổi: map, filter, reduce

Ba phương thức này nhận một hàm và chạy trên từng phần tử. Chúng trả về kết quả mới mà không đụng tới mảng gốc.

const diem = [5, 8, 6, 9];
const gapDoi = diem.map(d => d * 2);          // [10,16,12,18]
const dau = diem.filter(d => d >= 6);          // [8,6,9]
const tong = diem.reduce((t, d) => t + d, 0);  // 28
console.log(gapDoi, dau, tong);

map trả mảng cùng độ dài, filter trả mảng con theo điều kiện, reduce gom về một giá trị. Tham số thứ hai của reduce là giá trị khởi đầu. Bỏ nó đi với mảng rỗng sẽ gây lỗi TypeError.

Nếu cần tìm phần tử, hãy dùng find để lấy phần tử đầu tiên khớp, và findIndex để lấy vị trí. Muốn kiểm tra có tồn tại hay không, hãy dùng includes. Còn some và every kiểm tra điều kiện trên cả mảng.

Duyệt mảng

Duyệt mảng
Duyệt mảng

Có vài cách duyệt, mỗi cách hợp một hoàn cảnh. Vòng for cổ điển cho bạn chỉ số và quyền dùng break. Vòng for...of gọn hơn khi chỉ cần giá trị. Còn forEach tiện cho thao tác phụ như in ra màn hình, nhưng bạn không thể dừng giữa chừng.

const ten = ["An", "Binh", "Chi"];
for (let i = 0; i < ten.length; i++) console.log(i, ten[i]);
for (const t of ten) console.log(t);
ten.forEach((t, i) => console.log(i + 1, t));

Đừng dùng for...in cho mảng. Nó duyệt cả các thuộc tính không phải phần tử và thứ tự không được đảm bảo. Dùng nó để duyệt đối tượng thì ổn hơn.

Sắp xếp và những cái bẫy

Phương thức sort mặc định so sánh phần tử như chuỗi. Vì vậy [10, 9, 1].sort() cho kết quả [1, 10, 9]. Muốn sắp xếp số đúng, bạn phải truyền hàm so sánh (a, b) => a - b. Ngoài ra sort làm thay đổi mảng gốc, nên hãy sao chép bằng [...mang] trước nếu cần giữ bản cũ.

Một bẫy khác là sao chép mảng bằng phép gán. Viết const b = a chỉ tạo thêm một tên trỏ tới cùng mảng, sửa b là a đổi theo. Hãy dùng [...a] hoặc a.slice() để có bản sao thật. Nhớ rằng đây chỉ là bản sao nông với phần tử là đối tượng.

Khi nào nên dùng Map hay Set thay cho mảng

Nếu thường xuyên tìm phần tử theo khóa hoặc cần loại trùng, mảng sẽ chậm vì phải quét từ đầu. Khi đó cấu trúc Map trong JavaScript phù hợp hơn. Và nếu phần tử trong mảng là đối tượng, hãy cẩn thận với việc truy cập thuộc tính bị thiếu. Optional chaining giúp đọc chúng an toàn.

Mảng nhiều chiều và làm phẳng

Mảng có thể chứa mảng khác, và ta thường dùng cách này để biểu diễn bảng hay ma trận. Truy cập phần tử bằng hai chỉ số liên tiếp, ví dụ bang[1][2] lấy hàng thứ hai, cột thứ ba.

const bang = [[1, 2], [3, 4], [5, 6]];
console.log(bang[1][0]);          // 3
console.log(bang.flat());         // [1, 2, 3, 4, 5, 6]
const gapDoi = bang.map(h => h.map(x => x * 2));

Phương thức flat làm phẳng một cấp theo mặc định; truyền Infinity nếu bạn muốn làm phẳng mọi cấp. Còn flatMap kết hợp map và flat trong một bước, tiện khi mỗi phần tử sinh danh sách con.

Giải cấu trúc và toán tử trải

Hai cú pháp ES6 này làm code làm việc với mảng gọn đi nhiều. Giải cấu trúc lấy các phần tử ra biến riêng, còn toán tử trải ... trải các phần tử vào chỗ khác.

const [dau, thu2, ...conLai] = [10, 20, 30, 40];
console.log(dau, thu2, conLai);   // 10 20 [30, 40]

const a = [1, 2];
const b = [...a, 3, 4];           // [1, 2, 3, 4]
[a[0], a[1]] = [a[1], a[0]];      // đổi chỗ hai phần tử

Cách đổi chỗ hai phần tử ở dòng cuối khá tiện. Nhưng người đọc chưa quen có thể cần dừng lại một chút để hiểu. Hãy cân nhắc thêm một dòng chú thích khi dùng.

Các lỗi hay gặp

Lỗi đầu tiên là tưởng typeof phân biệt được mảng, trong khi nó trả về "object" cho cả mảng. Hãy dùng Array.isArray(x). Lỗi thứ hai là xóa phần tử bằng toán tử delete, thứ để lại một ô trống thay vì dồn mảng lại. Hãy dùng splice.

Lỗi thứ ba là gọi forEach với hàm async rồi mong nó chờ từng lời gọi. Thực tế forEach không chờ gì cả. Bạn nên dùng for...of với await.

Nếu quan tâm đến cách trình duyệt giải phóng mảng lớn không dùng nữa, hãy đọc garbage collection trong JavaScript.

Bạn có thể tra danh sách đầy đủ phương thức của mảng ở trang Array trên MDN. Hãy mở console và tự thử từng phương thức với dữ liệu của riêng bạn để nhớ lâu hơn.

Chuyên mục