DEV Community

Cover image for What Javascript Spread Operator is, How It Works and How to Use It

What Javascript Spread Operator is, How It Works and How to Use It

Alex Devero on September 14, 2020

JavaScript spread operator is one of the more popular features that were introduced in ES6. This tutorial will help you understand it. You will lea...
Collapse
 
jdforsythe profile image
Jeremy Forsythe • Edited

It doesn't actually create a deep copy.

const arr1 = [ { name: 'Abe' } ];
const arr2 = [...arr1];
arr2[0].name = 'Ben';
console.log(arr1[0].name); // Ben
Enter fullscreen mode Exit fullscreen mode

The same is true with objects. It just copies the first level of properties to the new iterable. If the values are primitives, it copies by value, but if they're objects it copies by reference.

Collapse
 
pentacular profile image
pentacular

I can't find anything about copy by reference in the ecmascript standard.

ecma-international.org/publication...

Can you show me what I'm missing?

What I do see in the standard is that the value of the object is copied, but that this is separate to the properties of the object -- is this what you're calling copy by reference?

Collapse
 
jdforsythe profile image
Jeremy Forsythe

The spec does not say it copies the value of the object, it says "keys and their values are copied onto a new object". tc39.es/proposal-object-rest-spread/

It is well know that making a "copy" of a primitive copies the value and making a "copy" of an object also copies the value, but the value is a reference to that object and not a clone of the object. This is what people refer to when they say copy "by reference" and is the source of the value vs reference debate which turns out to be a mostly semantic debate. JS copies by value but sometimes the value is a reference.

The proposal for this operator specifically calls it a "shallow clone (excluding prototype)" and it's just syntactic sugar for Object.assign() github.com/tc39/proposal-object-re...

Object.assign is 19.1.2.1 in the spec and you'll see there's no recursion and therefore it's not a deep copy/clone. It loops through the OwnPropertyKeys once and does a Get from the source and a Set on the target. It does not evaluate the OwnPropertyKeys of the value if the value is an object.

Collapse
 
alexdevero profile image
Alex Devero

Hi. I wrote about this topic of deep and shallow copies, or copy by value and copy by reference, a few months ago. Here is the link to that article: blog.alexdevero.com/shallow-deep-c...

Thread Thread
 
pentacular profile image
pentacular

Ok, I think I see the argument that you're making, but I don't think that it is sound.

A reference isn't a shallow copy -- it's a way to reach the original thing.

And pass by value doesn't imply a deep copy -- two distinct values can be used to indirectly access the same sub-structures.

You make the claim that:

This is not true for objects, including iterables. When you try to copy an object, such as array, you will not create a real copy. What will happen instead is that JavaScript will create new reference for that object. You can think about this as creating new alias or name.

Can you point me to where in the ecmascript standard it talks about creating a new reference for an object?

Or do you mean that it 'passes the value of the object, which allows access to the associated properties, making this a shallow copy of the original'?

This argument seems more reasonable -- but it doesn't involve any references or copy by reference -- it's simply copy by value, where the properties of the object are indirectly accessed by the object value.

But if you can show me where I've missed object references in the language specification, I'd be grateful.

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe

What he calls a "shallow copy" in the article, I'd call an alias.

let obj2 = obj1

This makes "obj2" a "reference" to the same object as "obj1", at the same memory address. Yes, obj2 gets a new slice of memory but only to store a reference to a memory address where the object is stored (i.e. it copies the reference memory address from obj1 to obj2). If obj1 was a primitive, though, it would be a literal copy of the value into a new memory address.

The object spread operator, or Object.assign() creates what I'd call a "shallow copy". It basically performs the "alias" operation from above on each property of obj1 into obj2

let obj2 = {...obj1}

In the case of primitive values for keys of obj1, just like the = assignment, you get a literal copy of the value. But for object values, you get the same memory address / reference / alias behavior.

