`Đối tượng` Array`
Bài viết này giải thích về đối tượng Array.
Tôi sẽ giải thích cách sử dụng mảng thực tế từng bước một cách dễ hiểu.
YouTube Video
Đối tượng Array`
Đối tượng Array của JavaScript là một trong những cấu trúc quan trọng nhất tạo nền tảng cho mọi loại xử lý dữ liệu. Từ các thao tác mảng cơ bản đến các hàm bậc cao hữu ích cho việc chuyển đổi dữ liệu hiệu quả, có nhiều tính năng mà bạn nên biết.
Những điều cơ bản về mảng
Trong JavaScript, mảng là một cấu trúc dữ liệu cơ bản để xử lý nhiều giá trị cùng lúc. Ở đây, chúng tôi giới thiệu cách tạo mảng và cách đọc, ghi các phần tử của chúng với các ví dụ đơn giản.
1// Create arrays in different ways
2const arr1 = [1, 2, 3]; // array literal
3const arr2 = new Array(4, 5, 6); // Array constructor
4const arr3 = Array.of(7, 8, 9); // Array.of
5
6console.log("arr1 created with literal. :", arr1);
7console.log("arr2 created with constructor:", arr2);
8console.log("arr3 created with Array.of. :", arr3);
9// arr1 created with literal. : [ 1, 2, 3 ]
10// arr2 created with constructor: [ 4, 5, 6 ]
11// arr3 created with Array.of. : [ 7, 8, 9 ]
12
13// Access and modify elements
14let first = arr1[0]; // read element
15console.log("First element of arr1:", first);
16// First element of arr1: 1
17
18arr1[1] = 20; // modify element
19console.log("arr1 after modifying index 1:", arr1);
20// arr1 after modifying index 1: [ 1, 20, 3 ]
21
22const len = arr1.length; // get length
23console.log("Length of arr1:", len);
24// Length of arr1: 3
- Đoạn mã này cho thấy ba cách để tạo mảng, cách đọc và cập nhật các phần tử bằng chỉ số, cũng như cách lấy chiều dài bằng thuộc tính
length. - Mảng dạng literal là phổ biến nhất và dễ đọc, được sử dụng thường xuyên nhất trong các tình huống hàng ngày.
Thêm và Xóa Phần tử (ở Cuối hoặc Đầu mảng)
Mảng cho phép bạn dễ dàng thêm hoặc xóa phần tử ở cuối hoặc đầu mảng. Những thao tác này cũng hữu ích khi triển khai các cấu trúc như stack hoặc queue.
1// Push and pop (stack-like)
2const stack = [];
3console.log("Initial stack:", stack);
4
5stack.push(1); // push 1
6console.log("After push(1):", stack);
7
8stack.push(2); // push 2
9console.log("After push(2):", stack);
10
11const last = stack.pop(); // pop -> 2
12console.log("Popped value:", last);
13console.log("Stack after pop():", stack);
14
15// Unshift and shift (queue-like)
16const queue = [];
17console.log("Initial queue:", queue);
18
19queue.push('a'); // add to end
20console.log("After push('a'):", queue);
21
22queue.unshift('start'); // add to front
23console.log("After unshift('start'):", queue);
24
25const firstItem = queue.shift(); // remove from front
26console.log("Shifted value:", firstItem);
27console.log("Queue after shift():", queue);
28
29// Initial stack: []
30// After push(1): [ 1 ]
31// After push(2): [ 1, 2 ]
32// Popped value: 2
33// Stack after pop(): [ 1 ]
34
35// Initial queue: []
36// After push('a'): [ 'a' ]
37// After unshift('start'): [ 'start', 'a' ]
38// Shifted value: start
39// Queue after shift(): [ 'a' ]
pushvàpophoạt động ở cuối mảng. Chúng lý tưởng để triển khai các cấu trúc stack.unshiftvàshifthoạt động ở đầu mảng. Tuy nhiên, lưu ý rằng thao tác ở đầu mảng yêu cầu dịch chuyển các chỉ số phần tử, điều này khiến nó trở nên tốn kém hơn.
Xử lý các phần tử ở giữa (splice và slice)
Khi xử lý các phần tử ở giữa mảng, hãy chọn giữa splice và slice dựa trên việc bạn có muốn thay đổi mảng gốc hay không. Nếu bạn chỉ muốn trích xuất một phần của mảng, hãy sử dụng slice; nếu bạn muốn chỉnh sửa trực tiếp mảng như chèn hoặc xóa phần tử, hãy sử dụng splice.
1// slice (non-destructive)
2const nums = [0, 1, 2, 3, 4];
3console.log("Original nums:", nums);
4
5const part = nums.slice(1, 4); // returns [1, 2, 3]
6console.log("Result of nums.slice(1, 4):", part);
7console.log("nums after slice (unchanged):", nums);
8
9// splice (destructive)
10const arr = [10, 20, 30, 40];
11console.log("\nOriginal arr:", arr);
12
13// remove 1 item at index 2, insert 25 and 27
14arr.splice(2, 1, 25, 27);
15console.log("After arr.splice(2, 1, 25, 27):", arr);
16
17// Original nums: [ 0, 1, 2, 3, 4 ]
18// Result of nums.slice(1, 4): [ 1, 2, 3 ]
19// nums after slice (unchanged): [ 0, 1, 2, 3, 4 ]
20
21// Original arr: [ 10, 20, 30, 40 ]
22// After arr.splice(2, 1, 25, 27): [ 10, 20, 25, 27, 40 ]
slicechỉ tách phần tử mà không làm thay đổi mảng gốc.splicethêm hoặc xóa phần tử và trực tiếp thay đổi mảng, vì vậy hãy đặc biệt chú ý tác động của nó.
Lặp qua mảng (for / for...of / forEach)
Có nhiều cách để xử lý mảng theo tuần tự và bạn có thể chọn phù hợp với mục đích và phong cách lập trình của mình. Dưới đây là ba cấu trúc vòng lặp điển hình.
1const items = ['apple', 'banana', 'cherry'];
2console.log("Items:", items);
3
4// classic for
5console.log("\n--- Classic for loop ---");
6for (let i = 0; i < items.length; i++) {
7 console.log(`Index: ${i}, Value: ${items[i]}`);
8}
9
10// for...of (recommended for values)
11console.log("\n--- for...of loop ---");
12for (const item of items) {
13 console.log(`Value: ${item}`);
14}
15
16// forEach (functional style)
17console.log("\n--- forEach loop ---");
18items.forEach((item, index) => {
19 console.log(`Index: ${index}, Value: ${item}`);
20});- Vòng lặp
forlà linh hoạt nhất, cho phép thao tác chỉ số và kiểm soát tốt quá trình lặp bằng các lệnh nhưbreak. for...ofcung cấp cách xử lý giá trị phần tử ngắn gọn và cân bằng về khả năng đọc mã.forEachcho phép viết mã theo phong cách hàm và thích hợp cho các thao tác phụ như log hoặc cập nhật dữ liệu từng phần tử. Tuy nhiên, lưu ý rằng bạn không thể dùngbreakhoặccontinue, và nó không phù hợp cho xử lý bất đồng bộ vớiawait.
map / filter / reduce — Các hàm bậc cao
map, filter và reduce là các hàm bậc cao thường được sử dụng khi biến đổi, lọc hoặc tổng hợp mảng. Vì bạn có thể biểu đạt rõ ràng các xử lý lặp lại, mã của bạn trở nên đơn giản và dễ hiểu.
1const numbers = [1, 2, 3, 4, 5];
2console.log("Original numbers:", numbers);
3
4// map: transform each item
5const doubled = numbers.map(n => n * 2);
6console.log("\nResult of map (n * 2):", doubled);
7
8// filter: select items
9const evens = numbers.filter(n => n % 2 === 0);
10console.log("Result of filter (even numbers):", evens);
11
12// reduce: accumulate to single value
13const sum = numbers.reduce((acc, n) => acc + n, 0);
14console.log("Result of reduce (sum):", sum);
15
16// Original numbers: [ 1, 2, 3, 4, 5 ]
17// Result of map (n * 2): [ 2, 4, 6, 8, 10 ]
18// Result of filter (even numbers): [ 2, 4 ]
19// Result of reduce (sum): 15
- Các phương thức này giúp bạn tập trung vào việc mình muốn thực hiện theo phong cách khai báo, tăng khả năng đọc mã và tránh tác dụng phụ không mong muốn.
find / findIndex / some / every
Dưới đây là tổng quan về các phương thức tìm kiếm và kiểm tra điều kiện. Chúng hữu ích cho việc tìm phần tử đáp ứng điều kiện nhất định hoặc kiểm tra logic trên tập hợp.
1const users = [
2 { id: 1, name: 'Alice' },
3 { id: 2, name: 'Bob' },
4 { id: 3, name: 'Carol' }
5];
6
7console.log("Users:", users);
8
9// Find the first user whose name is 'Bob'
10const bob = users.find(user => user.name === 'Bob');
11console.log("\nResult of find (name === 'Bob'):", bob);
12
13// Find index of the user whose id is 3
14const indexOfId3 = users.findIndex(user => user.id === 3);
15console.log("Result of findIndex (id === 3):", indexOfId3);
16
17// Check if there exists a user with id = 2
18const hasId2 = users.some(user => user.id === 2);
19console.log("Result of some (id === 2):", hasId2);
20
21// Check if all users have a numeric id
22const allHaveNumericId = users.every(user => typeof user.id === 'number');
23console.log("Result of every (id is number):", allHaveNumericId);
24// Result of find (name === 'Bob'): { id: 2, name: 'Bob' }
25// Result of findIndex (id === 3): 2
26// Result of some (id === 2): true
27// Result of every (id is number): true
findtrả về phần tử đầu tiên phù hợp với điều kiện.findIndextrả về chỉ số của phần tử phù hợp với điều kiện.sometrả vềtruenếu có ít nhất một phần tử đáp ứng điều kiện.everytrả vềtruenếu tất cả các phần tử đáp ứng điều kiện.
Tất cả các phương thức này rất hữu ích trong xử lý mảng, sử dụng phù hợp sẽ giúp mã của bạn ngắn gọn và rõ ràng.
Sắp xếp và các hàm so sánh
Mảng được sắp xếp bằng sort, nhưng mặc định so sánh phần tử dưới dạng chuỗi, điều này có thể dẫn đến kết quả không mong muốn khi sắp xếp số.
1const nums = [10, 2, 33, 4];
2console.log("Original nums:", nums);
3
4// Default sort: compares elements as strings (not suitable for numbers)
5nums.sort();
6console.log("\nAfter default sort (string comparison):", nums);
7
8// Numeric ascending sort using a compare function
9nums.sort((a, b) => a - b);
10console.log("After numeric sort (a - b):", nums);
11
12// Sort objects by a property
13const people = [{ age: 30 }, { age: 20 }, { age: 25 }];
14console.log("\nOriginal people:", people);
15
16people.sort((a, b) => a.age - b.age);
17console.log("After sorting people by age:", people);
18
19// After default sort (string comparison): [ 10, 2, 33, 4 ]
20// After numeric sort (a - b): [ 2, 4, 10, 33 ]
21// Original people: [ { age: 30 }, { age: 20 }, { age: 25 } ]
22// After sorting people by age: [ { age: 20 }, { age: 25 }, { age: 30 } ]
- Khi sắp xếp số hoặc đối tượng, bạn luôn phải chỉ định hàm so sánh để đảm bảo sắp xếp đúng thứ tự mong muốn.
- Trong hàm so sánh, giá trị trả về âm đặt
atrướcb, dương đặtbtrướca, còn 0 giữ nguyên thứ tự.
Sao chép mảng và tính bất biến
Khi sao chép mảng, điều quan trọng là phải hiểu sự khác biệt giữa 'sao chép tham chiếu' và 'sao chép nông'. Đặc biệt lưu ý nếu trong mảng có đối tượng, sao chép nông sẽ khiến các đối tượng bên trong bị chia sẻ.
Sao chép tham chiếu
Khi bạn gán một mảng cho biến khác, bản thân mảng không được sao chép mà 'tham chiếu' trỏ tới cùng một mảng sẽ được sao chép.
1const a = [1, 2, 3];
2console.log("Original a:", a);
3
4// Reference copy; modifying b also affects a
5const b = a;
6console.log("\nReference copy b = a:", b);
7// Reference copy b = a: [ 1, 2, 3 ]
8
9b[0] = 100;
10console.log("After modifying b[0] = 100:");
11console.log("a:", a); // a: [ 100, 2, 3 ] (affected)
12console.log("b:", b); // b: [ 100, 2, 3 ]
- Với sao chép tham chiếu, nếu bạn sửa đổi nội dung của mảng bằng biến sao chép, những thay đổi đó cũng sẽ được phản ánh lên biến gốc trỏ tới cùng mảng.
Sao chép nông
Việc sử dụng slice() hoặc cú pháp spread tạo ra 'sao chép nông' vì chỉ các giá trị được sao chép; mảng gốc và mảng sao chép sẽ được xử lý như hai thực thể riêng biệt.
1const a = [1, 2, 3];
2console.log("Original a:", a);
3
4// Shallow copy (new array)
5const b = a.slice();
6console.log("\nShallow copy using slice():", b);
7// Shallow copy using slice(): [ 100, 2, 3 ]
8
9const c = [...a];
10console.log("Shallow copy using spread [...a]:", c);
11// Shallow copy using spread [...a]: [ 100, 2, 3 ]
12
13// Modifying c or d does NOT affect a
14b[1] = 200;
15c[2] = 300;
16console.log("\nAfter modifying b[1] = 200 and c[2] = 300:");
17console.log("a:", a); // [ 100, 2, 3 ]
18console.log("b:", b); // [ 100, 200, 3 ]
19console.log("c:", c); // [ 100, 2, 300 ]
- Đoạn mã này minh họa rằng việc tạo một bản sao nông cho mảng bằng
slice()hoặc cú pháp spread sẽ không làm ảnh hưởng đến mảng gốc.
Sao chép nông và tính bất biến
Ngay cả khi bạn sao chép một mảng bằng 'sao chép nông,' việc chia sẻ ngoài ý muốn có thể xảy ra nếu mảng chứa các đối tượng bên trong.
1// Shallow copy doesn't clone inner objects
2const nested = [{ x: 1 }, { x: 2 }];
3console.log("\nOriginal nested:", nested);
4// Original nested: [ { x: 1 }, { x: 2 } ]
5
6const shallow = nested.slice();
7console.log("Shallow copy of nested:", shallow);
8// Shallow copy of nested: [ { x: 1 }, { x: 2 } ]
9
10// Changing inner object affects both arrays
11shallow[0].x = 99;
12console.log("\nAfter shallow[0].x = 99:");
13console.log("nested:", nested);
14console.log("shallow:", shallow);
15// nested: [ { x: 99 }, { x: 2 } ]
16// shallow: [ { x: 99 }, { x: 2 } ]
- Đoạn mã này minh họa rằng với sao chép nông, các đối tượng bên trong sẽ được chia sẻ, vì vậy việc sửa đổi các đối tượng đó sẽ ảnh hưởng đến cả mảng gốc và bản sao.
- Nếu bạn cần dữ liệu độc lập, bạn phải sao chép sâu (deep copy), chẳng hạn như dùng
structuredClone()hoặc chuyển đổi JSON.
Các phương thức tiện ích hữu dụng
Các phương thức dưới đây thường xuyên được sử dụng khi làm việc với mảng. Sử dụng các phương thức này hợp lý sẽ giúp bạn viết mã ngắn gọn và dễ đọc.
includes
Phương thức includes kiểm tra xem một giá trị cụ thể có tồn tại trong mảng hay không.
1// includes: check if array contains a value
2const letters = ['a', 'b', 'c'];
3const hasB = letters.includes('b');
4console.log("letters:", letters); // [ 'a', 'b', 'c' ]
5console.log("letters.includes('b'):", hasB); // true
- Trong đoạn mã này, phương thức
includesđược sử dụng để kiểm tra một cách ngắn gọn xem giá trị đã chỉ định có tồn tại trong mảng hay không.
concat
Phương thức concat trả về một mảng mới nối thêm mảng hoặc giá trị chỉ định vào cuối, đồng thời giữ nguyên mảng gốc.
1// concat: merge arrays without mutation
2const a1 = [1, 2];
3const a2 = [3, 4];
4const combined = a1.concat(a2);
5console.log("a1.concat(a2):", combined); // [ 1, 2, 3, 4 ]
6console.log("a1(unchanged):", a1); // [ 1, 2 ]
- Đoạn mã này cho thấy
concatlà không phá hủy (non-destructive), cho phép bạn tạo mảng mới mà vẫn giữ nguyên mảng gốc.
flat
Bằng cách sử dụng phương thức flat, bạn có thể làm phẳng các mảng lồng nhau.
1// flat and flatMap: flatten arrays or map + flatten in one step
2const nested = [1, [2, [3]]];
3console.log("nested:", nested); // nested: [ 1, [ 2, [ 3 ] ] ]
4console.log("nested.flat():", nested.flat()); // default depth = 1
5// nested.flat(): [ 1, 2, [ 3 ] ]
- Đoạn mã này minh họa kết quả của việc làm phẳng một mảng theo một cấp.
- Vì
flatcho phép bạn chỉ định độ sâu, bạn có thể linh hoạt xử lý các tầng lồng ghép theo nhu cầu.
flatMap
Phương thức flatMap áp dụng một phép biến đổi cho từng phần tử và sau đó tự động làm phẳng kết quả thành một mảng một chiều.
1// flat and flatMap: flatten arrays or map + flatten in one step
2const words = ['hello world', 'hi'];
3console.log("words:", words); // [ 'hello world', 'hi' ]
4
5const splitWords = words.flatMap(w => w.split(' '));
6console.log("words.flatMap(w => w.split(' ')):", splitWords);
7// words.flatMap(w => w.split(' ')): [ 'hello', 'world', 'hi' ]
- Đoạn mã này minh họa ví dụ mỗi chuỗi trong mảng được tách theo dấu cách, các kết quả được kết hợp lại và làm phẳng thành một mảng duy nhất.
join
Phương thức join tạo một chuỗi bằng cách nối các phần tử của mảng với một ký tự phân tách được chỉ định.
1// join: combine elements into a string with a separator
2const csv = ['a', 'b', 'c'].join(',');
3console.log("['a', 'b', 'c'].join(','):", csv); // a,b,c
- Trong đoạn mã này, phương thức
joinđược sử dụng để chuyển đổi một mảng thành chuỗi cách nhau bằng dấu phẩy.
Các sai lầm thường gặp
Các thao tác với mảng thoạt nhìn có vẻ đơn giản nhưng có nhiều điểm dễ dẫn đến hành vi không mong muốn. Nhiều sai sót dễ bị bỏ qua khi thao tác mảng hàng ngày, nên ghi nhớ các điểm sau sẽ nâng cao độ tin cậy của mã nguồn.
sort()củaArraymặc định sắp xếp theo kiểu chuỗi. Khi sắp xếp số đúng cách, bạn luôn phải cung cấp một hàm so sánh.- Sao chép mảng (bằng
slicehoặc cú pháp spread, v.v.) tạo ra bản sao nông. Nếu mảng chứa đối tượng, hãy cẩn trọng vì dữ liệu gốc có thể bị thay đổi ngoài ý muốn. splicelà phương thức phá hủy, thay đổi trực tiếp mảng, cònslicelà phương thức không phá hủy, không thay đổi mảng gốc. Quan trọng là sử dụng đúng phương thức cho nhu cầu của bạn.forEachkhông phù hợp cho vòng lặp với xử lý async bằngawait. Nếu bạn muốn thực hiện xử lý bất đồng bộ một cách chính xác theo thứ tự, nên dùngfor...of.
Ví dụ thực tế
Dưới đây là ví dụ kết hợp các phương thức mảng để 'tính tổng tuổi từ dữ liệu người dùng, lọc ra những người 30 tuổi trở lên và tạo danh sách tên.'.
1const users = [
2 { name: 'Alice', age: 28 },
3 { name: 'Bob', age: 34 },
4 { name: 'Carol', age: 41 },
5 { name: 'Dave', age: 19 }
6];
7
8console.log("Users:", users);
9
10// sum ages
11const totalAge = users.reduce((acc, u) => acc + u.age, 0);
12console.log("\nTotal age of all users:", totalAge);
13// Total age of all users: 122
14
15// filter and map names of users 30 and older
16const namesOver30 = users
17 .filter(u => u.age >= 30)
18 .map(u => u.name);
19
20console.log("Users aged 30 or older (names):", namesOver30);
21// Users aged 30 or older (names): [ 'Bob', 'Carol' ]
- Bằng cách kết hợp
reduce,filtervàmap, bạn có thể dễ dàng viết các thao tác tổng hợp, trích xuất điều kiện và chuyển đổi dữ liệu. - Một chuỗi 'xử lý dữ liệu' như vậy rất dễ đọc và thường được sử dụng trong thực tế vì hiếm khi có tác dụng phụ.
Tóm tắt
Với mảng trong JavaScript, ngay cả các thao tác cơ bản cũng có thể ứng dụng rộng rãi, và sử dụng hàm bậc cao sẽ giúp mã ngắn gọn, biểu cảm hơn. Có nhiều điểm cần hiểu, nhưng khi bạn thành thạo từng phương pháp thích hợp, xử lý dữ liệu sẽ trở nên mượt mà hơn nhiều.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.