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. Kế thừa trong JavaScript
Với cách nào chúng ta có thể kế thừa Dog
class?
class Dog { constructor(name) { this.name = name; }
}; class Labrador extends Dog { // 1 constructor(name, size) { this.size = size; } // 2 constructor(name, size) { super(name); this.size = size; } // 3 constructor(size) { super(name); this.size = size; } // 4 constructor(name, size) { this.name = name; this.size = size; } };
- A: 1
- B: 2
- C: 3
- D: 4
Đá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é ❓️
1.1. Kế thừa trong JavaScript
Trong JavaScript, kế thừa là một cơ chế cho phép một class (lớp con) kế thừa các thuộc tính và phương thức từ một class khác (lớp cha). Điều này giúp tái sử dụng code và tạo ra mối quan hệ phân cấp giữa các class.
1.2. Từ khóa extends
và super
extends
: Được sử dụng để tạo một class là class con của một class khác.super
: Được sử dụng để gọi constructor của lớp cha.
1.3. Phân tích các cách kế thừa
-
Cách 1:
constructor(name, size) { this.size = size; }
Lỗi: Không gọi
super()
trước khi sử dụngthis
. -
Cách 2:
constructor(name, size) { super(name); this.size = size; }
Đúng: Gọi
super(name)
trước, sau đó khởi tạo thuộc tính mới. -
Cách 3:
constructor(size) { super(name); this.size = size; }
Lỗi:
name
không được định nghĩa trong tham số của constructor. -
Cách 4:
constructor(name, size) { this.name = name; this.size = size; }
Lỗi: Không gọi
super()
trong constructor của lớp con.
1.4. Tại sao cách 2 là đúng?
- Gọi
super(name)
: Điều này đảm bảo constructor của lớp cha (Dog
) được gọi với tham sốname
. - Khởi tạo
this.size
: Sau khi gọisuper()
, ta có thể an toàn sử dụngthis
để thêm thuộc tính mới.
1.5. Lưu ý quan trọng
- Trong constructor của lớp con, ta phải gọi
super()
trước khi sử dụngthis
. super()
phải được gọi với đúng số lượng và kiểu tham số mà constructor của lớp cha yêu cầu.
Hiểu được cách kế thừa đúng đắn sẽ giúp bạn tránh được nhiều lỗi khi làm việc với OOP trong JavaScript. Hãy luôn nhớ gọi super()
đúng cách trong constructor của lớp con!
2. Module Import và Thứ tự Thực thi
Output là gì?
// index.js
console.log('running index.js');
import { sum } from './sum.js';
console.log(sum(1, 2)); // sum.js
console.log('running sum.js');
export const sum = (a, b) => a + b;
- A:
running index.js
,running sum.js
,3
- B:
running sum.js
,running index.js
,3
- C:
running sum.js
,3
,running index.js
- D:
running index.js
,undefined
,running sum.js
Đá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é ❓️
2.1. Module trong JavaScript
Module là một cách để tổ chức và chia nhỏ code thành các phần độc lập, có thể tái sử dụng. ES6 đã giới thiệu cú pháp import
và export
để làm việc với module.
2.2. Thứ tự thực thi của Module
Khi sử dụng import
, JavaScript sẽ thực hiện theo các bước sau:
- Parsing (Phân tích): JavaScript engine đọc và phân tích toàn bộ file.
- Hoisting của Import: Tất cả các câu lệnh
import
được đưa lên đầu scope. - Module Resolution và Loading: Các module được import sẽ được tải và thực thi.
- Evaluation (Đánh giá): Code trong file hiện tại được thực thi.
2.3. Phân tích ví dụ
Trong ví dụ của chúng ta:
index.js
được phân tích.import { sum } from './sum.js'
được hoisted.sum.js
được tải và thực thi, in rarunning sum.js
.- Quay lại
index.js
, in rarunning index.js
. sum(1, 2)
được gọi và in ra kết quả3
.
2.4. So sánh với CommonJS (require
)
Nếu chúng ta sử dụng require
thay vì import
:
// index.js
console.log('running index.js');
const { sum } = require('./sum.js');
console.log(sum(1, 2)); // sum.js
console.log('running sum.js');
exports.sum = (a, b) => a + b;
Kết quả sẽ là: running index.js
, running sum.js
, 3
Điều này là do require
là đồng bộ và được thực thi tại thời điểm nó được gọi.
2.5. Tại sao điều này quan trọng?
Hiểu về thứ tự thực thi của module giúp chúng ta:
- Tránh circular dependencies (phụ thuộc vòng).
- Tối ưu hóa performance bằng cách sắp xếp imports hợp lý.
- Debug dễ dàng hơn khi gặp vấn đề liên quan đến module.
Nhớ rằng, với ES modules, code trong module imported luôn được thực thi trước code trong module importing!
3. So sánh Primitive và Object trong JavaScript
Output là gì?
console.log(Number(2) === Number(2))
console.log(Boolean(false) === Boolean(false))
console.log(Symbol('foo') === Symbol('foo'))
- A:
true
,true
,false
- B:
false
,true
,false
- C:
true
,false
,true
- D:
true
,true
,true
Đá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. Primitive vs Object trong JavaScript
JavaScript có hai loại dữ liệu chính:
- Primitive: number, string, boolean, null, undefined, symbol, bigint
- Object: object, array, function, etc.
3.2. So sánh các giá trị Primitive
Khi so sánh các giá trị primitive, JavaScript so sánh giá trị của chúng.
console.log(Number(2) === Number(2)) // true
console.log(Boolean(false) === Boolean(false)) // true
Trong cả hai trường hợp này, chúng ta đang so sánh các giá trị primitive, nên kết quả là true
.
3.3. Symbol - Trường hợp đặc biệt
console.log(Symbol('foo') === Symbol('foo')) // false
Symbol là một trường hợp đặc biệt. Mỗi Symbol là một giá trị duy nhất, ngay cả khi chúng có cùng mô tả.
3.4. Tại sao Symbol('foo') !== Symbol('foo')?
- Tính duy nhất: Mỗi Symbol được tạo ra là duy nhất, bất kể mô tả của nó.
- Mô tả chỉ để debug: Chuỗi truyền vào Symbol() chỉ là mô tả, không ảnh hưởng đến giá trị của Symbol.
- Use-case: Symbols thường được sử dụng làm key duy nhất trong objects.
3.5. Ví dụ minh họa về Symbol
const sym1 = Symbol('description');
const sym2 = Symbol('description'); console.log(sym1 === sym2); // false
console.log(sym1.description === sym2.description); // true
3.6. Lưu ý quan trọng
- Khi so sánh objects (bao gồm arrays và functions), JavaScript so sánh tham chiếu, không phải giá trị.
- Để so sánh giá trị của objects, bạn cần so sánh từng thuộc tính hoặc sử dụng các phương pháp deep comparison.
Hiểu rõ về cách JavaScript so sánh các kiểu dữ liệu khác nhau sẽ giúp bạn tránh được nhiều lỗi không mong muốn trong quá trình phát triển!
4. Phương thức padStart() trong JavaScript
Output là gì?
const name = "Lydia Hallie"
console.log(name.padStart(13))
console.log(name.padStart(2))
- A:
"Lydia Hallie"
,"Lydia Hallie"
- B:
" Lydia Hallie"
," Lydia Hallie"
("[13x whitespace]Lydia Hallie"
,"[2x whitespace]Lydia Hallie"
) - C:
" Lydia Hallie"
,"Lydia Hallie"
("[1x whitespace]Lydia Hallie"
,"Lydia Hallie"
) - D:
"Lydia Hallie"
,"Lyd"
,
Đáp án của câu hỏi này là ↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C
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ương thức padStart()
padStart()
là một phương thức của String trong JavaScript, được sử dụng để thêm padding (ký tự đệm) vào đầu chuỗi cho đến khi chuỗi đạt được độ dài chỉ định.
Cú pháp:
str.padStart(targetLength [, padString])
targetLength
: Độ dài mong muốn của chuỗi kết quả.padString
(tùy chọn): Chuỗi để đệm. Mặc định là khoảng trắng.
4.2. Phân tích ví dụ
const name = "Lydia Hallie" // Độ dài: 12
console.log(name.padStart(13))
console.log(name.padStart(2))
name.padStart(13)
:- Độ dài mong muốn (13) > Độ dài hiện tại (12)
- Thêm 1 khoảng trắng vào đầu chuỗi
- Kết quả:
" Lydia Hallie"
name.padStart(2)
:- Độ dài mong muốn (2) < Độ dài hiện tại (12)
- Không thêm khoảng trắng nào
- Kết quả:
"Lydia Hallie"
(giữ nguyên chuỗi gốc)
4.3. Các ví dụ khác về padStart()
console.log("5".padStart(3, "0")); // "005"
console.log("123".padStart(5, "*")); // "**123"
console.log("abc".padStart(10, "foo")); // "foofoofabc"
4.4. Use cases của padStart()
-
Định dạng số: Thêm số 0 vào đầu số.
const formattedNumber = "42".padStart(5, "0"); // "00042"
-
Căn chỉnh text: Tạo khoảng trắng đều cho các chuỗi.
const items = ["Apple", "Banana", "Orange"]; items.forEach(item => console.log(item.padStart(10))); // " Apple" // " Banana" // " Orange"
-
Mã hóa thông tin: Ẩn một phần thông tin.
const lastFourDigits = "1234"; const maskedNumber = lastFourDigits.padStart(16, "*"); // "************1234"
4.5. Lưu ý quan trọng
- Nếu
targetLength
nhỏ hơn độ dài chuỗi hiện tại, chuỗi sẽ không bị cắt ngắn mà giữ nguyên. - Nếu
padString
quá dài, nó sẽ bị cắt ngắn để phù hợp vớitargetLength
.
Hiểu và sử dụng padStart()
hiệu quả có thể giúp bạn xử lý chuỗi một cách linh hoạt và chuyên nghiệp hơn trong JavaScript!
5. Nối chuỗi với Emoji trong JavaScript
Output là gì?
console.log("🥑" + "💻");
- A:
"🥑💻"
- B:
257548
- C: A string containing their code points
- 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é ❓️
5.1. Emoji trong JavaScript
Emoji là các ký tự Unicode, và trong JavaScript, chúng được xử lý như các chuỗi bình thường.
5.2. Phép toán cộng chuỗi
Trong JavaScript, khi sử dụng toán tử +
với các chuỗi, nó sẽ thực hiện phép nối chuỗi.
console.log("🥑" + "💻"); // "🥑💻"
Kết quả là một chuỗi mới chứa cả hai emoji.
5.3. Code points của Emoji
Mỗi emoji có một code point Unicode riêng:
console.log("🥑".codePointAt(0)); // 129361
console.log("💻".codePointAt(0)); // 128187
Tuy nhiên, khi nối chuỗi, JavaScript không quan tâm đến code points mà chỉ đơn giản là ghép các ký tự lại với nhau.
5.4. Các ví dụ khác về xử lý Emoji
-
Đếm độ dài chuỗi chứa Emoji:
console.log("🥑💻".length); // 4 (không phải 2)
Lưu ý: Nhiều emoji được biểu diễn bằng 2 "code units" trong JavaScript.
-
Lặp qua từng ký tự trong chuỗi Emoji:
for (let char of "🥑💻") { console.log(char); } // 🥑 // 💻
-
Sử dụng Emoji trong template literals:
const fruit = "🥑"; const device = "💻"; console.log(`I love ${fruit} and ${device}`); // "I love 🥑 and 💻"
5.5. Lưu ý quan trọng
- Emoji có thể gây ra vấn đề khi tính toán độ dài chuỗi hoặc cắt chuỗi.
- Một số emoji phức tạp (như emoji với tông màu da) có thể được tạo thành từ nhiều Unicode code points.
- Khi làm việc với emoji, nên sử dụng các thư viện chuyên dụng để xử lý chuỗi Unicode một cách chính xác.
Hiểu về cách JavaScript xử lý emoji sẽ giúp bạn tránh được nhiều lỗi không mong muốn khi làm việc với chuỗi trong các ứng dụng hiện đạ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. 🤗