Câu 1Khi hàm so sánh trả về số âm, sort đặt phần tử nào lên trước?
Sắp xếp và tìm kiếm — sort / find / some / every
Tìm hiểu sort với hàm so sánh, toSorted giữ nguyên mảng gốc, find và findIndex để tìm một phần tử, cùng some và every.
Nếu mỗi lần sắp xếp một danh sách bạn lại viết vòng for để đổi chỗ các phần tử, thì lần nào bạn cũng phải tự lo cả cách so sánh lẫn các bước đổi chỗ. Khi tìm một phần tử thỏa điều kiện cũng vậy, bạn vẫn phải kết hợp for với break để thoát ra ngay khi tìm thấy.
Bài này nói về sort để sắp xếp và find để tìm phần tử thỏa điều kiện.
Chỉ định thứ tự bằng hàm — sort và hàm so sánh
Giả sử bạn muốn hiển thị danh sách sản phẩm theo giá từ rẻ đến đắt. Đã có phương thức mảng lo việc sắp xếp, nhưng nếu chỉ viết products.sort() thì kết quả sẽ không theo giá, vì bạn chưa cho nó biết phải so sánh theo tiêu chí nào.
Bạn truyền cho sort (phương thức sắp xếp các phần tử của mảng) một hàm so sánh (hàm nhận 2 phần tử và trả về một số). Nếu gọi 2 phần tử nó nhận được lần lượt là a và b, thì khi giá trị trả về là số âm, a được đặt lên trước; khi là số dương, b được đặt lên trước. Viết a.price - b.price là bạn có thứ tự giá tăng dần.
const products = [
{ name: "Chuột không dây", price: 2980 },
{ name: "Sạc dự phòng", price: 4380 },
{ name: "Cáp USB-C", price: 980 },
{ name: "Lót chuột", price: 2980 },
];
// Hiệu âm thì a đứng trước, hiệu dương thì b đứng trước
products.sort((a, b) => a.price - b.price);
console.log(products.map((item) => item.price).join(", ")); // 980, 2980, 2980, 4380
// Kiểm tra thứ tự của 2 sản phẩm cùng giá
console.log(products[1].name, products[2].name); // Chuột không dây Lót chuột
// Đảo chiều phép trừ thì được thứ tự giảm dần
products.sort((a, b) => b.price - a.price);
console.log(products[0].name); // Sạc dự phòng
sort chỉ xem dấu của số được trả về, nên -2000 và -1 được xử lý như nhau. Khi muốn quyết định thứ tự giữa các sản phẩm cùng giá, hãy viết thêm vào hàm so sánh một bước so sánh giá trị khác trong trường hợp hiệu bằng 0.
Gọi sort không có hàm so sánh thì sao — so sánh mặc định và toSorted
Giả sử bạn muốn sắp xếp một mảng chứa thời gian (tính bằng giây) người dùng ở lại trang, từ ngắn đến dài. Nếu viết sort() mà không truyền hàm so sánh, bạn sẽ nhận được một thứ tự không khớp với độ lớn của các số, chẳng hạn 1180 giây lại đứng trước 25 giây.
Khi không có hàm so sánh, sort chuyển các phần tử thành chuỗi trước rồi so sánh từng ký tự một. 1180 được xử lý như "1180" và 25 như "25", nên thứ tự được quyết định bởi ký tự đầu tiên là 1 và 2. sort sắp xếp lại ngay trên chính mảng được gọi, nên khi muốn giữ cả thứ tự ban đầu, hãy dùng toSorted (phương thức trả về một mảng mới đã được sắp xếp).
const viewSeconds = [1180, 980, 25, 4380];
// Không có hàm so sánh thì các phần tử được so sánh như chuỗi
console.log(viewSeconds.sort().join(", ")); // 1180, 25, 4380, 980
// Truyền hàm so sánh thì các phần tử được xếp theo độ lớn của số
console.log(viewSeconds.sort((a, b) => a - b).join(", ")); // 25, 980, 1180, 4380
// toSorted trả về mảng mới, mảng gốc không thay đổi
const stayTimes = [1180, 980, 25, 4380];
console.log(stayTimes.toSorted((a, b) => a - b).join(", ")); // 25, 980, 1180, 4380
console.log(stayTimes.join(", ")); // 1180, 980, 25, 4380
Ngay khi thành chuỗi, "1180" đã chắc chắn đứng trước "25", vì vậy khi sắp xếp một mảng số, hãy truyền hàm so sánh. Lần gọi sort thứ hai trong đoạn code trên sắp xếp tiếp viewSeconds, tức mảng đã bị lần gọi thứ nhất đổi thứ tự.
sort ở hàng trên trả về chính là viewSeconds đã bị sắp xếp lại. Chỉ toSorted mới tạo ra mảng mới.Trình duyệt cũ không có toSorted
toSorted được thêm vào đặc tả từ ES2023, nên trên các trình duyệt cũ chưa hỗ trợ, dòng gọi nó sẽ dừng với TypeError. Để code chạy được cả trên những môi trường đó, hãy gọi sort trên một bản sao tạo bằng spread syntax, như [...stayTimes].sort((a, b) => a - b).
Lấy ra một phần tử thỏa điều kiện — find và findIndex
Giả sử bạn muốn lấy ra một lượt đặt phòng theo mã đặt phòng. Bạn có thể dùng filter, nhưng nó trả về mảng nên phải thêm [0] để đọc phần tử đầu tiên — và nếu không có lượt đặt nào khớp, bạn nhận về một mảng rỗng.
find (phương thức trả về phần tử đầu tiên thỏa điều kiện) duyệt từ đầu mảng và trả về phần tử đầu tiên mà callback trả về true. Khi cần vị trí thay vì phần tử, hãy dùng findIndex (phương thức trả về vị trí của phần tử đầu tiên thỏa điều kiện).
const reservations = [
{ code: "RSV-201", room: "Phòng họp A" },
{ code: "RSV-202", room: "Phòng họp B" },
{ code: "RSV-203", room: "Phòng họp C" },
];
// Trả về chính phần tử đầu tiên mà callback trả về true
const target = reservations.find((item) => item.code === "RSV-202");
console.log(target.room); // Phòng họp B
// Cần vị trí thì dùng findIndex
console.log(reservations.findIndex((item) => item.code === "RSV-202")); // 1
// Khi không có lượt đặt nào khớp
console.log(reservations.find((item) => item.code === "RSV-999")); // undefined
console.log(reservations.findIndex((item) => item.code === "RSV-999")); // -1
true, callback không được gọi cho phần tử thứ 3 nữa. Phần tử khớp càng nằm ở phía trước thì số lần gọi càng ít.find chỉ trả về phần tử khớp đầu tiên, nên không dùng được để gom tất cả khi có nhiều phần tử khớp. Khi cần tất cả, hãy dùng filter vốn trả về một mảng; còn khi chỉ cần một phần tử thì chọn find.
Không tìm thấy thì nhận về undefined
Nếu đọc thẳng giá trị mà find trả về, như target.room, thì khi không có phần tử nào khớp, chương trình sẽ dừng với TypeError: Cannot read properties of undefined (reading 'room'). Hãy kiểm tra bằng if (target) trước khi đọc.
Kiểm tra cả mảng bằng true/false — some và every
Giả sử bạn muốn dựa vào tình trạng tồn kho để quyết định các mặt hàng trong một đơn có giao cùng lúc được hay không. Bạn có thể dùng filter gom các mặt hàng hết hàng rồi đếm, nhưng thứ bạn cần không phải là một mảng — mà là một câu trả lời duy nhất, true hoặc false.
some (phương thức trả về true nếu có ít nhất một phần tử thỏa điều kiện) và every (phương thức trả về true nếu mọi phần tử đều thỏa điều kiện) trả về một giá trị boolean chứ không phải phần tử. Bạn có thể viết thẳng chúng làm điều kiện của if.
const orderItems = [
{ name: "Chai chiết", stock: 12 },
{ name: "Nước rửa tay", stock: 0 },
{ name: "Lưới tạo bọt", stock: 5 },
];
// Chỉ cần một phần tử thỏa điều kiện là true
console.log(orderItems.some((item) => item.stock === 0)); // true
// Chỉ true khi tất cả đều thỏa điều kiện
console.log(orderItems.every((item) => item.stock > 0)); // false
// Với mảng rỗng, some là false còn every là true
console.log([].some((item) => item.stock > 0)); // false
console.log([].every((item) => item.stock > 0)); // true
some lẫn every đều có câu trả lời ở phần tử thứ 2, phần tử có tồn kho là 0. every kết thúc ngay khi tìm thấy một phần tử không thỏa điều kiện.Với mảng rỗng, không có phần tử nào thỏa điều kiện nên some là false, và cũng không có phần tử nào vi phạm điều kiện nên every là true. Bảng dưới đây tóm tắt giá trị trả về của 4 phương thức tìm kiếm trong bài.
| Phương thức | Trả về | Khi không có phần tử nào khớp |
|---|---|---|
| find | Chính phần tử khớp đầu tiên | undefined |
| findIndex | Vị trí của phần tử khớp đầu tiên | -1 |
| some | true nếu có ít nhất một phần tử khớp | false |
| every | true nếu tất cả đều khớp | false (với mảng rỗng là true) |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu gọi sort() trên [1180, 980, 25] mà không có hàm so sánh, bạn nhận được thứ tự nào?
Câu 3Muốn kiểm tra bằng một giá trị boolean duy nhất xem mọi số lượng tồn kho có từ 1 trở lên hay không, bạn dùng phương thức nào?