Destructuring

Destructuring is an elegant way to extract data from arrays and objects. It removes excessive and repetitive use of indexing operators or dots to get to the data within arrays and objects. The notation takes a bit of getting used to, but soon you’ll be quite comfortable making use of it in your own code and also recognizing its use in code you read.

Array Destructuring

Functions typically return a single value: a primitive or an object. There is no elegant way to return multiple values other than returning an array. That makes the code within the function easy to write, but it turns the code on the receiving end messy. Let’s take a look at an example.

Here’s a function that returns an array.

 const​ getPresidentName = ​function​(number) {
 //implemented only for number 6
 return​ [​'John'​, ​'Quincy'​, ​'Adams'​];
 };

In the old style, to use the values in the array we would write something like this:

 const​ president6 = getPresidentName(6);
 const​ firstName = president6[0];
 const​ lastName = president6[2];

The function getPresidentName() returns the first name, the middle name, and the last name for a president at a given number. On the receiving end, we store that into a variable and then extract the parts we desire using the index operator []. The code is neither fluent nor intuitive. It would be nice to assign the result to variables like firstName and lastName without having to use the index operator; destructuring makes that possible.

Extracting Array Values

Let’s rewrite the receiving side using destructuring.

 const​ [firstName, middleName, lastName] = getPresidentName(6);

We declared three variables, firstName, middleName, and lastName, in one shot and initialized them with the three values in the array returned by the getPresidentName() function, respectively. We did not need the temporary garbage variable president6, and there was no need for the indexing operator.

Instead of const, we may define the variables as let as well.

Ignoring Values

If we cared only about the first name and not the other details, we would gently extract only the first name, like so:

 const​ [firstName] = getPresidentName(6);

The remaining values in the array would be discarded in this case.

More generally, we can extract only the specific values we care about and ignore the ones in the positions in between using an empty argument. For example, we can ignore the middle name and extract only the first and last names like this:

 const​ [firstName,, lastName] = getPresidentName(6);

Note that using multiple commas, like ,,,, may make the code hard to read, so do that sparingly.

Extracting More than Available Values

We saw examples of extracting all the values and also extracting fewer values than available. JavaScript does not get angry if we try to extract more than what’s available.

 const​ [firstName,, lastName, nickName] = getPresidentName(6);
 console.log(nickName);

The getPresidentName() function is returning an array with only three values. However, in the call we are asking for four values, though we’re ignoring the second value. Since there are only three values, the variable nickName is assigned a value of undefined.

Providing a Default Value

We got undefined when we asked for an extra value to be extracted. JavaScript is powerful, but it won’t magically create values for positions that don’t exist—but you can. Remember, from Defining Default Values for Parameters, a parameter picks up a default value if the argument is undefined. We can apply that feature here to substitute a default value if a value being extracted is undefined. Let’s change the previous code to give a default value for nickName.

 const​ [firstName,, lastName, nickName=​'Old Man Eloquent'​] =
  getPresidentName(6);
 console.log(nickName);

If the getPresidentName() function returned at least four values, then nickName will take the value from the result at the appropriate position. But if the function returned fewer than four values, then nickName will be given the default value assigned, “Old Man Eloquent.”

We can provide default values for variables at any position, not just trailing.

Rest Extraction

If we care to give a name for one or more variables but don’t want to lose the rest of the values, we can gather them into an array using the ... rest operator.

 const​ [firstName, ...otherParts] = getPresidentName(6);

The firstName will now contain the string ’John’ and the variable otherParts will contain the array [’Quincy’, ’Adams’].

Extracting into Existing Variable and Swapping

We can use the destructuring feature to swap two values. This eliminates the need for a temporary variable to store the values while we move them around. It also avoids the extra lines of code often used to swap.

 let​ [a, b] = [1, 2];
 console.log(​`a=​${a}​ b=​${b}​`​);
 [a, b] = [b, a];
 console.log(​`a=​${a}​ b=​${b}​`​);

The values in the variables are swapped, and the result is a and b with values 2 and 1, respectively.

This example also illustrated how to extract values into existing variables. We don’t have to include let or const. Existing variables may be assigned values from extraction using the same syntax of destructuring.

Extracting Parameter Values

So far you’ve seen how to extract values from an array created in place or returned from a function. We can also extract values in the parameter list from the arguments passed to a function.

 const​ printFirstAndLastOnly = ​function​([first,, last]) {
  console.log(​`first ​${first}​ last ​${last}​`​);
 };
 
 printFirstAndLastOnly([​'John'​, ​'Q'​, ​'Adams'​]);

By using destructuring, we avoided the index operator within the function and also the need for additional variables. Less noise, more elegant.

Destructuring is one of the most powerful features of modern JavaScript. Its power extends beyond arrays. We can use destructuring to extract properties of objects too, as you’ll see next.

Object Destructuring

Enhanced object literals that we saw earlier in this chapter provided a nice way to create an object using values from variables in lexical scope. Object destructuring is the opposite—it provides an elegant way to extract data from objects into variables in local or lexical scope.

