JavaScript Nâng Cao - Kỳ 19
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é.
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. Phương thức push() và giá trị trả về
let newList = [1, 2, 3].push(4)
console.log(newList.push(5))
- A:
[1, 2, 3, 4, 5]
- B:
[1, 2, 3, 5]
- C:
[1, 2, 3, 4]
- D:
Error
Đá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. Phương thức push() trong JavaScript
Trước hết, chúng ta cần hiểu rõ về phương thức push()
trong JavaScript. Phương thức này được sử dụng để thêm một hoặc nhiều phần tử vào cuối mảng. Tuy nhiên, điều quan trọng cần lưu ý là push()
không trả về mảng mới sau khi thêm phần tử, mà nó trả về độ dài mới của mảng.
1.2. Phân tích đoạn code
Hãy xem xét từng dòng code:
let newList = [1, 2, 3].push(4)
Ở đây, chúng ta đang gán giá trị trả về của [1, 2, 3].push(4)
cho biến newList
. Như đã nói, push()
trả về độ dài mới của mảng. Vậy newList
sẽ có giá trị là 4 (độ dài của mảng [1, 2, 3, 4]).
console.log(newList.push(5))
Ở dòng này, chúng ta đang cố gắng gọi phương thức push()
trên newList
. Tuy nhiên, newList
không phải là một mảng, mà là một số (4). Số không có phương thức push()
, vì vậy JavaScript sẽ throw ra một TypeError.
1.3. Kết luận
Đáp án đúng là D: Error
. Cụ thể hơn, chúng ta sẽ nhận được một TypeError với thông báo kiểu như "newList.push is not a function".
1.4. Bài học rút ra
Đây là một ví dụ điển hình về việc cần phải hiểu rõ giá trị trả về của các phương thức trong JavaScript. Nhiều lập trình viên mới thường nhầm tưởng rằng push()
trả về mảng mới, nhưng thực tế nó trả về độ dài mới của mảng.
Để tránh lỗi này, chúng ta có thể viết lại code như sau:
let arr = [1, 2, 3];
arr.push(4);
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]
Hoặc nếu muốn tạo một mảng mới:
let newList = [...[1, 2, 3], 4];
newList = [...newList, 5];
console.log(newList); // [1, 2, 3, 4, 5]
2. Prototype trong JavaScript
function giveLydiaPizza() {
return "Here is pizza!"
}
const giveLydiaChocolate = () => "Here's chocolate... now go hit the gym already."
console.log(giveLydiaPizza.prototype)
console.log(giveLydiaChocolate.prototype)
- A:
{ constructor: ...}
{ constructor: ...}
- B:
{}
{ constructor: ...}
- C:
{ constructor: ...}
{}
- D:
{ constructor: ...}
undefined
Đá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é ❓️
2.1. Prototype trong JavaScript
Trong JavaScript, mọi function đều có một thuộc tính prototype
mặc định. Tuy nhiên, điều này chỉ đúng với các function thông thường (regular functions), không áp dụng cho arrow functions.
2.2. Regular Function vs Arrow Function
-
Regular Function (
giveLydiaPizza
):- Được khai báo bằng từ khóa
function
- Có thuộc tính
prototype
prototype
là một object chứa thuộc tínhconstructor
và có thể được mở rộng
- Được khai báo bằng từ khóa
-
Arrow Function (
giveLydiaChocolate
):- Được khai báo bằng cú pháp
() =>
- Không có thuộc tính
prototype
- Được khai báo bằng cú pháp
2.3. Phân tích kết quả
-
console.log(giveLydiaPizza.prototype)
: Sẽ in ra một object có dạng{ constructor: f }
, trong đóf
chính là functiongiveLydiaPizza
. -
console.log(giveLydiaChocolate.prototype)
: Sẽ in raundefined
vì arrow function không có thuộc tínhprototype
.
2.4. Tại sao lại như vậy?
Sự khác biệt này xuất phát từ cách JavaScript xử lý các loại function khác nhau:
-
Regular functions được thiết kế để có thể được sử dụng như constructors (với từ khóa
new
), vì vậy chúng cần cóprototype
. -
Arrow functions không thể được sử dụng như constructors và không có
this
của riêng chúng, vì vậy chúng không cầnprototype
.
2.5. Ví dụ minh họa
function RegularFunc() {}
const ArrowFunc = () => {};
console.log(RegularFunc.prototype); // { constructor: f }
console.log(ArrowFunc.prototype); // undefined
// Có thể thêm methods vào prototype của regular function
RegularFunc.prototype.newMethod = function() {};
// Không thể làm điều tương tự với arrow function
// ArrowFunc.prototype.newMethod = function() {}; // Throws TypeError
2.6. Kết luận
Hiểu về prototype
và sự khác biệt giữa regular functions và arrow functions là rất quan trọng trong JavaScript. Nó không chỉ giúp bạn hiểu rõ hơn về cách ngôn ngữ hoạt động, mà còn giúp bạn tránh được những lỗi không đáng có khi làm việc với OOP trong JavaScript.
3. Object.entries() và Destructuring
const person = {
name: "Lydia",
age: 21
}
for (const [x, y] of Object.entries(person)) {
console.log(x, y)
}
- A:
name
Lydia
vàage
21
- B:
["name", "Lydia"]
và["age", 21]
- C:
["name", "age"]
vàundefined
- D:
Error
Đá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é ❓️
3.1. Object.entries()
Object.entries()
là một phương thức trong JavaScript được sử dụng để chuyển đổi một object thành một mảng các cặp [key, value]. Mỗi cặp [key, value] được biểu diễn dưới dạng một mảng con.
Ví dụ:
const obj = { a: 1, b: 2 };
console.log(Object.entries(obj)); // [['a', 1], ['b', 2]]
3.2. Destructuring trong vòng lặp for...of
Trong đoạn code của chúng ta, chúng ta sử dụng destructuring assignment trong vòng lặp for...of
. Cú pháp const [x, y]
cho phép chúng ta gán giá trị của mảng con (được trả về bởi Object.entries()
) vào hai biến x
và y
.
3.3. Phân tích từng bước
-
Object.entries(person)
trả về:[ ['name', 'Lydia'], ['age', 21] ]
-
Vòng lặp
for...of
sẽ lặp qua từng mảng con:- Trong lần lặp đầu tiên:
[x, y] = ['name', 'Lydia']
- Trong lần lặp thứ hai:
[x, y] = ['age', 21]
- Trong lần lặp đầu tiên:
-
Trong mỗi lần lặp,
console.log(x, y)
sẽ in ra giá trị củax
(key) vày
(value).
3.4. Kết quả
Vì vậy, output sẽ là:
name Lydia
age 21
3.5. Ví dụ mở rộng
Để hiểu rõ hơn về cách Object.entries()
và destructuring hoạt động, hãy xem xét ví dụ sau:
const car = {
brand: 'Toyota',
model: 'Camry',
year: 2022
};
for (const [key, value] of Object.entries(car)) {
console.log(`${key}: ${value}`);
}
Output:
brand: Toyota
model: Camry
year: 2022
3.6. Lưu ý
Destructuring là một tính năng mạnh mẽ trong JavaScript, cho phép chúng ta trích xuất dữ liệu từ arrays hoặc objects một cách dễ dàng. Kết hợp với Object.entries()
, nó cung cấp một cách tiện lợi để duyệt qua các cặp key-value của một object.
4. Rest Parameters và Syntax Error
function getItems(fruitList, ...args, favoriteFruit) {
return [...fruitList, ...args, favoriteFruit]
}
getItems(["banana", "apple"], "pear", "orange")
- A:
["banana", "apple", "pear", "orange"]
- B:
[["banana", "apple"], "pear", "orange"]
- C:
["banana", "apple", ["pear"], "orange"]
- D:
SyntaxError
Đá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é ❓️
4.1. Rest Parameters trong JavaScript
Rest parameters là một tính năng trong JavaScript cho phép một hàm nhận một số lượng đối số không xác định dưới dạng một mảng. Nó được biểu thị bằng ba dấu chấm (...
) trước tên tham số.
4.2. Quy tắc sử dụng Rest Parameters
Có một quy tắc quan trọng khi sử dụng rest parameters: nó phải là tham số cuối cùng trong danh sách tham số của hàm. Nếu không tuân thủ quy tắc này, JavaScript sẽ throw ra một SyntaxError.
4.3. Phân tích đoạn code
Trong đoạn code của chúng ta:
function getItems(fruitList, ...args, favoriteFruit) {
return [...fruitList, ...args, favoriteFruit]
}
Chúng ta đang vi phạm quy tắc sử dụng rest parameters. ...args
không phải là tham số cuối cùng, mà là favoriteFruit
.
4.4. Tại sao lại xảy ra lỗi?
JavaScript engine không thể xác định được đâu là phần thuộc về ...args
và đâu là favoriteFruit
. Nếu cho phép cú pháp này, sẽ dẫn đến sự mơ hồ trong cách truyền đối số vào hàm.
4.5. Cách sửa lỗi
Để sửa lỗi này, chúng ta cần đặt rest parameter ở cuối danh sách tham số:
function getItems(fruitList, favoriteFruit, ...args) {
return [...fruitList, favoriteFruit, ...args]
}
console.log(getItems(["banana", "apple"], "pear", "orange"))
// Output: ["banana", "apple", "pear", "orange"]
4.6. Ví dụ minh họa khác
Để hiểu rõ hơn về cách sử dụng rest parameters, hãy xem xét một vài ví dụ sau:
// Ví dụ 1: Sử dụng rest parameter đúng cách
function sum(first, second, ...rest) {
let result = first + second;
for (let num of rest) {
result += num;
}
return result;
}
console.log(sum(1, 2, 3, 4, 5)); // Output: 15
// Ví dụ 2: Sử dụng rest parameter với destructuring
function introduceTeam(teamName, ...members) {
console.log(`Team ${teamName} has ${members.length} members:`);
members.forEach(member => console.log(member));
}
introduceTeam("JavaScript", "Alice", "Bob", "Charlie");
// Output:
// Team JavaScript has 3 members:
// Alice
// Bob
// Charlie
4.7. Lưu ý quan trọng
- Rest parameter luôn tạo ra một mảng, ngay cả khi không có đối số nào được truyền vào.
- Chỉ có thể có một rest parameter trong một hàm.
- Rest parameter không thể được sử dụng trong object literal setters.
// Không hợp lệ
let obj = {
set latest(...args) { // SyntaxError
// code
}
};
4.8. Kết luận
Hiểu và sử dụng đúng rest parameters là một kỹ năng quan trọng trong JavaScript hiện đại. Nó cho phép chúng ta viết các hàm linh hoạt hơn, có thể xử lý số lượng đối số không xác định. Tuy nhiên, cần nhớ rằng rest parameter phải luôn là tham số cuối cùng trong danh sách tham số của hàm để tránh SyntaxError.
5. Automatic Semicolon Insertion (ASI) trong JavaScript
function nums(a, b) {
if
(a > b)
console.log('a is bigger')
else
console.log('b is bigger')
return
a + b
}
console.log(nums(4, 2))
console.log(nums(1, 2))
- A:
a is bigger
,6
vàb is bigger
,3
- B:
a is bigger
,undefined
vàb is bigger
,undefined
- C:
undefined
vàundefined
- D:
SyntaxError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: B
Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé ❓️
5.1. Automatic Semicolon Insertion (ASI)
ASI là một tính năng của JavaScript, nơi trình thông dịch tự động thêm dấu chấm phẩy ( vào cuối các câu lệnh khi nó nghĩ rằng cần thiết. Điều này cho phép lập trình viên bỏ qua dấu chấm phẩy trong nhiều trường hợp, nhưng đôi khi có thể dẫn đến những kết quả không mong muốn.
5.2. Phân tích đoạn code
Hãy xem xét hàm nums
:
function nums(a, b) {
if
(a > b)
console.log('a is bigger')
else
console.log('b is bigger')
return
a + b
}
Có hai điểm quan trọng cần lưu ý ở đây:
- Cú pháp của câu lệnh
if...else
là hợp lệ, mặc dù không được viết trên cùng một dòng. - Câu lệnh
return
và biểu thứca + b
được viết trên hai dòng khác nhau.
5.3. ASI và câu lệnh return
Khi return
và giá trị trả về được viết trên hai dòng khác nhau, ASI sẽ tự động thêm dấu chấm phẩy sau return
. Điều này có nghĩa là hàm sẽ trả về undefined
ngay lập tức, và dòng code a + b
sẽ không bao giờ được thực thi.
Thực tế, JavaScript sẽ hiểu đoạn code trên như sau:
function nums(a, b) {
if (a > b)
console.log('a is bigger');
else
console.log('b is bigger');
return;
a + b; // Dòng code này không bao giờ được thực thi
}
5.4. Kết quả thực thi
-
Khi gọi
nums(4, 2)
:- In ra "a is bigger"
- Trả về
undefined
-
Khi gọi
nums(1, 2)
:- In ra "b is bigger"
- Trả về
undefined
5.5. Cách sửa lỗi
Để hàm hoạt động như mong muốn, chúng ta nên viết return
và biểu thức trả về trên cùng một dòng:
function nums(a, b) {
if (a > b)
console.log('a is bigger')
else
console.log('b is bigger')
return a + b
}
console.log(nums(4, 2)) // "a is bigger", 6
console.log(nums(1, 2)) // "b is bigger", 3
5.6. Kinh nghiệm
-
Luôn cẩn thận khi sử dụng
return
trong JavaScript. Nếu bạn muốn trả về một giá trị, hãy đảm bảo rằng nó nằm trên cùng một dòng với từ khóareturn
hoặc bắt đầu ngay sau dấu mở ngoặc. -
Mặc dù ASI giúp code trở nên "sạch" hơn bằng cách cho phép bỏ qua dấu chấm phẩy, nhưng nó cũng có thể dẫn đến những lỗi khó phát hiện. Vì vậy, nhiều lập trình viên vẫn chọn cách viết dấu chấm phẩy một cách rõ ràng.
-
Sử dụng các công cụ linting như ESLint có thể giúp phát hiện những vấn đề tiềm ẩn liên quan đến ASI.
5.7. Kết luận
ASI là một tính năng gây tranh cãi trong JavaScript. Mặc dù nó có thể giúp code trông "sạch" hơn, nhưng cũng có thể dẫn đến những lỗi không mong muốn. Hiểu rõ về cách ASI hoạt động sẽ giúp bạn tránh được những lỗi như trong ví dụ trên và viết code JavaScript an toàn và đáng tin cậy hơn. Tuy nhiên gần đây đa số dự án đều sử dụng TypeScript và ESLint để giúp kiểm tra lỗi trước khi chạy code nên gặp lỗi này trở nên hiếm hơn.
Đó là tất cả những gì chúng ta cần biết về 5 ví dụ trong JavaScript Nâng Cao - Kỳ 19. Hy vọng qua bài viết này, các bạn đã hiểu rõ hơn về cách hoạt động của push()
, prototype
, Object.entries()
, rest parameters, và Automatic Semicolon Insertion trong JavaScript.
Hãy nhớ rằng, để trở thành một lập trình viên JavaScript giỏi, bạn không chỉ cần biết cú pháp, mà còn phải hiểu sâu về cách ngôn ngữ hoạt động. Tiếp tục thực hành và đừng ngại đặt câu hỏi nhé!
Hẹn gặp lại các bạn trong những bài viết tiếp theo của series JavaScript Nâng Cao. Chúc các bạn học tập tốt! 🚀🎉
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