Bài 6.2: Destructuring và spread/rest operators trong JS

Chào mừng trở lại với series Lập trình Web Front-end! Trong bài viết hôm nay, chúng ta sẽ cùng nhau lặn sâu vào hai khái niệm cực kỳ mạnh mẽ và được sử dụng rất phổ biến trong JavaScript hiện đại: Destructuring Assignment và bộ đôi Spread/Rest Operators (sử dụng cùng ký hiệu ...). Nắm vững chúng sẽ giúp code của bạn trở nên ngắn gọn, dễ đọcmạnh mẽ hơn đáng kể.

Bạn đã bao giờ cảm thấy mệt mỏi khi phải truy cập các thuộc tính của đối tượng hoặc các phần tử của mảng một cách lặp đi lặp lại chưa? Hay bạn muốn tạo bản sao (copy) một mảng/đối tượng mà không ảnh hưởng đến bản gốc, hoặc gộp nhiều mảng/đối tượng lại với nhau một cách dễ dàng? Đó chính là lúc DestructuringSpread/Rest tỏa sáng!

Hãy cùng bắt đầu hành trình khám phá!

Destructuring Assignment

Destructuring (hay Gán phá vỡ cấu trúc) là một cú pháp đặc biệt trong JavaScript cho phép bạn "giải nén" các giá trị từ mảng (arrays) hoặc đối tượng (objects) vào các biến riêng biệt một cách nhanh chóng.

Nó giống như việc bạn có một chiếc hộp chứa nhiều món đồ (mảng hoặc đối tượng), và bạn muốn lấy một vài món cụ thể ra và đặt tên riêng cho chúng thay vì cứ phải mở hộp mỗi lần sử dụng.

1. Destructuring Mảng (Array Destructuring)

Trước đây, để lấy các phần tử của mảng, bạn làm thế này:

const colors = ['red', 'green', 'blue'];

const firstColor = colors[0];
const secondColor = colors[1];

console.log(firstColor, secondColor); // Output: red green

Với Destructuring, mọi thứ trở nên đơn giảntrực quan hơn nhiều:

const colors = ['red', 'green', 'blue'];

// Destructuring mảng
const [firstColor, secondColor] = colors;

console.log(firstColor, secondColor); // Output: red green

Giải thích: Chúng ta khai báo một mảng các biến [firstColor, secondColor] và gán nó bằng mảng colors. JavaScript sẽ tự động gán phần tử đầu tiên của colors cho firstColor, phần tử thứ hai cho secondColor, và cứ thế tiếp diễn theo thứ tự.

Bạn có thể bỏ qua các phần tử mà bạn không quan tâm bằng cách để trống vị trí tương ứng:

const numbers = [1, 2, 3, 4, 5];

// Bỏ qua phần tử thứ 2 và thứ 3 (chỉ quan tâm thứ 1 và thứ 4)
const [one, , , four] = numbers;

console.log(one, four); // Output: 1 4
2. Destructuring Đối tượng (Object Destructuring)

Destructuring đối tượng cho phép bạn "giải nén" các thuộc tính (properties) của đối tượng dựa trên tên thuộc tính.

Cách truyền thống để lấy thuộc tính của đối tượng:

const person = { name: 'Alice', age: 30, city: 'New York' };

const personName = person.name;
const personAge = person.age;

console.log(personName, personAge); // Output: Alice 30

Với Destructuring đối tượng:

const person = { name: 'Alice', age: 30, city: 'New York' };

// Destructuring đối tượng
const { name, age } = person;

console.log(name, age); // Output: Alice 30

Giải thích: Chúng ta khai báo một đối tượng các biến { name, age } và gán nó bằng đối tượng person. JavaScript sẽ tìm các thuộc tính có tên nameage trong person và gán giá trị của chúng cho các biến cùng tên. Lưu ý rằng thứ tự các biến trong Destructuring đối tượng không quan trọng, chỉ có tên của chúng là quan trọng.

Bạn có thể gán giá trị của thuộc tính cho một biến có tên khác:

const user = { id: 1, username: 'john_doe' };

// Lấy thuộc tính 'username' và gán cho biến 'accountName'
const { username: accountName, id } = user;

console.log(accountName, id); // Output: john_doe 1
// console.log(username); // Lỗi! Biến username không tồn tại
3. Giá trị mặc định (Default Values)

Bạn có thể cung cấp giá trị mặc định trong quá trình Destructuring. Giá trị này sẽ được sử dụng nếu thuộc tính hoặc phần tử tương ứng không tồn tại hoặc có giá trị là undefined.

Với Mảng:

const fruits = ['apple', 'banana'];

// cherry không tồn tại, sẽ nhận giá trị mặc định 'cherry default'
const [f1, f2, f3 = 'cherry default'] = fruits;

console.log(f1, f2, f3); // Output: apple banana cherry default

Với Đối tượng:

const car = { make: 'Toyota', model: 'Camry' };

// year không tồn tại, sẽ nhận giá trị mặc định 2023
const { make, model, year = 2023 } = car;

console.log(make, model, year); // Output: Toyota Camry 2023
4. Destructuring lồng nhau (Nested Destructuring)

Bạn có thể giải nén cả các cấu trúc lồng nhau bên trong đối tượng hoặc mảng.

const student = {
  name: 'Peter',
  address: {
    street: '123 Main St',
    city: 'Anytown'
  },
  grades: [90, 85, 92]
};

// Lấy tên, thành phố và điểm đầu tiên
const {
  name,
  address: { city }, // Destructure address, rồi destructure city từ address
  grades: [firstGrade] // Destructure grades, rồi destructure phần tử đầu tiên
} = student;

console.log(name, city, firstGrade); // Output: Peter Anytown 90

Destructuring giúp code khi làm việc với dữ liệu phức tạp trở nên sạch sẽdễ theo dõi hơn nhiều so với việc truy cập từng cấp một (student.address.city).

Spread và Rest Operators (...)

Đây là hai khái niệm sử dụng cùng một ký hiệu là ba dấu chấm ..., nhưng mục đích và cách sử dụng của chúng lại khác nhau tùy thuộc vào ngữ cảnh.

  • Spread Operator (Toán tử trải ra): Dùng để mở rộng (trải ra) các phần tử của một iterable (như mảng, chuỗi) hoặc các thuộc tính của một đối tượng vào một ngữ cảnh nào đó (ví dụ: bên trong một mảng mới, một đối tượng mới, hoặc làm đối số cho hàm). Nó là hành động phân rã.
  • Rest Parameters (Tham số còn lại): Dùng để gom lại (thu thập) các phần còn lại của các phần tử/thuộc tính vào một mảng (trong trường hợp mảng/tham số hàm) hoặc một đối tượng (trong trường hợp đối tượng Destructuring). Nó là hành động gom nhóm.

Hãy cùng xem xét từng loại.

1. Spread Operator (...)

Spread Operator rất linh hoạt và hữu ích trong nhiều trường hợp.

a. Spread với Mảng
  • Sao chép mảng (Shallow Copy): Tạo một bản sao nông (shallow copy) của mảng mà không ảnh hưởng đến mảng gốc.

    const originalArray = [1, 2, 3];
    const copiedArray = [...originalArray]; // Sao chép mảng
    
    console.log(copiedArray); // Output: [1, 2, 3]
    console.log(originalArray === copiedArray); // Output: false (là một mảng mới)
    
    originalArray.push(4);
    console.log(originalArray); // Output: [1, 2, 3, 4]
    console.log(copiedArray); // Output: [1, 2, 3] (copiedArray không bị thay đổi)
    

    Đây là cách tạo bản sao mảng hiện đại và phổ biến.

  • Kết hợp (Nối) các mảng: Dễ dàng nối nhiều mảng lại với nhau.

    const arr1 = [1, 2];
    const arr2 = [3, 4];
    const arr3 = [5, 6];
    
    // Kết hợp các mảng
    const combinedArray = [...arr1, ...arr2, ...arr3];
    
    console.log(combinedArray); // Output: [1, 2, 3, 4, 5, 6]
    

    Ngắn gọn hơn nhiều so với arr1.concat(arr2, arr3).

  • Thêm phần tử vào mảng một cách bất biến (Immutable way): Tạo mảng mới với phần tử được thêm vào mà không thay đổi mảng gốc.

    const items = ['a', 'b'];
    const newItem = 'c';
    
    const newItems = [...items, newItem]; // Thêm vào cuối
    const itemsAtStart = [newItem, ...items]; // Thêm vào đầu
    
    console.log(newItems); // Output: ['a', 'b', 'c']
    console.log(itemsAtStart); // Output: ['c', 'a', 'b']
    console.log(items); // Output: ['a', 'b'] (mảng gốc không đổi)
    

    Cách này cực kỳ hữu ích trong các thư viện quản lý trạng thái như Redux.

b. Spread với Đối tượng

Spread Operator cũng hoạt động với các thuộc tính của đối tượng (từ ES2018).

  • Sao chép đối tượng (Shallow Copy): Tạo một bản sao nông của đối tượng.

    const originalObject = { a: 1, b: 2 };
    const copiedObject = { ...originalObject }; // Sao chép đối tượng
    
    console.log(copiedObject); // Output: { a: 1, b: 2 }
    console.log(originalObject === copiedObject); // Output: false
    
  • Kết hợp (Nối) các đối tượng: Kết hợp các thuộc tính từ nhiều đối tượng vào một đối tượng mới.

    const obj1 = { a: 1, b: 2 };
    const obj2 = { c: 3, d: 4 };
    
    // Kết hợp các đối tượng
    const mergedObject = { ...obj1, ...obj2 };
    
    console.log(mergedObject); // Output: { a: 1, b: 2, c: 3, d: 4 }
    
  • Thêm/Cập nhật thuộc tính một cách bất biến: Tạo đối tượng mới với các thuộc tính được thêm hoặc cập nhật mà không thay đổi đối tượng gốc.

    const settings = { theme: 'dark', fontSize: 16 };
    
    // Thêm thuộc tính mới và cập nhật thuộc tính cũ
    const newSettings = {
      ...settings,
      language: 'en', // Thêm mới
      fontSize: 18    // Cập nhật
    };
    
    console.log(newSettings); // Output: { theme: 'dark', fontSize: 18, language: 'en' }
    console.log(settings); // Output: { theme: 'dark', fontSize: 16 } (đối tượng gốc không đổi)
    

    Khi có thuộc tính trùng tên, giá trị của thuộc tính nằm sau trong cú pháp Spread sẽ ghi đè lên giá trị của thuộc tính nằm trước.

