refactor objects, add optional chaining

This commit is contained in:
Ilya Kantor 2020-05-03 16:56:16 +03:00
parent 09a964e969
commit b057341f6c
35 changed files with 579 additions and 435 deletions

View file

@ -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;
```

View file

@ -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";
*/!*
```

View file

@ -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!"`.
![](variable-copy-value.svg)
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"
};
```
![](variable-contains-reference.svg)
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:
![](variable-copy-reference.svg)
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:

View 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!"`.
![](variable-copy-value.svg)
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"
};
```
![](variable-contains-reference.svg)
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:
![](variable-copy-reference.svg)
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).

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -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.

View 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.

View file

@ -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
*!*

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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;

View 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`