This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Trong JavaScript, các hàm được coi là "First-Class Citizens" (Công dân hạng nhất). Điều này có nghĩa là bạn có thể gán hàm cho biến, truyền hàm làm đối số cho hàm khác, và trả về hàm từ một hàm khác. Đây chính là bệ phóng cho mô hình lập trình hàm Functional Programming (FP) vô cùng mạnh mẽ, hướng tới mã nguồn sạch, dễ dự đoán và tối ưu hiệu năng.

1. Pure Functions (Hàm thuần khiết) & Tính Bất Biến (Immutability)

Lập trình hàm trong JavaScript tựa vào hai cột trụ chính để loại bỏ trạng thái đột biến ngoài ý muốn:

  • Pure Functions (Hàm thuần khiết): Là một hàm mà với cùng một tập hợp đối số đầu vào, nó luôn trả về duy nhất một kết quả như nhau và hoàn toàn không gây ra bất kỳ Side Effect (Tác dụng phụ) nào (không ghi đè biến ngoài, không sửa đổi trực tiếp tham số truyền vào, không gọi API hay thao tác DOM). Điều này giúp hàm cực kỳ dễ viết Unit Test và có thể song song hóa an toàn.
  • Immutability (Tính bất biến): Giá trị không thể thay đổi sau khi khởi tạo. Khi muốn sửa đổi một Object hoặc Array, thay vì chỉnh sửa trực tiếp giá trị của chúng (gây đột biến dữ liệu ngầm dẫn đến lỗi khó debug), ta tạo ra một bản sao mới chứa giá trị thay đổi.

Tại sao Mutating State lại nguy hiểm?

Khi bạn truyền một đối tượng vào nhiều hàm và một trong các hàm đó thay đổi thuộc tính của nó (Mutation), các phần khác của ứng dụng đang tham chiếu đến đối tượng đó sẽ bị ảnh hưởng ngầm. Điều này tạo ra các side effects cực kỳ khó debug vì trạng thái của đối tượng có thể bị thay đổi ở bất kỳ đâu trong Call Stack.

Immutable Update Patterns trong JavaScript

Hãy xem cách cập nhật Object và Array mà không làm thay đổi dữ liệu gốc:

immutable_updates.js
// 1. Cập nhật Object
const user = { name: "An", role: "user", address: { city: "HCM" } };

// Object spread vs Object.assign (Cảnh báo: Đều là shallow copy!)
const updatedUser = { ...user, role: "admin" }; 
// ⚠️ nested object vẫn dùng chung tham chiếu!
updatedUser.address.city = "Hanoi"; // user.address.city cũng bị đổi thành "Hanoi"!

// Deep copy bằng structuredClone (ES2022)
const deepCopyUser = structuredClone(user);
deepCopyUser.address.city = "Danang"; // An toàn, user.address.city vẫn là "Hanoi"

// 2. Cập nhật Array
const list = [1, 2, 3, 4];

// Không dùng push, pop, shift, unshift, splice (các hàm này mutate array gốc)
// Hãy dùng: spread operator, map, filter, concat, slice
const added = [...list, 5];                // push
const removed = list.filter(x => x !== 3); // remove item 3
const updated = list.map(x => x === 2 ? 20 : x); // update item 2 -> 20

Immer-style Recipe Pattern tự chế

Các thư viện như Immer cho phép bạn viết code dạng "mutate" một draft object tạm thời, nhưng kết quả trả về là một object immutable mới hoàn toàn. Đây là một phiên bản rút gọn tự implement dùng Proxy:

mini_immer.js
function produce(baseState, recipe) {
  const draft = structuredClone(baseState); // Tạo bản sao deep
  recipe(draft); // Thực thi hàm biến đổi trên draft
  return Object.freeze(draft); // Trả về bản sao đóng băng (immutable)
}

