refactor objects, add optional chaining
|
@ -1,19 +0,0 @@
|
|||
Sure, it works, no problem.
|
||||
|
||||
The `const` only protects the variable itself from changing.
|
||||
|
||||
In other words, `user` stores a reference to the object. And it can't be changed. But the content of the object can.
|
||||
|
||||
```js run
|
||||
const user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
*!*
|
||||
// works
|
||||
user.name = "Pete";
|
||||
*/!*
|
||||
|
||||
// error
|
||||
user = 123;
|
||||
```
|
|
@ -1,18 +0,0 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Constant objects?
|
||||
|
||||
Is it possible to change an object declared with `const`? What do you think?
|
||||
|
||||
```js
|
||||
const user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
*!*
|
||||
// does it work?
|
||||
user.name = "Pete";
|
||||
*/!*
|
||||
```
|
|
@ -92,6 +92,30 @@ let user = {
|
|||
```
|
||||
That is called a "trailing" or "hanging" comma. Makes it easier to add/remove/move around properties, because all lines become alike.
|
||||
|
||||
````smart header="Object with const can be changed"
|
||||
Please note: an object declared as `const` *can* be modified.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
const user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
*!*
|
||||
user.name = "Pete"; // (*)
|
||||
*/!*
|
||||
|
||||
alert(user.name); // Pete
|
||||
```
|
||||
|
||||
It might seem that the line `(*)` would cause an error, but no. The `const` fixes the value of `user`, but not its contents.
|
||||
|
||||
The `const` would give an error only if we try to set `user=...` as a whole.
|
||||
|
||||
There's another way to make constant object properties, we'll cover it later in the chapter <info:property-descriptors>.
|
||||
````
|
||||
|
||||
## Square brackets
|
||||
|
||||
For multiword properties, the dot access doesn't work:
|
||||
|
@ -161,7 +185,7 @@ alert( user.key ) // undefined
|
|||
|
||||
### Computed properties
|
||||
|
||||
We can use square brackets in an object literal. That's called *computed properties*.
|
||||
We can use square brackets in an object literal, when creating an object. That's called *computed properties*.
|
||||
|
||||
For instance:
|
||||
|
||||
|
@ -249,9 +273,25 @@ let user = {
|
|||
};
|
||||
```
|
||||
|
||||
|
||||
## Property names limitations
|
||||
|
||||
Property names (keys) must be either strings or symbols (a special type for identifiers, to be covered later).
|
||||
As we already know, a variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc.
|
||||
|
||||
But for an object property, there's no such restriction:
|
||||
|
||||
```js run
|
||||
// these properties are all right
|
||||
let obj = {
|
||||
for: 1,
|
||||
let: 2,
|
||||
return: 3
|
||||
};
|
||||
|
||||
alert( obj.for + obj.let + obj.return ); // 6
|
||||
```
|
||||
|
||||
In short, there are no limitations on property names. They can be any strings or symbols (a special type for identifiers, to be covered later).
|
||||
|
||||
Other types are automatically converted to strings.
|
||||
|
||||
|
@ -267,25 +307,7 @@ alert( obj["0"] ); // test
|
|||
alert( obj[0] ); // test (same property)
|
||||
```
|
||||
|
||||
**Reserved words are allowed as property names.**
|
||||
|
||||
As we already know, a variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc.
|
||||
|
||||
But for an object property, there's no such restriction. Any name is fine:
|
||||
|
||||
```js run
|
||||
let obj = {
|
||||
for: 1,
|
||||
let: 2,
|
||||
return: 3
|
||||
};
|
||||
|
||||
alert( obj.for + obj.let + obj.return ); // 6
|
||||
```
|
||||
|
||||
We can use any string as a key, but there's a special property named `__proto__` that gets special treatment for historical reasons.
|
||||
|
||||
For instance, we can't set it to a non-object value:
|
||||
There's a minor gotcha with a special property named `__proto__`. We can't set it to a non-object value:
|
||||
|
||||
```js run
|
||||
let obj = {};
|
||||
|
@ -295,19 +317,13 @@ alert(obj.__proto__); // [object Object] - the value is an object, didn't work a
|
|||
|
||||
As we see from the code, the assignment to a primitive `5` is ignored.
|
||||
|
||||
The nature of `__proto__` will be revealed in detail later in the chapter [](info:prototype-inheritance).
|
||||
|
||||
As for now, it's important to know that such behavior of `__proto__` can become a source of bugs and even vulnerabilities if we intend to store user-provided keys in an object.
|
||||
|
||||
The problem is that a visitor may choose `__proto__` as the key, and the assignment logic will be ruined (as shown above).
|
||||
|
||||
There are two workarounds for the problem:
|
||||
1. Modify the object's behavior to treat `__proto__` as a regular property. We'll learn how to do it in the chapter [](info:prototype-methods).
|
||||
2. Using [Map](info:map-set) data structure which supports arbitrary keys. We'll learn it in the chapter <info:map-set>.
|
||||
We'll cover the special nature of `__proto__` in [subsequent chapters](info:prototype-inheritance), and suggest the [ways to fix](info:prototype-methods) such behavior.
|
||||
|
||||
## Property existence test, "in" operator
|
||||
|
||||
A notable objects feature is that it's possible to access any property. There will be no error if the property doesn't exist! Accessing a non-existing property just returns `undefined`. It provides a very common way to test whether the property exists -- to get it and compare vs undefined:
|
||||
A notable feature of objects in JavaScript, compared to many other languages, is that it's possible to access any property. There will be no error if the property doesn't exist!
|
||||
|
||||
Reading a non-existing property just returns `undefined`. So we can easily test whether the property exists:
|
||||
|
||||
```js run
|
||||
let user = {};
|
||||
|
@ -315,7 +331,7 @@ let user = {};
|
|||
alert( user.noSuchProperty === undefined ); // true means "no such property"
|
||||
```
|
||||
|
||||
There also exists a special operator `"in"` to check for the existence of a property.
|
||||
There's also a special operator `"in"` for that.
|
||||
|
||||
The syntax is:
|
||||
```js
|
||||
|
@ -333,17 +349,18 @@ alert( "blabla" in user ); // false, user.blabla doesn't exist
|
|||
|
||||
Please note that on the left side of `in` there must be a *property name*. That's usually a quoted string.
|
||||
|
||||
If we omit quotes, that would mean a variable containing the actual name will be tested. For instance:
|
||||
If we omit quotes, that means a variable, it should contain the actual name to be tested. For instance:
|
||||
|
||||
```js run
|
||||
let user = { age: 30 };
|
||||
|
||||
let key = "age";
|
||||
alert( *!*key*/!* in user ); // true, takes the name from key and checks for such property
|
||||
alert( *!*key*/!* in user ); // true, property "age" exists
|
||||
```
|
||||
|
||||
````smart header="Using \"in\" for properties that store `undefined`"
|
||||
Usually, the strict comparison `"=== undefined"` check the property existence just fine. But there's a special case when it fails, but `"in"` works correctly.
|
||||
Why does the `in` operator exist? Isn't it enough to compare against `undefined`?
|
||||
|
||||
Well, most of the time the comparison with `undefined` works fine. But there's But there's a special case when it fails, but `"in"` works correctly.
|
||||
|
||||
It's when an object property exists, but stores `undefined`:
|
||||
|
||||
|
@ -357,11 +374,10 @@ alert( obj.test ); // it's undefined, so - no such property?
|
|||
alert( "test" in obj ); // true, the property does exist!
|
||||
```
|
||||
|
||||
|
||||
In the code above, the property `obj.test` technically exists. So the `in` operator works right.
|
||||
|
||||
Situations like this happen very rarely, because `undefined` is usually not assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code.
|
||||
````
|
||||
Situations like this happen very rarely, because `undefined` should not be explicitly assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code.
|
||||
|
||||
|
||||
## The "for..in" loop
|
||||
|
||||
|
@ -396,7 +412,6 @@ Note that all "for" constructs allow us to declare the looping variable inside t
|
|||
|
||||
Also, we could use another variable name here instead of `key`. For instance, `"for (let prop in obj)"` is also widely used.
|
||||
|
||||
|
||||
### Ordered like an object
|
||||
|
||||
Are objects ordered? In other words, if we loop over an object, do we get all properties in the same order they were added? Can we rely on this?
|
||||
|
@ -480,262 +495,6 @@ for (let code in codes) {
|
|||
|
||||
Now it works as intended.
|
||||
|
||||
## Copying by reference
|
||||
|
||||
One of the fundamental differences of objects vs primitives is that they are stored and copied "by reference".
|
||||
|
||||
Primitive values: strings, numbers, booleans -- are assigned/copied "as a whole value".
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
let message = "Hello!";
|
||||
let phrase = message;
|
||||
```
|
||||
|
||||
As a result we have two independent variables, each one is storing the string `"Hello!"`.
|
||||
|
||||

|
||||
|
||||
Objects are not like that.
|
||||
|
||||
**A variable stores not the object itself, but its "address in memory", in other words "a reference" to it.**
|
||||
|
||||
Here's the picture for the object:
|
||||
|
||||
```js
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it.
|
||||
|
||||
**When an object variable is copied -- the reference is copied, the object is not duplicated.**
|
||||
|
||||
If we imagine an object as a cabinet, then a variable is a key to it. Copying a variable duplicates the key, but not the cabinet itself.
|
||||
|
||||
For instance:
|
||||
|
||||
```js no-beautify
|
||||
let user = { name: "John" };
|
||||
|
||||
let admin = user; // copy the reference
|
||||
```
|
||||
|
||||
Now we have two variables, each one with the reference to the same object:
|
||||
|
||||

|
||||
|
||||
We can use any variable to access the cabinet and modify its contents:
|
||||
|
||||
```js run
|
||||
let user = { name: 'John' };
|
||||
|
||||
let admin = user;
|
||||
|
||||
*!*
|
||||
admin.name = 'Pete'; // changed by the "admin" reference
|
||||
*/!*
|
||||
|
||||
alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference
|
||||
```
|
||||
|
||||
The example above demonstrates that there is only one object. As if we had a cabinet with two keys and used one of them (`admin`) to get into it. Then, if we later use the other key (`user`) we would see changes.
|
||||
|
||||
### Comparison by reference
|
||||
|
||||
The equality `==` and strict equality `===` operators for objects work exactly the same.
|
||||
|
||||
**Two objects are equal only if they are the same object.**
|
||||
|
||||
For instance, if two variables reference the same object, they are equal:
|
||||
|
||||
```js run
|
||||
let a = {};
|
||||
let b = a; // copy the reference
|
||||
|
||||
alert( a == b ); // true, both variables reference the same object
|
||||
alert( a === b ); // true
|
||||
```
|
||||
|
||||
And here two independent objects are not equal, even though both are empty:
|
||||
|
||||
```js run
|
||||
let a = {};
|
||||
let b = {}; // two independent objects
|
||||
|
||||
alert( a == b ); // false
|
||||
```
|
||||
|
||||
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 necessary very rarely and usually are a result of a coding mistake.
|
||||
|
||||
### Const object
|
||||
|
||||
An object declared as `const` *can* be changed.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
const user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
*!*
|
||||
user.age = 25; // (*)
|
||||
*/!*
|
||||
|
||||
alert(user.age); // 25
|
||||
```
|
||||
|
||||
It might seem that the line `(*)` would cause an error, but no, there's totally no problem. That's because `const` fixes only value of `user` itself. And here `user` stores the reference to the same object all the time. The line `(*)` goes *inside* the object, it doesn't reassign `user`.
|
||||
|
||||
The `const` would give an error if we try to set `user` to something else, for instance:
|
||||
|
||||
```js run
|
||||
const user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
*!*
|
||||
// Error (can't reassign user)
|
||||
*/!*
|
||||
user = {
|
||||
name: "Pete"
|
||||
};
|
||||
```
|
||||
|
||||
...But what if we want to make constant object properties? So that `user.age = 25` would give an error. That's possible too. We'll cover it in the chapter <info:property-descriptors>.
|
||||
|
||||
## Cloning and merging, Object.assign
|
||||
|
||||
So, copying an object variable creates one more reference to the same object.
|
||||
|
||||
But what if we need to duplicate an object? Create an independent copy, a clone?
|
||||
|
||||
That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed. Copying by reference is good most of the time.
|
||||
|
||||
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.
|
||||
|
||||
Like this:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
age: 30
|
||||
};
|
||||
|
||||
*!*
|
||||
let clone = {}; // the new empty object
|
||||
|
||||
// let's copy all user properties into it
|
||||
for (let key in user) {
|
||||
clone[key] = user[key];
|
||||
}
|
||||
*/!*
|
||||
|
||||
// now clone is a fully independent clone
|
||||
clone.name = "Pete"; // changed the data in it
|
||||
|
||||
alert( user.name ); // still John in the original object
|
||||
```
|
||||
|
||||
Also we can use the method [Object.assign](mdn:js/Object/assign) for that.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
Object.assign(dest, [src1, src2, src3...])
|
||||
```
|
||||
|
||||
- Arguments `dest`, and `src1, ..., srcN` (can be as many as needed) are objects.
|
||||
- It copies the properties of all objects `src1, ..., srcN` into `dest`. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns `dest`.
|
||||
|
||||
For instance, we can use it to merge several objects into one:
|
||||
```js
|
||||
let user = { name: "John" };
|
||||
|
||||
let permissions1 = { canView: true };
|
||||
let permissions2 = { canEdit: true };
|
||||
|
||||
*!*
|
||||
// copies all properties from permissions1 and permissions2 into user
|
||||
Object.assign(user, permissions1, permissions2);
|
||||
*/!*
|
||||
|
||||
// now user = { name: "John", canView: true, canEdit: true }
|
||||
```
|
||||
|
||||
If the receiving object (`user`) already has the same named property, it will be overwritten:
|
||||
|
||||
```js
|
||||
let user = { name: "John" };
|
||||
|
||||
// overwrite name, add isAdmin
|
||||
Object.assign(user, { name: "Pete", isAdmin: true });
|
||||
|
||||
// now user = { name: "Pete", isAdmin: true }
|
||||
```
|
||||
|
||||
We also can use `Object.assign` to replace the loop for simple cloning:
|
||||
|
||||
```js
|
||||
let user = {
|
||||
name: "John",
|
||||
age: 30
|
||||
};
|
||||
|
||||
*!*
|
||||
let clone = Object.assign({}, user);
|
||||
*/!*
|
||||
```
|
||||
|
||||
It copies all properties of `user` into the empty object and returns it. Actually, the same as the loop, but shorter.
|
||||
|
||||
Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them?
|
||||
|
||||
Like this:
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
sizes: {
|
||||
height: 182,
|
||||
width: 50
|
||||
}
|
||||
};
|
||||
|
||||
alert( user.sizes.height ); // 182
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
Like this:
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
sizes: {
|
||||
height: 182,
|
||||
width: 50
|
||||
}
|
||||
};
|
||||
|
||||
let clone = Object.assign({}, user);
|
||||
|
||||
alert( user.sizes === clone.sizes ); // true, same object
|
||||
|
||||
// user and clone share sizes
|
||||
user.sizes.width++; // change a property from one place
|
||||
alert(clone.sizes.width); // 51, see the result from the other one
|
||||
```
|
||||
|
||||
To fix that, we should use the 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".
|
||||
|
||||
There's a standard algorithm for deep cloning that handles the case above and more complex cases, called the [Structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#safe-passing-of-structured-data). In order not to reinvent the wheel, we can use a working implementation of it from the JavaScript library [lodash](https://lodash.com), the method is called [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep).
|
||||
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Objects are associative arrays with several special features.
|
||||
|
@ -753,10 +512,6 @@ Additional operators:
|
|||
- To check if a property with the given key exists: `"key" in obj`.
|
||||
- To iterate over an object: `for (let key in obj)` loop.
|
||||
|
||||
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. All operations via copied references (like adding/removing properties) are performed on the same single object.
|
||||
|
||||
To make a "real copy" (a clone) we can use `Object.assign` or [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep).
|
||||
|
||||
What we've studied in this chapter is called a "plain object", or just `Object`.
|
||||
|
||||
There are many other kinds of objects in JavaScript:
|
||||
|
|
228
1-js/04-object-basics/02-object-copy/article.md
Normal file
|
@ -0,0 +1,228 @@
|
|||
# Object copying, references
|
||||
|
||||
One of the fundamental differences of objects vs primitives is that they are stored and copied "by reference".
|
||||
|
||||
Primitive values: strings, numbers, booleans -- are assigned/copied "as a whole value".
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
let message = "Hello!";
|
||||
let phrase = message;
|
||||
```
|
||||
|
||||
As a result we have two independent variables, each one is storing the string `"Hello!"`.
|
||||
|
||||

|
||||
|
||||
Objects are not like that.
|
||||
|
||||
**A variable stores not the object itself, but its "address in memory", in other words "a reference" to it.**
|
||||
|
||||
Here's the picture for the object:
|
||||
|
||||
```js
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it.
|
||||
|
||||
**When an object variable is copied -- the reference is copied, the object is not duplicated.**
|
||||
|
||||
For instance:
|
||||
|
||||
```js no-beautify
|
||||
let user = { name: "John" };
|
||||
|
||||
let admin = user; // copy the reference
|
||||
```
|
||||
|
||||
Now we have two variables, each one with the reference to the same object:
|
||||
|
||||

|
||||
|
||||
We can use any variable to access the object and modify its contents:
|
||||
|
||||
```js run
|
||||
let user = { name: 'John' };
|
||||
|
||||
let admin = user;
|
||||
|
||||
*!*
|
||||
admin.name = 'Pete'; // changed by the "admin" reference
|
||||
*/!*
|
||||
|
||||
alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference
|
||||
```
|
||||
|
||||
The example above demonstrates that there is only one object. As if we had a cabinet with two keys and used one of them (`admin`) to get into it. Then, if we later use another key (`user`) we can see changes.
|
||||
|
||||
## Comparison by reference
|
||||
|
||||
The equality `==` and strict equality `===` operators for objects work exactly the same.
|
||||
|
||||
**Two objects are equal only if they are the same object.**
|
||||
|
||||
Here two variables reference the same object, thus they are equal:
|
||||
|
||||
```js run
|
||||
let a = {};
|
||||
let b = a; // copy the reference
|
||||
|
||||
alert( a == b ); // true, both variables reference the same object
|
||||
alert( a === b ); // true
|
||||
```
|
||||
|
||||
And here two independent objects are not equal, even though both are empty:
|
||||
|
||||
```js run
|
||||
let a = {};
|
||||
let b = {}; // two independent objects
|
||||
|
||||
alert( a == b ); // false
|
||||
```
|
||||
|
||||
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 occur very rarely, usually as a result of a coding mistake.
|
||||
|
||||
## Cloning and merging, Object.assign
|
||||
|
||||
So, copying an object variable creates one more reference to the same object.
|
||||
|
||||
But what if we need to duplicate an object? Create an independent copy, a clone?
|
||||
|
||||
That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed. Copying by reference is good most of the time.
|
||||
|
||||
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.
|
||||
|
||||
Like this:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
age: 30
|
||||
};
|
||||
|
||||
*!*
|
||||
let clone = {}; // the new empty object
|
||||
|
||||
// let's copy all user properties into it
|
||||
for (let key in user) {
|
||||
clone[key] = user[key];
|
||||
}
|
||||
*/!*
|
||||
|
||||
// now clone is a fully independent object with the same content
|
||||
clone.name = "Pete"; // changed the data in it
|
||||
|
||||
alert( user.name ); // still John in the original object
|
||||
```
|
||||
|
||||
Also we can use the method [Object.assign](mdn:js/Object/assign) for that.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
Object.assign(dest, [src1, src2, src3...])
|
||||
```
|
||||
|
||||
- The first argument `dest` is a target object.
|
||||
- Further arguments `src1, ..., srcN` (can be as many as needed) are source objects.
|
||||
- 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.
|
||||
- The call returns `dest`.
|
||||
|
||||
For instance, we can use it to merge several objects into one:
|
||||
```js
|
||||
let user = { name: "John" };
|
||||
|
||||
let permissions1 = { canView: true };
|
||||
let permissions2 = { canEdit: true };
|
||||
|
||||
*!*
|
||||
// copies all properties from permissions1 and permissions2 into user
|
||||
Object.assign(user, permissions1, permissions2);
|
||||
*/!*
|
||||
|
||||
// now user = { name: "John", canView: true, canEdit: true }
|
||||
```
|
||||
|
||||
If the copied property name already exists, it gets overwritten:
|
||||
|
||||
```js run
|
||||
let user = { name: "John" };
|
||||
|
||||
Object.assign(user, { name: "Pete" });
|
||||
|
||||
alert(user.name); // now user = { name: "Pete" }
|
||||
```
|
||||
|
||||
We also can use `Object.assign` to replace `for..in` loop for simple cloning:
|
||||
|
||||
```js
|
||||
let user = {
|
||||
name: "John",
|
||||
age: 30
|
||||
};
|
||||
|
||||
*!*
|
||||
let clone = Object.assign({}, user);
|
||||
*/!*
|
||||
```
|
||||
|
||||
It copies all properties of `user` into the empty object and returns it.
|
||||
|
||||
## Nested cloning
|
||||
|
||||
Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them?
|
||||
|
||||
Like this:
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
sizes: {
|
||||
height: 182,
|
||||
width: 50
|
||||
}
|
||||
};
|
||||
|
||||
alert( user.sizes.height ); // 182
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
Like this:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
sizes: {
|
||||
height: 182,
|
||||
width: 50
|
||||
}
|
||||
};
|
||||
|
||||
let clone = Object.assign({}, user);
|
||||
|
||||
alert( user.sizes === clone.sizes ); // true, same object
|
||||
|
||||
// user and clone share sizes
|
||||
user.sizes.width++; // change a property from one place
|
||||
alert(clone.sizes.width); // 51, see the result from the other one
|
||||
```
|
||||
|
||||
To fix that, we should use the 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".
|
||||
|
||||
There's a standard algorithm for deep cloning that handles the case above and more complex cases, called the [Structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#safe-passing-of-structured-data).
|
||||
|
||||
We can use recursion to implement it. Or, not to reinvent the wheel, take an existing implementation, for instance [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) from the JavaScript library [lodash](https://lodash.com).
|
||||
|
||||
## Summary
|
||||
|
||||
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.
|
||||
|
||||
All operations via copied references (like adding/removing properties) are performed on the same single object.
|
||||
|
||||
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).
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -233,98 +233,6 @@ The concept of run-time evaluated `this` has both pluses and minuses. On the one
|
|||
Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and avoid problems.
|
||||
```
|
||||
|
||||
## Internals: Reference Type
|
||||
|
||||
```warn header="In-depth language feature"
|
||||
This section covers an advanced topic, to understand certain edge-cases better.
|
||||
|
||||
If you want to go on faster, it can be skipped or postponed.
|
||||
```
|
||||
|
||||
An intricate method call can lose `this`, for instance:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
hi() { alert(this.name); },
|
||||
bye() { alert("Bye"); }
|
||||
};
|
||||
|
||||
user.hi(); // John (the simple call works)
|
||||
|
||||
*!*
|
||||
// now let's call user.hi or user.bye depending on the name
|
||||
(user.name == "John" ? user.hi : user.bye)(); // Error!
|
||||
*/!*
|
||||
```
|
||||
|
||||
On the last line there is a conditional operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`.
|
||||
|
||||
Then the method is immediately called with parentheses `()`. But it doesn't work correctly!
|
||||
|
||||
As you can see, the call results in an error, because the value of `"this"` inside the call becomes `undefined`.
|
||||
|
||||
This works (object dot method):
|
||||
```js
|
||||
user.hi();
|
||||
```
|
||||
|
||||
This doesn't (evaluated method):
|
||||
```js
|
||||
(user.name == "John" ? user.hi : user.bye)(); // Error!
|
||||
```
|
||||
|
||||
Why? If we want to understand why it happens, let's get under the hood of how `obj.method()` call works.
|
||||
|
||||
Looking closely, we may notice two operations in `obj.method()` statement:
|
||||
|
||||
1. First, the dot `'.'` retrieves the property `obj.method`.
|
||||
2. Then parentheses `()` execute it.
|
||||
|
||||
So, how does the information about `this` get passed from the first part to the second one?
|
||||
|
||||
If we put these operations on separate lines, then `this` will be lost for sure:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
hi() { alert(this.name); }
|
||||
}
|
||||
|
||||
*!*
|
||||
// split getting and calling the method in two lines
|
||||
let hi = user.hi;
|
||||
hi(); // Error, because this is undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`.
|
||||
|
||||
**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).**
|
||||
|
||||
The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language.
|
||||
|
||||
The value of Reference Type is a three-value combination `(base, name, strict)`, where:
|
||||
|
||||
- `base` is the object.
|
||||
- `name` is the property name.
|
||||
- `strict` is true if `use strict` is in effect.
|
||||
|
||||
The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is:
|
||||
|
||||
```js
|
||||
// Reference Type value
|
||||
(user, "hi", true)
|
||||
```
|
||||
|
||||
When parentheses `()` are called on the Reference Type, they receive the full information about the object and its method, and can set the right `this` (`=user` in this case).
|
||||
|
||||
Reference type is a special "intermediary" internal type, with the purpose to pass information from dot `.` to calling parentheses `()`.
|
||||
|
||||
Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "loses" `this`.
|
||||
|
||||
So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj['method']()` syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as [func.bind()](/bind#solution-2-bind).
|
||||
|
||||
## Arrow functions have no "this"
|
||||
|
||||
Arrow functions are special: they don't have their "own" `this`. If we reference `this` from such a function, it's taken from the outer "normal" function.
|
||||
|
|
176
1-js/04-object-basics/07-optional-chaining/article.md
Normal file
|
@ -0,0 +1,176 @@
|
|||
|
||||
# Optional chaining '?.'
|
||||
|
||||
[recent browser="new"]
|
||||
|
||||
The optional chaining `?.` is an error-prone way to access nested object properties, even if an intermediate property doesn't exist.
|
||||
|
||||
## The problem
|
||||
|
||||
If you've just started to read the tutorial and learn JavaScript, maybe the problem hasn't touched you yet, but it's quite common.
|
||||
|
||||
For example, some of our users have addresses, but few did not provide them. Then we can't safely read `user.address.street`:
|
||||
|
||||
```js run
|
||||
let user = {}; // the user happens to be without address
|
||||
|
||||
alert(user.address.street); // Error!
|
||||
```
|
||||
|
||||
Or, in the web development, we'd like to get an information about an element on the page, but it may not exist:
|
||||
|
||||
```js run
|
||||
// Error if the result of querySelector(...) is null
|
||||
let html = document.querySelector('.my-element').innerHTML;
|
||||
```
|
||||
|
||||
Before `?.` appeared in the language, the `&&` operator was used to work around that.
|
||||
|
||||
For example:
|
||||
|
||||
```js run
|
||||
let user = {}; // user has no address
|
||||
|
||||
alert( user && user.address && user.address.street ); // undefined (no error)
|
||||
```
|
||||
|
||||
AND'ing the whole path to the property ensures that all components exist, but is cumbersome to write.
|
||||
|
||||
## Optional chaining
|
||||
|
||||
The optional chaining `?.` stops the evaluation and returns `undefined` if the part before `?.` is `undefined` or `null`.
|
||||
|
||||
Further in this article, for brewity, we'll be saying that something "exists" if it's not `null` and not `undefined`.
|
||||
|
||||
|
||||
Here's the safe way to access `user.address.street`:
|
||||
|
||||
```js run
|
||||
let user = {}; // user has no address
|
||||
|
||||
alert( user?.address?.street ); // undefined (no error)
|
||||
```
|
||||
|
||||
Reading the address with `user?.address` works even if `user` object doesn't exist:
|
||||
|
||||
```js run
|
||||
let user = null;
|
||||
|
||||
alert( user?.address ); // undefined
|
||||
|
||||
alert( user?.address.street ); // undefined
|
||||
alert( user?.address.street.anything ); // undefined
|
||||
```
|
||||
|
||||
Please note: the `?.` syntax works exactly where it's placed, not any further.
|
||||
|
||||
In the last two lines the evaluation stops immediately after `user?.`, never accessing further properties. But if the `user` actually exists, then the further intermediate properties, such as `user.address` must exist.
|
||||
|
||||
```warn header="Don't overuse the optional chaining"
|
||||
We should use `?.` only where it's ok that something doesn't exist.
|
||||
|
||||
For example, if according to our coding logic `user` object must be there, but `address` is optional, then `user.address?.street` would be better.
|
||||
|
||||
So, if `user` happens to be undefined due to a mistake, we'll know about it and fix it. Otherwise, coding errors can be silenced where not appropriate, and become more difficult to debug.
|
||||
```
|
||||
|
||||
````warn header="The variable before `?.` must exist"
|
||||
If there's no variable `user`, then `user?.anything` triggers an error:
|
||||
|
||||
```js run
|
||||
// ReferenceError: user is not defined
|
||||
user?.address;
|
||||
```
|
||||
The optional chaining only tests for `null/undefined`, doesn't interfere with any other language mechanics.
|
||||
````
|
||||
|
||||
## Short-circuiting
|
||||
|
||||
As it was said before, the `?.` immediately stops ("short-circuits") the evaluation if the left part doesn't exist.
|
||||
|
||||
So, if there are any further function calls or side effects, they don't occur:
|
||||
|
||||
```js run
|
||||
let user = null;
|
||||
let x = 0;
|
||||
|
||||
user?.sayHi(x++); // nothing happens
|
||||
|
||||
alert(x); // 0, value not incremented
|
||||
```
|
||||
|
||||
## Other cases: ?.(), ?.[]
|
||||
|
||||
The optional chaining `?.` is not an operator, but a special syntax construct, that also works with functions and square brackets.
|
||||
|
||||
For example, `?.()` is used to call a function that may not exist.
|
||||
|
||||
In the code below, some of our users have `admin` method, and some don't:
|
||||
|
||||
```js run
|
||||
let user1 = {
|
||||
admin() {
|
||||
alert("I am admin");
|
||||
}
|
||||
}
|
||||
|
||||
let user2 = {};
|
||||
|
||||
*!*
|
||||
user1.admin?.(); // I am admin
|
||||
user2.admin?.();
|
||||
*/!*
|
||||
```
|
||||
|
||||
Here, in both lines we first use the dot `.` to get `admin` property, because the user object must exist, so it's safe read from it.
|
||||
|
||||
Then `?.()` checks the left part: if the user exists, then it runs (for `user1`). Otherwise (for `user2`) the evaluation stops without errors.
|
||||
|
||||
The `?.[]` syntax also works, if we'd like to use brackets `[]` to access properties instead of dot `.`. Similar to previous cases, it allows to safely read a property from an object that may not exist.
|
||||
|
||||
```js run
|
||||
let user1 = {
|
||||
firstName: "John"
|
||||
};
|
||||
|
||||
let user2 = null; // Imagine, we couldn't authorize the user
|
||||
|
||||
let key = "firstName";
|
||||
|
||||
alert( user1?.[key] ); // John
|
||||
alert( user2?.[key] ); // undefined
|
||||
|
||||
alert( user1?.[key]?.something?.not?.existing); // undefined
|
||||
```
|
||||
|
||||
Also we can use `?.` with `delete`:
|
||||
|
||||
```js run
|
||||
delete user?.name; // delete user.name if user exists
|
||||
```
|
||||
|
||||
```warn header="We can use `?.` for safe reading and deleting, but not writing"
|
||||
The optional chaining `?.` has no use at the left side of an assignment:
|
||||
|
||||
```js run
|
||||
// the idea of the code below is to write user.name, if user exists
|
||||
|
||||
user?.name = "John"; // Error, doesn't work
|
||||
// because it evaluates to undefined = "John"
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The `?.` syntax has three forms:
|
||||
|
||||
1. `obj?.prop` -- returns `obj.prop` if `obj` exists, otherwise `undefined`.
|
||||
2. `obj?.[prop]` -- returns `obj[prop]` if `obj` exists, otherwise `undefined`.
|
||||
3. `obj?.method()` -- calls `obj.method()` if `obj` exists, otherwise returns `undefined`.
|
||||
|
||||
As we can see, all of them are straightforward and simple to use. The `?.` checks the left part for `null/undefined` and allows the evaluation to proceed if it's not so.
|
||||
|
||||
A chain of `?.` allows to safely access nested properties.
|
||||
|
||||
Still, we should apply `?.` carefully, only where it's ok that the left part doesn't to exist.
|
||||
|
||||
So that it won't hide programming errors from us, if they occur.
|
|
@ -116,7 +116,7 @@ Unexpected things also may happen when assigning to `toString`, which is a funct
|
|||
|
||||
How can we avoid this problem?
|
||||
|
||||
First, we can just switch to using `Map`, then everything's fine.
|
||||
First, we can just switch to using `Map` for storage instead of plain objects, then everything's fine.
|
||||
|
||||
But `Object` can also serve us well here, because language creators gave thought to that problem long ago.
|
||||
|
||||
|
@ -128,7 +128,7 @@ So, if `obj.__proto__` is read or set, the corresponding getter/setter is called
|
|||
|
||||
As it was said in the beginning of this tutorial section: `__proto__` is a way to access `[[Prototype]]`, it is not `[[Prototype]]` itself.
|
||||
|
||||
Now, if we want to use an object as an associative array, we can do it with a little trick:
|
||||
Now, if we intend to use an object as an associative array and be free of such problems, we can do it with a little trick:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
|
|
|
@ -140,7 +140,7 @@ let eventMixin = {
|
|||
* menu.off('select', handler)
|
||||
*/
|
||||
off(eventName, handler) {
|
||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
let handlers = this._eventHandlers?.[eventName];
|
||||
if (!handlers) return;
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
if (handlers[i] === handler) {
|
||||
|
|
|
@ -18,7 +18,7 @@ let eventMixin = {
|
|||
* menu.off('select', handler)
|
||||
*/
|
||||
off(eventName, handler) {
|
||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
let handlers = this._eventHandlers?.[eventName];
|
||||
if (!handlers) return;
|
||||
for(let i = 0; i < handlers.length; i++) {
|
||||
if (handlers[i] == handler) {
|
||||
|
|
|
@ -301,7 +301,7 @@ async function* fetchCommits(repo) {
|
|||
|
||||
// (3) the URL of the next page is in the headers, extract it
|
||||
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
||||
nextPage = nextPage && nextPage[1];
|
||||
nextPage = nextPage?.[1];
|
||||
|
||||
url = nextPage;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
// the URL of the next page is in the headers, extract it
|
||||
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
||||
nextPage = nextPage && nextPage[1];
|
||||
nextPage = nextPage?.[1];
|
||||
|
||||
url = nextPage;
|
||||
|
||||
|
|
114
1-js/99-js-misc/04-reference-type/article.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
|
||||
# Reference Type
|
||||
|
||||
```warn header="In-depth language feature"
|
||||
This article covers an advanced topic, to understand certain edge-cases better.
|
||||
|
||||
It's not important. Many experienced developers live fine without knowing it. Read on if you're want to know how things work under the hood.
|
||||
```
|
||||
|
||||
A dynamically evaluated method call can lose `this`.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
hi() { alert(this.name); },
|
||||
bye() { alert("Bye"); }
|
||||
};
|
||||
|
||||
user.hi(); // works
|
||||
|
||||
// now let's call user.hi or user.bye depending on the name
|
||||
*!*
|
||||
(user.name == "John" ? user.hi : user.bye)(); // Error!
|
||||
*/!*
|
||||
```
|
||||
|
||||
On the last line there is a conditional operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`.
|
||||
|
||||
Then the method is immediately called with parentheses `()`. But it doesn't work correctly!
|
||||
|
||||
As you can see, the call results in an error, because the value of `"this"` inside the call becomes `undefined`.
|
||||
|
||||
This works (object dot method):
|
||||
```js
|
||||
user.hi();
|
||||
```
|
||||
|
||||
This doesn't (evaluated method):
|
||||
```js
|
||||
(user.name == "John" ? user.hi : user.bye)(); // Error!
|
||||
```
|
||||
|
||||
Why? If we want to understand why it happens, let's get under the hood of how `obj.method()` call works.
|
||||
|
||||
## Reference type explained
|
||||
|
||||
Looking closely, we may notice two operations in `obj.method()` statement:
|
||||
|
||||
1. First, the dot `'.'` retrieves the property `obj.method`.
|
||||
2. Then parentheses `()` execute it.
|
||||
|
||||
So, how does the information about `this` get passed from the first part to the second one?
|
||||
|
||||
If we put these operations on separate lines, then `this` will be lost for sure:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
hi() { alert(this.name); }
|
||||
}
|
||||
|
||||
*!*
|
||||
// split getting and calling the method in two lines
|
||||
let hi = user.hi;
|
||||
hi(); // Error, because this is undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`.
|
||||
|
||||
**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).**
|
||||
|
||||
The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language.
|
||||
|
||||
The value of Reference Type is a three-value combination `(base, name, strict)`, where:
|
||||
|
||||
- `base` is the object.
|
||||
- `name` is the property name.
|
||||
- `strict` is true if `use strict` is in effect.
|
||||
|
||||
The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is:
|
||||
|
||||
```js
|
||||
// Reference Type value
|
||||
(user, "hi", true)
|
||||
```
|
||||
|
||||
When parentheses `()` are called on the Reference Type, they receive the full information about the object and its method, and can set the right `this` (`=user` in this case).
|
||||
|
||||
Reference type is a special "intermediary" internal type, with the purpose to pass information from dot `.` to calling parentheses `()`.
|
||||
|
||||
Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "loses" `this`.
|
||||
|
||||
So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj['method']()` syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as [func.bind()](/bind#solution-2-bind).
|
||||
|
||||
## Summary
|
||||
|
||||
Reference Type is an internal type of the language.
|
||||
|
||||
Reading a property, such as with dot `.` in `obj.method()` returns not exactly the property value, but a special "reference type" value that stores both the property value and the object it was taken from.
|
||||
|
||||
That's for the subsequent method call `()` to get the object and set `this` to it.
|
||||
|
||||
For all other operations, the reference type automatically becomes the property value (a function in our case).
|
||||
|
||||
The whole mechanics is hidden from our eyes. It only matters in subtle cases, such as when a method is obtained dynamically from the object, using an expression.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
result of dot `.` isn't actually a method, but a value of `` needs a way to pass the information about `obj`
|