Bài 6.4: Thực hành sử dụng ES6+ features

Bài 6.4: Thực hành sử dụng ES6+ features
Chào mừng trở lại với series blog của chúng ta! Hôm nay, chúng ta sẽ cùng nhau đi sâu vào thế giới đầy màu sắc của JavaScript hiện đại với các tính năng từ ES6+ (ECMAScript 2015 và các phiên bản sau). Những cải tiến này không chỉ giúp code của chúng ta ngắn gọn, dễ đọc và mạnh mẽ hơn, mà còn mở ra nhiều cách tiếp cận mới trong lập trình.
ES6 đánh dấu một bước ngoặt lớn cho JavaScript, mang đến hàng loạt tính năng đột phá. Từ đó đến nay, mỗi năm lại có thêm các bản cập nhật nhỏ hơn (ES7, ES8, ES9,...) bổ sung những cải tiến mới. Việc nắm vững và áp dụng các tính năng này là thiết yếu cho bất kỳ lập trình viên Front-end hiện đại nào.
Hãy cùng thực hành ngay thôi nào!
1. let
và const
: Quản lý Biến Thông Minh Hơn
Trước ES6, chúng ta chỉ có var
. var
có phạm vi hoạt động (scope) là toàn cục (global) hoặc hàm (function). Điều này đôi khi dẫn đến những lỗi không mong muốn do biến bị ghi đè.
ES6 giới thiệu let
và const
với phạm vi hoạt động theo khối (block scope).
let
: Dùng để khai báo biến có thể thay đổi giá trị sau khi khai báo. Phạm vi là khối ({}
).const
: Dùng để khai báo biến có giá trị không thể gán lại sau khi khai báo. Phạm vi là khối ({}
). Tuy nhiên, với đối tượng (object) hoặc mảng (array), bạn vẫn có thể thay đổi nội dung bên trong nó, chỉ không thể gán lại biến đó sang một đối tượng/mảng khác.
Ví dụ:
function demonstrateScope() {
// Sử dụng var (cách cũ)
var oldVariable = "I am var";
if (true) {
var oldVariable = "I am var inside if"; // Ghi đè biến bên ngoài
console.log("Inside if (var):", oldVariable); // Output: Inside if (var): I am var inside if
}
console.log("Outside if (var):", oldVariable); // Output: Outside if (var): I am var inside if (bị ghi đè)
console.log("---");
// Sử dụng let và const (ES6+)
let newVariable = "I am let";
const constantValue = "I am const";
if (true) {
let newVariable = "I am let inside if"; // Biến mới, phạm vi chỉ trong if
const anotherConstant = "Another const inside if"; // Biến mới, phạm vi chỉ trong if
console.log("Inside if (let):", newVariable); // Output: Inside if (let): I am let inside if
console.log("Inside if (const):", constantValue); // Output: Inside if (const): I am const (truy cập từ ngoài)
console.log("Inside if (const block):", anotherConstant); // Output: Inside if (const block): Another const inside if
}
console.log("Outside if (let):", newVariable); // Output: Outside if (let): I am let (không bị ghi đè)
console.log("Outside if (const):", constantValue); // Output: Outside if (const): I am const
// console.log(anotherConstant); // Lỗi: anotherConstant is not defined (ngoài phạm vi khối if)
const myArray = [1, 2, 3];
myArray.push(4); // Hợp lệ: Thay đổi nội dung mảng
console.log("Modified const array:", myArray); // Output: Modified const array: [1, 2, 3, 4]
// myArray = [5, 6]; // Lỗi: Assignment to constant variable. (Không thể gán lại biến const)
}
demonstrateScope();
Giải thích: let
và const
giúp chúng ta tạo ra các biến có phạm vi hẹp hơn, tránh được các lỗi không đáng có do ghi đè biến. Luôn ưu tiên sử dụng const
khi giá trị không đổi, và let
khi cần thay đổi, hạn chế sử dụng var
.
2. Arrow Functions (=>
): Viết Hàm Ngắn Gọn và Hiểu this
Dễ Hơn
Arrow functions cung cấp một cú pháp ngắn gọn hơn để viết các biểu thức hàm (function expression
). Đặc biệt, chúng có cách xử lý từ khóa this
khác biệt so với các hàm truyền thống. Arrow functions không có this
của riêng chúng; thay vào đó, chúng kế thừa this
từ ngữ cảnh (scope) bao quanh gần nhất (lexical scoping).
Ví dụ:
// Hàm truyền thống
const sumTraditional = function(a, b) {
return a + b;
};
console.log("Traditional sum:", sumTraditional(5, 7)); // Output: Traditional sum: 12
// Arrow function - cú pháp ngắn gọn
const sumArrow = (a, b) => a + b;
console.log("Arrow sum:", sumArrow(5, 7)); // Output: Arrow sum: 12
// Arrow function với 1 tham số (có thể bỏ ngoặc đơn)
const square = x => x * x;
console.log("Square of 4:", square(4)); // Output: Square of 4: 16
// Arrow function không tham số
const sayHello = () => console.log("Hello from arrow function!");
sayHello(); // Output: Hello from arrow function!
// Arrow function trả về object literal (cần ngoặc đơn bao quanh object)
const createObject = (name, age) => ({ name: name, age: age });
console.log("Created object:", createObject("Bob", 28)); // Output: Created object: { name: 'Bob', age: 28 }
// Ví dụ về 'this' binding (rất quan trọng)
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(function() {
// Ở đây, 'this' trong hàm truyền thống thường trỏ ra ngoài (ví dụ: global object trong non-strict mode, hoặc undefined trong strict mode)
// console.log(this.seconds++); // Sẽ gây lỗi hoặc hành vi không mong muốn
}, 1000);
setInterval(() => {
// Arrow function KHÔNG có 'this' riêng, nó kế thừa 'this' từ ngữ cảnh 'start' (đó là instance của Timer)
this.seconds++; // Hoạt động đúng!
console.log("Seconds:", this.seconds);
}, 1000);
}
}
// const timer = new Timer();
// timer.start(); // Chạy thử và quan sát output sau mỗi giây
Giải thích: Arrow functions rất tiện lợi cho các hàm đơn giản hoặc khi cần giữ ngữ cảnh this
từ bên ngoài (như trong các hàm callback cho setTimeout
hay addEventListener
). Hãy thận trọng khi sử dụng chúng làm phương thức của object literal hoặc class, hoặc khi cần this
riêng của hàm.
3. Destructuring Assignment: "Phá Vỡ" Cấu Trúc Dữ Liệu
Destructuring assignment cho phép bạn giải nén các giá trị từ mảng (array) hoặc thuộc tính từ đối tượng (object) và gán chúng vào các biến riêng biệt một cách nhanh chóng và trực quan.
Ví dụ với Object Destructuring:
const person = {
firstName: "John",
lastName: "Doe",
age: 35,
country: "USA"
};
// Cách cũ
// const firstName = person.firstName;
// const age = person.age;
// Cách dùng Destructuring
const { firstName, age } = person;
console.log(firstName); // Output: John
console.log(age); // Output: 35
// Đổi tên biến khi Destructuring
const { lastName: familyName, country } = person;
console.log(familyName); // Output: Doe
console.log(country); // Output: USA
// Cung cấp giá trị mặc định
const { city = "Unknown" } = person;
console.log(city); // Output: Unknown (vì không có thuộc tính city trong object person)
Ví dụ với Array Destructuring:
const colors = ["red", "green", "blue", "yellow"];
// Cách cũ
// const firstColor = colors[0];
// const thirdColor = colors[2];
// Cách dùng Destructuring
const [firstColor, secondColor] = colors;
console.log(firstColor); // Output: red
console.log(secondColor); // Output: green
// Bỏ qua các phần tử
const [, , thirdColor] = colors; // Bỏ qua phần tử 0 và 1
console.log(thirdColor); // Output: blue
// Lấy phần còn lại của mảng (sử dụng Rest parameter, xem mục tiếp theo)
const [primaryColor, ...otherColors] = colors;
console.log("Primary:", primaryColor); // Output: Primary: red
console.log("Others:", otherColors); // Output: Others: ["green", "blue", "yellow"]
Giải thích: Destructuring làm cho việc truy cập các phần tử trong cấu trúc dữ liệu trở nên ngắn gọn và dễ đọc hơn rất nhiều, đặc biệt hữu ích khi làm việc với các object cấu hình hoặc kết quả từ API.
4. Spread và Rest Operators (...
): Linh Hoạt Với Mảng và Đối Tượng
Toán tử ba chấm (...
) là một trong những tính năng đa năng nhất của ES6+, được sử dụng cho hai mục đích chính:
- Spread Operator: "Trải" (expand) một iterable (như mảng, chuỗi) hoặc object literal thành các phần tử hoặc cặp key-value riêng lẻ.
- Rest Parameter: "Thu gom" (collect) nhiều phần tử riêng lẻ (thường là các tham số còn lại trong một hàm) thành một mảng.
Ví dụ với Spread Operator:
// Copy mảng (shallow copy)
const originalArray = [1, 2, 3];
const copiedArray = [...originalArray];
console.log("Copied array:", copiedArray); // Output: Copied array: [1, 2, 3]
// Kết hợp mảng
const array1 = [1, 2];
const array2 = [3, 4];
const combinedArray = [...array1, ...array2];
console.log("Combined array:", combinedArray); // Output: Combined array: [1, 2, 3, 4]
// Copy object (shallow copy)
const originalObject = { a: 1, b: 2 };
const copiedObject = { ...originalObject };
console.log("Copied object:", copiedObject); // Output: Copied object: { a: 1, b: 2 }
// Kết hợp object
const object1 = { a: 1, b: 2 };
const object2 = { c: 3, d: 4 };
const combinedObject = { ...object1, ...object2 };
console.log("Combined object:", combinedObject); // Output: Combined object: { a: 1, b: 2, c: 3, d: 4 }
// Thêm phần tử/thuộc tính mới khi copy/kết hợp
const updatedObject = { ...originalObject, b: 20, e: 5 }; // Ghi đè 'b', thêm 'e'
console.log("Updated object:", updatedObject); // Output: Updated object: { a: 1, b: 20, e: 5 }
// Sử dụng Spread để truyền tham số cho hàm
function multiply(x, y, z) {
return x * y * z;
}
const numbersToMultiply = [2, 3, 4];
console.log("Multiply with spread:", multiply(...numbersToMultiply)); // Output: Multiply with spread: 24
Ví dụ với Rest Parameter:
// Thu thập các tham số còn lại thành một mảng
function displayArguments(firstArg, ...restArgs) {
console.log("First argument:", firstArg);
console.log("Rest arguments:", restArgs); // restArgs là một MẢNG chứa 2, 3, 4, 5
}
displayArguments(1, 2, 3, 4, 5);
// Output:
// First argument: 1
// Rest arguments: [ 2, 3, 4, 5 ]
// Destructuring kết hợp với Rest
const [head, ...tail] = [10, 20, 30, 40];
console.log("Head:", head); // Output: Head: 10
console.log("Tail:", tail); // Output: Tail: [ 20, 30, 40 ]
Giải thích: Spread và Rest operators mang lại sự linh hoạt đáng kinh ngạc khi làm việc với mảng và đối tượng. Spread giúp tạo bản sao, kết hợp hoặc truyền dữ liệu dễ dàng. Rest giúp gom nhóm các phần tử hoặc tham số một cách gọn gàng.
5. Template Literals (` `${}`
): Chuỗi Mẫu Mạnh Mẽ
Template literals (sử dụng dấu backticks ``
) cho phép tạo chuỗi một cách trực quan và mạnh mẽ hơn so với chuỗi truyền thống (dùng ''
hoặc ""
).
Các tính năng chính:
- String Interpolation: Nhúng biến hoặc biểu thức trực tiếp vào chuỗi bằng cú pháp
${expression}
. - Multiline Strings: Viết chuỗi trải dài qua nhiều dòng mà không cần ký tự xuống dòng đặc biệt (
\n
).
Ví dụ:
const name = "Developer";
const age = 7;
// Cách cũ
// const greetingOld = "Hello, " + name + "! Your age is " + age + ".";
// console.log(greetingOld); // Output: Hello, Developer! Your age is 7.
// Sử dụng Template Literals (interpolation)
const greetingNew = `Hello, ${name}! Your age is ${age}.`;
console.log(greetingNew); // Output: Hello, Developer! Your age is 7.
const price = 10;
const quantity = 3;
const total = `The total cost is ${price * quantity} USD.`; // Nhúng biểu thức
console.log(total); // Output: The total cost is 30 USD.
// Chuỗi nhiều dòng
const multiLineText = `This is the first line.
This is the second line.
This is the third line, indented.`;
console.log(multiLineText);
/* Output:
This is the first line.
This is the second line.
This is the third line, indented.
*/
Giải thích: Template literals làm cho việc xây dựng các chuỗi phức tạp trở nên đơn giản và dễ đọc hơn rất nhiều, loại bỏ nhu cầu nối chuỗi bằng dấu +
.
6. Default Parameters: Tham Số Mặc Định Cho Hàm
Tính năng này cho phép bạn gán các giá trị mặc định cho tham số của hàm ngay trong phần khai báo. Nếu khi gọi hàm, tham số tương ứng không được truyền vào (hoặc được truyền giá trị là undefined
), giá trị mặc định sẽ được sử dụng.
Ví dụ:
// Hàm với tham số mặc định
function introduce(name = "Guest", greeting = "Hello") {
console.log(`${greeting}, my name is ${name}.`);
}
introduce("Alice", "Hi"); // Output: Hi, my name is Alice. (Sử dụng cả 2 giá trị truyền vào)
introduce("Bob"); // Output: Hello, my name is Bob. (Sử dụng giá trị mặc định cho greeting)
introduce(); // Output: Hello, my name is Guest. (Sử dụng cả 2 giá trị mặc định)
introduce(undefined, "Hey"); // Output: Hey, my name is Guest. (Sử dụng giá trị mặc định cho name)
introduce("Charlie", undefined); // Output: Hello, my name is Charlie. (Sử dụng giá trị mặc định cho greeting)
Giải thích: Default parameters giúp viết các hàm linh hoạt hơn, không cần kiểm tra if (param === undefined)
bên trong thân hàm nữa. Code trở nên sạch sẽ và dễ hiểu hơn.
7. Classes: Lập Trình Hướng Đối Tượng Rõ Ràng Hơn
ES6 giới thiệu cú pháp class
như một lớp syntactic sugar (cú pháp "đường") lên trên cơ chế kế thừa dựa trên prototype vốn có của JavaScript. Nó cung cấp một cách trực quan và quen thuộc hơn (đặc biệt với những người từ các ngôn ngữ OOP truyền thống) để định nghĩa các "đối tượng", các phương thức và thực hiện kế thừa.
Ví dụ (Cơ bản):
// Định nghĩa một Class
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
// Getter
get animalName() {
return this.name;
}
// Static method (gọi trực tiếp trên Class, không cần instance)
static compareAnimals(animal1, animal2) {
if (animal1.age && animal2.age) {
return animal1.age - animal2.age; // Giả định có thuộc tính age
}
return 0;
}
}
// Tạo instance (đối tượng) từ Class
const dog = new Animal("Dog");
dog.speak(); // Output: Dog makes a sound.
console.log(dog.animalName); // Output: Dog (sử dụng getter)
// Kế thừa Class (ES6+)
class Dog extends Animal {
constructor(name, breed) {
super(name); // Gọi constructor của lớp cha (Animal)
this.breed = breed;
}
// Ghi đè phương thức của lớp cha
speak() {
console.log(`${this.name} barks!`);
}
// Phương thức riêng của lớp con
displayBreed() {
console.log(`${this.name} is a ${this.breed}.`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // Output: Buddy barks! (Phương thức của lớp con)
myDog.displayBreed(); // Output: Buddy is a Golden Retriever.
console.log(myDog.animalName); // Output: Buddy (Kế thừa getter từ lớp cha)
// Sử dụng static method
// console.log(Animal.compareAnimals({ age: 3 }, { age: 5 })); // Output: -2
Giải thích: Cú pháp class
giúp tổ chức code theo hướng đối tượng rõ ràng hơn. constructor
là phương thức được gọi khi tạo đối tượng mới. Từ khóa extends
cho phép kế thừa, và super()
được dùng để gọi constructor/phương thức của lớp cha. static
methods thuộc về Class chứ không phải từng instance.
8. async
và await
: Xử Lý Bất Đồng Bộ Dễ Dàng Hơn
Trước ES6, xử lý bất đồng bộ thường liên quan đến các callback lồng nhau ("callback hell") hoặc sử dụng Promises với .then()
và .catch()
. ES6 giới thiệu Promises và ES2017 (ES8) đã mang đến async
/await
như một cú pháp tuyệt vời dựa trên Promises để viết code bất đồng bộ trông giống code đồng bộ, làm cho nó dễ đọc và dễ bảo trì hơn nhiều.
async
: Từ khóa được đặt trước một hàm để báo hiệu rằng hàm đó sẽ luôn trả về một Promise. Bên trong hàmasync
, bạn có thể sử dụngawait
.await
: Chỉ được sử dụng bên trong hàmasync
. Đặt trước một Promise,await
sẽ tạm dừng việc thực thi của hàmasync
cho đến khi Promise đó hoàn thành (resolved hoặc rejected). Giá trị trả về của Promise đã hoàn thành sẽ là kết quả của biểu thứcawait
.
Ví dụ:
// Một hàm trả về Promise (mô phỏng việc gọi API hoặc thao tác mất thời gian)
function fetchData(dataId) {
return new Promise((resolve, reject) => {
console.log(`Fetching data for ID: ${dataId}...`);
setTimeout(() => {
if (dataId === 1) {
resolve(`Data from server for ${dataId}`);
} else {
reject(`Error fetching data for ID: ${dataId}`);
}
}, 1000); // Giả lập độ trễ 1 giây
});
}
// Sử dụng async/await để xử lý Promise
async function processData() {
try {
console.log("Starting data processing...");
// Dòng này chờ fetchData(1) hoàn thành
const result1 = await fetchData(1);
console.log("Received:", result1);
console.log("Continuing processing...");
// Dòng này chờ fetchData(2) hoàn thành (sẽ bị reject)
const result2 = await fetchData(2); // Lỗi sẽ xảy ra ở đây
console.log("Received:", result2); // Dòng này sẽ không chạy nếu fetchData(2) bị reject
} catch (error) {
// Bắt lỗi nếu bất kỳ Promise nào bị reject bên trong hàm async
console.error("An error occurred:", error);
} finally {
console.log("Data processing finished.");
}
}
processData();
console.log("This line runs immediately, before processData completes."); // Minh họa tính bất đồng bộ
Giải thích: Hàm processData
được đánh dấu là async
. Khi gọi fetchData(1)
, await
tạm dừng thực thi hàm processData
cho đến khi fetchData(1)
trả về kết quả. Sau khi nhận được kết quả result1
, hàm tiếp tục chạy. Khi gọi fetchData(2)
và nó bị reject
, khối catch
sẽ bắt lỗi. Cú pháp này giúp luồng code bất đồng bộ trông rất giống code đồng bộ, loại bỏ sự phức tạp của chuỗi .then()
.
9. for...of
Loop: Lặp Qua Các Iterable
ES6 giới thiệu for...of
loop như một cách đơn giản và trực tiếp để lặp qua các giá trị của các đối tượng có thể lặp lại (iterable), bao gồm: Mảng (Arrays), Chuỗi (Strings), Map, Set, Arguments object, TypedArray, NodeList, v.v.
Nó khác với for...in
loop, vốn lặp qua các khóa (keys) hoặc chỉ mục (indices) của một đối tượng.
Ví dụ:
// Lặp qua mảng
const fruits = ["apple", "banana", "cherry"];
console.log("Iterating through array:");
for (const fruit of fruits) {
console.log(fruit);
}
/* Output:
apple
banana
cherry
*/
// Lặp qua chuỗi
const greeting = "Hello";
console.log("Iterating through string:");
for (const char of greeting) {
console.log(char);
}
/* Output:
H
e
l
l
o
*/
// So sánh với for...in (lặp qua chỉ mục/khóa)
console.log("Iterating with for...in (array):");
for (const index in fruits) {
console.log(index); // Output: 0, 1, 2 (các chỉ mục)
console.log(fruits[index]); // Output: apple, banana, cherry
}
const obj = { a: 1, b: 2, c: 3 };
// for (const key of obj) { } // Lỗi: obj is not iterable
// for...of không dùng trực tiếp cho object literal thông thường
console.log("Iterating with for...in (object):");
for (const key in obj) {
console.log(key, obj[key]);
}
/* Output:
a 1
b 2
c 3
*/
Giải thích: for...of
loop là cách ưu việt để lặp qua các giá trị trong mảng hoặc các cấu trúc dữ liệu tương tự. Nó trực tiếp truy cập giá trị của từng phần tử, làm code dễ hiểu hơn khi bạn chỉ quan tâm đến các phần tử đó. Hãy nhớ rằng nó không hoạt động trực tiếp trên các object literal thông thường, lúc đó bạn vẫn cần for...in
hoặc các phương thức như Object.keys()
, Object.values()
, Object.entries()
.
Comments