Học bằng cách đọc theo thứ tự

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à ab, 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
Số mà hàm so sánh trả về quyết định thứ tự
Hàm so sánha.price - b.pricea là 980b là 2980a là 2980b là 2980a là 4380b là 980-2000Trả về số âm0Không chênh lệch3400Trả về số dươngĐặt a lên trướcGiữ nguyênthứ tự ban đầuĐặt b lên trước
Cùng một hàm so sánh, nhưng số trả về thay đổi theo giá của 2 phần tử được truyền vào. 2 phần tử có hiệu bằng 0 giữ nguyên thứ tự như trước khi sắp xếp.

sort chỉ xem dấu của số được trả về, nên -2000-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.

Sắp xếp danh sách tin đăng của một ứng dụng mua bán đồ cũ theo thứ tự sẽ hiển thị trên màn hình. listings đã được khai báo sẵn.

① Hãy sắp xếp theo số ký tự của tên sản phẩm, ngắn trước, rồi hiển thị tên sản phẩm, nối bằng ", ".

② Hãy sắp xếp theo giá, rẻ trước, rồi hiển thị tên sản phẩm theo cùng cách.

③ Hãy sắp xếp lại sao cho các tin cùng giá được xếp theo lượt thích, nhiều trước, rồi hiển thị tên sản phẩm theo cùng cách.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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"25 như "25", nên thứ tự được quyết định bởi ký tự đầu tiên là 12. 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
Có hàm so sánh hay không quyết định thứ được đem ra so sánh
Viếtsort()Số được chuyểnthành chuỗiSo 1180 và 25từ ký tự đầu1180 đượcđưa lên trướcTruyền(a, b) => a - bTrừ trực tiếpdưới dạng số1180 - 25 làsố dương25 đượcđưa lên trước
Cùng là 1180 và 25, nhưng có truyền hàm so sánh hay không sẽ làm thay đổi loại giá trị được đem ra so sánh. Hai hàng cho ra phần tử đứng trước ngược nhau.

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 và toSorted tác động lên mảng như thế nào
viewSeconds.sort(...)Ghi đè lênchính mảng đóTrả vềchính mảng đóThứ tự ban đầubị mấtstayTimes.toSorted(...)Chỉ đọcmảng gốcTrả vềmột mảng mớiThứ tự ban đầuđược giữ lại
Thứ mà 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).

Sắp xếp rồi hiển thị danh sách mã thành viên và danh sách số lượng tồn kho. memberIds và stockCounts đã được khai báo sẵn.

① Hãy sắp xếp memberIds mà không truyền hàm so sánh, rồi hiển thị, nối bằng ", ".

② Hãy tạo một mảng mới là stockCounts đã sắp xếp mà không truyền hàm so sánh, rồi hiển thị theo cùng cách.

③ Hãy tạo một mảng mới là stockCounts đã sắp xếp từ nhỏ đến lớn, rồi hiển thị theo cùng cách.

④ Hãy hiển thị phần tử đầu tiên của stockCounts.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Tìm thấy là dừng ngay
Thứ 1: RSV-201trả về falseChuyển sangphần tử tiếp theoThứ 2: RSV-202trả về truePhần tử này làkết quả của findThứ 3: RSV-203không được gọiDừng kiểm trasau 2 phần tử
Ngay khi phần tử thứ 2 trả về 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.

Tìm mã giảm giá theo điều kiện trong số các mã đã phát hành. coupons đã được khai báo sẵn, mã nào có expired là true thì đã hết hạn.

① Hãy hiển thị mức giảm giá của mã có code là MEMBER15.

② Hãy hiển thị vị trí (đếm từ 0) của mã đã hết hạn.

③ Hãy hiển thị code của mã đầu tiên chưa hết hạn.

④ Hãy tìm mã có code là AUTUMN30; nếu có thì hiển thị mức giảm giá, nếu không thì hiển thị "Không tìm thấy".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
Nơi some và every dừng kiểm tra
Gọi some:tìm tồn kho 0Thứ 1: 12trả về falseThứ 2: 0trả về trueChốt true, khôngxem phần còn lạiGọi every:đều còn hàng?Thứ 1: 12trả về trueThứ 2: 0trả về falseChốt false, khôngxem phần còn lại
Cả 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 somefalse, và cũng không có phần tử nào vi phạm điều kiện nên everytrue. 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ứcTrả vềKhi không có phần tử nào khớp
findChính phần tử khớp đầu tiênundefined
findIndexVị trí của phần tử khớp đầu tiên-1
sometrue nếu có ít nhất một phần tử khớpfalse
everytrue nếu tất cả đều khớpfalse (với mảng rỗng là true)

Dựa vào số bài tập đã nộp và tỷ lệ chuyên cần, hãy kiểm tra xem có thể cấp chứng nhận hoàn thành cho tất cả học viên của một khóa học online hay không. learners đã được khai báo sẵn.

① Hãy hiển thị kết quả kiểm tra xem tất cả học viên có nộp ít nhất 1 bài tập hay không.

② Hãy hiển thị kết quả kiểm tra xem có ít nhất một học viên nộp 0 bài hay không.

③ Hãy hiển thị tên của học viên nộp 0 bài.

④ Hãy hiển thị kết quả kiểm tra xem tỷ lệ chuyên cần của tất cả học viên có từ 80% trở lên hay không.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Khi hàm so sánh trả về số âm, sort đặt phần tử nào lên trước?

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?