work
|
@ -490,13 +490,13 @@ alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference
|
||||||
|
|
||||||
The example above demonstrates that there is only one object. Like if we had a cabinet with two keys and used one of them (`admin`) to get into it -- later using the other one (`user`) we will see things modified.
|
The example above demonstrates that there is only one object. Like if we had a cabinet with two keys and used one of them (`admin`) to get into it -- later using the other one (`user`) we will see things modified.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Comparison by reference
|
### Comparison by reference
|
||||||
|
|
||||||
The equality `==` and strict equality `===` operators for objects work exactly the same, simple way.
|
The equality `==` and strict equality `===` operators for objects work exactly the same.
|
||||||
|
|
||||||
**Two object variables are equal only when reference the same object.**
|
**Two objects are equal only if they are the same object.**
|
||||||
|
|
||||||
|
For instance, two variables reference the same object, they are equal:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let a = {};
|
let a = {};
|
||||||
|
@ -506,9 +506,7 @@ alert( a == b ); // true, both variables reference the same object
|
||||||
alert( a === b ); // true
|
alert( a === b ); // true
|
||||||
```
|
```
|
||||||
|
|
||||||
In all other cases objects are non-equal, even if their content is the same.
|
And here two independent objects are not equal, even though both are empty:
|
||||||
|
|
||||||
For instance:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let a = {};
|
let a = {};
|
||||||
|
@ -517,9 +515,7 @@ let b = {}; // two independent objects
|
||||||
alert( a == b ); // false
|
alert( a == b ); // false
|
||||||
```
|
```
|
||||||
|
|
||||||
That rule only applies to object vs object equality checks.
|
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 say the truth, such comparisons occur very rarely in real code and usually are a result of a coding mistake.
|
||||||
|
|
||||||
For other comparisons like whether an object less/greater than another object (`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 say the truth, such comparisons occur very rarely in real code and usually are a result of a coding mistake.
|
|
||||||
|
|
||||||
## Cloning and merging, Object.assign
|
## Cloning and merging, Object.assign
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
Try running it:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
let str = "Hello";
|
|
||||||
|
|
||||||
str.test = 5; // (*)
|
|
||||||
|
|
||||||
alert(str.test);
|
|
||||||
```
|
|
||||||
|
|
||||||
There may be two kinds of result:
|
|
||||||
1. `undefined`
|
|
||||||
2. An error.
|
|
||||||
|
|
||||||
Why? Let's replay what's happening at line `(*)`:
|
|
||||||
|
|
||||||
1. When a property of `str` is accessed, a "wrapper object" is created.
|
|
||||||
2. The operation with the property is carried out on it. So, the object gets the `test` property.
|
|
||||||
3. The operation finishes and the "wrapper object" disappears.
|
|
||||||
|
|
||||||
So, on the last line, `str` has no trace of the property. A new wrapper object for every object operation on a string.
|
|
||||||
|
|
||||||
Some browsers though may decide to further limit the programmer and disallow to assign properties to primitives at all. That's why in practice we can also see errors at line `(*)`. It's a little bit farther from the specification though.
|
|
||||||
|
|
||||||
**This example clearly shows that primitives are not objects.**
|
|
||||||
|
|
||||||
They just can not store data.
|
|
||||||
|
|
||||||
All property/method operations are performed with the help of temporary objects.
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
importance: 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Can I add a string property?
|
|
||||||
|
|
||||||
|
|
||||||
Consider the following code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let str = "Hello";
|
|
||||||
|
|
||||||
str.test = 5;
|
|
||||||
|
|
||||||
alert(str.test);
|
|
||||||
```
|
|
||||||
|
|
||||||
How do you think, will it work? What will be shown?
|
|
|
@ -214,7 +214,6 @@ In this case `this` is `undefined` in strict mode. If we try to access `this.nam
|
||||||
|
|
||||||
In non-strict mode (if you forgot `use strict`) the value of `this` in such case will be the *global object* (`"window"` for browser, we'll study it later). This is just a historical thing that `"use strict"` fixes.
|
In non-strict mode (if you forgot `use strict`) the value of `this` in such case will be the *global object* (`"window"` for browser, we'll study it later). This is just a historical thing that `"use strict"` fixes.
|
||||||
|
|
||||||
|
|
||||||
Please note that usually a call of a function using `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object.
|
Please note that usually a call of a function using `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object.
|
||||||
|
|
||||||
```smart header="The consequences of unbound `this`"
|
```smart header="The consequences of unbound `this`"
|
||||||
|
@ -224,7 +223,6 @@ The idea of unbound, run-time evaluated `this` has both pluses and minuses. From
|
||||||
|
|
||||||
Here we are not to judge whether this language design decision is good or bad. We will understand how to work with it, how to get benefits and evade problems.
|
Here we are not to judge whether this language design decision is good or bad. We will understand how to work with it, how to get benefits and evade problems.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Internals: Reference Type
|
## Internals: Reference Type
|
||||||
|
|
||||||
An intricate method call can loose `this`, for instance:
|
An intricate method call can loose `this`, for instance:
|
||||||
|
|
|
@ -85,6 +85,39 @@ let user = new function() {
|
||||||
The constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code for a single complex object only.
|
The constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code for a single complex object only.
|
||||||
````
|
````
|
||||||
|
|
||||||
|
## Dual-use constructors: new.target
|
||||||
|
|
||||||
|
Inside a function, we can check how it is called with `new` or without it, using a special `new.target` property.
|
||||||
|
|
||||||
|
It is empty for ordinary runs and equals the function if called with `new`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function User() {
|
||||||
|
alert(new.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
User(); // undefined
|
||||||
|
new User(); // function User { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
That can be used to allow both `new` and ordinary syntax work the same:
|
||||||
|
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function User(name) {
|
||||||
|
if (!new.target) { // if you run me without new
|
||||||
|
return new User(name); // ...I will add new for you
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let john = User("John"); // redirects call to new User
|
||||||
|
alert(john.name); // John
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach is sometimes used in libraries to make the syntax more flexible. Probably not a good thing to use everywhere though, because it makes a bit less obvious what's going on for a person who's familiar with internals of `User`.
|
||||||
|
|
||||||
## Return from constructors
|
## Return from constructors
|
||||||
|
|
||||||
Usually, constructors do not have a `return` statement. Their task is to write all necessary stuff into `this`, and it automatically becomes the result.
|
Usually, constructors do not have a `return` statement. Their task is to write all necessary stuff into `this`, and it automatically becomes the result.
|
||||||
|
|
|
@ -16,15 +16,15 @@ The conversion of an object to primitive value (a number or a string) is a rare
|
||||||
|
|
||||||
Just think about cases when such conversion may be necessary. For instance, numeric conversion happens when we compare an object against a primitive: `user > 18`. But what such comparison actually means? Are we going to compare `18` against user's age? Then it would be more obvious to write `user.age > 18`. And it's easier to read and understand it too.
|
Just think about cases when such conversion may be necessary. For instance, numeric conversion happens when we compare an object against a primitive: `user > 18`. But what such comparison actually means? Are we going to compare `18` against user's age? Then it would be more obvious to write `user.age > 18`. And it's easier to read and understand it too.
|
||||||
|
|
||||||
Or, for a string conversion... Where does it happen? Usually, when we output an object. But simple ways of object-as-string output like `alert(user)` are only used for debugging and logging purposes. For real stuff, the output is more complicated, we may need to provide it additional parameters. That's why we usually implement it using object methods like `user.format()` or even in more advanced ways.
|
Or, for a string conversion... Where does it happen? Usually, when we output an object. But simple ways of object-as-string output like `alert(user)` are only used for debugging and logging purposes. For real stuff, the output is more complicated, we may need to configure it with additional parameters. That's why it is usually implemented with object methods like `user.format()` or even in more advanced ways.
|
||||||
|
|
||||||
So, most of the time, it's more flexible and gives more readable code to explicitly write an object property or call a method than rely on the conversion.
|
So, most of the time, it's more flexible and gives more readable code to explicitly write an object property or call a method than rely on the conversion.
|
||||||
|
|
||||||
That said, there are still valid reasons why we should know how to-primitive conversion works.
|
That said, there are still valid reasons why we should know how to-primitive conversion works.
|
||||||
|
|
||||||
- Simple object-as-string output may be useable sometimes. Without a customized conversion it will show `[object Object]`.
|
- Simple object-as-string output may be useable sometimes.
|
||||||
- Many built-in objects implement their own to-primitive conversion, we plan to cover that.
|
- Many built-in objects implement their own to-primitive conversion, we plan to cover that.
|
||||||
- Sometimes it just happens (on mistake?), and we should understand what's going on.
|
- Sometimes an unexpected conversion happens, and we should understand what's going on.
|
||||||
- Okay, the final one. There are quizzes and questions on interviews that rely on that knowledge. Looks like people think it's a good sigh that person understands Javascript if he knows type conversions well.
|
- Okay, the final one. There are quizzes and questions on interviews that rely on that knowledge. Looks like people think it's a good sigh that person understands Javascript if he knows type conversions well.
|
||||||
|
|
||||||
## ToPrimitive
|
## ToPrimitive
|
||||||
|
@ -78,7 +78,7 @@ There are 3 types (also called "hints") of object-to-primitive conversion:
|
||||||
|
|
||||||
Please note -- there are only three conversions. That simple. There is no "boolean" (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most built-ins do, then there are only two conversions.
|
Please note -- there are only three conversions. That simple. There is no "boolean" (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most built-ins do, then there are only two conversions.
|
||||||
|
|
||||||
To do the conversion, Javascript tries to find and call these three object methods:
|
To do the conversion, Javascript tries to find and call three object methods:
|
||||||
|
|
||||||
1. `Symbol.toPrimitive(hint)` if exists,
|
1. `Symbol.toPrimitive(hint)` if exists,
|
||||||
2. Otherwise if hint is `"string"`, try `toString()` and `valueOf()`, whatever exists.
|
2. Otherwise if hint is `"string"`, try `toString()` and `valueOf()`, whatever exists.
|
||||||
|
@ -109,7 +109,7 @@ As we can see from the code, `user` becomes a self-descriptive string or a money
|
||||||
|
|
||||||
### toString/valueOf
|
### toString/valueOf
|
||||||
|
|
||||||
Methods `toString` and `valueOf` come from the ancient times. That's why they are not symbols. They provide an alternative "old-style" way to implement the conversion.
|
Methods `toString` and `valueOf` come from ancient times. That's why they are not symbols. They provide an alternative "old-style" way to implement the conversion.
|
||||||
|
|
||||||
If there's no `Symbol.toPrimitive` then Javascript tries to find them and try in the order:
|
If there's no `Symbol.toPrimitive` then Javascript tries to find them and try in the order:
|
||||||
|
|
||||||
|
@ -166,20 +166,20 @@ There is no control whether `toString()` returns exactly a string, or whether `S
|
||||||
|
|
||||||
**The only mandatory thing: these methods must return a primitive.**
|
**The only mandatory thing: these methods must return a primitive.**
|
||||||
|
|
||||||
An operation that was the reason for the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary.
|
An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary.
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
- All mathematical operations except binary plus apply `ToNumber`:
|
- All mathematical operations except binary plus apply `ToNumber` after `ToPrimitive` with `"number"` hint:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let obj = {
|
let obj = {
|
||||||
toString() { // toString used for numeric conversion in the absense of valueOf
|
toString() { // toString handles all ToPrimitive in the absense of other methods
|
||||||
return "2";
|
return "2";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
alert(obj * 2); // 4
|
alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2
|
||||||
```
|
```
|
||||||
|
|
||||||
- Binary plus first checks if the primitive is a string, and then does concatenation, otherwise performs `ToNumber` and works with numbers.
|
- Binary plus first checks if the primitive is a string, and then does concatenation, otherwise performs `ToNumber` and works with numbers.
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var leader = {
|
let user = {
|
||||||
name: "Василий Иванович",
|
name: "John Smith",
|
||||||
age: 35
|
age: 35
|
||||||
};
|
};
|
||||||
|
|
||||||
var leaderStr = JSON.stringify(leader);
|
*!*
|
||||||
leader = JSON.parse(leaderStr);
|
let user2 = JSON.parse(JSON.stringify(user));
|
||||||
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
importance: 3
|
importance: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Превратите объект в JSON
|
# Turn the object into JSON and back
|
||||||
|
|
||||||
Превратите объект `leader` из примера ниже в JSON:
|
Turn the `leader` into JSON and then read it back into another variable.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var leader = {
|
let user = {
|
||||||
name: "Василий Иванович",
|
name: "John Smith",
|
||||||
age: 35
|
age: 35
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
После этого прочитайте получившуюся строку обратно в объект.
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
occupiedBy: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room
|
||||||
|
};
|
||||||
|
|
||||||
|
room.occupiedBy = meetup;
|
||||||
|
meetup.self = meetup;
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup, function replacer(key, value) {
|
||||||
|
return (key != "" && value == meetup) ? undefined : value;
|
||||||
|
}));
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"title":"Conference",
|
||||||
|
"occupiedBy":[{"name":"John"},{"name":"Alice"}],
|
||||||
|
"place":{"number":23}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we also need to test `key==""` to exclude the first call where it is normal that `value` is `meetup`.
|
||||||
|
|
42
1-js/5-data-types/08-json/2-serialize-event-circular/task.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Exclude backreferences
|
||||||
|
|
||||||
|
In simple cases of circular references, we can exclude an offending property from serialization by its name.
|
||||||
|
|
||||||
|
But sometimes there are many backreferences. And names may be used both in circular references and normal properties.
|
||||||
|
|
||||||
|
Write `replacer` function to stringify everything, but remove properties that reference `meetup`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
occupiedBy: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room
|
||||||
|
};
|
||||||
|
|
||||||
|
*!*
|
||||||
|
// circular references
|
||||||
|
room.occupiedBy = meetup;
|
||||||
|
meetup.self = meetup;
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup, function replacer(key, value) {
|
||||||
|
/* your code */
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* result should be:
|
||||||
|
{
|
||||||
|
"title":"Conference",
|
||||||
|
"occupiedBy":[{"name":"John"},{"name":"Alice"}],
|
||||||
|
"place":{"number":23}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
# Ответ на первый вопрос
|
|
||||||
|
|
||||||
Обычный вызов `JSON.stringify(team)` выдаст ошибку, так как объекты `leader` и `soldier` внутри структуры ссылаются друг на друга.
|
|
||||||
|
|
||||||
Формат JSON не предусматривает средств для хранения ссылок.
|
|
||||||
|
|
||||||
# Варианты решения
|
|
||||||
|
|
||||||
Чтобы превращать такие структуры в JSON, обычно используются два подхода:
|
|
||||||
|
|
||||||
1. Добавить в `team` свой код `toJSON`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
team.toJSON = function() {
|
|
||||||
/* свой код, который может создавать копию объекта без круговых ссылок и передавать управление JSON.stringify */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
При этом, конечно, понадобится и своя функция чтения из JSON, которая будет восстанавливать объект, а затем дополнять его круговыми ссылками.
|
|
||||||
2. Можно учесть возможную проблему в самой структуре, используя вместо ссылок `id`. Как правило, это несложно, ведь на сервере у данных тоже есть идентификаторы.
|
|
||||||
|
|
||||||
Изменённая структура может выглядеть так:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var leader = {
|
|
||||||
id: 12,
|
|
||||||
name: "Василий Иванович"
|
|
||||||
};
|
|
||||||
|
|
||||||
var soldier = {
|
|
||||||
id: 51,
|
|
||||||
name: "Петька"
|
|
||||||
};
|
|
||||||
|
|
||||||
*!*
|
|
||||||
// поменяли прямую ссылку на ID
|
|
||||||
leader.soldierId = 51;
|
|
||||||
soldier.leaderId = 12;
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
var team = {
|
|
||||||
12: leader,
|
|
||||||
51: soldier
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
..Но действительно ли это решение будет оптимальным? Использовать структуру стало сложнее, и вряд ли это изменение стоит делать лишь из-за JSON. Вот если есть другие преимущества, тогда можно подумать.
|
|
||||||
|
|
||||||
Универсальный вариант подхода, описанного выше -- это использование особой реализации JSON, которая не входит в стандарт и поддерживает расширенный формат для поддержки ссылок.
|
|
||||||
|
|
||||||
Она, к примеру, есть во фреймворке Dojo.
|
|
||||||
|
|
||||||
При вызове `dojox.json.ref.toJson(team)` будет создано следующее строковое представление:
|
|
||||||
|
|
||||||
```js no-beautify
|
|
||||||
[{"name":"Василий Иванович","soldier":{"name":"Петька","leader":{"$ref":"#0"}}},{"$ref":"#0.soldier"}]
|
|
||||||
```
|
|
||||||
|
|
||||||
Метод разбора такой строки -- также свой: `dojox.json.ref.fromJson`.
|
|
|
@ -1,26 +0,0 @@
|
||||||
importance: 3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Превратите объекты со ссылками в JSON
|
|
||||||
|
|
||||||
Превратите объект `team` из примера ниже в JSON:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var leader = {
|
|
||||||
name: "Василий Иванович"
|
|
||||||
};
|
|
||||||
|
|
||||||
var soldier = {
|
|
||||||
name: "Петька"
|
|
||||||
};
|
|
||||||
|
|
||||||
// эти объекты ссылаются друг на друга!
|
|
||||||
leader.soldier = soldier;
|
|
||||||
soldier.leader = leader;
|
|
||||||
|
|
||||||
var team = [leader, soldier];
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Может ли это сделать прямой вызов `JSON.stringify(team)`? Если нет, то почему?
|
|
||||||
2. Какой подход вы бы предложили для чтения и восстановления таких объектов?
|
|
|
@ -1,4 +1,4 @@
|
||||||
# JSON methods, toJSON [todo: after Date]
|
# JSON methods, toJSON
|
||||||
|
|
||||||
The [JSON](http://en.wikipedia.org/wiki/JSON) (JavaScript Object Notation) format is used to represent an object as a string.
|
The [JSON](http://en.wikipedia.org/wiki/JSON) (JavaScript Object Notation) format is used to represent an object as a string.
|
||||||
|
|
||||||
|
@ -14,6 +14,23 @@ JSON, despite its name (JavaScript Object Notation) is an independent standard.
|
||||||
|
|
||||||
In Javascript, the native method [JSON.stringify(value)](mdn:js/JSON/stringify) accepts a value and returns it's representation as a string in JSON format.
|
In Javascript, the native method [JSON.stringify(value)](mdn:js/JSON/stringify) accepts a value and returns it's representation as a string in JSON format.
|
||||||
|
|
||||||
|
The syntax:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let json = JSON.stringify(value[, replacer, space])
|
||||||
|
```
|
||||||
|
|
||||||
|
value
|
||||||
|
: A value to encode.
|
||||||
|
|
||||||
|
replacer
|
||||||
|
: Array of properties to encode or a mapping function `function(key, value)`.
|
||||||
|
|
||||||
|
space
|
||||||
|
: Amount of space to use for formatting
|
||||||
|
|
||||||
|
Most of time, `JSON.stringify` is used with first argument only.
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
```js run
|
```js run
|
||||||
let student = {
|
let student = {
|
||||||
|
@ -29,6 +46,7 @@ let json = JSON.stringify(student);
|
||||||
alert(typeof json); // we've got a string!
|
alert(typeof json); // we've got a string!
|
||||||
|
|
||||||
alert(json);
|
alert(json);
|
||||||
|
*!*
|
||||||
/* JSON-encoded object:
|
/* JSON-encoded object:
|
||||||
{
|
{
|
||||||
"name": "John",
|
"name": "John",
|
||||||
|
@ -38,9 +56,12 @@ alert(json);
|
||||||
"wife": null
|
"wife": null
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
The JSON-encoded object has several important differences from the original variant:
|
The resulting `json` string is a called *JSON-encoded* or *serialized* or *stringified* object. We are ready to send it over the wire or put into plain data store.
|
||||||
|
|
||||||
|
JSON-encoded object has several important differences from the original variant:
|
||||||
|
|
||||||
- Strings use double quotes. No single quotes or backticks in JSON. So `'John'` becomes `"John"`.
|
- Strings use double quotes. No single quotes or backticks in JSON. So `'John'` becomes `"John"`.
|
||||||
- Object property names are double-quoted also. Also obligatory. So `age:30` becomes `"age":30`.
|
- Object property names are double-quoted also. Also obligatory. So `age:30` becomes `"age":30`.
|
||||||
|
@ -73,9 +94,7 @@ The supported JSON types are:
|
||||||
|
|
||||||
In the examples above we can see them all.
|
In the examples above we can see them all.
|
||||||
|
|
||||||
JSON format does not support any other types.
|
JSON format does not support any other types. It is data-only cross-language specification, so some Javascript-specific things like function properties, `undefined` values and symbolic properties are just skipped by `JSON.stringify`:
|
||||||
|
|
||||||
For instance, functions and `undefined` values, symbolic properties are skipped by `JSON.stringify`:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let user = {
|
let user = {
|
||||||
|
@ -89,9 +108,7 @@ let user = {
|
||||||
alert( JSON.stringify(user) ); // {} (empty object)
|
alert( JSON.stringify(user) ); // {} (empty object)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom "toJSON"
|
From the other hand, nested objects that reference other objects and arrays are supported.
|
||||||
|
|
||||||
If an object is not satisfied with the default behavior of `JSON.stringify`, it override it by implementing method `toJSON`.
|
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
|
@ -100,13 +117,214 @@ let room = {
|
||||||
number: 23
|
number: 23
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = {
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
room,
|
||||||
|
participants: [{
|
||||||
|
name: "John"
|
||||||
|
}, {
|
||||||
|
name: "Alice"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup) );
|
||||||
|
/* The whole structure is stringified:
|
||||||
|
{
|
||||||
|
"title":"Conference",
|
||||||
|
"room":{"number":23},
|
||||||
|
"participants":[
|
||||||
|
{"name":"John"},{"name":"Alice"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
The only limitation for `JSON.stringify`: there must be no circular references.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
participants: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room // meetup references room
|
||||||
|
};
|
||||||
|
|
||||||
|
room.occupiedBy = meetup; // room references meetup
|
||||||
|
|
||||||
|
*!*
|
||||||
|
JSON.stringify(meetup); // Error: Converting circular structure to JSON
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the conversion fails, because of circular reference: `room.occupiedBy` references `meetup`, and `meetup.place` references `room`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Excluding and transforming: replacer
|
||||||
|
|
||||||
|
One of the ways to deal with circular references is to use the second, optional argument of `JSON.stringify`.
|
||||||
|
|
||||||
|
If we pass an array of properties to it, only these properties will be encoded.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
participants: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room // meetup references room
|
||||||
|
};
|
||||||
|
|
||||||
|
room.occupiedBy = meetup; // room references meetup
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup, *!*['title', 'participants']*/!*) );
|
||||||
|
// {"title":"Conference","participants":[{},{}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we are probably too strict. The property list is applied to the whole object structure. So participants are empty, because `name` is not in the list.
|
||||||
|
|
||||||
|
We are safe to include every property except `room.occupiedBy` that would cause the circular reference:
|
||||||
|
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
participants: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room // meetup references room
|
||||||
|
};
|
||||||
|
|
||||||
|
room.occupiedBy = meetup; // room references meetup
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup, *!*['title', 'participants', 'place', 'name', 'number']*/!*) );
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"title":"Conference",
|
||||||
|
"participants":[{"name":"John"},{"name":"Alice"}],
|
||||||
|
"place":{"number":23}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Now everything excepts `occupiedBy` is serialized.
|
||||||
|
|
||||||
|
Another way would be to provide a function instead the array as `replacer`.
|
||||||
|
|
||||||
|
The function will be called for every `(key,value)` pair and should return the "replaced" value, which will be used instead of the original one.
|
||||||
|
|
||||||
|
In our case, we can return `value` "as is" for everything except `occupiedBy`. To ignore `occupiedBy`, the code below returns `undefined`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
|
title: "Conference",
|
||||||
|
participants: [{name: "John"}, {name: "Alice"}],
|
||||||
|
place: room // meetup references room
|
||||||
|
};
|
||||||
|
|
||||||
|
alert( JSON.stringify(meetup, function replacer(key, value) {
|
||||||
|
alert(`${key}: ${value}`); // to see what replacer gets
|
||||||
|
return (key == 'occupiedBy') ? undefined : value;
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* key:value pairs that come to replacer:
|
||||||
|
: [object Object]
|
||||||
|
title: Conference
|
||||||
|
participants: [object Object],[object Object]
|
||||||
|
0: [object Object]
|
||||||
|
name: John
|
||||||
|
1: [object Object]
|
||||||
|
name: Alice
|
||||||
|
place: [object Object]
|
||||||
|
number: 23
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that `replacer` function gets every key/value pair including nested objects and array items. It is applied recursively. The value of `this` inside `replacer` is the object that contains the current property.
|
||||||
|
|
||||||
|
The first call is made using a special "wrapper object": `{"": meetup}`, the first `(key,value)` pair is an empty key and the target object as a whole. That's why the first line is `":[object Object]"`.
|
||||||
|
|
||||||
|
## Formatting: spacer
|
||||||
|
|
||||||
|
The third argument of `JSON.stringify(value, replacer, spaces) is the number of spaces to use for pretty formatting.
|
||||||
|
|
||||||
|
Previously, all objects had no indents and extra spaces.
|
||||||
|
|
||||||
|
Here they will have them:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
age: 25,
|
||||||
|
roles: {
|
||||||
|
isAdmin: false,
|
||||||
|
isEditor: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
alert(JSON.stringify(user, null, 2));
|
||||||
|
/* two-space indents:
|
||||||
|
{
|
||||||
|
"name": "John",
|
||||||
|
"age": 25,
|
||||||
|
"roles": {
|
||||||
|
"isAdmin": false,
|
||||||
|
"isEditor": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
alert(JSON.stringify(user, null, 4));
|
||||||
|
/* four-space indents:
|
||||||
|
{
|
||||||
|
"name": "John",
|
||||||
|
"age": 25,
|
||||||
|
"roles": {
|
||||||
|
"isAdmin": false,
|
||||||
|
"isEditor": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
The `spaces` parameter is used solely for logging and nice-output purposes.
|
||||||
|
|
||||||
|
## Custom "toJSON"
|
||||||
|
|
||||||
|
Previously, we used `replacer` to serialize objects "the smart way" and ignore circular links.
|
||||||
|
|
||||||
|
But if object is not satisfied with the default behavior of `JSON.stringify`, it can provide its own by implementing method `toJSON`.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let room = {
|
||||||
|
number: 23
|
||||||
|
};
|
||||||
|
|
||||||
|
let meetup = {
|
||||||
title: "Conference",
|
title: "Conference",
|
||||||
date: new Date(Date.UTC(2017, 0, 1)),
|
date: new Date(Date.UTC(2017, 0, 1)),
|
||||||
room
|
room
|
||||||
};
|
};
|
||||||
|
|
||||||
alert( JSON.stringify(event) );
|
alert( JSON.stringify(meetup) );
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
"title":"Conference",
|
"title":"Conference",
|
||||||
|
@ -132,7 +350,7 @@ let room = {
|
||||||
*/!*
|
*/!*
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = {
|
let meetup = {
|
||||||
title: "Conference",
|
title: "Conference",
|
||||||
room
|
room
|
||||||
};
|
};
|
||||||
|
@ -141,7 +359,7 @@ let event = {
|
||||||
alert( JSON.stringify(room) ); // 23
|
alert( JSON.stringify(room) ); // 23
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
alert( JSON.stringify(event) );
|
alert( JSON.stringify(meetup) );
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
"title":"Conference",
|
"title":"Conference",
|
||||||
|
@ -159,6 +377,17 @@ As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)`
|
||||||
|
|
||||||
To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/parse).
|
To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/parse).
|
||||||
|
|
||||||
|
The syntax:
|
||||||
|
```js
|
||||||
|
let value = JSON.parse(str[, reviver]);
|
||||||
|
```
|
||||||
|
|
||||||
|
str
|
||||||
|
: JSON-string to parse.
|
||||||
|
|
||||||
|
reviver
|
||||||
|
: Optional function(key,value) that will be called for each `(key,value)` pair and can transform the value.
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
|
@ -182,7 +411,7 @@ alert( user.friends[1] ); // 1
|
||||||
|
|
||||||
The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the format.
|
The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the format.
|
||||||
|
|
||||||
Here are typical mistakes of novice developers who try to write custom JSON manually (for debugging purposes mainly):
|
Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let json = `{
|
let json = `{
|
||||||
|
@ -200,66 +429,59 @@ There's another format named [JSON5](http://json5.org/), which allows unquoted k
|
||||||
|
|
||||||
The regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm.
|
The regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm.
|
||||||
|
|
||||||
## Умный разбор: JSON.parse(str, reviver)
|
## Using reviver
|
||||||
|
|
||||||
Метод `JSON.parse` поддерживает и более сложные алгоритмы разбора.
|
Imagine, we got a stringified `meetup` object from the server.
|
||||||
|
|
||||||
Например, мы получили с сервера объект с данными события `event`.
|
It looks like this:
|
||||||
|
|
||||||
Он выглядит так:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// title: название события, date: дата события
|
// title: (meetup title), date: (meetup date)
|
||||||
var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';
|
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
|
||||||
```
|
```
|
||||||
|
|
||||||
...И теперь нужно *восстановить* его, то есть превратить в JavaScript-объект.
|
...And now we reed to *deserialize* it, to turn back into Javascript object.
|
||||||
|
|
||||||
Попробуем вызвать для этого `JSON.parse`:
|
Let's do it by calling `JSON.parse`:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';
|
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
|
||||||
|
|
||||||
var event = JSON.parse(str);
|
let meetup = JSON.parse(str);
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
alert( event.date.getDate() ); // ошибка!
|
alert( meetup.date.getDate() ); // Error!
|
||||||
*/!*
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
...Увы, ошибка!
|
Wops! An error!
|
||||||
|
|
||||||
Дело в том, что значением `event.date` является строка, а отнюдь не объект `Date`. Откуда методу `JSON.parse` знать, что нужно превратить строку именно в дату?
|
The value of `meetup.date` is a string, not a `Date` object. How `JSON.parse` may know that it should transform that string into a `Date`?
|
||||||
|
|
||||||
**Для интеллектуального восстановления из строки у `JSON.parse(str, reviver)` есть второй параметр `reviver`, который является функцией `function(key, value)`.**
|
Let's pass to `JSON.parse` the reviving function that returns all values "as is", but `date` wll become a `Date`:
|
||||||
|
|
||||||
Если она указана, то в процессе чтения объекта из строки `JSON.parse` передаёт ей по очереди все создаваемые пары ключ-значение и может возвратить либо преобразованное значение, либо `undefined`, если его нужно пропустить.
|
|
||||||
|
|
||||||
В данном случае мы можем создать правило, что ключ `date` всегда означает дату:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
// дата в строке - в формате UTC
|
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
|
||||||
var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';
|
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
var event = JSON.parse(str, function(key, value) {
|
let meetup = JSON.parse(str, function(key, value) {
|
||||||
if (key == 'date') return new Date(value);
|
if (key == 'date') return new Date(value);
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
alert( event.date.getDate() ); // теперь сработает!
|
alert( meetup.date.getDate() ); // now works!
|
||||||
```
|
```
|
||||||
|
|
||||||
Кстати, эта возможность работает и для вложенных объектов тоже:
|
By the way, that works for nested objects as well:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
var schedule = '{ \
|
let schedule = `{
|
||||||
"events": [ \
|
"meetups": [
|
||||||
{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}, \
|
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
|
||||||
{"title":"День рождения","date":"2015-04-18T12:00:00.000Z"} \
|
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
|
||||||
]\
|
]
|
||||||
}';
|
}`;
|
||||||
|
|
||||||
schedule = JSON.parse(schedule, function(key, value) {
|
schedule = JSON.parse(schedule, function(key, value) {
|
||||||
if (key == 'date') return new Date(value);
|
if (key == 'date') return new Date(value);
|
||||||
|
@ -267,179 +489,17 @@ schedule = JSON.parse(schedule, function(key, value) {
|
||||||
});
|
});
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
alert( schedule.events[1].date.getDate() ); // сработает!
|
alert( schedule.meetups[1].date.getDate() ); // works!
|
||||||
*/!*
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
## Сериализация, метод JSON.stringify
|
|
||||||
|
|
||||||
Метод `JSON.stringify(value, replacer, space)` преобразует ("сериализует") значение в JSON-строку.
|
|
||||||
|
|
||||||
Пример использования:
|
## Summary
|
||||||
|
|
||||||
```js run
|
- JSON is a data format that has its own independent standard and libraries for most programming languages.
|
||||||
var event = {
|
- JSON supports plain objects, arrays, strings, numbers, booleans and `null`.
|
||||||
title: "Конференция",
|
- Javascript provides methods [JSON.stringify](mdn:js/JSON/stringify) to serialize into JSON and [JSON.parse](mdn:js/JSON/parse) to read from JSON.
|
||||||
date: "сегодня"
|
- Both methods support transformer functions for smart reading/writing.
|
||||||
};
|
- If an object has `toJSON`, then it is called by `JSON.stringify`.
|
||||||
|
|
||||||
var str = JSON.stringify(event);
|
|
||||||
alert( str ); // {"title":"Конференция","date":"сегодня"}
|
|
||||||
|
|
||||||
// Обратное преобразование.
|
|
||||||
event = JSON.parse(str);
|
|
||||||
```
|
|
||||||
|
|
||||||
**При сериализации объекта вызывается его метод `toJSON`.**
|
|
||||||
|
|
||||||
Если такого метода нет -- перечисляются его свойства, кроме функций.
|
|
||||||
|
|
||||||
Посмотрим это в примере посложнее:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var room = {
|
|
||||||
number: 23,
|
|
||||||
occupy: function() {
|
|
||||||
alert( this.number );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
event = {
|
|
||||||
title: "Конференция",
|
|
||||||
date: new Date(Date.UTC(2014, 0, 1)),
|
|
||||||
room: room
|
|
||||||
};
|
|
||||||
|
|
||||||
alert( JSON.stringify(event) );
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
"title":"Конференция",
|
|
||||||
"date":"2014-01-01T00:00:00.000Z", // (1)
|
|
||||||
"room": {"number":23} // (2)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
Обратим внимание на два момента:
|
|
||||||
|
|
||||||
1. Дата превратилась в строку. Это не случайно: у всех дат есть встроенный метод `toJSON`. Его результат в данном случае -- строка в таймзоне UTC.
|
|
||||||
2. У объекта `room` нет метода `toJSON`. Поэтому он сериализуется перечислением свойств.
|
|
||||||
|
|
||||||
Мы, конечно, могли бы добавить такой метод, тогда в итог попал бы его результат:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var room = {
|
|
||||||
number: 23,
|
|
||||||
*!*
|
|
||||||
toJSON: function() {
|
|
||||||
return this.number;
|
|
||||||
}
|
|
||||||
*/!*
|
|
||||||
};
|
|
||||||
|
|
||||||
alert( JSON.stringify(room) ); // 23
|
|
||||||
```
|
|
||||||
|
|
||||||
### Исключение свойств
|
|
||||||
|
|
||||||
Попытаемся преобразовать в JSON объект, содержащий ссылку на DOM.
|
|
||||||
|
|
||||||
Например:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var user = {
|
|
||||||
name: "Вася",
|
|
||||||
age: 25,
|
|
||||||
window: window
|
|
||||||
};
|
|
||||||
|
|
||||||
*!*
|
|
||||||
alert( JSON.stringify(user) ); // ошибка!
|
|
||||||
// TypeError: Converting circular structure to JSON (текст из Chrome)
|
|
||||||
*/!*
|
|
||||||
```
|
|
||||||
|
|
||||||
Произошла ошибка! В чём же дело, неужели некоторые объекты запрещены? Как видно из текста ошибки -- дело совсем в другом. Глобальный объект `window` -- сложная структура с кучей встроенных свойств и круговыми ссылками, поэтому его преобразовать невозможно. Да и нужно ли?
|
|
||||||
|
|
||||||
**Во втором параметре `JSON.stringify(value, replacer)` можно указать массив свойств, которые подлежат сериализации.**
|
|
||||||
|
|
||||||
Например:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var user = {
|
|
||||||
name: "Вася",
|
|
||||||
age: 25,
|
|
||||||
window: window
|
|
||||||
};
|
|
||||||
|
|
||||||
*!*
|
|
||||||
alert( JSON.stringify(user, ["name", "age"]) );
|
|
||||||
// {"name":"Вася","age":25}
|
|
||||||
*/!*
|
|
||||||
```
|
|
||||||
|
|
||||||
Для более сложных ситуаций вторым параметром можно передать функцию `function(key, value)`, которая возвращает сериализованное `value` либо `undefined`, если его не нужно включать в результат:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var user = {
|
|
||||||
name: "Вася",
|
|
||||||
age: 25,
|
|
||||||
window: window
|
|
||||||
};
|
|
||||||
|
|
||||||
*!*
|
|
||||||
var str = JSON.stringify(user, function(key, value) {
|
|
||||||
if (key == 'window') return undefined;
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
alert( str ); // {"name":"Вася","age":25}
|
|
||||||
```
|
|
||||||
|
|
||||||
В примере выше функция пропустит свойство с названием `window`. Для остальных она просто возвращает значение, передавая его стандартному алгоритму. А могла бы и как-то обработать.
|
|
||||||
|
|
||||||
```smart header="Функция `replacer` работает рекурсивно"
|
|
||||||
То есть, если объект содержит вложенные объекты, массивы и т.п., то все они пройдут через `replacer`.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Красивое форматирование
|
|
||||||
|
|
||||||
В методе `JSON.stringify(value, replacer, space)` есть ещё третий параметр `space`.
|
|
||||||
|
|
||||||
Если он является числом -- то уровни вложенности в JSON оформляются указанным количеством пробелов, если строкой -- вставляется эта строка.
|
|
||||||
|
|
||||||
Например:
|
|
||||||
|
|
||||||
```js run
|
|
||||||
var user = {
|
|
||||||
name: "Вася",
|
|
||||||
age: 25,
|
|
||||||
roles: {
|
|
||||||
isAdmin: false,
|
|
||||||
isEditor: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
*!*
|
|
||||||
var str = JSON.stringify(user, "", 4);
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
alert( str );
|
|
||||||
/* Результат -- красиво сериализованный объект:
|
|
||||||
{
|
|
||||||
"name": "Вася",
|
|
||||||
"age": 25,
|
|
||||||
"roles": {
|
|
||||||
"isAdmin": false,
|
|
||||||
"isEditor": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Итого
|
|
||||||
|
|
||||||
- JSON -- формат для представления объектов (и не только) в виде строки.
|
|
||||||
- Методы [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) и [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) позволяют интеллектуально преобразовать объект в строку и обратно.
|
|
||||||
|
|
||||||
|
|
BIN
1-js/5-data-types/08-json/json-meetup.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
1-js/5-data-types/08-json/json-meetup@2x.png
Normal file
After Width: | Height: | Size: 26 KiB |
285
1-js/5-data-types/09-property-flags-descriptors/article.md
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
|
||||||
|
# Property flags and descriptors [todo move to objects?]
|
||||||
|
|
||||||
|
Now as we know how to work with primitives and several concrete object-based types, let's return to objects in general.
|
||||||
|
|
||||||
|
As we know, objects can store properties.
|
||||||
|
|
||||||
|
But an object property is actually more complex thing than just a "key-value" mapping.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## Property flags
|
||||||
|
|
||||||
|
Object properties, besides a **`value`**, have three special attributes (so-called "flags"):
|
||||||
|
|
||||||
|
- **`writable`** -- if `true`, can be changed, otherwise it's read-only.
|
||||||
|
- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed.
|
||||||
|
- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not.
|
||||||
|
|
||||||
|
We didn't see them yet, because by default they are concealed. When we create a property "the usual way", all of them are `true`. But we also can change them any time.
|
||||||
|
|
||||||
|
First, let's see how to read the flags.
|
||||||
|
|
||||||
|
The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the information about a property.
|
||||||
|
|
||||||
|
The syntax is:
|
||||||
|
```js
|
||||||
|
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
|
||||||
|
```
|
||||||
|
|
||||||
|
`obj`
|
||||||
|
: The object to get information about.
|
||||||
|
|
||||||
|
`propertyName`
|
||||||
|
: The name of the property of interest.
|
||||||
|
|
||||||
|
The returned value is a so-called "property descriptor" object: it contains the value and all the flags.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John"
|
||||||
|
};
|
||||||
|
|
||||||
|
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||||
|
|
||||||
|
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||||
|
/* descriptor:
|
||||||
|
{
|
||||||
|
"value": "John",
|
||||||
|
"writable": true,
|
||||||
|
"enumerable": true,
|
||||||
|
"configurable": true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, we'll use [Object.defineProperty](mdn:js/Object/defineProperty) to change flags.
|
||||||
|
|
||||||
|
The syntax is:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Object.defineProperty(obj, propertyName, descriptor)
|
||||||
|
```
|
||||||
|
|
||||||
|
`obj`, `propertyName`
|
||||||
|
: The object and property to work on.
|
||||||
|
|
||||||
|
`descriptor`
|
||||||
|
: Property descriptor to apply.
|
||||||
|
|
||||||
|
If the property exist, it update its flags.
|
||||||
|
|
||||||
|
Otherwise, it creates the property with the provided value and flags. Please note, that if a flag is not supplied, it is assumed `false`.
|
||||||
|
|
||||||
|
For instance, here a property `name` is created with all falsy flags:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {};
|
||||||
|
|
||||||
|
Object.defineProperty(user, "name", {
|
||||||
|
value: "John"
|
||||||
|
});
|
||||||
|
|
||||||
|
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||||
|
|
||||||
|
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||||
|
/* compare it with "normally created" user.name above:
|
||||||
|
{
|
||||||
|
"value": "John",
|
||||||
|
*!*
|
||||||
|
"writable": false,
|
||||||
|
"enumerable": false,
|
||||||
|
"configurable": false
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's see effects of the flags by example.
|
||||||
|
|
||||||
|
## Read-only
|
||||||
|
|
||||||
|
Let's make `user.name` read-only by changing `writable` flag:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John"
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(user, "name", {
|
||||||
|
*!*
|
||||||
|
writable: false
|
||||||
|
*/!*
|
||||||
|
});
|
||||||
|
|
||||||
|
*!*
|
||||||
|
user.name = "Pete"; // Error: Cannot assign to read only property 'name'...
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Now no one can change the name of our user, unless he applies his own `defineProperty` to override ours.
|
||||||
|
|
||||||
|
Here's the same operation, but for the case when a property doesn't exist:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = { };
|
||||||
|
|
||||||
|
Object.defineProperty(user, "name", {
|
||||||
|
*!*
|
||||||
|
value: "Pete",
|
||||||
|
// for new properties need to explicitly list what's true
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true
|
||||||
|
*/!*
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(user.name); // Pete
|
||||||
|
user.name = "Alice"; // Error
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Non-enumerable
|
||||||
|
|
||||||
|
Now let's a custom `toString` to `user`.
|
||||||
|
|
||||||
|
Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. So we'll make ours behave the same.
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// By default, both our properties are listed:
|
||||||
|
for(let key in user) alert(key); // name, toString
|
||||||
|
|
||||||
|
Object.defineProperty(user, "toString", {
|
||||||
|
*!*
|
||||||
|
enumerable: false
|
||||||
|
*/!*
|
||||||
|
});
|
||||||
|
|
||||||
|
*!*
|
||||||
|
// Now toString disappears:
|
||||||
|
*/!*
|
||||||
|
for(let key in user) alert(key); // name
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-enumerable properties are also excluded from `Object.keys`.
|
||||||
|
|
||||||
|
## Non-configurable
|
||||||
|
|
||||||
|
Non-configurable flag is often preset for built-in objects and properties.
|
||||||
|
|
||||||
|
A non-configurable property can not be deleted or altered with `defineProperty`.
|
||||||
|
|
||||||
|
For instance, `Math.PI` is both read-only, non-enumerable and non-configurable:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
|
||||||
|
|
||||||
|
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"value": 3.141592653589793,
|
||||||
|
"writable": false,
|
||||||
|
"enumerable": false,
|
||||||
|
"configurable": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
So, a programmer is unable to change the value of that built-in constant or overwrite it.
|
||||||
|
|
||||||
|
```js run
|
||||||
|
Math.PI = 3; // Error
|
||||||
|
|
||||||
|
// delete Math.PI won't work either
|
||||||
|
```
|
||||||
|
|
||||||
|
Making non-configurable is one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties.
|
||||||
|
|
||||||
|
Here `user.name` is a forever sealed constant:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = { };
|
||||||
|
|
||||||
|
Object.defineProperty(user, "name", {
|
||||||
|
value: "John",
|
||||||
|
writable: false,
|
||||||
|
configurable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
*!*
|
||||||
|
// can't change it or its flags
|
||||||
|
// user.name = "Pete" won't work
|
||||||
|
// delete user.name won't work
|
||||||
|
// defineProperty won't work either:
|
||||||
|
Object.defineProperty(user, "name", {writable: true}); // Error
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
```smart header="Errors appear only in use strict"
|
||||||
|
In non-strict mode, there are no errors for writing to read-only properties and such, flag-violating actions are silently ignored.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Many properties at once
|
||||||
|
|
||||||
|
There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Object.defineProperties(user, {
|
||||||
|
name: { writable: false },
|
||||||
|
surname: { ... },
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
And, to get all descriptors, use [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors).
|
||||||
|
|
||||||
|
Together they can be used as an "property flags-aware" way of cloning an object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
|
||||||
|
```
|
||||||
|
|
||||||
|
(For the clone to be fully identical, we need to care about one more thing: "prototype", we'll see into it soon in the chapter [todo])
|
||||||
|
|
||||||
|
## Sealing an object globally
|
||||||
|
|
||||||
|
Property descriptors allow to forbid modifications of individual properties.
|
||||||
|
|
||||||
|
There are also methods that limit access to the whole object:
|
||||||
|
|
||||||
|
[Object.preventExtensions(obj)](mdn:js/Object/preventExtensions)
|
||||||
|
: Forbids to add properties to the object.
|
||||||
|
|
||||||
|
[Object.seal(obj)](mdn:js/Object/seal)
|
||||||
|
: Forbids to add/remove properties, sets for all existing properties `configurable: false`.
|
||||||
|
|
||||||
|
[Object.freeze(obj)](mdn:js/Object/freeze)
|
||||||
|
: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`.
|
||||||
|
|
||||||
|
And the tests for them:
|
||||||
|
|
||||||
|
[Object.isExtensible(obj)](mdn:js/Object/isExtensible)
|
||||||
|
: Returns `false` if adding properties is forbidden, otherwise `true`.
|
||||||
|
|
||||||
|
[Object.isSealed(obj)](mdn:js/Object/isSealed)
|
||||||
|
: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`.
|
||||||
|
|
||||||
|
[Object.isFrozen(obj)](mdn:js/Object/isFrozen)
|
||||||
|
: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`.
|
||||||
|
|
||||||
|
These methods are rarely used in practice.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
Check: new property has all non-enum, test
|
221
1-js/5-data-types/10-property-accessors/article.md
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
|
||||||
|
# Property getters and setters [todo move to objects?]
|
||||||
|
|
||||||
|
There are two kinds of properties.
|
||||||
|
|
||||||
|
The first kind is *data properties*. We already know how to work with them, actually, all properties that we've been using yet are data properties.
|
||||||
|
|
||||||
|
The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## Getters and setters
|
||||||
|
|
||||||
|
Accessor properties are represented by "getter" and "setter" methods. In an object literal they are preprended with words `get` and `set`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let obj = {
|
||||||
|
*!*get propName()*/!* {
|
||||||
|
// getter, the code executed on getting obj.propName
|
||||||
|
},
|
||||||
|
|
||||||
|
*!*set propName(value)*/!* {
|
||||||
|
// setter, the code executed on setting obj.propName = value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The getter works when `obj.propName` is read, the setter -- when it is assigned.
|
||||||
|
|
||||||
|
For instance, we have a `user` object with `name` and `surname`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
surname: "Smith"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
surname: "Smith",
|
||||||
|
|
||||||
|
*!*
|
||||||
|
get fullName() {
|
||||||
|
return `${this.name} ${this.surname}`;
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
};
|
||||||
|
|
||||||
|
*!*
|
||||||
|
alert(user.fullName); // John Smith
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't call `user.fullName` as a function, we read it normally, and it runs behind the scenes.
|
||||||
|
|
||||||
|
As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error.
|
||||||
|
|
||||||
|
Let's fix it by adding a setter for `user.fullName`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
surname: "Smith",
|
||||||
|
|
||||||
|
get fullName() {
|
||||||
|
return `${this.name} ${this.surname}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
*!*
|
||||||
|
set fullName(value) {
|
||||||
|
[this.name, this.surname] = value.split(" ");
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
};
|
||||||
|
|
||||||
|
// set fullName is executed with the given value.
|
||||||
|
user.fullName = "Alice Cooper";
|
||||||
|
|
||||||
|
alert(user.name); // Alice
|
||||||
|
alert(user.surname); // Cooper
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we have a "virtual" property. It is readable and writable, but in fact does not exist.
|
||||||
|
|
||||||
|
```smart
|
||||||
|
We can either work with a property as a "data property" or as an "accessor property", these two never mix.
|
||||||
|
|
||||||
|
Once a property as defined with `get prop()`, it can't be assigned with `obj.prop=`, unless there's a setter too.
|
||||||
|
|
||||||
|
And if a property is defined with `set prop()`, then it can't be read unless there's also a getter.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Accessor descriptors
|
||||||
|
|
||||||
|
Differences between data properties and accessors are also reflected in their descriptors.
|
||||||
|
|
||||||
|
For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions.
|
||||||
|
|
||||||
|
So an accessor descriptor may have:
|
||||||
|
|
||||||
|
- **`get`** -- a function without arguments, that works when a property is read,
|
||||||
|
- **`set`** -- a function with one argument, that is called when the property is set,
|
||||||
|
- **`enumerable`** -- same as for data properties,
|
||||||
|
- **`configurable`** -- same as for data properties.
|
||||||
|
|
||||||
|
For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
name: "John",
|
||||||
|
surname: "Smith"
|
||||||
|
};
|
||||||
|
|
||||||
|
*!*
|
||||||
|
Object.defineProperty(user, 'fullName', {
|
||||||
|
get() {
|
||||||
|
return `${this.name} ${this.surname}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
[this.name, this.surname] = value.split(" ");
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(user.fullName); // John Smith
|
||||||
|
|
||||||
|
for(let key in user) alert(key);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Smarter getters/setters
|
||||||
|
|
||||||
|
A combination of getter/setter can be used to validate property values at the moment of assignment.
|
||||||
|
|
||||||
|
For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`, at the same time providing smart getter/setter for it:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let user = {
|
||||||
|
get name() {
|
||||||
|
return this._name;
|
||||||
|
},
|
||||||
|
|
||||||
|
set name(value) {
|
||||||
|
if (value.length < 4) {
|
||||||
|
throw new Error("Name is too short, need at least 4 characters");
|
||||||
|
}
|
||||||
|
this._name = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
user.name = "Pete";
|
||||||
|
alert(user.name); // Pete
|
||||||
|
|
||||||
|
user.name = ""; // Error
|
||||||
|
```
|
||||||
|
|
||||||
|
Technically, the "real" name is stored in `user._name`, so the outer code may access it. In Javascript there's no way to prevent reading an object property. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside.
|
||||||
|
|
||||||
|
|
||||||
|
## For compatibility
|
||||||
|
|
||||||
|
One of great ideas behind getters and setters -- they allow to take control over a property at any moment.
|
||||||
|
|
||||||
|
For instance, we start implementing user objects using data properties `name` and `age`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function User(name, age) {
|
||||||
|
this.name = name;
|
||||||
|
this.age = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
let john = new User("John", 25);
|
||||||
|
|
||||||
|
alert( john.age ); // 25
|
||||||
|
```
|
||||||
|
|
||||||
|
...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function User(name, birthday) {
|
||||||
|
this.name = name;
|
||||||
|
this.birthday = birthday;
|
||||||
|
}
|
||||||
|
|
||||||
|
let john = new User("John", new Date(1992, 6, 1));
|
||||||
|
```
|
||||||
|
|
||||||
|
Now what to do with the old code that still uses `age`?
|
||||||
|
|
||||||
|
We can try to find all such places and fix them, but that takes time and not always possible with 3rd party libraries. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want.
|
||||||
|
|
||||||
|
Adding a getter for `age` mitigates the problem:
|
||||||
|
|
||||||
|
```js run no-beautify
|
||||||
|
function User(name, birthday) {
|
||||||
|
this.name = name;
|
||||||
|
this.birthday = birthday;
|
||||||
|
|
||||||
|
*!*
|
||||||
|
// age is calculated from the current date and birthday
|
||||||
|
Object.defineProperty(this, "age", {
|
||||||
|
get() {
|
||||||
|
let todayYear = new Date().getFullYear();
|
||||||
|
return todayYear - this.birthday.getFullYear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
|
||||||
|
let john = new User("John", new Date(1992, 6, 1));
|
||||||
|
|
||||||
|
alert( john.birthday ); // birthday is available
|
||||||
|
alert( john.age ); // ...as well as the age
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -135,12 +135,12 @@ For `Math.max`:
|
||||||
```js run
|
```js run
|
||||||
let arr = [3, 5, 1];
|
let arr = [3, 5, 1];
|
||||||
|
|
||||||
alert( Math.max(...arr) ); // 5
|
alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments)
|
||||||
```
|
```
|
||||||
|
|
||||||
Unlike rest parameters, there is no restrictions of now many iterables we use.
|
Unlike rest parameters, spread operators can appear as many times as needed within a single call.
|
||||||
|
|
||||||
Here's example of a combination:
|
Here's an example:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let arr = [3, 5, 1];
|
let arr = [3, 5, 1];
|
||||||
|
@ -171,8 +171,9 @@ let merged = [0, ...arr, 2, ...arr2];
|
||||||
alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2)
|
alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In the examples above we used an array to demonstrate the spread operator, but any iterable will do.
|
||||||
|
|
||||||
In the examples above we used an array, but any iterable will do including strings:
|
For instance, here we use spread operator to turn the string into array of characters:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let str = "Hello";
|
let str = "Hello";
|
||||||
|
@ -180,9 +181,9 @@ let str = "Hello";
|
||||||
alert( [...str] ); // H,e,l,l,o
|
alert( [...str] ); // H,e,l,l,o
|
||||||
```
|
```
|
||||||
|
|
||||||
The spread operator `...str` actually does the same as `for..of` to gather elements. For a string, `for..of` returns characters one by one. And then it passes them as a list to array initializer `["h","e","l","l","o"]`.
|
The spread operator `...str` uses the same iterator mechanism as `for..of` to iterate and gather elements. For a string, `for..of` returns characters, so `...str` becomes `"h","e","l","l","o"`. The list of characters is passed to array initializer `[]`.
|
||||||
|
|
||||||
If our purpose is only to convert an iterable to array, then we can also use `Array.from`:
|
Here we could also use `Array.from` that converts an iterable (a string) into an array:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let str = "Hello";
|
let str = "Hello";
|
||||||
|
@ -191,7 +192,7 @@ let str = "Hello";
|
||||||
alert( Array.from(str) ); // H,e,l,l,o
|
alert( Array.from(str) ); // H,e,l,l,o
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The result is the same as `[...str]`.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
|
|
|
@ -119,23 +119,24 @@ for(let [key, value] of users.entries()) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
````smart header="No holes"
|
````smart header="Arrays are treated as contiguous, without holes"
|
||||||
All these methods treat arrays as contiguous. "Holes" are considered `undefined` items.
|
All these methods treat arrays as contiguous. "Holes" are considered `undefined` items.
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let arr = [];
|
let arr = [];
|
||||||
arr[4] = "test";
|
arr[4] = "test"; // items till index 4 are assumed to be undefined
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
// all keys till arr.length
|
// arr.keys() lists keys contigously from 0 to 4
|
||||||
*/!*
|
*/!*
|
||||||
for(let i of arr.keys()) alert(i); // 0,1,2,3,4
|
for(let i of arr.keys()) alert(i); // 0,1,2,3,4
|
||||||
|
|
||||||
alert(`Length: ${arr.length}`); // 5, remember, length is the last index + 1
|
alert(`Length: ${arr.length}`); // 5, remember, length is the last index + 1
|
||||||
```
|
```
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
## Over Sets, Maps...
|
## Over sets, maps...
|
||||||
|
|
||||||
As we've seen in the chapter <info:map-set-weakmap-weakset>, `Map` and `Set` also implement methods `keys()`, `values()` and `entries()`.
|
As we've seen in the chapter <info:map-set-weakmap-weakset>, `Map` and `Set` also implement methods `keys()`, `values()` and `entries()`.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
Разница в поведении станет очевидной, если рассмотреть код внутри функции.
|
||||||
|
|
||||||
|
Поведение будет различным, если управление каким-то образом выпрыгнет из `try..catch`.
|
||||||
|
|
||||||
|
Например, `finally` сработает после `return`, но до передачи управления внешнему коду:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function f() {
|
||||||
|
try {
|
||||||
|
...
|
||||||
|
*!*
|
||||||
|
return result;
|
||||||
|
*/!*
|
||||||
|
} catch (e) {
|
||||||
|
...
|
||||||
|
} finally {
|
||||||
|
очистить ресурсы
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Или же управление может выпрыгнуть из-за `throw`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function f() {
|
||||||
|
try {
|
||||||
|
...
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
...
|
||||||
|
if(не умею обрабатывать эту ошибку) {
|
||||||
|
*!*
|
||||||
|
throw e;
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
очистить ресурсы
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В этих случаях именно `finally` гарантирует выполнение кода до окончания работы `f`, просто код не будет вызван.
|
|
@ -0,0 +1,39 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Finally или просто код?
|
||||||
|
|
||||||
|
Сравните два фрагмента кода.
|
||||||
|
|
||||||
|
1. Первый использует `finally` для выполнения кода по выходу из `try..catch`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
try {
|
||||||
|
начать работу
|
||||||
|
работать
|
||||||
|
} catch (e) {
|
||||||
|
обработать ошибку
|
||||||
|
} finally {
|
||||||
|
*!*
|
||||||
|
финализация: завершить работу
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. Второй фрагмент просто ставит очистку ресурсов за `try..catch`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
try {
|
||||||
|
начать работу
|
||||||
|
} catch (e) {
|
||||||
|
обработать ошибку
|
||||||
|
}
|
||||||
|
|
||||||
|
*!*
|
||||||
|
финализация: завершить работу
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужно, чтобы код финализации всегда выполнялся при выходе из блока `try..catch` и, таким образом, заканчивал начатую работу. Имеет ли здесь `finally` какое-то преимущество или оба фрагмента работают одинаково?
|
||||||
|
|
||||||
|
Если имеет, то дайте пример когда код с `finally` работает верно, а без -- неверно.
|
|
@ -0,0 +1,34 @@
|
||||||
|
Вычислить любое выражение нам поможет `eval`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
alert( eval("2+2") ); // 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Считываем выражение в цикле `while(true)`. Если при вычислении возникает ошибка -- ловим её в `try..catch`.
|
||||||
|
|
||||||
|
Ошибкой считается, в том числе, получение `NaN` из `eval`, хотя при этом исключение не возникает. Можно бросить своё исключение в этом случае.
|
||||||
|
|
||||||
|
Код решения:
|
||||||
|
|
||||||
|
```js run demo
|
||||||
|
var expr, res;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
expr = prompt("Введите выражение?", '2-');
|
||||||
|
if (expr == null) break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = eval(expr);
|
||||||
|
if (isNaN(res)) {
|
||||||
|
throw new Error("Результат неопределён");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
alert( "Ошибка: " + e.message + ", повторите ввод" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert( res );
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Eval-калькулятор с ошибками [todo: NO EVAL YET]
|
||||||
|
|
||||||
|
Напишите интерфейс, который принимает математическое выражение (в `prompt`) и результат его вычисления через `eval`.
|
||||||
|
|
||||||
|
**При ошибке нужно выводить сообщение и просить переввести выражение**.
|
||||||
|
|
||||||
|
Ошибкой считается не только некорректное выражение, такое как `2+`, но и выражение, возвращающее `NaN`, например `0/0`.
|
||||||
|
|
||||||
|
[demo]
|
||||||
|
|
659
1-js/6-more-syntax/4-try-catch/article.md
Normal file
|
@ -0,0 +1,659 @@
|
||||||
|
# Error handling, "try..catch"
|
||||||
|
|
||||||
|
No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response and for a thousand of other reasons.
|
||||||
|
|
||||||
|
Usually, a script "dies" (immediately stops) in case of an error, printing it to console.
|
||||||
|
|
||||||
|
But there's a syntax construct `try..catch` that allows to "catch" errors and, instead of dying, do something more reasonable.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## The "try..catch" syntax
|
||||||
|
|
||||||
|
The `try..catch` construct has two main blocks: `try`, and then `catch`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
try {
|
||||||
|
|
||||||
|
// code...
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
// error handling
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It works like this:
|
||||||
|
|
||||||
|
1. First, the code in `try {...}` is executed.
|
||||||
|
2. If there were no errors, then `catch(err)` is ignored: the execution reaches the end of `try` and then jumps over `catch`.
|
||||||
|
3. If an error occurs, then `try` execution is stopped, and the control flows to the beginning of `catch(err)`. The `err` variable (can use any name for it) contains an error object with details about what's happened.
|
||||||
|
|
||||||
|
**So, an error inside `try` does not kill the script: we have a chance to handle it in `catch`.**
|
||||||
|
|
||||||
|
Let's see the examples.
|
||||||
|
|
||||||
|
- An errorless example: shows `alert` `(1)` and `(2)`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
|
||||||
|
alert('Start of try runs'); // *!*(1) <--*/!*
|
||||||
|
|
||||||
|
// ...no errors here
|
||||||
|
|
||||||
|
alert('End of try runs'); // *!*(2) <--*/!*
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
alert('Catch is ignored, because there are no errors'); // (3)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("...Then the execution continues");
|
||||||
|
```
|
||||||
|
- An example with error: shows `(1)` and `(3)`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
|
||||||
|
alert('Start of try runs'); // *!*(1) <--*/!*
|
||||||
|
|
||||||
|
*!*
|
||||||
|
lalala; // error, variable is not defined!
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
alert('End of try (never reached)'); // (2)
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
alert(`Error: ${e.name}`); // *!*(3) <--*/!*
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("...Then the execution continues");
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that if the code structure is violated, like a figure bracket is left unclosed, then `try..catch` can't help. Such errors are fatal, the engine just cannot run the code.
|
||||||
|
|
||||||
|
There is a better term for errors that we are catching: "an exceptional situation" or just "an exception". It's much more precise, meaning exactly the situation when a already-running and well-formed code meets a problem.
|
||||||
|
|
||||||
|
For all built-in errors, the error object inside `catch` block has two main properties:
|
||||||
|
|
||||||
|
`name`
|
||||||
|
: Error name. For an undefined variable that's `"ReferenceError"`.
|
||||||
|
|
||||||
|
`message`
|
||||||
|
: Textual message about error details.
|
||||||
|
|
||||||
|
There are other non-standard properties in most environments. One of most widely used and supported everywhere is:
|
||||||
|
|
||||||
|
`stack`
|
||||||
|
: Current call stack, a string with information about the sequence of nested calls that led to the error. Used for debugging purposes.
|
||||||
|
|
||||||
|
|
||||||
|
````warn header="`try..catch` only works in synchronous code"
|
||||||
|
If an exception happens in the future code, like those inside `setTimeout`, then `try..catch` won't catch it:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
setTimeout(function() {
|
||||||
|
blablabla; // script will die here
|
||||||
|
}, 1000);
|
||||||
|
} catch (e) {
|
||||||
|
alert( "won't work" );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That's because at the moment of running the function from `setTimeout`, the current script will have already been finished, the engine will have left `try..catch` contruct.
|
||||||
|
|
||||||
|
To catch an exception inside a scheduled function, `try..catch` must be inside that function.
|
||||||
|
````
|
||||||
|
|
||||||
|
|
||||||
|
## Using try..catch
|
||||||
|
|
||||||
|
Let's explore a real-life use case of `try..catch`.
|
||||||
|
|
||||||
|
As we already know, JavaScript supports method [JSON.parse(str)](mdn:js/JSON/parse) to read JSON-encoded values.
|
||||||
|
|
||||||
|
Usually it's used to decode the data received over the network, from the server or another source.
|
||||||
|
|
||||||
|
We receive them and call `JSON.parse`, like this:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let json = '{"name":"John", "age": 30}'; // data from the server
|
||||||
|
|
||||||
|
let user = JSON.parse(json); // reading the object
|
||||||
|
|
||||||
|
// now user is an object with properties from the string
|
||||||
|
alert( user.name ); // John
|
||||||
|
alert( user.age ); // 30
|
||||||
|
```
|
||||||
|
|
||||||
|
More detailed information about JSON you can find in the chapter <info:json>.
|
||||||
|
|
||||||
|
**If `json` is malformed, `JSON.parse` generates an error, so the script "dies".**
|
||||||
|
|
||||||
|
Are we satisfied with that? Of course, not!
|
||||||
|
|
||||||
|
This way if something's wrong with the data, the visitor will never know that (unless he opens developer console).
|
||||||
|
|
||||||
|
And people really really don't like when something "just dies" without any error message.
|
||||||
|
|
||||||
|
Let's use `try..catch` to handle the error:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let json = "{ bad json }";
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
*!*
|
||||||
|
let user = JSON.parse(json); // <-- error happens
|
||||||
|
*/!*
|
||||||
|
alert( user.name ); // doesn't work
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
*!*
|
||||||
|
// ...the execution jumps here
|
||||||
|
alert( "Our apologies, the data has errors, we'll try to request them one more time." );
|
||||||
|
alert( e.name );
|
||||||
|
alert( e.message );
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we use `alert` only to output the message, but we can do much more: do a network request, suggest an alternative way to the visitor, send the information about the error to logging facility... All much better than just dying.
|
||||||
|
|
||||||
|
## Throwing own errors
|
||||||
|
|
||||||
|
Imagine for a minute that `json` is syntactically correct... But doesn't have a required `"name"` property:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let json = '{ "age": 30 }'; // incomplete data
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let user = JSON.parse(json); // <-- no errors
|
||||||
|
*!*
|
||||||
|
alert( user.name ); // no name!
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
alert( "doesn't execute" );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here `JSON.parse` runs normally, but the absense of `"name"` is actually an error for us.
|
||||||
|
|
||||||
|
To unify error handling, we'll use `throw` operator.
|
||||||
|
|
||||||
|
### "Throw" operator
|
||||||
|
|
||||||
|
The `throw` operator generates an error.
|
||||||
|
|
||||||
|
The syntax is:
|
||||||
|
|
||||||
|
```js
|
||||||
|
throw <error object>
|
||||||
|
```
|
||||||
|
|
||||||
|
Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it's better to use objects, preferrably with `name` and `message` properties.
|
||||||
|
|
||||||
|
Javascript has many built-in constructors for standard errors: `Error`, `SyntaxError`, `ReferenceError`, `TypeError` and others. We can use them to create objects as well.
|
||||||
|
|
||||||
|
Their syntax is:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let error = new Error(message);
|
||||||
|
```
|
||||||
|
|
||||||
|
For built-in errors (not for any objects, just for errors), the `name` property is exactly the name of the constructor. And `message` is taken from the argument.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let error = new Error("Things happen :k");
|
||||||
|
|
||||||
|
alert(error.name); // Error
|
||||||
|
alert(error.message); // Things happen :k
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's see what kind of error `JSON.parse` generates:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
JSON.parse("{ bad json o_O }");
|
||||||
|
} catch(e) {
|
||||||
|
*!*
|
||||||
|
alert(e.name); // SyntaxError
|
||||||
|
*/!*
|
||||||
|
alert(e.message); // Unexpected token o in JSON at position 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As we can see, that's a `SyntaxError`.
|
||||||
|
|
||||||
|
...And in our case, the absense of `name` can be treated as a syntax error also, assuming that users follow a sort of "schema" that requires the existance of `"name"`.
|
||||||
|
|
||||||
|
So let's throw it:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let json = '{ "age": 30 }'; // incomplete data
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let user = JSON.parse(json); // <-- no errors
|
||||||
|
|
||||||
|
if (!user.name) {
|
||||||
|
*!*
|
||||||
|
throw new SyntaxError("Incomplete data: no name"); // (*)
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
|
||||||
|
alert( user.name );
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the line `(*)` the `throw` operator generates `SyntaxError` with the given `message`, the same way as Javascript would generate itself. The execution of `try` immediately stops and the control flow jumps into `catch`.
|
||||||
|
|
||||||
|
Now `catch` became a single place for all error handling: both for `JSON.parse` and other cases.
|
||||||
|
|
||||||
|
## Rethrowing
|
||||||
|
|
||||||
|
In the example above we implemented error handling for incorrect data. But is it possible that another unexpected error happens in `try {...}` block?
|
||||||
|
|
||||||
|
Of course, it is! Normally, a code is a bag with errors. It's typical that even in an open-source utility like `ssh` that is used by millions for decades -- suddenly a crazy bug is discovered that leads to terrible hacks. Not to mention other similar cases.
|
||||||
|
|
||||||
|
In our case, `catch` block is meant to process "incorrect data" errors. But right now it catches everything.
|
||||||
|
|
||||||
|
For instance, say, we made a programming error, a mistype:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
JSON.papaparse(); // a mistype, no such function
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
alert( "JSON Error: " + e.message ); // JSON Error: JSON.papaparse is not a function
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By nature, `catch` gets all errors from `try`. Here it got an unexpected type of error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug.
|
||||||
|
|
||||||
|
Fortunately, we can find out which error we've got, for instance by its `name`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
JSON.papaparse(); // JSON.papaparse is not a function
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
*!*
|
||||||
|
alert(e.name); // "TypeError" for trying to call undefined property
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule is simple:
|
||||||
|
|
||||||
|
**Catch should only process errors that it knows and throw all others.**
|
||||||
|
|
||||||
|
The technique is called "rethrowing":
|
||||||
|
|
||||||
|
1. Catch gets all errors.
|
||||||
|
2. In `catch(e) {...}` block we analyze the error object `e`.
|
||||||
|
2. If we don't know how to handle it, then do `throw e`.
|
||||||
|
|
||||||
|
In the code below, `catch` only handles `SyntaxError`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let json = '{ "age": 30 }'; // incomplete data
|
||||||
|
try {
|
||||||
|
|
||||||
|
let user = JSON.parse(json);
|
||||||
|
|
||||||
|
if (!user.name) {
|
||||||
|
throw new SyntaxError("Incomplete data: no name");
|
||||||
|
}
|
||||||
|
|
||||||
|
*!*
|
||||||
|
blabla(); // unexpected error
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
alert( user.name );
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
*!*
|
||||||
|
if (e.name == "SyntaxError") {
|
||||||
|
alert( "JSON Error: " + e.message );
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The error made inside `catch` block "falls out" of `try..catch` and can be either caught by an outer `try..catch` construct (if exists) or kills the script.
|
||||||
|
|
||||||
|
The example below demonstrates how such errors can be caught by one more level of `try..catch`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function readData() {
|
||||||
|
let json = '{ "age": 30 }';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ...
|
||||||
|
*!*
|
||||||
|
blabla(); // error!
|
||||||
|
*/!*
|
||||||
|
} catch (e) {
|
||||||
|
// ...
|
||||||
|
if (e.name != 'SyntaxError') {
|
||||||
|
*!*
|
||||||
|
throw e; // rethrow (don't know how to deal with it)
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
readData();
|
||||||
|
} catch (e) {
|
||||||
|
*!*
|
||||||
|
alert( "External catch got: " + e ); // caught it!
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here `readData` only knows how to handle `SyntaxError`, while the outer `try..catch` knows how to handle everything.
|
||||||
|
|
||||||
|
## Wrapping exceptions
|
||||||
|
|
||||||
|
And now -- the most advanced technique to work with exceptions. It is often used in object-oriented code.
|
||||||
|
|
||||||
|
The purpose of the function `readData` in the code above is -- to "read the data", right? There may occur different kinds of errors in the process, not only `SyntaxError`, but `URIError` (wrong usage of some functions) or network errors, or others.
|
||||||
|
|
||||||
|
The code which calls `readData` wants either a result or the information about the error.
|
||||||
|
|
||||||
|
The important questions it: does it have to know about all possible types of reading errors and catch them?
|
||||||
|
|
||||||
|
Usually, the answer is: "No".
|
||||||
|
|
||||||
|
The outer code wants to be "one level above all that". It wants to have a "data reading error", and why exactly it happened -- is usually irrelevant. Or, even better if there is a way to get such details, but that's rarely needed.
|
||||||
|
|
||||||
|
In our case, when error occurs, we will create our own object `ReadError`, with the proper message. And we'll also keep the original error in its `cause` property, just in case.
|
||||||
|
|
||||||
|
Here you are:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function ReadError(message, cause) {
|
||||||
|
this.message = message;
|
||||||
|
this.cause = cause;
|
||||||
|
this.name = 'ReadError';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readData() {
|
||||||
|
let json = '{ bad json }';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ...
|
||||||
|
JSON.parse(json);
|
||||||
|
// ...
|
||||||
|
} catch (e) {
|
||||||
|
// ...
|
||||||
|
if (e.name == 'URIError') {
|
||||||
|
throw new ReadError("URI Error", e);
|
||||||
|
} else if (e.name == 'SyntaxError') {
|
||||||
|
*!*
|
||||||
|
throw new ReadError("Syntax error in the data", e);
|
||||||
|
*/!*
|
||||||
|
} else {
|
||||||
|
throw e; // rethrow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
readData();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name == 'ReadError') {
|
||||||
|
*!*
|
||||||
|
alert( e.message );
|
||||||
|
alert( e.cause ); // original error
|
||||||
|
*/!*
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The approach is called "wrapping exceptions", because we take "low level exceptions" and "wrap" them into `ReadError` that is more abstract, and probably more convenient to use for the calling code.
|
||||||
|
|
||||||
|
The best thing about it is that we can modify `readData`, catch more low-level errors in the process -- and we will not need to add tests for them in `catch` of the external code.
|
||||||
|
|
||||||
|
The external code does not cares about low-level details. It handles `ReadError`, that's enough.
|
||||||
|
|
||||||
|
## try..catch..finally
|
||||||
|
|
||||||
|
Wait, that's not all.
|
||||||
|
|
||||||
|
The `try..catch` construct may have one more code clause: `finally`.
|
||||||
|
|
||||||
|
If it exists, it runs in all cases:
|
||||||
|
|
||||||
|
- after `try`, if there were no errors,
|
||||||
|
- after `catch`, if there were errors.
|
||||||
|
|
||||||
|
The extended syntax looks like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
*!*try*/!* {
|
||||||
|
... try to execute the code ...
|
||||||
|
} *!*catch*/!*(e) {
|
||||||
|
... handle errors ...
|
||||||
|
} *!*finally*/!* {
|
||||||
|
... execute always ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Try to run this?
|
||||||
|
|
||||||
|
```js run
|
||||||
|
try {
|
||||||
|
alert( 'try' );
|
||||||
|
if (confirm('Make an error?')) BAD_CODE();
|
||||||
|
} catch (e) {
|
||||||
|
alert( 'catch' );
|
||||||
|
} finally {
|
||||||
|
alert( 'finally' );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The code has two variants:
|
||||||
|
|
||||||
|
1. If say answer "Yes" to error, then `try -> catch -> finally`.
|
||||||
|
2. If say "No", then `try -> finally`.
|
||||||
|
|
||||||
|
The `finally` clause is often used when we start doing something before `try..catch` and want to finalize it in any case of outcome.
|
||||||
|
|
||||||
|
For instance, we want to measure time that a Fibonacci numbers function `fib(n)` takes. But it returns an error for negative or non-integer numbers:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let num = +prompt("Enter a positive integer number?", 35)
|
||||||
|
|
||||||
|
let diff, result;
|
||||||
|
|
||||||
|
function fib(n) {
|
||||||
|
if (n < 0 || Math.trunc(n) != n) {
|
||||||
|
throw new Error("Must not be negative, and also an integer.");
|
||||||
|
}
|
||||||
|
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = fib(num);
|
||||||
|
} catch (e) {
|
||||||
|
result = 0;
|
||||||
|
*!*
|
||||||
|
} finally {
|
||||||
|
diff = Date.now() - start;
|
||||||
|
}
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
alert(result || "error occured");
|
||||||
|
|
||||||
|
alert( `execution took ${diff}ms` );
|
||||||
|
```
|
||||||
|
|
||||||
|
Here `finally` guarantees that the time will be measured in both situations -- in case of a successful execution of `fib` and in case of an error.
|
||||||
|
|
||||||
|
You can check that by running the code with `num=35` -- executes normally, `finally` after `try`, and then with `num=-1`, there will be an immediate error, an the execution will take `0ms`.
|
||||||
|
|
||||||
|
```smart header="Variables are local to try..catch..finally clauses"
|
||||||
|
Please note that `result` and `diff` variables are declared *before* `try..catch`.
|
||||||
|
|
||||||
|
Otherwise, if `let` is made inside the `{...}` clause, it is only visible inside of it.
|
||||||
|
```
|
||||||
|
|
||||||
|
````smart header="`finally` and `return`"
|
||||||
|
Finally clause works for *any* exit from `try..catch` including `return`.
|
||||||
|
|
||||||
|
In the example below, there's a `return` in `try`. In this case, `finally` is executed just before the control returns to the outer code.
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function func() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
/* ... */
|
||||||
|
} finally {
|
||||||
|
*!*
|
||||||
|
alert( 'finally' );
|
||||||
|
*/!*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert( func() ); // first works alert from finally, and then this one
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
````smart header="`try..finally`"
|
||||||
|
|
||||||
|
The `try..finally` construct, without `catch` clause, is used when we don't want or don't know how to handle errors, but want to be sure that processes that we started are finalized.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function func() {
|
||||||
|
// start doing something that needs completion
|
||||||
|
try {
|
||||||
|
// ...
|
||||||
|
} finally {
|
||||||
|
// complete that thing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
## Global catch
|
||||||
|
|
||||||
|
Let's imagine we've got a fatal error outside of `try..catch`, and the script died.
|
||||||
|
|
||||||
|
Is there a way to react on it?
|
||||||
|
|
||||||
|
There is none in the specification, but environments may provide it.
|
||||||
|
|
||||||
|
Node.JS has [process.on('uncaughtException')](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property. It will run in case of an uncaught error.
|
||||||
|
|
||||||
|
The syntax:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.onerror = function(message, url, line, col, error) {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`message`
|
||||||
|
: Error message.
|
||||||
|
|
||||||
|
`url`
|
||||||
|
: URL of the script where error happened.
|
||||||
|
|
||||||
|
`line`, `col`
|
||||||
|
: Line and column numbers where error happened.
|
||||||
|
|
||||||
|
`error`
|
||||||
|
: Error object.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html run untrusted refresh height=1
|
||||||
|
<script>
|
||||||
|
*!*
|
||||||
|
window.onerror = function(message, url, line, col, error) {
|
||||||
|
alert(`${message}\n At ${line}:${col} of ${url}`);
|
||||||
|
};
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
function readData() {
|
||||||
|
badFunc(); // wops, something went wrong!
|
||||||
|
}
|
||||||
|
|
||||||
|
readData();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The role of the global handler is usually not to recover the script execution -- that's probably impossible, but to send the error message to developers.
|
||||||
|
|
||||||
|
There are also web-services that provide error-logging facilities, like <https://errorception.com> or <http://www.muscula.com>. They give a script with custom `window.onerror` function, and once inserted into a page, it reports about all errors it gets to their server. Afterwards developers can browse them and get notifications on email about fresh errors.
|
||||||
|
|
||||||
|
## Summary [todo]
|
||||||
|
|
||||||
|
Обработка ошибок -- большая и важная тема.
|
||||||
|
|
||||||
|
В JavaScript для этого предусмотрены:
|
||||||
|
|
||||||
|
- Конструкция `try..catch..finally` -- она позволяет обработать произвольные ошибки в блоке кода.
|
||||||
|
|
||||||
|
Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего.
|
||||||
|
|
||||||
|
Кроме того, иногда проверить просто невозможно, например `JSON.parse(str)` не позволяет "проверить" формат строки перед разбором. В этом случае блок `try..catch` необходим.
|
||||||
|
|
||||||
|
Полный вид конструкции:
|
||||||
|
|
||||||
|
```js
|
||||||
|
*!*try*/!* {
|
||||||
|
.. пробуем выполнить код ..
|
||||||
|
} *!*catch*/!*(e) {
|
||||||
|
.. перехватываем исключение ..
|
||||||
|
} *!*finally*/!* {
|
||||||
|
.. выполняем всегда ..
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Возможны также варианты `try..catch` или `try..finally`.
|
||||||
|
- Оператор `throw err` генерирует свою ошибку, в качестве `err` рекомендуется использовать объекты, совместимые с встроенным типом [Error](http://javascript.ru/Error), содержащие свойства `message` и `name`.
|
||||||
|
|
||||||
|
Кроме того, мы рассмотрели некоторые важные приёмы:
|
||||||
|
|
||||||
|
- Проброс исключения -- `catch(err)` должен обрабатывать только те ошибки, которые мы рассчитываем в нём увидеть, остальные -- пробрасывать дальше через `throw err`.
|
||||||
|
|
||||||
|
Определить, нужная ли это ошибка, можно, например, по свойству `name`.
|
||||||
|
- Оборачивание исключений -- функция, в процессе работы которой возможны различные виды ошибок, может "обернуть их" в одну общую ошибку, специфичную для её задачи, и уже её пробросить дальше. Чтобы, при необходимости, можно было подробно определить, что произошло, исходную ошибку обычно присваивают в свойство этой, общей. Обычно это нужно для логирования.
|
||||||
|
- В `window.onerror` можно присвоить функцию, которая выполнится при любой "выпавшей" из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.
|
||||||
|
|
||||||
|
Later we'll learn more oop and inheritance for errors.
|
||||||
|
|
||||||
|
TODO: try..catch in decorators? If I leave it here.
|
||||||
|
|
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 64 KiB |
|
@ -1 +0,0 @@
|
||||||
# Deeper
|
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
@ -4,7 +4,9 @@ importance: 5
|
||||||
|
|
||||||
# Are counters independent?
|
# Are counters independent?
|
||||||
|
|
||||||
What is the second counter going to show? `0,1` or `2,3` or something else?
|
Here we make two counters: `counter` and `counter2` using the same `makeCounter` function.
|
||||||
|
|
||||||
|
Are they independent? What is the second counter going to show? `0,1` or `2,3` or something else?
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function makeCounter() {
|
function makeCounter() {
|
||||||
|
@ -15,8 +17,8 @@ function makeCounter() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var counter = makeCounter();
|
let counter = makeCounter();
|
||||||
var counter2 = makeCounter();
|
let counter2 = makeCounter();
|
||||||
|
|
||||||
alert( counter() ); // 0
|
alert( counter() ); // 0
|
||||||
alert( counter() ); // 1
|
alert( counter() ); // 1
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
Both nested functions are created within the same Lexical Environment.
|
||||||
|
|
||||||
|
So they share the same `count`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function Counter() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
this.up = function() {
|
||||||
|
return ++count;
|
||||||
|
};
|
||||||
|
this.down = function() {
|
||||||
|
return --count;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = new Counter();
|
||||||
|
|
||||||
|
alert( counter.up() ); // 1
|
||||||
|
alert( counter.up() ); // 2
|
||||||
|
alert( counter.down() ); // 1
|
||||||
|
```
|
|
@ -0,0 +1,29 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Counter object
|
||||||
|
|
||||||
|
Here a counter object is made with the help of the constructor function.
|
||||||
|
|
||||||
|
Will it work? What will it show?
|
||||||
|
|
||||||
|
```js
|
||||||
|
function Counter() {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
this.up = function() {
|
||||||
|
return ++count;
|
||||||
|
};
|
||||||
|
this.down = function() {
|
||||||
|
return --count;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = new Counter();
|
||||||
|
|
||||||
|
alert( counter.up() ); // ?
|
||||||
|
alert( counter.up() ); // ?
|
||||||
|
alert( counter.down() ); // ?
|
||||||
|
```
|
||||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
@ -1,9 +1,7 @@
|
||||||
|
|
||||||
# Closure
|
# Closure
|
||||||
|
|
||||||
Javascript is a very function-oriented language. There's a lot of freedom.
|
Javascript is a very function-oriented language, it gives a lot of freedom. A function can be created at one moment, then passed as a value to another variable or function and called from a totally different place much later.
|
||||||
|
|
||||||
A function can be created at one moment, then passed as a value to another variable or function and called from a totally different place much later. In different environments, a function can be assigned to run on various events: mouse clicks, network requests etc.
|
|
||||||
|
|
||||||
We know that a function can access variables outside of it. And this feature is used quite often.
|
We know that a function can access variables outside of it. And this feature is used quite often.
|
||||||
|
|
||||||
|
@ -11,15 +9,15 @@ But what happens when outer variables have change? Does a function get a new val
|
||||||
|
|
||||||
Also, what happens when a function travels to another place of the code -- will it get access to variables in the new place?
|
Also, what happens when a function travels to another place of the code -- will it get access to variables in the new place?
|
||||||
|
|
||||||
We realy should understand what's going on before doing complex things with functions. There is no general programming answer for that. Different languages behave differently. Here we'll cover Javascript of course.
|
We realy should understand what's going on before doing complex things with functions. There is no general programming answer for that. Different languages behave differently. Here we'll cover Javascript.
|
||||||
|
|
||||||
[cut]
|
[cut]
|
||||||
|
|
||||||
## Introductory questions
|
## A couple of questions
|
||||||
|
|
||||||
Let's formulate two questions for the seed, and then lay out the internal mechanics piece-by-piece, so that you can easily see what's going on and have no problems in the future.
|
Let's formulate two questions for the seed, and then study internal mechanics piece-by-piece, so that you will be able to answer them and have no problems with similar ones in the future.
|
||||||
|
|
||||||
1. A function uses an external variable. The variable changes. Will it pick up a new variant?
|
1. A function uses an external variable `name`. The variable changes before function runs. Will it pick up a new variant?
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let name = "John";
|
let name = "John";
|
||||||
|
@ -30,43 +28,48 @@ Let's formulate two questions for the seed, and then lay out the internal mechan
|
||||||
|
|
||||||
name = "Pete";
|
name = "Pete";
|
||||||
|
|
||||||
sayHi(); // what does it show?
|
sayHi(); // what will it show: "John" or "Pete"?
|
||||||
```
|
```
|
||||||
|
|
||||||
Such situation often occurs in practice when we assign a function to be called on some action using in-browser mechanisms (or similar things for other JS environments).
|
Such situations are common in both browser and server-side development. A function may be assigned to execute on user action or network request etc.
|
||||||
|
|
||||||
Things may change between the function creation and invocation. The question is: will it pick up the changes.
|
So, the the question is: does it pick up latest changes?
|
||||||
|
|
||||||
|
|
||||||
2. A function makes another function and returns it. Will it access outer variables from its creation place or the invocation place?
|
2. A function can make another function and return it. That new function can be called from somewhere else. Will it have access to outer variables from its creation place or the invocation place or maybe both?
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function makeWorker() {
|
function makeWorker() {
|
||||||
let name = "Pete";
|
let name = "Pete";
|
||||||
|
|
||||||
return function() {
|
return function() {
|
||||||
alert(name); // where from?
|
alert(name);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = "John";
|
let name = "John";
|
||||||
|
|
||||||
|
// create a function
|
||||||
let work = makeWorker();
|
let work = makeWorker();
|
||||||
|
|
||||||
work(); // what does it show? "Pete" (name where created) or "John" (name where called)?
|
// call it
|
||||||
|
work(); // what will it show? "Pete" (name where created) or "John" (name where called)?
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Lexical Environment
|
## Lexical Environment
|
||||||
|
|
||||||
To understand what's going on, let's first discuss what a "variable" really is.
|
To understand what's going on, let's first discuss what a "variable" technically is.
|
||||||
|
|
||||||
In Javascript, every running function, a code block and the script as a whole has an associated object named *Lexical Environment*.
|
In Javascript, every running function, code block and the script as a whole have an associated object named *Lexical Environment*.
|
||||||
|
|
||||||
The Lexical Environment consists of two parts:
|
The Lexical Environment object consists of two parts:
|
||||||
|
|
||||||
1. An object that stores all local variables (and other information of this kind like value of `this`). It is called *Environment Record* in the specification.
|
1. *Environment Record* -- an object that has all local variables as its properties (and some other information like the value of `this`).
|
||||||
2. An reference to the *outer lexical environment*, the one associated with the structure right outside of it.
|
2. An reference to the *outer lexical environment*, the one associated with the structure right outside of it.
|
||||||
|
|
||||||
|
So, a "variable" is just a property of the special internal object, Environment Record. "To get or change a variable" means "to get or change the property".
|
||||||
|
|
||||||
For instance, in this simple code, there is only one Lexical Environment:
|
For instance, in this simple code, there is only one Lexical Environment:
|
||||||
|
|
||||||

|

|
||||||
|
@ -75,28 +78,35 @@ This is a so-called global Lexical Environment, associated with the whole script
|
||||||
|
|
||||||
On the picture above the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer one, so that's `null`.
|
On the picture above the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer one, so that's `null`.
|
||||||
|
|
||||||
Technically, the term "variable" means a property of the Lexical Environment (its Environment Record to be precise). "To get or change a variable" means "to get or change the property".
|
|
||||||
|
|
||||||
Here's the bigger picture of how `let` variables work:
|
Here's the bigger picture of how `let` variables work:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- When a script starts, the Lexical Environment is empty.
|
1. When a script starts, the Lexical Environment is empty.
|
||||||
- The `let phrase` definition appears. Now it initially has no value, so `undefined` is stored.
|
2. The `let phrase` definition appears. Now it initially has no value, so `undefined` is stored.
|
||||||
- The `phrase` is assigned.
|
3. The `phrase` is assigned.
|
||||||
- The `phrase` changes the value.
|
4. The `phrase` changes the value.
|
||||||
|
|
||||||
Now let's add a function:
|
To summarize:
|
||||||
|
|
||||||
|
- A variable is a property of a special internal object, associated with the currently executing block/function/script.
|
||||||
|
- Working with variables is actually working with the properties of that object.
|
||||||
|
|
||||||
|
### Function Declaration
|
||||||
|
|
||||||
|
Function Declarations are a bit special. They are processed when a Lexical Environment is created. For the global Lexical Environment, it means the moment when the script is started.
|
||||||
|
|
||||||
|
That's why we can use them even before they are defined, like `say` in the code below:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Here we see an important moment related to Function Declarations. They are processed when a Lexical Environment is created. For the global Lexical Environment, it means the moment when the script is started. So `say` exists from the very beginning (and we can call it prior to declaration).
|
Here we have `say` defined from the beginning of the script, and `let` appears a bit later.
|
||||||
|
|
||||||
The `let` definition is processed normally, so it is added later.
|
### Inner and outer Lexical Environment
|
||||||
|
|
||||||
Now let's run the function `say()`.
|
Now let's run the function `say()`.
|
||||||
|
|
||||||
When it runs, the new function Lexical Environment is created automatically, for variables and parameters of the current function call:
|
When a function runs, a new function Lexical Environment is created automatically, for variables and parameters of the call:
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
```js
|
```js
|
||||||
|
@ -112,22 +122,23 @@ say("John"); // Hello, John
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
```smart
|
||||||
|
Please note that a function Lexical Environment is only created when a function starts executing!
|
||||||
|
|
||||||
|
And if a function is called multiple times, then each invocation will have its own Lexical Environment, with local variables and parameters specific for that very run.
|
||||||
|
```
|
||||||
|
|
||||||
We now have two Lexical Environments: the inner (for the function call) and the outer (global):
|
We now have two Lexical Environments: the inner (for the function call) and the outer (global):
|
||||||
|
|
||||||
- The inner Lexical Environment corresponds to the current `sayHi` execution. It has a single variable: `name`.
|
- The inner Lexical Environment corresponds to the current `say` execution. It has a single variable: `name`.
|
||||||
- The outer Lexical Environment is the one outside of the function, it stores `phrase` and function `say`.
|
- The outer Lexical Environment is the one where the function is defined, "right outside" of its definition. Here the function is defined in the global Lexical Environment, so this is it. The inner Lexical Environment references the global one.
|
||||||
- The inner Lexical Environment references the global one.
|
|
||||||
|
|
||||||
As we'll see further, functions can be more nested, so the chain of outer references can be longer.
|
|
||||||
|
|
||||||
|
|
||||||
Please note that the Lexical Environment for function `say` is only created when the function starts executing! And if the function is called multiple times, then each invocation has it's own Lexical Environment, with local variables and parameters for the current run.
|
**When a code wants to access a variable -- it is first searched in the inner Lexical Environment, then in the outer one, and further until the end of the chain.**
|
||||||
|
|
||||||
**When a code wants to access a variable -- it is first searched in the current Lexical Environment, then in the outer one, and further until the end of the chain.**
|
If a variable is not found anywhere, that's an error in strict mode. Without `use strict` an assignment to an undefined variable is possible, but is not a good thing.
|
||||||
|
|
||||||
If not found, that's always an error in strict mode. Without `use strict` an assignment to an undefined variable is possible (`phrase="Hello"` without `let`), but is not a good thing.
|
Let's see what it means for our example:
|
||||||
|
|
||||||
In our example:
|
|
||||||
|
|
||||||
- When the `alert` inside `say` wants to access `name`, it is found immediately in the function Lexical Environment.
|
- When the `alert` inside `say` wants to access `name`, it is found immediately in the function Lexical Environment.
|
||||||
- When the code wants to access `phrase`, then there is no `phrase` locally, so follows the `outer` reference and finds it globally.
|
- When the code wants to access `phrase`, then there is no `phrase` locally, so follows the `outer` reference and finds it globally.
|
||||||
|
@ -164,22 +175,22 @@ The execution flow of the code above:
|
||||||
3. When the function `say()`, is executed and takes `name` from outside. Here that's from the global Lexical Environment where it's already `"Pete"`.
|
3. When the function `say()`, is executed and takes `name` from outside. Here that's from the global Lexical Environment where it's already `"Pete"`.
|
||||||
|
|
||||||
```smart header="Lexical Environment is a specification object"
|
```smart header="Lexical Environment is a specification object"
|
||||||
A Lexical Environment is an internal object. We can't get this object in our code and manipulate its properties.
|
"Lexical Environment" is a specification object. We can't get this object in our code and manipulate it directly. Javascript engines also may tweak it, exclude variables that are obviously unused and apply other optimizations.
|
||||||
|
|
||||||
Also Javascript engines are not required to use exactly objects to implement the functionality. The code must behave as if it uses them, but technically it may be much more optimized.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Nested functions
|
## Nested functions
|
||||||
|
|
||||||
We can create a function everywhere in Javascript. Even inside another function.
|
A function is called "nested" when it is created inside another function.
|
||||||
|
|
||||||
That can help to organize the code, like this:
|
Technically, that is easily possible.
|
||||||
|
|
||||||
|
We can use it to organize the code, like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function sayHiBye(firstName, lastName) {
|
function sayHiBye(firstName, lastName) {
|
||||||
|
|
||||||
// helper function to use below
|
// helper nested function to use below
|
||||||
function getFullName() {
|
function getFullName() {
|
||||||
return firstName + " " + lastName;
|
return firstName + " " + lastName;
|
||||||
}
|
}
|
||||||
|
@ -190,11 +201,26 @@ function sayHiBye(firstName, lastName) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here the *nested* function `getFullName()` has access to the outer variables.
|
Here the *nested* function `getFullName()` is made for convenience. It has access to outer variables.
|
||||||
|
|
||||||
What's more interesting, a nested function can be returned. And used somewhere else in the code. And nevertheless it still keeps the access to the same outer variables.
|
What's more interesting, a nested function can be returned: as a property of an object or as a result by itself. And then used somewhere else. But no matter where, it still keeps the access to the same outer variables.
|
||||||
|
|
||||||
An example:
|
An example with [constructor function](info:constructor-new):
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// constructor function returns a new object
|
||||||
|
function User(name) {
|
||||||
|
// method is actually a nested function
|
||||||
|
this.sayHi = function() {
|
||||||
|
alert(name);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = new User("John");
|
||||||
|
user.sayHi();
|
||||||
|
```
|
||||||
|
|
||||||
|
An example with function result:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
function makeCounter() {
|
function makeCounter() {
|
||||||
|
@ -212,18 +238,17 @@ alert( counter() ); // 1
|
||||||
alert( counter() ); // 2
|
alert( counter() ); // 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We'll continue on with `makeCounter` example. It creates the "counter" function that returns the next number on each invocation. Despite being simple, such code structure still has practical applications, for instance, a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator).
|
||||||
The `makeCounter()` creates a "counter" function that returns the next number on each invocation. In more complex cases, the counter might be a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) or an object, but let's stay simple for now.
|
|
||||||
|
|
||||||
The questions may arise:
|
The questions may arise:
|
||||||
|
|
||||||
1. How it works?
|
1. How it works?
|
||||||
2. What happens if there is a global variable named `count`? Can it confuse the `counter`?
|
2. Can we somehow reset the counter?
|
||||||
3. What if we call `makeCounter` multiple times? Are the resulting `counter` functions independent or they share the same count?
|
3. What if we call `makeCounter` multiple times? Are the resulting `counter` functions independent or they share the same `count`?
|
||||||
|
|
||||||
We'll answer the 1st question and the other ones will also become obvious.
|
The first question has two answers: a simple one, easy to understand and apply, and a complex one, involving knowledge of internals.
|
||||||
|
|
||||||
**The visually simple rule: a function looks for a variable from inside to outside.**
|
The simple one is: a function looks for a variable from inside to outside.
|
||||||
|
|
||||||
So, for the example, above, the order will be:
|
So, for the example, above, the order will be:
|
||||||
|
|
||||||
|
@ -233,19 +258,19 @@ So, for the example, above, the order will be:
|
||||||
2. The variables of the outer function.
|
2. The variables of the outer function.
|
||||||
3. ...And further until it reaches globals, then the search stops.
|
3. ...And further until it reaches globals, then the search stops.
|
||||||
|
|
||||||
No matter where the function is called, the rule is the same.
|
No matter where the function is called, the rule is the same. Doesn't matter where the function is called: it searches variables from where it was born.
|
||||||
|
|
||||||
**If a variable is modified, it is modified on the place where it is found, so future accesses will get the updated variant.**
|
Then, if a variable is found and modified, it's changed on the place where it is found.
|
||||||
|
|
||||||
So `count++` finds the outer variable and increases it "at place" every time, thus returning the next value every time.
|
So `count++` finds the outer variable and increases it "at place" every time, thus returning the next value every time.
|
||||||
|
|
||||||
The rule is good for eyes and usually enough, but in more complex situations, the more solid understanding of internals may be needed. So here you go.
|
That simple rule is usually good enough, but in more complex situations, more in-depth understanding of internals may be needed. So here you go.
|
||||||
|
|
||||||
## Environments in detail
|
## Environments in detail
|
||||||
|
|
||||||
When a function is created, it has a special hidden property `[[Environment]]` that keeps the reference to the Lexical Environment where it is created. So it kind of remembers where it was made. And when the function runs, this property is used as the outer lexical reference, giving the direction for the search.
|
For a more in-depth understanding, this section elaborates `makeCounter` example in more detail, adding some missing pieces.
|
||||||
|
|
||||||
Let's go low-level and examine how `counter()` works:
|
Here's what's going on step-by-step:
|
||||||
|
|
||||||
1. When the script has just started, there is only global Lexical Environment:
|
1. When the script has just started, there is only global Lexical Environment:
|
||||||
|
|
||||||
|
@ -253,17 +278,27 @@ Let's go low-level and examine how `counter()` works:
|
||||||
|
|
||||||
At this moment there is only `makeCounter` function. And it did not run yet.
|
At this moment there is only `makeCounter` function. And it did not run yet.
|
||||||
|
|
||||||
All functions "on birth" receive a hidden property `[[Environment]]` with the reference to the Lexical Environment of creation. For `makeCounter` that's the global one.
|
All functions "on birth" receive a hidden property `[[Environment]]` with the reference to the Lexical Environment of their creation. So a function kind of remembers where it was made. In the future, when the function runs, `[[Environment]]` is used as the outer lexical reference.
|
||||||
|
|
||||||
2. The code runs on, and the call to `makeCounter()` is performed:
|
Here, `makeCounter` is created in the global Lexical Environment, so `[[Environment]]` keeps the reference to it.
|
||||||
|
|
||||||
|
2. Then the code runs on, and the call to `makeCounter()` is performed:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The Lexical Environment for the `makeCounter()` call is created. It stores local variables, in our case `count: 0` is the only local variable. The property `[[Environment]]` of `makeCounter` is used as an outer lexical reference for the new Lexical Environment, so it points to the global one.
|
The Lexical Environment for the `makeCounter()` call is created.
|
||||||
|
|
||||||
Now we have two Lexical Environments: the first one is global, the second one is for the current `makeCounter` call.
|
As all Lexical Environments, it stores two things:
|
||||||
|
- Environment Record with local variables, in our case `count` is the only local variable.
|
||||||
|
- The outer lexical reference, which is set using `[[Environment]]` of the function.
|
||||||
|
|
||||||
3. During the execution of `makeCounter()` the tiny nested function is created. Here Function Expression is used to define the function. But that doesn't matter. All functions get the `[[Environment]]` property that references the Lexical Environment where they are made. For our new nested function that is the current Lexical Environment of `makeCounter()`:
|
Here `[[Environment]]` of `makeCounter` references the global Lexical Environment. So, now we have two Lexical Environments: the first one is global, the second one is for the current `makeCounter` call, with the outer reference to global.
|
||||||
|
|
||||||
|
3. During the execution of `makeCounter()`, a tiny nested function is created.
|
||||||
|
|
||||||
|
It doesn't matter how the function is created: using Function Declaration or Function Expression or an object method. All functions get the `[[Environment]]` property that references the Lexical Environment where they are made.
|
||||||
|
|
||||||
|
For our new nested function that is the current Lexical Environment of `makeCounter()`:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -271,11 +306,13 @@ Let's go low-level and examine how `counter()` works:
|
||||||
|
|
||||||
So we still have two Lexical Environments. And a function which has `[[Environment]]` referencing to the inner one of them.
|
So we still have two Lexical Environments. And a function which has `[[Environment]]` referencing to the inner one of them.
|
||||||
|
|
||||||
4. As the execution goes on, the call to `makeCounter()` finishes, and the result (the tiny nested function) is assigned to the global variable `counter`. When we run `counter()`, the single line of code will be executed: `return count++`.
|
4. As the execution goes on, the call to `makeCounter()` finishes, and the result (the tiny nested function) is assigned to the global variable `counter`:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
5. Then the `counter()` call executes. An "empty" Lexical Environment is created (no local variables). But the `[[Environment]]` is used for its outer reference, providing access to the variables of the former `makeCounter()` call:
|
That function has only one line: `return count++`, that will be executed when we run it.
|
||||||
|
|
||||||
|
5. When the `counter()` is called, an "empty" Lexical Environment is created for it. It has no local variables by itself. But the `[[Environment]]` of `counter` is used for the outer reference, so it has access to the variables of the former `makeCounter()` call, where it was created:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -283,9 +320,9 @@ Let's go low-level and examine how `counter()` works:
|
||||||
|
|
||||||
When it looks for `count`, it finds it among the variables `makeCounter`, in the nearest outer Lexical Environment.
|
When it looks for `count`, it finds it among the variables `makeCounter`, in the nearest outer Lexical Environment.
|
||||||
|
|
||||||
The funny thing is that `makeCounter()` finished some time ago. But its variables are still alive, and accessible from a nested function.
|
Please note how memory management works here. When `makeCounter()` call finished some time ago, its Lexical Environment was retained in memory, because there's a nested function with `[[Environment]]` referencing it.
|
||||||
|
|
||||||
When `counter()` finishes, its Lexical Environment is cleared from memory. There are no nested function or other reason to keep it. But the old Lexical Environment of `makeCounter` is retained for future accesses.
|
Generally, a Lexical Environment object lives until there is a function which may use it. And when there are none, it is cleared.
|
||||||
|
|
||||||
6. The call to `counter()` not only returns the value of `count`, but also increases it. Note that the modification is done "at place". The value of `count` is modified exactly in the environment where it was found.
|
6. The call to `counter()` not only returns the value of `count`, but also increases it. Note that the modification is done "at place". The value of `count` is modified exactly in the environment where it was found.
|
||||||
|
|
||||||
|
@ -293,6 +330,7 @@ Let's go low-level and examine how `counter()` works:
|
||||||
|
|
||||||
So we return to the previous step with the only change -- the new value of `count`. The following calls all do the same.
|
So we return to the previous step with the only change -- the new value of `count`. The following calls all do the same.
|
||||||
|
|
||||||
|
7. Next `counter()` invocations do the same.
|
||||||
|
|
||||||
The answer to the second seed question should now be obvious.
|
The answer to the second seed question should now be obvious.
|
||||||
|
|
||||||
|
@ -300,14 +338,14 @@ The `work()` function in the code below uses the `name` from the place of its or
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
...But if there were no `name` in `makeWorker()`, then the search would go outside and take the global variable as we can see from the chain above.
|
So, the result is `"Pete"`.
|
||||||
|
|
||||||
The same for `counter()` calls. The closest outer variable is always used.
|
...But if there were no `name` in `makeWorker()`, then the search would go outside and take the global variable as we can see from the chain above. In that case it would be `"John"`.
|
||||||
|
|
||||||
```smart header="Closures"
|
```smart header="Closures"
|
||||||
There is a general programming term "closure", that developers generally should know.
|
There is a general programming term "closure", that developers generally should know.
|
||||||
|
|
||||||
A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible or needs to be explicitly specified. But as explained above, in Javascript all functions are closures.
|
A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in Javascript all functions are closures.
|
||||||
|
|
||||||
That is: all of them automatically remember where they are created using a hidden `[[Environment]]` property, and all of them can access outer variables.
|
That is: all of them automatically remember where they are created using a hidden `[[Environment]]` property, and all of them can access outer variables.
|
||||||
|
|
||||||
|
@ -316,7 +354,9 @@ When on an interview a frontend developer gets a question about "what's a closur
|
||||||
|
|
||||||
## Code blocks and loops, IIFE
|
## Code blocks and loops, IIFE
|
||||||
|
|
||||||
A code block has it's own Lexical Environment and hence local variables.
|
The examples above concentrated on functions. But Lexical Environments also exist for code blocks `{...}`.
|
||||||
|
|
||||||
|
They are created when a code block runs and contain block-local variables.
|
||||||
|
|
||||||
In the example below, when the execution goes into `if` block, the new Lexical Environment is created for it:
|
In the example below, when the execution goes into `if` block, the new Lexical Environment is created for it:
|
||||||
|
|
||||||
|
@ -334,12 +374,11 @@ alert(user); // Error, can't see such variable!
|
||||||
```
|
```
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The new Lexical Environment gets the enclosing one as the outer reference, so `phrase` can be found. But all variables and Function Expressions declared inside `if`, will reside in that Lexical Environment.
|
The new Lexical Environment gets the enclosing one as the outer reference, so `phrase` can be found. But all variables and Function Expressions declared inside `if`, will reside in that Lexical Environment.
|
||||||
|
|
||||||
After `if` finishes, its Lexical Environment is normally destroyed (unless there's a living nested function). That's why the `alert` below won't see the `user`.
|
After `if` finishes, the `alert` below won't see the `user`.
|
||||||
|
|
||||||
For a loop, every run has a separate Lexical Environment. The loop variable is its part:
|
For a loop, every run has a separate Lexical Environment. The loop variable is its part:
|
||||||
|
|
||||||
|
@ -384,11 +423,11 @@ They look like this:
|
||||||
|
|
||||||
Here a Function Expression is created and immediately called. So the code executes right now and has its own private variables.
|
Here a Function Expression is created and immediately called. So the code executes right now and has its own private variables.
|
||||||
|
|
||||||
The Function Expression is wrapped with brackets `(function {...})`, because otherwise Javascript would try to read it as Function Declaration:
|
The Function Expression is wrapped with brackets `(function {...})`, because when Javascript meets `"function"` in the main code flow, it understands it as a start of Function Declaration. But a Function Declaration must have a name, so there will be an error:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
// Error: Unexpected token (
|
// Error: Unexpected token (
|
||||||
function() { // <-- JavaScript assumes it is a Function Declarations, but no name
|
function() { // <-- JavaScript cannot find function name, meets ( and gives error
|
||||||
|
|
||||||
let message = "Hello";
|
let message = "Hello";
|
||||||
|
|
||||||
|
@ -397,50 +436,59 @@ function() { // <-- JavaScript assumes it is a Function Declarations, but no nam
|
||||||
}();
|
}();
|
||||||
```
|
```
|
||||||
|
|
||||||
...But we can't use Function Declaration here, because Javascript does not allow Function Declarations to be called immediately:
|
We can say "okay, let it be Function Declaration, let's add a name", but it won't work. Javascript does not allow Function Declarations to be called immediately:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
// syntax error because of brackets below
|
// syntax error because of brackets below
|
||||||
function go() {
|
function go() {
|
||||||
|
|
||||||
}(); // <-- okay for Function Expressions, error for Function Declarations
|
}(); // <-- can't call Function Declaration immediately
|
||||||
```
|
```
|
||||||
|
|
||||||
So the brackets are needed to show Javascript that the function is created in the context of another expression, and hence it's Function Declaration.
|
So the brackets are needed to show Javascript that the function is created in the context of another expression, and hence it's a Function Expression. Needs no name and can be called immediately.
|
||||||
|
|
||||||
There are other ways to do that:
|
There are other ways to tell Javascript that we mean Function Expression:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
!function() {
|
// Ways to create IIFE
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
alert("Brackets around the function");
|
||||||
|
}*!*)*/!*();
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
alert("Brackets around the whole thing");
|
||||||
|
}()*!*)*/!*;
|
||||||
|
|
||||||
|
*!*!*/!*function() {
|
||||||
alert("Bitwise NOT operator starts the expression");
|
alert("Bitwise NOT operator starts the expression");
|
||||||
}();
|
}();
|
||||||
|
|
||||||
+function() {
|
*!*+*/!*function() {
|
||||||
alert("Unary plus starts the expression");
|
alert("Unary plus starts the expression");
|
||||||
}();
|
}();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In all cases above we declare a Function Expression and run it immediately.
|
||||||
|
|
||||||
## Garbage collection
|
## Garbage collection
|
||||||
|
|
||||||
Lexical Environment objects that we've been talking about follow same garbage collection rules as regular ones.
|
Lexical Environment objects that we've been talking about are subjects to same memory management rules as regular values.
|
||||||
|
|
||||||
So, a Lexical Environment exists while there's a nested function referencing it with its `[[Environment]]`.
|
- Usually, Lexical Environment is cleaned up after the function run. For instance:
|
||||||
|
|
||||||
- Usually, Lexical Environment is cleaned up after the function run. Even if it has a nested function, for instance:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function f() {
|
function f() {
|
||||||
let value = 123;
|
let value1 = 123;
|
||||||
|
let value2 = 456;
|
||||||
function g() {} // g is local
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f();
|
f();
|
||||||
```
|
```
|
||||||
|
|
||||||
Here both `value` and `g` become unreachable after the end of `f()`, and so even though `g` references its outer lexical environment, that doesn't matter.
|
It's obvious that both values are unaccessible after the end of `f()`. Formally, there are no references to Lexical Environment object with them, so it gets cleaned up.
|
||||||
|
|
||||||
- But if `g` were returned and kept reachable, then that its reference keeps the outer lexical environment alive as well:
|
- But if there's a nested function that is still reachable after the end of `f`, then its `[[Environment]]` reference keeps the outer lexical environment alive as well:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function f() {
|
function f() {
|
||||||
|
@ -453,7 +501,7 @@ So, a Lexical Environment exists while there's a nested function referencing it
|
||||||
*/!*
|
*/!*
|
||||||
}
|
}
|
||||||
|
|
||||||
let g = f(); // function g will live and keep the outer lexical environment in memory
|
let g = f(); // g is reachable, and keeps the outer lexical environment in memory
|
||||||
```
|
```
|
||||||
|
|
||||||
- If `f()` is called many times, and resulting functions are saved, then the corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the code below:
|
- If `f()` is called many times, and resulting functions are saved, then the corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the code below:
|
||||||
|
@ -467,9 +515,10 @@ So, a Lexical Environment exists while there's a nested function referencing it
|
||||||
|
|
||||||
// 3 functions in array, every of them links to LexicalEnvrironment
|
// 3 functions in array, every of them links to LexicalEnvrironment
|
||||||
// from the corresponding f() run
|
// from the corresponding f() run
|
||||||
|
// LE LE LE
|
||||||
let arr = [f(), f(), f()];
|
let arr = [f(), f(), f()];
|
||||||
```
|
```
|
||||||
- A Lexical Environment object lives until it is reachable. In the code below, after `g` becomes unreachable, it dies with it:
|
- A Lexical Environment object dies when no nested functions remain that reference it. In the code below, after `g` becomes unreachable, it dies with it:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function f() {
|
function f() {
|
||||||
|
@ -490,11 +539,9 @@ So, a Lexical Environment exists while there's a nested function referencing it
|
||||||
|
|
||||||
As we've seen, in theory while a function is alive, all outer variabels are also retained.
|
As we've seen, in theory while a function is alive, all outer variabels are also retained.
|
||||||
|
|
||||||
But in practice, JS engines try to optimize that. They analyze variable usage and if it's easy to see that an outer variable is not used -- it is removed.
|
But in practice, Javascript engines try to optimize that. They analyze variable usage and if it's easy to see that an outer variable is not used -- it is removed.
|
||||||
|
|
||||||
In the code above `value` is not used in `g`. So it will be cleaned up from the memory.
|
**An important side effect in V8 (Chrome, Opera) is that such variable will become unavailable in debugging.**
|
||||||
|
|
||||||
**Important side effect in V8 (Chrome, Opera) is that such variable will become unavailable in debugging!**
|
|
||||||
|
|
||||||
Try running the example below with the open Developer Tools in Chrome.
|
Try running the example below with the open Developer Tools in Chrome.
|
||||||
|
|
||||||
|
@ -515,9 +562,9 @@ var g = f();
|
||||||
g();
|
g();
|
||||||
```
|
```
|
||||||
|
|
||||||
As you could see -- there is no such variable! The engine decided that we won't need it and removed it.
|
As you could see -- there is no such variable! In theory, it should be accessible, but the engine optimized it out.
|
||||||
|
|
||||||
That may lead to funny (if not such time-consuming) debugging issues, especially when we can see instead of *our* variable the more outer one:
|
That may lead to funny (if not such time-consuming) debugging issues. One of them -- we can see a same-named outer variable instead of the expected one:
|
||||||
|
|
||||||
```js run global
|
```js run global
|
||||||
let value = "Surprise!";
|
let value = "Surprise!";
|
||||||
|
@ -552,11 +599,11 @@ In the very first chapter about [variables](info:variables), we mentioned three
|
||||||
2. `const`
|
2. `const`
|
||||||
3. `var`
|
3. `var`
|
||||||
|
|
||||||
Here we only talked about `let`. But `const` behaves totally the same way in terms of Lexical Environments.
|
`let` and `const` behave exactly the same way in terms of Lexical Environments.
|
||||||
|
|
||||||
The `var` is a very different beast, coming from old times. It's generally not used in modern scripts, but still lurks in the old ones. If you don't plan meeting such scripts you may even skip this subsection or postpone until it bites.
|
But `var` is a very different beast, that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones. If you don't plan meeting such scripts you may even skip this subsection or postpone, but then there's a chance that it bites you.
|
||||||
|
|
||||||
From the first sight, `var` behaves similar to `let`:
|
From the first sight, `var` behaves similar to `let`. That is, declares a variable:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
function sayHi() {
|
function sayHi() {
|
||||||
|
@ -570,9 +617,9 @@ sayHi();
|
||||||
alert(phrase); // Error, phrase is not defined
|
alert(phrase); // Error, phrase is not defined
|
||||||
```
|
```
|
||||||
|
|
||||||
...But let's list the differences.
|
...But here are the differences.
|
||||||
|
|
||||||
`var` variables only live in function and global Lexical Environments, they ignore blocks.
|
`var` variables are either funciton-wide or global, they are visible through blocks.
|
||||||
: For instance:
|
: For instance:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
@ -585,9 +632,9 @@ alert(phrase); // Error, phrase is not defined
|
||||||
*/!*
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
If we used `let test` on the 2nd line, then it wouldn't be visible to `alert`. But `var` variables ignore code blocks, so here we've got a global `test`.
|
If we used `let test` on the 2nd line, then it wouldn't be visible to `alert`. But `var` ignores code blocks, so we've got a global `test`.
|
||||||
|
|
||||||
The same thing for loops:
|
The same thing for loops: `var` can not be block or loop-local:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
for(var i = 0; i < 10; i++) {
|
for(var i = 0; i < 10; i++) {
|
||||||
|
@ -617,7 +664,7 @@ alert(phrase); // Error, phrase is not defined
|
||||||
As we can see, `var` pierces through `if`, `for` or other code blocks. That's because long time ago in Javascript blocks had no Lexical Environments. And `var` is a reminiscence of that.
|
As we can see, `var` pierces through `if`, `for` or other code blocks. That's because long time ago in Javascript blocks had no Lexical Environments. And `var` is a reminiscence of that.
|
||||||
|
|
||||||
`var` declarations are processed when the function starts (or script starts for globals).
|
`var` declarations are processed when the function starts (or script starts for globals).
|
||||||
: In other words, unlike `let/const` that appear at the moment of their declaration, `var` variables are defined from the beginning of the function.
|
: In other words, `var` variables are defined from the beginning of the function.
|
||||||
|
|
||||||
So this code:
|
So this code:
|
||||||
|
|
||||||
|
@ -666,7 +713,7 @@ alert(phrase); // Error, phrase is not defined
|
||||||
|
|
||||||
So in the example above, `if (false)` branch never executes, but that doesn't matter. The `var` inside it is processed in the beginning of the function.
|
So in the example above, `if (false)` branch never executes, but that doesn't matter. The `var` inside it is processed in the beginning of the function.
|
||||||
|
|
||||||
**The pitfall is that declarations are hoisted, but assignments are not.**
|
**The important thing is that declarations are hoisted, but assignments are not.**
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
|
@ -702,19 +749,30 @@ alert(phrase); // Error, phrase is not defined
|
||||||
sayHi();
|
sayHi();
|
||||||
```
|
```
|
||||||
|
|
||||||
The `alert` runs without an error, because the variable `phrase` is defined from the start of the function. But its value is assigned below, so it shows `undefined`.
|
**In other words, all `var` variables exist, but are `undefined` at the beginning of a function.**
|
||||||
|
|
||||||
|
Tthey are assigned later as the execution reaches assignments.
|
||||||
|
|
||||||
The features described above make using `var` inconvenient most of time. First, we can't create block-local variables. And hoisting just creates more space for errors. So, once again, for new scripts `var` is used exceptionally rarely.
|
In the example above, `alert` runs without an error, because the variable `phrase` exists. But its value is not yet assigned, so it shows `undefined`.
|
||||||
|
|
||||||
|
```warn header="Why `var` is not used"
|
||||||
|
Special behaviors of described in this section make using `var` inconvenient most of time. First, we can't create block-local variables. And hoisting just creates more space for errors. So, once again, for new scripts `var` is used exceptionally rarely.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Global object
|
## Global object
|
||||||
|
|
||||||
A *global object* is the object that provides access to built-in functions and values, defined by the specification and the environment.
|
When Javascript was created, there was an idea of a "global object" that provides all global variables and functions. It was planned that multiple in-browser scripts would use that single global object and share variables through it.
|
||||||
|
|
||||||
|
Since then, Javascript greatly evolved, and that idea of linking code through global variables became much less appealing. In modern Javascript, the concept of modules too its place.
|
||||||
|
|
||||||
|
But the global object still remains in the specification.
|
||||||
|
|
||||||
In a browser it is named "window", for Node.JS it is "global", for other environments it may have another name.
|
In a browser it is named "window", for Node.JS it is "global", for other environments it may have another name.
|
||||||
|
|
||||||
|
It does two things:
|
||||||
|
|
||||||
|
1. Provides access to built-in functions and values, defined by the specification and the environment.
|
||||||
For instance, we can call `alert` directly or as a method of `window`:
|
For instance, we can call `alert` directly or as a method of `window`:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
|
@ -724,9 +782,9 @@ alert("Hello");
|
||||||
window.alert("Hello");
|
window.alert("Hello");
|
||||||
```
|
```
|
||||||
|
|
||||||
And the same applies to other built-ins. E.g. we can use `window.Array` instead of `Array`.
|
The same applies to other built-ins. E.g. we can use `window.Array` instead of `Array`.
|
||||||
|
|
||||||
The global object also carries global Function Declarations and `var` variables. We can read them and write using its properties, for instance:
|
2. Provides access to global Function Declarations and `var` variables. We can read them and write using its properties, for instance:
|
||||||
|
|
||||||
<!-- no-strict to move variables out of eval -->
|
<!-- no-strict to move variables out of eval -->
|
||||||
```js untrusted run no-strict refresh
|
```js untrusted run no-strict refresh
|
||||||
|
@ -746,7 +804,7 @@ window.test = 5;
|
||||||
alert(test); // 5
|
alert(test); // 5
|
||||||
```
|
```
|
||||||
|
|
||||||
...But the global object does not have variables declared with `let/const`:
|
...But the global object does not have variables declared with `let/const`!
|
||||||
|
|
||||||
```js untrusted run no-strict refresh
|
```js untrusted run no-strict refresh
|
||||||
*!*let*/!* user = "John";
|
*!*let*/!* user = "John";
|
||||||
|
@ -756,15 +814,24 @@ alert(window.user); // undefined, don't have let
|
||||||
alert("user" in window); // false
|
alert("user" in window); // false
|
||||||
```
|
```
|
||||||
|
|
||||||
Here you can clearly see that `let user` is not in `window`.
|
```smart header="The global object is not a global Environment Record"
|
||||||
|
In versions of ECMAScript prior to ES-2015, there were no `let/const` variables, only `var`. And global object was used as a global Environment Record (wordings were a bit different, but that's the gist).
|
||||||
|
|
||||||
|
But starting from ES-2015, these entities are split apart. There's a global Lexical Environment with its Environment Record. And there's a global object that provides *some* of global variables.
|
||||||
|
|
||||||
|
As a practical difference, global `let/const` variables are definitively properties of the global Environment Record, but they do not exist in the global object.
|
||||||
|
|
||||||
|
Naturally, that's because the idea of a global object as a way to access "all global things" comes from ancient times. Nowadays is not considered to be a good thing. Modern language features like `let/const` do not make friends with it, but old ones are still compatible.
|
||||||
|
```
|
||||||
|
|
||||||
That's because the idea of a global object as a way to access "all global things" comes from ancient times. Nowadays is not considered to be a good thing. Modern language features like `let/const` do not make friends with it, but old ones try to be compatible.
|
|
||||||
|
|
||||||
### Uses of "window"
|
### Uses of "window"
|
||||||
|
|
||||||
In server-side environments like Node.JS, the `global` object is used exceptionally rarely. Probably it would be fair to say "never".
|
In server-side environments like Node.JS, the `global` object is used exceptionally rarely. Probably it would be fair to say "never".
|
||||||
|
|
||||||
In-browser `window` is sometimes used for following purposes:
|
In-browser `window` is sometimes used though.
|
||||||
|
|
||||||
|
Usually, it's not a good idea to use it, but here are some examples you can meet.
|
||||||
|
|
||||||
1. To access exactly the global variable if the function has the local one with the same name.
|
1. To access exactly the global variable if the function has the local one with the same name.
|
||||||
|
|
||||||
|
@ -782,15 +849,15 @@ In-browser `window` is sometimes used for following purposes:
|
||||||
sayHi();
|
sayHi();
|
||||||
```
|
```
|
||||||
|
|
||||||
Such use is typically a workaround. Would be better to name variables in a way that does require to write it this way. And note the `var user`. The trick doesn't work with `let` variables.
|
Such use is a workaround. Would be better to name variables differently, that won't require use to write the code it this way. And please note `"var"` before `user`. The trick doesn't work with `let` variables.
|
||||||
|
|
||||||
2. To check if a certain global variable or a builtin exists.
|
2. To check if a certain global variable or a builtin exists.
|
||||||
|
|
||||||
For instance, we want to check whether a global function `XMLHttpRequest` exists.
|
For instance, we want to check whether a global function `XMLHttpRequest` exists.
|
||||||
|
|
||||||
We can't write `if (XMLHttpRequest)`, because if there's no such global, that's an access to undefined variable, an error.
|
We can't write `if (XMLHttpRequest)`, because if there's no `XMLHttpRequest`, there will be an error (variable not defined).
|
||||||
|
|
||||||
But we can get it via `window.XMLHttpRequest`:
|
But we can read it from `window.XMLHttpRequest`:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
if (window.XMLHttpRequest) {
|
if (window.XMLHttpRequest) {
|
||||||
|
@ -798,19 +865,20 @@ In-browser `window` is sometimes used for following purposes:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If there is no such global function then `window.XMLHttpRequest` is just an access to unexisting object property. That's `undefined`, no error, so it works.
|
If there is no such global function then `window.XMLHttpRequest` is just a non-existing object property. That's `undefined`, no error, so it works.
|
||||||
|
|
||||||
We can also write the test without `window`:
|
We can also write the test without `window`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
if (typeof XMLHttpRequest == 'function') {
|
if (typeof XMLHttpRequest == 'function') {
|
||||||
/* is there a function XMLHttpRequest? */
|
/* is there a function XMLHttpRequest? */
|
||||||
/* this will also use a local XMLHttpRequest if exists */
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This doesn't use `window`, but is (theoretically) less reliable, because `typeof` may use a local XMLHttpRequest, and we want the global one.
|
||||||
|
|
||||||
3. The rare, special in-browser usage is to take the variable from the right window.
|
|
||||||
|
3. To take the variable from the right window. That's probably the most valid use case.
|
||||||
|
|
||||||
A browser may open multiple windows and tabs. A window may also embed another one in `<iframe>`. Every browser window has its own `window` object and global variables. Javascript allows windows that come from the same site (same protocol, host, port) to access variables from each other.
|
A browser may open multiple windows and tabs. A window may also embed another one in `<iframe>`. Every browser window has its own `window` object and global variables. Javascript allows windows that come from the same site (same protocol, host, port) to access variables from each other.
|
||||||
|
|
||||||
|
@ -832,30 +900,34 @@ In-browser `window` is sometimes used for following purposes:
|
||||||
*!*
|
*!*
|
||||||
alert( iframe.contentWindow.Array );
|
alert( iframe.contentWindow.Array );
|
||||||
*/!*
|
*/!*
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
As we know, usually `this` is used inside an object method to access the object. But there are special cases when `this` equals `window`:
|
Here, first two alerts use the current window, and the latter two take variables from `iframe` window. Can be any variables if `iframe` originates from the same protocol/host/port.
|
||||||
|
|
||||||
1. The value of the global `this` is `window`:
|
### "this" and global object
|
||||||
|
|
||||||
|
There are two special cases that link `this` and global object.
|
||||||
|
|
||||||
|
1. In the browser, the value of `this` in the global area is `window`:
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
// outside of functions
|
// outside of functions
|
||||||
alert( this === window ); // true
|
alert( this === window ); // true
|
||||||
```
|
```
|
||||||
|
|
||||||
In non-browser environments, it can be any other object, the specification does not say anything more exact.
|
Other, non-browser environments, may use another value for `this` in such cases.
|
||||||
|
|
||||||
2. When a function with `this` is called in not-strict mode (forgot `use strict`?):
|
2. When a function with `this` is called in not-strict mode, it gets the global object as `this`:
|
||||||
```js run no-strict
|
```js run no-strict
|
||||||
// not in strict mode (!)
|
// not in strict mode (!)
|
||||||
function f() {
|
function f() {
|
||||||
alert(this); // [object Window] (in strict mode would be undefined)
|
alert(this); // [object Window]
|
||||||
}
|
}
|
||||||
|
|
||||||
f(); // called without an object
|
f(); // called without an object
|
||||||
```
|
```
|
||||||
|
|
||||||
By specification, `this` in this case must be the global object. In non-browser environments it usually has another name, for instance, in Node.JS it is called `global`.
|
By specification, `this` in this case must be the global object, even in non-browser environments like Node.JS. That's for compatibility with old scripts, in strict mode `this` would be `undefined`.
|
||||||
|
|
BIN
1-js/8-more-functions/2-closure/lexenv-if.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
1-js/8-more-functions/2-closure/lexenv-if@2x.png
Normal file
After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
BIN
1-js/8-more-functions/2-closure/lexenv-nested-makecounter-2.png
Normal file
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
BIN
1-js/8-more-functions/2-closure/lexenv-nested-makecounter-6.png
Normal file
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |