Мелкие хитрости в отладке кода с помощью alert. Часть 1

October 31, 2007

Я полагаю - не секрет, что каждый программист тратит значительную долю своего времени не на написание кода, а на отладку, попытку найти ошибки, и понять “почему же эта штука работает не так, как я планировал”. Следовательно, нам просто необходимы качественные и удобные средства для отладки, профилирования кода – все то, что позволяет понять нам “что же там происходит на самом деле внутри?”. Рядом находятся инструменты, служащие для управления базой багов, ведения журналов пожеланий, исправлений и заметок к очередному билду - средства позволяющие организовать прочную связь “постановщик задач - программист - тестировщик”. Сегодня я расскажу об отладке javascript-кода.

Самым первым средством для отладки, с ним знаком каждый начинающий свой путь в веб-разработке, - это alert. Функция alert получает в качестве параметра некоторую строку текста и выводит ее на экран с помощью диалогового окна сообщения (см. рис. 1).
  1. <script>
  2.   var x = 10;
  3.   var y =  x / 3;
  4.   alert (y); // чему же равна переменная y?
  5. </script>


Чем плох такой подход? Да почти, всем. Во-первых, с помощью alert вы может выводить на экран только простые типы данных: числа, строки. Если же вас интересует внутреннее устройство некоторого объекта, то приходится писать код, который перебирает все свойства объекта и выводит их по очереди. И хорошо, если это простенький объект, например, такой:
  1. var obj = {fio: "Bill", age: 21, sex: "male"}; 
  2.   alert (obj); // а как насчет вывода содержимого сложного объекта?
Функция alert сообщит нам, что переменная obj представляет собой объект, но ни слова о том какие поля (fio, age, sex) и чему они равны (см. рис. 2).



Давайте напишем функцию которая получает на вход объект, перебирает в цикле его свойства и формирует строку для последующего вывода ее alert-ом. В основе этой функции лежит цикл “for (i in obj)” – его назначение перебрать все свойства некоторого объекта, имена этих свойств будут помещаться в переменную I, затем же используя обращение obj[i] мы узнаем и само значение свойства. Чтобы меня не сразу “запинали” за приведенный код, я решил добавить проверку на рекурсию, так если объект содержит внутри себя ссылку прямым или косвенным образом на самого себя, то очевидно, что необходимо такую ситуацию обработать как-то по-особенному – если мы не остановимся и будем всякий встретившийся объект печатать, то попадем в бесконечный, не цикл, но бесконечную рекурсию вызовов функции _objDump. Для этого я ввел второй параметр для _objDump – это массив пройденных уже элементов, при первом вызове из функции objDump я кладу в этот массив ссылку на собственно сам распечатываемый элемент. Третий параметр – level – нужен для “красоты”. Перед выводом имен полей и значений объектов я печатаю некоторое количество пробелов, так чтобы в случае сложной иерархии вложенностей объектов друг в друга вывод был бы в виде “лесенки” (см. рис. 3).


  1. // назначение этой функции просто вызвать функцию _objDump,
  2. // которая собственно и содержит цикл перебирающий свойства объекта obj
  3. function objDump (obj, except) { 
  4.  return  _objDump (obj, [obj], 0);
  5. }
  6.  
  7. function inArray (arr, elt){
  8.  for (var i = 0; i < arr.length; i++)
  9.    if (elt == arr[i]) return i;
  10.  return false;
  11. }
  12. function _objDump (obj, used, level) { 
  13. var rez = '';
  14. var spaces = "";
  15. for (var i = 0; i < level; i++)
  16.   spaces += "  ";
  17.   rez += spaces+('---Dumping object: ' + obj.toString () + "\n"); 
  18.   for (var i in obj) { 
  19.     if (used && inArray(used ,obj[i]) !== false) {
  20.       rez += spaces+"->"+"!Self Reference"+"\n";
  21.       continue; 
  22.     }
  23.     rez += (spaces+"->" + i + ': '); 
  24.     if ( (typeof obj[i]) == "object"){
  25.      try{
  26.       rez += _objDump (obj[i], used.slice(0).push(obj[i]), 1 +level) +"\n";
  27.      }catch (eee) {rez += spaces+"->"+"!Error"+"\n";}
  28.     }
  29.     else
  30.       rez +=(spaces+"->"+obj[i] + "\n"); 
  31.   } 
  32.   return  rez; 
  33. }
  34.  
  35. // ------------ а теперь пример использования функций --------
  36. var obj = {fio: "Bill", age: 21, sex: "male", complex : {classrom: 314, teacher: "Vasyano"} }; 
  37. obj.gravity = 1.0;
  38. obj.self = obj;// а вот теперь хитрость, наш объект ссылается на самого себя - 
  39. // вполне себе привычная ситуация для сложных структур данных
  40. alert (objDump(obj)); // а как насчет вывода содержимого сложного объекта?
По правде говоря, достичь той же самой цели – вывода содержимого объекта в удобочитаемом виде - можно и меньшими затратами. В javascript для массивов существует метод toSource, который формирует строку в формате “похожем на json” и, что особенно приятно, корректно обрабатывает сложные иерархии объектов и рекурсию.
  1. var obj = {fio: "Bill", age: 21, sex: "male", complex : {classrom: 314, teacher: "Vasyano"} }; 
  2. obj.gravity = 1.0;
  3. obj.self = obj;// а вот теперь хитрость, наш объект ссылается на самого себя - 
  4. // вполне себе привычная ситуация для сложных структур данных
  5. alert (obj.toSource ());
Заметьте на рис. 4, что значением свойства self является #1, и это же имя встречается в самом начале выводимой строки. Хотя и первый вариант кода имеет право на существование.



Маленький трюк: если содержимое объекта очень, очень, и еще раз, очень большое, то вывод с помощью alert-а приведет к тому, что окно сообщения станет огромным, обрежется по нижнему краю, например, так (см. рис. 5). Если же открыть страницу в opera, то окно сообщения будет позволять выделять текст сообщения, следовательно, вы его копируете в буфер обмена, открываете блокнот, вставляете и пытаетесь понять, что означает этот десяток килобайт “букав” (см. рис. 6).





Какие еще “фишки” можно добавить для alert? Если код достаточно велик и alert-ы срабатывают многократно, то неплохо было бы создать какой-то журнал, в котором всегда можно было бы глянуть чему равна была та или иная переменная. Плюс работа с всплывающим окном крайне не удобна при отладке скриптов работающих с обработкой событий мыши. Например, делая классическое выпадающее меню вы столкнетесь с тем, что появление диалогового окна alert-а приводит к генерации лишних событий “onmouseout” и “onmouseover”. Лучше всего, чтобы при запуске скрипта было создано всплывающее окно, в котором бы и накапливался весь текст выводимый alert-ами, например, так:
  1. var extw = null;// ссылка на окно для вывода значений переменных
  2. // функция записывает в окно, дату и значение переменной – переданной как параметр
  3. function alert2 (what){
  4.  if (extw == null)
  5.   extw = window.open ();
  6.  extw.document.write ('<b>'+new Date()+'</b> => <pre>'+what+'</pre><br />');
  7. }
  8.  
  9. var obj = {fio: "Bill", age: 21, sex: "male", complex : {classrom: 314, teacher: "Vasyano"} }; 
  10. obj.gravity = 1.0;
  11. obj.self = obj;// а вот теперь хитрость, наш объект ссылается на самого себя 
  12. // - вполне себе привычная ситуация для сложных структур данных
  13.  
  14. alert2 (objDump(obj));// используем новую и улучшенную версию alert-а
  15. alert ('старый добрый alert'); // используем классический alert
  16. alert2 ('и еще немножко данных'); // используем новую и улучшенную версию alert-а
Результат работы скрипта показан на рис. 7.



В следующий раз я расскажу о более “продвинутых” средствах отладки, специфических для конкретных браузеров.