const state = { name: "Quang", tags: ["js", "html"] };
const nextState = produce(state, draft => {
  draft.name = "Tang Quang";
  draft.tags.push("css");
});

console.log(state.name);      // "Quang" (không đổi)
console.log(nextState.name);  // "Tang Quang" (đổi)
console.log(state.tags);      // ["js", "html"] (không đổi)
▶ Thử chạy: Immutability Lab

2. Kỹ Thuật Nâng Cao: Currying, Memoization & Pipe/Compose

Currying (Chuyển đổi tham số)

Currying là kỹ thuật biến đổi một hàm nhận vào nhiều tham số thành một chuỗi các hàm liên tiếp, mỗi hàm chỉ nhận vào duy nhất một tham số. Kỹ thuật này dựa trên Closures để ghi nhớ các tham số trước đó, rất hữu ích khi bạn muốn tạo ra các hàm chuyên biệt (Partial Application) từ một hàm tổng quát (ví dụ: tạo hàm ghi log lỗi chuyên dụng từ một hàm log chung).

Memoization (Tối ưu hóa bằng Cache)

Memoization là giải pháp lưu trữ kết quả của các hàm tính toán nặng dựa trên đối số đầu vào (Arguments). Khi hàm được gọi lại với cùng đối số đó, ta lấy ngay kết quả từ bộ nhớ đệm (Cache) mà không cần thực thi lại logic tính toán, tối ưu hóa triệt để hiệu năng của các hàm đệ quy hoặc xử lý dữ liệu phức tạp.

Xây dựng hàm Pipe & Compose

Để xâu chuỗi các hàm xử lý dữ liệu liên tiếp mà không bị lồng cú pháp khó đọc, chúng ta xây dựng hai helper kinh điển:

  • Pipe: Thực thi chuỗi các hàm từ trái qua phải (chảy dọc dữ liệu).
  • Compose: Thực thi chuỗi các hàm từ phải qua trái (đúng theo thứ tự toán học).
Cú pháp Pipe và Compose
const pipe = (...fns) => (x) => fns.reduce((res, fn) => fn(res), x);
const compose = (...fns) => (x) => fns.reduceRight((res, fn) => fn(res), x);

3. Pipeline Xử Lý Array & Tối Ưu Hóa V8 Elements Kinds

FP khuyến khích xử lý danh sách bằng cách xâu chuỗi các High-Order Functions như map(), filter(), và reduce() để tạo thành đường ống dữ liệu sạch sẽ, tránh dùng vòng lặp for thủ công dễ lỗi chỉ số.

Bản chất tối ưu hóa của V8 Engine: Elements Kinds trong Array

JavaScript không yêu cầu định nghĩa kiểu dữ liệu cho mảng. Tuy nhiên, ở cấp độ bộ nhớ, V8 Engine theo dõi chặt chẽ cấu trúc phần tử của mảng để gán nhãn Elements Kinds, quyết định tốc độ thực thi của các phương thức xử lý mảng:

  1. SMI Elements (Small Integers): Mảng chỉ chứa toàn số nguyên nhỏ (ví dụ: [1, 2, 3]). Đây là kiểu mảng chạy nhanh nhất vì V8 lưu chúng trực tiếp dưới dạng mảng C++ tuyến tính liền kề trong bộ nhớ.
  2. Double Elements: Mảng chứa số thực (ví dụ: [1, 2.5, 3]). Nếu bạn đẩy một số thực vào mảng SMI, V8 sẽ phải chạy cơ chế Deoptimization (Hạ cấp) toàn bộ mảng sang kiểu Double Elements, gây hao phí hiệu năng.
  3. Regular Elements: Mảng chứa các đối tượng phức tạp hoặc chuỗi ký tự (ví dụ: [1, "Hi", {}]).

