If you’ve spent plenty of time wading through modern JavaScript, odds are you’ve seen enough ellipses (...
) to put even the most brooding 90s role-playing game protagonist to shame. I wouldn’t fault you for finding them a little confusing. Granted, I wouldn’t fault you for finding anything about JavaScript confusing, but I’ve always thought those ellipses were uniquely unintuitive at a glance. It doesn’t help that you’ll frequently encounter these little weirdos in the context of “destructuring assignment,” which is a strange syntax in and of itself.
A destructuring assignment allows you to extract individual values from an array or object and assign them to a set of identifiers without needing to access the values of each element the old-fashioned way—one at a time, by index or key like this:
- Code language
- js
const myArray = [ true, false, false ]; const firstElement = myArray[0]; const secondElement = myArray[1]; const thirdElement = myArray[2];
In its simplest form — called “binding pattern destructuring” — each value is unpacked from the array or object literal and assigned to a corresponding identifier, all of which are initialized with a single let
or const
(or var
, I suppose, if you’re feeling nostalgic for function-scoping).
The assigned value is the array or object literal to be destructured. When working with an array the identifiers are wrapped in a pair of brackets, and each identifier you define within those brackets will correspond to the same index in the source array:
- Code language
- js
const myArray = [10, 200, 3000 ]; const [ firstElement, secondElement, thirdElement ] = myArray;
- Code language
- output
firstElement; > 10 secondElement; > 200 thirdElement; > 3000
Elements can be skipped over by using a comma but leaving out the identifier, the way you’d leave out a value when creating a sparse array:
- Code language
- js
const myArray = [ "goose", "duck", "duck", "goose" ]; const [ firstElement, , , fourthElement ] = myArray;
- Code language
- output
firstElement; > "goose" fourthElement; > "goose"
You’ll sometimes see destructuring referred to as “unpacking” a data structure, but despite how that might sound, keep in mind that destructuring doesn’t modify the original array or object:
- Code language
- js
const myArray = [ "first", "second", "third" ]; const [ startElement, middleElement, endElement ] = myArray;
- Code language
- output
myArray; > Array(3) [ "first", "second", "third" ]
Destructuring: not just for arrayspermalink
Now, you’re not often going to create a data structure and then immediately assign the values of the elements it contains to a bunch of identifiers like this, but now and then you are going to have to grab the contents of a data structure put together elsewhere in a script in order to use or manipulate those values. For example, say an API provided you with an object literal containing information about an image that you want to use to construct an img
element:
- Code language
- js
const myImage = { "src": "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs", "alt": "A single black pixel.", "size": { "width": 600, "height": 400 } };
Pulling this object apart isn’t the most onerous task in all of web development, sure, but it feels a little clunky doing it one line at a time:
- Code language
- js
const imgContainer = document.querySelector( ".img-container" ); const src = myImage.src; const alt = myImage.alt || ""; const width = myImage.size.width || 800; const height = myImage.size.height || 400; if(imgSource) { imgContainer.innerHTML = `<img src="${ src }" alt="${ alt }" height="${ height }" width="${ width }">`; }
You can destructure objects just like you can destructure an array, with a few differences in syntax. First, the identifiers are wrapped in a pair of curly braces rather than brackets. Second, the identifiers are populated with the values of the corresponding object keys, regardless of the order in which they’re specified:
- Code language
- js
const { alt, size, src } = myImage; const { height, width } = size;
- Code language
- output
alt; > "A single black pixel."
And just like assigning these values to identifiers using dot notation, you can set default values that will be assigned if a property isn’t present at all, or it contains an explicit undefined
value:
- Code language
- js
const { size, src, alt = "" } = myImage; const { width = 800, height = 450 } = size;
We can make this even more concise. We don’t have to unpack the nested size
object separately; we can unpack it at the same time.
- Code language
- js
const { src, alt = "", size: { width = 800, height = 450 } } = myImage;
Which leaves us with this updated code:
- Code language
- js
const imgContainer = document.querySelector( ".img-container" ); const { src, alt = "", size: { width = 800, height = 450 } } = myImage; if(imgSource) { imgContainer.innerHTML = `<img src="${ src }" alt="${ alt }" height="${ height }" width="${ width }">`; }
All told, the destructuring assignment provides you with a quick and convenient way to break down complex data structures, but it definitely isn’t the most approachable syntax for how dense it is.
So where does the ellipsis come in?permalink
Well, in the context of a destructuring assignment, an ellipsis followed by an identifier represents a “rest property” — an identifier that will contain the rest of the array or object being unpacked as a new array or object. This rest property will contain all the remaining elements beyond the ones we’ve explicitly unpacked to their own identifiers, all bundled up in the same kind of data structure as the one we unpacked:
- Code language
- js
const myArray = [ false, true, false ]; const [ firstElement, ...remainingElements ] = myArray;
- Code language
- output
firstElement; > false remainingElements; > Array [ true, false ]
Another example:
- Code language
- js
const myObject = { "key1": "first value", "key2": "second value", "key3": "third value" }; const { key1, ...otherProperties } = myObject;
- Code language
- output
key1; > "first value" otherProperties; > Object { key2: "second value", key3: "third value" }
Like the destructuring assignment itself, rest properties probably don’t seem all that useful in a vacuum like this — again, we’re pulling apart a simple data structure that we only just put together. Where it becomes really useful is working with large data structures that you don’t necessarily control.
For example, say you were working with the output of a static site generator for a single page:
- Code language
- js
const postData = { inputPath: './index.md', url: '/', lede: "This is the introduction to the post", date: new Date(), title: 'My Title', postId: 25, tags: ['tag1', 'tag2'], body: 'This is the body of the post' }
This object addresses two concerns, all mingled together: there’s meta information about the post — the path the file that generated the post, the path where the generated post will live, an ID for the post, the tags associated with the post — and the content that makes up the post itself. Odds are, we’ll need all of this information, but accessing each property as-needed would be repetitive so we’ll use destructuring syntax to grab the meta information we need and retain all the rest of the properties — the post content itself — as a new object:
- Code language
- js
const { inputPath, url, postId, tags, ...postContent } = postData;
- Code language
- output
postContent; > Object { lede: "This is the introduction to the post", date: Date Fri Aug 23 2024 14:05:19 GMT-0400 (Eastern Daylight Time), title: "My Title", body: "This is the body of the post" }
One line! No need to grab each property value and assign it to an identifier independently, no need to continually access a big unwieldy object throughout a script, and all the properties that make up the post itself bundled up in a tidy new object. Minimal fuss and hardly any muss.
The rest and spread operatorspermalink
Rest
You’ll most frequently run into the rest operator (...
) in a destructuring assignment, but like an indecisive text-messager, JavaScript is going to present you with ellipses in a few unexpected places. Those uses all have something in common with the one you’ve come to know from destructuring: they’re all concerned with aggregating data into, or spreading data out from, a data structure.
In front of the identifier for a function parameter, an ellipsis performs the same function as it does when performing destructuring assignment: as a “rest operator,” it bundles up all the rest of the arguments passed to this function as an iterable data structure — an array — and assign it the identifier that follows the ellipsis. This allows you to create “variadic functions,” a sure-to-impress term that really just means “a function that can accept any number of arguments.”
- Code language
- js
function myFunction( firstParameter, ...remainingParameters ) { };
Spread
The last place you’ll run into an ellipsis (apart from especially emotionally-charged comments, that is) is something altogether different. In contexts where array elements or a function’s arguments are expected, that same ellipsis will take on an entirely different name and use: the “spread operator” (...
), which expands an iterable data structure—an array, an object literal, or even a string — into its individual elements.
The most common uses of the spread operator are copying and combining arrays:
- Code language
- js
const myArray = [ 4, 5, 6 ]; const myMergedArray = [1, 2, 3, ...myArray ];
- Code language
- output
myMergedArray; > Array(6) [ 1, 2, 3, 4, 5, 6 ]
Now, again, remember that spread syntax applies only where arguments in a function call or elements of an array are expected. As you saw in the example above, an array pretty predictably accepts elements from an array. Less predictably, so does an object literal:
- Code language
- js
const myArray = [ true, false ]; const myObject = { ...myArray };
- Code language
- output
myObject; > Object { 0: true, 1: false }
It’s a safe bet that you’ll never find yourself in a situation where you need to spread the contents of a data structure into… well, nothing. If you were to try, say, for the sake of an example in a blog post about JavaScript’s many ellipses:
- Code language
- js
const myArray = [ 1, 2, 3 ];
- Code language
- output
...myArray; > Uncaught SyntaxError: expected expression, got '...'
No dice. But if I’d used that same syntax inside of a console.log
, then I’d be using the spread operator in the context of an argument to the console.log
method, so it does work:
- Code language
- js
const myArray = [ 1, 2, 3 ]; console.log( ...myArray );
- Code language
- output
> 1 2 3
Object Spread
Using a spread operator with object literals is a more recent addition to JavaScript: while the spread operator itself was added in 2015’s ES6, it only applies to object literals as of ES2018. The spread operator creates “shallow” copies of an object. That is, it will spread a value’s “own properties” — that is, any enumerable properties not inherited by way of the prototype chain — into a new object.
- Code language
- js
const oldObject = { "key1": "first value", "key2": "second value", "key3": "third value" }; const myObject = { "key0": "zeroth value", ...oldObject };
- Code language
- output
myObject; > Object { key0: "zeroth value", key1: "first value", key2: "second value", key3: "third value" }
This is an unbelievably useful syntax, allowing you to copy and merge objects with just a few characters.
A few things to keep in mind: when merging arrays that contain duplicate keys, the values associated with those keys will be overwritten:
- Code language
- js
const firstObject = { "key1" : "first value", "key2" : "second value", "key3" : "third value" }; const secondObject = { "key0" : "zeroth value", "key1" : "another value" }; const myObject = { ...firstObject, ...secondObject }
- Code language
- output
myObject; > Object { key1: "another value", key2: "second value", key3: "third value", key0: "zeroth value" }
Also, because an object isn’t iterable in the same way an array or string is, the context for object spread isn’t quite the same — while arrays and strings can be spread into an object, an array, or across a function’s arguments, an object can only be spread into another object:
- Code language
- js
const myObject = { "key1": "first value", "key2": "second value", "key3": "third value" }; console.log( ...myObject );
- Code language
- output
> Uncaught SyntaxError: expected expression, got '...'
Bringing it all together
Once you’ve got the hang of all these syntaxes, it isn’t hard to see how a script file might end up with more ellipses than a LiveJournal post circa 2005.
Let’s revisit both of our closer-to-reality examples, for both destructuring and spread syntax: an object containing information about an image we want to render, and an object containing a bunch of information about a single blog post.
- Code language
- js
const apiPost = { inputPath: './index.md', url: '/', lede: "This is the introduction to the post", date: new Date(), title: 'My Title', postId: 25, tags: ['tag1', 'tag2'], body: 'This is the body of the post' }; const apiImage = { "src": "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs", "alt": "...", "size": { "width": 600, "height": 400 } };
We could work with these objects the way they are, right out of the box, but that’s a lot to wade through. Instead, we’ll use destructuring syntax to break down the postData
object into two separate concerns, the way we did earlier: the post’s meta information, and the body of the post that we’ll use to populate the rendered page.
- Code language
- js
const { inputPath, url, postId, tags, ...postContent } = apiPost;
- Code language
- output
postContent; > Object { lede: "This is the introduction to the post", date: Date Fri Sep 13 2024 15:39:50 GMT-0400 (Eastern Daylight Time), title: "My Title", body: "This is the body of the post" }
Now, let’s use use object spread to combine the newly-created postContent
object with the object containing information about our image, but with one addition: since we’ll be putting this image up at the top of the page and we don’t want to run afoul of any issues with Largest Contentful Paint, we may want to render this with an explicit loading="eager"
attribute — we’ll add that to the object representing the image data as well.
- Code language
- js
const myPost = { ...postContent, "heroImg": { "loading": "eager", ...apiImage } };
And in the end, we’re left with an object that contains only the properties we need, including a quick addition of our own — one-stop shopping for all our blog-post-rendering needs:
- Code language
- output
myPost; > Object { lede: "This is the introduction to the post", date: Date Fri Sep 13 2024 13:05:12 GMT-0400 (Eastern Daylight Time), title: "My Title", body: "This is the body of the post", heroImg: { alt: "...", loading: "eager", size: Object { width: 600, height: 400 }, src: "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs" } }
It’s much easier to work with than line-after-line of dot notation and plucking properties one-by-one out of whatever disorganized objects have been foisted upon us. And the downsides, you ask? I mean… I guess our code might end up reading a little more… well… nah, nevermind…