renovations
This commit is contained in:
parent
c7d4c7e3ff
commit
e1948130f6
170 changed files with 1496 additions and 1161 deletions
|
@ -96,9 +96,9 @@ user = null;
|
|||
admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка!
|
||||
```
|
||||
|
||||
**Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана!**
|
||||
Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана.
|
||||
|
||||
Через `this` метод может обратиться к любому свойству объекта, а, при желании, и передать объект куда-либо:
|
||||
Через `this` метод может не только обратиться к любому свойству объекта, но и передать куда-то ссылку на сам объект целиком:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -112,8 +112,8 @@ var user = {
|
|||
*/!*
|
||||
};
|
||||
|
||||
function showName(obj) {
|
||||
alert( obj.name );
|
||||
function showName(namedObj) {
|
||||
alert( namedObj.name );
|
||||
}
|
||||
|
||||
user.sayHi(); // Василий
|
||||
|
@ -157,7 +157,7 @@ admin['g'](); // Админ (не важно, доступ к объекту ч
|
|||
*/!*
|
||||
```
|
||||
|
||||
**Значение `this` не зависит от того, как функция была создана, оно определяется исключительно в момент вызова.**
|
||||
Итак, значение `this` не зависит от того, как функция была создана, оно определяется исключительно в момент вызова.
|
||||
|
||||
## Значение this при вызове без контекста
|
||||
|
||||
|
@ -176,7 +176,9 @@ function func() {
|
|||
func();
|
||||
```
|
||||
|
||||
В современном стандарте языка это поведение изменено, вместо глобального объекта `this` будет `undefined`.
|
||||
Таково поведение в старом стандарте.
|
||||
|
||||
А в режиме `use strict` вместо глобального объекта `this` будет `undefined`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -188,12 +190,13 @@ function func() {
|
|||
func();
|
||||
```
|
||||
|
||||
Это стоит иметь в виду для общего развития, но обычно если в функции используется `this`, то она, всё же, проектируется для вызова в контексте объекта.
|
||||
Обычно если в функции используется `this`, то она, всё же, служит для вызова в контексте объекта, так что такая ситуация -- скорее исключение.
|
||||
|
||||
[warn header="`this` теряется при операциях с методом"]
|
||||
Ещё раз обратим внимание: контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта.
|
||||
## Ссылочный тип
|
||||
|
||||
Чтобы `this` передался правильно, нужно вызвать функцию именно через точку (или квадратные скобки). Любой более хитрый вызов приведёт к потере контекста, например:
|
||||
Контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта. Чтобы `this` передался, нужно вызвать функцию именно через точку (или квадратные скобки).
|
||||
|
||||
Любой более хитрый вызов приведёт к потере контекста, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -204,25 +207,38 @@ var user = {
|
|||
};
|
||||
|
||||
user.hi(); // Вася (простой вызов работает)
|
||||
|
||||
*!*
|
||||
// а теперь вызовем user.hi или user.bye в зависимости от имени
|
||||
(user.name == "Вася" ? user.hi : user.bye)(); // undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. При этом `this` теряется.
|
||||
В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. Но `this` при этом теряется.
|
||||
|
||||
Иначе говоря, такой вызов эквивалентен двум строкам:
|
||||
Если хочется понять, почему, то причина кроется в деталях работы вызова `obj.method()`.
|
||||
|
||||
```js
|
||||
var method = (user.name == "Вася" ? user.hi : user.bye);
|
||||
method(); // без this
|
||||
```
|
||||
Он ведь, на самом деле, состоит из двух независимых операций: точка `.` -- получение свойства и скобки `()` -- его вызов (предполагается, что это функция).
|
||||
|
||||
[/warn]
|
||||
|
||||
## Задачи
|
||||
Функция, как мы говорили раньше, сама по себе не запоминает контекст. Чтобы "донести его" до скобок, JavaScript применяет "финт ушами" -- точка возвращает не функцию, а значение специального "ссылочного" типа [Reference Type](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-reference-specification-type).
|
||||
|
||||
Этот тип представляет собой связку "base-name-strict", где:
|
||||
<ul>
|
||||
<li>*base* -- как раз объект,</li>
|
||||
<li>*name* -- имя свойства,</li>
|
||||
<li>*strict* -- вспомогательный флаг для передачи `use strict`.</li>
|
||||
</ul>
|
||||
|
||||
То есть, ссылочный тип (Reference Type) -- это своеобразное "три-в-одном". Он существует исключительно для целей спецификации, мы его не видим, поскольку любой оператор тут же от него избавляется:
|
||||
|
||||
<ul>
|
||||
<li>Скобки `()` получают из `base` значение свойства `name` и вызывают в контексте base.</li>
|
||||
<li>Другие операторы получают из `base` значение свойства `name` и используют, а остальные компоненты игнорируют.</li>
|
||||
</ul>
|
||||
|
||||
Поэтому любая операция над результатом операции получения свойства, кроме вызова, приводит к потере контекста.
|
||||
|
||||
Аналогично работает и получение свойства через квадратные скобки `obj[method]`.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
Если с одной стороны -- объект, а с другой -- нет, то сначала приводится объект.
|
||||
|
||||
В данном случае сравнение означает численное приведение. У массивов нет `valueOf`, поэтому вызывается `toString`, который возвращает список элементов через запятую.
|
||||
|
||||
В данном случае, элемент только один - он и возвращается. Так что `['x']` становится `'x'`. Получилось `'x' == 'x'`, верно.
|
||||
|
||||
P.S.
|
||||
По той же причине верны равенства:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( ['x','y'] == 'x,y' ); // true
|
||||
alert( [] == '' ); // true
|
||||
```
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# ['x'] == 'x'
|
||||
|
||||
[importance 5]
|
||||
|
||||
Почему результат `true` ?
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( ['x'] == 'x' );
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Первый alert(foo)
|
||||
|
||||
Возвращает строковое представление объекта, используя `toString`, т.е. `"foo"`.
|
||||
|
||||
# Второй alert(foo + 1)
|
||||
Оператор `'+'` преобразует объект к примитиву, используя `valueOf`, так что результат: `3`.
|
||||
|
||||
# Третий alert(foo + '3')
|
||||
|
||||
То же самое, что и предыдущий случай, объект превращается в примитив `2`. Затем происходит сложение `2 + '3'`. Оператор `'+'` при сложении чего-либо со строкой приводит и второй операнд к строке, а затем применяет конкатенацию, так что результат -- строка `"23"`.
|
|
@ -0,0 +1,24 @@
|
|||
# Преобразование
|
||||
|
||||
[importance 5]
|
||||
|
||||
Объявлен объект с `toString` и `valueOf`.
|
||||
|
||||
Какими будут результаты `alert`?
|
||||
|
||||
```js
|
||||
var foo = {
|
||||
toString: function () {
|
||||
return 'foo';
|
||||
},
|
||||
valueOf: function () {
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
|
||||
alert(foo);
|
||||
alert(foo + 1);
|
||||
alert(foo + "3");
|
||||
```
|
||||
|
||||
Подумайте, прежде чем ответить.
|
|
@ -0,0 +1,34 @@
|
|||
# Ответ по первому равенству
|
||||
|
||||
Два объекта равны только тогда, когда это один и тот же объект.
|
||||
|
||||
В первом равенстве создаются два массива, это разные объекты, так что они неравны.
|
||||
|
||||
# Ответ по второму равенству
|
||||
|
||||
<ol>
|
||||
<li>Первым делом, обе части сравнения вычисляются. Справа находится `![]`. Логическое НЕ `'!'` преобразует аргумент к логическому типу. Массив является объектом, так что это `true`. Значит, правая часть становится `![] = !true = false`. Так что получили:
|
||||
|
||||
```js
|
||||
alert( [] == false );
|
||||
```
|
||||
|
||||
</li>
|
||||
<li>Проверка равенства между объектом и примитивом вызывает численное преобразование объекта.
|
||||
|
||||
У массива нет `valueOf`, сработает `toString` и преобразует массив в список элементов, то есть - в пустую строку:
|
||||
|
||||
```js
|
||||
alert( '' == false );
|
||||
```
|
||||
|
||||
</li>
|
||||
<li>Сравнение различных типов вызывает численное преобразование слева и справа:
|
||||
|
||||
```js
|
||||
alert( 0 == 0 );
|
||||
```
|
||||
|
||||
Теперь результат очевиден.
|
||||
</li>
|
||||
</ol>
|
|
@ -0,0 +1,13 @@
|
|||
# Почему [] == [] неверно, а [ ] == ![ ] верно?
|
||||
|
||||
[importance 5]
|
||||
|
||||
Почему первое равенство -- неверно, а второе -- верно?
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( [] == [] ); // false
|
||||
alert( [] == ![] ); // true
|
||||
```
|
||||
|
||||
Какие преобразования происходят при вычислении?
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
|
||||
```js
|
||||
new Date(0) - 0 = 0 // (1)
|
||||
new Array(1)[0] + "" = "undefined" // (2)
|
||||
({})[0]
= undefined // (3)
|
||||
[1] + 1 = "11" // (4)
|
||||
[1,2] + [3,4] = "1,23,4" // (5)
|
||||
[] + null + 1 = "null1" // (6)
|
||||
[[0]][0][0] = 0 // (7)
|
||||
({} + {}) = "[object Object][object Object]" // (8)
|
||||
```
|
||||
|
||||
<ol>
|
||||
<li>`new Date(0)` -- дата, созданная по миллисекундам и соответствующая 0мс от 1 января 1970 года 00:00:00 UTC. Оператор минус `-` преобразует дату обратно в число миллисекунд, то есть в `0`.</li>
|
||||
<li>`new Array(num)` при вызове с единственным аргументом-числом создаёт массив данной длины, без элементов. Поэтому его нулевой элемент равен `undefined`, при сложении со строкой получается строка `"undefined"`.</li>
|
||||
<li>Фигурные скобки -- это создание пустого объекта, у него нет свойства `'0'`. Так что значением будет `undefined`.
|
||||
Обратите внимание на внешние, круглые скобки. Если их убрать и запустить `{}[0]` в отладочной консоли браузера -- будет `0`, т.к. скобки `{}` будут восприняты как пустой блок кода, после которого идёт массив.</li>
|
||||
<li>Массив преобразуется в строку `"1"`. Оператор `"+"` при сложении со строкой приводит второй аргумент к строке -- значит будет `"1" + "1" = "11"`.</li>
|
||||
<li>Массивы приводятся к строке и складываются.</li>
|
||||
<li>Массив преобразуется в пустую строку `"" + null + 1`, оператор `"+"` видит, что слева строка и преобразует `null` к строке, получается `"null" + 1`, и в итоге `"null1"`.</li>
|
||||
<li>`[[0]]` -- это вложенный массив `[0]` внутри внешнего `[ ]`. Затем мы берём от него нулевой элемент, и потом еще раз.
|
||||
|
||||
Если это непонятно, то посмотрите на такой пример:
|
||||
|
||||
```js
|
||||
alert( [1,[0],2][1] );
|
||||
```
|
||||
|
||||
Квадратные скобки после массива/объекта обозначают не другой массив, а взятие элемента.
|
||||
</li>
|
||||
<li>Каждый объект преобразуется к примитиву. У встроенных объектов `Object` нет подходящего `valueOf`, поэтому используется `toString`, так что складываются в итоге строковые представления объектов.</li>
|
||||
</ol>
|
|
@ -0,0 +1,17 @@
|
|||
# Вопросник по преобразованиям, для объектов
|
||||
|
||||
[importance 5]
|
||||
|
||||
Подумайте, какой результат будет у выражений ниже. Когда закончите -- сверьтесь с решением.
|
||||
|
||||
```js
|
||||
new Date(0) - 0
|
||||
new Array(1)[0] + ""
|
||||
({})[0]
|
||||
[1] + 1
|
||||
[1,2] + [3,4]
|
||||
[] + null + 1
|
||||
[[0]][0][0]
|
||||
({} + {})
|
||||
```
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Подсказка
|
||||
|
||||
Чтобы `sum(1)`, а также `sum(1)(2)` можно было вызвать новыми скобками -- результатом `sum` должна быть функция.
|
||||
|
||||
Но эта функция также должна уметь превращаться в число. Для этого нужно дать ей соответствующий `valueOf`. А если мы хотим, чтобы и в строковом контексте она вела себя так же -- то `toString`.
|
||||
|
||||
# Решение
|
||||
|
||||
Функция, которая возвращается `sum`, должна накапливать значение при каждом вызове.
|
||||
|
||||
Удобнее всего хранить его в замыкании, в переменной `currentSum`. Каждый вызов прибавляет к ней очередное значение:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
function sum(a) {
|
||||
|
||||
var currentSum = a;
|
||||
|
||||
function f(b) {
|
||||
currentSum += b;
|
||||
return f;
|
||||
}
|
||||
|
||||
f.toString = function() { return currentSum; };
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
alert( sum(1)(2) ); // 3
|
||||
alert( sum(5)(-1)(2) ); // 6
|
||||
alert( sum(6)(-1)(-2)(-3) ); // 0
|
||||
alert( sum(0)(1)(2)(3)(4)(5) ); // 15
|
||||
```
|
||||
|
||||
При внимательном взгляде на решение легко заметить, что функция `sum` срабатывает только один раз. Она возвращает функцию `f`.
|
||||
|
||||
Затем, при каждом запуске функция `f` добавляет параметр к сумме `currentSum`, хранящейся в замыкании, и возвращает сама себя.
|
||||
|
||||
**В последней строчке `f` нет рекурсивного вызова.**
|
||||
|
||||
Вот так была бы рекурсия:
|
||||
|
||||
```js
|
||||
function f(b) {
|
||||
currentSum += b;
|
||||
return f(); // <-- подвызов
|
||||
}
|
||||
```
|
||||
|
||||
А в нашем случае, мы просто возвращаем саму функцию, ничего не вызывая.
|
||||
|
||||
```js
|
||||
function f(b) {
|
||||
currentSum += b;
|
||||
return f; // <-- не вызывает сама себя, а возвращает ссылку на себя
|
||||
}
|
||||
```
|
||||
|
||||
Эта `f` используется при следующем вызове, опять возвратит себя, и так сколько нужно раз. Затем, при использовании в строчном или численном контексте -- сработает `toString`, который вернет текущую сумму `currentSum`.
|
|
@ -0,0 +1,17 @@
|
|||
# Сумма произвольного количества скобок
|
||||
|
||||
[importance 2]
|
||||
|
||||
Напишите функцию `sum`, которая будет работать так:
|
||||
|
||||
```js
|
||||
sum(1)(2) == 3; // 1 + 2
|
||||
sum(1)(2)(3) == 6; // 1 + 2 + 3
|
||||
sum(5)(-1)(2) == 6
|
||||
sum(6)(-1)(-2)(-3) == 0
|
||||
sum(0)(1)(2)(3)(4)(5) == 15
|
||||
```
|
||||
|
||||
Количество скобок может быть любым.
|
||||
|
||||
Пример такой функции для двух аргументов -- есть в решении задачи [](/task/closure-sum).
|
238
1-js/6-objects-more/2-object-conversion/article.md
Normal file
238
1-js/6-objects-more/2-object-conversion/article.md
Normal file
|
@ -0,0 +1,238 @@
|
|||
# Преобразование объектов: toString и valueOf
|
||||
|
||||
Ранее, в главе [](/types-conversion) мы рассматривали преобразование типов для примитивов. Теперь добавим в нашу картину мира объекты.
|
||||
|
||||
Бывают операции, при которых объект должен быть преобразован в примитив.
|
||||
[cut]
|
||||
Например:
|
||||
|
||||
<ul>
|
||||
<li>Строковое преобразование -- если объект выводится через `alert(obj)`.</li>
|
||||
<li>Численное преобразование -- при арифметических операциях, сравнении с примитивом.</li>
|
||||
<li>Логическое преобразование -- при `if(obj)` и других логических операциях.</li>
|
||||
</ul>
|
||||
|
||||
Рассмотрим эти преобразования по очереди.
|
||||
|
||||
## Логическое преобразование
|
||||
|
||||
Проще всего -- с логическим преобразованием.
|
||||
|
||||
**Любой объект в логическом контексте -- `true`, даже если это пустой массив `[]` или объект `{}`.**
|
||||
|
||||
```js
|
||||
//+ run
|
||||
if ( {} && [] ) {
|
||||
alert("Все объекты - true!"); // alert сработает
|
||||
}
|
||||
```
|
||||
|
||||
## Строковое преобразование
|
||||
|
||||
Строковое преобразование проще всего увидеть, если вывести объект при помощи `alert`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
firstName: 'Василий'
|
||||
};
|
||||
|
||||
alert(user); // [object Object]
|
||||
```
|
||||
|
||||
Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка `"[object Object]"`.
|
||||
|
||||
Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное.
|
||||
|
||||
**Если в объекте присутствует метод `toString`, который возвращает примитив, то он используется для преобразования.**
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
|
||||
firstName: 'Василий',
|
||||
|
||||
*!*toString:*/!* function() {
|
||||
return 'Пользователь ' + this.firstName;
|
||||
}
|
||||
};
|
||||
|
||||
alert( user ); // Пользователь Василий
|
||||
```
|
||||
|
||||
[smart header="Результатом `toString` может быть любой примитив"]
|
||||
Метод `toString` не обязан возвращать именно строку.
|
||||
|
||||
Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {
|
||||
toString: function() { return 123; }
|
||||
};
|
||||
|
||||
alert(obj); // 123
|
||||
```
|
||||
|
||||
Поэтому мы и называем его здесь *"строковое преобразование"*, а не "преобразование к строке".
|
||||
[/smart]
|
||||
|
||||
Все объекты, включая встроенные, имеют свои реализации метода `toString`, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( [1,2] ); // toString для массивов выводит список элементов "1,2"
|
||||
alert( new Date ); // toString для дат выводит дату в виде строки
|
||||
alert( function() { } ); // toString для функции выводит её код
|
||||
```
|
||||
|
||||
## Численное преобразование
|
||||
|
||||
Для численного преобразования объекта используется метод `valueOf`, а если его нет -- то `toString`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var room = {
|
||||
number: 777,
|
||||
|
||||
valueOf: function() { return this.number; },
|
||||
toString: function() { return this.number; }
|
||||
};
|
||||
|
||||
alert( +room ); // 777, *!*вызвался valueOf*/!*
|
||||
|
||||
delete room.valueOf; // *!*valueOf удалён*/!*
|
||||
|
||||
alert( +room ); // 777, *!*вызвался toString*/!*
|
||||
```
|
||||
|
||||
Метод `valueOf` обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом -- не обязательно числовое.
|
||||
|
||||
[smart header="У большинства объектов нет `valueOf`"]
|
||||
У большинства встроенных объектов такого `valueOf` нет, поэтому численное и строковое преобразования для них работают одинаково.
|
||||
|
||||
Исключением является объект `Date`, который поддерживает оба типа преобразований:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( new Date() ); // toString: Дата в виде читаемой строки
|
||||
alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970
|
||||
```
|
||||
|
||||
[/smart]
|
||||
|
||||
[smart header="Детали спецификации"]
|
||||
Если посмотреть в стандарт, то в пункте [15.2.4.4](http://es5.github.com/x15.2.html#x15.2.4.4) говорится о том, что `valueOf` есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется.
|
||||
[/smart]
|
||||
|
||||
## Две стадии преобразования
|
||||
|
||||
Итак, объект преобразован в примитив при помощи `toString` или `valueOf`. Далее, вполне возможно,
|
||||
Если необходимо, что полученный из объекта примитив будет преобразован дальше, уже по правилам для примитивов.
|
||||
|
||||
Например, рассмотрим применение к объекту операции `==`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {
|
||||
valueOf: function() { return 1; }
|
||||
};
|
||||
|
||||
alert(obj == true); // true
|
||||
```
|
||||
|
||||
Объект `obj` был сначала преобразован в примитив, используя численное преобразование, получилось `1 == true`.
|
||||
|
||||
Далее, так как значения всё ещё разных типов, применяются правила преобразования примитивов, результат: `true`.
|
||||
|
||||
То же самое -- при сложении с объектом при помощи `+`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {
|
||||
valueOf: function() { return 1; }
|
||||
};
|
||||
|
||||
alert(obj + "test"); // 1test
|
||||
```
|
||||
|
||||
Или вот, для разности объектов:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var a = {
|
||||
valueOf: function() { return "1"; }
|
||||
};
|
||||
var b = {
|
||||
valueOf: function() { return "2"; }
|
||||
};
|
||||
|
||||
alert(a - b); // "1" - "2" = -1
|
||||
```
|
||||
|
||||
[warn header="Исключение: `Date`"]
|
||||
Объект `Date`, по историческим причинам, является исключением.
|
||||
|
||||
Бинарный оператор плюс `+` обычно использует числовое преобразование, но в случае с `Date` -- строковое:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
// бинарный вариант, строчное преобразование
|
||||
alert( new Date + "" ); // "строка даты"
|
||||
|
||||
// унарный вариант, как и - * /, приводит к числу
|
||||
alert( +new Date ); // число миллисекунд
|
||||
```
|
||||
[/warn]
|
||||
|
||||
[warn header="Как испугать Java-разработчика"]
|
||||
В языке Java (это не JavaScript, другой язык, здесь приведён для примера) логические значения можно создавать, используя синтаксис `new Boolean(true/false)`, например `new Boolean(true)`.
|
||||
|
||||
В JavaScript тоже есть подобная возможность, которая возвращает "объектную обёртку" для логического значения.
|
||||
|
||||
Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к JavaScript, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var value = new Boolean(false);
|
||||
if ( value ) {
|
||||
alert(true); // сработает!
|
||||
}
|
||||
```
|
||||
|
||||
Почему запустился `alert`? Ведь в `if` находится `false`... Проверим:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var value = new Boolean(false);
|
||||
|
||||
*!*
|
||||
alert(value); // выводит false, все ок..
|
||||
*/!*
|
||||
|
||||
if ( value ) {
|
||||
alert(true); // ..но тогда почему выполняется alert в if ?!?
|
||||
}
|
||||
```
|
||||
|
||||
Дело в том, что `new Boolean` -- это не примитивное значение, а объект. Поэтому в логическом контексте он преобразуется к`true`, в результате работает первый пример.
|
||||
|
||||
А второй пример вызывает `alert`, который преобразует объект к строке, и он становится `"false"`.
|
||||
|
||||
**В JavaScript вызовы `new Boolean/String/Number` не используются, а используются простые вызовы соответствующих функций, они преобразуют значение в примитив нужного типа, например `Boolean(val) === !!val`.**
|
||||
[/warn]
|
||||
|
||||
## Итого
|
||||
|
||||
<ul>
|
||||
<li>В логическом контексте объект -- всегда `true`.</li>
|
||||
<li>При строковом преобразовании объекта используется его метод `toString`. Он должен возвращать примитивное значение, причём не обязательно именно строку.
|
||||
</li>
|
||||
<li>Для численного преобразования используется метод `valueOf`, который также может возвратить любое примитивное значение. У большинства объектов `valueOf` не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется `toString`.</li>
|
||||
</ul>
|
||||
|
||||
Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты [11.8.5](http://es5.github.com/x11.html#x11.8.5), [11.9.3](http://es5.github.com/x11.html#x11.9.3), а также [9.1](http://es5.github.com/x9.html#x9.1) и [9.3](http://es5.github.com/x9.html#x9.3).
|
||||
|
||||
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Обычный синтаксис `{...}` позволяет создать один объект. Но зачастую нужно создать много однотипных объектов.
|
||||
|
||||
Для этого используют функции, запуская их при помощи специального оператора `new`.
|
||||
Для этого используют "функции-конструкторы", запуская их при помощи специального оператора `new`.
|
||||
[cut]
|
||||
## Конструктор
|
||||
|
||||
|
@ -21,9 +21,9 @@ var animal = new Animal("ёжик");
|
|||
*/!*
|
||||
```
|
||||
|
||||
Технически, никаких ограничений нет -- любую функцию можно вызвать при помощи `new`. Но при этом она работает несколько иным образом, чем обычно, поэтому функции, предназначенные к вызову через `new`, называют с большой буквы.
|
||||
Технически, любую функцию можно вызвать при помощи `new`. Но при этом она работает несколько иным образом, чем обычно, поэтому функции, предназначенные к вызову через `new`, называют с большой буквы.
|
||||
|
||||
**Алгоритм работы оператора `new`:**
|
||||
**Алгоритм работы функции, запущенной через `new`:**
|
||||
|
||||
<ol>
|
||||
<li>Автоматически создается новый пустой объект.</li>
|
||||
|
@ -33,7 +33,7 @@ var animal = new Animal("ёжик");
|
|||
</ol>
|
||||
|
||||
|
||||
В результате вызова `new Animal("ёжик");` получаем объект:
|
||||
В результате вызова `new Animal("ёжик");` получаем такой объект:
|
||||
|
||||
```js
|
||||
animal = {
|
||||
|
@ -42,15 +42,18 @@ animal = {
|
|||
}
|
||||
```
|
||||
|
||||
Иными словами, при вызове `new Animal` происходит что-то в таком духе (комментарии -- это то, что делает интерпретатор):
|
||||
Иными словами, при вызове `new Animal` происходит что-то в таком духе (первая и последняя строка -- это то, что делает интерпретатор):
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
*!*
|
||||
// this = {}
|
||||
*/!*
|
||||
|
||||
// в this пишем свойства, методы
|
||||
this.name = name;
|
||||
this.canWalk = true;
|
||||
|
||||
*!*
|
||||
// return this
|
||||
*/!*
|
||||
|
@ -67,7 +70,7 @@ function Animal(name) {
|
|||
<li>При вызове `return` с примитивным значением, оно будет отброшено.</li>
|
||||
</ul>
|
||||
|
||||
Иными словами, вызов `return` с объектом вернёт объект, а с чем угодно, кроме объекта -- прекратит выполнение функции и возвратит `this`.
|
||||
Иными словами, вызов `return` с объектом вернёт объект, а с чем угодно, кроме объекта -- возвратит, как обычно, `this`.
|
||||
|
||||
Например, возврат объекта:
|
||||
|
||||
|
@ -97,9 +100,9 @@ function BigAnimal() {
|
|||
alert( new BigAnimal().name ); // Мышь, получили this (а Годзилла пропал)
|
||||
```
|
||||
|
||||
Эта особенность работы `new` прописана в стандарте, знать о ней полезно, но используется она весьма редко.
|
||||
Эта особенность работы `new` прописана в стандарте, но используется она весьма редко.
|
||||
|
||||
[smart]
|
||||
[smart header="Можно без скобок"]
|
||||
Кстати, при вызове `new` без аргументов скобки можно не ставить:
|
||||
|
||||
```js
|
||||
|
@ -108,11 +111,14 @@ var animal = new BigAnimal; // <-- без скобок
|
|||
var animal = new BigAnimal();
|
||||
```
|
||||
|
||||
Не сказать, что выбрасывание скобок -- "хороший стиль", но такой синтаксис допустим стандартом.
|
||||
[/smart]
|
||||
|
||||
## Создание методов в конструкторе
|
||||
|
||||
Использование функций для создания объекта дает большую гибкость. Можно передавать конструктору параметры, определяющие как его создавать.
|
||||
Использование функций для создания объекта дает большую гибкость. Можно передавать конструктору параметры, определяющие как его создавать, и он будет "клепать" объекты заданным образом.
|
||||
|
||||
Добавим в создаваемый объект ещё и метод.
|
||||
|
||||
Например, `new User(name)` создает объект с заданным значением свойства `name` и методом `sayHi`:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
```js
|
||||
//+ run
|
||||
|
||||
function User(fullName) {
|
||||
this.fullName = fullName;
|
||||
|
||||
Object.defineProperties(this, {
|
||||
|
||||
firstName: {
|
||||
|
||||
get: function() {
|
||||
return this.fullName.split(' ')[0];
|
||||
},
|
||||
|
||||
set: function(newFirstName) {
|
||||
this.fullName = newFirstName + ' ' + this.lastName;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
lastName: {
|
||||
|
||||
get: function() {
|
||||
return this.fullName.split(' ')[1];
|
||||
},
|
||||
|
||||
set: function(newLastName) {
|
||||
this.fullName = this.firstName + ' ' + newLastName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
var vasya = new User("Василий Попкин");
|
||||
|
||||
// чтение firstName/lastName
|
||||
alert(vasya.firstName); // Василий
|
||||
alert(vasya.lastName); // Попкин
|
||||
|
||||
// запись в lastName
|
||||
vasya.lastName = 'Сидоров';
|
||||
|
||||
alert(vasya.fullName); // Василий Сидоров
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Добавить get/set-свойства
|
||||
|
||||
[importance 5]
|
||||
|
||||
Вам попал в руки код объекта `User`, который хранит имя и фамилию в свойстве `this.fullName`:
|
||||
|
||||
```js
|
||||
function User(fullName) {
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
var vasya = new User("Василий Попкин");
|
||||
```
|
||||
|
||||
Имя и фамилия всегда разделяются пробелом.
|
||||
|
||||
Сделайте, чтобы были доступны свойства `firstName` и `lastName`, причём не только на чтение, но и на запись, вот так:
|
||||
|
||||
```js
|
||||
var vasya = new User("Василий Попкин");
|
||||
|
||||
// чтение firstName/lastName
|
||||
alert(vasya.firstName); // Василий
|
||||
alert(vasya.lastName); // Попкин
|
||||
|
||||
// запись в lastName
|
||||
vasya.lastName = 'Сидоров';
|
||||
|
||||
alert(vasya.fullName); // Василий Сидоров
|
||||
```
|
||||
|
||||
Важно: не рекомендуется дублировать одни и те же данные в различных свойствах. Поэтому в этой задаче `fullName` должно остаться свойством, а `firstName/lastName` -- реализованы через `get/set`.
|
394
1-js/6-objects-more/4-descriptors-getters-setters/article.md
Normal file
394
1-js/6-objects-more/4-descriptors-getters-setters/article.md
Normal file
|
@ -0,0 +1,394 @@
|
|||
# Дескрипторы, геттеры и сеттеры свойств
|
||||
|
||||
В этой главе мы рассмотрим возможности, которые позволяют очень гибко и мощно управлять всеми свойствами объекта, включая их аспекты -- изменяемость, видимость в цикле `for..in` и даже "невидимые" геттеры-сеттеры.
|
||||
|
||||
Они поддерживаются всеми современными браузерами, но не IE8-. Точнее говоря, они поддерживаются даже в IE8, но не для всех объектов, а только для DOM-объектов (используются при работе со страницей, это сейчас вне нашего рассмотрения).
|
||||
|
||||
[cut]
|
||||
## Дескрипторы в примерах
|
||||
|
||||
Основной метод для управления свойствами -- [Object.defineProperty](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty).
|
||||
|
||||
Он позволяет определить свойство путём задания "дескриптора" -- описания, включающего в себя ряд важных внутренних параметров.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
```js
|
||||
Object.defineProperty(obj, prop, descriptor)
|
||||
```
|
||||
|
||||
Аргументы:
|
||||
<dl>
|
||||
<dt>`obj`</dt>
|
||||
<dd>Объект, в котором объявляется свойство.</dd>
|
||||
<dt>`prop`</dt>
|
||||
<dd>Имя свойства, которое нужно объявить или модифицировать.</dd>
|
||||
<dt>`descriptor`</dt>
|
||||
<dd>Дескриптор -- объект, который описывает поведение свойства. В нём могут быть следующие поля:
|
||||
|
||||
<ul>
|
||||
<li>`value` -- значение свойства, по умолчанию `undefined`</li>
|
||||
<li>`writable` -- значение свойства можно менять, если `true`. По умолчанию `false`.</li>
|
||||
<li>`configurable` -- если `true`, то свойство можно удалять, а также менять его в дальнейшем при помощи новых вызовов `defineProperty`. По умолчанию `false`.</li>
|
||||
<li>`enumerable` -- если `true`, то свойство будет участвовать в переборе `for..in`. По умолчанию `false`.</li>
|
||||
<li>`get` -- функция, которая возвращает значение свойства. По умолчанию `undefined`.</li>
|
||||
<li>`set` -- функция, которая записывает значение свойства. По умолчанию `undefined`.</li>
|
||||
</ul>
|
||||
|
||||
Чтобы избежать конфликта, запрещено одновременно указывать значение `value` и функции `get/set`. Либо значение, либо функции для его чтения-записи, одно из двух. Также запрещено и не имеет смысла указывать `writable` при наличии `get/set`-функций.
|
||||
|
||||
Далее мы подробно разберём эти свойства на примерах.
|
||||
|
||||
## Обычное свойство
|
||||
|
||||
Обычное свойство добавить очень просто.
|
||||
|
||||
Два таких вызова работают одинаково:
|
||||
|
||||
```js
|
||||
var user = {};
|
||||
|
||||
// 1. простое присваивание
|
||||
user.name = "Вася";
|
||||
|
||||
// 2. указание значения через дескриптор
|
||||
Object.defineProperty(user, "name", { value: "Вася" });
|
||||
```
|
||||
|
||||
## Свойство-константа
|
||||
|
||||
Для того, чтобы сделать свойство неизменяемым, добавим ему флаги `writable` и `configurable`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
*!*
|
||||
"use strict";
|
||||
*/!*
|
||||
|
||||
var user = {};
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
value: "Вася",
|
||||
writable: false, // запретить присвоение "user.name="
|
||||
configurable: false // запретить удаление "delete user.name"
|
||||
});
|
||||
|
||||
// Теперь попытаемся изменить это свойство.
|
||||
|
||||
// в strict mode присвоение "user.name=" вызовет ошибку
|
||||
*!*
|
||||
user.name = "Петя";
|
||||
*/!*
|
||||
```
|
||||
|
||||
Заметим, что без `use strict` операция записи "молча" не сработает, а при `use strict` дополнительно генерируется ошибка.
|
||||
|
||||
## Свойство, скрытое для for..in
|
||||
|
||||
Встроенный метод `toString`, как и большинство встроенных методов, не участвует в цикле `for..in`. Это удобно, так как обычно такое свойство является "служебным".
|
||||
|
||||
К сожалению, свойство `toString`, объявленное обычным способом, будет видно в цикле `for..in`, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
name: "Вася",
|
||||
toString: function() { return this.name; }
|
||||
};
|
||||
|
||||
*!*
|
||||
for(var key in user) alert(key); // name, toString
|
||||
*/!*
|
||||
```
|
||||
|
||||
Мы бы хотели, чтобы поведение нашего метода `toString` было таким же, как и стандартного.
|
||||
|
||||
`Object.defineProperty` может исключить `toString` из списка итерации, поставив ему флаг `enumerable: false`. По стандарту, у встроенного `toString` этот флаг уже стоит.
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
name: "Вася",
|
||||
toString: function() { return this.name; }
|
||||
};
|
||||
|
||||
*!*
|
||||
// помечаем toString как не подлежащий перебору в for..in
|
||||
Object.defineProperty(user, "toString", {enumerable: false});
|
||||
|
||||
for(var key in user) alert(key); // name
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание, вызов `defineProperty` не перезаписал свойство, а просто модифицировал настройки у существующего `toString`.
|
||||
|
||||
## Свойство-функция
|
||||
|
||||
Дескриптор позволяет задать свойство, которое на самом деле работает как функция. Для этого в нём нужно указать эту функцию в `get`.
|
||||
|
||||
Например, у объекта `user` есть обычные свойства: имя `firstName` и фамилия `surname`.
|
||||
|
||||
Создадим свойство `fullName`, которое на самом деле является функцией:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
firstName: "Вася",
|
||||
surname: "Петров"
|
||||
}
|
||||
|
||||
Object.defineProperty(user, "fullName", {
|
||||
*!*get*/!*: function() {
|
||||
return this.firstName + ' ' + this.surname;
|
||||
}
|
||||
});
|
||||
|
||||
*!*
|
||||
alert(user.fullName); // Вася Петров
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание, снаружи `fullName` -- это обычное свойство `user.fullName`. Но дескриптор указывает, что на самом деле его значение возвращается функцией.
|
||||
|
||||
Также можно указать функцию, которая используется для записи значения, при помощи дескриптора `set`.
|
||||
|
||||
Например, добавим возможность присвоения `user.fullName` к примеру выше:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
firstName: "Вася",
|
||||
surname: "Петров"
|
||||
}
|
||||
|
||||
Object.defineProperty(user, "fullName", {
|
||||
|
||||
get: function() {
|
||||
return this.firstName + ' ' + this.surname;
|
||||
},
|
||||
|
||||
*!*
|
||||
set: function(value) {
|
||||
var split = value.split(' ');
|
||||
this.firstName = split[0];
|
||||
this.surname = split[1];
|
||||
}
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
user.fullName = "Петя Иванов";
|
||||
*/!*
|
||||
alert(user.firstName); // Петя
|
||||
alert(user.surname); // Иванов
|
||||
```
|
||||
|
||||
## Указание get/set в литералах
|
||||
|
||||
Если мы создаём объект при помощи синтаксиса `{ ... }`, то задать свойства-функции можно прямо в его определении.
|
||||
|
||||
Для этого используется особый синтаксис: `get свойство` или `set свойство`.
|
||||
|
||||
Например, ниже объявлен геттер-сеттер `fullName`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {
|
||||
firstName: "Вася",
|
||||
surname: "Петров",
|
||||
|
||||
*!*
|
||||
get fullName() {
|
||||
*/!*
|
||||
return this.firstName + ' ' + this.surname;
|
||||
},
|
||||
|
||||
*!*
|
||||
set fullName(value) {
|
||||
*/!*
|
||||
var split = value.split(' ');
|
||||
this.firstName = split[0];
|
||||
this.surname = split[1];
|
||||
}
|
||||
};
|
||||
|
||||
*!*
|
||||
alert(user.fullName); // Вася Петров (из геттера)
|
||||
|
||||
user.fullName = "Петя Иванов";
|
||||
alert(user.firstName); // Петя (поставил сеттер)
|
||||
alert(user.surname); // Иванов (поставил сеттер)
|
||||
*/!*
|
||||
```
|
||||
|
||||
## Да здравствуют get/set!
|
||||
|
||||
Казалось бы, зачем нам назначать get/set для свойства через всякие хитрые вызовы, когда можно сделать просто функции с самого начала? Например, `getFullName`, `setFullName`...
|
||||
|
||||
Конечно, в ряде случаев свойства выглядят короче, такое решение просто может быть красивым. Но основной бонус -- это гибкость, возможность получить контроль над свойством в любой момент!
|
||||
|
||||
Например, в начале разработки мы используем обычные свойства, например у `User` будет имя `name` и возраст `age`:
|
||||
|
||||
```js
|
||||
function User(name, age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
var pete = new User("Петя", 25);
|
||||
|
||||
alert(pete.age); // 25
|
||||
```
|
||||
|
||||
С обычными свойствами в коде меньше букв, они удобны, причины использовать функции пока нет.
|
||||
|
||||
...Но рано или поздно может произойти что-то, что потребует более сложной логики.
|
||||
|
||||
Например, формат данных изменился и теперь вместо возраста `age` хранится дата рождения `birthday`:
|
||||
|
||||
```js
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
var pete = new User("Петя", new Date(1987, 6, 1));
|
||||
```
|
||||
|
||||
Что теперь делать со старым кодом, который выводит свойство `age`?
|
||||
|
||||
Можно, конечно, найти все места и поправить их, но это долго, а иногда и невозможно, скажем, если вы взаимодействуете со сторонней библиотекой, код в которой -- чужой и влезать в него нежелательно.
|
||||
|
||||
Добавление `get`-функции `age` позволяет обойти проблему легко и непринуждённо:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
|
||||
*!*
|
||||
Object.defineProperty(this, "age", {
|
||||
get: function() {
|
||||
var todayYear = new Date().getFullYear();
|
||||
return todayYear - this.birthday.getFullYear();
|
||||
}
|
||||
});
|
||||
*/!*
|
||||
}
|
||||
|
||||
var pete = new User("Петя", new Date(1987, 6, 1));
|
||||
|
||||
alert(pete.age); // получает возраст из даты рождения
|
||||
```
|
||||
|
||||
Таким образом, `defineProperty` позволяет нам использовать обычные свойства и, при необходимости, в любой момент заменить их на функции, сохраняя полную совместимость.
|
||||
|
||||
## Другие методы работы со свойствами
|
||||
|
||||
<dl>
|
||||
<dt>[Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties)</dt>
|
||||
<dd>Позволяет объявить несколько свойств сразу:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var user = {}
|
||||
|
||||
Object.defineProperties(user, {
|
||||
*!*
|
||||
firstName: {
|
||||
*/!*
|
||||
value: "Петя"
|
||||
},
|
||||
|
||||
*!*
|
||||
surname: {
|
||||
*/!*
|
||||
value: "Иванов"
|
||||
},
|
||||
|
||||
*!*
|
||||
fullName: {
|
||||
*/!*
|
||||
get: function() {
|
||||
return this.firstName + ' ' + this.surname;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
alert( user.fullName ); // Петя Иванов
|
||||
```
|
||||
|
||||
</dd>
|
||||
<dt>[Object.keys(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys), [Object.getOwnPropertyNames(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)</dt>
|
||||
<dd>Возвращают массив -- список свойств объекта.
|
||||
|
||||
`Object.keys` возвращает только `enumerable`-свойства.
|
||||
|
||||
`Object.getOwnPropertyNames` -- возвращает все:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
internal: 3
|
||||
};
|
||||
|
||||
Object.defineProperty(obj, "internal", {enumerable: false});
|
||||
|
||||
*!*
|
||||
alert( Object.keys(obj) ); // a,b
|
||||
alert( Object.getOwnPropertyNames(obj) ); // a, internal, b
|
||||
*/!*
|
||||
```
|
||||
|
||||
</dd>
|
||||
<dt>[Object.getOwnPropertyDescriptor(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)</dt>
|
||||
<dd>Возвращает дескриптор для свойства с `prop`.
|
||||
|
||||
Полученный дескриптор можно изменить и использовать `defineProperty` для сохранения изменений, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var obj = { test: 5 };
|
||||
*!*
|
||||
var descriptor = Object.getOwnPropertyDescriptor(obj, 'test');
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
// заменим value на геттер, для этого...
|
||||
*/!*
|
||||
delete descriptor.value; // ..нужно убрать value/writable
|
||||
delete descriptor.writable;
|
||||
descriptor.get = function() { // и поставить get
|
||||
alert("Preved :)");
|
||||
};
|
||||
|
||||
*!*
|
||||
// поставим новое свойство вместо старого
|
||||
*/!*
|
||||
|
||||
// если не удалить - defineProperty объединит старый дескриптор с новым
|
||||
delete obj.test;
|
||||
|
||||
Object.defineProperty(obj, 'test', descriptor);
|
||||
|
||||
obj.test; // Preved :)
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
...И несколько методов, которые используются очень редко:
|
||||
<dl>
|
||||
<dt>[Object.preventExtensions(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)</dt>
|
||||
<dd>Запрещает добавление свойств в объект.</dd>
|
||||
<dt>[Object.seal(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)</dt>
|
||||
<dd>Запрещает добавление и удаление свойств, все текущие свойства делает `configurable: false`.</dd>
|
||||
<dt>[Object.freeze(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze)</dt>
|
||||
<dd>Запрещает добавление, удаление и изменение свойств, все текущие свойства делает `configurable: false, writable: false`.</dd>
|
||||
<dt>[Object.isExtensible(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible), [Object.isSealed(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed), [Object.isFrozen(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen)</dt>
|
||||
<dd>Возвращают `true`, если на объекте были вызваны методы `Object.preventExtensions/seal/freeze`.</dd>
|
||||
</dl>
|
||||
|
|
@ -50,7 +50,7 @@ Article.showCount(); // (2)
|
|||
|
||||
Здесь `Article.count` -- статическое свойство, а `Article.showCount` -- статический метод.
|
||||
|
||||
**Обратите внимание на контекст `this`. Несмотря на то, что переменная и метод -- статические, он всё ещё полезен. В строке `(1)` он равен `Article`!**
|
||||
Обратим внимание на использование `this` в примере выше. Несмотря на то, что переменная и метод -- статические, он всё ещё полезен. В строке `(1)` он равен `Article`.
|
||||
|
||||
## Пример: сравнение объектов
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Явное указание this: "call", "apply"
|
||||
|
||||
Итак, мы знаем, что в `this` -- это текущий объект при вызове "через точку" и новый объект при конструировании через `new`.
|
||||
Итак, мы знаем, что `this` -- это текущий объект при вызове "через точку" и новый объект при конструировании через `new`.
|
||||
|
||||
В этой главе наша цель получить окончательное и полное понимание `this` в JavaScript. Для этого не хватает всего одного элемента: способа явно указать `this` при помощи методов `call` и `apply`.
|
||||
|
||||
|
@ -26,7 +26,7 @@ function showFullName() {
|
|||
}
|
||||
```
|
||||
|
||||
**Обратите внимание, JavaScript позволяет использовать `this` везде. Любая функция может в своём коде упомянуть `this`, каким будет это значение -- выяснится в момент запуска.**
|
||||
Пока объекта нет, но это нормально, ведь JavaScript позволяет использовать `this` везде. Любая функция может в своём коде упомянуть `this`, каким будет это значение -- выяснится в момент запуска.
|
||||
|
||||
Вызов `showFullName.call(user)` запустит функцию, установив `this = user`, вот так:
|
||||
|
||||
|
@ -138,13 +138,13 @@ alert( obj.join(';') ); // "A;Б;В"
|
|||
|
||||
[/smart]
|
||||
|
||||
**...Однако, копирование метода из одного объекта в другой не всегда приемлемо!**
|
||||
...Однако, копирование метода из одного объекта в другой не всегда приемлемо!
|
||||
|
||||
Представим на минуту, что вместо `arguments` у нас -- произвольный объект. У него тоже есть числовые индексы, `length` и мы хотим вызвать в его контексте метод `[].join`. То есть, ситуация похожа на `arguments`, но (!) вполне возможно, что у объекта есть *свой* метод `join`.
|
||||
|
||||
Поэтому копировать `[].join`, как сделано выше, нельзя: если он перезапишет собственный `join` объекта, то будет страшный бардак и путаница.
|
||||
|
||||
**Безопасно вызвать метод нам поможет `call`:**
|
||||
Безопасно вызвать метод нам поможет `call`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -208,7 +208,16 @@ showFullName.call(user, 'firstName', 'surname');
|
|||
showFullName.apply(user, ['firstName', 'surname']);
|
||||
```
|
||||
|
||||
Преимущество `apply` перед `call` отчётливо видно в следующем примере, когда мы формируем массив аргументов динамически:
|
||||
Преимущество `apply` перед `call` отчётливо видно, когда мы формируем массив аргументов динамически.
|
||||
|
||||
Например, в JavaScript есть встроенная функция `Math.max(a, b, c...)`, которая возвращает максимальное значение из аргументов:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( Math.max(1, 5, 2) ); // 5
|
||||
```
|
||||
|
||||
При помощи `apply` мы могли бы найти максимум в произвольном массиве, вот так:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -221,57 +230,46 @@ arr.push(2);
|
|||
alert( Math.max.apply(null, arr) ); // 5
|
||||
```
|
||||
|
||||
Обратим внимание, в примере выше вызывается метод `Math.max`. Его стандартное применение -- это выбор максимального аргумента из переданных, которых может быть сколько угодно:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( Math.max(1, 5, 2) ); // 5
|
||||
alert( Math.max(1, 5, 2, 8) ); // 8
|
||||
```
|
||||
|
||||
В примере выше мы передали аргументы через массив -- второй параметр `apply`... Но вы, наверное, заметили небольшую странность? В качестве контекста `this` был передан `null`.
|
||||
|
||||
Строго говоря, полным эквивалентом вызову `Math.max(1,2,3)` был бы вызов `Math.max.apply(Math, [1,2,3])`. В обоих этих вызовах контекстом будет объект `Math`.
|
||||
|
||||
Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод `Math.max`не использует `this`. Действительно, зачем `this`, если нужно всего лишь выбрать максимальный из аргументов?
|
||||
|
||||
**Вот так, при помощи `apply` мы получили короткий и элегантный способ вычислить максимальное значение в массиве!**
|
||||
Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод `Math.max` не использует `this`. Действительно, зачем `this`, если нужно всего лишь выбрать максимальный из аргументов? Вот так, при помощи `apply` мы получили короткий и элегантный способ вычислить максимальное значение в массиве!
|
||||
|
||||
[smart header="Вызов `call/apply` с `null` или `undefined`"]
|
||||
|
||||
В старом стандарте при указании первого аргумента `null` или `undefined` в `call/apply`, функция получала `this = window`, например:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
function f() {
|
||||
alert(this);
|
||||
}
|
||||
|
||||
f.call(null); // window
|
||||
```
|
||||
|
||||
Это поведение исправлено в современном стандарте ([15.3](http://es5.github.com/x15.3.html#x15.3.4.3)).
|
||||
|
||||
Если функция работает в строгом режиме, то `this` передаётся "как есть":
|
||||
В современном стандарте `call/apply` передают `this` "как есть". А в старом, без `use strict`, при указании первого аргумента `null` или `undefined` в `call/apply`, функция получает `this = window`, например:
|
||||
|
||||
Современный стандарт:
|
||||
```js
|
||||
//+ run
|
||||
function f() {
|
||||
"use strict";
|
||||
*!*
|
||||
alert(this); // null, а не window
|
||||
alert(this); // null
|
||||
*/!*
|
||||
}
|
||||
|
||||
f.call(null);
|
||||
```
|
||||
|
||||
Без `use strict`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
function f() {
|
||||
alert(this); // window
|
||||
}
|
||||
|
||||
f.call(null);
|
||||
```
|
||||
|
||||
[/smart]
|
||||
|
||||
## Итого про this
|
||||
|
||||
|
||||
Значение `this` устанавливается в зависимости от того, как вызвана функция:
|
||||
|
||||
<dl>
|
||||
<dt>При вызове функции как метода</dt>
|
||||
<dd>
|
|
@ -39,7 +39,7 @@ setTimeout( user.sayHi, 1000); // undefined (не Вася!)
|
|||
*/!*
|
||||
```
|
||||
|
||||
**При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`!**
|
||||
При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`!
|
||||
|
||||
Это произошло потому, что в примере выше `setTimeout` получил функцию `user.sayHi`, но не её контекст. То есть, последняя строчка аналогична двум таким:
|
||||
|
||||
|
@ -48,7 +48,8 @@ var f = user.sayHi;
|
|||
setTimeout(f, 1000); // контекст user потеряли
|
||||
```
|
||||
|
||||
**Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?**
|
||||
|
||||
Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?
|
||||
|
||||
Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать.
|
||||
|
||||
|
@ -86,24 +87,23 @@ setTimeout(function() {
|
|||
|
||||
```js
|
||||
function bind(func, context) {
|
||||
return function() {
|
||||
return function() { // (*)
|
||||
return func.apply(context, arguments);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
<dl>
|
||||
<dt>`func`</dt>
|
||||
<dd>Произвольная функция</dd>
|
||||
<dt>`context`</dt>
|
||||
<dd>Произвольный объект</dd>
|
||||
Результатом вызова `bind(func, context)`, как видно из кода, является анонимная функция функция `(*)`, вот она отдельно:
|
||||
|
||||
Результатом вызова `bind(func, context)` будет, как видно из кода, функция-обёртка, которая передаёт все вызовы `func`, указывая при этом правильный контекст `context`.
|
||||
```js
|
||||
function() { // (*)
|
||||
return func.apply(context, arguments);
|
||||
};
|
||||
```
|
||||
|
||||
Чтобы понять, как она работает, нужно вспомнить тему "замыкания" -- здесь `bind` возвращает анонимную функцию, которая при вызове получает контекст `context` из внешней области видимости и передаёт вызов в `func` вместе с этим контекстом `context` и аргументами `arguments`.
|
||||
Если её вызвать с какими-то аргументами, то она сама ничего не делает, а "передаёт вызов" в `func`. Здесь используется `apply`, чтобы вызвать `func` с теми же аргументами, которые получила эта анонимная функция и с контекстом `context`, который берётся из замыкания (был задан при вызове `bind`).
|
||||
|
||||
**В результате вызова `bind` мы получаем как бы "ту же функцию, но с фиксированным контекстом".**
|
||||
Иными словами, в результате вызова `bind` мы получаем "функцию-обёртку", которая прозрачно передаёт вызов в `func`, с фиксированным контекстом `context`.
|
||||
|
||||
Пример с `bind`:
|
||||
|
||||
|
@ -151,7 +151,7 @@ var wrapper = func.bind(context[, arg1, arg2...])
|
|||
<dd>Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.</dd>
|
||||
</dl>
|
||||
|
||||
Результат вызова: `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, но это делается редко, мы поговорим о них позже.
|
||||
Результат вызова `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, тогда и они будут фиксированы, а новые будут уже за ними, но об этом чуть позже.
|
||||
|
||||
Пример со встроенным методом `bind`:
|
||||
|
||||
|
@ -234,13 +234,12 @@ alert( triple(4) ); // = mul(3, 4) = 12
|
|||
alert( triple(5) ); // = mul(3, 5) = 15
|
||||
```
|
||||
|
||||
**При помощи `bind` мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в `setTimeout` или сделать с ней что-то ещё.**
|
||||
При помощи `bind` мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в `setTimeout` или сделать с ней что-то ещё.
|
||||
|
||||
|
||||
## Задачи
|
||||
## Функция дла задач
|
||||
|
||||
|
||||
Рассмотрим для дальнейших задач "функцию для вопросов" `ask`:
|
||||
В задачах этого раздела предполагается, что объявлена следующая "функция вопросов" `ask`:
|
||||
|
||||
```js
|
||||
function ask(question, answer, ok, fail) {
|
||||
|
@ -250,9 +249,9 @@ function ask(question, answer, ok, fail) {
|
|||
}
|
||||
```
|
||||
|
||||
Пока в этой функции ничего особого нет. Её назначение -- задать вопрос `question` и, если ответ совпадёт с `answer`, то запустить функцию `ok()`, а иначе -- функцию `fail()`.
|
||||
Её назначение -- задать вопрос `question` и, если ответ совпадёт с `answer`, то запустить функцию `ok()`, а иначе -- функцию `fail()`.
|
||||
|
||||
Однако, тем не менее, эта функция взята из реального проекта. Просто обычно она сложнее, вместо `alert/prompt` -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно.
|
||||
В реальном проекте она будет сложнее, вместо `alert/prompt` -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно.
|
||||
|
||||
Пример использования:
|
||||
|
|
@ -3,15 +3,14 @@
|
|||
JavaScript предоставляет удивительно гибкие возможности по работе с функциями: их можно передавать, в них можно записывать данные как в объекты, у них есть свои встроенные методы...
|
||||
|
||||
Конечно, этим нужно уметь пользоваться. В этой главе, чтобы более глубоко понимать работу с функциями, мы рассмотрим создание функций-обёрток или, иначе говоря, "декораторов".
|
||||
|
||||
[cut]
|
||||
|
||||
## Примеры декораторов
|
||||
[Декоратор](http://en.wikipedia.org/wiki/Decorator_pattern) -- приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение.
|
||||
|
||||
<a href="http://en.wikipedia.org/wiki/Decorator_pattern">Декоратор</a> -- приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение.
|
||||
*Декоратор* получает функцию и возвращает обертку, которая делает что-то своё "вокруг" вызова основной функции.
|
||||
|
||||
***Декоратор* получает функцию и возвращает обертку, которая делает что-то своё "вокруг" вызова основной функции.**
|
||||
|
||||
### bind -- привязка контекста
|
||||
## bind -- привязка контекста
|
||||
|
||||
Один простой декоратор вы уже видели ранее -- это функция [bind](/bind):
|
||||
|
||||
|
@ -25,11 +24,32 @@ function bind(func, context) {
|
|||
|
||||
Вызов `bind(func, context)` возвращает обёртку, которая ставит `this` и передаёт основную работу функции `func`.
|
||||
|
||||
### Декоратор -- измеритель времени
|
||||
## Декоратор-таймер
|
||||
|
||||
Посмотрим немного более сложный декоратор, замеряющий время выполнения функции:
|
||||
Создадим более сложный декоратор, замеряющий время выполнения функции.
|
||||
|
||||
**При помощи декоратора `timingDecorator` мы можем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени:**
|
||||
Он будет называться `timingDecorator` и получать функцию вместе с "названием таймера", а возвращать -- функцию-обёртку, которая измеряет время и прибавляет его в специальный объект `timer` по свойству-названию.
|
||||
|
||||
Использование:
|
||||
```js
|
||||
function f(x) { } // любая функция
|
||||
|
||||
var timers = {}; // объект для таймеров
|
||||
|
||||
// отдекорировали
|
||||
f = timingDecorator(f, "myFunc");
|
||||
|
||||
// запускаем
|
||||
f(1);
|
||||
f(2);
|
||||
f(3); // функция работает как раньше, но время подсчитывается
|
||||
|
||||
alert(timers.myFunc); // общее время выполнения всех вызовов f
|
||||
```
|
||||
|
||||
При помощи декоратора `timingDecorator` мы сможем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени.
|
||||
|
||||
Его реализация:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -40,7 +60,7 @@ function timingDecorator(f, timer) {
|
|||
return function() {
|
||||
var start = performance.now();
|
||||
|
||||
var result = f.apply(this, arguments);
|
||||
var result = f.apply(this, arguments); // (*)
|
||||
|
||||
if (!timers[timer]) timers[timer] = 0;
|
||||
timers[timer] += performance.now() - start;
|
||||
|
@ -70,11 +90,15 @@ alert( timers.fibo + 'мс' );
|
|||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание на ключевую строку декоратора`var result = f.apply(this, arguments)`.
|
||||
Обратим внимание на строку `(*)` внутри декоратора, которая и осуществляет передачу вызова:
|
||||
|
||||
**Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию, так что изнутри `f` всё выглядит так, как будто это была вызвана она, а не декоратор.**
|
||||
```js
|
||||
var result = f.apply(this, arguments); // (*)
|
||||
```
|
||||
|
||||
### Декоратор для проверки типа
|
||||
Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию `f`, так что изнутри `f` всё выглядит так, как была вызвана она напрямую, а не декоратор.
|
||||
|
||||
## Декоратор для проверки типа
|
||||
|
||||
В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект.
|
||||
|
||||
|
@ -85,6 +109,7 @@ function sum(a, b) {
|
|||
return a + b;
|
||||
}
|
||||
|
||||
// передадим в функцию для сложения чисел нечисловые значения
|
||||
alert( sum(true, { name: "Вася", age: 35 }) ); // true[Object object]
|
||||
```
|
||||
|
||||
|
@ -142,7 +167,7 @@ sum(1, ["array", "in", "sum?!?"]); // некорректный аргумент
|
|||
|
||||
**Один раз пишем декоратор и дальше просто применяем этот функционал везде, где нужно.**
|
||||
|
||||
### Декоратор проверки доступа
|
||||
## Декоратор проверки доступа
|
||||
|
||||
И наконец посмотрим ещё один, последний пример.
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# Методы объектов и контекст вызова
|
||||
|
||||
Начинаем изучать объектно-ориентированную разработку -- как работают объекты и функции, что такое контекст вызова и почему его значение нельзя предсказать. Как, всё же, гарантировать правильный контекст и многие другие, не самые простые, темы.
|
||||
Начинаем изучать объектно-ориентированную разработку -- как работают объекты и функции, что такое контекст вызова и способы его передачи.
|
Loading…
Add table
Add a link
Reference in a new issue