Mục lục

Lập trình Hàm (Functional Programming) trong JS

Tại sao FP là tương lai của Frontend và cách áp dụng Pure Functions, Immutability, Closures vào kiến trúc ứng dụng.

1. Vấn đề: "The Ghost in the Machine" (Lỗi ma quái)

Hãy xem đoạn code sau và đoán kết quả:

javascript:
let user = { name: "Huy", premium: false };

function applyFreeTrial(u) {
  if (u.name === "Huy") {
    u.premium = true; // Side effect: thay đổi trực tiếp dữ liệu đầu vào
    return "Trial Started";
  }
}

const msg = applyFreeTrial(user);
// 100 dòng code sau...
console.log(user.premium); // Kết quả: true (Nhưng chưa chắc bạn muốn điều này!)

Thất bại:

  • Dữ liệu bị "hỏng": Một hàm kiểm tra vô tình thay đổi trạng thái của cả ứng dụng.
  • Khó truy vết: Bạn mất cả ngày để tìm xem hàm nào đã đổi premium từ false sang true.

Đây chính là lý do Functional Programming (FP) ra đời - để mang lại sự kiểm soát tuyệt đối thông qua các bộ quy tắc khắt khe.

2. Pure Functions (Hàm thuần khiết)

Một hàm được gọi là Pure nếu:

  1. Luôn trả về cùng một kết quả với cùng một bộ tham số.
  2. Không gây ra Side Effects (không thay đổi biến toàn cầu, không gọi API, không console.log bên trong).
javascript:
// Impure (Xấu)
let tax = 0.1;
function calculatePrice(price) {
  return price + (price * tax); // Phụ thuộc biến bên ngoài
}

// Pure (Tốt)
function calculatePrice(price, taxRate) {
  return price + (price * taxRate);
}

2. Immutability (Tính bất biến)

Trong FP, chúng ta không thay đổi dữ liệu gốc. Thay vào đó, chúng ta tạo ra một bản sao mới với những thay đổi cần thiết. Điều này giúp tránh được các lỗi "ma quái" khi một object bị thay đổi ở nhiều nơi khác nhau.

javascript:
### Lab: Thử nghiệm Immutability
Hãy chạy mã dưới đây để thấy sự khác biệt giữa việc thay đổi trực tiếp (Mutation) và tạo bản sao mới (Immutability).

```javascript-sandbox:Lab: State Management Patterns
const state = {
  user: "Antigravity",
  tokens: 100
};

// Kiểu Junior: Mutation (Thay đổi trực tiếp)
const juniorState = state;
juniorState.tokens = 150; 
console.log("Original state has changed!", state.tokens); // 150 (LỖI logic tiềm ẩn)

// Kiểu Senior: Immutability (Sử dụng Spread operator)
const seniorState = {
  ...state,
  tokens: 200
};
console.log("Original state stays safe:", state.tokens); // 150 (từ mutation trước)
console.log("New state created:", seniorState.tokens); // 200

3. Higher-Order Functions (HOF)

HOF là các hàm nhận hàm khác làm tham số hoặc trả về một hàm. Đây là công cụ mạnh mẽ nhất để tái sử dụng mã nguồn.

Ví dụ kinh điển: .map(), .filter(), .reduce().

javascript:
const numbers = [1, 2, 3, 4];
const doubles = numbers.map(n => n * 2); // map là một HOF

4. Closures: Linh hồn của FP

Closures cho phép một hàm ghi nhớ môi trường nơi nó được tạo ra, ngay cả sau khi môi trường đó đã thực thi xong. Đây là nền tảng của Encapsulation (Đóng gói) trong JS mà không cần dùng đến private class.

javascript:
function createCounter() {
  let count = 0; // Biến private
  return {
    increment: () => ++count,
    getCount: () => count
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
// console.log(count); // LỖI: count không thể truy cập từ bên ngoài

5. Tại sao Senior lại yêu FP?

  1. Dễ Test: Pure functions cực kỳ dễ viết unit test vì không cần giả lập (mock) môi trường.
  2. Dễ Dự đoán: Không có side effects giúp giảm thiểu lỗi "ngẫu nhiên" khi ứng dụng phình to.
  3. Concurrency: Trong môi trường đa luồng (như Web Workers), immutability giúp tránh được tranh chấp bộ nhớ (race conditions).
  4. Time Travel Debugging: Các tool như Redux DevTools hoạt động được là nhờ tính bất biến của dữ liệu.

Tình huống thực tế & Phỏng vấn

Scenario: Tối ưu hóa hiệu năng với Pure Functions

Bối cảnh: Bạn đang làm việc trên một dashboard hiển thị hàng ngàn giao dịch tài chính. Mỗi khi một giao dịch mới được thêm vào, toàn bộ danh sách bị render lại, gây giật lag.

Vấn đề: Team đang dùng push() để thêm phần tử vào mảng transactions. Vì transactions cũ và mới có cùng tham chiếu bộ nhớ (reference), React không nhận diện được sự thay đổi để tối ưu hiển thị.

Câu hỏi: Bạn sẽ áp dụng nguyên tắc nào của FP để giải quyết vấn đề này?

Xem phân tích Senior
  1. Áp dụng Immutability: Thay vì transactions.push(newTx), hãy dùng setTransactions([...transactions, newTx]). Việc tạo tham chiếu mới giúp các công cụ tối ưu như React.memo hoạt động chính xác.
  2. Pure Data Transformation: Viết một hàm pure formatData(list) để xử lý hiển thị. Hàm này không làm thay đổi list gốc, giúp code dễ trace lỗi hơn.

Interview Question: High Level

Q: Tại sao Closures lại là nền tảng của Functional Programming trong JavaScript? Cho ví dụ thực tế về việc dùng Closure để che giấu dữ liệu (Data Hiding).

Gợi ý đáp án

Đáp án: Closures cho phép chúng ta tạo ra các "Private Scope". Trong FP, chúng ta tránh dùng Class vì nó mang theo nhiều trạng thái (state) phức tạp. Closure giúp đóng gói logic và dữ liệu mà không cần đến this.

Ví dụ:

javascript:
const createLogger = (prefix) => (msg) => console.log(`[${prefix}] ${msg}`);
const docLogger = createLogger("DOCS");
docLogger("Lesson started"); // "[DOCS] Lesson started"

Hàm con ghi nhớ biến prefix ngay cả khi createLogger đã kết thúc.


Tổng kết

Chuyển đổi tư duy từ OOP sang FP không phải là chuyện một sớm một chiều. Hãy bắt đầu bằng cách cố gắng viết các hàm nhỏ, thuần khiết và hạn chế thay đổi trực tiếp các object/array. Đó là bước đi đầu tiên để trở thành một Senior Engineer thực thụ.

Quảng cáo
mdhorizontal