This commit is contained in:
Ilya Kantor 2016-11-14 16:31:21 +03:00
parent 3defacc09d
commit f99574f53b
178 changed files with 530 additions and 271 deletions

View file

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

View file

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

View file

@ -0,0 +1,280 @@
# Свои ошибки, наследование от Error
Когда мы работаем с внешними данными, возможны самые разные ошибки.
Если приложение сложное, то ошибки естественным образом укладываются в иерархию, разобраться в которой помогает `instanceof`.
## Свой объект ошибки
Для примера создадим функцию `readUser(json)`, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера -- может, нашего, а может -- чужого, в общем -- желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные -- не важно, для наглядности поработаем с JSON.
Пример `json` на входе в функцию: `{ "name": "Вася", "age": 30 }`.
В процессе работы `readUser` возможны различные ошибки. Одна -- очевидно, `SyntaxError` -- если передан некорректный JSON.
Но могут быть и другие, например `PropertyError` -- эта ошибка будет возникать, если в прочитанном объекте нет свойства `name` или `age`.
Реализуем класс `PropertyError`:
```js
function PropertyError(property) {
Error.call(this, 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`) не назначает. Единственный возможный смысл его вызова -- он ставит специальное внутреннее свойство `[[ErrorData]]`, которое выводится в `toString` и позволяет увидеть, что это ошибка. Поэтому по стандарту вызывать конструктор `Error` при наследовании в таких случаях рекомендовано.
```
## 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; // неизвестная ошибка, не знаю что с ней делать
}
}
```
Всё работает -- и наша ошибка `PropertyError` и встроенная `SyntaxError` корректно генерируются, перехватываются, обрабатываются.
Обратим внимание на проверку типа ошибки в `try..catch`. Оператор `instanceof` проверяет класс с учётом наследования. Это значит, что если мы в дальнейшем решим создать новый тип ошибки, наследующий от `PropertyError`, то проверка `err instanceof PropertyError` для класса-наследника тоже будет работать. Код получился расширяемым, это очень важно.
## Дальнейшее наследование
`PropertyError` -- это просто общего вида ошибка в свойстве. Создадим ошибку `PropertyRequiredError`, которая означает, что свойства нет.
Это подвид `PropertyError`, так что унаследуем от неё. Общий вид конструктора-наследника -- стандартный:
```js
function PropertyRequiredError(property) {
// вызываем конструктор родителя и передаём текущие аргументы
PropertyError.apply(this, arguments);
...
}
```
Достаточно ли в наследнике просто вызвать конструктор родителя? Увы, нет.
Если так поступить, то свойство `this.name` будет некорректным, да и `Error.captureStackTrace` тоже получит неправильную функцию вторым параметром.
Можно ли как-то поправить конструктор родителя, чтобы от него было проще наследовать?
Для этого нужно убрать из него упоминания о конкретном классе `PropertyError`, чтобы сделать код универсальным. Частично -- это возможно. Как мы помним, существует свойство `constructor`, которое есть в `prototype` по умолчанию, и которое мы можем намеренно сохранить при наследовании.
Исправим родителя `PropertyError` для более удобного наследования от него:
```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` используем `constructor` чтобы получить именно конструктор для текущего объекта. В наследнике там будет `PropertyRequiredError`, как и задумано.
Мы убрали одну жёсткую привязку к `PropertyError`, но со второй (`this.name`), увы, сложности. Оно должно содержать имя ошибки, то есть, имя её функции-конструктора. Его можно получить через `this.name = this.constructor.name`, но в IE11- это работать не будет.
Если подерживать IE11-, то тут уж придётся в наследнике его записывать вручную.
Полный код для наследника:
```js
function PropertyRequiredError(property) {
PropertyError.apply(this, arguments);
this.name = 'PropertyRequiredError';
this.message = 'Отсутствует свойство ' + property;
}
PropertyRequiredError.prototype = Object.create(PropertyError.prototype);
PropertyRequiredError.prototype.constructor = PropertyRequiredError;
var err = new PropertyRequiredError("age");
// пройдёт проверку
alert( err instanceof PropertyError ); // true
```
Здесь заодно и `message` в наследнике было перезаписано на более точное. Если хочется избежать записи и перезаписи, то можно оформить его в виде геттера через `Object.defineProperty`.
## Итого
- Чтобы наследовать от ошибок `Error`, нужно самостоятельно позаботиться о `name`, `message` и `stack`.
- Благодаря тому, что `instanceof` поддерживает наследование, удобно организуются проверки на нужный тип. В иерархию ошибок можно в любой момент добавить новые классы, с понятным кодом и предсказуемым поведением.
Чтобы создавать наследники от `Error` было проще, можно создать класс `CustomError`, записать в него универсальный код, наподобие `PropertyError` и далее наследовать уже от него:
```js
*!*
// общего вида "наша" ошибка
*/!*
function CustomError(message) {
this.name = "CustomError";
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error()).stack;
}
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
*!*
// наследник
*/!*
function PropertyError(property) {
CustomError.call(this, "Отсутствует свойство " + property)
this.name = "PropertyError";
this.property = property;
}
PropertyError.prototype = Object.create(CustomError.prototype);
PropertyError.prototype.constructor = PropertyError;
*!*
// и ещё уровень
*/!*
function PropertyRequiredError(property) {
PropertyError.call(this, property);
this.name = 'PropertyRequiredError';
this.message = 'Отсутствует свойство ' + property;
}
PropertyRequiredError.prototype = Object.create(PropertyError.prototype);
PropertyRequiredError.prototype.constructor = PropertyRequiredError;
*!*
// использование
*/!*
var err = new PropertyRequiredError("age");
// пройдёт проверку
alert( err instanceof PropertyRequiredError ); // true
alert( err instanceof PropertyError ); // true
alert( err instanceof CustomError ); // true
alert( err instanceof Error ); // true
```