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.
|
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
|
## Square brackets
|
||||||
|
|
||||||
For multiword properties, the dot access doesn't work:
|
For multiword properties, the dot access doesn't work:
|
||||||
|
@ -161,7 +185,7 @@ alert( user.key ) // undefined
|
||||||
|
|
||||||
### Computed properties
|
### 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:
|
For instance:
|
||||||
|
|
||||||
|
@ -249,9 +273,25 @@ let user = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Property names limitations
|
## 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.
|
Other types are automatically converted to strings.
|
||||||
|
|
||||||
|
@ -267,25 +307,7 @@ alert( obj["0"] ); // test
|
||||||
alert( obj[0] ); // test (same property)
|
alert( obj[0] ); // test (same property)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Reserved words are allowed as property names.**
|
There's a minor gotcha with a special property named `__proto__`. We can't set it to a non-object value:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let obj = {};
|
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.
|
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).
|
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.
|
||||||
|
|
||||||
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>.
|
|
||||||
|
|
||||||
## Property existence test, "in" operator
|
## 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
|
```js run
|
||||||
let user = {};
|
let user = {};
|
||||||
|
@ -315,7 +331,7 @@ let user = {};
|
||||||
alert( user.noSuchProperty === undefined ); // true means "no such property"
|
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:
|
The syntax is:
|
||||||
```js
|
```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.
|
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
|
```js run
|
||||||
let user = { age: 30 };
|
let user = { age: 30 };
|
||||||
|
|
||||||
let key = "age";
|
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`"
|
Why does the `in` operator exist? Isn't it enough to compare against `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.
|
|
||||||
|
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`:
|
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!
|
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.
|
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
|
## 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.
|
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
|
### 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?
|
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.
|
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
|
## Summary
|
||||||
|
|
||||||
Objects are associative arrays with several special features.
|
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 check if a property with the given key exists: `"key" in obj`.
|
||||||
- To iterate over an object: `for (let key in obj)` loop.
|
- 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`.
|
What we've studied in this chapter is called a "plain object", or just `Object`.
|
||||||
|
|
||||||
There are many other kinds of objects in JavaScript:
|
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.
|
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 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.
|
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?
|
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.
|
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.
|
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
|
```js run
|
||||||
*!*
|
*!*
|
||||||
|
|
|
@ -140,7 +140,7 @@ let eventMixin = {
|
||||||
* menu.off('select', handler)
|
* menu.off('select', handler)
|
||||||
*/
|
*/
|
||||||
off(eventName, handler) {
|
off(eventName, handler) {
|
||||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
let handlers = this._eventHandlers?.[eventName];
|
||||||
if (!handlers) return;
|
if (!handlers) return;
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
if (handlers[i] === handler) {
|
if (handlers[i] === handler) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ let eventMixin = {
|
||||||
* menu.off('select', handler)
|
* menu.off('select', handler)
|
||||||
*/
|
*/
|
||||||
off(eventName, handler) {
|
off(eventName, handler) {
|
||||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
let handlers = this._eventHandlers?.[eventName];
|
||||||
if (!handlers) return;
|
if (!handlers) return;
|
||||||
for(let i = 0; i < handlers.length; i++) {
|
for(let i = 0; i < handlers.length; i++) {
|
||||||
if (handlers[i] == handler) {
|
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
|
// (3) the URL of the next page is in the headers, extract it
|
||||||
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
||||||
nextPage = nextPage && nextPage[1];
|
nextPage = nextPage?.[1];
|
||||||
|
|
||||||
url = nextPage;
|
url = nextPage;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
// the URL of the next page is in the headers, extract it
|
// the URL of the next page is in the headers, extract it
|
||||||
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
|
||||||
nextPage = nextPage && nextPage[1];
|
nextPage = nextPage?.[1];
|
||||||
|
|
||||||
url = nextPage;
|
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`
|