Understanding Destructuring, Rest Parameters, and Spread Syntax
Introduction
Many new features for working with arrays and objects have been made available to the JavaScript language since the 2015 Edition of the ECMAScript specification. A few of the notable ones that you will learn in this article are destructuring, rest parameters, and spread syntax. These features provide more direct ways of accessing the members of an array or an object, and can make working with these data structures quicker and more succinct.
Many other languages do not have corresponding syntax for destructuring, rest parameters, and spread, so these features may have a learning curve both for new JavaScript developers and those coming from another language. In this article, you will learn how to destructure objects and arrays, how to use the spread operator to unpack objects and arrays, and how to use rest parameters in function calls.
Destructuring
Destructuring assignment is a syntax that allows you to assign object properties or array items as variables. This can greatly reduce the lines of code necessary to manipulate data in these structures. There are two types of destructuring: Object destructuring and Array destructuring.
Object Destructuring
Object destructuring allows you to create new variables using an object property as the value.
Consider this example, an object that represents a note with an id
, title
, and date
:
const note = {
id: 1,
title: 'My first note',
date: '01/01/1970',
}
Traditionally, if you wanted to create a new variable for each property, you would have to assign each variable individually, with a lot of repetition:
// Create variables from the Object properties
const id = note.id
const title = note.title
const date = note.date
With object destructuring, this can all be done in one line. By surrounding each variable in curly brackets {}
, JavaScript will create new variables from each property with the same name:
// Destructure properties into variables
const { id, title, date } = note
Now, console.log()
the new variables:
console.log(id)
console.log(title)
console.log(date)
You will get the original property values as output:
1
My first note
01/01/1970
Note: Destructuring an object does not modify the original object. You could still call the original
note
with all its entries intact.
The default assignment for object destructuring creates new variables with the same name as the object property. If you do not want the new variable to have the same name as the property name, you also have the option of renaming the new variable by using a colon (:
) to decide a new name, as seen with noteId
in the following:
// Assign a custom name to a destructured value
const { id: noteId, title, date } = note
Log the new variable noteId
to the console:
console.log(noteId)
You will receive the following output:
1
You can also destructure nested object values. For example, update the note
object to have a nested author
object:
const note = {
id: 1,
title: 'My first note',
date: '01/01/1970',
author: {
firstName: 'Sherlock',
lastName: 'Holmes',
},
}
Now you can destructure note
, then destructure once again to create variables from the author
properties:
// Destructure nested properties
const {
id,
title,
date,
author: { firstName, lastName },
} = note
Next, log the new variables firstName
and lastName
using template literals:
console.log(`${firstName} ${lastName}`)
This will give the following output:
Sherlock Holmes
Note that in this example, though you have access to the contents of the author
object, the author
object itself is not accessible. In order to access an object as well as its nested values, you would have to declare them separately:
// Access object and nested values
const {
author,
author: { firstName, lastName },
} = note
console.log(author)
This code will output the author
object:
{firstName: "Sherlock", lastName: "Holmes"}
Because of this property, destructuring an object is not only useful for reducing the amount of code that you have to write; it also allows you to target your access to the properties you care about.
Finally, destructuring can be used to access the object properties of primitive values. For example, String is a global object for strings, and has a length
property:
const { length } = 'A string'
This will find the inherent length property of a string and set it equal to the length
variable. Log length
to see if this worked:
console.log(length)
You will get the following output:
8
The string A string
was implicitly converted into an object here to retrieve the length
property.
Array Destructuring
Array destructuring allows you to create new variables using an array item as a value. Consider this example, an array with the various parts of a date:
const date = ['1970', '12', '01']
Arrays in JavaScript are guaranteed to preserve their order, so in this case the first index will always be a year, the second will be the month, and so on. Knowing this, you can create variables from the items in the array:
// Create variables from the Array items
const year = date[0]
const month = date[1]
const day = date[2]
But doing this manually can take up a lot of space in your code. With array destructuring, you can unpack the values from the array in order and assign them to their own variables, like so:
// Destructure Array values into variables
const [year, month, day] = date
Now log the new variables:
console.log(year)
console.log(month)
console.log(day)
You will get the following output:
1970
12
01
Values can be skipped by leaving the destructuring syntax blank between commas:
// Skip the second item in the array
const [year, , day] = date
console.log(year)
console.log(day)
Running this will give the value of year
and day
:
1970
01
Nested arrays can also be destructured. First, create a nested array:
// Create a nested array
const nestedArray = [1, 2, [3, 4], 5]
Then destructure that array and log the new variables:
// Destructure nested items
const [one, two, [three, four], five] = nestedArray
console.log(one, two, three, four, five)
You will receive the following output:
1 2 3 4 5
Destructuring syntax can be applied to destructure the parameters in a function. To test this out, you will destructure the keys
and values
out of Object.entries()
.
First, declare the note
object:
const note = {
id: 1,
title: 'My first note',
date: '01/01/1970',
}
Given this object, you could list the key-value pairs by destructuring arguments as they are passed to the forEach()
method:
// Using forEach
Object.entries(note).forEach(([key, value]) => {
console.log(`${key}: ${value}`)
})
Or you could accomplish the same thing using a for
loop:
// Using a for loop
for (let [key, value] of Object.entries(note)) {
console.log(`${key}: ${value}`)
}
Either way, you will receive the following:
id: 1
title: My first note
date: 01/01/1970
Object destructuring and array destructuring can be combined in a single destructuring assignment. Default parameters can also be used with destructuring, as seen in this example that sets the default date to new Date()
.
First, declare the note
object:
const note = {
title: 'My first note',
author: {
firstName: 'Sherlock',
lastName: 'Holmes',
},
tags: ['personal', 'writing', 'investigations'],
}
Then destructure the object, while also setting a new date
variable with the default of new Date()
:
const {
title,
date = new Date(),
author: { firstName },
tags: [personalTag, writingTag],
} = note
console.log(date)
console.log(date)
will then give output similar to the following:
Fri May 08 2020 23:53:49 GMT-0500 (Central Daylight Time)
As shown in this section, the destructuring assignment syntax adds a lot of flexibility to JavaScript and allows you to write more succinct code. In the next section, you will see how spread syntax can be used to expand data structures into their constituent data entries.
Spread
Spread syntax (...
) is another helpful addition to JavaScript for working with arrays, objects, and function calls. Spread allows objects and iterables (such as arrays) to be unpacked, or expanded, which can be used to make shallow copies of data structures to increase the ease of data manipulation.
Spread with Arrays
Spread can simplify common tasks with arrays. For example, let's say you have two arrays and want to combine them:
// Create an Array
const tools = ['hammer', 'screwdriver']
const otherTools = ['wrench', 'saw']
Originally you would use concat()
to concatenate the two arrays:
// Concatenate tools and otherTools together
const allTools = tools.concat(otherTools)
Now you can also use spread to unpack the arrays into a new array:
// Unpack the tools Array into the allTools Array
const allTools = [...tools, ...otherTools]
console.log(allTools)
Running this would give the following:
["hammer", "screwdriver", "wrench", "saw"]
This can be particularly helpful with immutability. For example, you might be working with an app that has users
stored in an array of objects:
// Array of users
const users = [
{ id: 1, name: 'Ben' },
{ id: 2, name: 'Leslie' },
]
You could use push
to modify the existing array and add a new user, which would be the mutable option:
// A new user to be added
const newUser = { id: 3, name: 'Ron' }
users.push(newUser)
But this changes the user
array, which we might want to preserve.
Spread allows you to create a new array from the existing one and add a new item to the end:
const updatedUsers = [...users, newUser]
console.log(users)
console.log(updatedUsers)
Now the new array, updatedUsers
, has the new user, but the original users
array remains unchanged:
[{id: 1, name: "Ben"}
{id: 2, name: "Leslie"}]
[{id: 1, name: "Ben"}
{id: 2, name: "Leslie"}
{id: 3, name: "Ron"}]
Creating copies of data instead of changing existing data can help prevent unexpected changes. In JavaScript, when you create an object or array and assign it to another variable, you are not actually creating a new object—you are passing a reference.
Take this example, in which an array is created and assigned to another variable:
// Create an Array
const originalArray = ['one', 'two', 'three']
// Assign Array to another variable
const secondArray = originalArray
Removing the last item of the second Array will modify the first one:
// Remove the last item of the second Array
secondArray.pop()
console.log(originalArray)
This will give the output:
["one", "two"]
Spread allows you to make a shallow copy of an array or object, meaning that any top level properties will be cloned, but nested objects will still be passed by reference. For simple arrays or objects, a shallow copy may be all you need.
If you write the same example code, but copy the array with spread, and the original Array will no longer be modified:
// Create an Array
const originalArray = ['one', 'two', 'three']
// Use spread to make a shallow copy
const secondArray = [...originalArray]
// Remove the last item of the second Array
secondArray.pop()
console.log(originalArray)
The following will be logged to the console:
["one", "two", "three"]
Spread can also be used to convert a set, or any other iterable to an Array.
Create a new set and add some entries to it:
// Create a set
const set = new Set()
set.add('octopus')
set.add('starfish')
set.add('whale')
Next, use the spread operator with set
and log the results:
// Convert Set to Array
const seaCreatures = [...set]
console.log(seaCreatures)
This will give the following:
["octopus", "starfish", "whale"]
This can also be useful for creating an array from a string:
const string = 'hello'
const stringArray = [...string]
console.log(stringArray)
This will give an array with each character as an item in the array:
["h", "e", "l", "l", "o"]
Spread with Objects
When working with objects, spread can be used to copy and update objects.
Originally, Object.assign()
was used to copy an object:
// Create an Object and a copied Object with Object.assign()
const originalObject = { enabled: true, darkMode: false }
const secondObject = Object.assign({}, originalObject)
The secondObject
will now be a clone of the originalObject
.
This is simplified with the spread syntax—you can shallow copy an object by spreading it into a new one:
// Create an object and a copied object with spread
const originalObject = { enabled: true, darkMode: false }
const secondObject = { ...originalObject }
console.log(secondObject)
This will result in the following:
{enabled: true, darkMode: false}
Just like with arrays, this will only create a shallow copy, and nested objects will still be passed by reference.
Adding or modifying properties on an existing object in an immutable fashion is simplified with spread. In this example, the isLoggedIn
property is added to the user
object:
const user = {
id: 3,
name: 'Ron',
}
const updatedUser = { ...user, isLoggedIn: true }
console.log(updatedUser)
This will output the following:
{id: 3, name: "Ron", isLoggedIn: true}
One important thing to note with updating objects via spread is that any nested object will have to be spread as well. For example, let's say that in the user
object there is a nested organization
object:
const user = {
id: 3,
name: 'Ron',
organization: {
name: 'Parks & Recreation',
city: 'Pawnee',
},
}
If you tried to add a new item to organization
, it would overwrite the existing fields:
const updatedUser = { ...user, organization: { position: 'Director' } }
console.log(updatedUser)
This would result in the following:
id: 3
name: "Ron"
organization: {position: "Director"}
If mutability is not an issue, the field could be updated directly:
user.organization.position = 'Director'
But since we are seeking an immutable solution, we can spread the inner object to retain the existing properties:
const updatedUser = {
...user,
organization: {
...user.organization,
position: 'Director',
},
}
console.log(updatedUser)
This will give the following:
id: 3
name: "Ron"
organization: {name: "Parks & Recreation", city: "Pawnee", position: "Director"}
Spread with Function Calls
Spread can also be used with arguments in function calls.
As an example, here is a multiply
function that takes three parameters and multiplies them:
// Create a function to multiply three items
function multiply(a, b, c) {
return a * b * c
}
Normally, you would pass three values individually as arguments to the function call, like so:
multiply(1, 2, 3)
This would give the following:
6
However, if all the values you want to pass to the function already exist in an array, the spread syntax allows you to use each item in an array as an argument:
const numbers = [1, 2, 3]
multiply(...numbers)
This will give the same result:
6
Note: Without spread, this can be accomplished by using
apply()
:
multiply.apply(null, [1, 2, 3])
This will give:
6
Now that you have seen how spread can shorten your code, you can take a look at a different use of the ...
syntax: rest parameters.
Rest Parameters
The last feature you will learn in this article is the rest parameter syntax. The syntax appears the same as spread (...
) but has the opposite effect. Instead of unpacking an array or object into individual values, the rest syntax will create an array of an indefinite number of arguments.
In the function restTest
for example, if we wanted args
to be an array composed of an indefinite number of arguments, we could have the following:
function restTest(...args) {
console.log(args)
}
restTest(1, 2, 3, 4, 5, 6)
All the arguments passed to the restTest
function are now available in the args
array:
[1, 2, 3, 4, 5, 6]
Rest syntax can be used as the only parameter or as the last parameter in the list. If used as the only parameter, it will gather all arguments, but if it's at the end of a list, it will gather every argument that is remaining, as seen in this example:
function restTest(one, two, ...args) {
console.log(one)
console.log(two)
console.log(args)
}
restTest(1, 2, 3, 4, 5, 6)
This will take the first two arguments individually, then group the rest into an array:
1
2
[3, 4, 5, 6]
In older code, the arguments
variable could be used to gather all the arguments passed through to a function:
function testArguments() {
console.log(arguments)
}
testArguments('how', 'many', 'arguments')
This would give the following output:
Arguments(3) ["how", "many", "arguments"]
However, this has a few disadvantages. First, the arguments
variable cannot be used with arrow functions.
const testArguments = () => {
console.log(arguments)
}
testArguments('how', 'many', 'arguments')
This would yield an error:
Uncaught ReferenceError: arguments is not defined
Additionally, arguments
is not a true array and cannot use methods like map
and filter
without first being converted to an array. It also will collect all arguments passed instead of just the rest of the arguments, as seen in the restTest(one, two, ...args)
example.
Rest can be used when destructuring arrays as well:
const [firstTool, ...rest] = ['hammer', 'screwdriver', 'wrench']
console.log(firstTool)
console.log(rest)
This will give:
hammer
["screwdriver", "wrench"]
Rest can also be used when destructuring objects:
const { isLoggedIn, ...rest } = { id: 1, name: 'Ben', isLoggedIn: true }
console.log(isLoggedIn)
console.log(rest)
Giving the following output:
true
{id: 1, name: "Ben"}
In this way, rest syntax provides efficient methods for gathering an indeterminate amount of items.
Conclusion
In this article, you learned about destructuring, spread syntax, and rest parameters. In summary:
- Destructuring is used to create varibles from array items or object properties.
- Spread syntax is used to unpack iterables such as arrays, objects, and function calls.
- Rest parameter syntax will create an array from an indefinite number of values.
Destructuring, rest parameters, and spread syntax are useful features in JavaScript that help keep your code succinct and clean.
If you would like to see destructuring in action, take a look at How To Customize React Components with Props, which uses this syntax to destructure data and pass it to custom front-end components. If you'd like to learn more about JavaScript, return to our How To Code in JavaScript series page.
This article was originally written for DigitalOcean.
Comments