Đây là những khái niệm thường xuyên được hỏi khi phỏng vấn vị trí middle – senior Javascript. Trả lời được những câu hỏi này bạn hơn đa số phần còn lại của Javascript Developer rồi. Nếu bạn còn mông lung về chúng thì sau bài hôm nay mình tin rằng bạn sẽ tự tin hơn khi có ai đó hỏi bạn về những thứ này. Bắt đầu thôi!

Higher Order Function là gì

Higher order function là một function mà nhận vào tham số là function hoặc return về một function. Có vẻ vẫn khó hiểu nhỉ. Xem ví dụ nhé.

const tinhTong = a => b => a + b
const ketQua = [1, 2, 3, 4, 5].map((item) => item * item)

console.log(tinhTong(1)(2)) // 3
console.log(ketQua) // [ 1, 4, 9, 16, 25 ]

Hàm tinhTong return về một function, hàm map thì nhận vào một function. Và cả 2 đều được tính là một HOF ( higher order function ).

Trong Higher order function có 3 khái niệm các bạn cần nắm chắc là Callback function, Closure Currying. Mình sẽ đi vào chi tiết như sau.

1. Callback funtion

Callback function là một function mà được truyền vào một function khác như một tham số. Khá là đơn giản nhưng callback được áp dụng rất nhiều trong javascript. Vì Javascript là ngôn ngữ hướng sự kiện nên thay vì chờ đợi phản hồi, nó sẽ thực thi các tác vụ khác.

Một ví dụ hay gặp là thao tác lắng nghe sự kiện trong javascript. Tham số thứ 2 sau ‘click’ là một callback

document.getElementById("button").addEventListener("click", () => {
  alert("YOU CLICKED ME!");
});

hoặc là hàm forEach, map đối với array đều có tham số truyền vào là một function và đó là callback function

const num = [2, 4, 6, 8];
num.forEach((item, index) => {
  console.log("STT: ", index, "la ", item);
});
const result = num.map((item, index) => `STT: ${index} la ${item}`);

hoặc các thao tác bất đồng bộ trong NodeJS như readFile. Khi đọc xong một file thì ta mới có thể thao tác với giữ liệu vừa đọc được.

fs.readFile('/etc/passwd', (err, data) => {
  if (err) throw err;
  console.log(data);
});

2. Closure

Không có một định nghĩa chính thức nào cho Closure, một số khái niệm trên mạng khá lằng nhằn. Có nơi gọi nó là function, có nơi gọi nó là một kĩ thuật trong javascript. Mình có đúc kết lại một định nghĩa dễ hiểu và dễ hình dung nhất cho các bạn như thế này.

Để tìm hiểu về closure thì bạn phải hiểu về lexical scoping. Cái này rất đơn giản, Lexical scoping là cách mà bạn có thể dùng các biến được khai báo ở function cha ngay tại function con (nested function), bởi vì function con chứa scope của function cha.

Nếu function cha return về function con thì điều đó gọi là closure

Cùng xem xét ví dụ sau

const increase = () => {
  let x = 0;
  const increaseInner = () => ++x;
  return increaseInner;
};
const myFunc = increase();
console.log(increase()()); // 1
console.log(increase()()); // 1
console.log(myFunc()); // 1
console.log(myFunc()); // 2
console.log(myFunc()); // 3

Khi ta chạy hàm increase() thì sẽ return về hàm increaseInner, và chưa hề chạy qua đoạn code trong hàm increaseInner. Trong trường hợp này myFunc tham chiếu đến một instance increaseInner. Ta gọi increase là một closure function hay increase áp dụng kĩ thuật closure đều được.

Trong một số ngôn ngữ lập trình thì biến cục bộ bên trong hàm chỉ tồn tại khi hàm thực thi ( nghĩa là sau khi gọi increase() xong thì x sẽ bị giải phóng và không thể truy cập được nữa ). Nhưng trong Javascript thì nhờ vào myFunc tham chiếu đến instance increaseInner nên giúp duy trì biến x tồn tại. Vì vậy khi gọi myFunc() , giá trị biến x vẫn được tính toán và được đưa vào hàm . Từ đó sẽ cho ra các kết quả tăng dần như trên.

3. Currying

Currying là một kỹ thuật mà cho phép chuyển đổi một function nhiều tham số thành những function liên tiếp có một tham số.

Ví dụ về cách gọi hàm tinhTong(1)(2) đầu bài viết chính là một currying. Và trong kĩ thuật thuật currying này thì cũng áp dụng closure vì chính currying cũng dùng biến trong function cha.

Hãy giải thử 3 bài toán sau:

  • Hãy tìm các số tự nhiên bé hơn 10 và là số lẻ.
  • Hãy tìm các số tự nhiên bé hơn 20 và là số chẵn.
  • Hãy tìm các số tự nhiên bé hơn 30 và là số nếu chia 3 thì dư 2.

Một chút suy nghĩ 💡 , nếu bình thường thì chúng ta sẽ code bằng 3 hàm như thế này cho 3 bài toán trên

const findNumber1 = () => {
  const result = [];
  for (let i = 0; i < 10; i++) {
    if (i % 2 === 1) {
      result.push(i);
    }
  }
  return result;
};
const findNumber2 = () => {
  const result = [];
  for (let i = 0; i < 20; i++) {
    if (i % 2 === 0) {
      result.push(i);
    }
  }
  return result;
};
const findNumber3 = () => {
  const result = [];
  for (let i = 0; i < 30; i++) {
    if (i % 3 === 2) {
      result.push(i);
    }
  }
  return result;
};
findNumber1();
findNumber2();
findNumber3();

Cách này khá dài dòng, ở trong function nào chúng ta cũng lặp lại dòng khai báo resultfor. Như vậy rõ ràng không có tính tái sử dụng. Nếu áp dụng callback thì sẽ như sau

const findNumber = (num, func) => {
  const result = [];
  for (let i = 0; i < num; i++) {
    if (func(i)) {
      result.push(i);
    }
  }
  return result;
};
findNumber(10, (number) => number % 2 === 1);
findNumber(20, (number) => number % 2 === 0);
findNumber(30, (number) => number % 3 === 2);

Gọn hơn cách trên khá nhiều đúng không. Còn một tùy chọn khác là nếu ta dùng currying thì nó sẽ ra như thế này

const findNumber = (num) => (func) => {
  const result = [];
  for (let i = 0; i < num; i++) {
    if (func(i)) {
      result.push(i);
    }
  }
  return result;
};
findNumber(10)((number) => number % 2 === 1);
findNumber(20)((number) => number % 2 === 0);
findNumber(30)((number) => number % 3 === 2);

Nhìn  không khác mấy so với callback, hay nói cách khác là khó đọc hơn, nhưng dù sao đi nữa thì đó là currying. Trong thực tế khi code với react thỉnh thoảng bạn sẽ đụng đến currying như thế này.

Tóm lại

Cá nhân mình thấy dùng currying làm cho code dài hơn và khó đọc hơn nên ít khi mình áp dụng kĩ thuật này trừ khi trong một số trường hợp đặc biệt. Closure cũng ít dùng, còn lại callback thì dùng nhiều.

Theo mình người code giỏi là một người code sao cho đảm bảo về hiệu năng cũng như tính dễ đọc. Nếu một vấn đề có cách giải quyết đơn giản thì hãy làm đơn giản, không cần phải áp dụng những kĩ thuật cao siêu mà hiệu quả đem lại không khác nhau.

Hết bài rồi đó, thực sự những cái này hơi hơi khó hiểu và trừu tượng nhưng mình cũng đã tìm những ví dụ và giải thích sao cho dễ hiểu nhất rồi. Hi vọng giúp ích được các bạn trên con đường master Javascript. Đây là bài đầu tiên trong series Javascript nâng cao, tiếp theo là bài về Scope và Hoisting chuyên sâu. Nếu ai chưa like thì hãy mau like Fanpage Xdevclass để nhận thông báo bài mới nào 😛