Nên cân nhắc kỹ trước khi sử dụng forEach()!

6 min read

foreach

Gần đây, khi viết code, tôi đã gặp lỗi khi dùng forEach() để lặp qua một mảng và gọi API. Vấn đề là không thể dùng async function trong forEach()! Một người bạn chỉ ra lỗi này, và tôi phải chuyển về dùng vòng lặp for. Dù đã giải quyết, nhưng điều này làm tôi mất khá nhiều thời gian.

Tôi biết từ lâu rằng forEach() không hiệu quả bằng for, và có nhiều hạn chế khác. Vì vậy, tôi đã tổng hợp một danh sách các vấn đề lớn nhất của phương thức này:

  • Hiệu suất: forEach() chậm hơn 95% so với for.
  • Không thể thoát vòng lặp: Không dùng được break hoặc continue.
  • Không tương thích với async functions: forEach() không hoạt động tốt với async/await.
  • Có các phương thức khác tốt hơn: map(), reduce(), filter() thường phù hợp hơn.
  • Không có giá trị trả về: forEach() luôn trả về undefined.
  • Xử lý mảng thưa: forEach() bỏ qua các khoảng trống trong mảng.

1. Performance Issues

So sánh với vòng lặp for
Một trong những hạn chế lớn nhất của hàm forEach() là hiệu năng của nó so với các vòng lặp for truyền thống. Điều này trở nên đặc biệt rõ ràng trong các ứng dụng quy mô lớn, nơi ngay cả những sự thiếu hiệu quả nhỏ cũng có thể dẫn đến sự chậm lại đáng chú ý.

Performance

Để hiểu sự khác biệt về hiệu suất, hãy so sánh forEach() với vòng lặp for truyền thống thông qua một loạt điểm chuẩn. Bạn có thể thử chạy đoạn mã sau đây để nhận ra sự khác biệt

Với forEach() :

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);

console.time('forEach');
largeArray.forEach((num) => {
  // Perform a simple operation
  let result = num * 2;
});
console.timeEnd('forEach');

// Output: 
// forEach: 9.346923828125 ms

Với for:

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);

console.time('for');
for (let i = 0; i < largeArray.length; i++) {
  // Perform a simple operation
  let result = largeArray[i] * 2;
}
console.timeEnd('for');

// Output: 
// for: 2.3388671875 ms

Khi chạy các bài kiểm tra hiệu suất, bạn sẽ nhận thấy sự khác biệt đáng kể về thời gian thực thi: vòng lặp for thường nhanh hơn forEach() gấp bốn lần. Nguyên nhân chính bao gồm:

  • Chi phí hàm: forEach() gọi một hàm callback cho từng phần tử, gây ra chi phí bổ sung do gọi hàm nhiều lần.
  • Giao thức iterator: forEach() sử dụng giao thức iterator, làm chậm quá trình thực thi.
  • Tối ưu hóa: Các vòng lặp for truyền thống dễ dàng hơn cho các engine JavaScript tối ưu hóa so với các hàm bậc cao như forEach().

Đối với các ứng dụng quy mô lớn hoặc khi hiệu suất là yếu tố quan trọng, bạn nên cân nhắc kỹ lưỡng việc lựa chọn phương pháp lặp.

Mặc dù forEach() có cú pháp gọn gàng và dễ đọc hơn (và điều này rất hữu ích!), nhưng chi phí về hiệu suất có thể quá cao, đặc biệt khi xử lý khối lượng dữ liệu lớn. Đây là lý do đủ để bạn nên sử dụng vòng lặp for làm mặc định, dù mã có thể không sạch sẽ bằng.

2. Inability to Escape the Loop

Một trong những hạn chế chính của hàm forEach() là không hỗ trợ các câu lệnh điều khiển luồng như break hoặc continue.

Điều này có nghĩa là khi forEach() bắt đầu duyệt qua một mảng, nó sẽ thực thi hàm callback được cung cấp cho mỗi phần tử mà không bị gián đoạn. Điều này có thể gây trở ngại khi bạn cần thoát khỏi vòng lặp sớm hoặc bỏ qua một số phần tử dựa trên điều kiện cụ thể.

Trong khi đó, với vòng lặp for truyền thống, bạn có thể sử dụng break để thoát khỏi vòng lặp sớm hoặc continue để bỏ qua một lần lặp và tiếp tục với phần tử tiếp theo. Tuy nhiên, các câu lệnh này không thể được sử dụng với forEach().

Việc không thể sử dụng break hoặc continue trong forEach() sẽ dẫn đến việc chạy tất cả các lần lặp và có khả năng thực hiện các lệnh gọi dư thừa. Đây là một thành tựu tiềm năng khác về hiệu suất ngoài các vấn đề cơ bản mà chúng tôi đã nêu ở trên!.

3. Incompatibility with Async Functions

Một hạn chế đáng kể khác của forEach() là tính không tương thích của nó với các hàm không đồng bộ. Đây chính là điều thôi thúc tôi viết bài này ngay từ đầu!

Phương thức forEach() không chờ thực thi các lời hứa trong lệnh gọi lại của nó, điều này khiến cho các lệnh gọi không đồng bộ không thể sử dụng được.

Điều này có thể dẫn đến việc hoàn thành vòng lặp trước khi tất cả các hoạt động không đồng bộ kết thúc. Bạn có thể gặp may mắn trong một số trường hợp, nhưng cuối cùng bạn sẽ thấy có vấn đề. Tệ hơn nữa, vấn đề sẽ không nhất quán, do đó việc gỡ lỗi là một cơn ác mộng.

Hoạt động không đồng bộ với forEach():

let fetchData = async (id) => {
  // Simulate async API call
  return new Promise((resolve) => setTimeout(() => resolve(`Data for ${id}`), 1000));
};

let ids = [1, 2, 3, 4, 5];

ids.forEach(async (id) => {
  let data = await fetchData(id);
  console.log(data);
});
// Output: Likely an empty array or undefined

Trong ví dụ trên, phương thức forEach() không đợi tìm nạp Data được giải quyết, dẫn đến tình huống vòng lặp hoàn thành trước khi bất kỳ dữ liệu nào được ghi lại.

4. Better Options for Arrays

Mặc dù hàm forEach() là một cách đơn giản để duyệt qua các mảng, nhưng nó không phải lúc nào cũng là công cụ tốt nhất cho việc thao tác với mảng. Trong nhiều trường hợp, các phương thức khác như map(), reduce(), và filter() cung cấp các giải pháp hiệu quả và dễ đọc hơn để xử lý và biến đổi mảng.

Lợi ích về hiệu suất và khả năng đọc
Việc sử dụng các hàm bậc cao hơn này giúp mã ngắn gọn hơn và có thể dẫn đến cải thiện hiệu suất do các công cụ JavaScript tối ưu hóa tốt hơn. Các phương pháp này thúc đẩy phong cách lập trình chức năng để có thêm các điểm thú vị dành cho trẻ em, có thể nâng cao khả năng đọc và bảo trì mã.

Khả năng đọc và bảo trì
Khi viết mã, khả năng đọc và bảo trì là những yếu tố quan trọng cần xem xét. Mã sạch hơn dễ hiểu, gỡ lỗi và mở rộng hơn. Bây giờ chúng ta có thể xem cách bạn có thể xâu chuỗi các phương thức này để tăng sức mạnh cho mã của mình và thậm chí còn tiến xa hơn những gì forEach() có thể tự làm.

Danh sách này nên gióng lên cảnh báo cho hầu hết các nhà phát triển, đặc biệt là những người muốn viết mã theo kiểu hàm (functional programming) ít có khả năng gây lỗi hơn.

Tôi không muốn nói rằng bạn nên hoàn toàn không sử dụng forEach(), nhưng tôi nghĩ rằng tốt nhất là nên tránh sử dụng nó. Thay vào đó, hãy ưu tiên sử dụng các phương thức mảng trực tiếp hơn (hoặc chuỗi của chúng), và chỉ quay lại sử dụng vòng lặp for nếu không có lựa chọn nào khác.

Bằng cách này, bạn sẽ tránh được các vấn đề tiềm ẩn, trường hợp đặc biệt, và lỗi trong tương lai… đặc biệt là khi một nhà phát triển khác tiếp tục mở rộng mã của bạn.

Tham khảo thêm tại: https://app.daily.dev/posts/the-dark-side-of-foreach-why-you-should-think-twice-before-using-it-slhsbmoyn

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *