Destructuring Assignment in ES6

Destructuring Assignment ES6

Introduction

ES2015 (hay còn gọi với cái tên quen thuộc hơn là ES6) giới thiệu khá nhiều thay đổi về mặt cú pháp giúp cho việc trình bày logic trở nên gọn gàng và dễ hiểu hơn. Một trong những thay đổi đó là việc giới thiệu một cú pháp mới - Destructuring hay Destructuring Assignment với tên gọi đầy đủ. Chắc hẳn chúng ta đã khá quên thuộc với các phép gán (assignment), vậy việc thêm từ destructuring phía trước có gì thay đổi không? Nếu chỉ dựa vào tên gọi của cú pháp chúng ta sẽ cảm thấy khá khó hiểu và có thể đặt ra một loạt các câu hỏi liên quan đến cái tên đơn giản đó.

Trong bài viết này, chúng ta sẽ tập trung tìm hiểu về Destructuring Assignment, từ những định nghĩa tổng quát, đến cách sử dụng cũng như đi sâu tìm hiểu cốt lõi bên trong của cú pháp đó.

General Definition

Destructuring Assignment là một cú pháp cho phép tách dữ liệu được lưu trữ bên trong (nested) Objects hoặc Arrays (tổng quát hơn là các iterable values) và gán chúng cho các biến riêng biệt.

Từ định nghĩa trên chúng ta có thể chia Destructuring Assignment ra thành hai nhóm chính:

  • Object Destructuring
  • Array Destructuring

Trước khi tìm hiểu từng nhóm trên, chúng ta hay cùng xem qua một vài ví dụ đơn giản sau:

Object Destructuring

const person = { first: 'Foo', last: 'Bar' };
const {first, last} = person;

console.log(first); // Foo
console.log(last);  // Bar

//------------------------------------------
// Aliases

let characters = {a: 'a', b: 'b', c: 'c'};
let {a: d, b: e, c: f} = characters;

console.log(d, e, f); // a b c
console.log(a);       // Uncaught ReferenceError: a is not defined

//------------------------------------------
// Vue Resource - Fetching notifications from API endpoint

this.$http.get('/api/notifications')
	.then(({data}) => {
		let {counter, notifications} = data;
		
		this.unreadCounter = counter;
		this.notifications = notifications;
	});

Array Destructuring

let characters = ['a', 'b', 'c'];
let [d, e, f] = characters;

console.log(d, e, f); // a b c

Khi nói đến phép gán thông thường, chúng ta thường thấy các cú pháp về mảng hay object xuất hiện ở phía bên phải (right-hand side - RHS) của phép gán và được gọi là assigning values. Tuy nhiên khi destructure một mảng hay một đối tượng, các cú pháp trên sẽ xuất hiện phía bên trái (left-hand side - LHS) của phép gán; trong khí đó RHS của phép gán sẽ là giá trị cần destructuring.

Như vậy, hiểu một cách đơn giản, Destructuring Assignment sẽ là sự đảo ngược của Normal Assignment? Hay hiểu đơn giản hơn nữa, chúng ta lấy một phép gán thông thường và flip nó, kết quả thu được sẽ là một Destructuring Assignment?

Tuy nhiên trên thưc tế, nó không hoàn toàn giống như chúng ta nghĩ, đó cũng là lí do tại sao chúng ta lại đặt dấu (?) cho hai nhận xét phía trên. Trong phần sau của bài viết, chúng ta sẽ tìm hiểu rõ hơn về Object DestructuringArray Destructuring.

// Normal assignment
let numbers = [1, 2, 3];
let person = { name: 'Captain America', age: 97 };

// Destructuring assignment
let [a, b, c] = numbers;
let {name, age} = person;

Array Destructuring

Khi sử dụng Array Destructuring, trên thực tế một iterator sẽ được sử dụng để tách các giá trị cần thiết ra khỏi destructuring source (hay giá trị ở RHS của phép gán). Do đó, chúng ta có thể sử dụng Array Destructuring cho các iterable values (không phải chỉ riêng các arrays), cụ thể sẽ là strings, arrays, sets, maps, function arguments, DOM elements. Chúng ta cũng có thể sử dụng Array Destructuring với các toán tử như Spread nếu cần thiết. Dưới đây là một số ví dụ minh họa:

Strings

let message = 'Hello';
let [a, b] = message;
let [x, y, ...z] = message;

console.log(a, b);    // H e
console.log(x, y, z); // H e ['l', 'l', 'o']

Arrays

let numbers = [101, 102, 103];
let [x, y, z] = numbers;

console.log(x, y, z); // 101 102 103

Sets

let set = new Set().add('foo').add('bar');
let [a, b] = set;

console.log(a, b); // foo bar

Maps

let map = new Map().set('a', 1).set('b', 2);
let [x, y] = map;

console.log(x, y); // ["a", 1] ["b", 2]

Dưới đây sẽ là một số ví dụ mà việc sử dụng Array Destructuring là không khả thi:

// Empty object
let obj = {};
let [x] = obj; // TypeError

// Undefined values
let und = undefined;
let [x] = und; // TypeError

// Null values
let response = null;
let[x] = response; // TypeError

Object Destructuring

Chúng ta sẽ bắt đầu bằng một ví dụ đơn giản. Giả sử chúng ta có một function foo() trả về một object với các property tương ứng là x, y và z:

function foo() {
    return {
        x: 1, 
        y: 2,
        z: 3
    };
}

Object Destructuring hay còn gọi với một tên khác là Object Property Assignment Pattern cho phép chúng ta gán property value của một object cho các biến tương ứng. Chúng ta sẽ có hai cách viết như sau:

let {x: x, y: y, z: z} = foo();
let {x, y, z} = foo();

Cách viết đầu tiên là dạng đầy đủ của Object Destructuring trong đó mỗi property của object sẽ được gán với một biến xác định dưới dạng <<property>>: <<variable>>. Tuy nhiên nếu tên của biến trùng với tên của property, chúng ta có thể loại bỏ phần <<property>>: trong cú pháp trên và kết quả thu được sẽ là cách viết thứ hai (shortened syntax). Trong trường hợp chúng ta muốn gán property của object cho một biến với tên khác so với tên của property, cách viết đầu tiên sẽ được sử dụng.

let {x: a, y: b, z: c} = foo();

console.log(a, b, c); // 1 2 3
console.log(x);       // Uncaught ReferenceError: x is not defined

Chúng ta sẽ dành một chút thời gian để trả lời câu hỏi ở phần trước; liệu {x: a, y: b, z: c} khi đứng ở LHS và RHS của phép gán có gì khác nhau không?

// Destructuring assignment
let {x: a, y: b, z: c} = foo();

// Object literal
let a = 1, b = 2, c = 3;
let bar = { x: a, y: b, z: c };

Nếu để ý kĩ chúng ta sẽ thấy trong ví dụ về Object Literal, x, y, và z là các property của object bar còn a, b, và c sẽ là các source value được sử dụng để gán cho các property của object bar.

Hiểu một cách đơn giản hơn, trong Object Literal, các property sẽ được gán giá trị dưới dạng target <= source. Tuy nhiên khi xem xét Object Destructuring thứ tự sẽ bị đảo ngược thành source => target (not source <= target).

Khi sử dụng Object Destructuring trên một giá trị cụ thể, giá trị đó sẽ được ép kiểu về Object. Tuy nhiên việc ép kiểu không sử dụng Object() constructor mà sẽ sử dụng toObject(). Điểm khác nhau cơ bản giữa hai phương thức này là object constructor sẽ convert các giá trị nullundefined về empty object còn toObject sẽ trả về một exception (TypeError).

Object('abc');     // String abc
Object(123);       // Number 123
Object(undefined); // Object {}
Object(null);      // Object {}

let {x} = undefined; // TypeError
let {y} = null;      // TypeError

Default Values

Khi sử dụng Destructuring Assignment chúng ta có thể gán giá trị mặc định cho các target value (LHS) sử dụng = syntax, tương tự khi ta gán giá trị mặc định cho function arguments. Chúng ta sẽ xét một số ví dụ đơn giản sau đây (chúng ta sẽ sử dụng lại function foo() ở phía trên để tiện cho việc minh họa).

let {x = 4, y = 5, z = 6, t = 10} = foo();
console.log(x, y, z, t); // 1 2 3 10

let {x = 4, y = 5, z = 6, t} = foo();
console.log(x, y, z, t); // 1 2 3 undefined

Như chúng ta thấy, nếu property của object tồn tại, giá trị của property đó sẽ được sử dụng. Nếu property không tồn tại giá trị mặc định sẽ được sử dụng (hoặc undefined sẽ được trả về trong trường hợp không có giá trị mặc định nào được cung cấp). Tuy nhiên việc sử dụng default value khi destructure một mảng hay đối tượng nếu không được xem xét kĩ sẽ làm cho logic trở nên khó hiểu cũng như khó debug hơn.

Nếu giá trị được gán là undefined, default value sẽ được sử dụng:

const [x = 1] = [undefined];
console.log(x); // 1

const [y = 2] = {y: undefined};
console.log(y); // 2

Default value không nhất thiết phải là một giá trị cố định, nó có thể là kết quả của function invocation,... hay đúng hơn nó có thể là một Computed Property:

function foo() {
    return 1;
}

let obj1 = {x: 2};
let obj2 = {x: undefined};

let {x=foo()} = obj1;
console.log(x); // 2

let {x=foo()} = obj2;
console.log(x); // 1

Default value có thể là giá trị của các biến khác trong LHS của Destructuring Assignment, tuy nhiên thứ tự khai báo của các biến là quan trọng trong trường hợp này:

