Tiếng Việt

Khai phá sức mạnh của lập trình hướng chức năng với mảng JavaScript. Học cách biến đổi, lọc và giảm dữ liệu hiệu quả bằng các phương thức tích hợp.

Làm chủ Lập trình Hướng chức năng với Mảng JavaScript

Trong bối cảnh phát triển web không ngừng phát triển, JavaScript tiếp tục là một nền tảng cốt lõi. Trong khi các mô hình lập trình hướng đối tượng và mệnh lệnh từ lâu đã chiếm ưu thế, lập trình hướng chức năng (FP) đang ngày càng thu hút được sự chú ý. FP nhấn mạnh tính bất biến, các hàm thuần túy và mã khai báo, dẫn đến các ứng dụng mạnh mẽ hơn, dễ bảo trì hơn và có thể dự đoán được hơn. Một trong những cách mạnh mẽ nhất để áp dụng lập trình hướng chức năng trong JavaScript là tận dụng các phương thức mảng gốc của nó.

Hướng dẫn toàn diện này sẽ đi sâu vào cách bạn có thể khai thác sức mạnh của các nguyên tắc lập trình hướng chức năng bằng cách sử dụng các mảng JavaScript. Chúng ta sẽ khám phá các khái niệm chính và trình bày cách áp dụng chúng bằng các phương thức như map, filterreduce, thay đổi cách bạn xử lý thao tác dữ liệu.

Lập trình Hướng chức năng là gì?

Trước khi đi sâu vào mảng JavaScript, hãy cùng định nghĩa ngắn gọn về lập trình hướng chức năng. Về cốt lõi, FP là một mô hình lập trình coi tính toán là việc đánh giá các hàm toán học và tránh thay đổi trạng thái và dữ liệu có thể thay đổi. Các nguyên tắc chính bao gồm:

Áp dụng các nguyên tắc này có thể dẫn đến mã dễ suy luận, kiểm thử và gỡ lỗi hơn, đặc biệt là trong các ứng dụng phức tạp. Các phương thức mảng của JavaScript hoàn toàn phù hợp để triển khai các khái niệm này.

Sức mạnh của các Phương thức Mảng JavaScript

Các mảng JavaScript đi kèm với một bộ phương thức tích hợp phong phú cho phép thao tác dữ liệu tinh vi mà không cần dùng đến các vòng lặp truyền thống (như for hoặc while). Các phương thức này thường trả về các mảng mới, thúc đẩy tính bất biến và chấp nhận các hàm callback, cho phép cách tiếp cận chức năng.

Hãy cùng khám phá các phương thức mảng chức năng cơ bản nhất:

1. Array.prototype.map()

Phương thức map() tạo một mảng mới được điền bằng kết quả của việc gọi một hàm được cung cấp cho từng phần tử trong mảng gọi nó. Nó lý tưởng để biến đổi từng phần tử của mảng thành một thứ gì đó mới.

Cú pháp:

array.map(callback(currentValue[, index[, array]])[, thisArg])

Đặc điểm chính:

Ví dụ: Nhân đôi mỗi Số

Hãy tưởng tượng bạn có một mảng các số và bạn muốn tạo một mảng mới nơi mỗi số được nhân đôi.

const numbers = [1, 2, 3, 4, 5];

// Sử dụng map để biến đổi
const doubledNumbers = numbers.map(number => number * 2);

console.log(numbers); // Output: [1, 2, 3, 4, 5] (mảng gốc không thay đổi)
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]

Ví dụ: Trích xuất Thuộc tính từ Đối tượng

Một trường hợp sử dụng phổ biến là trích xuất các thuộc tính cụ thể từ một mảng các đối tượng. Giả sử chúng ta có danh sách người dùng và chỉ muốn lấy tên của họ.

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

const userNames = users.map(user => user.name);

console.log(userNames); // Output: ['Alice', 'Bob', 'Charlie']

2. Array.prototype.filter()

Phương thức filter() tạo một mảng mới với tất cả các phần tử vượt qua bài kiểm tra được thực hiện bởi hàm được cung cấp. Nó được sử dụng để chọn các phần tử dựa trên một điều kiện.

Cú pháp:

array.filter(callback(element[, index[, array]])[, thisArg])

Đặc điểm chính:

Ví dụ: Lọc các Số chẵn

Hãy lọc mảng số để chỉ giữ lại các số chẵn.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Sử dụng filter để chọn các số chẵn
const evenNumbers = numbers.filter(number => number % 2 === 0);

