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

1. Оборачивайте свой код в замыкание

Области видимости в JavaScript — тема достаточно обширная. Но в контексте первого правила все очень просто: если вы объявляете переменную за пределами функции, она становится свойством глобального объекта (в браузере это window):

var a = "hello";
console.log(window.a); // hello

Если не следить за именами переменных, можно случайно перезаписать уже существующее свойство, что может привести к ошибкам. Поэтому свой код лучше оборачивать в замыкание — немедленно вызываемую анонимную функцию. Звучит заумно, но на самом деле все очень просто:

(function () {
    var a = "hello";
    console.log(window.a) // undefined
})();

На первый взгляд этот код может показаться очень сложным. Попробуем упростить, для наглядности:

var closureFunction = function () {
    var a = "hello";
    console.log(window.a); // undefined
};
closureFunction();

Мы объявляем и немедленно вызываем функцию. Скобочки вокруг объявления функции необходимы JavaScript-интерпретатору.

Все переменные, объявленные внутри нашего замыкания, будут доступны только в области видимости замыкания (а также в дочерних областях видимости). Но иногда требуется объявить глобальную переменную, например конструктор класса. Для этого достаточно записать необходимое значение непосредственно в window:

(function () {
    var foo = "foo";
    window.bar = "bar";
})();

console.log(foo, bar); // undefined, "bar"

Если глобальных переменных требуется две или больше, рекомендую передавать объект window внутрь замыкания параметром функции. Это позволит JavaScript-минификаторам эффективнее оптимизировать ваш код:

(function (win) {
    var foo = "foo";
    win.bar = "bar";
})(window);

console.log(foo, bar); // undefined, "bar"

2. Объявляйте переменные с использованием var

Все очень просто: если переменная не объявлена с помощью ключевого слова var, она попадает в глобальную область видимости, даже если объявление находится внутри замыкания. Чаще всего это не то, что нам нужно. Более того, в зависимости от ряда факторов это может привести к ошибке и остановке выполнения кода.

Поэтому просто приучите себя все переменные объявлять с помощью ключевого слова var.

3. Сравнивайте значения переменных с помощью операторов === и !==

JavaScript — язык без строгой типизации. Это значит, что значение переменной может менять свой тип в зависимости от контекста, в котором это значение используется. Естественно, в JavaScript есть вполне четкие правила преобразования одного типа данных в другой, но иногда они ведут себя немного неочевидно. Попробуйте угадать, каким будет результат следующих выражений:

console.log(0 == "0");
console.log(!!0);
console.log(!!"0");

А результат будет следующим:

true
false
true

Иными словами, несмотря на то, что строка "0" равна числу 0, а число 0 — значение ложное, строка "0" — значение истинное. Такие дела. Чтобы избежать подобных проблем, либо изучите абсолютно все правила преобразования типов, либо используйте операторы === и !== для сравнения. Эти операторы аналогичны == и != соответственно, но не приводят типы перед сравнением:

console.log(0 === "0"); // false

4. Кешируйте длину массивов при итерации с помощью цикла for

В JavaScript, как и во многих других языках, существует оператор цикла for:

var arr = ["red", "green", "blue", "alpha"];

for (var i = 0; i < arr.length; i++) {
    console.log("цвет", i + 1, "—", arr[i]);
}

Кажется, все верно, не так ли? Вот только свойство length у массива считается динамически (т.е. при каждом обращении к нему), что может сказаться на производительности цикла при больших размерах массива. Поэтому всегда кешируйте значение свойства length:

for (var i = 0, len = array.length; i < len; i++) {
    console.log("цвет", i + 1, "—", arr[i]);
}

Теперь длина массива вычисляется один раз при инициализации цикла.

5. Оборачивайте объявления callback-функций в циклах в замыкание

var elements = document.getElementsByTagName("div");
for (var i = 0, len = elements.length; i < len; i++) {
    elements[i].addEventListener("click", function () {
        console.log("Div number", i);
    });
}

Вопреки ожиданиям, на какой бы элемент div я ни нажал, в сообщении будет указан индекс последнего элемента div. Связано это с тем, что внутрь callback-функции, передаваемой в обработчик события, доступно значение переменной i, а не его копия в момент объявления функции. Решить это проблему можно созданием замыкания внутри цикла:

var elements = document.getElementsByTagName("div");
for (var i = 0, len = elements.length; i < len; i++) {
    (function () {
        var j = i;
        elements[i].addEventListener("click", function () {
            console.log("Div number", j);
        });
    })();
}

Теперь внутри замыкания мы копируем значение переменной i в переменную j, и используем ее внутри обработчика события.

6. Не используйте ключевое слово this без понимания концепции контекста исполнения

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

var getThis = function () {
    if (this === window) console.log("Global context");
    else console.log("Context:", this);
};
var myObj = {
    name: "My test object",
    getThis: getThis
};
getThis();
myObj.getThis();

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

var myObj = {
    doSomething: function () {
        var self = this;
        (function () {
            console.log(self, this);
        })();
    }
};
myObj.doSomething();

Другой вариант — использовать методы call() и apply(), которые есть у любой функции. Эти методы позволяют задать контекст исполнения в момент вызова функции:

var myObj = {
    doSomething: function () {
        var self = this;
        (function () {
            console.log(self, this);
        }).call(this);
    }
};
myObj.doSomething();

К сожалению, задать контекст исполнения функции, не исполняя ее, эти методы не позволяют, но эту проблему достаточно легко решить с помощью метода bind():

var myObj = {
    doSomething: function () {
        return function () {
            console.log(this);
        }.bind(this);
    }
};
var fn = myObj.doSomething();
fn();

Есть метод bind() не во всех браузерах, но у подавляющего большинства фреймворков есть его аналоги. Недостатком метода bind() является невозможность дальнейшего изменения контекста исполнения функции, поэтому злоупотреблять им не стоит.

7. Не пытайтесь эмулировать классическую модель наследования

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

8. Не начинайте изучение JavaScript с фреймворков

Пожалуй, самая страшная ошибка большинства новичков — они начинают изучение JavaScript с решения практических задач с применением чего-то вроде jQuery. jQuery — отличный фреймворк, но он стирает грань между JavaScript и DOM, а четкое понимание того, где заканчивается одно и начинается другое — очень важно. Поэтому откройте консоль Firebug или Chrome Developer Tools на пустой странице и начните экспериментировать. Научитесь решать задачи без фреймворков.

 

Сегодня JavaScript — не просто язык скриптиков в браузере. С ним работают как на сервере, так и на стороне клиента, разрабатывают приложения для мобильных устройств. Существует множество инструментов, позволяющих преобразовать код с других языков на JavaScript. Четкое понимание и умение эффективно работать с этим языком даст разработчику огромное преимущество во множестве областей разработки. Надеюсь, данная статья поможет вам в этом.