Как избегать распространенных ошибок в JavaScript

Время от времени все мы делаем ошибки. Это может произойти по многим причинам: недостаток опыта, плохое настроение, поджимают сроки сдачи, головная боль и т.д. Не смотря на то, что какие-то моменты в JavaScript'е могут казаться незначительными, они могут стать причиной серьезных проблем и ночной отладки кода в будущем. Поэтому вне зависимости от внешних факторов вы должны уделять внимание деталям во время программирования. Я хочу дать вам пару советов для улучшения всей этой ситуации.

Классическая ошибка с циклом "for ... in"

Прежде всего я хочу показать одну небольшую ошибку, которую совершают многие программисты:

var i;

for (i in products) {
    // делаем что-то полезное
}

Выглядит довольно просто и безопасно, верно? Нет! Сейчас я объясню суть проблемы:

1) в нашем примере `products` можеть быть как Array так и Object

2) стандартные свойства массивов и объектов недоступны при обходе в цикле for .. in. Под стандартными свойствами я имею в виду например свойство `length` массива и т.д.

Предлагаю добавить немного деталей в наш предыдущий пример, чтобы сделать его более наглядным:

var products = [
        {title: 'Symfony2 кружка', description: '+10 очков к настроению'},
        {title: 'Symfony2 футболка', description: '+100 очков к навыкам программирования'},
    ],
    i;

for (i in products) {
    console.log('property: ' + i);
    console.log('product:', products[i]);
    // делаем что-то полезное
}

Все работает, но что случится если кто-то расширит тип данных Array? Да, это плохая практика, но вы не можете гарантировать ее отсутствие. Некоторые люди используют расширение базовых типов данных для обратной совместимости с Internet Explorer. Например, кто-то добавит удобный метод к массиву:

Array.prototype.max = function () {
    return Math.max.apply(null, this);
};

Проблема с расширением базовых типов данных заключается в том, что новые свойства становятся доступными в цикле for .. in. Таким образом, если мы запустим наш пример кода после расширения базового типа данных, мы увидим совсем другую картину:

property: 0
product: Object {title: "Symfony2 cup", description: "+10 points to mood"}
property: 1
product: Object {title: "Symfony2 T-Shirt", description: "+100 points to programming skills"}
property: max
product: () {
    return Math.max.apply(null, this);
}

Даже не смотря на то, что свойство `length` массива products равно 2, наш for .. in цикл при обходе находит и дополнительно обрабатывает свойство max. Это поломает наш пример кода, т.к. вместо товара на последней итерации мы попытаемся отрисовать функцию max. Дьявол кроется в деталях.

Сейчас я хочу показать несколько подходов, которые помогут избавиться от подобных проблем.

Решение для массивов

Самое простое что вы можете сделать - отказаться от `for .. in` и использовать простой for цикл и свойство `length`:

var i;

for (i = 0; i < products.length; i += 1) {
    /* делаем полезные вещи */
}

Таким способом мы будем защищены от подводных камней из предыдущего примера. Возникает вполне логичный вопрос, что делать с ассоциативными массивами?

hasOwnProperty

Когда вы работеете с объектами вместо массивов, то не можете использовать свойство `length`. Возможно вам просто нравится цикл `for .. in` и вы хотите его использовать и для массивов и для объектов. Тут нам поможет метод `hasOwnProperty`, который работает как для массивов, так и для объектов:

var i;

for (i in products) {
    if (products.hasOwnProperty(i)) {
        console.log(i);
        // делаем что-то полезное
    }
}

jQuery $.each

Если вы используете jQuery, тогда стоит обратить внимание на его метод `$.each`. Он правильно обходит данные и вам не прийдется ничего дополнительно писать:

$.each(products, function (index, product) {
    console.log('index', index, 'product', product);
    // и снова делаем что-то полезное
});

angular.forEach

Многие библиотеки/фреймворки имеют методы подобные $.each и AngularJs не является исключением. Он предлагает нам безопасный метод forEach:

angular.forEach(products, function(value, key) {
    // обрабатываем данные
});

Я описал лишь одну потенциальную проблему и надеюсь что даже ее будет достаточно, чтобы замотивировать вас писать код более внимательно и грамотно. Сейчас предлагаю пройтись по инструментам, которые вам в этом помогут:

JSHint

JSHint - это программа, которая показывает подозрительные участки кода в программах, написанных на JavaScript. Проект состоит из самой библиотеки и консольной программы, распространяемой как Node модуль.

Вы можете поиграть с JSHint на его официльном сайте. Достаточно просто скопировать свой JavaScript код в терминал и справа вы увидите все предупреждения. Рекомендую также включать режим `strict`:

'use strict';
// только потом пишем остальной код

Интерпретатор будет выкидывать дополнительные ошибки, которые по умолчанию глушатся. Вы можете использовать JSHint несколькими способами:

1) В качестве консольной программы. Достаточно установить его с помощью npm и он будет доступен в терминале как jshint.

2) Вы также можете использовать его как расширение к вашему любимому IDE. У JSHint есть много расширений для текстовых редакторов и IDE, таких как VIM, Sublime, WebStorm и т.д. Более того, в продуктах JetBrains по умолчанию есть поддержка анализа кода с помощью JSHint and JSLint в реальном времени.

Если мы скопируем наш пример кода в JSHint:

var products = [],
    i;

for (i in products) {
    console.log('property: ' + i);
}

Он скажет нам:

One warning
14	The body of a for in should be wrapped in an if statement to filter unwanted properties from the prototype.

Отлично! Используя JSHint как расширение для вашего IDE или в качестве отдельного консольного приложения вы всегда будете видеть все подобные ошибки и не позволите им испортить ваш проект в будущем.

JSLint

Цель у данного инструмента такая же как и у JSHint. На самом деле JSLint является прородителем JSHint, разница между ними заключается в гибкости настроек и жесткости проверок кода. JSLint менее гибок, менее настраиваем (некоторые из его правил нельзя выключить) и в целом более критично относится к коду. Например:

var a = [1, 2, 3];

for (var i = 0; i < a.length; a++)
{
    console.log(a[i]);
}

Данный код пройдет проверку в JSHint, однако JSLint укажет на его недостатки (в соответствии с более жесткой политикой JSLint'a). Причина кроется в том, что блок `var` по правилам JSLint должен быть определен выше другого кода.

Порой бывает не совсем понятно почему код не проходит проверку в JSLint или JSHint. Да, мы можем прочитать саму ошибку, но бывает тяжело понять ее природу и потенциальные проблемы, к которым она приводит. Для таких случаев существуют сайты подобные jslinterrors, где вы найдете подробные описания.

Вывод

Используя JSHint или JSLint вы можете оградить ваш проект от потенциальных проблем. Более того, вы можете быть уверены в том, что вы и ваша команда следуете одним и тем же соглашениям в плане написания кода. Рекомендую начать с JSHint, опробовать его, поиграть с настройками и лишь потом попробовать JSLint. Для большинства людей JSHint будет отличным выбором.

Читайте также:

Менеджеры Js/Css библиотек при работе над Symfony проектом

В процессе разработки, нам необходимо использовать стороние библиотеки, и соответсвенно необходимо централизованно их устанавливать. Прошли те времена когда для того чтобы поставить библиотеку, надо было скачать скрипты и распаковать их себе в проект. Сейчас для этого используют менеджеры библиотек, их очень много например: Сomposer, Bower, Npm, Component и другие. В проектах на Symfony2 используется Composer, но им не очень удобно устанавливать js/css библиотеки, поскольку все библиотеки ставятся в папку которая не должна быть доступна из браузера. Мы рассмотрим какими способами в Symfony2 проекте можно ставить js/css библиотеки, некоторые способы подойдут любому проекту в котором используется Composer.