The third type of copy, a deep copy / clone, is not built in to the language. This is a recursive copy where object values from obj1 are recursively evaluated in the same way as the top level properties and copied "by value" (once a primitive value is finally reached) to the corresponding place in obj2. You can fairly easily write a recursive deep copy function or use Lodash cloneDeep lodash.com/docs/#cloneDeep if that is your desire, but that's not what the spread operator does.

Thread Thread
 
pentacular profile image
pentacular
let obj2 = obj1;

This initializes obj2 with the value of obj1.

So I guess you're writing "reference" because you agree that it isn't actually a reference?

1. Let bindingId be StringValue of BindingIdentifier.
2. Let lhs be ResolveBinding(bindingId).
3. If IsAnonymousFunctionDefinition(Initializer) is true, then
a. Let value be NamedEvaluation of Initializer with argument bindingId.
4. Else,
a. Let rhs be the result of evaluating Initializer.
b. Let value be ? GetValue(rhs).
5. Return InitializeReferencedBinding(lhs, value).

What you may notice here is that there is no distinction between primitive and object types -- in both cases we get the value and initialize the variable with it.

I think the confusion in these cases comes from a mistaken belief that the properties of an object are part of the object's value -- but this clearly isn't the case in the language specification.

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe • Edited

There is a distinction. Read the GetValue definition and follow it down.

In any case, we're down a semantic hole about the language spec. My example in my first comment clearly shows that spread doesn't create a deep copy.

Thread Thread
 
pentacular profile image
pentacular
1. ReturnIfAbrupt(V).
2. If Type(V) is not Reference, return V.
3. Let base be GetBase(V).
4. If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
5. If IsPropertyReference(V) is true, then
a. If HasPrimitiveBase(V) is true, then
i. Assert: In this case, base will never be undefined or null.
ii. Set base to ! ToObject(base).
b. Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).
6. Else,
a. Assert: base is an Environment Record.
b. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).

I'm not seeing the relevant distinction -- could you point it out?

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe

No, I'm not going to read the spec for you.

Thread Thread
 
pentacular profile image
pentacular

I did the reading.

There's nothing there that distinguishes between primitive and object values.

So, I think you're mistaken and am giving you the opportunity to reconsider your claim.

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe

If you do a behavior test in any interpreter there is obviously a difference. I'm pretty sure not every JS interpreter developer is mistaken. There is a clear difference but we've gone from me saying "hey, that's not a deep copy" to you asking me to point out the intricacies of the JS spec. Sorry, read it again. Every JS interpreter dev can't be wrong and you're right.

Thread Thread
 
pentacular profile image
pentacular

What is the obvious difference?

Every JS interpreter I've used copies the value of the object exactly as I expect, given my understanding of the spec.

Noting that the spec differentiates between an object value and the properties associated with that object value.

Please clarify the behavior test that you have in mind so that it can be tested.

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe

You're moving the goal posts. I said it distinguishes between an object and primitives. That is demonstrated by the example in my first comment, which is why it's not a deep copy. Every interpreter runs my example the same way, allowing you to mutate the original using the copy.

Every interpreter since I started writing JS in the 1990s treats objects and primitives differently. There are countless articles over the last 20 years about that fact. You can't really believe that everyone is wrong. Given that, it must be you who misunderstands the spec. So I suggest you look up all the definitions of things used inside GetValue. If you look at it long enough, you'll see that it quite clearly follows a different path for primitive values than it does for objects.

Thread Thread
 
pentacular profile image
pentacular

What's the distinction that it makes?

JS doesn't allow you to mutate values at all -- and there's no difference between objects and values in that regard.

The only things you can mutate in JS are properties and variables.

(And this is one of the reasons that properties are not part of the object's value)

Thread Thread
 
fernandomk6 profile image
Fernando Henrique

Objects and primitives have their values copied when the assignment sign is used. Primitive values are literal values, object values are pointers.

Collapse
 
alexdevero profile image
Alex Devero

Thank you Jeremy for pointing this out. You are correct. Spread will create deep copy only for the top-level properties. In other cases, combination of JSON.parse() and JSON.stringify will create a deep copy.

Collapse
 
fernandomk6 profile image
Fernando Henrique

Excellent article, keep contributing to the javascript community