From f702e3d4eabb00172e538ab6b5a66e1ccb510299 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 20 Jan 2015 18:27:28 +0300 Subject: [PATCH] renovations --- .../13-types-conversion/article.md | 2 +- 1-js/2-first-steps/4-variables/variable.svg | 38 +-- .../3-bitwise-symmetry/solution.md | 2 +- .../9-array-iteration/article.md | 2 +- .../5-class-inheritance/article.md | 22 ++ 1-js/9-prototypes/6-instanceof/article.md | 125 +------- 1-js/9-prototypes/7-class-extend/article.md | 300 ------------------ .../7-oop-errors/1-format-error/solution.md | 28 ++ .../7-oop-errors/1-format-error/task.md | 17 + 1-js/9-prototypes/7-oop-errors/article.md | 230 ++++++++++++++ .../8-why-prototypes-better/article.md | 266 ---------------- .../10-compare-document-position/article.md | 2 +- .../article.md | 2 +- .../8-keyboard-events/article.md | 2 +- .../3-events-change/article.md | 2 +- .../2-webcomponent-core/article.md | 2 +- 3-more/10-ajax/16-ajax-summary/article.md | 2 +- 3-more/2-animation/2-bezier/article.md | 2 +- 3-more/2-animation/3-css-animation/article.md | 2 +- .../3-gcc-advanced-optimization/article.md | 6 +- 3-more/6-extra/11-intl/article.md | 4 +- 21 files changed, 332 insertions(+), 726 deletions(-) delete mode 100644 1-js/9-prototypes/7-class-extend/article.md create mode 100644 1-js/9-prototypes/7-oop-errors/1-format-error/solution.md create mode 100644 1-js/9-prototypes/7-oop-errors/1-format-error/task.md create mode 100644 1-js/9-prototypes/7-oop-errors/article.md delete mode 100644 1-js/9-prototypes/8-why-prototypes-better/article.md diff --git a/1-js/2-first-steps/13-types-conversion/article.md b/1-js/2-first-steps/13-types-conversion/article.md index 8526f07d..f22b20e1 100644 --- a/1-js/2-first-steps/13-types-conversion/article.md +++ b/1-js/2-first-steps/13-types-conversion/article.md @@ -51,7 +51,7 @@ var a = +"123"; // 123 var a = Number("123"); // 123, тот же эффект ``` - +
diff --git a/1-js/2-first-steps/4-variables/variable.svg b/1-js/2-first-steps/4-variables/variable.svg index 041ceea9..f2ac40a4 100644 --- a/1-js/2-first-steps/4-variables/variable.svg +++ b/1-js/2-first-steps/4-variables/variable.svg @@ -1,28 +1,24 @@ - + - Diagrams + variable Created with Sketch. - - - - - - - - - - - "Hello!" - - - - - - - - message + + + + + + + + + "Hello!" + + + + + + Message diff --git a/1-js/2-first-steps/9-bitwise-operators/3-bitwise-symmetry/solution.md b/1-js/2-first-steps/9-bitwise-operators/3-bitwise-symmetry/solution.md index abc0d296..737c32cc 100644 --- a/1-js/2-first-steps/9-bitwise-operators/3-bitwise-symmetry/solution.md +++ b/1-js/2-first-steps/9-bitwise-operators/3-bitwise-symmetry/solution.md @@ -3,7 +3,7 @@ Посмотрим, можно ли поменять местами биты слева и справа. Например, таблица истинности для `^`: -
ЗначениеПреобразуется в...
`undefined``NaN`
`null``0`
+
diff --git a/1-js/4-data-structures/9-array-iteration/article.md b/1-js/4-data-structures/9-array-iteration/article.md index bf02ed2b..37392b0c 100644 --- a/1-js/4-data-structures/9-array-iteration/article.md +++ b/1-js/4-data-structures/9-array-iteration/article.md @@ -149,7 +149,7 @@ alert(result); // 15 В виде таблицы где каждая строка -- вызов функции на очередном элементе массива: -
`a` `b`
+
diff --git a/1-js/9-prototypes/5-class-inheritance/article.md b/1-js/9-prototypes/5-class-inheritance/article.md index c4d13e09..3cf4d4d1 100644 --- a/1-js/9-prototypes/5-class-inheritance/article.md +++ b/1-js/9-prototypes/5-class-inheritance/article.md @@ -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/). diff --git a/1-js/9-prototypes/6-instanceof/article.md b/1-js/9-prototypes/6-instanceof/article.md index dff06ccd..b8d048a8 100644 --- a/1-js/9-prototypes/6-instanceof/article.md +++ b/1-js/9-prototypes/6-instanceof/article.md @@ -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` должна бросать исключение в случаях, когда: - -
    -
  1. В JSON синтаксическая ошибка, то есть "падает" вызов `JSON.parse`.
  2. -
  3. В получившемся объекте нет свойства `name` или `age`.
  4. -
  5. Свойство `age` (возраст) -- не число.
  6. -
- -Для каждого из этих типов ошибок сделаем отдельный класс -- это поможет позже легко идентифицировать произошедшее: - -
    -
  1. `SyntaxError` -- ошибка "что-то не так в данных", встроенный класс, ошибка такого типа генерируется как раз `JSON.parse`.
  2. -
  3. `PropertyRequiredError` -- ошибка "нет свойства", будет наследовать от `SyntaxError`, так как является подвидом синтаксической ошибки.
  4. -
  5. `FormatError` -- "ошибка форматирования", тоже наследник `SyntaxError`.
  6. -
- -Вот ошибки в 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` мы получили удобную поддержку иерархии ошибок, с возможностью в любой момент добавить новые классы, понятным кодом и предсказуемым поведением. - ## Итого + +Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования. diff --git a/1-js/9-prototypes/7-class-extend/article.md b/1-js/9-prototypes/7-class-extend/article.md deleted file mode 100644 index 3483325e..00000000 --- a/1-js/9-prototypes/7-class-extend/article.md +++ /dev/null @@ -1,300 +0,0 @@ -# Фреймворк Class.extend - -Можно использовать прототипное наследование и не повторять `Rabbit.prototype.method = ...` при определении каждого метода, не иметь проблем с конструкторами и так далее. - -Для этого используют ООП-фреймворк -- библиотеку, в которой об этом подумали "за нас". - -В этой главе мы рассмотрим один из таких фреймворков: - - -Можно сказать, что фреймворк представляет собой "синтаксический сахар" к наследованию на классах. -[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)` делает следующее: - - - -Как видим, всё весьма просто. - -Но фреймворк этим не ограничивается и добавляет ряд других интересных возможностей. - -## Статические свойства - -У метода `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) -- класс, реализующий какое-либо чётко выделенное поведение, который не предназначен для порождения самостоятельно используемых объектов, а используется для *уточнения* поведения других классов. - -Иными словами, *примесь* позволяет легко добавить в существующий класс новые возможности, например: - - -**Как правило, примесь реализуется в виде объекта, свойства которого копируются в прототип.** - -Например, напишем примесь `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` существует синтаксис с первым аргументом-массивом:** - - -Если первый аргумент -- массив, то его элементы `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-функционал и т.п. - -## Итого - -
    -
  1. **Фреймворк имеет основной метод `Class.extend` с несколькими вариациями:** -
      -
    • `Class.extend(props)` -- просто класс с прототипом `props`.
    • -
    • `Class.extend(props, staticProps)` -- класс с прототипом `props` и статическими свойствами `staticProps`.
    • -
    • `Class.extend(mixins, props [, staticProps])` -- если первый аргумент массив, то он интерпретируется как примеси. Их свойства копируются в прототип перед `props`.
    • -
    -
  2. -
  3. **У созданных этим методом классов также есть `extend` для продолжения наследования.**
  4. -
  5. **Методы родителя можно вызвать при помощи `this._super(...)`.**
  6. -
