JavaScript Nâng Cao - Kỳ 22
Có một câu nói vui là: Trên đời chỉ có thứ nhiều người chửi và thứ không ai thèm dùng.
Javascript là một ví dụ điển hình, nó có một số điểm thú vị nhưng cũng khiến chúng ta phải đau đầu. Lý thuyết thì dễ hiểu, nhưng khi thực hành là cả một vấn đề. Vậy nên, mình sẽ cùng các bạn đi sâu vào từng ví dụ cụ thể và phân tích, mổ xẻ nó để hiểu hơn về Javascript nhé
Series này có thể sẽ khá dài mình không biết sẽ có bao nhiêu Kỳ tuy nhiên để tiện cho các bạn nào không đọc các bài trước đó của mình về JS thì trong loạt bài này mình sẽ giải thích lại toàn bộ. Các lý thuyết trong loạt bài này mình cũng có thể sẽ giải thích lại nhiều lần (tùy hứng) để các bạn có thể nắm rõ nó hơn nhé.
Ok vào bài thôi nào... GÉT GÔ 🚀
Nếu có bất kỳ câu hỏi nào đừng ngại hãy bình luận dưới phần
comment
nhé. Hoặc chỉ cần để lại mộtcomment chào mình
là đã giúp mình có thêm động lực hoàn thành series này. Cảm ơn các bạn rất nhiều. 🤗
1. Truy cập thuộc tính trong object
Output của đoạn code bên dưới là gì:
const colorConfig = {
red: true,
blue: false,
green: true,
black: true,
yellow: false,
}
const colors = ["pink", "red", "blue"]
console.log(colorConfig.colors[1])
- A:
true
- B:
false
- C:
undefined
- D:
TypeError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
1.1. Cách truy cập thuộc tính trong JavaScript
Trong JavaScript, chúng ta có hai cách chính để truy cập thuộc tính của một object:
- Sử dụng dấu chấm (dot notation):
object.property
- Sử dụng dấu ngoặc vuông (bracket notation):
object['property']
Trong ví dụ trên, chúng ta đang sử dụng cách thứ nhất: colorConfig.colors
.
1.2. Phân tích đoạn code
Khi JavaScript thực thi dòng console.log(colorConfig.colors[1])
, nó sẽ làm theo các bước sau:
- Tìm kiếm thuộc tính
colors
trong objectcolorConfig
. - Vì
colorConfig
không có thuộc tínhcolors
, kết quả củacolorConfig.colors
sẽ làundefined
. - Tiếp theo, JavaScript cố gắng truy cập phần tử có index 1 của
undefined
bằng cách sử dụng[1]
.
Tuy nhiên, chúng ta không thể truy cập thuộc tính của undefined
. Đây là lý do tại sao JavaScript ném ra một TypeError
.
1.3. Cách truy cập chính xác
Nếu chúng ta muốn truy cập phần tử thứ hai của mảng colors
, chúng ta có thể làm như sau:
console.log(colors[1]); // Kết quả: "red"
Hoặc nếu chúng ta muốn kiểm tra giá trị của thuộc tính red
trong colorConfig
:
console.log(colorConfig.red); // Kết quả: true
hoặc
console.log(colorConfig['red']); // Kết quả: true
1.4. Lưu ý quan trọng
Khi sử dụng dấu chấm để truy cập thuộc tính, JavaScript sẽ tìm kiếm một thuộc tính có tên chính xác như tên được chỉ định. Trong trường hợp này, nó tìm kiếm thuộc tính có tên là "colors" trong object colorConfig
.
Khi sử dụng dấu ngoặc vuông, JavaScript sẽ đánh giá biểu thức bên trong ngoặc vuông trước. Điều này cho phép chúng ta sử dụng các biến hoặc biểu thức phức tạp để truy cập thuộc tính.
Ví dụ:
const propertyName = 'red';
console.log(colorConfig[propertyName]); // Kết quả: true
1.5. Tóm lại
Hiểu rõ cách truy cập thuộc tính trong JavaScript là rất quan trọng để tránh các lỗi không mong muốn như trong ví dụ trên. Luôn đảm bảo rằng bạn đang truy cập đúng thuộc tính và sử dụng đúng cú pháp để tránh các TypeError
không cần thiết.
2. So sánh emoji
Output của đoạn code bên dưới là gì:
console.log('❤️' === '❤️')
- A:
true
- B:
false
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
2.1. Emoji là gì?
Trước khi đi vào phân tích, chúng ta cần hiểu rõ emoji là gì. Emoji không phải là một khái niệm đặc biệt trong JavaScript hay bất kỳ ngôn ngữ lập trình nào. Thực chất, emoji chỉ là các ký tự Unicode đặc biệt.
2.2. Unicode và emoji
Unicode là một chuẩn mã hóa ký tự, được thiết kế để biểu diễn và xử lý văn bản của hầu hết các hệ thống văn bản trên thế giới. Emoji cũng được biểu diễn bằng các mã Unicode.
Ví dụ, emoji trái tim ❤️ có mã Unicode là "U+2764 U+FE0F".
2.3. So sánh chuỗi trong JavaScript
Trong JavaScript, khi chúng ta sử dụng toán tử ===
để so sánh hai chuỗi, JavaScript sẽ so sánh từng ký tự trong chuỗi một cách tuần tự.
Trong trường hợp này, cả hai chuỗi đều chứa cùng một emoji trái tim. Do đó, khi JavaScript so sánh chúng, nó sẽ thấy rằng chúng hoàn toàn giống nhau.
2.4. Kết quả và giải thích
Kết quả của phép so sánh '❤️' === '❤️'
là true
vì:
- Cả hai chuỗi đều chứa cùng một emoji.
- Emoji này được biểu diễn bằng cùng một chuỗi mã Unicode.
- JavaScript so sánh các chuỗi này ký tự theo ký tự và thấy chúng hoàn toàn giống nhau.
2.5. Lưu ý quan trọng
Mặc dù trong trường hợp này kết quả khá đơn giản và dễ hiểu, nhưng khi làm việc với emoji và các ký tự Unicode khác, có một số điểm cần lưu ý:
-
Một số emoji có thể được biểu diễn bằng nhiều cách khác nhau. Ví dụ, một emoji có thể được tạo thành từ nhiều ký tự Unicode kết hợp lại.
-
Hiển thị của emoji có thể khác nhau trên các nền tảng khác nhau, mặc dù mã Unicode của chúng giống nhau.
-
Khi xử lý văn bản có chứa emoji, cần cẩn thận với các thao tác như cắt chuỗi hoặc đếm độ dài chuỗi, vì một emoji có thể chiếm nhiều hơn một đơn vị trong chuỗi.
2.6. Tóm lại
Hiểu biết về cách JavaScript xử lý emoji và các ký tự Unicode khác là rất quan trọng, đặc biệt trong thời đại mà emoji ngày càng phổ biến trong giao tiếp trực tuyến. Điều này giúp chúng ta tránh được các lỗi không mong muốn khi xử lý văn bản có chứa emoji trong các ứng dụng của mình.
3. Các phương thức xử lý mảng
Phép toán nào sau đây làm thay đổi mảng gốc?
const emojis = ['✨', '🥑', '😍']
emojis.map(x => x + '✨')
emojis.filter(x => x !== '🥑')
emojis.find(x => x !== '🥑')
emojis.reduce((acc, cur) => acc + '✨')
emojis.slice(1, 2, '✨')
emojis.splice(1, 2, '✨')
- A:
All of them
- B:
map
reduce
slice
splice
- C:
map
slice
splice
- D:
splice
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
3.1. Phân tích từng phương thức
Để hiểu tại sao chỉ có splice
làm thay đổi mảng gốc, chúng ta cần xem xét cách hoạt động của từng phương thức:
-
map():
- Tạo một mảng mới với kết quả của việc gọi một hàm cho mọi phần tử mảng.
- Không thay đổi mảng gố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 do hàm cung cấp.
- Không thay đổi mảng gốc.
-
find():
- Trả về giá trị của phần tử đầu tiên trong mảng thỏa mãn hàm kiểm tra được cung cấp.
- Không thay đổi mảng gốc.
-
reduce():
- Thực thi một hàm reducer cho mỗi phần tử của mảng, kết quả là một giá trị duy nhất.
- Không thay đổi mảng gốc.
-
slice():
- Trả về một bản sao nông của một phần của mảng.
- Không thay đổi mảng gốc.
-
splice():
- Thay đổi nội dung của một mảng bằng cách xóa hoặc thay thế các phần tử hiện có và/hoặc thêm các phần tử mới.
- Thay đổi mảng gốc.
3.2. Tại sao splice() thay đổi mảng gốc?
splice()
là phương thức duy nhất trong danh sách trên có khả năng thay đổi mảng gốc. Nó có thể thêm, xóa hoặc thay thế các phần tử trong mảng.
Trong ví dụ emojis.splice(1, 2, '✨')
:
- Bắt đầu từ vị trí index 1
- Xóa 2 phần tử
- Thêm phần tử '✨' vào vị trí đó
Sau khi thực hiện phép toán này, mảng emojis
sẽ trở thành ['✨', '✨']
.
3.3. Immutability và các phương thức xử lý mảng
Các phương thức như map()
, filter()
, reduce()
được thiết kế để không thay đổi mảng gốc, tuân theo nguyên tắc immutability (bất biến). Điều này có một số lợi ích:
- Dễ dàng theo dõi các thay đổi trong ứng dụng.
- Tránh các side effects không mong muốn.
- Hỗ trợ tốt cho việc tối ưu hóa hiệu suất trong các framework hiện đại.
3.4. Lưu ý khi sử dụng splice()
Mặc dù splice()
rất mạnh mẽ và linh hoạt, việc sử dụng nó cần phải cẩn thận:
- Nó thay đổi mảng gốc, có thể gây ra các side effects không mong muốn.
- Trong các ứng dụng lớn, việc sử dụng
splice()
có thể gây khó khăn trong việc theo dõi trạng thái. - Nếu bạn muốn giữ nguyên mảng gốc, hãy cân nhắc sử dụng các phương thức không làm thay đổi mảng như
slice()
kết hợp với spread operator.
3.5. Ví dụ minh họa
Để hiểu rõ hơn về cách hoạt động của các phương thức này, hãy xem xét một vài ví dụ:
const emojis = ['✨', '🥑', '😍'];
// map()
const mappedEmojis = emojis.map(x => x + '✨');
console.log(mappedEmojis); // ['✨✨', '🥑✨', '😍✨']
console.log(emojis); // ['✨', '🥑', '😍'] - mảng gốc không thay đổi
// filter()
const filteredEmojis = emojis.filter(x => x !== '🥑');
console.log(filteredEmojis); // ['✨', '😍']
console.log(emojis); // ['✨', '🥑', '😍'] - mảng gốc không thay đổi
// splice()
emojis.splice(1, 2, '✨');
console.log(emojis); // ['✨', '✨'] - mảng gốc đã thay đổi
3.6. Tóm lại
Hiểu rõ về cách hoạt động của các phương thức xử lý mảng trong JavaScript là rất quan trọng. Điều này giúp chúng ta:
- Viết code hiệu quả và dễ bảo trì hơn.
- Tránh được các lỗi không mong muốn liên quan đến việc thay đổi dữ liệu.
- Áp dụng các nguyên tắc lập trình hiện đại như immutability khi cần thiết.
Khi làm việc với mảng, hãy luôn cân nhắc xem bạn có thực sự cần thay đổi mảng gốc hay không. Trong nhiều trường hợp, việc tạo ra một bản sao mới của mảng (sử dụng các phương thức như map()
, filter()
, slice()
) sẽ an toàn và linh hoạt hơn.
4. Tương tác giữa kiểu dữ liệu cơ bản và object
Output của đoạn code bên dưới là gì:
const food = ['🍕', '🍫', '🥑', '🍔']
const info = { favoriteFood: food[0] }
info.favoriteFood = '🍝'
console.log(food)
- A:
['🍕', '🍫', '🥑', '🍔']
- B:
['🍝', '🍫', '🥑', '🍔']
- C:
['🍝', '🍕', '🍫', '🥑', '🍔']
- D:
ReferenceError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
4.1. Kiểu dữ liệu trong JavaScript
Trong JavaScript, có hai loại kiểu dữ liệu chính:
- Kiểu dữ liệu nguyên thủy (Primitive types): Number, String, Boolean, Null, Undefined, Symbol
- Kiểu dữ liệu tham chiếu (Reference types): Object, Array, Function
4.2. Cách JavaScript xử lý các kiểu dữ liệu
- Kiểu dữ liệu nguyên thủy được lưu trữ và truy cập trực tiếp theo giá trị.
- Kiểu dữ liệu tham chiếu được lưu trữ và truy cập gián tiếp thông qua một tham chiếu (reference).
4.3. Phân tích đoạn code
Hãy đi qua từng dòng code:
-
const food = ['🍕', '🍫', '🥑', '🍔']
- Tạo một mảng
food
chứa các emoji.
- Tạo một mảng
-
const info = { favoriteFood: food }
- Tạo một object
info
với thuộc tínhfavoriteFood
có giá trị là phần tử đầu tiên của mảngfood
(tức là '🍕'). - Lưu ý rằng giá trị được gán cho
favoriteFood
là một chuỗi (kiểu dữ liệu nguyên thủy), không phải là một tham chiếu đến phần tử trong mảngfood
.
- Tạo một object
-
info.favoriteFood = '🍝'
- Thay đổi giá trị của thuộc tính
favoriteFood
trong objectinfo
thành '🍝'. - Điều này không ảnh hưởng đến mảng
food
vìfavoriteFood
chỉ chứa một bản sao của giá trị từfood
, không phải một tham chiếu đến nó.
- Thay đổi giá trị của thuộc tính
-
console.log(food)
- In ra mảng
food
. - Mảng
food
không bị thay đổi bởi các thao tác trước đó, nên vẫn giữ nguyên giá trị ban đầu.
- In ra mảng
4.4. Tại sao mảng food không thay đổi?
Mảng food
không thay đổi vì:
- Khi gán
food
choinfo.favoriteFood
, chúng ta chỉ đang sao chép giá trị của phần tử đầu tiên trong mảng (một chuỗi), không phải tham chiếu đến phần tử đó. - Việc thay đổi
info.favoriteFood
sau đó chỉ ảnh hưởng đến objectinfo
, không liên quan gì đến mảngfood
.
4.5. Lưu ý quan trọng
- Khi làm việc với kiểu dữ liệu nguyên thủy (như chuỗi trong trường hợp này), JavaScript luôn tạo một bản sao của giá trị.
- Nếu
food
là một mảng các object và chúng ta thay đổi một thuộc tính của object đó, thì mảngfood
sẽ bị ảnh hưởng vì object là kiểu dữ liệu tham chiếu.
4.6. Ví dụ minh họa
Để hiểu rõ hơn về sự khác biệt giữa kiểu dữ liệu nguyên thủy và tham chiếu, hãy xem xét ví dụ sau:
// Với kiểu dữ liệu nguyên thủy
let a = 5;
let b = a;
b = 10;
console.log(a); // 5
console.log(b); // 10
// Với kiểu dữ liệu tham chiếu
let obj1 = { value: 5 };
let obj2 = obj1;
obj2.value = 10;
console.log(obj1.value); // 10
console.log(obj2.value); // 10
4.7. Tóm lại
Hiểu rõ về cách JavaScript xử lý các kiểu dữ liệu khác nhau là rất quan trọng để tránh các lỗi không mong muốn trong quá trình phát triển. Khi làm việc với các kiểu dữ liệu nguyên thủy, chúng ta có thể yên tâm rằng các thao tác trên một biến sẽ không ảnh hưởng đến các biến khác. Tuy nhiên, với kiểu dữ liệu tham chiếu, cần phải cẩn thận hơn vì các thay đổi có thể ảnh hưởng đến nhiều nơi trong code.
5. Phương thức JSON.parse()
Phép toán này dùng để làm gì?
JSON.parse()
- A: Parse JSON thành một giá trị JavaScript
- B: Parse một JavaScript object thành JSON
- C: Parse giá trị JavaScript bất kì thành JSON
- D: Parse JSON thành một JavaScript object
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
5.1. JSON là gì?
JSON (JavaScript Object Notation) là một định dạng dữ liệu nhẹ, dễ đọc và dễ viết đối với con người, đồng thời cũng dễ dàng để máy phân tích và tạo ra. JSON thường được sử dụng để truyền dữ liệu giữa server và web application.
5.2. Chức năng của JSON.parse()
JSON.parse()
là một phương thức trong JavaScript được sử dụng để chuyển đổi một chuỗi JSON thành một giá trị hoặc object JavaScript. Đây là cách chúng ta "giải mã" dữ liệu JSON nhận được từ server hoặc từ một nguồn khác.
5.3. Cách sử dụng JSON.parse()
Cú pháp cơ bản:
JSON.parse(text[, reviver])
text
: Chuỗi JSON cần parse.reviver
(tùy chọn): Hàm được gọi cho mỗi cặp key/value, có thể được sử dụng để chuyển đổi giá trị.
5.4. Ví dụ minh họa
Hãy xem xét một vài ví dụ để hiểu rõ hơn về cách JSON.parse()
hoạt động:
- Parse một số:
const jsonNumber = JSON.parse('4');
console.log(jsonNumber); // 4 (number)
- Parse một mảng:
const jsonArray = JSON.parse('[1, 2, 3]');
console.log(jsonArray); // [1, 2, 3] (array)
- Parse một object:
const jsonObject = JSON.parse('{"name":"John", "age":30}');
console.log(jsonObject); // {name: "John", age: 30} (object)
- Sử dụng reviver function:
const jsonText = '{"birthDate":"1990-01-01T00:00:00.000Z"}';
const person = JSON.parse(jsonText, (key, value) => {
if (key === 'birthDate') return new Date(value);
return value;
});
console.log(person.birthDate); // Date object
5.5. JSON.parse() vs JSON.stringify()
Trong khi JSON.parse()
chuyển đổi chuỗi JSON thành giá trị JavaScript, JSON.stringify()
làm điều ngược lại: chuyển đổi giá trị JavaScript thành chuỗi JSON.
const obj = { name: "John", age: 30 };
const jsonString = JSON.stringify(obj);
console.log(jsonString); // '{"name":"John","age":30}'
const parsedObj = JSON.parse(jsonString);
console.log(parsedObj); // { name: "John", age: 30 }
5.6. Lưu ý quan trọng
JSON.parse()
sẽ ném ra mộtSyntaxError
nếu chuỗi JSON không hợp lệ.- JSON không hỗ trợ tất cả các kiểu dữ liệu JavaScript. Ví dụ,
undefined
,Function
, vàSymbol
sẽ bị bỏ qua khi chuyển đổi sang JSON. - Dates được chuyển đổi thành chuỗi khi stringify và không tự động chuyển đổi lại thành Date objects khi parse (trừ khi bạn sử dụng reviver function).
5.7. Tóm lại
JSON.parse()
là một công cụ quan trọng trong việc xử lý dữ liệu trong JavaScript, đặc biệt là khi làm việc với API hoặc lưu trữ dữ liệu dưới dạng chuỗi. Hiểu rõ cách sử dụng JSON.parse()
sẽ giúp bạn xử lý dữ liệu hiệu quả hơn trong các ứng dụng web của mình.
Kết luận
Qua bài viết này, chúng ta đã đi sâu vào một số khía cạnh quan trọng của JavaScript:
- Cách truy cập thuộc tính trong object và sự khác biệt giữa dot notation và bracket notation.
- Cách JavaScript xử lý emoji và các ký tự Unicode.
- Các phương thức xử lý mảng và tác động của chúng đến mảng gốc.
- Sự khác biệt giữa kiểu dữ liệu nguyên thủy và tham chiếu trong JavaScript.
- Chức năng và cách sử dụng của phương thức
JSON.parse()
.
Những kiến thức này không chỉ giúp bạn hiểu rõ hơn về cách JavaScript hoạt động, mà còn giúp bạn tránh được nhiều lỗi phổ biến trong quá trình phát triển. Hãy nhớ rằng, việc hiểu sâu về ngôn ngữ lập trình bạn đang sử dụng là chìa khóa để trở thành một lập trình viên giỏi.
Nếu có bất kỳ câu hỏi nào đừng ngại hãy bình luận dưới phần
comment
nhé. Hoặc chỉ cần để lại mộtcomment chào mình
là đã giúp mình có thêm động lực hoàn thành series này. Cảm ơn các bạn rất nhiều. 🤗
All rights reserved