console.log(numbers); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(evenNumbers); // Output: [2, 4, 6, 8, 10]

Ví dụ: Lọc Người dùng Hoạt động

Từ mảng người dùng của chúng ta, hãy lọc những người dùng được đánh dấu là hoạt động.

const users = [
  { id: 1, name: 'Alice', isActive: true },
  { id: 2, name: 'Bob', isActive: false },
  { id: 3, name: 'Charlie', isActive: true },
  { id: 4, name: 'David', isActive: false }
];

const activeUsers = users.filter(user => user.isActive);

console.log(activeUsers); 
/* Output:
[
  { id: 1, name: 'Alice', isActive: true },
  { id: 3, name: 'Charlie', isActive: true }
]
*/

3. Array.prototype.reduce()

Phương thức reduce() thực thi một hàm callback “reducer” do người dùng cung cấp trên mỗi phần tử của mảng, theo thứ tự, truyền vào giá trị trả về từ phép tính trên phần tử trước đó. Kết quả cuối cùng của việc chạy reducer trên tất cả các phần tử của mảng là một giá trị duy nhất.

Đây có thể nói là phương thức đa năng nhất trong các phương thức mảng và là nền tảng của nhiều mô hình lập trình hướng chức năng, cho phép bạn “giảm” một mảng thành một giá trị duy nhất (ví dụ: tổng, tích, số đếm, hoặc thậm chí là một đối tượng hoặc mảng mới).

Cú pháp:

array.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

Đặc điểm chính:

Ví dụ: Tính tổng các Số

Hãy tính tổng tất cả các số trong mảng của chúng ta.

const numbers = [1, 2, 3, 4, 5];

// Sử dụng reduce để tính tổng các số
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // 0 là initialValue

console.log(sum); // Output: 15

Giải thích:

Ví dụ: Nhóm các Đối tượng theo một Thuộc tính

Chúng ta có thể sử dụng reduce để biến đổi một mảng các đối tượng thành một đối tượng mà các giá trị được nhóm theo một thuộc tính cụ thể. Hãy nhóm những người dùng của chúng ta theo trạng thái `isActive` của họ.

const users = [
  { id: 1, name: 'Alice', isActive: true },
  { id: 2, name: 'Bob', isActive: false },
  { id: 3, name: 'Charlie', isActive: true },
  { id: 4, name: 'David', isActive: false }
];

const groupedUsers = users.reduce((acc, user) => {
  const status = user.isActive ? 'active' : 'inactive';
  if (!acc[status]) {
    acc[status] = [];
  }
  acc[status].push(user);
  return acc;
}, {}); // Đối tượng trống {} là initialValue

console.log(groupedUsers);
/* Output:
{
  active: [
    { id: 1, name: 'Alice', isActive: true },
    { id: 3, name: 'Charlie', isActive: true }
  ],
  inactive: [
    { id: 2, name: 'Bob', isActive: false },
    { id: 4, name: 'David', isActive: false }
  ]
}
*/

Ví dụ: Đếm Số lần xuất hiện

Hãy đếm tần suất của mỗi loại trái cây trong một danh sách.

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];

const fruitCounts = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});

console.log(fruitCounts); // Output: { apple: 3, banana: 2, orange: 1 }

4. Array.prototype.forEach()

Mặc dù forEach() không trả về một mảng mới và thường được coi là mệnh lệnh hơn vì mục đích chính của nó là thực thi một hàm cho mỗi phần tử mảng, nhưng nó vẫn là một phương thức cơ bản đóng vai trò trong các mô hình chức năng, đặc biệt là khi cần có tác dụng phụ hoặc khi lặp mà không cần kết quả đã biến đổi.

Cú pháp:

array.forEach(callback(element[, index[, array]])[, thisArg])

Đặc điểm chính:

Ví dụ: In từng Phần tử

const messages = ['Hello', 'Functional', 'World'];

messages.forEach(message => console.log(message));
// Output:
// Hello
// Functional
// World

Lưu ý: Đối với các phép biến đổi và lọc, mapfilter được ưa chuộng hơn vì tính bất biến và bản chất khai báo của chúng. Sử dụng forEach khi bạn đặc biệt cần thực hiện một hành động cho mỗi mục mà không thu thập kết quả vào một cấu trúc mới.

5. Array.prototype.find()Array.prototype.findIndex()

Các phương thức này hữu ích để định vị các phần tử cụ thể trong một mảng.

Ví dụ: Tìm một Người dùng

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