c. Spread với Đối số hàm

Spread Operator cho phép bạn "trải" các phần tử của một mảng thành các đối số riêng lẻ khi gọi một hàm.

function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];

// Sử dụng spread để truyền các phần tử mảng làm đối số
const total = sum(...numbers);

console.log(total); // Output: 6

Trước đây, bạn sẽ phải sử dụng sum.apply(null, numbers). Spread Operator giúp cú pháp gọn gàngdễ đọc hơn nhiều.

2. Rest Parameters (...)

Rest Parameters (Tham số còn lại) được sử dụng trong định nghĩa hàm hoặc trong Destructuring để gom lại các giá trị "còn lại" vào một mảng (hoặc đối tượng trong Destructuring đối tượng).

a. Rest Parameters trong Hàm

Khi định nghĩa một hàm, bạn có thể sử dụng ... trước tên tham số cuối cùng. Tham số này sẽ thu thập tất cả các đối số "còn lại" được truyền vào hàm thành một mảng.

function logArguments(firstArg, secondArg, ...restArgs) {
  console.log('Đối số thứ nhất:', firstArg);
  console.log('Đối số thứ hai:', secondArg);
  console.log('Các đối số còn lại (dạng mảng):', restArgs);
}

logArguments(1, 2, 3, 4, 5);
// Output:
// Đối số thứ nhất: 1
// Đối số thứ hai: 2
// Các đối số còn lại (dạng mảng): [3, 4, 5]

logArguments('a');
// Output:
// Đối số thứ nhất: a
// Đối số thứ hai: undefined
// Các đối số còn lại (dạng mảng): []

Rest Parameters phải là tham số cuối cùng trong danh sách tham số của hàm. Chỉ có thể có một Rest Parameter.

b. Rest trong Destructuring Mảng

Trong Destructuring mảng, bạn có thể sử dụng ... trước tên biến cuối cùng để gom các phần tử còn lại của mảng vào một mảng mới.

const list = [10, 20, 30, 40, 50];

// Lấy 2 phần tử đầu, gom phần còn lại
const [first, second, ...remaining] = list;

console.log(first);    // Output: 10
console.log(second);   // Output: 20
console.log(remaining); // Output: [30, 40, 50]

Tương tự như Rest Parameters trong hàm, Rest Element trong Destructuring mảng cũng phải là phần tử cuối cùng.

c. Rest trong Destructuring Đối tượng

Trong Destructuring đối tượng, bạn có thể sử dụng ... trước tên biến cuối cùng để gom các thuộc tính còn lại của đối tượng vào một đối tượng mới.

const product = {
  id: 'p1',
  name: 'Laptop',
  price: 1200,
  category: 'Electronics',
  brand: 'AwesomeTech'
};

// Lấy id và name, gom các thuộc tính còn lại
const { id, name, ...details } = product;

console.log(id);      // Output: p1
console.log(name);    // Output: Laptop
console.log(details); // Output: { price: 1200, category: 'Electronics', brand: 'AwesomeTech' }

Rest Element trong Destructuring đối tượng cũng phải là phần tử cuối cùng.

Phân biệt Spread và Rest (...)

Mặc dù sử dụng cùng ký hiệu ..., hãy nhớ sự khác biệt về ngữ cảnh sử dụngmục đích:

  • ... khi được sử dụng để trải các phần tử/thuộc tính ra (ví dụ: trong literal mảng/đối tượng, khi gọi hàm) là Spread Operator.
  • ... khi được sử dụng để gom các phần tử/thuộc tính còn lại vào một biến (ví dụ: trong danh sách tham số hàm, trong Destructuring) là Rest Operator (hoặc Rest Parameters/Rest Element).

Nghĩ đơn giản: Spreadmở hộp, Restđóng gói phần còn lại.

Tổng kết

Destructuring và Spread/Rest Operators là những công cụ không thể thiếu trong JavaScript hiện đại. Chúng giúp chúng ta viết code ngắn gọn hơn, rõ ràng hơn và dễ bảo trì hơn.

  • Destructuring: "Giải nén" giá trị từ mảng/đối tượng vào các biến.
  • Spread (...): "Trải ra" các phần tử/thuộc tính (khi tạo mảng/đối tượng mới, gọi hàm).
  • Rest (...): "Gom lại" các phần tử/thuộc tính còn lại vào một mảng/đối tượng (trong tham số hàm, Destructuring).

Hãy dành thời gian thực hành với các ví dụ này và thử áp dụng chúng vào các đoạn code của bạn. Bạn sẽ nhanh chóng thấy được sự hiệu quả mà chúng mang lại!

Comments

There are no comments at the moment.