renovations
This commit is contained in:
parent
56b567a5fb
commit
f702e3d4ea
21 changed files with 332 additions and 726 deletions
|
@ -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>
|
||||
|
|
|
@ -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=""Hello!"" 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-+-"World!"" 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=""Hello!"" 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 |
|
@ -3,7 +3,7 @@
|
|||
Посмотрим, можно ли поменять местами биты слева и справа.
|
||||
|
||||
Например, таблица истинности для `^`:
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>`a`</th>
|
||||
<th>`b`</th>
|
||||
|
|
|
@ -149,7 +149,7 @@ alert(result); // 15
|
|||
|
||||
В виде таблицы где каждая строка -- вызов функции на очередном элементе массива:
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
|
|
@ -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/).
|
||||
|
||||
|
|
|
@ -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` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования.
|
||||
|
|
|
@ -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]
|
28
1-js/9-prototypes/7-oop-errors/1-format-error/solution.md
Normal file
28
1-js/9-prototypes/7-oop-errors/1-format-error/solution.md
Normal 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
|
||||
```
|
17
1-js/9-prototypes/7-oop-errors/1-format-error/task.md
Normal file
17
1-js/9-prototypes/7-oop-errors/1-format-error/task.md
Normal 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
|
||||
```
|
230
1-js/9-prototypes/7-oop-errors/article.md
Normal file
230
1-js/9-prototypes/7-oop-errors/article.md
Normal 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` и далее наследовать уже от него.
|
|
@ -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 переименовывают их, делая короче и таким образом уменьшая размер кода.
|
||||
|
||||
**Это означает, что код, написанный в функциональном стиле, сожмётся лучше.**
|
||||
|
||||
## Итого
|
||||
|
||||
Получилось, что функциональный паттерн в сочетании с наследованием обладает рядом серьёзных проблем.
|
||||
|
||||
Его, по сути, основное достоинство -- это использование локальных функций и переменных, в которые никак нельзя залезть снаружи, и которые дают лучшее сжатие кода минификаторами.
|
||||
|
||||
Кроме того, если программировать без фреймворков, то функциональный стиль -- наиболее нагляден и прост.
|
||||
|
||||
Но если в проекте нужен единообразный стиль ООП, то лучше использовать прототипный подход, возможно прибавив "сахарку" в виде ООП-фреймворка (тысячи их), а функциональный использовать в тех случаях, когда *уже есть* сторонняя библиотека или конструкторы в этом стиле, которые нужно расширить.
|
|
@ -194,7 +194,7 @@ function compareDocumentPosition(a, b) {
|
|||
```
|
||||
|
||||
Список битовых масок для проверки:
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Биты</th>
|
||||
<th>Число</th>
|
||||
|
|
|
@ -422,7 +422,7 @@ alert( document.body.getAttribute('AbBa') ); // что должен вернут
|
|||
|
||||
Таблица сравнений для атрибутов и свойств:
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Свойства</th>
|
||||
<th>Атрибуты</th>
|
||||
|
|
|
@ -229,7 +229,7 @@ document.getElementById('my').onkeypress = function(e) {
|
|||
|
||||
Перечислим их в таблице, обращая основное внимание на особенности работы с ними.
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Категория</th>
|
||||
<th>Примеры</th>
|
||||
|
|
|
@ -175,7 +175,7 @@ function showCount() {
|
|||
|
||||
События изменения данных:
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Событие</th>
|
||||
<th>Описание</th>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
Они были детально рассмотрены в предыдущих главах раздела.
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>`XMLHttpRequest`</th>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -166,7 +166,7 @@ CSS-анимации особенно рекомендуются на мобил
|
|||
|
||||
Остальные кривые являются короткой записью следующих `cubic-bezier`. В таблице ниже показано соответствие:
|
||||
|
||||
<table class="bordered">
|
||||
<table>
|
||||
<tr>
|
||||
<th>`ease`</th>
|
||||
<th>`ease-in`</th>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue