Lập Trình Hàm với Javascript: Filter & Chaining

3642

Sự linh hoạt là một trong những điểm tôi thích nhất trong quá trình sử dụng JavaScript. JavaScript cho bạn cơ hội áp dụng lập trình hướng đối tượng, lập trình vô điều kiện, và thậm chí là lập trình hàm. Và bạn có thể chuyển đổi luân phiên liên tục giữa các thể loại lập trình khác nhau tùy theo nhu cầu hiện tại, cũng như sở thích và kỳ vọng của team mình.

Tuy có hỗ trợ các kỹ thuật lập trình hàm, JavaScript không được tối ưu cho lập trình hàm như các ngôn ngữ như Haskell hay Scala. Tuy không thường cấu trúc các chương trình JavaScript của mình theo hướng 100% lập trình hàm, tôi vẫn rất thích sử dụng các khái niệm lập trình hàm để giúp code của tôi được sạch hơn và tập trung vào thiết kế code có thể được dễ dàng tái sử dụng và test hiệu quả hơn.

Filtering để giới hạn Data Set

Với sự xuất hiện của ES5, JavaScript Arrays được thừa hưởng một vài method giúp cho việc lập trình hàm được tiện lợi hơn nữa. JavaScript Arrays giờ đây có thể map, reduce, và filter ngay mà không cần cài đặt thêm. Mỗi method trong nhóm này sẽ áp dụng lên từng item trong một array (và không cần vòng lặp hay thay đổi đến state cục bộ), thực hiện một lượt phân tích trả kết quả sử dụng được ngay hoặc chuyển giao để xử lý sau.

Trong bài viết này, tôi muốn giới thiệu bạn với tính năng lọc (filter). Filter cho phép bạn đánh giá từng item trong một array dựa trên điều kiện test được đưa vào, và xác định liệu có trả kết quả array mới chứa element đó hay không. Khi bạn sử dụng method ‘Filter’ của Array, kết quả bạn nhận được sẽ là một array khác, cùng độ dài với array ban đầu hoặc nhỏ hơ, chứa một bộ phận các item trong array gốc khớp với điều kiện bạn đưa ra.

Dùng vòng lặp để thể hiện thao tác Filter

Một trong những ví dụ về lợi ích của filter khi giải quyết một vấn đề có thể kết đến khả năng giới hạn một array các string để hiển thị chỉ những string có ba ký tự. Giải pháp ở đây khá đơn giản, và chúng ta có thể dễ dàng thực hiện thông qua vòng lặp for JavaScript vanilla mà không cần method filter. Ví dụ cụ thể như sau:

Những gì chúng ta đang làm ở đây là xác định một array chứ ba strings, và tạo một array trống để lưu trữ các string có ba ký tự trong đó. Chúng ta sẽ các định một biến đếm (count variable) để dùng trong vòng lặp for trong quá trình lặp qua array. Mỗi khi chúng ta bắt gặp một string có chính xác ba ký tự, ta sẽ push string này vào array rỗng vừa tạo. Khi đã thao tác xong, chúng ta chỉ việc log kết quả thôi.

Chúng ta hiển nhiên có thể hoàn toàn tinh chỉnh array gốc trực tiếp trong vòng lặp, nhưng khi làm như vậy chúng ta sẽ vinh viễn mất các giá trị ban đầu. Tạo array mới và giữ nguyên giá trị gốc sẽ an toàn hơn.

Dùng Mothod Filter

Cách trên, xét về mặt kỹ thuật không có gì sai, nhưng tính khả dụng của method filter trên Array cho phép chúng ta code thêm sạch và đơn giản hơn. Dưới đây là ví dụ với method filter để thực hiện cùng một nhiệm vụ:

Như trước đó, chúng ta bắt đầu với một biến có chứa array gốc, sau đó xác định một biến mới cho array (sắp sửa chứa chỉ những string có ba ký tự). Nhưng trong trường hợp này, khi xác định array thứ hai, chúng ta sẽ chỉ định nó trực tiếp với kết quả sau khi áp dụng method filter lên array động vật ban đầu. Chúng ta sẽ nhập vào filter một hàm nội dòng, chỉ trả true nếu giá trị mà hàm này đang hoạt động có độ dài là ba.

