renovations

This commit is contained in:
Ilya Kantor 2015-01-20 18:27:28 +03:00
parent 56b567a5fb
commit f702e3d4ea
21 changed files with 332 additions and 726 deletions

View file

@ -51,7 +51,7 @@ var a = +"123"; // 123
var a = Number("123"); // 123, тот же эффект
```
<table class="bordered">
<table>
<tr><th>Значение</th><th>Преобразуется в...</th></tr>
<tr><td>`undefined`</td><td>`NaN`</td></tr>
<tr><td>`null`</td><td>`0`</td></tr>

View file

@ -1,28 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="179px" height="105px" viewBox="0 0 179 105" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<svg width="148px" height="128px" viewBox="0 0 148 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
<title>Diagrams</title>
<title>variable</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Variable-box" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<path d="M44.5,46 L146.5,46" id="Line-2" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M2.5,102.5 L44.5,46.5" id="Line" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M133.5,101.5 L175.5,45.5" id="Line-3" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M44.5,6 L176.5,6" id="Line-7" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M2.5,62.5 L44.5,6.5" id="Line-8" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M133.5,62.5 L175.5,6.5" id="Line-9" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M44,46.5 L44,6.5" id="Line-4" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<rect id="Rectangle-2" stroke="#979797" stroke-width="2" fill="#E8E8E8" sketch:type="MSShapeGroup" transform="translate(82.959003, 37.776161) rotate(47.000000) translate(-82.959003, -37.776161) " x="43" y="25.247795" width="79.918006" height="25.0567324"></rect>
<text id="&quot;Hello!&quot;" sketch:type="MSTextLayer" transform="translate(86.193270, 39.500000) rotate(47.000000) translate(-86.193270, -39.500000) " font-family="Open Sans" font-size="14" font-weight="normal" fill="#373535">
<tspan x="61.6932699" y="45">"Hello!"</tspan>
</text>
<path d="M176,45 L176,6.5" id="Line-5" stroke="#979797" stroke-width="2" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<rect id="Rectangle-4" stroke="#979797" stroke-width="2" fill="#FFFFFF" sketch:type="MSShapeGroup" x="1" y="63" width="133" height="40"></rect>
<g id="Rectangle-3-+-Oval-1-+-message" sketch:type="MSLayerGroup" transform="translate(38.000000, 76.000000)">
<rect id="Rectangle-3" stroke="#979797" fill="#FFFFFF" sketch:type="MSShapeGroup" x="0" y="0" width="63" height="16"></rect>
<circle id="Oval-1" stroke="#979797" sketch:type="MSShapeGroup" cx="3.5" cy="3.5" r="1.5"></circle>
<text id="message" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#373535">
<tspan x="7" y="12">message</tspan>
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="variable" sketch:type="MSArtboardGroup" transform="translate(-161.000000, -71.000000)">
<g id="noun_1211_cc" sketch:type="MSLayerGroup" transform="translate(161.000000, 71.000000)">
<path d="M17,37.1960457 L129.557785,37.1960457 L129.557785,80.1456641 C129.557785,80.5185728 129.479295,81.0083855 129.279367,81.4404861 C129.07944,81.8725868 112.704625,117 112.704625,117 L112.704625,62.6396695 L129.559266,37.1960457 L148,9 L35.4407339,9 L17,37.1960457 L17,37.1960457 Z" id="Shape" fill="#BCA68E" sketch:type="MSShapeGroup"></path>
<path d="M17,66 L17,38 L2,66" id="Shape" fill="#BCA68E" sketch:type="MSShapeGroup"></path>
<g id="Rectangle-5-+-&quot;World!&quot;" transform="translate(15.000000, 0.000000)">
<path d="M18.9106925,0.395208397 L73.4146415,58.8435379 L55.0893075,75.9321883 L0.585358502,17.4838588 L18.9106925,0.395208397 L18.9106925,0.395208397 Z" id="Rectangle-5" stroke="#8A704D" stroke-width="2" fill="#FFF9EB" sketch:type="MSShapeGroup"></path>
<text id="&quot;Hello!&quot;" sketch:type="MSTextLayer" transform="translate(39.885486, 40.782373) rotate(47.000000) translate(-39.885486, -40.782373) " font-family="Open Sans" font-size="14" font-weight="526" fill="#8A704D">
<tspan x="10.392998" y="46.2823729">"Hello!"</tspan>
</text>
</g>
<path d="M0,68 L0,122.729971 C0,126.150501 1.4836395,128 4.38712201,128 L104.473443,128 C107.59502,128 110,125.4521 110,124.524372 L110,68 L0,68 L0,68 Z" id="Shape" fill="#BCA68E" sketch:type="MSShapeGroup"></path>
</g>
<text id="Message" sketch:type="MSTextLayer" font-family="Open Sans" font-size="18" font-weight="526" fill="#FFFFFF">
<tspan x="178" y="176">Message</tspan>
</text>
</g>
</g>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

@ -3,7 +3,7 @@
Посмотрим, можно ли поменять местами биты слева и справа.
Например, таблица истинности для `^`:
<table class="bordered">
<table>
<tr>
<th>`a`</th>
<th>`b`</th>

View file

@ -149,7 +149,7 @@ alert(result); // 15
В виде таблицы где каждая строка -- вызов функции на очередном элементе массива:
<table class="bordered">
<table>
<thead>
<tr>
<th></th>

View file

@ -331,4 +331,26 @@ var rabbit = new Rabbit('Кроль');
rabbit.run();
```
Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.
Кроме того, есть ещё неявное, но очень важное архитектурное отличие.
Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно.
Иначе говоря, в функциональном стиле в процессе создания `Rabbit` нужно обязательно вызывать `Animal.apply(this, arguments)`, чтобы получить методы родителя -- и если этот `Animal.apply` кроме добавления методов говорит: "Му-у-у!", то это проблема:
```js
function Animal() {
this.walk = function() { alert('walk')};
alert('Му-у-у!');
}
function Rabbit() {
Animal.apply(this, arguments); // как избавиться от мычания, но получить walk?
}
```
...Которой нет в прототипном подходе, потому что в процессе создания `new Rabbit` мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.
Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса -- она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор [6to5](http://6to5.org/).

View file

@ -81,132 +81,11 @@ alert( rabbit instanceof Rabbit ); // false
[/warn]
## instanceof + наследование + try..catch = ♡
Когда мы работаем с внешними данными, возможны самые разные ошибки.
Создание иерархии ошибок вносит порядок в происходящее, а `instanceof` внутри `try..catch` позволяет легко понять, что за ошибка произошла и обработать, либо пробросить её дальше.
Для примера создадим функцию `readUser(json)`, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера -- может, нашего, а может -- чужого, в общем -- желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные -- не важно, для наглядности поработаем с JSON.
Пример правильного JSON: `{ "name": "Вася", "age": 30 }`.
Функция `readUser` должна бросать исключение в случаях, когда:
<ol>
<li>В JSON синтаксическая ошибка, то есть "падает" вызов `JSON.parse`.</li>
<li>В получившемся объекте нет свойства `name` или `age`.</li>
<li>Свойство `age` (возраст) -- не число.</li>
</ol>
Для каждого из этих типов ошибок сделаем отдельный класс -- это поможет позже легко идентифицировать произошедшее:
<ol>
<li>`SyntaxError` -- ошибка "что-то не так в данных", встроенный класс, ошибка такого типа генерируется как раз `JSON.parse`.</li>
<li>`PropertyRequiredError` -- ошибка "нет свойства", будет наследовать от `SyntaxError`, так как является подвидом синтаксической ошибки.</li>
<li>`FormatError` -- "ошибка форматирования", тоже наследник `SyntaxError`.</li>
</ol>
Вот ошибки в JS:
```js
function PropertyRequiredError(property) {
*!*
this.property = property;
this.message = "Отсутствует свойство " + property;
*/!*
this.name = 'PropertyRequired';
}
PropertyRequiredError.prototype = Object.create(SyntaxError.prototype);
function FormatError(message) {
this.message = message;
this.name = 'FormatError';
}
FormatError.prototype = Object.create(SyntaxError.prototype);
```
Понятное дело, эти классы ошибок имеют общий характер и могут использоваться не только в данной конкретной функции, но и в других местах кода -- при обработке любых данных.
**У разных типов ошибок могут быть разные конструкторы, разные дополнительные свойства, которые позволят в дальнейшем удобно работать с ошибкой.**
В коде выше обратите внимание на `PropertyRequiredError` -- конструтор этой ошибки получает отсутствующее свойство и сохраняет его в дополнительном свойстве `property`, в дополнение к стандартному `message`. В дальнейшем, для особой обработки этой ошибки, его легко можно будет получить.
Код ниже -- полная реализация `readUser`:
```js
function PropertyRequiredError(property) {
this.property = property;
this.message = "Отсутствует свойство " + property;
this.name = 'PropertyRequired';
}
PropertyRequiredError.prototype = Object.create(SyntaxError.prototype);
function FormatError(message) {
this.message = message;
this.name = 'FormatError';
}
FormatError.prototype = Object.create(SyntaxError.prototype);
function readUser(data) {
var user = JSON.parse(data);
validateUser(user);
return user;
}
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (typeof user.age != "number") {
throw new FormatError("Возраст - не число");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
try {
readUser('{ "name": "Вася", "age": "unknown" }');
} catch (e) {
*!*
if (e instanceof PropertyRequiredError) {
if (e.property == 'name') {
// в данном месте кода возможны анонимы, ошибка поправима
user[e.property] = "Аноним";
} else {
alert(e.message);
}
} else if (e instanceof SyntaxError) {
alert("Ошибка в данных: " + e.message);
} else {
throw e; // неизвестная ошибка, не знаю что с ней делать
}
*/!*
}
```
Обратим внимание -- в данном конкретном месте кода мы допускаем анонимных посетителей, поэтому в случае, если отсутствует `name` -- исправляем эту ошибку. Мы можем легко это сделать, благодаря наличию у `PropertyRequiredError` дополнительного (по сравнению со стандартными ошибками) свойства `property`.
**Для проверки, какая именно ошибка произошла, вместо `e.name` используется `instanceof`.**
Это позволяет как выделить какие-то отдельные типы ошибок (`e instanceof PropertyRequiredError`), так и проверить общий тип, с которым мы умеем работать, без оглядки на его детали (`e instanceof SyntaxError`).
Благодаря `instanceof` мы получили удобную поддержку иерархии ошибок, с возможностью в любой момент добавить новые классы, понятным кодом и предсказуемым поведением.
## Итого
<ul>
<li>Оператор `obj instanceof Func` проверяет тот факт, что `obj` является результатом вызова `new Func`. Он учитывает цепочку `__proto__`, поэтому наследование поддерживается.</li>
<li>Оператор `instanceof` не сможет проверить тип значения, если объект создан в одном окне/фрейме, а проверяется в другом. Это потому, что в каждом окне -- своя иерархия объектов. Для точной проверки типов встроенных объектов можно использовать свойство `[[Class]]`.</li>
<li>Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования.</li>
</ul>
Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования.

View file

@ -1,300 +0,0 @@
# Фреймворк Class.extend
Можно использовать прототипное наследование и не повторять `Rabbit.prototype.method = ...` при определении каждого метода, не иметь проблем с конструкторами и так далее.
Для этого используют ООП-фреймворк -- библиотеку, в которой об этом подумали "за нас".
В этой главе мы рассмотрим один из таких фреймворков:
<ul>
<li>С удобным синтаксисом наследования.</li>
<li>С вызовом родительских методов.</li>
<li>С поддержкой статических методов и *примесей*.</li>
</ul>
Можно сказать, что фреймворк представляет собой "синтаксический сахар" к наследованию на классах.
[cut]
Оригинальный код этого фреймворка были предложены Джоном Ресигом: [Simple JavaScript Inheritance](http://ejohn.org/blog/simple-javascript-inheritance/), но подход это не новый, его варианты используются во многих фреймворках и знакомство с ним будет очень кстати.
Полный код фреймворка: [class-extend.js](http://js.cx/libs/class-extend.js). Он содержит много комментариев, чтобы было проще его понять, но смотреть его лучше после того, как ознакомитесь с возможностями.
## Создание класса
Итак, начнём.
Фреймворк предоставляет всего один метод: `Class.extend`.
Чтобы представлять себе, как выглядит класс "на фреймворке", взглянем на рабочий пример:
```js
//+ run
// Объявление класса Animal
var Animal = *!*Class.extend*/!*({
init: function(name) {
this.name = name;
},
run: function() {
alert(this.name + " бежит!");
}
});
// Создать (вызовется `init`)
var animal = new Animal("Зверь");
// Вызвать метод
animal.run(); // "Зверь бежит!"
```
Готово, создан класс `Animal`.
Внутри `Class.extend(props)` делает следующее:
<ul>
<li>Создаётся объект, в который копируются свойства и методы из `prop`. Это будет прототип.</li>
<li>Объявляется функция, которая вызывает `this.init`, в её `prototype` записывается созданный прототип.</li>
<li>Эта функция возвращается, она и есть конструктор `Animal`.</li>
</ul>
Как видим, всё весьма просто.
Но фреймворк этим не ограничивается и добавляет ряд других интересных возможностей.
## Статические свойства
У метода `Class.extend` есть и второй, необязательный аргумент: объект `staticProps`.
Если он есть, то его свойства копируются в саму функцию-конструктор.
Например:
```js
//+ run
// Объявить класс Animal
var Animal = Class.extend({
init: function(name){
this.name = name;
},
toString: function(){
return this.name;
}
},
*!*
{ // статические свойства
compare: function(a, b) {
return a.name - b.name;
}
});
*/!*
var arr = [new Animal('Зорька'), new Animal('Бурёнка')]
*!*
arr.sort(Animal.compare);
*/!*
alert(arr); // Бурёнка, Зорька
```
## Наследование
Метод `extend` копируется в создаваемые классы.
Поэтому его можно вызывать на любом конструкторе, чтобы создать ему класс-наследник.
Например, создадим `Rabbit`, наследующий от `Animal`:
```js
//+ run
// Создать Animal, всё как обычно
var Animal = Class.extend({
init: function(name) {
this.name = name;
},
run: function() {
alert(this.name + ' бежит!');
}
});
// Объявить класс Rabbit, *!*наследующий*/!* от Animal
var Rabbit = *!*Animal.extend*/!*({
init: function(name) {
*!*
this._super(name); // вызвать родительский init(name)
*/!*
},
run: function() {
this._super(); // вызвать родительский run
alert('..и мощно прыгает за морковкой!');
}
});
*!*
var rabbit = new Rabbit("Кроль");
rabbit.run(); // "Кроль бежит!", затем "..и мощно прыгает за морковкой!"
*/!*
```
## Метод this._super
В коде выше появился ещё один замечательный метод: `this._super`.
**Вызов `this._super(аргументы)` вызывает метод *родительского класса*, с указанными аргументами.**
То есть, здесь он запустит родительский `init(name)`:
```js
init: function(name) {
this._super(name); // вызвать родительский init(name)
}
```
...А здесь -- родительский `run`:
```js
run: function() {
this._super(); // вызвать родительский run
alert('..и мощно прыгает за морковкой!');
}
```
Работает это, примерно, так: когда фреймворк копирует методы в прототип, он смотрит их код, и если видит там слово `_super`, то оборачивает метод в обёртку, которая ставит `this._super` в метод родителя, затем вызывает метод, а затем возвращает `this._super` как было ранее.
Это вызывает некоторые дополнительные расходы при объявлении, так как чтобы проверить, есть ли обращение к `_super`, фреймворк при копировании методов преобразует их через `toString` в строку и ищет в ней обращение.
Как правило, эти расходы несущественны, если нужно их минимизировать -- не составляет труда изъять эту возможность из фреймворка или учесть в инструментах сжатия (минификации) кода.
Кстати, примерно это минификатор Google Closure Compiler, когда сжимает код, написанный на "дружащей" с ним Google Closure Library.
## Примеси [#mixins]
Согласно теории ООП, *примесь* (англ. mixin) -- класс, реализующий какое-либо чётко выделенное поведение, который не предназначен для порождения самостоятельно используемых объектов, а используется для *уточнения* поведения других классов.
Иными словами, *примесь* позволяет легко добавить в существующий класс новые возможности, например:
<ul>
<li>Публикация событий и подписка на них.</li>
<li>Работ c шаблонизатором.</li>
<li>... любое поведение, дополняющее объект.</li>
</ul>
**Как правило, примесь реализуется в виде объекта, свойства которого копируются в прототип.**
Например, напишем примесь `EventMixin` для работы с событиями. Она будет содержать три метода -- `on/off` (подписка) и `trigger` (генерация события):
```js
var EventMixin = {
on: function (eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
off: function(eventName, handler) {
...
},
trigger: function (eventName, args) {
if (!this.eventHandlers || !this._eventHandlers[eventName]) {
return;
}
var handlers = this._eventHandlers[eventName];
for (var i = 0; i < handlers.length; i++) {
handlers[i].apply(this, args);
}
}
};
```
Скопировав свойства из `EventMixin` в любой объект, мы дадим ему возможность генерировать события (`trigger`) и подписываться на них (`on/off`).
Чтобы было проще, во фреймворк добавлена возможность указания примесей при объявлении класса.
**Для добавления примесей у метода `Class.extend` существует синтаксис с первым аргументом-массивом:**
<ul>
<li>**`Class.extend([mixin1, mixin2...], props, staticProps)`.**</li>
</ul>
Если первый аргумент -- массив, то его элементы `mixin1, mixin2..` записываются в прототип по очереди, перед `props`, примерно так:
```js
for(var key in mixin1) prototype[key] = mixin1[key];
for(var key in mixin2) prototype[key] = mixin2[key];
...
for(var key in props) prototype[key] = props[key];
```
При этом, если названия методов совпадают, то последующий затрёт предыдущий, так как в объекте может быть только одно свойство с данным названием. Впрочем, обычно такого не происходит, т.к. примеси проектируются так, чтобы их методы были уникальными и ни с чем не конфликтовали.
Применение:
```js
*!*
var Rabbit = Class.extend( [ EventMixin ], {
*/!*
/* свойства и методы для Rabbit */
});
var rabbit = new Rabbit();
*!*rabbit.on*/!*("jump", function() { // повесить функцию на событие jump
alert("jump &-@!");
});
*!*rabbit.trigger*/!*('jump'); // alert сработает!
```
Примеси могут быть самыми разными. Например `TemplateMixin` для работы с шаблонами:
```js
Rabbit = Class.extend([EventMixin, TemplateMixin], {
/* Теперь Rabbit умеет использовать события и шаблоны */
});
```
Красиво, не правда ли? Всего лишь указали одну-другую примесь и объект уже всё умеет!
Примеси могут задавать и многое другое, например автоматически подписывать компонент на стандартные события, добавлять AJAX-функционал и т.п.
## Итого
<ol>
<li>**Фреймворк имеет основной метод `Class.extend` с несколькими вариациями:**
<ul>
<li>`Class.extend(props)` -- просто класс с прототипом `props`.</li>
<li>`Class.extend(props, staticProps)` -- класс с прототипом `props` и статическими свойствами `staticProps`.</li>
<li>`Class.extend(mixins, props [, staticProps])` -- если первый аргумент массив, то он интерпретируется как примеси. Их свойства копируются в прототип перед `props`.</li>
</ul>
</li>
<li>**У созданных этим методом классов также есть `extend` для продолжения наследования.**</li>
<li>**Методы родителя можно вызвать при помощи `this._super(...)`.**</li>
</ol>
Плюсы и минусы:
[compare]
+Такой фреймворк удобен потому, что класс можно задать одним вызовом `Class.extend`, с читаемым синтаксисом, удобным наследованием и вызовом родительских методов.
-Редакторы и IDE, как правило, не понимают такой синтаксис, а значит, не предоставляют автодополнение. При этом они обычно понимают объявление методов через явную запись в объект или в прототип.
-Есть некоторые дополнительные расходы, связанные с реализацией `_super`. Если они критичны, то их можно избежать.[/compare]
То, как работает фреймворк, подробно описано в комментариях: [class-extend.js](http://js.cx/libs/class-extend.js).
[head]
<script src="http://js.cx/libs/class-extend.js"></script>
[/head]

View file

@ -0,0 +1,28 @@
```js
//+ run
function FormatError(message) {
this.name = "FormatError";
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error()).stack;
}
}
FormatError.prototype = Object.create(SyntaxError.prototype);
FormatError.prototype.constructor = FormatError;
// Использование
var err = new FormatError("ошибка форматирования");
alert(err.message); // ошибка форматирования
alert(err.name); // FormatError
alert(err.stack); // стек на момент генерации ошибки
alert(err instanceof SyntaxError); // true
```

View file

@ -0,0 +1,17 @@
# Унаследуйте от SyntaxError
[importance 5]
Создайте ошибку `FormatError`, которая будет наследовать от встроенного класса `SyntaxError`.
Синтаксис для её создания -- такой же, как обычно:
```js
var err = new FormatError("ошибка форматирования");
alert(err.message); // ошибка форматирования
alert(err.name); // FormatError
alert(err.stack); // стек на момент генерации ошибки
alert(err instanceof SyntaxError); // true
```

View file

@ -0,0 +1,230 @@
# Свои ошибки, наследование от Error
Когда мы работаем с внешними данными, возможны самые разные ошибки.
Если приложение сложное, то ошибки естественным образом укладываются в иерархию, разобраться в которой помогает `instanceof`.
## Свой объект ошибки
Для примера создадим функцию `readUser(json)`, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера -- может, нашего, а может -- чужого, в общем -- желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные -- не важно, для наглядности поработаем с JSON.
Пример `json` на входе в функцию: `{ "name": "Вася", "age": 30 }`.
В процессе работы `readUser` возможны различные ошибки. Одна -- очевидно, `SyntaxError` -- если передан некорректный JSON.
Но могут быть и другие, например `PropertyError` -- эта ошибка будет возникать, если в прочитанном объекте нет свойства `name` или `age`.
Реализуем её:
```js
function PropertyError(property) {
this.name = "PropertyError";
this.property = property;
this.message = "Ошибка в свойстве " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, PropertyError);
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
```
Посмотрим внимательнее на код -- в нём важные детали того, как можно позаботиться о стандартных свойствах объекта ошибки:
<dl>
<dt>`name` -- имя ошибки.</dt>
<dd>Должно совпадать с именем функции, просто записали строку в него.</dd>
<dt>`message` -- сообщение об ошибке.</dt>
<dd>Несмотря на то, что `PropertyError` наследует от `Error` (последняя строка), конструктор у неё немного другой. Он принимает не сообщение об ошибке, а название свойства `property`, ну а сообщение генерируется из него.
В результате в ошибке есть как стандартное свойство `message`, так и более точное `property`.
Это полезная практика -- добавлять в объект ошибки свойства, более подробно описывающие ситуацию, которых нет в базовых объектах `Error`.</dd>
<dt>`stack` -- стек вызовов, которые в итоге привели к ошибке.</dt>
<dd>У встроенных объектов `Error` это свойство есть автоматически, вот к примеру:
```js
//+ run
function f() {
alert( new Error().stack );
}
f();
```
Если же объект делаем мы, то "по умолчанию" такого свойства у него не будет. Нам нужно как-то самим узнавать последовательность вложенных вызовов на текущий момент. Однако удобного способа сделать это в JavaScript нет, поэтому мы поступаем хитро и копируем его из нового объекта `new Error`, который генерируем тут же.
В V8 (Chrome, Opera, Node.JS) есть нестандартное расширение [Error.captureStackTrace](https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi), которое позволяет стек получать.
Строка из кода выше:
```js
Error.captureStackTrace(this, PropertyError);
```
Вызов записывает в объект `this` (текущий объект ошибки) стек вызовов, а второй аргумент -- это текущий конструктор, он не обязателен, но если есть, то говорит, что при генерации стека нужно на этой функции остановиться. В результате в стеке не будет информации о том, что делалось внутри конструктора `PropertyError`.
То есть, будет последовательность вызовов до генерации ошибки, но не включая код самого конструктора ошибки, который, как правило, не интересен. Такое поведение максимально соответствует встроенным ошибкам JavaScript.
</dd>
</dl>
[smart header="Конструктор родителя здесь не нужен"]
В коде выше не вызывается конструктор родителя. Обычно, когда мы наследуем, то мы вызываем его.
В данном случае вызов выглядел бы как `Error.call(this, message)`.
Однако, встроенный конструктор `Error` на редкость прост, он ничего полезного не делает, даже свойство `this.message` (не говоря уже об `name` и `stack`) не назначает. Поэтому и вызывать его здесь нет необходимости.
[/smart]
## instanceof + try..catch = ♡
Давайте теперь используем наш новый класс для `readUser`:
```js
//+ run
*!*
// Объявление
*/!*
function PropertyError(property) {
this.name = "PropertyError";
this.property = property;
this.message = "Отсутствует свойство " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, PropertyError);
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
*!*
// Генерация ошибки
*/!*
function readUser(data) {
var user = JSON.parse(data);
if (!user.age) {
throw new PropertyError("age");
}
if (!user.name) {
throw new PropertyError("name");
}
return user;
}
*!*
// Запуск и try..catch
*/!*
try {
var user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof PropertyError) {
if (err.property == 'name') {
// если в данном месте кода возможны анонимы, то всё нормально
*!*
alert("Здравствуйте, Аноним!");
*/!*
} else {
alert(err.message); // Отсутствует свойство ...
}
} else if (err instanceof SyntaxError) {
alert("Ошибка в данных: " + err.message);
} else {
throw err; // неизвестная ошибка, не знаю что с ней делать
}
}
```
Обратим внимание на проверку типа ошибки в `try..catch`.
Оператор `instanceof` поддерживает иерархию. Это значит, что если мы в дальнейшем решим как-то ещё уточнить тип ошибки `PropertyError`, то для объекта, наследующего от него, `e instanceof PropertyError` по-прежнему будет работать.
## Дальнейшее наследование
Чтобы создать иерархию, нужно наследовать от `PropertyError`.
`PropertyError` -- это просто общего вида ошибка в свойстве. Создадим ошибку `PropertyRequiredError`, которая означает, что свойства нет.
Типичный вид конструктора-наследника -- такой:
```js
function PropertyRequiredError(property) {
PropertyError.apply(this, arguments);
...
}
```
Можем ли мы просто вызвать конструктор родителя и ничего не делать в дополнение? Увы, нет.
Если так поступить, то свойство `this.name` будет некорректным, да и `Error.captureStackTrace` тоже получит неправильную функцию вторым параметром.
Можно ли как-то поправить конструктор родителя? Убрать из него все упоминания о конкретном классе `PropertyError` и сделать код универсальным?
Частично -- да. Как мы помним, существует свойство `constructor`, которое есть в `prototype` по умолчанию, и которое мы можем намеренно сохранить при наследовании:
```js
function PropertyError(property) {
this.name = "PropertyError";
this.property = property;
this.message = "Отсутствует свойство " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, *!*this.constructor*/!*); // (*)
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
*!*
PropertyError.prototype.constructor = PropertyError;
*/!*
```
В строке `(*)` это свойство было использовано, чтобы получить конструктор уже не в виде жёсткой ссылки `PropertyError`, а тот, который использован для текущего объекта. В наследнике там будет `PropertyRequiredError`, как и задумано.
Мы убрали одно упоминание, но с `this.name`, увы, сложности. Сейчас при наследовании оно будет всегда `"PropertyError"`. Все браузеры, кроме IE11-, поддерживают имя у Function Declaration, то есть имя функции можно было бы получить из `this.constructor.name`, но в IE11- это работать не будет.
Если подерживать IE11-, то тут уж придётся в наследнике его записывать вручную.
Полный код для наследника:
```js
function PropertyRequiredError(property) {
PropertyError.apply(this, arguments);
this.name = 'PropertyRequiredError';
this.message = 'Отсутствует свойство ' + property;
}
PropertyRequiredError.prototype = Object.create(PropertyError);
PropertyRequiredError.prototype.constructor = PropertyRequiredError;
var err = new PropertyRequiredError("age");
// пройдёт проверку
alert(err instanceof PropertyError); // true
```
Здесь заодно и `message` было перезаписано на более точное. Если хочется избежать записи и перезаписи, то можно оформить его в виде геттера через `Object.defineProperty`.
## Итого
<ul>
<li>Чтобы наследовать встроенному классу ошибок `Error`, нужно самостоятельно позаботиться о `name`, `message` и `stack`.</li>
<li>Благодаря `instanceof` мы получили удобную поддержку иерархии ошибок, с возможностью в любой момент добавить новые классы, понятным кодом и предсказуемым поведением.</li>
</ul>
Чтобы создавать наследники от `Error` было проще, можно создать класс `CustomError`, записать в него универсальный код, наподобие `PropertyError` и далее наследовать уже от него.

View file

@ -1,266 +0,0 @@
# Сравнение с функциональным наследованием
В этой главе мы озаботимся тем, чтобы сравнить прототипный и функциональный подход к ООП.
Причём, сделать это грамотно, с учётом того, что реально происходит "под капотом" интерпретатора.
Нас интересуют три показателя:
<ol>
<li>Эффективность по памяти.</li>
<li>Скорость работы.</li>
<li>Архитектурные ограничения (если есть) и плюшки.</li>
</ol>
[cut]
## Класс Machine
Объявим класс `Machine` двумя способами:
<dl>
<dt>Функциональное объявление:</dt>
<dd>Состоит из единственной функции-конструктора, которая записывает в объект всё, что нужно. Приватные данные сохраняются в локальные переменные и доступны через замыкание:
```js
function MachineOne(power) {
var enabled = false;
this.enable = function() { enabled = true; };
this.disable = function() { enabled = false; };
// ...
}
```
При этом у каждого объекта будет своя копия методов `enable` и `disable`, которые создаются каждый раз заново.
</dd>
<dt>Прототипное объявление:</dt>
<dd>В объекте хранится только то, что ему нужно. Методы записываются в прототип:
```js
function MachineTwo(power) {
this._enabled = false;
}
MachineTwo.prototype.enable = function() {
this._enabled = true;
};
MachineTwo.prototype.disable = function() {
this._enabled = false;
};
```
</dd>
</dl>
## Сравнение памяти
Оценим затраты памяти в функциональном стиле:
```js
var machine = new MachineOne();
затраты памяти =
сам объект +
свойства и методы в нём (this.enable/disable) +
замыкание, объект с приватными переменными (var enabled)
```
В этой, казалось бы, очевидной формуле кроется серьёзная ошибка.
По коду кажется, каждый объект хранит свою копию методов `this.enable/disable`, однако это не совсем так.
Интерпретаторы оптимизируют создание и хранение одинаковых одинаковых функций. "Под капотом" *строка с кодом* функции `enable/disable` хранится только один раз, и её разделяют между собой все объекты `MachineOne`. То есть, если вывести функцию в виде строки `alert(machine.enable)`, то каждый объект возьмёт код из единого для всех места в памяти.
Далее, при использовании, строка с кодом на JavaScript превращается в *машинный код*, который может по-разному оптимизироваться, в зависимости от того, как именно используется функция, но и здесь интерпретатор старается разделять одинаково оптимизированный код между объектами.
**То есть, на самом деле в каждом объекте хранится не полная копия метода, а скорее "метаданные", которые указывают, где в памяти лежит соответствующим образом оптимизированная функция.**
Теперь прототипный стиль:
```js
var machine = new MachineTwo();
затраты памяти =
сам объект +
свойства (this._enabled)
```
Если сравнить, то мы видим, что значение `var enabled` переместилось в сам объект, произошла небольшая экономия на объекте LexicalEnvironment, который больше не нужен.
Кроме того, методы находятся в прототипе. Интерпретатор делает неплохую работу по оптимизации функционального стиля можно сказать, что "почти вся" информация о функциях будет разделяться между объектами, но в прототипном подходе функции разделяются на 100%, без "почти".
**Вывод: прототипный стиль требует меньше памяти, так как не хранится LexicalEnvironment и методы (совсем).**
В случае, когда объект хранит мало данных, и методы маленькие, разница в памяти может быть существенной. В браузере Chrome (V8) для описанных выше `MachineOne` и `MachineTwo` она может составлять 5-8 раз. Но это лишь потому, что объекты полностью синтетические, в них почти нет кода и данных. В реальности она меньше, порядка 1-3 раз, конечно это зависит от конкретного объекта.
## Сравнение производительности
Создание объекта в функциональном стиле дольше, поскольку происходят присвоения в `this`. Это очевидно.
Но может показаться, что при этом скорость доступа к таким методом "особо быстрая", так как они хранятся в самом объекте, а не в его прототипе.
Это не так.
"Под капотом" интерпретатор при первом вызове метода пробежится по цепочке `__proto__`, запомнит место, где его нашёл, и далее будет обращаться прямо туда.
**В современных браузерах скорость доступа к методам в прототипе и в объекте одинакова.**
Функциональный стиль и здесь не имеет преимущества.
## Красота синтаксиса
В функциональном стиле мы имеем красивые приватные переменные и функции. Это хорошо.
Но пользоваться публичными методами менее удобно.
Скажем, мы хотим при создании `new Machine` тут же включить машину вызовом `this.enable()`:
```js
function Machine(power) {
var enabled = false;
this.enable = function() { enabled = true; };
this.disable = function() { enabled = false; };
*!*
// нужно писать этот вызов внизу
this.enable();
*/!*
}
```
Мы вынуждены написать вызов `this.enable()` внизу, под определением соответствующего метода.
**Если методов много и они длинные, то получается, что при чтении кода нам нужно проматывать в конец файла. Это неудобно!**
Типичное средство обхода -- объявлять все методы через Function Declaration, а внизу выносить во внешний интерфейс нужные:
```js
function Machine(power) {
var enabled = false;
enable();
function enable() { enabled = true; };
function disable() { enabled = false; };
*!*
this.enable = enable;
this.disable = disable;
*/!*
}
```
Ничего такого, но приходится писать лишние буквы, а у программиста и так нелёгкий труд.
Прототипное наследование похожей проблемы не имеет. Зато там нужно писать слово `prototype`, что, впрочем, исправляется различными ООП-фреймворками.
## Архитектурные ограничения
Наследование, реализованное в функциональном стиле, обладает важным архитектурным ограничением.
**Конструктор наследника получает контроль лишь после полной инициализации родителя, и это может быть слишком поздно.**
Например, пусть конструктор `Machine` при инициализации вызывает свой метод `work()`. Это достаточно типично, что при создании объект тут же делает что-то полезное или заполняет себя важными данными.
Потомок -- `CoffeeMachine` захочет переопределить этот метод. Реализация будет выглядеть так:
```js
//+ run
// Родитель:
function Machine() {
this.work = function() {
alert('Гр-р-р-р! Бям-бямс...');
};
this.work();
}
// Потомок:
function CoffeeMachine() {
Machine.apply(this, arguments);
*!*
// попытаемся переопределить метод в потомке
this.work = function() {
alert('Вжжжжжжжжж!');
};
*/!*
}
// переопределение не сработало!
*!*
var coffeeMachine = new CoffeeMachine(); // Гр-р-р-р! Бям-бямс...!
*/!*
```
Вызвался метод `work` не потомка, а родителя!
Это естественно, ведь первым делом мы вызвали `Machine.apply(this, arguments)`, в котором используется старый `work`.
**Методы для инициализации, уже использованные родителем, переопределить в потомке нельзя: слишком поздно.**
Недостаток этот -- весьма серьёзный. Фактически, он ограничивает возможности построения архитектуры.
Заметим, что при использовании прототипов такой проблемы не возникает. Потому что сначала полностью задаются конструкторы, методы, задаётся порядок поиска через прототипы, а уже *потом* создаются объекты.
Аналогичный код через прототипы:
```js
//+ run
function Machine() {
this.work();
}
Machine.prototype.work = function() {
alert('Гр-р-р-р! Бям-бямс...');
};
function CoffeeMachine() {
Machine.apply(this, arguments);
}
CoffeeMachine.prototype = Object.create(Machine.prototype);
CoffeeMachine.prototype.work = function() {
alert('Вжжжжжжжжж!');
};
// переопределение сработает, work найден в CoffeeMachine.prototype
*!*
var coffeeMachine = new CoffeeMachine(); // Вжжжжжжжжж!
*/!*
```
## Не учитывается наследование в instanceof
Есть и ещё одна проблема функционального подхода.
При наследовании в функциональном стиле проверка `coffeeMachine instanceof Machine` вернёт `false`.
Это вполне естественно, ведь, формально говоря, `CoffeeMachine` не является `Machine`.
Единственная связь между ними -- конструктор `CoffeeMachine` вызвал функцию `Machine` в своём контексте. Оператор `instanceof` работает через проверку цепочки прототипов, а здесь её нет.
**Здесь прототипный подход гораздо удобнее.**
Конечно, можно попробовать запоминать, кого и в каком порядке вызывали, разработать свой аналог `instanceof`, но обычно так не делают, т.к. в прототипах встроенный `instanceof` просто работает.
## Сжатие JavaScript
При функциональном наследовании используются локальные переменные и функции.
Современные средства сжатия JavaScript переименовывают их, делая короче и таким образом уменьшая размер кода.
**Это означает, что код, написанный в функциональном стиле, сожмётся лучше.**
## Итого
Получилось, что функциональный паттерн в сочетании с наследованием обладает рядом серьёзных проблем.
Его, по сути, основное достоинство -- это использование локальных функций и переменных, в которые никак нельзя залезть снаружи, и которые дают лучшее сжатие кода минификаторами.
Кроме того, если программировать без фреймворков, то функциональный стиль -- наиболее нагляден и прост.
Но если в проекте нужен единообразный стиль ООП, то лучше использовать прототипный подход, возможно прибавив "сахарку" в виде ООП-фреймворка (тысячи их), а функциональный использовать в тех случаях, когда *уже есть* сторонняя библиотека или конструкторы в этом стиле, которые нужно расширить.

View file

@ -194,7 +194,7 @@ function compareDocumentPosition(a, b) {
```
Список битовых масок для проверки:
<table class="bordered">
<table>
<tr>
<th>Биты</th>
<th>Число</th>

View file

@ -422,7 +422,7 @@ alert( document.body.getAttribute('AbBa') ); // что должен вернут
Таблица сравнений для атрибутов и свойств:
<table class="bordered">
<table>
<tr>
<th>Свойства</th>
<th>Атрибуты</th>

View file

@ -229,7 +229,7 @@ document.getElementById('my').onkeypress = function(e) {
Перечислим их в таблице, обращая основное внимание на особенности работы с ними.
<table class="bordered">
<table>
<tr>
<th>Категория</th>
<th>Примеры</th>

View file

@ -175,7 +175,7 @@ function showCount() {
События изменения данных:
<table class="bordered">
<table>
<tr>
<th>Событие</th>
<th>Описание</th>

View file

@ -169,7 +169,7 @@ var timer = document.createElement("button", "my-timer");
Следующие методы автоматически вызываются во время жизненного цикла элемента:
<table class="bordered">
<table>
<tr><td>`createdCallback`</td><td>Элемент создан</td></tr>
<tr><td>`attachedCallback`</td><td>Элемент добавлен в документ</td></tr>
<tr><td>`detachedCallback`</td><td>Элемент удалён из документа</td></tr>

View file

@ -25,7 +25,7 @@
Они были детально рассмотрены в предыдущих главах раздела.
<table class="bordered">
<table>
<tr>
<th></th>
<th>`XMLHttpRequest`</th>

View file

@ -112,7 +112,7 @@ if (!document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Basi
<li>На каждом из этих отрезков берётся точка, находящаяся от начала на расстоянии от 0 до `t` пропорционально длине. То есть, при `t=0` -- точка будет в начале, при `t=0.25` -- на расстоянии в 25% от начала отрезка, при `t=0.5` -- 50%(на середине), при `t=1` -- в конце. Так как **чёрных** отрезков -- два, то и точек выходит две штуки.</li>
<li>Эти точки соединяются. На рисунке ниже соединяющий их отрезок изображён <span style="color:blue">синим</span>.
<table class="bordered">
<table>
<tr><td>При `t=0.25`</td><td>При `t=0.5`</td></tr>
<tr>
<td><img src="bezier3-draw1.png"></td>

View file

@ -166,7 +166,7 @@ CSS-анимации особенно рекомендуются на мобил
Остальные кривые являются короткой записью следующих `cubic-bezier`. В таблице ниже показано соответствие:
<table class="bordered">
<table>
<tr>
<th>`ease`</th>
<th>`ease-in`</th>

View file

@ -370,10 +370,10 @@ SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
Но, в остальном, это совершенно разные вещи.
<table class="bordered">
<table>
<thead>
<th style="text-align:center">Экстерн</th>
<th style="text-align:center">Экспорт</th>
<th>Экстерн</th>
<th>Экспорт</th>
</thead>
<tbody>
<tr>

View file

@ -147,7 +147,7 @@ var formatter = new Intl.DateFormatter([locales, [options] ])
Первый аргумент -- такой же, как и в `Collator`, а в объекте `options` мы можем определить, какие именно части даты показывать (часы, месяц, год...) и в каком формате.
Полный список свойств `options`:
<table class="bordered">
<table>
<tr>
<th>Свойство</th>
<th>Описание</th>
@ -321,7 +321,7 @@ formatter.format(number); // форматирование
Список опций:
<table class="bordered">
<table>
<tr>
<th>Свойство </th>
<th>Описание </th>