Случилось так, что в последнее время мне пришлось читать и рефакторить очень много ужасного javascript-кода. Работа с таким кодом стоит очень многих нервов при сопровождении, да и писать/отлаживать такой код не приятно. Мысли о том, что заставляет людей писать плохой код и как с этим можно бороться заставили меня писать эту статью. Не претендую на сколь-нибудь полное раскрытие темы борьбы за качество кода, хочу рассмотреть лишь некоторые аспекты, доставляющие наибольшее количество проблем. В качестве основного инструмента оптимизации качества кода предлагаю использовать JSLint, который несмотря на все плюсы, не является панацеей и может служить лишь отправной точкой для дальнейшего улучшения кода.
Всех у кого хоть раз болела голова при написании/чтении javascript кода прошу под кат.
Стандарт на службе у человека или наоборот?
JavaScript расхолаживает. Этот язык спроектирован так, чтобы быть максимально простым и ненавязчивым, чтобы человек не знающих многих премудростей, мог легко начать писать на javascript и решать при этом реальные задачи. Обилие javascript-фреймворков, о которых разговор пойдет позднее, только ухудшает ситуацию. Итак, javascript — язык грязный, допускающий вольности со стороны разработчика. Таким образом, если просто писать как получается, то написать множно сколь угодно плохо — язык не запрещает. Естественно, для сложных проектов такой подход недопустим.
Чтобы сделать язык строже, существуют стандарты кодирования. В своей практике я получаю наибольшую отдачу, используя инструмент валидации кода — JSLint. Если пройти по ссылке, можно прочитать предупреждение автора, Дугласа Крокфорда, о том что JSLint will hurt your feelings. И это действительно так — меня очень сильно задело, когда валидатор нашел полсотни ошибок в 50 строках работающего кода. Поначалу казалось дикостью, что всем этим ненужным условностям надо следовать, что надо ломать пальцы переучиваясть ставить пробелы и ненужные скобки. Писать вместо
if(element.parentNode&&Math.max(offset1,offset2,offset3)>x1){ arr = new Array(); for(var i in some_object){ if (i == qwerty) continue; arr.push(i); } }
как-то так:
/*global qwerty: true, some_object: false */ if (element.parentNode && Math.max(offset1, offset2, offset3) > x1) { var arr = [], i; for (i in some_object) { if (some_object.hasOwnProperty(i)) { if (i === qwerty) { continue; } arr.push(i); } } }
Потом на глаза попалась библиотека jQuery, где весь исходный код валиден (ну почти), а также много другого хорошего и умного кода. Я решил следовать этому стандарту в качестве эксперимента. В результате получил сильно возросшее качество создаваемого кода, которое выражается в меньшем количестве ошибок при отладке, лучшей читабельности, очевидности кода. При чтении кода сразу видны становятся проблемные места, ошибки в проектировании, потенциальные уязвимости и неоднозначности. Также есть полезный побочный эффект использования JSLint — в ответ на предупреждение валидатора приходится иногда задумываться в таких местах на которые просто не обращаешь внимания обычно, как правило это приводит к исправлению ошибки до её фактического проявления, то есть облегчает отладку, а иногда и помогает улучшить архитектуру разрабатываемого приложения. В общем, мир программирования стал бы гораздо чище и лучше, если бы валидация кода с помощью JSLint стала обычной практикой при разработке.
JSLint. Основные требования к коду
Привожу далее вольную интерпретацию текста Дугласа Крокфорда касательно ограничений, накладываемых на javascript. Лучше читать, оригинал, конечно.
Глобальные и локальные переменные
Глобальные переменные по праву можно назвать величайшим злом не только в javascript, но и во всех языках программирования. Думаю, не надо объяснять, почему (если надо то вот). В javascript глобальные переменные можно объявить в локальном контексте, что только усугубляет ситуацию, потому что иногда можно объявить глобальную переменную по ошибке, просто поторопившись, и, если повезет с названием переменной, то разработчик который будет сопровождать ваш код сойдет с ума, пытаясь понять смысл, который попытался вложить в глобальную переменную автор.
Ничего такого не произошло бы, если бы использовался JSLint, который запрещает объявлять переменные без использования ключевого слова var. А если переменная действительно глобальная, JSLint заставит вас указать это явно, посредством комментария /*global variablename */, причем конкретизируя, допускается ли в данном контексте присвоение переменной значения.
В строгом режиме JSLint приучает нас использовать var не более одного раза для каждого локального пространства имен. Это конечно перегиб, но я бы лучше воспринял это как призыв к рефакторингу — ведь переменную лучше всего объявлять непосредственно перед использованием, а значит группы объявлений должны быть разбросаны по методу, локализуя места использований, таким образом, локальные области большого метода лучше будет оформить как новые функции с названиями, определяющими суть действия, задокументировав таким образом код, не используя комментарий, одновременно улучшив валидность по JSLint.
Точка с запятой
Больной вопрос для многих программистов, в особенности Ruby — не любят точки с запятой ставить и всё тут. При написании кода на javascript надо зарубить на носу раз и навсегда — точки с запятой ставятся после каждой инструкции. Исключения: for, function, if, switch, try и while. Обратите внимание, что не надо ставить точку с запятой при объявлении функции:
function f(x) { }
однако, необходимо ставить ее при присвоении переменной значения:
var f = function (x) { };
Перенос длинной строки
Строка более 80 символов — плохо. Забудьте про widescreen-мониторы, дело не в скроллинге, строка кода должна охватываться одним взглядом. Но и строки разбивать надо особо — чтобы после в месте разрыва строки явно отслеживалась синтаксическая незавершенность. JSLint допускает разрыв строки только после следующих операторов:
, . ; : { } ( [ = < > ? ! + - * / % ~ ^ | & == != <= >= += -= *= /= %= ^= |= &= << >> || && === !== <<= >>= >>> >>>=
JSLint запрещает перевод длинной инструкции после строки, числа, идентификатора, закрывающей скобки или суффикс-операторов: ) ] ++ —
Локальные пространства имен
В javascript локальное пространство имен порождает только одна инструкция — функция. Ни for, ни while, ни if не порождают локальных пространств имен. То есть, в следующем примере:
if (x === y) { var z = 1; } alert(z); // 1
Переменная определенная внутри блока if доступна и вне блока, в то время как
(function () { var x = 1; })(); alert(x); // error (undefined variable x);
переменная объявленная в функции не будет доступна вне ее, однако, если переменная объявлена вне функции, а функция объявляется в одном контексте с переменной, то переменная будет также видна в функции, другими словами:
var x = 1; function f() { alert(x); } f(); // 1
Касательно понимания механизмов работы замыканий обычно возникают трудности в примерах типа такого:
for (x = 1; x < 10; x += 1) { setTimeout(function () { console.log(x); }, 10); }
Этот пример при первом запуске породит вывод
10, 10, 10, 10, 10, 10, 10, 10, 10, 10
а при втором
19, 10, 10, 10, 10, 10, 10, 10, 10, 10
при третьем
28, 10, 10, 10, 10, 10, 10, 10, 10, 10
вместо ожидаемого
1, 2, 3, 4, 5, 6, 7, 8, 9
который может получиться только при помощи правильного использования механизма видимости имен:
var f = function (x) { setTimeout(function () { console.log(x); }, 0); }; for (x = 1; x < 10; x += 1) { f(x); }
Обязательные блоки
Строго обязательно использовать блок (оформляется фигурными скобками) при использовании if, for и других инструкций.
if (condition) statement; // надо заменить на if (condition) { statement; }
И первый и второй код работают одинаково, однако первого на практике следует избегать.
Конструкции языка
for in
Такого рода циклы надо использовать с осторожностью, поскольку проходя по всем членам объекта или массива, этот цикл также пройдет и по унаследованным свойствам/методам прототипа. JSLint требует чтобы тело такого цикла в обязательном порядке было обернуто блоком if.
for (name in object) { if (object.hasOwnProperty(name)) { .... } }
switch
Обязательно использовать break после каждого case
void
Не надо использовать это. Лучше обычный undefined, во избежание недопонимания.
== и !=
Использовать с особой осторожностью. Необходимо помнить, что эти операторы приводят тип данных, то есть ' \t\r\n' == 0 это true. Лучше использовать === поскольку этот оператор сравнивает и соответствие типов, а приведение делать вручную, используя parseInt(str, radix) там где надо интерпретировать строку как число.
=
Оператор присваивания лучше не использовать в качестве условия в if:
if (x = y) { }
поскольку обычно это означает
if (x == y) { }
у читающего такой код обычно возникает недопонимание — может быть здесь ошибка? Если действительно надо использовать такую конструкцию, необходимо указать это явно, используя двойные скобки:
if ((x = y)) { }
Резюме по JSLint
Здесь приведен не полный перечень требований JSLint, а лишь те, на которые наиболее часто приходится обращать внимание. Думаю, заинтересовавшемуся человеку не составит труда сходить за полным перечнем на JSLint.com. Здесь же хотел бы добавить еще, что для многих редакторов существуют плагины, позволяющие валидировать код при помощи JSLInt по мере ввода. Лично пользовался плагинами для Eclipse и jEdit. Есть плагин под NetBeans. В крайнем случае, можно воспользоваться on-line валидатором.