Let’s extract the data from an object using the hard way first and then see how object destructuring helps. Suppose we have an object that holds the details of a person.

 const​ weight = ​'WeightKG'​;
 
 const​ sam = {
  name: ​'Sam'​,
  age: 2,
  height: 110,
  address: { street: ​'404 Missing St.'​},
  shipping: { street: ​'500 NoName St.'​},
  [weight]: 15,
  [Symbol.​for​(​'favoriteColor'​)]: ​'Orange'​,
 };

Now, let’s examine the code to extract the values for the properties. If we want to get only some of the properties, here’s a piece of code to achieve this, using the old techniques.

 const​ firstName = sam.name;
 const​ theAge = sam.age;

We wanted to extract the value of the name property and the age property into the variables firstName and theAge, respectively. But that took a couple of lines and an invocation of the dot notation—that’s rather verbose and it can get tiring. Destructuring can reduce a lot of that cruft.

Destructuring Objects

The destructuring syntax for objects is a bit different from the one we used for arrays; after all, the properties of objects are not positional but referenced using property names. Furthermore, a few other restrictions kick in, as we’ll see soon. Let’s use object destructuring to extract the desired properties.

 const​ { name: firstName, age: theAge } = sam;

This extracts the value in the name property into firstName and the value in the age property into the theAge variable. If the syntax looks a bit strange, be assured it is. Here’s a way to make sense of it. Instead of thinking of it as extraction, think of it as pattern matching.

Suppose we say { a: 1, b: X, c: 3 } = { a: 1, b: 22, c: 3 } and ask the value of X; in a snap, the response will be 22. For every property with the same name on both sides, we compare the values and determine that the variable X should be the value for b on the right side. Now, let’s look at the previous code in the same light, with the actual object substituted for the reference sam:

 const​ { name: firstName, age: theAge } = { name: ​'Sam'​, age: 2, height: 110 };

The property name is mapped to the variable firstName on the left side and the value Sam on the right side. As a result, firstName is given the value Sam. Likewise, age, which is mapped to theAge, is given the value 2. Since we’re not using height on the left side, it’s ignored.

Extracting to Variables with the Same Name

In the previous example, we extracted name into firstName. If the name of the local variable is the same name as the property, then we can remove the colon and the part to the right, like so:

 const​ { name, age: theAge } = sam;

Using this syntax, now we created local variables named name and theAge. name is shortcut for name: name. The name variable is initialized with the value Sam of the name property whereas the theAge variable is initialized with the value of the age property.

Extracting Computed Properties

In addition to the simple property names like name, age, and height, the object we created also has computed properties: [weight] and [Symbol.for(’favoriteColor’)]. Let’s see how to extract the computed properties of an object.

 const​ { [weight]: wt, [Symbol.​for​(​'favoriteColor'​)]: favColor } = sam;

To extract the computed properties, we use the [] notation and to the right of the colon provide a local variable name. In the example, wt is assigned the value of the computed property [weight] and favColor is assigned the value of the Symbol property, respectively.

Assigning Default Values

When extracting, if a property we ask for is not present, we can assign a default value for the missing property.

Next, we’re asking for a favorite property that is not present in the object being extracted from. If the value is not present, the default value assigned to the property on the left-hand side kicks in.

 const​ { lat, lon, favorite = ​true​} = {lat: 84.45, lon: -114.12};

The values for lat and lon are assigned the appropriate value. The favorite variable takes on a value of true.

In the previous example, the name of the local variable is expected to be the same as the name of the property favorite, whether or not that property is present. When using default values, we can also provide a different name for the local variable—for example, liked, using the syntax favorite: liked = true.

Extracting When Passing to a Function

Let’s create a function that receives the sam object and prints the name and age.

 const​ printInfo = ​function​(person) {
  console.log(​`​${person.name}​ is ​${person.age}​ years old`​);
 };
 
 printInfo(sam);

This is a traditional way of writing—we receive the entire object and use the dot notation to access the properties we are interested in. If, for instance, we wanted to access the age property multiple times in the function, then we would either have to use person.age each time or assign the value of the property to a temporary local variable, such as age, and use that instead of the dot notation each time.

Using the object destructuring syntax, we can combine parameter declaration with object value extraction. Let’s reimplement the printInfo() function to use object destructuring.

 const​ printInfo = ​function​({name: theName, age: theAge}) {
  console.log(​`​${theName}​ is ​${theAge}​ years old`​);
 };
 
 printInfo(sam);

There is no difference in calling the function; we still pass the entire object. However, during the invocation of the function, the two desired properties name and age are extracted into the variables theName and theAge.

Of course, if we want to keep the local variable names/parameters the same name as the properties, we can use the shorthand notation, like so:

 const​ printInfo = ​function​({name, age}) {
  console.log(​`​${name}​ is ​${age}​ years old`​);
 };

Deep Destructuring