const [x=3, y=x] = [];     // x=3; y=3
const [x=3, y=x] = [7];    // x=7; y=7
const [x=3, y=x] = [7, 2]; // x=7; y=2
const [x=y, y=3] = [];     // ReferenceError

Nested Destructuring && Destructuring Parameters

Nested Destructuring

Trong các ví dụ từ đầu đến thời điểm này, các giá trị được destructure chỉ là các mảng và đối tượng đơn giản. Trên thực tế Destructuring Assignment có thể được sử dụng cho cả nested arraysnested objects. Việc destructure object có thể được sử dụng để làm phẳng (flatten một object). Dưới đây là một số ví dụ đơn giản:

let numbers = [1, [2, 3, 4], 5];
let [a, [b, c, d], e] = numbers;
console.log(a, b, c, d, e); // 1 2 3 4 5

let person = { name: 'Foo', age: 15, information: {address: 'Bar', phone: '0199999999'} };
let {name, age, information: {address, phone}} = person;
console.log(name);    // Foo
console.log(age);     // 15
console.log(address); // Bar
console.log(phone);   // 0199999999

Destructuring Parameters

Khi chúng ta thực thi một function với các arguments, trên thực tế các arguments đó sẽ được gán cho các parameters được định nghĩa trong function definition. Do đó, chúng ta hoàn toàn có thể sử dụng Destructuring Assignment cho function parameters. Dưới đây là một số ví dụ đơn giản:

// Array destructuring for parameters
function foo([a, b]) {
    console.log(a + b);
}
foo([1, 2]); // 3

// Object destructuring for parameters
function bar({x, y}) {
    console.log(x, y);
}
foo({x: 1, y: 2}); // 1 2
foo({});           // undefined undefined

Default values và các biến thể của Destructuring Assignment đề cập trong các ví dụ nói trên đều có thể áp dụng với Destructuring Parameters. Tuy nhiên chúng ta cần lưu ý khi làm việc với các default values do chúng có thể gây ra những nhầm lẫn không đáng có, chúng ta hay xem xét ví dụ dưới đây:

function foo({ x = 5 } = {}, { y } = { y: 5 }) {
    console.log(x, y);
}

foo({}, {y: 10});          // 5 10
foo({x: 15}, {});          // 15 undefined
foo({}, {});               // 5 undefined
foo({x: 15}, {y: 10});     // 15 10
foo({}, undefined);        // 5 5
foo(undefined, undefined); // 5 5
foo({});                   // 5 5

Trong function foo() phía trên, chúng ta có hai parameters là xy và chúng đều được gán các giá trị mặc đinh sử dụng Object Destructuring. Đối với parameter x, nó sẽ được gán giá trị mặc định là 5 và nó sẽ nhận giá trị mặc định đó nếu như argument đầu tiên truyền cho function foo() không có property với tên là x hoặc argument đó là undefined. Trong trường hợp parameter y, thoạt nhìn chúng ta sẽ nghĩ cũng tương tự như parameter x nếu argument thứ hai truyền cho function foo() là một empty object, giá trị mặc định 5 sẽ được sử dụng. Tuy nhiên, điều đó là không đúng. Sự khác biệt ở đây là việc gán giá trị mặc định cho parameter x xảy ra bên trong destructuring pattern còn parameter y thì không. Do đó, nếu truyền một empty object cho cả hai argument của function foo(). Cả hai parameter sẽ đều sử dụng Object Destructuring để lấy ra giá trị cần thiết và kết quả sẽ là khác nhau:

{x = 5} = {}; // x <= 5
{y} = {};     // y <= undefined

Như kết quả khi console.log() các trường hợp, chúng ta sẽ thấy parameter y chỉ nhận giá trị mặc định là 5 khi argument thứ hai không được truyền vào hoặc nhận giá trị là undefined; parameter x sẽ nhận giá trị mặc định khi argument đầu tiên là undefined, empty object, hoặc một object không chứa property mang tên x.

Conclusion

Trong bài viết ngắn này, chúng ta đã đề cập đến một cú pháp khá mới trong ES2015 - Destructuring Assginment, cách sử dụng cú pháp đó cũng nhưng những vấn đề cần lưu ý khi sử dụng nó. Việc sử dụng Destructuring Assignment đem lại khá nhiều lợi ích tuy nhiên chúng ta cũng không nên lạm dụng nó hoặc sử dụng nó không đúng cách. Chúng ta đều không muốn mất cả vài phút đồng hồ để decode những logic chúng ta chỉ mới viết một tháng trước đúng không? Hi vọng bài viết sẽ giúp ích được cho các bạn một phần nào đó khi tìm hiều về ES2015 nói chung và Destructuring Assignment nói riêng.

References

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment http://exploringjs.com/es6/ch_destructuring.html https://medium.freecodecamp.com/learn-es6-the-dope-way-part-iv-default-parameters-destructuring-assignment-a-new-es6-method-44393190b8c9#.nfallixi6


All Rights Reserved