Javascript: destructuring, rest parameters, spread, shallow và deep copy
Bài đăng này đã không được cập nhật trong 4 năm
Xin chào mọi người, hôm nay chúng ta sẽ tìm hiểu về các khái niệm rất hay dùng khi làm việc với javascript và những lưu ý khi sử dụng chúng để tránh những bugs không mong muốn trong quá trình làm việc.
1. Destructuring
Destructuring là kiểu khai báo cho phép ta khai báo và gán biến với các thuộc tính trong object hoặc phần tử trong array. Chúng rất hữu dụng giúp giảm thiểu số dòng code không cần thiết giúp code trở nên sáng sủa hơn. Có 2 loại destructuring đó là: Object destructuring và Array destructuring
1.1 Object Destructuring
Object destructuring cho phép tạo khởi tạo biến từ thông qua thuộc tính của object. Hãy xem xét ví dụ sau đây:
const note = {
id: 1,
title: 'My first note',
date: '20/09/2020',
}
Bình thường, nếu muốn lấy từ giá trị thuộc tính ta khởi tạo như sau:
const id = note.id
const title = note.title
const date = note.date
Với destructuring ta chỉ cần khai báo
const { id, title, date } = note;
console.log(id); // 1
console.log(title); // My first note
console.log(date); // 20/09/2020
Có thể thấy code chỉ cần rút gọn lại trong 1 dòng. Một lưu ý nữa là các thuộc tính của object ban đầu sẽ được giữ nguyên hoàn toàn, mặc dù nếu có thay đổi các giá trị đã khai báo với destructuring.
Mặc định thì nếu khai báo như trên thì tên của các biến khai báo sẽ trùng với tên các thuộc tính trong object. Nếu muốn thay đổi một tên khác ta sẽ phải khai báo như sau
const { id: noteId, title, date } = note; // khai báo tên mới sau dấu :
console.log(nodeId); // 1
console.log(title); // My first note
console.log(date); // 20/09/2020
Hoàn toàn có thể dùng destructuring với nested object như sau
const note = {
id: 1,
title: 'My first note',
date: '20/09/2020',
author: {
firstName: 'Nguyen',
lastName: 'A',
},
}
Lấy thông tin của author trong object trên dùng destructuring
const {
id,
title,
date,
author: { firstName, lastName },
} = note
console.log(firstName); // Nguyen
console.log(lastName); // A
Chú ý là mặc dù khai báo như trên nhưng ta không thể lấy được object author mà nếu muốn thì ta phải khai báo một biến riêng
const { author, author: { firstName, lastName } } = note;
console.log(author); // { firstName: 'Nguyen', lastName: 'A' }
Destructuring còn có thể sử dụng để khai báo biến thông qua thuộc tính object của các kiểu primitive. Ví dụ String có thuộc tính là length . Ta có thể khai báo như sau
const { length } = 'string';
console.log(length); // kết quả là 6
1.2 Array Destructuring
Tương tự với object, ta cũng có thể dùng destructuring với array như sau
const date = ['2020', '20', '09'];
const [year, month, day] = date; // khác với object là khai báo trong dấu [] thay vì {}
console.log(year, month, day); // 2020, 20, 09
Muốn skip một giá trị nào đó ta khai báo như sau
const [year, , day] = date;
Cũng hoàn toàn có thể dùng với nested array
const nestedArray = [1, 2, [3, 4], 5];
const [one, two, [three, four], five] = nestedArray;
console.log(one, two, three, four, five); // kết quả: 1, 2, 3, 4, 5
Destructuring cũng có thể áp dụng với parameter trong một function. Xem xét ví dụ sau
const note = {
id: 1,
title: 'My first note',
date: '01/01/1970',
}
// Sử dụng destructuring với forEach
Object.entries(note).forEach(([key, value]) => {
console.log(`${key}: ${value}`)
});
// Một cách khác là dùng với vòng for...of
for (let [key, value] of Object.entries(note)) {
console.log(`${key}: ${value}`)
}
Nếu muốn khai báo một giá trị default với destructuring ta dùng như sau:
const {
title,
date = new Date(),
author: { firstName },
} = note
console.log(date); // giá trị gán default
2. Spread
Spread là một syntax hữu ích nữa của javascript khi làm việc với arrays, object và function call. Spread cũng là một kiểu khai báo cho phép ta có thể dễ dàng tạo shallow copy. Ta sẽ tìm hiểu sâu hơn về shallow copy trong mục tiếp theo.
2.1 Spread với Arrays
Hãy xem xét ví dụ sau
const array1 = [1, 2];
const array2 = [3, 4];
Bình thường nếu muốn gộp chúng làm một array ta thường sử dụng hàm concat()
: array1.concat(array2)
. Với spread ta có thể khai báo như sau
const combineArray = [...array1, ...array2];
console.log(combineArray); // 1, 2, 3, 4
Nó rất hữu ích trong trường hợp immutability. Ví dụ
const users = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
]
// trường hợp thêm mới một user
const newUser = { id: 3, name: 'C' }
users.push(newUser)
Nhưng nó làm thay đổi mảng users hiện tại. Trong trường hợp này có thể sử dụng spread
const updatedUsers = [...users, newUser]
console.log(users); // không bị thay đổi
console.log(updatedUsers);
Như ta đã biết khi tạo array trong javascript và gán cho một biến thì thực chất không tạo ra mảng mới mà chỉ là dùng biến để reference tới mảng dẫn đến việc khi thay đổi thì mảng ban đầu cũng bị thay đổi theo
const originalArray = [1, 2, 3];
const secondArray = originalArray;
secondArray.pop();
console.log(originalArray); // mảng ban đầu bị thay đổi theo
Với spread sẽ cho phép ta tạo ra một bản 'shallow copy' của array hoặc object
const secondArray = [...originalArray];
secondArray.pop();
console.log(originalArray); // không bị thay đổi mảng ban đầu
Spread còn có thể sử dụng để biến Set hoặc các iterable khác thành array
const set = new Set()
set.add('1')
set.add('2')
set.add('3')
const newSet = [...set];
2.2 Spread với Objects
Spread có thể sử dụng để copy và update object
const originalObject = { a: 1, b: 2 }
const secondObject = { ...originalObject }
console.log(secondObject)
Giống như array thì nó sẽ tạo ra bản shallow copy, với nested object thì vẫn ở dạng reference. Ví dụ
const myObj = { a: 1, b: 2 };
const addObj = { c: 3, d: 4, e: { f: 5} };
const newObj = { ...myObj, ...addObj };
addObj.e.f = 888; // object nested sẽ ở dạng reference nên khi thay đổi thì newObj cũng bị thay đổi theo
console.log(newObj); // kết quả { a: 1, b: 2, c: 3, d: 4, e: { f: 888 } }
Tuy nhiên, nếu muốn tạo ra newObj
giống như trên mà ko sợ thay đổi bởi reference ta có thể làm như sau:
const myObj = { a: 1, b: 2 };
const addObj = { c: 3, d: 4, e: { f: 5} };
const newObj = { ...myObj, c: 3, d: 4, e: { newObj.e } };
addObj.e.f = 999;
console.log(newObj); // kết quả { a: 1, b: 2, c: 3, d: 4, e: { f: 5 } } không bị thay đổi theo addObj
Một chú ý nữa là nếu spread object cùng với một object khác có trùng thuộc tính thì object mới này sẽ ghi đè nên object cũng đã có. Như ví dụ sau
const user = {
id: 1,
name: 'A',
organization: {
name: 'B',
city: 'HaNoi',
},
}
const updatedUser = { ...user, organization: { position: 'VietNam' } }
console.log(updatedUser); // kết quả { id: 1, name: 'A', organization: { position: 'VietNam' } } thuộc tính organization đã bị ghi đè
2.3 Spread với Function
Có thể sử dụng spread với function như sau:
function addition(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
multiply(...numbers); // kết quả = 6
Ngoài ra cũng có thể sử dụng với apply addition.apply(null, [1, 2, 3])
3. Rest Parameters
Tiếp theo ta sẽ tìm hiểu với rest parameters, cú pháp thì viết giống với spread cũng là dấu 3 chấm (...) nhưng công dụng thì ngược lại, rest parameter sẽ tạo ra một array các phần tử. Hãy xem ví dụ sau
function restTest(...args) {
console.log(args)
}
restTest(1, 2, 3, 4, 5, 6) // kết quả [1, 2, 3, 4, 5, 6]
Rest parameter có thể được sử dụng là một parameter hoặc là một parameter ở cuối. Nếu trường hợp là một parameter thì nó sẽ gộp tất cả các arguments, nếu là cuối danh sách thì nó sẽ gộp các arguments còn lại
function restTest(one, two, ...rest) {
console.log(one)
console.log(two)
console.log(rest)
}
restTest(1, 2, 3, 4, 5, 6);
// kết quả là
1
2
[3, 4, 5, 6]
Với các code cũ ở phiên bản ES trước, có thể thực hiện điều trên với arguments
function restTest() {
console.log(arguments)
}
testArguments(1, 2, 3, 4, 5, 6); // [1, 2, 3, 4, 5, 6]
Tuy nhiên, nếu dùng với arguments sẽ có một số bất lợi. Nó không thể sử dụng với arrow function
const restTest = () => {
console.log(arguments);
}
testArguments(1, 2, 3, 4, 5, 6); // sẽ báo lỗi arguments is not defined
Thêm nữa, arguments cũng không phải là một array nên không thể sử dụng các hàm của array như map, filter, ... mà phải chuyển nó thành dạng array trước. Rest cũng có thể sử dụng với array
const [one, ...rest] = [1, 2, 3]
console.log(one); // 1
console.log(rest); // [2, 3]
Rest cũng có thể áp dụng với destructuring
const { name, ...rest } = { id: 1, name: 'Nguyen A', age: 30 }
console.log(name); // Nguyen A
console.log(rest); // { id: 1, age: 30 }
Lưu ý: chỉ sử dụng rest parameter ở vị trí cuối cùng, nếu đặt ở vị trí khác sẽ báo lỗi.
4. Shallow và deep copy
Phần này chúng ta sẽ tìm hiểu kĩ hơn về shallow và deep copy để tránh những bugs không mong muốn trong quá trình làm việc với javascript.
Shallow copy (tạm dịch là sao chép nông): có thể hiểu đơn giản là copy thành một object mới nhưng object này vẫn còn liên quan đến object cũ, tức là một số trường hợp nếu thay đổi object cũ thì object mới cũng bị thay đổi theo
Deep copy (sao chép sâu): hiểu đơn giản là copy một thành object mới mà object này không có liên quan gì đến object cũ.
4.1 Shallow copy
Shallow copy dùng với spread
Trong trường hợp object hoặc array không phức tạp ở dạng nested có thể sử dụng spread để copy một object như sau
const obj = { a: 1, b: 2, c: 3 };
const clone = { ...obj };
console.log(clone);
Nếu object ở dạng nested nếu dùng cách này sẽ gây ra hiện tượng như sau
const obj = { a: 1, b: 2, c: { d: 3 } };
const clone = { ...obj };
obj.c.d = 5; thay đổi giá trị ở obj
console.log(clone); // kết quả là clone object cũng bị thay đổi theo
Shallow copy dùng với Object.assign
Có thể copy một object sử dụng Object.assign. Tìm hiểu thêm về Object.assign tại đây
const obj = { a: 1, b: 2, c: 3};
const clone = Object.assign( {}, obj );
console.log(clone); // { a: 1, b: 2, c: 3 };
Trường hợp copy như thế này vẫn sẽ xảy ra hiện tượng với nested object như trong ví dụ khi dùng với spread.
4.2 Deep copy
Chính vì có hiện tượng như ở trên nên trong nhiều trường hợp cần phải deep copy để tạo ra object mới, tránh gây ra lỗi không mong muốn.
Sử dụng JSON.parse() và JSON.stringify()
const obj = { a: 1, b: 2, c: { d: 3 } };
const clone = JSON.parse(JSON.stringify(obj));
obj.c.d = 5;
console.log(clone); // {a: 1, b: 2, c: { d: 3 } }; // không bị phụ thuộc vào object cũ
Tuy nhiên, cách này vẫn sẽ tồn tại một nhược điểm, hãy xem xét ví dụ sau
console.log(JSON.parse(
JSON.stringify({
a: new Date(),
b: NaN,
c: new Function(),
d: undefined,
e: function() {},
f: Number,
g: false,
h: Infinity
})
))
// Kết quả là một số thuộc tính sẽ bị mất đi hoặc thành null như NAN, undefined, function, Infinity
{ a: "2020-09-20T15:48:39.335Z", b: null, g: false, h: null }
Sử dụng các thư viện
Chính vì có những nhược điểm trên nên khi muốn deep copy object một cách an toàn ta có thể sử dụng các thư viện như Lodash, rfdc...
const cloneDeep = require('lodash/clonedeep');
const addObj = {
e: 6
};
const originalObject = {
a: 1,
b: '2',
c: 3,
d: addObj
};
const deepClonedObject = clonedeep(originalObject);
addObj.e = 9999;
console.log(deepClonedObject); // không bị thay đổi theo
Với rfdc, tham khảo thêm doc tại đây
const clone = require('rfdc')() // Returns the deep copy function
clone({ a: 1, b: { c: 2 } }) // { a: 1, b: { c: 2 } }
Như vậy bài viết đã tìm hiểu các khái niệm rất hay dùng khi làm việc với javascript cùng với những lưu ý để tránh gặp bugs không mong muốn. Hi vọng bài viết giúp ích cho mọi người. See you!
Reference
https://www.digitalocean.com/community/tutorials/js-deep-cloning-javascript-objects
https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089
https://www.taniarascia.com/understanding-destructuring-rest-spread/
https://www.javascripttutorial.net/object/3-ways-to-copy-objects-in-javascript/
All rights reserved