work
This commit is contained in:
parent
4c531b5ae7
commit
d4c714cbe1
261 changed files with 7370 additions and 546 deletions
280
1-js/9-object-inheritance/17-oop-errors/article.md
Normal file
280
1-js/9-object-inheritance/17-oop-errors/article.md
Normal 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
|
||||
```
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue