Hàm cũng là giá trị
Trong JavaScript, hàm là một giá trị như mọi giá trị khác. Bạn có thể lưu nó vào biến, đặt nó vào mảng, truyền nó cho một hàm khác, hoặc trả nó về từ một hàm. Chỉ riêng điều đó đã mở ra cả một phong cách lập trình:
Một khi đã quen coi hàm là giá trị, hàm bậc cao sẽ không còn bí ẩn nữa. Hàm bậc cao (higher-order function) đơn giản là một hàm nhận một hàm khác làm đối số, trả về một hàm, hoặc cả hai. Định nghĩa chỉ có vậy.
Nhận một hàm làm đối số
Dạng phổ biến nhất: một hàm nhận một callback và chạy nó giúp bạn. Bạn đã dùng những hàm như vậy mà không để ý.
forEach là hàm bậc cao: nó nhận hàm của bạn và gọi nó một lần cho mỗi phần tử. setTimeout là hàm bậc cao: nó nhận hàm của bạn và gọi nó sau một khoảng thời gian. Bạn tập trung vào việc làm gì; chúng lo chuyện khi nào và bao nhiêu lần.
Tự viết một hàm như vậy trông cũng tương tự. Đây là một hàm nhỏ chỉ chạy callback khi điều kiện đúng:
action là một tham số tình cờ chứa một hàm. Gọi nó bằng action() sẽ chạy bất cứ hàm nào được truyền vào.
Ba hàm bạn sẽ thật sự dùng: map, filter, reduce
Mảng có sẵn các phương thức bậc cao thay thế được hầu hết các vòng lặp for mà bạn sẽ phải viết. Nắm được ba phương thức này, rất nhiều code hằng ngày sẽ ngắn gọn và rõ ràng hơn.
map: biến đổi từng phần tử
map gọi hàm của bạn một lần cho mỗi phần tử và gom các giá trị trả về vào một mảng mới. Cùng độ dài, nội dung đã được biến đổi. Mảng gốc không bị thay đổi.
filter: giữ lại những phần tử khớp điều kiện
filter giữ lại các phần tử mà callback trả về giá trị truthy và bỏ đi phần còn lại. Giá trị trả về là một mảng mới, có thể ngắn hơn.
reduce: gộp cả mảng thành một giá trị
reduce là phương thức đa năng nhất. Callback nhận vào biến tích lũy hiện tại và phần tử đang xét; bất cứ thứ gì bạn trả về sẽ thành biến tích lũy kế tiếp. Đối số thứ hai (ở đây là 0) là giá trị khởi đầu.
Bạn có thể nối chúng lại với nhau. Đây là lúc phong cách này phát huy tác dụng:
Đọc từ trên xuống: giữ lại các đơn đã thanh toán, lấy ra giá, rồi cộng tổng. Không vòng lặp, không biến đếm phải thay đổi, không phải lo lệch một đơn vị.
Trả về một hàm từ một hàm
Nửa còn lại của hàm bậc cao: một hàm tạo ra và trả về một hàm khác:
multiplyBy(2) chạy một lần và trả lại một hàm hoàn toàn mới. Hàm mới đó vẫn nhớ factor: đó là closure, và nó sẽ có trang riêng ở phần sau. Lúc này, điều cần nhớ là: gọi multiplyBy với các đối số khác nhau cho bạn các hàm chuyên biệt khác nhau từ cùng một khuôn mẫu.
Mẫu này xuất hiện ở khắp nơi:
Một định nghĩa, hai hàm dùng lại được. Tốt hơn nhiều so với tự viết tay warn và info rồi phải giữ chúng đồng bộ với nhau.
Hàm có tên và callback viết trực tiếp
Bạn có thể truyền một arrow function viết trực tiếp, hoặc truyền một hàm theo tên. Cả hai đều chạy được, hãy chọn cách nào dễ đọc hơn:
Khi truyền isEven (không có cặp ngoặc tròn), bạn đang đưa chính hàm đó. Thêm () sẽ gọi hàm ngay lập tức và truyền kết quả của nó, một lỗi người mới hay mắc:
nums.filter(isEven); // correct: passes the function
nums.filter(isEven()); // wrong: calls isEven with no args, passes the result
Nếu callback bắt đầu dài quá vài dòng, hãy tách nó ra và đặt cho nó một cái tên. Phần code xung quanh thường sẽ dễ đọc hơn hẳn.
Một ví dụ hoàn chỉnh
Hàm bậc cao tỏa sáng khi bạn ghép các mảnh nhỏ lại với nhau. Giả sử bạn có danh sách sản phẩm và muốn lấy tên các sản phẩm giá rẻ, còn hàng, viết in hoa:
Mỗi hàm hỗ trợ chỉ có một nhiệm vụ. Mỗi phương thức mảng thực hiện một phép biến đổi. Chuỗi xử lý đọc lên như một bản mô tả điều bạn muốn, chứ không phải mô tả cách lặp.
Khi nào không nên dùng
Các phương thức bậc cao rất tuyệt, nhưng không phải lúc nào cũng thay được vòng lặp:
- Nếu bạn cần dừng giữa chừng,
forhoặcfor...ofvớibreakrõ ràng hơn là cố thoát ra khỏiforEach. - Nếu bạn làm việc bất đồng bộ bên trong callback,
mapvàforEachsẽ không chờ nó. Hãy dùngfor...ofvớiawait, hoặcPromise.allvớimap. - Nếu callback đang thay đổi trạng thái dùng chung, bạn đang đi xa khỏi điểm mạnh của phong cách này. Hãy dùng một vòng lặp thường, hoặc refactor để trả về giá trị mới.
Dùng đúng chỗ, map, filter và reduce loại bỏ phần lớn code vòng lặp rườm rà trong code hằng ngày. Dùng ở mọi nơi, chúng bắt đầu làm code khó đọc. Hãy chọn công cụ thể hiện ý định rõ ràng nhất.
Tiếp theo: object
Hàm không phải là loại giá trị duy nhất đáng để xây dựng trên đó. Object là công cụ chủ lực của JavaScript để nhóm dữ liệu và hành vi liên quan lại với nhau, và cũng chính là thứ chứa đầy trong các mảng bạn vừa filter và map. Đó là nội dung của trang tiếp theo.
Câu hỏi thường gặp
Hàm bậc cao trong JavaScript là gì?
Hàm bậc cao là hàm làm ít nhất một trong hai việc: nhận một hàm khác làm đối số, hoặc trả về một hàm làm kết quả. Array.prototype.map, setTimeout và addEventListener đều là hàm bậc cao: chúng nhận một callback và chạy nó giúp bạn.
map, filter và reduce khác nhau thế nào?
map biến đổi từng phần tử và trả về một mảng mới có cùng độ dài. filter giữ lại các phần tử mà callback trả về giá trị truthy và trả về một mảng (có thể ngắn hơn). reduce gộp cả mảng thành một giá trị duy nhất bằng cách kết hợp từng phần tử một. Cả ba đều là hàm bậc cao nhận một callback.
Vì sao lại trả về một hàm từ một hàm khác?
Để tạo ra các hàm hỗ trợ nhỏ, có thể cấu hình mà không lặp lại logic. Một hàm như multiplyBy(n) trả về một hàm mới nhân với n, nên multiplyBy(2) và multiplyBy(10) cho bạn hai hàm chuyên biệt từ cùng một định nghĩa. Mẫu này dùng closure và rất phổ biến trong trình xử lý sự kiện, middleware và các thư viện tiện ích.