JavaScript Nâng Cao - Kỳ 11
Có một câu nói 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. Tham trị và tham chiếu trong JavaScript
Output của đoạn code sau là gì?
function getInfo(member, year) {
member.name = "Lydia";
year = "1998";
}
const person = { name: "Sarah" };
const birthYear = "1997";
getInfo(person, birthYear);
console.log(person, birthYear);
- A:
{ name: "Lydia" }, "1997"
- B:
{ name: "Sarah" }, "1998"
- C:
{ name: "Lydia" }, "1998"
- D:
{ name: "Sarah" }, "1997"
Đá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é ❓️
1.1. Tham trị (Pass by Value) và Tham chiếu (Pass by Reference)
Trong JavaScript, khi chúng ta truyền đối số vào hàm, có hai cách mà giá trị được xử lý:
-
Tham trị (Pass by Value): Áp dụng cho các kiểu dữ liệu nguyên thủy (primitive types) như number, string, boolean, null, undefined.
-
Tham chiếu (Pass by Reference): Áp dụng cho các đối tượng (objects) bao gồm arrays, functions, và object literals.
1.2. Phân tích đoạn code
Trong ví dụ của chúng ta:
const person = { name: "Sarah" };
const birthYear = "1997";
getInfo(person, birthYear);
person
là một object, nên nó được truyền vào hàmgetInfo
bằng tham chiếu.birthYear
là một string (kiểu nguyên thủy), nên nó được truyền vào hàmgetInfo
bằng tham trị.
1.3. Điều gì xảy ra trong hàm getInfo
?
function getInfo(member, year) {
member.name = "Lydia";
year = "1998";
}
-
member.name = "Lydia"
: Vìmember
là tham chiếu đếnperson
, nên thay đổimember.name
sẽ thay đổiperson.name
. -
year = "1998"
: Đây chỉ thay đổi bản sao local củabirthYear
trong hàmgetInfo
, không ảnh hưởng đếnbirthYear
bên ngoài.
1.4. Kết quả
Sau khi gọi getInfo(person, birthYear)
:
person
đã bị thay đổi:{ name: "Lydia" }
birthYear
vẫn giữ nguyên:"1997"
Vì vậy, khi chúng ta log ra:
console.log(person, birthYear);
Kết quả sẽ là: { name: "Lydia" }, "1997"
1.5. Lưu ý quan trọng
Hiểu rõ về tham trị và tham chiếu là cực kỳ quan trọng trong JavaScript. Nó giúp chúng ta tránh được nhiều lỗi không mong muốn, đặc biệt khi làm việc với objects và arrays.
Ví dụ, nếu bạn muốn thay đổi một object mà không ảnh hưởng đến object gốc, bạn nên tạo một bản sao của object đó:
const originalObj = { x: 1, y: 2 };
const copyObj = { ...originalObj };
copyObj.x = 100;
console.log(originalObj); // { x: 1, y: 2 }
console.log(copyObj); // { x: 100, y: 2 }
Trong ví dụ trên, chúng ta đã sử dụng spread operator (...) để tạo một bản sao shallow của originalObj
. Khi thay đổi copyObj
, originalObj
không bị ảnh hưởng.
2. Xử lý lỗi với try-catch và throw
Output của đoạn code sau là gì?
function greeting() {
throw "Hello world!";
}
function sayHi() {
try {
const data = greeting();
console.log("It worked!", data);
} catch (e) {
console.log("Oh no an error!", e);
}
}
sayHi();
- A:
"It worked! Hello world!"
- B:
"Oh no an error: undefined
- C:
SyntaxError: can only throw Error objects
- D:
"Oh no an error: Hello world!
Đá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. Từ khóa throw
Trong JavaScript, throw
được sử dụng để ném ra một ngoại lệ (exception). Khi một ngoại lệ được ném ra, nó sẽ dừng thực thi chương trình ngay lập tức và chuyển quyền điều khiển cho đoạn code xử lý ngoại lệ đầu tiên trong call stack.
2.2. Cấu trúc try-catch
Cấu trúc try-catch
được sử dụng để xử lý các ngoại lệ:
- Đoạn code trong khối
try
sẽ được thực thi. - Nếu có bất kỳ ngoại lệ nào xảy ra trong khối
try
, nó sẽ được bắt bởi khốicatch
. - Biến trong
catch(e)
sẽ chứa thông tin về ngoại lệ đã xảy ra.
2.3. Phân tích đoạn code
Trong ví dụ của chúng ta:
-
Hàm
greeting()
ném ra một ngoại lệ với giá trị là chuỗi "Hello world!". -
Trong hàm
sayHi()
:- Chúng ta cố gắng gọi
greeting()
trong khốitry
. - Vì
greeting()
ném ra một ngoại lệ, nên dòngconsole.log("It worked!", data);
sẽ không bao giờ được thực thi. - Ngoại lệ được bắt bởi khối
catch
, và giá trị của ngoại lệ ("Hello world!") được gán cho biếne
.
- Chúng ta cố gắng gọi
-
Cuối cùng, chúng ta in ra "Oh no an error!" cùng với giá trị của
e
, là "Hello world!".
2.4. Lưu ý quan trọng
-
Trong JavaScript, bạn có thể throw bất kỳ giá trị nào, không chỉ giới hạn ở các đối tượng Error.
-
Tuy nhiên, một best practice là luôn throw các đối tượng Error hoặc các đối tượng kế thừa từ Error. Điều này cung cấp thêm thông tin như stack trace, giúp debug dễ dàng hơn.
throw new Error("This is an error message");
-
Khi làm việc với các API bất đồng bộ, bạn có thể sử dụng async/await kết hợp với try-catch:
async function fetchData() { try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); } catch (error) { console.error("An error occurred:", error); } }
Hiểu và sử dụng đúng cách try-catch và throw sẽ giúp bạn xử lý lỗi một cách hiệu quả, làm cho code của bạn mạnh mẽ và dễ bảo trì hơn.
3. Constructor Functions và Return
Output của đoạn code sau là gì?
function Car() {
this.make = "Lamborghini";
return { make: "Maserati" };
}
const myCar = new Car();
console.log(myCar.make);
- A:
"Lamborghini"
- B:
"Maserati"
- C:
ReferenceError
- D:
TypeError
Đá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é ❓️
3.1. Constructor Functions trong JavaScript
Constructor functions là một cách để tạo ra các đối tượng trong JavaScript. Khi chúng ta sử dụng từ khóa new
trước một hàm, JavaScript sẽ xem hàm đó như một constructor function và tạo ra một đối tượng mới.
Thông thường, một constructor function sẽ thực hiện các bước sau:
- Tạo ra một đối tượng mới.
- Gán
this
trong hàm tới đối tượng mới được tạo. - Thực thi code trong hàm.
- Trả về đối tượng mới (trừ khi có một câu lệnh
return
rõ ràng trả về một đối tượng khác).
3.2. Phân tích đoạn code
Trong ví dụ của chúng ta:
function Car() {
this.make = "Lamborghini";
return { make: "Maserati" };
}
Hàm Car
này có hai điểm đặc biệt:
- Nó gán
this.make = "Lamborghini"
. - Nó có một câu lệnh
return
rõ ràng trả về một đối tượng khác{ make: "Maserati" }
.
3.3. Quy tắc đặc biệt với return
trong Constructor Functions
Khi một constructor function trả về một đối tượng rõ ràng, đối tượng đó sẽ được ưu tiên thay vì đối tượng mới được tạo ra bởi new
. Điều này có nghĩa là:
- Nếu
return
một giá trị nguyên thủy (như số, chuỗi, boolean), nó sẽ bị bỏ qua và đối tượng mới vẫn được trả về. - Nếu
return
một đối tượng, đối tượng đó sẽ được trả về thay vì đối tượng mới.
3.4. Kết quả
Trong trường hợp này, mặc dù chúng ta đã gán this.make = "Lamborghini"
, nhưng vì hàm Car
trả về một đối tượng khác { make: "Maserati" }
, đối tượng này sẽ được ưu tiên.
Vì vậy, khi chúng ta tạo myCar
bằng new Car()
, myCar
sẽ là đối tượng { make: "Maserati" }
.
Khi chúng ta log myCar.make
, kết quả sẽ là "Maserati"
.
3.5. Ví dụ minh họa
Để hiểu rõ hơn, hãy xem xét ví dụ sau:
function House() {
this.color = "Red";
return "Blue house";
}
function Apartment() {
this.floors = 5;
return { floors: 10 };
}
const myHouse = new House();
const myApartment = new Apartment();
console.log(myHouse.color); // "Red"
console.log(myApartment.floors); // 10
Trong ví dụ này:
House
trả về một giá trị nguyên thủy (chuỗi), nên nó bị bỏ qua vàmyHouse
vẫn có thuộc tínhcolor
là "Red".Apartment
trả về một đối tượng, nênmyApartment
sẽ là đối tượng được trả về, có thuộc tínhfloors
là 10.
3.6. Lưu ý quan trọng
Việc trả về một đối tượng khác từ constructor function không phải là một best practice. Nó có thể gây nhầm lẫn và khó debug. Thông thường, chúng ta nên để constructor function tạo và trả về đối tượng mới một cách tự nhiên, không can thiệp vào quá trình này bằng return
.
4. Biến toàn cục và từ khóa let
Output của đoạn code sau là gì?
(() => {
let x = (y = 10);
})();
console.log(typeof x);
console.log(typeof y);
- A:
"undefined", "number"
- B:
"number", "number"
- C:
"object", "number"
- D:
"number", "undefined"
Đá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. Phân tích đoạn code
Đoạn code này có vẻ đơn giản, nhưng nó chứa một số điểm thú vị về cách JavaScript xử lý biến và phạm vi (scope). Hãy chia nhỏ nó ra:
(() => {
let x = (y = 10);
})();
Đây là một Immediately Invoked Function Expression (IIFE). Nó được thực thi ngay lập tức sau khi được định nghĩa.
4.2. Gán giá trị và khai báo biến
Dòng let x = (y = 10);
thực sự là một cách viết tắt của:
y = 10;
let x = y;
Điều này có nghĩa là:
y
được gán giá trị 10.x
được khai báo với từ khóalet
và được gán giá trị củay
.
4.3. Phạm vi của biến
x
được khai báo vớilet
, nên nó chỉ tồn tại trong phạm vi của IIFE.y
không được khai báo với bất kỳ từ khóa nào (var
,let
, hoặcconst
), nên nó trở thành một biến toàn cục. Nó sẽ tương đương vớiwindow.y
trong trình duyệt hoặcglobal.y
trong Node.js.
4.4. Kết quả
Khi chúng ta gọi console.log(typeof x);
:
x
không tồn tại bên ngoài IIFE, nêntypeof x
sẽ trả về"undefined"
.
Khi chúng ta gọi console.log(typeof y);
:
y
là một biến toàn cục với giá trị 10, nêntypeof y
sẽ trả về"number"
.
4.5. Ví dụ minh họa
Để hiểu rõ hơn về cách JavaScript xử lý biến trong trường hợp này, hãy xem xét ví dụ sau:
(() => {
let a = (b = 5);
console.log(a); // 5
console.log(b); // 5
})();
console.log(typeof a); // "undefined"
console.log(typeof b); // "number"
Trong ví dụ này:
a
chỉ tồn tại trong IIFE.b
trở thành biến toàn cục.
4.6. Lưu ý quan trọng
Việc tạo ra biến toàn cục một cách không chủ ý như trong trường hợp của y
có thể dẫn đến các lỗi khó phát hiện và gây ô nhiễm không gian tên toàn cục. Đây là một trong những lý do tại sao "use strict" mode được khuyến nghị sử dụng trong JavaScript hiện đại.
Trong strict mode, đoạn code trên sẽ gây ra lỗi vì y
không được khai báo trước khi sử dụng:
"use strict";
(() => {
let x = (y = 10); // Throws ReferenceError: y is not defined
})();
Để tránh những vấn đề như vậy, luôn khai báo biến của bạn với let
, const
, hoặc var
(trong trường hợp cần thiết) trước khi sử dụng.
5. Xóa thuộc tính của đối tượng
Output của đoạn code sau là gì?
class Dog {
constructor(name) {
this.name = name;
}
}
Dog.prototype.bark = function() {
console.log(`Woof I am ${this.name}`);
};
const pet = new Dog("Mara");
pet.bark();
delete Dog.prototype.bark;
pet.bark();
- A:
"Woof I am Mara"
,TypeError
- B:
"Woof I am Mara"
,"Woof I am Mara"
- C:
"Woof I am Mara"
,undefined
- D:
TypeError
,TypeError
Đá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. Prototype trong JavaScript
Trước khi đi vào phân tích đoạn code, chúng ta cần hiểu về khái niệm prototype trong JavaScript.
Mỗi đối tượng trong JavaScript đều có một thuộc tính đặc biệt gọi là prototype
. Khi chúng ta tạo một phương thức cho prototype
của một lớp, tất cả các thể hiện (instances) của lớp đó đều có thể truy cập phương thức này.
5.2. Phân tích đoạn code
Trong ví dụ của chúng ta:
class Dog {
constructor(name) {
this.name = name;
}
}
Dog.prototype.bark = function() {
console.log(`Woof I am ${this.name}`);
};
const pet = new Dog("Mara");
pet.bark();
delete Dog.prototype.bark;
pet.bark();
-
Chúng ta định nghĩa một lớp
Dog
với một constructor nhận vào tham sốname
. -
Sau đó, chúng ta thêm một phương thức
bark
vào prototype của lớpDog
. Điều này có nghĩa là tất cả các instance củaDog
sẽ có thể truy cập phương thức này. -
Chúng ta tạo một instance
pet
của lớpDog
với tên "Mara". -
Khi gọi
pet.bark()
lần đầu tiên, nó sẽ thực thi bình thường và in ra "Woof I am Mara". -
Sau đó, chúng ta xóa phương thức
bark
khỏi prototype củaDog
bằng cách sử dụngdelete Dog.prototype.bark
. -
Khi chúng ta cố gắng gọi
pet.bark()
lần thứ hai, JavaScript sẽ không tìm thấy phương thức này trong prototype chain và sẽ throw ra mộtTypeError
.
5.3. Giải thích chi tiết
Khi chúng ta xóa một phương thức từ prototype, tất cả các instance của lớp đó sẽ mất quyền truy cập vào phương thức đó. Điều này xảy ra vì JavaScript sử dụng cơ chế prototype chain để tìm kiếm các phương thức và thuộc tính.
Khi bạn gọi một phương thức trên một object, JavaScript sẽ:
- Tìm kiếm phương thức đó trực tiếp trên object.
- Nếu không tìm thấy, nó sẽ tìm kiếm trong prototype của object đó.
- Nếu vẫn không tìm thấy, nó sẽ tiếp tục tìm kiếm trong prototype của prototype, và cứ thế tiếp tục cho đến khi đạt đến cuối chuỗi prototype.
Trong trường hợp này, sau khi xóa bark
từ Dog.prototype
, không còn nơi nào trong chuỗi prototype chứa phương thức bark
nữa. Do đó, khi cố gắng gọi pet.bark()
, JavaScript không thể tìm thấy phương thức này và throw ra TypeError
.
5.4. Ví dụ minh họa
Để hiểu rõ hơn về cách prototype chain hoạt động, hãy xem xét ví dụ sau:
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
console.log("Some generic animal sound");
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log("Woof!");
};
const myDog = new Dog("Buddy");
myDog.makeSound(); // "Some generic animal sound"
myDog.bark(); // "Woof!"
delete Dog.prototype.bark;
myDog.bark(); // TypeError: myDog.bark is not a function
delete Animal.prototype.makeSound;
myDog.makeSound(); // TypeError: myDog.makeSound is not a function
Trong ví dụ này, chúng ta có thể thấy rõ cách JavaScript tìm kiếm phương thức trong chuỗi prototype, và điều gì xảy ra khi chúng ta xóa các phương thức từ các mức khác nhau trong chuỗi.
5.5. Tóm lại
Việc hiểu rõ về prototype và cách JavaScript tìm kiếm phương thức trong chuỗi prototype là rất quan trọng. Nó không chỉ giúp chúng ta hiểu cách các object và class hoạt động trong JavaScript, mà còn giúp chúng ta tránh được các lỗi không mong muốn khi làm việc với prototype.
Khi xóa một phương thức từ prototype, hãy cẩn thận vì điều này sẽ ảnh hưởng đến tất cả các instance hiện tại và tương lai của class đó. Trong thực tế, việc xóa phương thức từ prototype không phải là một thực hành phổ biến và nên được thực hiện một cách cẩn thận, chỉ khi thực sự cần thiết.
Kết luận
Qua 5 ví dụ trên, chúng ta đã đi sâu vào một số khía cạnh quan trọng của JavaScript như tham trị và tham chiếu, xử lý lỗi với try-catch, cách hoạt động của constructor functions, sự khác biệt giữa arrow functions và regular functions, và cách JavaScript xử lý prototype chain.
Những kiến thức này không chỉ giúp chúng ta hiểu rõ hơn về cách JavaScript hoạt động, mà còn giúp chúng ta viết code hiệu quả hơn và tránh được nhiều lỗi phổ biến.
Hãy nhớ rằng, JavaScript là một ngôn ngữ rất linh hoạt và có nhiều đặc điểm độc đáo. Việc hiểu rõ những đặc điểm này sẽ giúp bạn trở thành một lập trình viên JavaScript giỏi hơn.
Hy vọng rằng bài viết này đã mang lại cho bạn những kiến thức bổ ích. Hãy tiếp tục thực hành và khám phá thêm về JavaScript nhé!
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