Skip to content

Commit 73459e6

Browse files
authored
Merge pull request #259 from odsantos/update-object-references-and-copying
Update "Object references and copying" article
2 parents cfce798 + 0b96b57 commit 73459e6

File tree

1 file changed

+96
-35
lines changed

1 file changed

+96
-35
lines changed

1-js/04-object-basics/02-object-copy/article.md

Lines changed: 96 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ And here's how it's actually stored in memory:
3737

3838
The object is stored somewhere in memory (at the right of the picture), while the `user` variable (at the left) has a "reference" to it.
3939

40-
We may think of an object variable, such as `user`, as like a sheet of paper with the address of the object on it.
40+
We may think of an object variable, such as `user`, like a sheet of paper with the address of the object on it.
4141

4242
When we perform actions with the object, e.g. take a property `user.name`, the JavaScript engine looks at what's at that address and performs the operation on the actual object.
4343

@@ -100,15 +100,37 @@ alert( a == b ); // false
100100
101101
For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to tell the truth, such comparisons are needed very rarely -- usually they appear as a result of a programming mistake.
102102
103+
````smart header="Const objects can be modified"
104+
An important side effect of storing objects as references is that an object declared as `const` *can* be modified.
105+
106+
For instance:
107+
108+
```js run
109+
const user = {
110+
name: "John"
111+
};
112+
113+
*!*
114+
user.name = "Pete"; // (*)
115+
*/!*
116+
117+
alert(user.name); // Pete
118+
```
119+
120+
It might seem that the line `(*)` would cause an error, but it does not. The value of `user` is constant, it must always reference the same object, but properties of that object are free to change.
121+
122+
In other words, the `const user` gives an error only if we try to set `user=...` as a whole.
123+
124+
That said, if we really need to make constant object properties, it's also possible, but using totally different methods. We'll mention that in the chapter <info:property-descriptors>.
125+
````
126+
103127
## Cloning and merging, Object.assign [#cloning-and-merging-object-assign]
104128

105129
So, copying an object variable creates one more reference to the same object.
106130

107-
But what if we need to duplicate an object? Create an independent copy, a clone?
108-
109-
That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. But there is rarely a need -- copying by reference is good most of the time.
131+
But what if we need to duplicate an object?
110132

111-
But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level.
133+
We can create a new object and replicate the structure of the existing one, by iterating over its properties and copying them on the primitive level.
112134

113135
Like this:
114136

@@ -133,21 +155,22 @@ clone.name = "Pete"; // changed the data in it
133155
alert( user.name ); // still John in the original object
134156
```
135157

136-
Also we can use the method [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) for that.
158+
We can also use the method [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).
137159

138160
The syntax is:
139161

140162
```js
141-
Object.assign(dest, [src1, src2, src3...])
163+
Object.assign(dest, ...sources)
142164
```
143165

144166
- The first argument `dest` is a target object.
145-
- Further arguments `src1, ..., srcN` (can be as many as needed) are source objects.
146-
- It copies the properties of all source objects `src1, ..., srcN` into the target `dest`. In other words, properties of all arguments starting from the second are copied into the first object.
147-
- The call returns `dest`.
167+
- Further arguments is a list of source objects.
148168

149-
For instance, we can use it to merge several objects into one:
150-
```js
169+
It copies the properties of all source objects into the target `dest`, and then returns it as the result.
170+
171+
For example, we have `user` object, let's add a couple of permissions to it:
172+
173+
```js run
151174
let user = { name: "John" };
152175
153176
let permissions1 = { canView: true };
@@ -159,6 +182,9 @@ Object.assign(user, permissions1, permissions2);
159182
*/!*
160183
161184
// now user = { name: "John", canView: true, canEdit: true }
185+
alert(user.name); // John
186+
alert(user.canView); // true
187+
alert(user.canEdit); // true
162188
```
163189
164190
If the copied property name already exists, it gets overwritten:
@@ -171,9 +197,9 @@ Object.assign(user, { name: "Pete" });
171197
alert(user.name); // now user = { name: "Pete" }
172198
```
173199
174-
We also can use `Object.assign` to replace `for..in` loop for simple cloning:
200+
We also can use `Object.assign` to perform a simple object cloning:
175201
176-
```js
202+
```js run
177203
let user = {
178204
name: "John",
179205
age: 30
@@ -182,15 +208,18 @@ let user = {
182208
*!*
183209
let clone = Object.assign({}, user);
184210
*/!*
211+
212+
alert(clone.name); // John
213+
alert(clone.age); // 30
185214
```
186215
187-
It copies all properties of `user` into the empty object and returns it.
216+
Here it copies all properties of `user` into the empty object and returns it.
188217
189218
There are also other methods of cloning an object, e.g. using the [spread syntax](info:rest-parameters-spread) `clone = {...user}`, covered later in the tutorial.
190219
191220
## Nested cloning
192221
193-
Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them?
222+
Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects.
194223
195224
Like this:
196225
```js run
@@ -205,9 +234,7 @@ let user = {
205234
alert( user.sizes.height ); // 182
206235
```
207236
208-
Now it's not enough to copy `clone.sizes = user.sizes`, because the `user.sizes` is an object, it will be copied by reference. So `clone` and `user` will share the same sizes:
209-
210-
Like this:
237+
Now it's not enough to copy `clone.sizes = user.sizes`, because `user.sizes` is an object, and will be copied by reference, so `clone` and `user` will share the same sizes:
211238

212239
```js run
213240
let user = {
@@ -223,42 +250,76 @@ let clone = Object.assign({}, user);
223250
alert( user.sizes === clone.sizes ); // true, same object
224251
225252
// user and clone share sizes
226-
user.sizes.width++; // change a property from one place
227-
alert(clone.sizes.width); // 51, see the result from the other one
253+
user.sizes.width = 60; // change a property from one place
254+
alert(clone.sizes.width); // 60, get the result from the other one
228255
```
229256

230-
To fix that, we should use a cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning".
257+
To fix that and make `user` and `clone` truly separate objects, we should use a cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning" or "structured cloning". There's [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) method that implements deep cloning.
231258

232-
We can use recursion to implement it. Or, to not reinvent the wheel, take an existing implementation, for instance [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) from the JavaScript library [lodash](https://lodash.com).
233259

234-
````smart header="Const objects can be modified"
235-
An important side effect of storing objects as references is that an object declared as `const` *can* be modified.
260+
### structuredClone
236261

237-
For instance:
262+
The call `structuredClone(object)` clones the `object` with all nested properties.
263+
264+
Here's how we can use it in our example:
238265
239266
```js run
240-
const user = {
241-
name: "John"
267+
let user = {
268+
name: "John",
269+
sizes: {
270+
height: 182,
271+
width: 50
272+
}
242273
};
243274
244275
*!*
245-
user.name = "Pete"; // (*)
276+
let clone = structuredClone(user);
246277
*/!*
247278
248-
alert(user.name); // Pete
279+
alert( user.sizes === clone.sizes ); // false, different objects
280+
281+
// user and clone are totally unrelated now
282+
user.sizes.width = 60; // change a property from one place
283+
alert(clone.sizes.width); // 50, not related
249284
```
250285
251-
It might seem that the line `(*)` would cause an error, but it does not. The value of `user` is constant, it must always reference the same object, but properties of that object are free to change.
286+
The `structuredClone` method can clone most data types, such as objects, arrays, primitive values.
252287
253-
In other words, the `const user` gives an error only if we try to set `user=...` as a whole.
288+
It also supports circular references, when an object property references the object itself (directly or via a chain or references).
254289
255-
That said, if we really need to make constant object properties, it's also possible, but using totally different methods. We'll mention that in the chapter <info:property-descriptors>.
256-
````
290+
For instance:
291+
292+
```js run
293+
let user = {};
294+
// let's create a circular reference:
295+
// user.me references the user itself
296+
user.me = user;
297+
298+
let clone = structuredClone(user);
299+
alert(clone.me === clone); // true
300+
```
301+
302+
As you can see, `clone.me` references the `clone`, not the `user`! So the circular reference was cloned correctly as well.
303+
304+
Although, there are cases when `structuredClone` fails.
305+
306+
For instance, when an object has a function property:
307+
308+
```js run
309+
// error
310+
structuredClone({
311+
f: function() {}
312+
});
313+
```
314+
315+
Function properties aren't supported.
316+
317+
To handle such complex cases we may need to use a combination of cloning methods, write custom code or, to not reinvent the wheel, take an existing implementation, for instance [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) from the JavaScript library [lodash](https://lodash.com).
257318
258319
## Summary
259320
260321
Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object itself.
261322
262323
All operations via copied references (like adding/removing properties) are performed on the same single object.
263324
264-
To make a "real copy" (a clone) we can use `Object.assign` for the so-called "shallow copy" (nested objects are copied by reference) or a "deep cloning" function, such as [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep).
325+
To make a "real copy" (a clone) we can use `Object.assign` for the so-called "shallow copy" (nested objects are copied by reference) or a "deep cloning" function `structuredClone` or use a custom cloning implementation, such as [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep).

0 commit comments

Comments
 (0)