Cảnh báo Holey Arrays (Mảng có lỗ hổng): Nếu bạn tạo lỗ hổng trong mảng bằng cách gán chỉ mục cách quãng (ví dụ: gán arr[10] = 5 khi mảng mới có độ dài là 3), mảng sẽ bị chuyển thành dạng Holey Elements (ví dụ: HOLEY_SMI_ELEMENTS). Mỗi khi bạn truy cập chỉ mục trong mảng Holey, V8 bắt buộc phải chạy thuật toán tìm kiếm đắt đỏ dọc theo chuỗi Prototype Chain (lục tìm trong Array.prototypeObject.prototype) để kiểm tra xem chỉ mục trống đó có giá trị mặc định nào không, khiến hiệu năng giảm sút nghiêm trọng.

[!TIP] Để mảng đạt hiệu suất tối đa: Tránh tạo lỗ hổng chỉ mục (Holes), giữ các phần tử trong mảng đồng nhất về kiểu dữ liệu (Monomorphic) và hạn chế thay đổi kiểu phần tử sau khi khởi tạo.

4. Code Thực Hành Nâng Cao: functional_patterns.js

Dưới đây là mã nguồn thực hành tích hợp đầy đủ so sánh Pure/Impure, Currying loggers, Fibonacci tối ưu bằng Memoize, Pipeline xử lý Array, hàm Pipe/Compose tự viết và ví dụ về V8 Elements Kinds:

functional_patterns.js
/**
 * Bài 3: JavaScript Functional Programming, High-Order Functions, Pipe/Compose và V8 Elements Kinds
 * Giáo trình tự học JavaScript - js-tools.org
 */

console.log("=== 1. Pure Functions & Immutability ===");

// Hàm không thuần khiết (impure) - làm thay đổi trực tiếp thuộc tính đối tượng (Side Effect)
function updateAgeImpure(user, newAge) {
  user.age = newAge; 
  return user;
}

// Hàm thuần khiết (pure) - tạo ra bản sao mới, bảo toàn nguyên gốc đối tượng cũ
function updateAgePure(user, newAge) {
  return { ...user, age: newAge }; // spread operator tạo bản sao mới
}

const userObj = { name: "An", age: 20 };
const updatedUser = updateAgePure(userObj, 21);
console.log("Đối tượng gốc không đổi (Immutability):", userObj); 
console.log("Đối tượng mới tạo ra:", updatedUser); 


console.log("\n=== 2. Currying (Chuyển đổi tham số) ===");

const add = (a, b, c) => a + b + c;

// Curried function dạng mũi tên lồng nhau
const curriedAdd = (a) => (b) => (c) => a + b + c;

console.log("Cộng thông thường add(1, 2, 3):", add(1, 2, 3));
console.log("Cộng curried curriedAdd(1)(2)(3):", curriedAdd(1)(2)(3));

// Ứng dụng: Tạo các hàm ghi log chuyên biệt
const log = (importance) => (message) => {
  console.log(`[${importance.toUpperCase()}] [${new Date().toLocaleTimeString()}]: ${message}`);
};

const logInfo = log("info");
const logError = log("error");

logInfo("Hệ thống khởi chạy thành công.");
logError("Kết nối cơ sở dữ liệu thất bại.");


console.log("\n=== 3. Memoization (Tối ưu hóa bằng lưu trữ bộ nhớ đệm) ===");

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// High-Order Function để thực hiện memoize tổng quát
function memoize(fn) {
  const cache = {}; // Object đóng vai trò làm bộ nhớ đệm
  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

const fastFibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return fastFibonacci(n - 1) + fastFibonacci(n - 2);
});

console.time("Fibonacci thông thường (n=35)");
fibonacci(35);
console.timeEnd("Fibonacci thông thường (n=35)"); 

console.time("Fibonacci tối ưu qua Memoize (n=35)");
fastFibonacci(35);
console.timeEnd("Fibonacci tối ưu qua Memoize (n=35)"); 


console.log("\n=== 4. Khai thác High-Order Functions trên Array ===");

const products = [
  { id: 1, name: "Laptop", price: 1500, category: "Tech" },
  { id: 2, name: "Phone", price: 800, category: "Tech" },
  { id: 3, name: "Book", price: 20, category: "Lifestyle" },
  { id: 4, name: "Keyboard", price: 100, category: "Tech" }
];

// Pipeline biến đổi dữ liệu sạch sẽ
const totalTechPrice = products
  .filter(p => p.category === "Tech") 
  .map(p => p.price)                   
  .reduce((sum, price) => sum + price, 0); 

console.log("Tổng giá trị các sản phẩm Tech:", totalTechPrice); 


console.log("\n=== 5. Xây dựng hàm Pipe & Compose ===");

// Pipe chạy từ trái qua phải (f -> g -> h)
const pipe = (...fns) => (x) => fns.reduce((res, fn) => fn(res), x);

// Compose chạy từ phải qua trái (h -> g -> f)
const compose = (...fns) => (x) => fns.reduceRight((res, fn) => fn(res), x);

const doubleVal = x => x * 2;
const addTen = x => x + 10;

const runPipe = pipe(doubleVal, addTen); // (x * 2) + 10
const runCompose = compose(doubleVal, addTen); // (x + 10) * 2

console.log("Pipe(doubleVal, addTen)(5) = ", runPipe(5)); // 20
console.log("Compose(doubleVal, addTen)(5) = ", runCompose(5)); // 30


console.log("\n=== 6. Mô phỏng V8 Elements Kinds trong Array ===");

// 1. SMI Elements: Mảng chứa toàn số nguyên nhỏ (Small Integers) -> Truy cập bộ nhớ O(1) tối đa
const arrSmi = [1, 2, 3]; // PACKED_SMI_ELEMENTS

// 2. Double Elements: Chứa số thực -> V8 chuyển đổi toàn bộ mảng sang dạng Double
arrSmi.push(4.5); // PACKED_DOUBLE_ELEMENTS (deoptimized)

// 3. Holey Elements: Có lỗ hổng trong chỉ mục mảng
const arrHoley = [1, 2, 3];
arrHoley[10] = 11; // Lỗ hổng ở chỉ mục 3 đến 9 -> HOLEY_SMI_ELEMENTS (truy cập chậm do check prototype)

console.log("arrSmi và arrHoley đã được khởi tạo để V8 xử lý phân loại Elements Kinds.");

5. Playground: Pure vs Impure Functions

Hãy tự chạy và quan sát sự khác biệt quan trọng giữa hàm thuần khiết và hàm có side effect:

▶ Thử chạy: Pure vs Impure Functions

6. Playground: Generic curry() — Chuyển Bất Kỳ Hàm Nào Thành Curried

Viết thủ công (a) => (b) => (c) => a+b+c cho từng hàm rất tẻ nhạt. Hàm curry() tổng quát giải quyết vấn đề này: nó nhận bất kỳ hàm đa tham số nào và trả về phiên bản curried tự động:

▶ Thử chạy: Generic curry() + Partial Application

7. Playground: Memoization — Benchmark Fibonacci

Memoization tiết kiệm thời gian tính toán bằng cách cache kết quả. Với Fibonacci đệ quy, số lần gọi hàm giảm từ hàng triệu xuống còn O(n):

▶ Thử chạy: Memoization Benchmark

8. Functor & Maybe Monad — Xử Lý Null-Safe Chaining

Functor là bất kỳ container nào có phương thức .map() tuân theo quy tắc: ánh xạ hàm lên giá trị bên trong mà giữ nguyên cấu trúc container. Array là functor. Một ví dụ hữu ích hơn là Maybe Monad — container xử lý null-safe chaining mà không cần if (val === null) ở khắp nơi:

Maybe Monad
// Cách truyền thống: if/else lồng nhau (Pyramid of Doom)
function getUserAvatarOld(userId) {
  const user = getUser(userId);
  if (user === null) return 'default.png';
  const profile = user.profile;
  if (profile === null) return 'default.png';
  const avatar = profile.avatar;
  if (avatar === null) return 'default.png';
  return avatar;
}

// Maybe Monad: null-safe chaining thanh lịch
class Maybe {
  constructor(value) { this._value = value; }

  static of(value) { return new Maybe(value); }

  isNothing() { return this._value === null || this._value === undefined; }

  // map: áp dụng fn nếu có giá trị, bỏ qua nếu null/undefined
  map(fn) {
    return this.isNothing() ? this : Maybe.of(fn(this._value));
  }

  // chain: giống map nhưng fn trả về Maybe (tránh Maybe lồng Maybe)
  chain(fn) {
    return this.isNothing() ? this : fn(this._value);
  }

  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this._value;
  }
}

// Sử dụng:
const getUser = (id) => Maybe.of(id === 1 ? { profile: { avatar: 'me.jpg' } } : null);

// Với user hợp lệ
const avatar1 = getUser(1)
  .map(u => u.profile)
  .map(p => p.avatar)
  .getOrElse('default.png');
console.log(avatar1);  // "me.jpg"

// Với user null — không crash, trả về default
const avatar2 = getUser(999)
  .map(u => u.profile)
  .map(p => p.avatar)
  .getOrElse('default.png');
console.log(avatar2);  // "default.png"
▶ Thử chạy: Maybe Monad Lab

9. Transducer — Pipeline Không Tạo Array Trung Gian

Vấn đề với .filter().map().reduce(): mỗi bước tạo một array tạm, tốn bộ nhớ khi xử lý tập dữ liệu lớn. Transducer giải quyết bằng cách compose các transformation trước, rồi chạy toàn bộ pipeline chỉ một lần duy nhất qua reduce:

Transducer concept
// Cách thông thường: 3 array tạm
const nums = Array.from({ length: 1000 }, (_, i) => i);
const result1 = nums
  .filter(n => n % 2 === 0)    // → Array[500]
  .map(n => n * 3)             // → Array[500]
  .reduce((acc, n) => acc + n, 0);

// Transducer: chỉ 1 lần reduce, không array tạm
const mapping = (fn) => (reducer) => (acc, val) => reducer(acc, fn(val));
const filtering = (pred) => (reducer) => (acc, val) => pred(val) ? reducer(acc, val) : acc;
const append = (acc, val) => { acc.push(val); return acc; };
const sum = (acc, val) => acc + val;

const transduce = (xform, reducer, init, coll) =>
  coll.reduce(xform(reducer), init);

// Compose 2 transducers: filter rồi map, chạy 1 lần
const xform = filtering(n => n % 2 === 0)(mapping(n => n * 3)(sum));
const result2 = nums.reduce((acc, val) => xform(acc, val), 0);

console.log(result1 === result2); // true — cùng kết quả!
// Nhưng result2 không tạo array tạm, tiết kiệm ~2x memory

Trắc nghiệm 1: V8 Elements Kinds

Điều gì xảy ra khi bạn gán arr[100] = 5 cho mảng [1, 2, 3]?

Trắc nghiệm 2: Maybe Monad

Trong code Maybe.of(null).map(x => x.name).getOrElse(\'unknown\'), kết quả là gì?

Tải file code thực hành minh họa bài học

File đầy đủ tích hợp Pure/Impure, generic curry(), Memoization benchmark, Pipe/Compose, Maybe Monad và V8 Elements Kinds:

Tải về functional_patterns.js

Related Articles

Bài viết liên quan trong series

Lesson 6: Error Handling & Debugging in JavaScript Bài 6: Xử Lý Lỗi & Gỡ Rối trong JavaScript Lesson 4: OOP in JS: Prototype Chain & ES6 Classes Bài 4: Hướng đối tượng trong JS: Prototype & ES6 Class Back to JavaScript Series Overview Quay lại Lộ trình JavaScript Series