const bob = users.find(user => user.name === 'Bob');
const bobIndex = users.findIndex(user => user.name === 'Bob');
const nonExistentUser = users.find(user => user.name === 'David');
const nonExistentIndex = users.findIndex(user => user.name === 'David');

console.log(bob); // Output: { id: 2, name: 'Bob' }
console.log(bobIndex); // Output: 1
console.log(nonExistentUser); // Output: undefined
console.log(nonExistentIndex); // Output: -1

6. Array.prototype.some()Array.prototype.every()

Các phương thức này kiểm tra xem tất cả các phần tử trong mảng có vượt qua bài kiểm tra được thực hiện bởi hàm được cung cấp hay không.

Ví dụ: Kiểm tra Trạng thái Người dùng

const users = [
  { id: 1, name: 'Alice', isActive: true },
  { id: 2, name: 'Bob', isActive: false },
  { id: 3, name: 'Charlie', isActive: true }
];

const hasInactiveUser = users.some(user => !user.isActive);
const allAreActive = users.every(user => user.isActive);

console.log(hasInactiveUser); // Output: true (vì Bob không hoạt động)
console.log(allAreActive); // Output: false (vì Bob không hoạt động)

const allUsersActive = users.filter(user => user.isActive).length === users.length;
console.log(allUsersActive); // Output: false

// Cách khác sử dụng every trực tiếp
const allUsersActiveDirect = users.every(user => user.isActive);
console.log(allUsersActiveDirect); // Output: false

Chuỗi các Phương thức Mảng cho các Thao tác Phức tạp

Sức mạnh thực sự của lập trình hướng chức năng với các mảng JavaScript tỏa sáng khi bạn nối chuỗi các phương thức này lại với nhau. Vì hầu hết các phương thức này trả về các mảng mới (ngoại trừ forEach), bạn có thể liền mạch chuyển đầu ra của một phương thức thành đầu vào của một phương thức khác, tạo ra các đường ống dữ liệu thanh lịch và dễ đọc.

Ví dụ: Tìm Tên Người dùng Hoạt động và Nhân đôi ID của họ

Hãy tìm tất cả người dùng hoạt động, trích xuất tên của họ, sau đó tạo một mảng mới nơi mỗi tên được tiền tố bằng một số đại diện cho chỉ mục của nó trong danh sách *đã lọc*, và ID của họ được nhân đôi.

const users = [
  { id: 1, name: 'Alice', isActive: true },
  { id: 2, name: 'Bob', isActive: false },
  { id: 3, name: 'Charlie', isActive: true },
  { id: 4, name: 'David', isActive: true },
  { id: 5, name: 'Eve', isActive: false }
];

const processedActiveUsers = users
  .filter(user => user.isActive) // Chỉ lấy người dùng hoạt động
  .map((user, index) => ({      // Biến đổi mỗi người dùng hoạt động
    name: `${index + 1}. ${user.name}`,
    doubledId: user.id * 2
  }));

console.log(processedActiveUsers);
/* Output:
[
  { name: '1. Alice', doubledId: 2 },
  { name: '2. Charlie', doubledId: 6 },
  { name: '3. David', doubledId: 8 }
]
*/

Cách tiếp cận theo chuỗi này là khai báo: chúng ta chỉ định các bước (lọc, sau đó ánh xạ) mà không cần quản lý vòng lặp tường minh. Nó cũng bất biến, vì mỗi bước tạo ra một mảng hoặc đối tượng mới, để lại mảng users gốc không bị thay đổi.

Tính bất biến trong Thực tế

Lập trình hướng chức năng phụ thuộc nhiều vào tính bất biến. Điều này có nghĩa là thay vì sửa đổi các cấu trúc dữ liệu hiện có, bạn tạo các cấu trúc mới với các thay đổi mong muốn. Các phương thức mảng của JavaScript như map, filterslice vốn hỗ trợ điều này bằng cách trả về các mảng mới.

Tại sao tính bất biến lại quan trọng?

Khi bạn cần thực hiện một thao tác mà theo cách truyền thống sẽ sửa đổi một mảng (như thêm hoặc xóa một phần tử), bạn có thể đạt được tính bất biến bằng cách sử dụng các phương thức như slice, cú pháp spread (...), hoặc bằng cách kết hợp các phương thức chức năng khác.

Ví dụ: Thêm một Phần tử một cách Bất biến

const originalArray = [1, 2, 3];

// Cách mệnh lệnh (sửa đổi originalArray)
// originalArray.push(4);