- -Плюсы и минусы: -[compare] -+Такой фреймворк удобен потому, что класс можно задать одним вызовом `Class.extend`, с читаемым синтаксисом, удобным наследованием и вызовом родительских методов. --Редакторы и IDE, как правило, не понимают такой синтаксис, а значит, не предоставляют автодополнение. При этом они обычно понимают объявление методов через явную запись в объект или в прототип. --Есть некоторые дополнительные расходы, связанные с реализацией `_super`. Если они критичны, то их можно избежать.[/compare] - -То, как работает фреймворк, подробно описано в комментариях: [class-extend.js](http://js.cx/libs/class-extend.js). -[head] - -[/head] \ No newline at end of file diff --git a/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md b/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md new file mode 100644 index 00000000..75e08b3e --- /dev/null +++ b/1-js/9-prototypes/7-oop-errors/1-format-error/solution.md @@ -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 +``` \ No newline at end of file diff --git a/1-js/9-prototypes/7-oop-errors/1-format-error/task.md b/1-js/9-prototypes/7-oop-errors/1-format-error/task.md new file mode 100644 index 00000000..26201691 --- /dev/null +++ b/1-js/9-prototypes/7-oop-errors/1-format-error/task.md @@ -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 +``` diff --git a/1-js/9-prototypes/7-oop-errors/article.md b/1-js/9-prototypes/7-oop-errors/article.md new file mode 100644 index 00000000..e736fc44 --- /dev/null +++ b/1-js/9-prototypes/7-oop-errors/article.md @@ -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); +``` + +Посмотрим внимательнее на код -- в нём важные детали того, как можно позаботиться о стандартных свойствах объекта ошибки: + +
+
`name` -- имя ошибки.
+
Должно совпадать с именем функции, просто записали строку в него.
+
`message` -- сообщение об ошибке.
+
Несмотря на то, что `PropertyError` наследует от `Error` (последняя строка), конструктор у неё немного другой. Он принимает не сообщение об ошибке, а название свойства `property`, ну а сообщение генерируется из него. + +В результате в ошибке есть как стандартное свойство `message`, так и более точное `property`. + +Это полезная практика -- добавлять в объект ошибки свойства, более подробно описывающие ситуацию, которых нет в базовых объектах `Error`.
+
`stack` -- стек вызовов, которые в итоге привели к ошибке.
+
У встроенных объектов `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. +
+
+ +[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`. + +## Итого + + + +Чтобы создавать наследники от `Error` было проще, можно создать класс `CustomError`, записать в него универсальный код, наподобие `PropertyError` и далее наследовать уже от него. diff --git a/1-js/9-prototypes/8-why-prototypes-better/article.md b/1-js/9-prototypes/8-why-prototypes-better/article.md deleted file mode 100644 index 1bb6e93a..00000000 --- a/1-js/9-prototypes/8-why-prototypes-better/article.md +++ /dev/null @@ -1,266 +0,0 @@ -# Сравнение с функциональным наследованием - -В этой главе мы озаботимся тем, чтобы сравнить прототипный и функциональный подход к ООП. - -Причём, сделать это грамотно, с учётом того, что реально происходит "под капотом" интерпретатора. - -Нас интересуют три показателя: -
    -
  1. Эффективность по памяти.
  2. -
  3. Скорость работы.
  4. -
  5. Архитектурные ограничения (если есть) и плюшки.
  6. -
- -[cut] - -## Класс Machine - -Объявим класс `Machine` двумя способами: -
-
Функциональное объявление:
-
Состоит из единственной функции-конструктора, которая записывает в объект всё, что нужно. Приватные данные сохраняются в локальные переменные и доступны через замыкание: - -```js -function MachineOne(power) { - var enabled = false; - - this.enable = function() { enabled = true; }; - this.disable = function() { enabled = false; }; - - // ... -} -``` - -При этом у каждого объекта будет своя копия методов `enable` и `disable`, которые создаются каждый раз заново. -
-
Прототипное объявление:
-
В объекте хранится только то, что ему нужно. Методы записываются в прототип: - -```js -function MachineTwo(power) { - this._enabled = false; -} - -MachineTwo.prototype.enable = function() { - this._enabled = true; -}; - -MachineTwo.prototype.disable = function() { - this._enabled = false; -}; -``` - -
-
- -## Сравнение памяти - -Оценим затраты памяти в функциональном стиле: - -```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 переименовывают их, делая короче и таким образом уменьшая размер кода. - -**Это означает, что код, написанный в функциональном стиле, сожмётся лучше.** - -## Итого - -Получилось, что функциональный паттерн в сочетании с наследованием обладает рядом серьёзных проблем. - -Его, по сути, основное достоинство -- это использование локальных функций и переменных, в которые никак нельзя залезть снаружи, и которые дают лучшее сжатие кода минификаторами. - -Кроме того, если программировать без фреймворков, то функциональный стиль -- наиболее нагляден и прост. - -Но если в проекте нужен единообразный стиль ООП, то лучше использовать прототипный подход, возможно прибавив "сахарку" в виде ООП-фреймворка (тысячи их), а функциональный использовать в тех случаях, когда *уже есть* сторонняя библиотека или конструкторы в этом стиле, которые нужно расширить. diff --git a/2-ui/1-document/10-compare-document-position/article.md b/2-ui/1-document/10-compare-document-position/article.md index e52dbd5a..22f23a06 100644 --- a/2-ui/1-document/10-compare-document-position/article.md +++ b/2-ui/1-document/10-compare-document-position/article.md @@ -194,7 +194,7 @@ function compareDocumentPosition(a, b) { ``` Список битовых масок для проверки: -
+
diff --git a/2-ui/1-document/7-attributes-and-custom-properties/article.md b/2-ui/1-document/7-attributes-and-custom-properties/article.md index 85747058..c4be92c5 100644 --- a/2-ui/1-document/7-attributes-and-custom-properties/article.md +++ b/2-ui/1-document/7-attributes-and-custom-properties/article.md @@ -422,7 +422,7 @@ alert( document.body.getAttribute('AbBa') ); // что должен вернут Таблица сравнений для атрибутов и свойств: -
Биты Число
+
diff --git a/2-ui/3-event-details/8-keyboard-events/article.md b/2-ui/3-event-details/8-keyboard-events/article.md index 9802e62d..03b290bd 100644 --- a/2-ui/3-event-details/8-keyboard-events/article.md +++ b/2-ui/3-event-details/8-keyboard-events/article.md @@ -229,7 +229,7 @@ document.getElementById('my').onkeypress = function(e) { Перечислим их в таблице, обращая основное внимание на особенности работы с ними. -
Свойства Атрибуты
+
diff --git a/2-ui/4-forms-controls/3-events-change/article.md b/2-ui/4-forms-controls/3-events-change/article.md index bc671cb4..7b181559 100644 --- a/2-ui/4-forms-controls/3-events-change/article.md +++ b/2-ui/4-forms-controls/3-events-change/article.md @@ -175,7 +175,7 @@ function showCount() { События изменения данных: -
Категория Примеры
+
diff --git a/3-more/1-webcomponents/2-webcomponent-core/article.md b/3-more/1-webcomponents/2-webcomponent-core/article.md index 1c1bd54f..426b444a 100644 --- a/3-more/1-webcomponents/2-webcomponent-core/article.md +++ b/3-more/1-webcomponents/2-webcomponent-core/article.md @@ -169,7 +169,7 @@ var timer = document.createElement("button", "my-timer"); Следующие методы автоматически вызываются во время жизненного цикла элемента: -
Событие Описание
+
diff --git a/3-more/10-ajax/16-ajax-summary/article.md b/3-more/10-ajax/16-ajax-summary/article.md index d1cade8b..f3415c10 100644 --- a/3-more/10-ajax/16-ajax-summary/article.md +++ b/3-more/10-ajax/16-ajax-summary/article.md @@ -25,7 +25,7 @@ Они были детально рассмотрены в предыдущих главах раздела. -
`createdCallback`Элемент создан
`attachedCallback`Элемент добавлен в документ
`detachedCallback`Элемент удалён из документа
+
diff --git a/3-more/2-animation/2-bezier/article.md b/3-more/2-animation/2-bezier/article.md index 6e7497fb..9696fd1b 100644 --- a/3-more/2-animation/2-bezier/article.md +++ b/3-more/2-animation/2-bezier/article.md @@ -112,7 +112,7 @@ if (!document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Basi
  • На каждом из этих отрезков берётся точка, находящаяся от начала на расстоянии от 0 до `t` пропорционально длине. То есть, при `t=0` -- точка будет в начале, при `t=0.25` -- на расстоянии в 25% от начала отрезка, при `t=0.5` -- 50%(на середине), при `t=1` -- в конце. Так как **чёрных** отрезков -- два, то и точек выходит две штуки.
  • Эти точки соединяются. На рисунке ниже соединяющий их отрезок изображён синим. -
  • `XMLHttpRequest`
    +
    diff --git a/3-more/2-animation/3-css-animation/article.md b/3-more/2-animation/3-css-animation/article.md index 2dd69883..91b041f0 100644 --- a/3-more/2-animation/3-css-animation/article.md +++ b/3-more/2-animation/3-css-animation/article.md @@ -166,7 +166,7 @@ CSS-анимации особенно рекомендуются на мобил Остальные кривые являются короткой записью следующих `cubic-bezier`. В таблице ниже показано соответствие: -
    При `t=0.25`При `t=0.5`
    +
    diff --git a/3-more/5-compress/3-gcc-advanced-optimization/article.md b/3-more/5-compress/3-gcc-advanced-optimization/article.md index 8e7bf6ca..fbcdb00c 100644 --- a/3-more/5-compress/3-gcc-advanced-optimization/article.md +++ b/3-more/5-compress/3-gcc-advanced-optimization/article.md @@ -370,10 +370,10 @@ SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler Но, в остальном, это совершенно разные вещи. -
    `ease` `ease-in`
    +
    - - + + diff --git a/3-more/6-extra/11-intl/article.md b/3-more/6-extra/11-intl/article.md index 4e739b7f..3b4cb175 100644 --- a/3-more/6-extra/11-intl/article.md +++ b/3-more/6-extra/11-intl/article.md @@ -147,7 +147,7 @@ var formatter = new Intl.DateFormatter([locales, [options] ]) Первый аргумент -- такой же, как и в `Collator`, а в объекте `options` мы можем определить, какие именно части даты показывать (часы, месяц, год...) и в каком формате. Полный список свойств `options`: -
    ЭкстернЭкспортЭкстернЭкспорт
    +
    @@ -321,7 +321,7 @@ formatter.format(number); // форматирование Список опций: -
    Свойство Описание
    +
    Свойство Описание