JavaScript: Tìm hiểu các đối tượng Map và Set trong JavaScript

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Trong JavaScript, các nhà phát triển thường dành nhiều thời gian để quyết định cấu trúc dữ liệu chính xác để sử dụng. Điều này là do việc chọn cấu trúc dữ liệu chính xác có thể giúp bạn dễ dàng thao tác dữ liệu đó hơn sau này, tiết kiệm thời gian và làm cho code dễ hiểu hơn. Hai cấu trúc dữ liệu chủ yếu để lưu trữ tập hợp dữ liệu là Đối tượng và Mảng (một kiểu đối tượng). Các nhà phát triển sử dụng Đối tượng để lưu trữ các cặp key/value và Mảng để lưu trữ danh sách được lập chỉ mục. Tuy nhiên, để cung cấp cho các nhà phát triển sự linh hoạt hơn, đặc tả ECMAScript 2015 đã giới thiệu hai loại đối tượng mới có thể lặp (iterable) là: Map, là tập hợp có thứ tự của các cặp key/value và Set, là tập hợp các giá trị duy nhất.

Trong bài viết này, bạn sẽ xem xét các đối tượng Map và Set, điều gì làm cho chúng giống hoặc khác với Đối tượng và Mảng, các thuộc tính và phương thức có sẵn của chúng, và các ví dụ về một số cách sử dụng trong thực tế.

Map

Map là một tập hợp các cặp key/value có thể sử dụng bất kỳ kiểu dữ liệu nào làm khóa và có thể duy trì thứ tự các entry của nó. Map có các phần tử của cả Đối tượng (tập hợp cặp key/value duy nhất) và Mảng (tập hợp có thứ tự), nhưng giống với Đối tượng hơn về mặt khái niệm. Điều này là do, mặc dù kích thước và thứ tự của các entry được giữ nguyên như một Mảng, nhưng bản thân các entry là các cặp key/value giống như Đối tượng.

Map có thể được khởi tạo với cú pháp new Map():

const map = new Map()

Điều này sẽ cho ta một Map trống:

Output

Map(0) {}

Thêm giá trị vào map

Bạn có thể thêm các giá trị vào map bằng phương thức set(). Đối số đầu tiên sẽ là key và đối số thứ hai sẽ là value.

Đoạn code sau sẽ thêm ba cặp key/value vào map:

map.set('firstName', 'Luke')
map.set('lastName', 'Skywalker')
map.set('occupation', 'Jedi Knight')

Ở đây chúng ta bắt đầu xem map có các phần tử của cả Đối tượng và Mảng như thế nào. Giống như Mảng, chúng ta có một bộ sưu tập được lập chỉ mục bằng 0 và chúng ta cũng có thể xem có bao nhiêu mục trong Map theo mặc định. Map sử dụng của pháp => để biểu thị các cặp key/value như key => value:  

Output

Map(3)

0: {"firstName" => "Luke"}

1: {"lastName" => "Skywalker"}

2: {"occupation" => "Jedi Knight"}

Ví dụ này trông tương tự như một đối tượng thông thường với các khóa dựa trên chuỗi, nhưng chúng ta có thể sử dụng bất kỳ loại dữ liệu nào làm khóa với Map.

Ngoài việc thiết lập các giá trị trên Map theo cách thủ công, chúng ta cũng có thể khởi tạo Map với các giá trị đã có. Chúng ta thực hiện việc này bằng cách sử dụng Mảng của Mảng chứa hai phần tử là mỗi cặp key/value, trông giống như sau:

[ [ 'key1', 'value1'], ['key2', 'value2'] ]

Sử dụng cú pháp sau, chúng ta có thể tạo lại cùng một Map:

const map = new Map([
  ['firstName', 'Luke'],
  ['lastName', 'Skywalker'],
  ['occupation', 'Jedi Knight'],
])

Lưu ý: Ví dụ này sử dụng dấu phẩy ở cuối, còn được gọi là dấu phẩy treo. Đây là một thực tế định dạng JavaScript trong đó mục cuối cùng trong chuỗi khi khai báo một tập hợp dữ liệu có dấu phẩy ở cuối. Mặc dù lựa chọn định dạng này có thể được sử dụng để tạo ra sự khác biệt rõ ràng hơn và thao tác mã dễ dàng hơn, việc sử dụng nó hay không là một vấn đề tùy chọn. Để biết thêm thông tin về dấu phẩy cuối, hãy xem bài viết Dấu phẩy cuối này từ tài liệu web MDN.

Ngẫu nhiên, cú pháp này giống với kết quả của việc gọi Object.entries() trên một Đối tượng. Điều này cung cấp một cách sẵn sàng để chuyển đổi một Đối tượng thành một Map, như được hiển thị trong khối mã sau:

const luke = {
  firstName: 'Luke',
  lastName: 'Skywalker',
  occupation: 'Jedi Knight',
}

const map = new Map(Object.entries(luke))

Ngoài ra, bạn có thể biến Map trở lại thành Đối tượng hoặc Mảng chỉ với một dòng lệnh.

Lệnh sau đây chuyển đổi một Map thành một Đối tượng:

const obj = Object.fromEntries(map)

Điều này sẽ dẫn đến giá trị sau của obj:

Output

{firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}

Bây giờ, hãy chuyển đổi một Map thành một Mảng:

const arr = Array.from(map)

Điều này sẽ dẫn đến Mảng sau cho arr:

Output

[ ['firstName', 'Luke'], ['lastName', 'Skywalker'], ['occupation', 'Jedi Knight'] ]

Key (khóa) của Map

Map chấp nhận bất kỳ kiểu dữ liệu nào làm khóa và không cho phép các giá trị khóa trùng lặp. Chúng ta có thể chứng minh điều này bằng cách tạo một Map và sử dụng các giá trị không phải chuỗi làm khóa, cũng như đặt hai giá trị cho cùng một khóa.

Đầu tiên, hãy khởi tạo một Map bằng các khóa không phải chuỗi:

const map = new Map()

map.set('1', 'String one')
map.set(1, 'This will be overwritten')
map.set(1, 'Number one')
map.set(true, 'A Boolean')

Ví dụ này sẽ ghi đè khóa đầu tiên của 1 bằng một chuỗi và nó sẽ coi (treat) chuỗi '1' và số 1 là các khóa duy nhất:

Output

0: {"1" => "String one"}

1: {1 => "Number one"}

2: {true => "A Boolean"}

Mặc dù người ta thường tin rằng một Đối tượng JavaScript thông thường đã có thể xử lý Number, boolean và các kiểu dữ liệu nguyên thủy khác làm khóa, nhưng thực tế không phải vậy, vì Đối tượng thay đổi tất cả các khóa thành chuỗi.

Ví dụ: khởi tạo một đối tượng bằng khóa số và so sánh giá trị của khóa số 1 và khóa được xâu chuỗi "1":

// Khởi tạo một đối tượng với một key là số
const obj = { 1: 'One' }

// Key thực tế là một chuỗi
obj[1] === obj['1']  // true

Đây là lý do tại sao nếu bạn cố gắng sử dụng một Đối tượng làm khóa, nó sẽ in ra chuỗi object Object thay thế.

Ví dụ: tạo một Đối tượng và sau đó sử dụng nó làm khóa của một Đối tượng khác:

// Tạo một đối tượng
const objAsKey = { foo: 'bar' }

// Sử dụng đối tượng làm khóa của đối tượng khác
const obj = {
  [objAsKey]: 'What will happen?'
}

Kết quả:

Output

{[object Object]: "What will happen?"}

Đây không phải là trường hợp của Map. Hãy thử tạo một Đối tượng và đặt nó làm khóa của Map:

// Tạo một đối tượng
const objAsKey = { foo: 'bar' }

const map = new Map()

// Thiết lập đối tượng như là khóa của Map
map.set(objAsKey, 'What will happen?')

key của phần tử Map bây giờ là đối tượng mà chúng ta đã tạo.

Output

key: {foo: "bar"}

value: "What will happen?"

Có một điều quan trọng cần lưu ý về việc sử dụng Đối tượng hoặc Mảng làm khóa: Map đang sử dụng tham chiếu đến Đối tượng để so sánh bình đẳng, không phải giá trị theo nghĩa đen của Đối tượng. Trong JavaScript thì {} === {} sẽ trả về false, bởi vì hai Đối tượng không phải là hai Đối tượng giống nhau, mặc dù có cùng giá trị (trống).

Điều đó có nghĩa là việc thêm hai Đối tượng duy nhất có cùng giá trị sẽ tạo một Map có hai entry:

// Thêm hai đối tượng duy nhất nhưng tương tự nhau làm key cho Map
map.set({}, 'One')
map.set({}, 'Two')

Kết quả:

Output

Map(2) {{…} => "One", {…} => "Two"}

Nhưng việc sử dụng cùng một tham chiếu Đối tượng hai lần sẽ tạo một Map với một entry.

// Thêm một đối tượng hai lần làm khóa của Map
const obj = {}

map.set(obj, 'One')
map.set(obj, 'Two')

Kết quả:

Output

Map(1) {{…} => "Two"}

Thứ hai set() là cập nhật cùng một khóa chính xác với khóa đầu tiên, vì vậy chúng ta có một Map chỉ có một giá trị.

Lấy và xóa các phần tử khỏi Map

Một trong những nhược điểm khi làm việc với Đối tượng là có thể khó liệt kê chúng hoặc làm việc với tất cả các khóa hoặc giá trị. Ngược lại, cấu trúc Map có rất nhiều thuộc tính tích hợp giúp làm việc với các phần tử của chúng trực tiếp và dễ dàng hơn.

Chúng ta có thể tạo một Map mới để thử nghiệm các phương thức và thuộc tính sau : delete(), has()get() và size.

// Tạo một Map mới
const map = new Map([
  ['animal', 'otter'],
  ['shape', 'triangle'],
  ['city', 'New York'],
  ['country', 'Bulgaria'],
])

Sử dụng phương thức has() để kiểm tra sự tồn tại của một phần tử trong Map. has() sẽ trả về một Boolean.

// Check xem key có tồn tại trong Map
map.has('shark') // false
map.has('country') // true

Sử dụng phương thức get() để lấy một giá trị theo khóa.

// Lấy một phần tử từ Map
map.get('animal') // "otter"

Một lợi ích cụ thể của Map so với Đối tượng là bạn có thể tìm thấy kích thước của Map bất kỳ lúc nào, giống như bạn có thể làm với Mảng. Bạn có thể nhận được số lượng các phần tử trong Map với thuộc tính size. Điều này bao gồm ít bước hơn so với việc chuyển đổi một Đối tượng thành một Mảng để tìm kích thước.

// Lấy kích thước của Map
map.size // 4

Sử dụng phương thức delete() để xóa một mục khỏi Map bằng khóa. Phương thức này sẽ trả về true nếu một mục tồn tại và đã bị xóa, và false nếu nó không khớp với bất kỳ mục nào.

// Xóa một mục khỏi Map bằng key
map.delete('city') // true

Điều này sẽ dẫn đến Map sau:

Output

Map(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}

Cuối cùng, ta có thể xóa tất cả các phần tử của Map bằng clear().

// Empty một Map
map.clear()

Kết quả:

Output

Map(0) {}

Khóa, giá trị và entry cho Map

Các đối tượng có thể truy xuất các khóa, giá trị và entry bằng cách sử dụng các thuộc tính của hàm tạo Object. Mặt khác, Map có các phương thức nguyên mẫu cho phép chúng ta nhận trực tiếp các khóa, giá trị và entry của đối tượng Map.

Tất cả các phương thức keys()values() và entries() đều trả về một MapIterator tương tự như một Mảng mà bạn có thể sử dụng để lặp for...of qua các giá trị.

Đây là một ví dụ khác về Map, chúng ta có thể sử dụng để thử nghiệm các phương thức này:

const map = new Map([
  [1970, 'bell bottoms'],
  [1980, 'leg warmers'],
  [1990, 'flannel'],
])

Phương thức keys() trả về các khóa:

map.keys()
Output

MapIterator {1970, 1980, 1990}

Phương thức values() trả về các giá trị:

map.values()
Output

MapIterator {"bell bottoms", "leg warmers", "flannel"}

Phương thức entries() trả về một mảng các cặp khóa/giá trị:

map.entries()
Output

MapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}

Iterate với Map

Map có một phương thức được xây dựng sẵn là forEach, phương thức này được dùng để thực hiện iterate (lặp). Callback forEach của Map sẽ lặp qua valuekey và chính bản thân nó, điều này sẽ khác so với Mảng lặp lại qua itemindex và chính bản thân mảng đó.

// Map 
Map.prototype.forEach((value, key, map) = () => {})

// Array
Array.prototype.forEach((item, index, array) = () => {})

Đây là một lợi thế lớn đối với Map so với Đối tượng, vì Đối tượng cần được chuyển đổi với keys()values() hoặc entries() và không có cách đơn giản nào để truy xuất các thuộc tính của Đối tượng mà không cần chuyển đổi nó.

Để chứng minh điều này, hãy lặp Map của chúng ta và ghi các cặp khóa / giá trị vào console:

// Ghi các key và value của Map với forEach
map.forEach((value, key) => {
  console.log(`${key}: ${value}`)
})

Kết quả:

Output

1970: bell bottoms 1980: leg warmers 1990: flannel

Vì vòng lặp for...of sẽ lặp lặp đi lặp lại trên các đoạn lặp như Map và Mảng, chúng ta có thể nhận được cùng một kết quả chính xác bằng cách cấu trúc lại mảng các mục Map:

// Hủy key và value khỏi mục Map
for (const [key, value] of map) {
  // Ghi key và value của Map với for...of
  console.log(`${key}: ${value}`)
}

Các thuộc tính và phương thức của Map

Bảng sau đây hiển thị danh sách các thuộc tính và phương thức của Map:

Thuộc tính / Phương thức Mô tả Trả về
set(key, value) Thêm cặp khóa / giá trị vào Map Đối tượng Map
delete(key) Xóa cặp khóa / giá trị khỏi Map theo khóa Boolean
get(key) Trả về một giá trị theo khóa giá trị
has(key) Kiểm tra sự hiện diện của một phần tử trong Map bằng khóa Boolean
clear() Xóa tất cả các mục khỏi Map N/A
keys() Trả về tất cả các khóa trong Map Đối tượng MapIterator
values() Trả về tất cả các giá trị trong Map Đối tượng MapIterator
entries() Trả về tất cả các khóa và giá trị trong Map dưới dạng [key, value] Đối tượng MapIterator
forEach() Lặp qua Map theo thứ tự chèn N/A
size Trả về số lượng phần tử của Map Số

Khi nào sử dụng Map

Tóm lại, Map tương tự như Đối tượng ở chỗ chúng chứa các cặp khóa/giá trị, nhưng Map có một số lợi thế so với đối tượng:

  • Kích thước - Map có thuộc tính size, trong khi Đối tượng không có để truy xuất kích thước của chúng.
  • Lặp - Map có thể lặp trực tiếp, trong khi Đối tượng thì không.
  • Tính linh hoạt - Map có thể có bất kỳ kiểu dữ liệu nào (nguyên thủy hoặc Đối tượng) làm khóa cho một giá trị, trong khi Đối tượng chỉ có thể có chuỗi.
  • Có thứ tự - Map giữ nguyên thứ tự chèn của chúng, trong khi đối tượng không có thứ tự đảm bảo.

Do những yếu tố này, Map là một cấu trúc dữ liệu mạnh mẽ cần xem xét. Tuy nhiên, Object cũng có một số lợi thế quan trọng:

  • JSON - Các đối tượng hoạt động hoàn hảo với JSON.parse() và JSON.stringify(), hai hàm cần thiết để làm việc với JSON, một định dạng dữ liệu phổ biến mà nhiều API REST xử lý.
  • Làm việc với một phần tử duy nhất - Làm việc với một giá trị đã biết trong một Đối tượng, bạn có thể truy cập trực tiếp vào nó bằng khóa mà không cần sử dụng một phương thức nào, nhưng với Map chẳng hạn thì cần phương thức get().

Những đặc điểm trên của Map và Đối tượng sẽ giúp bạn quyết định xem Map hoặc Đối tượng có phải là cấu trúc dữ liệu phù hợp cho trường hợp sử dụng của bạn hay không.

Set

Set là một tập hợp các giá trị duy nhất. Không giống như Map, một Set về mặt khái niệm giống với Mảng hơn là một Đối tượng, vì nó là một danh sách các giá trị chứ không phải các cặp khóa / giá trị. Tuy nhiên, Set không phải là sự thay thế cho Mảng, mà là một phần bổ sung để cung cấp hỗ trợ bổ sung để làm việc với dữ liệu trùng lặp.

Bạn có thể khởi tạo Set bằng cú pháp new Set().

const set = new Set()

Kết quả:

Output

Set(0) {}

Các phần tử (mục - item) có thể được thêm vào Set bằng phương thức add() (Điều này không được nhầm lẫn với phương thức set() có sẵn của Map, mặc dù chúng tương tự nhau).

// Thêm các item vào Set
set.add('Beethoven')
set.add('Mozart')
set.add('Chopin')

Vì Set chỉ có thể chứa các giá trị duy nhất nên mọi nỗ lực thêm giá trị đã tồn tại sẽ bị bỏ qua.

set.add('Chopin') // Set sẽ vẫn chỉ chứa 3 giá trị ở trên

Lưu ý : So sánh bình đẳng tương tự áp dụng cho các key của Map thì cũng áp dụng cho các item của Set. Hai đối tượng có cùng giá trị nhưng không chia sẻ cùng một tham chiếu sẽ không được coi là bằng nhau.

Bạn cũng có thể khởi tạo Set với Mảng giá trị. Nếu có các giá trị trùng lặp trong mảng, chúng sẽ bị xóa khỏi Set.

// Tạo Set từ Mảng
const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
Kết quả:
Output

Set(3) {"Beethoven", "Mozart", "Chopin"}

Ngược lại, một Set có thể được chuyển đổi thành Mảng với một dòng lệnh như sau:

const arr = [...set]
Output

(3) ["Beethoven", "Mozart", "Chopin"]

Set có nhiều phương thức và thuộc tính giống như Map, bao gồm delete()has()clear() và size.

// Xóa một item
set.delete('Beethoven') // true

// Kiểm tra sự tồn tại của item
set.has('Beethoven') // false

// Xóa toàn bộ Set
set.clear()

// Lấy kích thước của Set
set.size // 0

Lưu ý rằng Set không có cách nào để truy cập giá trị bằng khóa hoặc chỉ mục, như Map.get(key) hoặc arr[index].

Khóa, giá trị và entry cho Set

Map và Set đều có các phương thức keys()values() và entries() trả về một Iterator. Tuy nhiên, trong khi mỗi phương thức này có một mục đích riêng biệt trong Map, thì Set không có khóa và do đó khóa là bí danh cho các giá trị. Điều này có nghĩa là cả keys() và values() sẽ trả về cùng một Iterator, và entries() sẽ trả về giá trị hai lần. Sẽ hợp lý nhất thì ta chỉ sử dụng values() với Set, vì hai phương thức còn lại tồn tại để đảm bảo tính nhất quán và khả năng tương thích chéo với Map.

const set = new Set([1, 2, 3])
// Lấy các giá trị của Set
set.values()

Kết quả:

Output

SetIterator {1, 2, 3}

Lặp với Set

Giống như Map, Set có phương thức forEach() tích hợp sẵn. Vì Set không có khóa, nên tham số đầu tiên và thứ hai của lệnh forEach() sẽ trả về cùng một giá trị, vì vậy không có trường hợp sử dụng nào cho nó ngoài khả năng tương thích với Map. Các tham số của forEach() là (value, key, set).

Cả hai forEach() và for...of đều có thể được sử dụng trên Set. Đầu tiên, hãy xem xét forEach():

const set = new Set(['hi', 'hello', 'good day'])

// Lặp một Set với forEach
set.forEach((value) => console.log(value))

Còn đây là for...of:

// Lặp Set với for...of
for (const value of set) {  
    console.log(value);
}

Cả hai cách thức trên sẽ mang lại những kết quả sau:

Output

hi

hello

good day

Các thuộc tính và phương thức của Set

Bảng sau đây hiển thị danh sách các thuộc tính và phương thức của Set:

Thuộc tính / Phương thức Mô tả Trả về
add(value) Thêm một mục mới vào Set Đối tượng Set
delete(value) Xóa mục đã chỉ định khỏi Set Boolean
has() Kiểm tra sự hiện diện của một mục trong Set Boolean
clear() Xóa tất cả các mục khỏi Set N/A
keys() Trả về tất cả các giá trị trong Set (giống như values()) Đối tượng SetIterator
values() Trả về tất cả các giá trị trong Set (giống như keys()) Đối tượng SetIterator
entries() Trả về tất cả các giá trị trong Set dạng [value, value] Đối tượng SetIterator
forEach() Lặp thông qua Set theo thứ tự chèn N/A
size Trả về số lượng mục của Set Số

Khi nào thì sử dụng Set

Set là một bổ sung hữu ích cho bộ công cụ JavaScript của bạn, đặc biệt để làm việc với các giá trị trùng lặp trong dữ liệu.

Trong một dòng, chúng ta có thể tạo một Mảng mới mà không có các giá trị trùng lặp từ Mảng có các giá trị trùng lặp.

const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]

Kết quả:

Output

(3) [1, 2, 3]

Set có thể được sử dụng để tìm kết hợp, giao điểm và sự khác biệt giữa hai tập hợp dữ liệu. Tuy nhiên, Mảng có lợi thế đáng kể so với Set về thao tác bổ sung dữ liệu thông qua các phương thức sort()map()filter() và reduce(), cũng như khả năng tương thích trực tiếp với các phương thức của JSON.

Kết luận

Trong bài viết này, bạn đã biết rằng Map là tập hợp các cặp khóa / giá trị có thứ tự và Set là tập hợp các giá trị duy nhất. Cả hai cấu trúc dữ liệu này đều bổ sung các khả năng bổ sung cho JavaScript và đơn giản hóa các tác vụ phổ biến như tìm độ dài của bộ sưu tập cặp khóa / giá trị và xóa các mục trùng lặp khỏi tập dữ liệu, tương ứng. Mặt khác, Đối tượng và Mảng thường được sử dụng để lưu trữ và thao tác dữ liệu trong JavaScript và có khả năng tương thích trực tiếp với JSON, điều này tiếp tục biến chúng trở thành cấu trúc dữ liệu quan trọng nhất, đặc biệt là để làm việc với REST API. Map và Set chủ yếu hữu ích khi hỗ trợ cấu trúc dữ liệu cho Đối tượng và Mảng.

Nếu bạn muốn tìm hiểu thêm về JavaScript, hãy xem trang chủ của loạt Cách viết mã trong JavaScript hoặc duyệt qua loạt bài Cách viết mã trong Node.js của chúng tôi để biết các bài viết về phát triển back-end.

» Tiếp: Các trường hợp không nên sử dụng hàm mũi tên
« Trước: Tìm hiểu về hàm hủy (destructuring), tham số rest và cú pháp spread trong JavaScript
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!