Cách hoạt động của method filter như sau: method này sẽ chạy qua từng element trong array và áp dụng hàm test lên element đó. Nếu hàm test trả true cho element đó, array dó method filter trả về sẽ chứa element đó. Các element khác sẽ bị bỏ qua.

Bạn có thể thấy code lúc này trong sạch hơn nhiều. Không cần phải biết trước filter làm gì, bạn có thể nhìn vào đoạn code này và tự suy ra được mục đích của từng phần trong đó.

Một trong những lợi ích rất hay của lập trình hàm mà ít ai để ý đến đó là “độ sạch” đến từ việc giảm số lượng state cục bộ được lưu trữ, và giới hạn việc chỉnh sửa các biến ngoại lai (external variable) ở ngay trong hàm. Trong trường hợp này, biến count và nhiều state khác mà array threeLetterAnimals thu nhận khi chúng ta lặp qua array gốc chỉ là thêm state nữa để theo dõi mà thôi. Khi sử dụng filter, ta đồng thời đã loại bỏ vòng lặp for cũng như biến count. Và chúng ta không làm biến đổi giá trị của array mới nhiều lần như trước nữa; mà chỉ xác định một lần, rồi chỉ định giá trị từ việc áp dụng điều kiện filter đến array ban đầu.

Các cách Format Filter khác

Code của chúng ta còn có thể trở nên ngắn gọn hơn nữa nếu tận dụng khai báo const và hàm mũi tên nội dòng ẩn danh (anonymous inline arrow functions). Chúng ta còn có một số tính năng EcmaScript 6 (ES6) hiện đã được hỗ trợ sẵn trong hầu hết trình duyệt và JavaScript engine.

Tuy việc sử dụng các cú pháp mới hơn tỏ ra là một ý hay trong đa số trường hợp, nhưng bạn vẫn phải chú ý chọn lọc nên dùng cú pháp nào (trừ khi bạn cần code mới khớp với codebase đã có), Khi ta càng rút ngắn, mỗi dòng code sẽ phải trở nên phức tạp hơn một chút.

Một lý do khiến JavaScript hấp dẫn, thú vị trong mắt nhiều người là khả năng kết hợp và sáng tạo theo nhiều hướng khác nhau để thực hiện cùng một kết quả; với kích thước, độ hiệu quả, độ rõ ràng hoặc khả năng maintain phù hợp với team của từng người. Nhưng như vậy cũng tạo ra nhiều khó khăn cho các team trong quá trình hoạch định phong cách code chung, cũng như phân tích lợi và hại của từng sự lựa chọn.

Trong trường hợp này, để giúp code dễ đọc và linh hoạt hơn, chúng ta nên tận dụng hàm mũi tên nội dòng ẩn danh này, và biến nó thành hàm có tên truyền thống, chuyển hàm có tên đó vào method filter. Đoạn code lúc này trở thành:

Tất cả mọi thứ ta làm ở đây là trích ra “hàm mũi tên nội dòng ẩn danh” đã xác định bên trên và biến hàm này thành một hàm có tên riêng biệt. Như ta thấy, chúng ta đã xác định một hàm thuần tiếp nhận kiểu giá trị phù hợp cho các element của array, và trả cùng một kiểu kết quả. Chúng ta cũng có thể chỉ chuyển tên của hàm đó trực tiếp vào method filter để làm điều kiện.

Tham gia ngay để biết thêm về cuộc chiến

Review nhanh về Map và Reduce

Filter làm việc song song với hai method hàm cho Array nữa từ ES5, là mapreduce. Và nhờ vào khả năng chain method trong JavaScript, bạn có thể sử dụng tổ hợp này để đẽo gọt được code sạch, từ đó thực hiện được những hàm khá phức tạp.

Nhắc nhanh: method map sẽ chạy qua từng element một trong array và chỉnh sửa element đó theo một hàm cụ thể, trả kết quả array mới cùng độ dài với các giá trị đã qua chỉnh sửa.

Method reduce chạy qua array và thực hiện một loạt thao tác, mang theo kết quả đang chạy của những thao tác đó vào một accumulator. Khi hoàn thành, method này sẽ trả một kết quả cuối cùng. Trong trường hợp này, chúng ta sẽ sử dụng argument thứ hai để đặt accumulator về lại số 0.

Cả ba method trên đều không động đến array gốc (và theo hướng lập trình hàm đúng thì nên như vậy).

Chaining Map, Reduce, và Filter

Để minh họa những gì bạn có thể làm được với cả ba method này, hãy tưởng tượng bạn muốn tiếp nhận một array các string, và trả về một string duy nhất bao hàm các string có ba ký tự từ array gốc, nhưng bạn còn muốn format string kết quả trong StudlyCaps nữa. Không dùng đến map, reduce, và filter, bạn có thể dùng:

Tất nhiên cách này chạy ổn, nhưng ta đang tạo quá nhiều biến thừa thãi, và phải maintain stage của một array bị thay đổi liên tục qua nhiều vòng lặp khác nhau. Ta hoàn toàn có thể làm tốt hơn thế này.

Và nếu bạn đang thắc mắc về logic đằng sau việc khai biến, tôi thích dùng let hơn để khai báo một array đích rỗng (dù xét về kỹ thuật thì nên được khai bằng const). Dùng let sẽ nhắc nhở tôi rằng nội dung của array sẽ bị biến đổi.

Hãy thử tạo một vài hàm thuần tiếp nhận string và trả kết quả cũng là string. Sau đó ta có thể dùng các hàm này trong chain method map, reducefilter; chuyển giao kết quả từ method này sang method tiếp đó bằng cách dưới đây:

Trong trường hợp này chúng ta xác định những hàm thuần studlyCaps, exactlyThree, và capitalize. Chúng ta có thể chuyển những hàm này trực tiếp đến map, reduce, và filter trong một chuỗi liên tục duy nhất. Trước hết, chúng ta sẽ filter array ban đầu với exactlyThree, sau đó map kết quả đến capitalize, và cuối cùng reduce kết quả này với studlyCaps. Và ta sẽ chỉ định kết quả cuối cùng của chuỗi thao tác đó trực tiếp đến biến threeLetterAnimals mới, hoàn toàn không có vòng lặp, không state trung gian và array gốc vẫn giữ nguyên.

Code kết quả sẽ trở nên sạch và dễ test hơn, và cho ta một loạt hàm thuần có thể được sử dụng lại hoặc tinh chỉnh đơn giản để áp dụng trong các bối cảnh khác.

Tương quan Filter và Hiệu năng

Có một điểm khác đáng tiếc là cho đến khi trình duyệt và các JavaScript engine tối ưu cho các methoid Array mới, method filter có khuynh hướng thực thi chậm hơn một chút so với vòng lặp for.

Dù có chậm hơn chút đỉnh (chênh lệch rất khó nhận thấy), thì tôi vẫn khuyến khích dùng method Array hơn vì chúng cho ra code sạch hơn rất nhiều. Với code sạch, việc maintain sẽ trở nên rất “dễ thở”. Hơn nữa, với đa số trường hợp ứng dụng hiện nay, điểm khác biệt nhỏ trong hiệu năng này sẽ không gây trở ngại quá lớn cho người dùng.

Trong trường hợp khuyết điểm hiệu năng của method Array tỏ ra khá nhiêm trọng, bạn cũng đã biết chính xác cách và chỗ cần tối ưu rồi. Và khi các JavaScript engine bắt đầu tối ưu cho những method mới này, mọi thứ sẽ chỉ tốt hơn mà thôi.

Đừng ngần ngại dùng filter ngay từ ngày hôm nay. Chức năng này hiện đã sẵn có trong ES5 (hầu như được hỗ trợ ở mọi nơi). Code bạn viết được sẽ sạch hơn và dễ maintain hơn. Với method filter, bạn hoàn toàn có thể tự tin rằng bạn sẽ không phải thay đổi state của array đang đánh giá nữa. Bạn sẽ luôn có một array mới cho mỗi lần thao tác và array gốc sẽ không bị thay đổi.

Techtalk via Sitepoint