What is spread?
It lets you expand any iterable (like an array or string) in an array or function parameters, or expand any object into another object.
By now, we’ve seen plenty of spread examples (React, Redux, etc)
Combining arrays with spreadCombining objects with spread
Let’s dive deeper into each one.
We’ll create a function called identity
that just returns whatever parameter we give it.
identity = (arg) => arg
And a simple array.
arr = [1, 2, 3]
If you call identity
with arr
, we know what’ll happen
identity returns whatever you give it
But what if you spread (can I use it as a verb?) arr
into identity
?
Wait, where’s 2 and 3? identity
’s holding out on us!
Mmm, probably not. Something else is going on here. Let’s use my favorite tool for analyzing next-gen JavaScript code: the Babel REPL.
REPL stands for Read, Evaluate, Print, Loop, meaning, “I’ll read/evaluate your code and print the result as many times as you want–like a loop.” A browser’s JavaScript console is a REPL, for example. Instant feedback.
Alright, head over https://babeljs.io/repl/ and view the REPL in all its glory. You enter code on the left, and its transformed by Babel and printed on the right.
Make sure to check the es2015 option to have your code properly transformed
Let’s add our initial code
Whoa _toConsumableArray
, what’s that? Let’s expand and restructure it.
function _toConsumableArray(arr) {
// if it's already an array
if (Array.isArray(arr)) {
// create a new array
// of the same length
var newArr = Array(arr.length);
var i = 0;
// and populate it with the
// original's contents
for (i; i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
// If it's not an array,
// turn it into one
return Array.from(arr);
}
}
Okay cool… If we have an array return a new copy, otherwise, make an array out of it. But something’s still not adding up here…
Why did identity(…arr) return 1?!
_toConsumableArray
returns an array. identity
returns whatever you give it. We should’ve gotten an array!
Look further down in the REPL’s output.
identity.apply(undefined, _toConsumableArray(arr))
Interesting…
You can use apply
to invoke a function and pass it parameters, sure…but why not just use identity(_toConsumableArray(arr))
? Isn’t that easier?
The first sentence:
The
apply()
method calls a function with a giventhis
value, andarguments
provided as an array (or an array-like object).
That makes so much sense. apply
takes an array of params and feeds it to your function one by one.
// 1
identity.apply(undefined, [1, 2, 3]);
is the same as saying this
// 1
identity(1, 2, 3);
and NOT this
// This says identity([1, 2, 3])
// which is **NOT** the same
identity(_toConsumableArray(arr));
And in JavaScript, extra parameters are thrown away.
combineWords = (one, two) => `${one} ${two}`;
// 'big sandwich'
combineWords('big', 'sandwich');
// 'now' isn't used
// still returns 'big sandwich'
combineWords('big', 'sandwich', 'now');
See that? We kept getting 'big sandwich'
because combineWords
only accepts two arguments. Any others are thrown out.
If you want identity
to return all of its arguments, use the rest syntax to get the rest of them. 👏
restIdentity = (...args) => args;
restIdentity(...arr); // [1, 2, 3]
This uses rest to capture all params in an array. Since we spread [1, 2, 3]
, Babel turned it into
restIdentity.apply(undefined, [1, 2, 3]);
or
restIdentity(1, 2, 3);
Quick recap:
- When using spread in function calls, Babel “fakes it” by wrapping your arguments in
_toConsumableArray
and invoking your function’sapply
method to…apply them. 👏 - Since
identity
only returns the first argument, passings params withapply
will only return your first argument. All others are discarded. - If you’d like
identity
to capture all parameters in a single array, use rest syntax:(…params) => params
That’s one mystery solved! So what actually happens when you combine arrays using spread? Using our earlier example:
Okay, our arrays are still wrapped in _toConsumableArray
and then concatenated to an empty array using the concat
method.
Once again, ❤️ MDN Docs:
The
concat()
method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
Nice. So however many arrays we combine, concat
returns a new array with the end result.
What about objects?
Arrays and objects aren’t the same- Captain Obvious
I’m predicting that _toConsumableArray
won’t suffice when we’re using spread on objects. Let’s see what our good friend, REPL, has to say.
I broke it
Whoops, I forgot to mention: make sure to select any of the stage-x presets.
I fixed it
So with that out of the way, let’s look at our newly generated code from when we entered bigFoot
and friends.
I see a familiar face on line 3
Look very closely at line 3…
It’s Object.assign
Seems if you’re merging objects with spread, Babel looks for Object.assign
in your browser. If Object.assign
isn’t available, it falls back to a hand-written function.
I think we’ll get two gems for the price of one today.
By understanding that custom function Babel wrote, we’ll understand object spread and Object.assign
at the same time!
Let’s restructure and play with this in DevTools
function fakeObjectAssign(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
debugger;
for (var key in source) {
debugger;
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
debugger;
return target;
}
Remember, Object.assign
’s first parameter is the target.
Object.assign({}, bill, bigFoot);
Here an empty object {}
is populated with bill
and bigFoot
’s fusion. So you get a brand new object.
{ name: 'bill', shoeSize: 9001 }
So we’d like to use fakeObjectAssign
the same way.
If you used our restructured fakeObjectAssign
above, you should be looking at a debugger
statement right now.
Let’s break this down a bit.
Our target
is a brand-new object, and each argument after that is what we’re merging together. Once the merge’s complete, stick it all into target
.
Remember how I mentioned using rest parameters to capture your arguments in an array?
restIdentity = (...args) => args;
Pre-ES6 arrow functions, we used the arguments
object.
function oldSchoolArguments() {
return arguments;
}
For fun, I’ll do
oldSchoolArguments('Hello', 'World');
It’s not quite an array but it has indices, so its iterable. We can loop through it.
That’s what fakeObjectAssign
does on line 2
for (var i = 1; i < arguments.length; i++)
Wait, but arguments
indices are zero-based, meaning the first argument’s in position 0. This loop skips the first argument!
Ahh but remember, target
is at position 0, and we don’t want to touch it until we’re done merging everything else.
Looking at line 3, arguments[i]
(the current argument) has been appropriately named source
, because according to the docs, every argument after target
is called a source object.
Now on line 7 it loops through the current source
and does an interesting check
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
hasOwnProperty
is a method on every JavaScript object that tells you whether or not that object has a certain property. Inherited properties don’t count.
bill.hasOwnProperty('name')
returns true because we directly defined name
on bill
. But what if bill
inherits a method sayName
?
function inherit(name) {
this.name = name;
inherit.prototype.sayName = function() {
return this.name;
};
}
I won’t cover prototypes in this post. If you’re unfamiliar with them, just understand that bill
will inherit the sayName
method. Further reading if you’re interested.
We’ve got the point across: hasOwnProperty
only returns true
for properties defined directly on that object.
Back to our loop:
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
If key
was defined directly on source
, copy it to target
.
That’s it.
If that property was inherited from somewhere else, we don’t want it.
Similar to our .apply
question above: why are we using .call
instead of just source.hasOwnProperty(key)
?
In a nutshell (from my understanding), it’s a safety-net against how a source
object might’ve been created. If you’d like to dig deeper, see this awesome StackOverflow answer.
Let’s finish this!
Jump to the next debugger
, and you see that target
has a name
property now. It’s bill
!
Bill! Bill! Bill!
On the next debugger
(line 8), we’re evaluating bigFoot
’s original properties
bigFoot does indeed have a shoeSize > 9000
Jump to the next debugger
and we’re on line 16, just before we return the target
object. This means we’ve finished looping through each argument
and its properties.
And bigFootBill
has been successfully created.
Quick recap:
- Spread and
Object.assign
are exactly the same regarding objects. Spread usesObject.assign
if your browser supports it. Object.assign
’s first parameter is thetarget
object, every parameter after that is asource
object to be merged into thetarget
.- Inherited properties don’t count. A
source
object’s property will only get merged if it was defined directly on thatsource
object.
That was fun. I learned several new things writing this article, and couldn’t be happier that you stuck with me through all of it.
Until next time!