So far in the examples we extracted the top-level properties of objects. The destructuring syntax makes it easy to extract properties in lower levels or embedded objects as well. Let’s extract the street property of the address embedded object in sam.

 const​ { name, address: { street } } = sam;

As we saw before, the name property, which is a top-level property, is extracted. In addition, the street property that is nested within the address property is extracted. Use caution; this syntax defines only two variables, name and street. It does not define the address variable. After these changes, we can access name and street, but any attempt to access address will result in a “variable not defined” error.

Dealing with Collisions

We managed to extract the street property from address, but what if we wanted to extract the street properties from both address and shipping? The problem is not the extraction part, but that it will not make sense to assign two values simultaneously to a single variable. Thus we have to provide two different local variable names. We can keep the variable the same as the property for one of the properties and assign a different name for the other. Alternatively, we can provide a different variable name than the property name for both properties.

Let’s rewrite the previous code to extract both the street properties:

 const​ { name, address: { street }, shipping: { street: shipStreet } } = sam;

Again, address and shipping are not defined as variables; only name, street, and shipStreet are defined and the values extracted into them.

Extracting into Existing Variables

In the examples so far, we’ve extracted properties from objects into new variables, defined using const or possibly let. We are not limited to extracting into new variables. We can extract and assign to existing variables in local or lexical scope.

In Extracting into Existing Variable and Swapping, we extracted values from an array into existing variables simply using the [existingVariable] = array syntax. So, we may be tempted to try something like this:

 let​ theName = ​'--'​;
 { name: theName } = sam;

The problem here is that we are expecting things to be consistent, but that’s too much to expect in our field. The previous code will result in a grim message:

 { name: theName } = sam;
  ^
 
 SyntaxError: Unexpected token =

JavaScript does not understand the sudden appearance of the assignment operator after what appears either like a block of code with a label or a JavaScript object.

No worries; all we have to do is whisper into JavaScript’s ear that we’re not defining a block of code or a new object but extracting into an existing variable, like so:

 let​ theName = ​'--'​;
 ({ name: theName } = sam);

All we had to do was wrap the extraction code inside a pair of ()—it did not take a whole lot to please the interpreter. The semicolon should be outside the ().

Extracting with …

In the examples so far, we have extracted part of the object, leaving behind some properties. Sometimes we want to copy the whole object, and at the same time, maybe add new properties, or change the values of existing properties. Destructuring can do this quite well.

As an example, the popular JavaScript state container library Redux[16] is based on immutable state. Instead of modifying existing objects, it transforms them into new objects. Typically a transformation may change one or two property values or add a new property while copying the bulk of the existing object’s properties.

Let’s take a look at a poor approach to copying first and see why this should be avoided, especially when using libraries like Redux.

 const​ addAge = ​function​(person, theAge) {
 return​ {first: person.first, last: person.last, age: theAge };
 };
 
 const​ parker = { first: ​'Peter'​, last: ​'Parker'​ };
 
 console.log(addAge(parker, 15));

The addAge() function creates a new object and copies over the first and last properties—currently that’s all the properties in the given object. In addition, it adds a new age property and assigns the given value of theAge. If the given person object already has an age property, the copy will have a different value.

The output of the code shows that things are working as expected:

 { first: 'Peter', last: 'Parker', age: 15 }

But there’s a catch. The intent of the addAge() function was to either add or replace the age property in the copy while retaining all the other properties. The code, as written, is not extensible. If we add another property—for example, email—to the person, the addAge() function will not bring that over. Also, if we remove an existing property, the code will produce an unintended result—the copied object will have an undesirable value of undefined for the removed property.

Let’s modify the person instance before sending it to addAge, like so:

 const​ parker = { first: ​'Peter'​, last: ​'Parker'​,
  email: ​'[email protected]'​ };
 
 console.log(addAge(parker, 15));

The output shows that the new property added to the person is missing from the copy.

 { first: 'Peter', last: 'Parker', age: 15 }

The spread operator ... in the destructuring syntax saves the day, as we’d expect from a superhero, and makes the code extensible. Let’s rewrite the previous code to use the spread operator with destructuring.

 const​ addAge = ​function​(person, theAge) {
 return​ {...person, last: person.last.toUpperCase(), age: theAge };
 }
 
 const​ parker = { first: ​'Peter'​, last: ​'Parker'​,
  email: ​'[email protected]'​ };
 
 console.log(addAge(parker, 15));

To make a copy of the object, we use the spread operator ... and then list the new properties and/or properties we’d like to replace. In this example, we replace the last property and at the same time add a new age property.

Let’s quickly take a look at the output:

 { first: 'Peter',
  last: 'PARKER',
  email: '[email protected]',
  age: 15 }

The output shows that the last property has been given a new value in the copy; the age property has been added; and the other properties, though not mentioned explicitly during copying, were kept intact.

If the intent is to keep all the current properties of an object while replacing the values of some and optionally adding a few new properties, then rely on the spread operator instead of specifying each property by name. This is critical from the extensibility point of view and will minimize the need to track down bugs later on.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.66.151