So sánh chi tiết về Object.assign và toán tử spread của JavaScript để thao tác đối tượng, bao gồm các phép đo hiệu suất và ví dụ thực tế.
JavaScript Object.assign và Toán tử Spread: So sánh Hiệu suất & Các trường hợp sử dụng
JavaScript cung cấp nhiều cách để thao tác với đối tượng. Hai phương thức phổ biến là Object.assign()
và toán tử spread (...
). Cả hai đều cho phép bạn sao chép các thuộc tính từ một hoặc nhiều đối tượng nguồn sang một đối tượng đích. Tuy nhiên, chúng khác nhau về cú pháp, hiệu suất và cơ chế hoạt động. Bài viết này sẽ cung cấp một sự so sánh toàn diện để giúp bạn chọn đúng công cụ cho trường hợp sử dụng cụ thể của mình.
Tìm hiểu về Object.assign()
Object.assign()
là một phương thức sao chép tất cả các thuộc tính riêng có thể đếm được (enumerable own properties) từ một hoặc nhiều đối tượng nguồn vào một đối tượng đích. Nó sửa đổi trực tiếp đối tượng đích và trả về đối tượng đó. Cú pháp cơ bản là:
Object.assign(target, ...sources)
target
: Đối tượng đích mà các thuộc tính sẽ được sao chép vào. Đối tượng này sẽ bị sửa đổi.sources
: Một hoặc nhiều đối tượng nguồn mà từ đó các thuộc tính sẽ được sao chép.
Ví dụ:
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target); // Output: { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target); // Output: true
Trong ví dụ này, các thuộc tính của source
được sao chép vào target
. Lưu ý rằng thuộc tính b
bị ghi đè, và returnedTarget
là cùng một đối tượng với target
.
Tìm hiểu về Toán tử Spread
Toán tử spread (...
) cho phép bạn mở rộng một đối tượng có thể lặp (iterable) (như một mảng hoặc một đối tượng) thành các phần tử riêng lẻ. Khi được sử dụng với các đối tượng, nó tạo ra một bản sao nông (shallow copy) các thuộc tính của đối tượng vào một đối tượng mới. Cú pháp rất đơn giản:
const newObject = { ...sourceObject };
Ví dụ:
const source = { a: 1, b: 2 };
const newObject = { ...source };
console.log(newObject); // Output: { a: 1, b: 2 }
console.log(newObject === source); // Output: false
Ở đây, newObject
chứa một bản sao các thuộc tính của source
. Điều quan trọng là newObject
là một đối tượng *mới*, khác biệt với source
.
Những khác biệt chính
Mặc dù cả Object.assign()
và toán tử spread đều đạt được kết quả tương tự (sao chép thuộc tính đối tượng), chúng có những khác biệt quan trọng:
- Tính bất biến: Toán tử spread tạo ra một đối tượng mới, giữ nguyên đối tượng gốc (bất biến).
Object.assign()
sửa đổi trực tiếp đối tượng đích (biến đổi). - Xử lý đối tượng đích:
Object.assign()
cho phép bạn chỉ định một đối tượng đích, trong khi toán tử spread luôn tạo ra một đối tượng mới. - Tính đếm được của thuộc tính: Cả hai phương thức đều sao chép các thuộc tính có thể đếm được. Các thuộc tính không thể đếm được sẽ không được sao chép.
- Thuộc tính kế thừa: Cả hai phương thức đều không sao chép các thuộc tính được kế thừa từ chuỗi prototype.
- Setters:
Object.assign()
gọi các setters trên đối tượng đích. Toán tử spread thì không; nó gán giá trị trực tiếp. - Nguồn là Undefined hoặc Null:
Object.assign()
bỏ qua các đối tượng nguồn lànull
vàundefined
. Việc spreadnull
hoặcundefined
sẽ gây ra lỗi.
So sánh hiệu suất
Hiệu suất là một yếu tố quan trọng cần cân nhắc, đặc biệt khi xử lý các đối tượng lớn hoặc các thao tác thường xuyên. Các phép đo vi mô (microbenchmarks) luôn cho thấy rằng toán tử spread nhìn chung nhanh hơn Object.assign()
. Sự khác biệt này xuất phát từ cách triển khai bên trong của chúng.
Tại sao Toán tử Spread nhanh hơn?
Toán tử spread thường được hưởng lợi từ các triển khai nội bộ được tối ưu hóa trong các engine JavaScript. Nó được thiết kế đặc biệt cho việc tạo đối tượng và mảng, cho phép các engine thực hiện các tối ưu hóa mà không thể thực hiện được với Object.assign()
, một phương thức có mục đích chung hơn. Object.assign()
cần xử lý nhiều trường hợp khác nhau, bao gồm setters và các bộ mô tả thuộc tính khác nhau, làm cho nó vốn dĩ phức tạp hơn.
Ví dụ đo lường hiệu suất (minh họa):
// Simplified example (actual benchmarks require more iterations and robust testing)
const object1 = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const object2 = { f: 6, g: 7, h: 8, i: 9, j: 10 };
// Using Object.assign()
console.time('Object.assign');
for (let i = 0; i < 1000000; i++) {
Object.assign({}, object1, object2);
}
console.timeEnd('Object.assign');
// Using Spread Operator
console.time('Spread Operator');
for (let i = 0; i < 1000000; i++) {
({...object1, ...object2 });
}
console.timeEnd('Spread Operator');
Lưu ý: Đây là một ví dụ cơ bản cho mục đích minh họa. Các phép đo hiệu suất trong thế giới thực nên sử dụng các thư viện đo lường chuyên dụng (như Benchmark.js) để có kết quả chính xác và đáng tin cậy. Mức độ chênh lệch về hiệu suất có thể thay đổi tùy thuộc vào engine JavaScript, kích thước đối tượng và các thao tác cụ thể đang được thực hiện.
Các trường hợp sử dụng: Object.assign()
Mặc dù có lợi thế về hiệu suất của toán tử spread, Object.assign()
vẫn có giá trị trong các tình huống cụ thể:
- Sửa đổi một đối tượng hiện có: Khi bạn cần cập nhật một đối tượng tại chỗ (mutation) thay vì tạo một đối tượng mới. Điều này phổ biến khi làm việc với các thư viện quản lý trạng thái có thể thay đổi hoặc khi hiệu suất là cực kỳ quan trọng và bạn đã phân tích mã của mình để xác nhận rằng việc tránh cấp phát đối tượng mới là đáng giá.
- Gộp nhiều đối tượng vào một đối tượng đích duy nhất:
Object.assign()
có thể gộp hiệu quả các thuộc tính từ nhiều đối tượng nguồn vào một đối tượng đích duy nhất. - Làm việc với các môi trường JavaScript cũ hơn: Toán tử spread là một tính năng của ES6. Nếu bạn cần hỗ trợ các trình duyệt hoặc môi trường cũ hơn không hỗ trợ ES6,
Object.assign()
cung cấp một giải pháp thay thế tương thích (mặc dù bạn có thể cần phải polyfill nó). - Gọi Setters: Nếu bạn cần kích hoạt các setters được định nghĩa trên đối tượng đích trong quá trình gán thuộc tính,
Object.assign()
là lựa chọn đúng đắn.
Ví dụ: Cập nhật Trạng thái theo cách có thể thay đổi (Mutable)
let state = { name: 'Alice', age: 30 };
function updateName(newName) {
Object.assign(state, { name: newName }); // Mutates the 'state' object
}
updateName('Bob');
console.log(state); // Output: { name: 'Bob', age: 30 }
Các trường hợp sử dụng: Toán tử Spread
Toán tử spread thường được ưu tiên sử dụng vì những lợi thế về tính bất biến và hiệu suất trong hầu hết các phát triển JavaScript hiện đại:
- Tạo đối tượng mới với các thuộc tính hiện có: Khi bạn muốn tạo một đối tượng mới với một bản sao các thuộc tính từ một đối tượng khác, thường có một số sửa đổi.
- Lập trình hàm: Toán tử spread rất phù hợp với các nguyên tắc lập trình hàm, vốn nhấn mạnh tính bất biến và tránh các tác dụng phụ (side effects).
- Cập nhật trạng thái React: Trong React, toán tử spread thường được sử dụng để tạo các đối tượng trạng thái mới khi cập nhật trạng thái của component một cách bất biến.
- Redux Reducers: Các reducer của Redux thường sử dụng toán tử spread để trả về các đối tượng trạng thái mới dựa trên các action.
Ví dụ: Cập nhật Trạng thái React một cách bất biến
import React, { useState } from 'react';
function MyComponent() {
const [state, setState] = useState({ name: 'Charlie', age: 35 });
const updateAge = (newAge) => {
setState({ ...state, age: newAge }); // Creates a new state object
};
return (
<div>
<p>Name: {state.name}</p>
<p>Age: {state.age}</p>
<button onClick={() => updateAge(36)}>Increment Age</button>
</div>
);
}
export default MyComponent;
Bản sao nông (Shallow Copy) và Bản sao sâu (Deep Copy)
Điều quan trọng là phải hiểu rằng cả Object.assign()
và toán tử spread đều thực hiện một bản sao *nông* (shallow copy). Điều này có nghĩa là chỉ các thuộc tính ở cấp cao nhất được sao chép. Nếu một đối tượng chứa các đối tượng hoặc mảng lồng nhau, chỉ có các tham chiếu đến các cấu trúc lồng nhau đó được sao chép, chứ không phải bản thân các cấu trúc lồng nhau.
Ví dụ về Bản sao nông:
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.a = 3; // Modifies 'copy.a', but 'original.a' remains unchanged
copy.b.c = 4; // Modifies 'original.b.c' because 'copy.b' and 'original.b' point to the same object
console.log(original); // Output: { a: 1, b: { c: 4 } }
console.log(copy); // Output: { a: 3, b: { c: 4 } }
Để tạo một bản sao *sâu* (deep copy) (nơi các đối tượng lồng nhau cũng được sao chép), bạn cần sử dụng các kỹ thuật như:
JSON.parse(JSON.stringify(object))
: Đây là một phương pháp đơn giản nhưng có thể chậm. Nó không hoạt động với các hàm, ngày tháng, hoặc các tham chiếu vòng tròn._.cloneDeep()
của Lodash: Một hàm tiện ích được cung cấp bởi thư viện Lodash.- Một hàm đệ quy tùy chỉnh: Phức tạp hơn nhưng cung cấp sự kiểm soát tối đa đối với quá trình sao chép sâu.
- Structured Clone: Sử dụng
window.structuredClone()
cho trình duyệt hoặc góistructuredClone
cho Node.js để sao chép sâu các đối tượng.
Các phương pháp hay nhất
- Ưu tiên Toán tử Spread cho Tính bất biến: Trong hầu hết các ứng dụng JavaScript hiện đại, hãy ưu tiên toán tử spread để tạo các đối tượng mới một cách bất biến, đặc biệt khi làm việc với quản lý trạng thái hoặc lập trình hàm.
- Sử dụng Object.assign() để thay đổi các đối tượng hiện có: Chọn
Object.assign()
khi bạn đặc biệt cần sửa đổi một đối tượng hiện có tại chỗ. - Hiểu rõ về Sao chép nông và Sao chép sâu: Hãy nhận thức rằng cả hai phương pháp đều thực hiện sao chép nông. Sử dụng các kỹ thuật phù hợp để sao chép sâu khi cần thiết.
- Đo lường hiệu suất khi nó là yếu tố quan trọng: Nếu hiệu suất là tối quan trọng, hãy tiến hành các phép đo hiệu suất kỹ lưỡng để so sánh hiệu suất của
Object.assign()
và toán tử spread trong trường hợp sử dụng cụ thể của bạn. - Cân nhắc đến tính dễ đọc của mã: Chọn phương pháp mang lại mã dễ đọc và dễ bảo trì nhất cho nhóm của bạn.
Các vấn đề quốc tế cần cân nhắc
Hành vi của Object.assign()
và toán tử spread nhìn chung là nhất quán trên các môi trường JavaScript khác nhau trên toàn thế giới. Tuy nhiên, cần lưu ý các vấn đề tiềm ẩn sau:
- Mã hóa ký tự: Đảm bảo rằng mã của bạn xử lý mã hóa ký tự một cách chính xác, đặc biệt khi xử lý các chuỗi chứa ký tự từ các ngôn ngữ khác nhau. Cả hai phương pháp đều sao chép đúng các thuộc tính chuỗi, nhưng các vấn đề về mã hóa có thể phát sinh khi xử lý hoặc hiển thị các chuỗi này.
- Định dạng Ngày và Giờ: Khi sao chép các đối tượng chứa ngày tháng, hãy lưu ý đến múi giờ và định dạng ngày tháng. Nếu bạn cần tuần tự hóa hoặc giải tuần tự hóa ngày tháng, hãy sử dụng các phương pháp thích hợp để đảm bảo tính nhất quán giữa các khu vực khác nhau.
- Định dạng Số: Các khu vực khác nhau sử dụng các quy ước định dạng số khác nhau (ví dụ: dấu phân cách thập phân, dấu phân cách hàng nghìn). Hãy nhận thức về những khác biệt này khi sao chép hoặc thao tác các đối tượng chứa dữ liệu số có thể được hiển thị cho người dùng ở các ngôn ngữ khác nhau.
Kết luận
Object.assign()
và toán tử spread là những công cụ có giá trị để thao tác đối tượng trong JavaScript. Toán tử spread thường mang lại hiệu suất tốt hơn và thúc đẩy tính bất biến, khiến nó trở thành lựa chọn ưu tiên trong nhiều ứng dụng JavaScript hiện đại. Tuy nhiên, Object.assign()
vẫn hữu ích để thay đổi các đối tượng hiện có và hỗ trợ các môi trường cũ hơn. Hiểu rõ sự khác biệt và các trường hợp sử dụng của chúng sẽ giúp bạn viết mã JavaScript hiệu quả, dễ bảo trì và mạnh mẽ hơn.