среда, 14 мая 2008 г.

Программная обработка исключительных ситуаций в JavaScript

Зачем JS-приграммисту может понадобиться обрабатывать ошибки в сценариях? В принципе, не существует задач, при решении которых без механизма исключений никак нельзя было бы обойтись. Однако часто их использование оказывается заметно удобнее бесконечных if`ов - поскольку в коде мы с их помощью изолируем себя от некоторых ситуаций, в которых программа должна вести себя по-другому (главным образом, это может быть связано с неадекватной работой пользователя, например, при вводе данных) - и сосредотачиваемся на описании алгоритма в случае нужных нам условий, а уже потом отдельно разбираться, как говорится, с косяками. Это как в жизни - мы часто отодвигаем проблемы на потом, просто помним, что они у нас есть, и когда будет время - мы их решим, в противовес if`овому подходу, когда мы всё время должны решать проблемы реакции на неадекватные зигзаги сценария не отходя от кассы. По себе могу сказать, что писать сложный код так намного удобней - и более того, по мере усложнения кода использование исключений становится всё более и более необходимым. А если забыть обработать какое-то исключение, заявленное при написании "чистого" кода - браузер об этом скорее всего напомнит при тестировании.

Для тех, кто не знаком с механизмом исключений в Java


Механизм исключений в JavaScript очень похож на механизм исключений в Java, так что если вы знакомы с последним, можете спокойно пропустить этот раздел и перейти к чтению следующего - в этом рассматриваются азы, общие для этих двух механизмов.

Исключение - это некий объект, который характеризует состояние системы в момент, когда произошла какая-то исключительная ситуация, т.е. в скрипте либо возникла ошибка, в силу чего стало невозможным его дальнейшее выполнение, либо в скрипте программист явно указал возбудить исключение при некоторых условиях при помощи соответствующей конструкции throw. По сравнению со старыми языками, где в этой ситуации интерпретатор или скомпилированный код выдавали ошибки, современные языки сделали шаг вперёд, позволив программам самим реагироваать на исключения в себе - это повысило надёжность ПО и увеличило качество кода при написании сложных программ.

По сути, на уровне кода обработка исключений очень похожа на обработку событий - если сопоставить объекту Error объект event, то всё довольно быстро встанет на свои места - если код вызывает ошибку, происходит специфическое событие - исключение - его параметры записываются в специальный объект и он передаётся обработчику, который его обрабатывает.


Для тех, кто знаком с механизмом исключений в Java


Здесь я кратко приведу различия между реализацией исключений в Java и JavaScript - пропустите этот раздел если с Java вы не знакомы.

Главное отличие от Java-реализации состоит в отсутствии (в силу не классовой, а прототипной реализации ООП) возможности использовать множественный оператор catch, основанный на наследовании и восходящем преобразовании. Здесь оператор catch может быть только один для каждого try`я. Этот приём можно менее изящно реализовать в обработчике на основе оператора switch.

Блок finally присутствует, но здесь он нужен ещё реже. Сам я, работая с JavaScript, ни разу не сталкивался с ситуацией, где бы он мне понадобился или был удобен - поэтому здесь я о нём тихонько умолчу, только намекнув, что он, в принципе, здесь есть...

Ну и, конечно, нет великолепной возможности (отсутствующей даже в C#) Java, обязывающей указывать возможность вызова исключения функцией при её описании с помощью ключевого слова throws, отсутствие какового сулит обернуться большой путаницей при использовании больших сторонних библиотек...

Объект исключения


Обычно в качестве объекта исключения выступает объект Error, хотя, строго говоря, конструкция throw может передавать абсолютно любой объект (!), который может быть принят оператором catch и соответственно обработан.

К сожалению, и область обработки исключительных ситуаций задела браузерная война - боевые действия в этой области привели к тому, что объекты Error в ECMAScript и в JScript практически не имеют ничего общего.

Какую информацию о состоянии может в себе нести исключение? По большому счёту всего 2 поля - некое название, характеризующие тип исключения, и сообщение, уточняющее особенности конкретного исключения в данном случае - если пользователь возбуждает исключение сам, то он придумывает сообщение сам, если его генерирует система - то уже она решает, какое сообщение писать. Прибавим к этим 2-м свойствам 3-е - prototype, для возможности изменять его характеристики ОО-средствами и 4-е стандартное toString() - и получим объект исключения от ECMAScript, в котором название называется "name" (в случае пользовательских объектов оно содержит строку "Error", а в случае ошибки браузера - название из небольшого списка, представленного в следующей таблице), а строка сообщения - свойство "message". Конструктор исключения от ECMAScript выглядит так:
new Error(message);
У Microsoft вместо имени у ошибки появляется составной номер (свойство "number"), состоящий из кода источника ошибки (facility code) и номера самой ошибки. Что бы выделить номер источника, можно воспользоваться выражением
(e.number >> 16) & 0x1FFF
, а что бы выделить код самой ошибки - выражением
e.number & 0xFFFF
, где e - объект Error), и "description", полностью аналогичное свойству "message" в ECMAScript (начиная с IE 5.5 значение свойства "description" доступно и через свойство "message", т.е. по существу, они - синонимы, ссылающиеся на одно и то же значение). Соответствие номеров описаниям для JScript можно найти в этой таблице.
Конструктор объекта исключения от Microsoft IE выглядит так:
new Error(errorNumber, message);
- этот формат не совместим с приведённым ранее форматом, соответственно, к сожалению, для различных браузеров придётся писать разные возбуждения исключений...

Впрочем, популярный особенно в Linux-среде open-source`ный браузер Mozilla так же нередко расширяет стандарты (хотя существующие - поддерживает, в отличае от IE). В объекте Error для этого браузера добавлено 2 интересных свойства для локализации исключения - "fileName" и "lineNumber", хранящие, соответственно, информацию о файле, где произошла ошибка (актуально, когда в проекте много *.js - файлов и непонятно в котором из них ошибка) и о номере строки в этом файле.

Пример использования


Теперь давайте перейдём к практике. Для отлова исключений используется блок try..catch, выглядит это примерно так:
try {
    оператор1
} catch (исключение) {
    оператор2
}
, где: оператор1 - оператор или их группа, где может быть вызвано исключение, оператор2 - обработчик исключения, исключение - обычно объект Error, характеризующий исключение и передаваемый оператору2.

Отметим, что оператор1 может включать вызовы функций - если в них возникнут исключения и там не окажется более глубоко вложенных блоков try..catch, внутри которых оно будет вызвано, то их поймает именно этот catch.

Теперь о самостоятельном вызове с помощью оператора throw. Как и в других темах, существует 2 способа писать универсальный код:
  • Различать браузеры и писать разный код для разных браузеров, и
  • Писать более простой код, который относится к области пересечения браузеров.
Я нахожу более мудрым второй способ и по возможности стараюсь использовать его. В данном случае это означает, что лучше отказаться от объекта Error из-за разного синтаксиса конструкторов и присваивать исключению простую строку:
function getMonthName(month) {
    month--; // Переводим month в индекс массива (1=январь, 12=декабрь)
    var months=["январь","февраль","март","апрель","май","июнь","июль",
"август","сентябрь","октябрь","ноябрь","декабрь"];
    if (months[month] != null)
        return months[month];
    else
        throw "Неверный месяц";
}

try {
    monthName = getMonthName(myMonth); // возможно исключение
} catch (e) {
    monthName="неизвестно";
}
document.write(monthName);
Ссылки по теме для более глубокого изучения:
1) Ю.С. Лукач, "Справочник Web-разработчика", раздел "Обработка исключений";
2) Степанищев Евгений: Блоки try... catch... finally... в JScript 5. Статья на сайте CitForum.ru

P.S. Это одна из ранних моих статей, опубликованных на форуме программистов "Vingrad". Ознакомиться с комментариями Vingrad`цев можно тут.