// Cách chức năng sử dụng cú pháp spread
const newArrayWithPush = [...originalArray, 4];
console.log(originalArray); // Output: [1, 2, 3]
console.log(newArrayWithPush); // Output: [1, 2, 3, 4]

// Cách chức năng sử dụng slice và nối chuỗi (ít phổ biến hơn bây giờ)
const newArrayWithSlice = originalArray.slice(0, originalArray.length).concat(4);
console.log(newArrayWithSlice); // Output: [1, 2, 3, 4]

Ví dụ: Xóa một Phần tử một cách Bất biến

const originalArray = [1, 2, 3, 4, 5];

// Xóa phần tử ở chỉ mục 2 (giá trị 3)

// Cách chức năng sử dụng slice và cú pháp spread
const newArrayAfterSplice = [
  ...originalArray.slice(0, 2),
  ...originalArray.slice(3)
];
console.log(originalArray); // Output: [1, 2, 3, 4, 5]
console.log(newArrayAfterSplice); // Output: [1, 2, 4, 5]

// Sử dụng filter để xóa một giá trị cụ thể
const newValueToRemove = 3;
const arrayWithoutValue = originalArray.filter(item => item !== newValueToRemove);
console.log(arrayWithoutValue); // Output: [1, 2, 4, 5]

Các thực hành tốt nhất và Kỹ thuật Nâng cao

Khi bạn đã quen hơn với các phương thức mảng chức năng, hãy xem xét các thực hành này:

Ví dụ: Cách tiếp cận Chức năng để Tổng hợp Dữ liệu

Hãy tưởng tượng bạn có dữ liệu bán hàng từ các khu vực khác nhau và muốn tính tổng doanh số bán hàng cho mỗi khu vực, sau đó tìm khu vực có doanh số cao nhất.

const salesData = [
  { region: 'North', amount: 100 },
  { region: 'South', amount: 150 },
  { region: 'North', amount: 120 },
  { region: 'East', amount: 200 },
  { region: 'South', amount: 180 },
  { region: 'North', amount: 90 }
];

// 1. Tính tổng doanh số theo khu vực sử dụng reduce
const salesByRegion = salesData.reduce((acc, sale) => {
  acc[sale.region] = (acc[sale.region] || 0) + sale.amount;
  return acc;
}, {});

// salesByRegion sẽ là: { North: 310, South: 330, East: 200 }

// 2. Chuyển đổi đối tượng tổng hợp thành một mảng các đối tượng để xử lý thêm
const salesArray = Object.keys(salesByRegion).map(region => ({
  region: region,
  totalAmount: salesByRegion[region]
}));

// salesArray sẽ là: [
//   { region: 'North', totalAmount: 310 },
//   { region: 'South', totalAmount: 330 },
//   { region: 'East', totalAmount: 200 }
// ]

// 3. Tìm khu vực có doanh số cao nhất bằng reduce
const highestSalesRegion = salesArray.reduce((max, current) => {
  return current.totalAmount > max.totalAmount ? current : max;
}, { region: '', totalAmount: -Infinity }); // Khởi tạo với một số rất nhỏ

console.log('Sales by Region:', salesByRegion);
console.log('Sales Array:', salesArray);
console.log('Region with Highest Sales:', highestSalesRegion);

/*
Output:
Sales by Region: { North: 310, South: 330, East: 200 }
Sales Array: [
  { region: 'North', totalAmount: 310 },
  { region: 'South', totalAmount: 330 },
  { region: 'East', totalAmount: 200 }
]
Region with Highest Sales: { region: 'South', totalAmount: 330 }
*/

Kết luận

Lập trình hướng chức năng với các mảng JavaScript không chỉ là một lựa chọn về phong cách; đó là một cách mạnh mẽ để viết mã sạch hơn, có thể dự đoán và mạnh mẽ hơn. Bằng cách áp dụng các phương thức như map, filterreduce, bạn có thể biến đổi, truy vấn và tổng hợp dữ liệu của mình một cách hiệu quả đồng thời tuân thủ các nguyên tắc cốt lõi của lập trình hướng chức năng, đặc biệt là tính bất biến và các hàm thuần túy.

Khi bạn tiếp tục hành trình phát triển JavaScript của mình, việc tích hợp các mô hình chức năng này vào quy trình làm việc hàng ngày của bạn chắc chắn sẽ dẫn đến các ứng dụng dễ bảo trì và có khả năng mở rộng hơn. Hãy bắt đầu bằng cách thử nghiệm với các phương thức mảng này trong các dự án của bạn và bạn sẽ sớm nhận ra giá trị to lớn của chúng.