JavaScript sử dụng dấu ba chấm (...
) cho cả toán tử còn lại và trải rộng. Nhưng hai toán tử này không giống nhau.
Sự khác biệt chính giữa phần còn lại và trải rộng là toán tử phần còn lại đặt phần còn lại của một số giá trị cụ thể do người dùng cung cấp vào một mảng JavaScript. Nhưng cú pháp trải rộng mở rộng các lần lặp thành các phần tử riêng lẻ.
Chẳng hạn, hãy xem đoạn mã này sử dụng phần còn lại để đặt một số giá trị vào một mảng:
// Use rest to enclose the rest of specific user-supplied values into an array:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
// Invoke myBio function while passing five arguments to its parameters:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, chúng tôi đã sử dụng ...otherInfo
tham số còn lại để đặt "CodeSweetly"
, "Web Developer"
và "Male"
thành một mảng.
Bây giờ, hãy xem xét ví dụ về toán tử trải phổ này:
// Define a function with three parameters:
function myBio(firstName, lastName, company) {
return `${firstName} ${lastName} runs ${company}`;
}
// Use spread to expand an array’s items into individual arguments:
myBio(...["Oluwatobi", "Sofela", "CodeSweetly"]);
// The invocation above will return:
“Oluwatobi Sofela runs CodeSweetly”
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, chúng tôi đã sử dụng toán tử trải rộng (...
) lây lan ["Oluwatobi", "Sofela", "CodeSweetly"]
nội dung của trên myBio()
thông số của.
Đừng lo lắng nếu bạn chưa hiểu phần còn lại hoặc các toán tử trải rộng. Bài viết này đã có bạn bảo hiểm!
Trong các phần tiếp theo, chúng ta sẽ thảo luận về cách hoạt động của phần còn lại và trải rộng trong JavaScript.
Vì vậy, không cần phải bận tâm thêm nữa, hãy bắt đầu với toán tử còn lại.
Chính xác thì toán tử còn lại là gì?
Các nhà điều hành phần còn lại được sử dụng để đặt phần còn lại của một số giá trị cụ thể do người dùng cung cấp vào một mảng JavaScript.
Vì vậy, ví dụ, đây là cú pháp còn lại:
...yourValues
Dấu ba chấm (...
) trong đoạn mã trên tượng trưng cho toán tử còn lại.
Văn bản sau toán tử còn lại tham chiếu các giá trị bạn muốn đặt bên trong một mảng. Bạn chỉ có thể sử dụng nó trước tham số cuối cùng trong định nghĩa hàm.
Để hiểu rõ hơn về cú pháp, hãy xem phần còn lại hoạt động như thế nào với các hàm JavaScript.
Toán tử còn lại hoạt động như thế nào trong một chức năng?
Trong các hàm JavaScript, phần còn lại được sử dụng làm tiền tố cho tham số cuối cùng của hàm.
Đây là một ví dụ:
// Define a function with two regular parameters and one rest parameter:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
Toán tử còn lại (...
) hướng dẫn máy tính thêm bất cứ thứ gì otherInfo
(đối số) do người dùng cung cấp thành một mảng. Sau đó, gán mảng đó cho otherInfo
tham số.
Như vậy, chúng tôi gọi ...otherInfo
một tham số nghỉ ngơi.
Ghi chú: Đối số là các giá trị tùy chọn mà bạn có thể chuyển đến tham số của hàm thông qua bộ gọi.
Đây là một ví dụ khác:
// Define a function with two regular parameters and one rest parameter:
function myBio(firstName, lastName, ...otherInfo) {
return otherInfo;
}
// Invoke myBio function while passing five arguments to its parameters:
myBio("Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male");
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, lưu ý rằng myBio
lời gọi của nó đã truyền năm đối số cho hàm.
Nói cách khác, "Oluwatobi"
và "Sofela"
được giao cho firstName
và lastName
thông số.
Đồng thời, toán tử còn lại đã thêm các đối số còn lại ( "CodeSweetly"
, "Web Developer"
và "Male"
) thành một mảng và gán mảng đó cho otherInfo
tham số.
Vì vậy, myBio()
chức năng trả lại chính xác ["CodeSweetly", "Web Developer", "Male"]
như nội dung của otherInfo
thông số nghỉ ngơi.
Hãy coi chừng! Bạn không thể dùng nó “use strict”
Bên trong một hàm chứa tham số nghỉ
Hãy nhớ rằng bạn không thể sử dụng “use strict”
chỉ thị bên trong bất kỳ hàm nào chứa tham số nghỉ, tham số mặc định hoặc tham số phá hủy. Nếu không, máy tính sẽ báo lỗi cú pháp.
Chẳng hạn, hãy xem xét ví dụ dưới đây:
// Define a function with one rest parameter:
function printMyName(...value) {
"use strict";
return value;
}
// The definition above will return:
"Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list"
Dùng thử trên CodeSandbox
printMyName()
đã trả về lỗi cú pháp vì chúng tôi đã sử dụng “use strict”
chỉ thị bên trong một chức năng với một tham số còn lại.
Nhưng giả sử bạn cần chức năng của mình ở chế độ nghiêm ngặt đồng thời sử dụng tham số còn lại. Trong trường hợp như vậy, bạn có thể viết “use strict”
chỉ thị bên ngoài chức năng.
Đây là một ví dụ:
// Define a “use strict” directive outside your function:
"use strict";
// Define a function with one rest parameter:
function printMyName(...value) {
return value;
}
// Invoke the printMyName function while passing two arguments to its parameters:
printMyName("Oluwatobi", "Sofela");
// The invocation above will return:
["Oluwatobi", "Sofela"]
Dùng thử trên CodeSandbox
Ghi chú: Chỉ đặt các “use strict”
chỉ thị bên ngoài chức năng của bạn nếu toàn bộ tập lệnh hoặc phạm vi kèm theo ở chế độ nghiêm ngặt là được.
Vì vậy, bây giờ chúng ta đã biết cách thức hoạt động của phần còn lại trong một hàm, chúng ta có thể nói về cách thức hoạt động của nó trong một phép gán phá hủy.
Cách toán tử còn lại hoạt động trong nhiệm vụ hủy cấu trúc
Toán tử còn lại thường được sử dụng làm tiền tố của biến cuối cùng của phép gán hủy.
Đây là một ví dụ:
// Define a destructuring array with two regular variables and one rest variable:
const [firstName, lastName, ...otherInfo] = [
"Oluwatobi", "Sofela", "CodeSweetly", "Web Developer", "Male"
];
// Invoke the otherInfo variable:
console.log(otherInfo);
// The invocation above will return:
["CodeSweetly", "Web Developer", "Male"]
Hãy dùng thử trên StackBlitz
Toán tử còn lại (...
) hướng dẫn máy tính thêm phần còn lại của các giá trị do người dùng cung cấp vào một mảng. Sau đó, nó gán mảng đó cho otherInfo
Biến đổi.
Như vậy, bạn có thể gọi ...otherInfo
một biến nghỉ ngơi.
Đây là một ví dụ khác:
// Define a destructuring object with two regular variables and one rest variable:
const { firstName, lastName, ...otherInfo } = {
firstName: "Oluwatobi",
lastName: "Sofela",
companyName: "CodeSweetly",
profession: "Web Developer",
gender: "Male"
}
// Invoke the otherInfo variable:
console.log(otherInfo);
// The invocation above will return:
{companyName: "CodeSweetly", profession: "Web Developer", gender: "Male"}
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, lưu ý rằng toán tử còn lại đã gán một đối tượng thuộc tính — không phải một mảng — cho otherInfo
Biến đổi.
Nói cách khác, bất cứ khi nào bạn sử dụng phần còn lại trong một đối tượng phá hủy, toán tử phần còn lại sẽ tạo ra một đối tượng thuộc tính.
Tuy nhiên, nếu bạn sử dụng phần còn lại trong một mảng hoặc hàm phá hủy, thì toán tử sẽ mang lại một mảng bằng chữ.
Trước khi chúng tôi kết thúc cuộc thảo luận của mình về phần còn lại, bạn nên biết một số khác biệt giữa các đối số JavaScript và tham số phần còn lại. Vì vậy, hãy nói về điều đó dưới đây.
Đối số so với Tham số còn lại: Sự khác biệt là gì?
Dưới đây là một số khác biệt giữa đối số JavaScript và tham số còn lại:
Sự khác biệt 1: Các arguments
đối tượng là một đối tượng giống như mảng — không phải là một mảng thực!
Hãy nhớ rằng đối tượng đối số JavaScript không phải là một mảng thực. Thay vào đó, nó là một đối tượng dạng mảng không có các tính năng toàn diện của một mảng JavaScript thông thường.
Tuy nhiên, tham số còn lại là một đối tượng mảng thực. Như vậy, bạn có thể sử dụng tất cả các phương thức mảng trên đó.
Vì vậy, ví dụ, bạn có thể gọi sort()
, map()
, forEach()
hoặc pop()
phương pháp trên một tham số nghỉ ngơi. Nhưng bạn không thể làm điều tương tự trên đối tượng đối số.
Sự khác biệt 2: Bạn không thể sử dụng arguments
đối tượng trong một chức năng mũi tên
Các arguments
đối tượng không khả dụng trong chức năng mũi tên, vì vậy bạn không thể sử dụng nó ở đó. Nhưng bạn có thể sử dụng tham số còn lại trong tất cả các chức năng — bao gồm cả chức năng mũi tên.
Sự khác biệt 3: Hãy nghỉ ngơi theo sở thích của bạn
Tốt nhất là sử dụng các tham số còn lại thay vì arguments
đối tượng — đặc biệt là khi viết mã tương thích với ES6.
Bây giờ chúng ta đã biết nghỉ ngơi hoạt động như thế nào, hãy thảo luận về spread
toán tử để chúng ta có thể thấy sự khác biệt.
Toán tử lây lan là gì và hoạt động như thế nào spread
làm việc trong JavaScript?
Các toán tử lây lan (...
) giúp bạn mở rộng các lần lặp thành các phần tử riêng lẻ.
Cú pháp trải rộng hoạt động trong các ký tự mảng, các lệnh gọi hàm và các đối tượng thuộc tính được khởi tạo để trải các giá trị của các đối tượng có thể lặp lại thành các mục riêng biệt. Vì vậy, nó làm điều ngược lại với toán tử còn lại.
Ghi chú: Toán tử trải rộng chỉ có hiệu quả khi được sử dụng trong các ký tự mảng, lệnh gọi hàm hoặc đối tượng thuộc tính được khởi tạo.
Vì vậy, chính xác điều này có nghĩa là gì? Hãy xem với một số ví dụ.
Ví dụ về trải rộng 1: Cách thức hoạt động của trải rộng trong một mảng chữ
const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
[ "Oluwatobi", "Sofela", "is", "my", "name." ]
Hãy dùng thử trên StackBlitz
Đoạn mã trên đã sử dụng trải rộng (...
) để sao chép myName
mảng vào aboutMe
.
Ghi chú:
- thay đổi để
myName
sẽ không phản ánh trongaboutMe
bởi vì tất cả các giá trị bên trongmyName
là nguyên thủy. Do đó, toán tử trải rộng chỉ cần sao chép và dánmyName
nội dung của vàoaboutMe
mà không tạo bất kỳ tham chiếu nào trở lại mảng ban đầu. - Như @nombrekeff đã đề cập trong một nhận xét ở đây, toán tử trải rộng chỉ thực hiện bản sao nông. Vì vậy, hãy nhớ rằng giả sử
myName
chứa bất kỳ giá trị không nguyên thủy nào, máy tính sẽ tạo một tham chiếu giữamyName
vàaboutMe
. Xem thông tin 3 để biết thêm về cách hoạt động của toán tử trải rộng với các giá trị nguyên thủy và không nguyên thủy. - Giả sử chúng ta không sử dụng cú pháp lây lan để nhân đôi
myName
nội dung của. Ví dụ, nếu chúng ta đã viếtconst aboutMe = ["Oluwatobi", myName, "name."]
. Trong trường hợp như vậy, máy tính sẽ gán một tham chiếu trở lạimyName
. Như vậy, bất kỳ thay đổi nào được thực hiện trong mảng ban đầu sẽ phản ánh trong mảng trùng lặp.
Ví dụ 2 về trải rộng: Cách sử dụng trải rộng để chuyển đổi một chuỗi thành các mục mảng riêng lẻ
const myName = "Oluwatobi Sofela";
console.log([...myName]);
// The invocation above will return:
[ "O", "l", "u", "w", "a", "t", "o", "b", "i", " ", "S", "o", "f", "e", "l", "a" ]
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, chúng tôi đã sử dụng cú pháp lây lan (...
) trong một đối tượng chữ mảng ([...]
) mở rộng myName
giá trị chuỗi của thành các mục riêng lẻ.
Như vậy, "Oluwatobi Sofela"
được mở rộng thành [ "O", "l", "u", "w", "a", "t", "o", "b", "i", " ", "S", "o", "f", "e", "l", "a" ]
.
Ví dụ 3 về trải rộng: Toán tử trải rộng hoạt động như thế nào trong một lệnh gọi hàm
const numbers = [1, 3, 5, 7];
function addNumbers(a, b, c, d) {
return a + b + c + d;
}
console.log(addNumbers(...numbers));
// The invocation above will return:
16
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, chúng tôi đã sử dụng cú pháp trải rộng để trải rộng numbers
nội dung của mảng trên addNumbers()
thông số của.
Giả sử numbers
mảng có nhiều hơn bốn mục. Trong trường hợp này, máy tính sẽ chỉ sử dụng bốn mục đầu tiên làm addNumbers()
lập luận và bỏ qua phần còn lại.
Đây là một ví dụ:
const numbers = [1, 3, 5, 7, 10, 200, 90, 59];
function addNumbers(a, b, c, d) {
return a + b + c + d;
}
console.log(addNumbers(...numbers));
// The invocation above will return:
16
Hãy dùng thử trên StackBlitz
Đây là một ví dụ khác:
const myName = "Oluwatobi Sofela";
function spellName(a, b, c) {
return a + b + c;
}
console.log(spellName(...myName)); // returns: "Olu"
console.log(spellName(...myName[3])); // returns: "wundefinedundefined"
console.log(spellName([...myName])); // returns: "O,l,u,w,a,t,o,b,i, ,S,o,f,e,l,aundefinedundefined"
console.log(spellName({...myName})); // returns: "[object Object]undefinedundefined"
Hãy dùng thử trên StackBlitz
Ví dụ về trải rộng 4: Cách thức hoạt động của trải rộng trong một đối tượng theo nghĩa đen
const myNames = ["Oluwatobi", "Sofela"];
const bio = { ...myNames, runs: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ 0: "Oluwatobi", 1: "Sofela", runs: "codesweetly.com" }
Hãy dùng thử trên StackBlitz
Trong đoạn mã trên, chúng tôi đã sử dụng trải rộng bên trong bio
đối tượng mở rộng myNames
các giá trị thành các thuộc tính riêng lẻ.
Những điều cần biết về toán tử lây lan
Hãy ghi nhớ ba thông tin cần thiết này bất cứ khi nào bạn chọn sử dụng toán tử chênh lệch.
Thông tin 1: Toán tử trải rộng không thể mở rộng giá trị của đối tượng bằng chữ
Vì đối tượng thuộc tính không phải là đối tượng có thể lặp lại nên bạn không thể sử dụng toán tử trải rộng để mở rộng giá trị của nó.
Tuy nhiên, bạn có thể sử dụng toán tử trải rộng để sao chép các thuộc tính từ đối tượng này sang đối tượng khác.
Đây là một ví dụ:
const myName = { firstName: "Oluwatobi", lastName: "Sofela" };
const bio = { ...myName, website: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };
Hãy dùng thử trên StackBlitz
Đoạn mã trên đã sử dụng toán tử trải rộng để sao chép myName
nội dung của vào bio
vật.
Ghi chú:
- Toán tử trải rộng chỉ có thể mở rộng giá trị của các đối tượng có thể lặp lại.
- Một đối tượng chỉ có thể lặp lại nếu nó (hoặc bất kỳ đối tượng nào trong chuỗi nguyên mẫu của nó) có một thuộc tính với khóa @@ iterator.
- Array, TypedArray, String, Map và Set đều là các kiểu có thể lặp lại tích hợp sẵn vì chúng có
@@iterator
tài sản theo mặc định. - Đối tượng thuộc tính không phải là kiểu dữ liệu có thể lặp lại vì nó không có
@@iterator
tài sản theo mặc định. - Bạn có thể làm cho một đối tượng thuộc tính có thể lặp lại bằng cách thêm
@@iterator
lên nó.
Thông tin 2: Toán tử trải rộng không sao chép các thuộc tính giống hệt nhau
Giả sử bạn đã sử dụng toán tử trải rộng để sao chép các thuộc tính từ đối tượng A sang đối tượng B. Và giả sử đối tượng B chứa các thuộc tính giống hệt với các thuộc tính trong đối tượng A. Trong trường hợp này, các phiên bản của B sẽ ghi đè lên các phiên bản bên trong A.
Đây là một ví dụ:
const myName = { firstName: "Tobi", lastName: "Sofela" };
const bio = { ...myName, firstName: "Oluwatobi", website: "codesweetly.com" };
console.log(bio);
// The invocation above will return:
{ firstName: "Oluwatobi", lastName: "Sofela", website: "codesweetly.com" };
Hãy dùng thử trên StackBlitz
Quan sát rằng toán tử trải rộng không sao chép myName
‘S firstName
tài sản vào bio
đối tượng vì bio
đã chứa một firstName
tài sản.
Thông tin 3: Cẩn thận với cách hoạt động của trải rộng khi được sử dụng trên các đối tượng chứa các phần tử không nguyên thủy!
Giả sử bạn đã sử dụng toán tử trải rộng trên một đối tượng (hoặc mảng) chỉ chứa các giá trị nguyên thủy. Máy tính sẽ không phải tạo bất kỳ tham chiếu nào giữa đối tượng gốc và đối tượng trùng lặp.
Chẳng hạn, hãy xem xét mã này bên dưới:
const myName = ["Sofela", "is", "my"];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
["Oluwatobi", "Sofela", "is", "my", "name."]
Hãy dùng thử trên StackBlitz
Quan sát rằng mọi mục trong myName
là một giá trị nguyên thủy. Do đó, khi chúng tôi sử dụng toán tử trải rộng để sao chép myName
vào trong aboutMe
máy tính không tạo bất kỳ tham chiếu nào giữa hai mảng.
Như vậy, bất kỳ thay đổi nào bạn thực hiện đối với myName
sẽ không phản ánh trong aboutMe
và ngược lại.
Ví dụ, hãy thêm nhiều nội dung hơn vào myName
:
myName.push("real");
Bây giờ, hãy kiểm tra trạng thái hiện tại của myName
và aboutMe
:
console.log(myName); // ["Sofela", "is", "my", "real"]
console.log(aboutMe); // ["Oluwatobi", "Sofela", "is", "my", "name."]
Hãy dùng thử trên StackBlitz
Thông báo rằng myName
nội dung cập nhật của không phản ánh trong aboutMe
– bởi vì sự lây lan không tạo ra tham chiếu nào giữa mảng ban đầu và mảng trùng lặp.
Chuyện gì xảy ra nếu myName
chứa các mục không nguyên thủy?
Giả sử myName
chứa không nguyên thủy. Trong trường hợp đó, trải rộng sẽ tạo ra một tham chiếu giữa bản gốc không nguyên thủy và bản sao.
Đây là một ví dụ:
const myName = [["Sofela", "is", "my"]];
const aboutMe = ["Oluwatobi", ...myName, "name."];
console.log(aboutMe);
// The invocation above will return:
[ "Oluwatobi", ["Sofela", "is", "my"], "name." ]
Hãy dùng thử trên StackBlitz
quan sát rằng myName
chứa một giá trị không nguyên thủy.
Do đó, sử dụng toán tử trải rộng để sao chép myName
nội dung của vào aboutMe
khiến máy tính tạo tham chiếu giữa hai mảng.
Như vậy, bất kỳ thay đổi nào bạn thực hiện đối với myName
bản sao của sẽ phản ánh trong aboutMe
phiên bản của nó và ngược lại.
Ví dụ, hãy thêm nhiều nội dung hơn vào myName
:
myName[0].push("real");
Bây giờ, hãy kiểm tra trạng thái hiện tại của myName
và aboutMe
:
console.log(myName); // [["Sofela", "is", "my", "real"]]
console.log(aboutMe); // ["Oluwatobi", ["Sofela", "is", "my", "real"], "name."]
Hãy dùng thử trên StackBlitz
Thông báo rằng myName
nội dung cập nhật của được phản ánh trong aboutMe
— bởi vì spread đã tạo tham chiếu giữa mảng ban đầu và mảng trùng lặp.
Đây là một ví dụ khác:
const myName = { firstName: "Oluwatobi", lastName: "Sofela" };
const bio = { ...myName };
myName.firstName = "Tobi";
console.log(myName); // { firstName: "Tobi", lastName: "Sofela" }
console.log(bio); // { firstName: "Oluwatobi", lastName: "Sofela" }
Hãy dùng thử trên StackBlitz
Trong đoạn trích trên, myName
cập nhật của không phản ánh trong bio
bởi vì chúng tôi đã sử dụng toán tử trải rộng trên một đối tượng chỉ chứa các giá trị nguyên thủy.
Ghi chú: Một nhà phát triển sẽ gọi myName
một đối tượng nông bởi vì nó chỉ chứa các mục nguyên thủy.
Đây là một ví dụ nữa:
const myName = {
fullName: { firstName: "Oluwatobi", lastName: "Sofela" }
};
const bio = { ...myName };
myName.fullName.firstName = "Tobi";
console.log(myName); // { fullName: { firstName: "Tobi", lastName: "Sofela" } }
console.log(bio); // { fullName: { firstName: "Tobi", lastName: "Sofela" } }
Hãy dùng thử trên StackBlitz
Trong đoạn trích trên, myName
cập nhật của được phản ánh trong bio
bởi vì chúng tôi đã sử dụng toán tử trải rộng trên một đối tượng chứa giá trị không nguyên thủy.
Ghi chú:
- Chúng tôi gọi
myName
một đối tượng sâu bởi vì nó chứa một mục không nguyên thủy. - Bạn làm bản sao nông khi bạn tạo các tham chiếu trong khi sao chép một đối tượng vào một đối tượng khác. Ví dụ,
...myName
tạo ra một bản sao nông củamyName
đối tượng vì bất kỳ thay đổi nào bạn thực hiện trong cái này sẽ phản ánh trong cái kia. - Bạn làm sao chép sâu khi bạn sao chép các đối tượng mà không tạo tham chiếu. Chẳng hạn, tôi có thể sao chép sâu
myName
vào trongbio
bằng cách làmconst bio = JSON.parse(JSON.stringify(myName))
. Làm như vậy, máy tính sẽ nhân bảnmyName
vào trongbio
không có tạo bất kỳ tài liệu tham khảo. - Bạn có thể ngắt tham chiếu giữa hai đối tượng bằng cách thay thế
fullName
đối tượng bên trongmyName
hoặcbio
với một đối tượng mới. Ví dụ, làmmyName.fullName = { firstName: "Tobi", lastName: "Sofela" }
sẽ ngắt kết nối con trỏ giữamyName
vàbio
.
gói nó lên
Bài viết này đã thảo luận về sự khác biệt giữa các toán tử còn lại và trải rộng. Chúng tôi cũng đã sử dụng các ví dụ để xem cách hoạt động của từng toán tử.
Cảm ơn vì đã đọc!