Вступление

Авторы

Это руководство является результатом работы двух заядлых пользователей Stack Overflow: Иво Ветцель /Ivo Wetzel/ (автора текста) и Чжан И Цзян /Zhang Yi Jiang/ (дизайнера).

Участники

Переводчики

Лицензия

JavaScript Гарден распространяется под лицензией MIT и располагается на GitHub. Если вы найдёте ошибку или опечатку, пожалуйста сообщите нам о ней или запросите права на загрузку в репозиторий. Кроме того, вы можете найти нас в комнате JavaScript среди чатов Stack Overflow.

Объекты

Объекты и их свойства

В JavaScript всё ведет себя, как объект, лишь за двумя исключениями — null и undefined.

false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

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

2.toString(); // вызывает SyntaxError

Есть несколько способов обойти этот недостаток и любой из них можно использовать для того, чтобы работать с числами, как с объектами:

2..toString(); // вторая точка распознаётся корректно
2 .toString(); // обратите внимание на пробел перед точкой
(2).toString(); // двойка вычисляется заранее

Объекты как тип данных

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

Используя объектный литерал — нотацию {} — можно создать простой объект. Новый объект наследуется от Object.prototype и не имеет собственных свойств.

var foo = {}; // новый пустой объект

// новый объект со свойством 'test', имеющим значение 12
var bar = {test: 12};

Доступ к свойствам

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

var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // работает

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

Удаление свойств

Единственный способ удалить свойство у объекта — использовать оператор delete; устанавливая свойство в undefined или null, вы только заменяете связанное с ним значение, но не удаляете ключ.

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

Приведённый код выведет две строки: bar undefined и foo null — на самом деле удалено было только свойство baz и посему только оно будет отсутствовать в выводе.

Запись ключей

var test = {
    'case': 'Я — ключевое слово, поэтому меня надо записывать строкой',
    delete: 'Я тоже ключевое слово, так что я' // не является ошибкой, бросает SyntaxError только в версиях ECMAScript ниже 5ой версии
};

Свойства объектов могут записываться как явно символами, так и в виде закавыченных строк. В связи с другим упущением в парсере JavaScript, этот код выбросит SyntaxError во всех версиях ранее ECMAScript 5.

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

От перев.: И еще один пример в пользу строковой нотации, это относится к JSON:

// валидный JavaScript и валидный JSON
{
    "foo": "oof",
    "bar": "rab"
}

// валидный JavaScript и НЕвалидный JSON
{
    foo: "oof",
    bar: "rab"
}

Великий Прототип

В JavaScript отсутствует классическая модель наследования — вместо неё используется прототипная модель.

Хотя её часто расценивают как один из недостатков JavaScript, на самом деле прототипная модель наследования намного мощнее классической. К примеру, поверх неё можно предельно легко реализовать классическое наследование, а попытки совершить обратное вынудят вас попотеть.

Из-за того, что JavaScript — практически единственный широко используемый язык с прототипным наследованием, придётся потратить некоторое время на осознание различий между этими двумя моделями.

Первое важное отличие заключается в том, что наследование в JavaScript выполняется с использованием так называемых цепочек прототипов.

function Foo() {
    this.value = 42;
}
Foo.prototype.method = function() {}

function Bar() {}

// Зададим наследование от Foo
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.foo = 'Hello World';

// Убедимся, что Bar является действующим конструктором
Bar.prototype.constructor = Bar;

var test = new Bar() // создадим новый экземпляр bar

// Цепочка прототипов, которая получится в результате
test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: 'Hello World', value: 42 }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* и т.д. */ }

В приведённом коде объект test наследует оба прототипа: Bar.prototype и Foo.prototype; следовательно, он имеет доступ к функции method которую мы определили в прототипе Foo. Также у него есть доступ к свойству value одного уникального экземпляра Foo, который является его прототипом. Важно заметить, что код new Bar() не создаёт новый экземпляр Foo, а повторно вызывает функцию, которая была назначена его прототипом: таким образом все новые экземпляры Bar будут иметь одинаковое свойство value.

Поиск свойств

При обращении к какому-либо свойству объекта, JavaScript проходит вверх по цепочке прототипов этого объекта, пока не найдет свойство c запрашиваемым именем.

Если он достигнет верхушки этой цепочки (Object.prototype) и при этом так и не найдёт указанное свойство, вместо него вернётся значение undefined.

Свойство prototype

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

function Foo() {}
Foo.prototype = 1; // ничего не произойдёт
Foo.prototype = {
    "foo":"bar"
};

При этом присвоение объектов, как в примере выше, позволит вам динамически создавать цепочки прототипов.

Производительность

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

Вдобавок, при циклическом переборе свойств объекта, будет обработано каждое свойство, существующее в цепочке прототипов.

Расширение встроенных прототипов

Часто встречается неверное применение прототипов — расширение прототипа Object.prototype или прототипов одного из встроенных объектов JavaScript.

Подобная практика нарушает принцип инкапсуляции и имеет соответствующее название — monkey patching. К сожалению, в основу многих широко распространенных фреймворков, например Prototype, положен принцип изменения базовых прототипов. Вам же стоит запомнить — от хорошей жизни прототипы встроенных объектов не меняют.

Единственным оправданием для расширения встроенных прототипов может быть только воссоздание возможностей более новых движков JavaScript, например функции Array.forEach, которая появилась в версии 1.6.

Заключение

Перед тем, как вы приступите к разработке сложных приложений на JavaScript, вы должны полностью осознать как работают прототипы, и как организовывать наследование на их основе. Также, помните о зависимости между длиной цепочек прототипов и производительностью — разрывайте их при необходимости. Кроме того — никогда не расширяйте прототипы встроенных объектов (ну, если только для совместимости с новыми возможностями Javascript).

Функция hasOwnProperty

Если вам необходимо проверить, определено ли свойство у самого объекта, а не в его цепочке прототипов, вы можете использовать метод hasOwnProperty, который все объекты наследуют от Object.prototype.

hasOwnProperty — единственная функция в JavaScript, которая позволяет получить свойства объекта без обращения к цепочке его прототипов.

// испортим Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

Только используя hasOwnProperty можно гарантировать правильный результат при переборе свойств объекта. И нет иного способа для определения свойств, которые определены в самом объекте, а не где-то в цепочке его прототипов.

hasOwnProperty как свойство

JavaScript не резервирует свойство с именем hasOwnProperty. Так что, если есть потенциальная возможность, что объект может содержать свойство с таким именем, требуется использовать внешний вариант функции hasOwnProperty чтобы получить корректные результаты.

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Да прилетят драконы'
};

foo.hasOwnProperty('bar'); // всегда возвращает false

// Используем метод hasOwnProperty пустого объекта
// и передаём foo в качестве this
({}).hasOwnProperty.call(foo, 'bar'); // true

Заключение

Единственным способом проверить существование свойства у объекта является использование метода hasOwnProperty. При этом рекомендуется использовать этот метод в каждом цикле for in вашего проекта, чтобы избежать возможных ошибок с ошибочным заимствованием свойств из прототипов родительских объектов. Также вы можете использовать конструкцию {}.hasOwnProperty.call(...) на случай, если кто-то вздумает расширить прототипы встроенных объектов.

Цикл for in

Как и оператор in, цикл for in проходит по всей цепочке прототипов, обходя свойства объекта.

// Испортим Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // печатает и bar и moo
}

Так как изменить поведение цикла for in как такового не представляется возможным, то для фильтрации нежелательных свойств объекта внутри этого цикла используют метод hasOwnProperty из Object.prototype.

Использование hasOwnProperty в качестве фильтра

// возьмём foo из примера выше
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

Это единственная версия правильного использования цикла. Благодаря использованию hasOwnProperty будет выведено только свойство moo. Если же убрать hasOwnProperty, код становится нестабилен и могут возникнуть ошибки, особенно если кто-то изменил встроенные прототипы, такие как Object.prototype.

Один из самых популярных фреймворков Prototype как раз этим и славится, и если вы его подключаете, то не забудьте использовать hasOwnProperty внутри цикла for in, иначе у вас гарантированно возникнут проблемы.

Рекомендации

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

Функции

Выражения и объявление функций

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

Объявление function

// всё просто и привычно
function foo() {}

В следующем примере описанная функция резервируется перед запуском всего скрипта; за счёт этого она доступна в любом месте кода, вне зависимости от того, где она определена — даже если функция вызывается до её фактического объявления в коде.

foo(); // сработает, т.к. функция будет создана до выполнения кода
function foo() {}

function как выражение

var foo = function() {};

В этом примере безымянная и анонимная функция присваивается переменной foo.

foo; // 'undefined'
foo(); // вызовет TypeError
var foo = function() {};

Так как в данном примере выражение var — это определение функции, переменная с именем foo будет заранее зарезервирована перед запуском скрипта (таким образом, foo уже будет определена во время его работы).

Но поскольку присвоения исполняются непосредственно во время работы кода, foo по умолчанию будет присвоено значение undefined (до обработки строки с определением функции):

var foo; // переменная неявно резервируется
foo; // 'undefined'
foo(); // вызовет TypeError
foo = function() {};

Выражения с именованными фунциями

Существует еще нюанс, касающийся именованных функций создающихся через присваивание:

var foo = function bar() {
    bar(); // работает
}
bar(); // получим ReferenceError

Здесь объект bar не доступен во внешней области, так как имя bar используется только для присвоения переменной foo; однако bar можно вызвать внутри функции. Такое поведение связано с особенностью работы JavaScript с пространствами имен - имя функции всегда доступно в локальной области видимости самой функции.

Как работает this

В JavaScript область ответственности специальной переменной this концептуально отличается от того, за что отвечает this в других языках программирования. Различают ровно пять вариантов того, к чему привязывается this в языке.

1. Глобальная область видимости

this;

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

2. Вызов функции

foo();

Тут this также ссылается на глобальный объект.

3. Вызов метода

test.foo();

В данном примере this ссылается на test.

4. Вызов конструктора

new foo();

Если перед вызовом функции присутствует ключевое слово new, то данная функция будет действовать как конструктор. Внутри такой функции this будет указывать на новосозданный Object.

5. Переопределение this

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // массив развернётся в a = 1, b = 2, c = 3
foo.call(bar, 1, 2, 3); // аналогично

Когда мы используем методы call или apply из Function.prototype, то внутри вызываемой функции this явным образом будет присвоено значение первого передаваемого параметра.

Исходя из этого, в предыдущем примере (строка с apply) правило #3 вызов метода не будет применено, и this внутри foo будет присвоено bar.

Наиболее распространенные ошибки

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

Foo.method = function() {
    function test() {
        // this ссылается на глобальный объект
    }
    test();
};

Распространенным заблуждением будет то, что this внутри test ссылается на Foo, но это не так.

Для того, чтобы получить доступ к Foo внутри функции test, необходимо создать локальную переменную внутри method, которая и будет ссылаться на Foo.

Foo.method = function() {
    var that = this;
    function test() {
        // Здесь используем that вместо this
    }
    test();
};

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

Назначение методов

Еще одной фичей, которая не работает в JavaScript, является создание псевдонимов для методов, т.е. присвоение метода объекта переменной.

var test = someObject.methodTest;
test();

Следуя первому правилу test вызывается как обычная функция; следовательно this внутри него больше не ссылается на someObject.

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

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

В момент, когда будет вызван method нового экземпляра Bar, this будет ссылаться на этот самый экземпляр.

Замыкания и ссылки

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

Эмуляция приватных свойств

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

В данном примере Counter возвращает два замыкания: функции increment и get. Обе эти функции сохраняют ссылку на область видимости Counter и, соответственно, имеют доступ к переменной count из этой самой области.

Как это работает

Поскольку в JavaScript нельзя присваивать или ссылаться на области видимости, заполучить count извне не представляется возможным. Единственным способом взаимодействовать с ним остается использование двух замыканий.

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

В приведенном примере мы не изменяем переменную count в области видимости Counter, т.к. foo.hack не объявлен в данной области. Вместо этого будет создана или перезаписана глобальная переменная count;

Замыкания внутри циклов

Часто встречается ошибка, когда замыкания используют внутри циклов, передавая переменную индекса внутрь.

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

Данный код не будет выводить числа с 0 до 9, вместо этого число 10 будет выведено десять раз.

Анонимная функция сохраняет ссылку на i и, когда будет вызвана функция console.log, цикл for уже закончит свою работу, а в i будет содержаться 10.

Для получения желаемого результата необходимо создать копию переменной i.

Во избежание ошибок

Для того, чтобы скопировать значение индекса из цикла, лучше всего использовать анонимную функцию как обёртку.

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}

Анонимная функция-обертка будет вызвана сразу же, и в качестве первого аргумента получит i, значение которой будет скопировано в параметр e.

Анонимная функция, которая передается в setTimeout, теперь содержит ссылку на e, значение которой не изменяется циклом.

Еще одним способом реализации является возврат функции из анонимной функции-обертки, поведение этого кода будет таким же, как и в коде из предыдущего примера.

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

Объект arguments

В области видимости любой функции в JavaScript есть доступ к специальной переменной arguments. Эта переменная содержит в себе список всех аргументов, переданных данной функции.

Объект arguments не является наследником Array. Он, конечно же, очень похож на массив и даже содержит свойство length — но он не наследует Array.prototype, а представляет собой Object.

По этой причине, у объекта arguments отсутствуют стандартные методы массивов, такие как push, pop или slice. Хотя итерация с использованием обычного цикла for по аргументам работает вполне корректно, вам придётся конвертировать этот объект в настоящий массив типа Array, чтобы применять к нему стандартные методы массивов.

Конвертация в массив

Указанный код вернёт новый массив типа Array, содержащий все элементы объекта arguments.

Array.prototype.slice.call(arguments);

Эта конвертация занимает много времени и использовать её в критических частях кода не рекомендуется.

Передача аргументов

Ниже представлен рекомендуемый способ передачи аргументов из одной функции в другую.

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // делаем здесь что-нибудь
}

Другой трюк — использовать и call и apply вместе, чтобы быстро создать несвязанную обёртку:

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// Создаём несвязанную версию "method"
// Она принимает параметры: this, arg1, arg2...argN
Foo.method = function() {

    // Результат: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);

};

Формальные аргументы и индексы аргументов

Объект arguments создаёт по геттеру и сеттеру и для всех своих свойств и для формальных параметров функции.

В результате, изменение формального параметра также изменит значение соответствующего свойства объекта arguments и наоборот.

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

Мифы и правда о производительности

Объект arguments создаётся во всех случаях, лишь за двумя исключениями — когда он переопределён внутри функции (по имени) или когда одним из её параметров является переменная с таким именем. Неважно, используется при этом сам объект или нет.

Геттеры и сеттеры создаются всегда; так что их использование практически никак не влияет на производительность.

Однако, есть один момент, который может радикально понизить производительность современных движков JavaScript. Этот момент — использование arguments.callee.

function foo() {
    arguments.callee; // сделать что-либо с этим объектом функции
    arguments.callee.caller; // и с вызвавшим его объектом функции
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // При обычных условиях должна бы была быть развёрнута...
    }
}

В коде выше, функция foo не может быть развёрнута (а могла бы), потому что для корректной работы ей необходима ссылка и на себя и на вызвавший её объект. Это не только кладёт на лопатки механизм развёртывания, но и нарушает принцип инкапсуляции, поскольку функция становится зависима от конкретного контекста вызова.

Крайне не рекомендуется использовать arguments.callee или какое-либо из его свойств. Никогда.

Конструктор

Создание конструкторов в JavaScript также отличается от большинства других языков. Любая функция, вызванная с использованием ключевого слова new, будет конструктором.

Внутри конструктора (вызываемой функции) this будет указывать на новосозданный Object. Прототипом этого нового объекта будет prototype функции, которая была вызвана в качестве конструктора.

Если вызываемая функция не имеет явного возврата посредством return, то вернётся this — этот новый объект.

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

В этом примере Foo вызывается в виде конструктора, следовательно прототип созданного объекта будет привязан к Foo.prototype.

В случае, когда функция в явном виде возвращает некое значение используя return, то в результате выполнения конструктора мы получим именно его, но только если возвращаемое значение представляет собой Object.

function Bar() {
    return 2;
}
new Bar(); // новый объект

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // возвращённый объект

Если же опустить ключевое слово new, то функция не будет возвращать никаких объектов.

function Foo() {
    this.bla = 1; // устанавливается глобальному объекту
}
Foo(); // undefined

Этот пример в некоторых случаях всё-таки может сработать: это связано с поведением this в JavaScript — он будет восприниматься парсером как глобальный объект.

Фабрики

Если хотите избавится от необходимости использования new, напишите конструктор, возвращающий значение посредством return.

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

В обоих случаях при вызове Bar мы получим один и тот же результат — новый объект со свойством method (спасибо замыканию за это).

Также следует заметить, что вызов new Bar() никак не связан с прототипом возвращаемого объекта. Хоть прототип и назначается всем новосозданным объектам, но Bar никогда не возвращает этот новый объект.

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

Создание объектов с использованием фабрик

Часто не рекомендуют использовать new, поскольку если вы его забудете, это может привести к ошибкам.

Чтобы создать новый объект, лучше использовать фабрику и создать новый объект внутри этой фабрики.

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

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

  1. Он использует больше памяти, поскольку созданные объекты не хранят методы в прототипе и соответственно для каждого нового объекта создаётся копия каждого метода.
  2. Чтобы эмулировать наследование, фабрике нужно скопировать все методы из другого объекта или установить прототипом нового объекта старый.
  3. Разрыв цепочки прототипов просто по причине забытого ключевого слова new идёт вразрез с духом языка.

Заключение

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

Области видимости и пространства имён

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

function test() { // область видимости
    for(var i = 0; i < 10; i++) { // не область видимости
        // считаем
    }
    console.log(i); // 10
}

Также JavaScript не знает ничего о различиях в пространствах имён: всё определяется в глобально доступном пространстве имён.

Каждый раз, когда JavaScript обнаруживает ссылку на переменную, он будет искать её всё выше и выше по областям видимости, пока не найдёт её. В случае, если он достигнет глобальной области видимости и не найдет запрошенное имя и там тоже, он ругнётся ReferenceError.

Проклятие глобальных переменных

// скрипт A
foo = '42';

// скрипт B
var foo = '42'

Вышеприведённые два скрипта не приводят к одному результату. Скрипт A определяет переменную по имени foo в глобальной области видимости, а скрипт B определяет foo в текущей области видимости.

Повторимся, это вообще не тот же самый эффект. Если вы не используете var — то вы в большой опасности.

// глобальная область видимости
var foo = 42;
function test() {
    // локальная область видимости
    foo = 21;
}
test();
foo; // 21

Из-за того что оператор var опущен внутри функции, фунция test перезапишет значение foo. Это поначалу может показаться не такой уж и большой проблемой, но если у вас имеется тысяча строк JavaScript-кода и вы не используете var, то вам на пути встретятся страшные и трудноотлаживаемые ошибки — и это не шутка.

// глобальная область видимости
var items = [/* какой-то список */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // область видимости subLoop
    for(i = 0; i < 10; i++) { // пропущенный оператор var
        // делаем волшебные вещи!
    }
}

Внешний цикл прекратит работу сразу после первого вызова subLoop, поскольку subLoop перезаписывает глобальное значение переменной i. Использование var во втором цикле for могло бы вас легко избавить от этой ошибки. Никогда не забывайте использовать var, если только влияние на внешнюю область видимости не является тем, что вы намерены получить.

Локальные переменные

Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var.

// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // локальная область видимости для функции test
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

В то время как foo и i — локальные переменные в области видимости функции test, присвоение bar переопределит значение одноимённой глобальной переменной.

Всплытие

В JavaScript действует механизм всплытия определения. Это значит, что оба определения с использованием var и определение function будут перенесены наверх заключающей их области видимости.

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

Этот код трансформируется ещё перед исполнением. JavaScript перемещает операторы var и определение function наверх ближайшей оборачивающей области видимости.

// выражения с var переместились сюда
var bar, someValue; // по умолчанию - 'undefined'

// определение функции тоже переместилось
function test(data) {
    var goo, i, e; // потерянная блочная область видимости
                   // переместилась сюда
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // вылетает с ошибкой TypeError,
       // поскольку bar всё ещё 'undefined'
someValue = 42; // присвоения не подвержены всплытию
bar = function() {};

test();

Потерянная область видимости блока не только переместит операторы var вовне циклов и их тел, но и сделает результаты некоторых конструкций с if неинтуитивными.

В исходном коде оператор if изменял глобальную переменную goo, когда, как оказалось, он изменяет локальную переменную — в результате работы всплытия.

Если вы не знакомы со всплытием, то можете посчитать, что нижеприведённый код должен породить ReferenceError.

// проверить, проинициализована ли SomeImportantThing
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

Но, конечно же, этот код работает: из-за того, что оператор var был перемещён наверх глобальной области видимости

var SomeImportantThing;

// другой код может инициализировать здесь переменную SomeImportantThing,
// а может и нет

// убедиться, что она всё ещё здесь
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

Порядок разрешения имён

Все области видимости в JavaScript, включая глобальную области видимости, содержат специальную, определённую внутри них, переменную this, которая ссылается на текущий объект.

Области видимости функций также содержат внутри себя переменную arguments, которая содержит аргументы, переданные в функцию.

Например, когда JavaScript пытается получить доступ к переменной foo в области видимости функции, он будет искать её по имени в такой последовательности:

  1. Если в текущей области видимости есть выражение var foo, использовать его.
  2. Если один из параметров функции называется foo, использовать его.
  3. Если функция сама называется foo, использовать её.
  4. Перейти на одну область видимости выше и начать с п. 1

Пространства имён

Нередкое последствие наличия только одного глобального пространства имён — проблема с перекрытием имён переменных. В JavaScript эту проблему легко избежать, используя анонимные обёртки.

(function() {
    // самостоятельно созданное "пространство имён"

    window.foo = function() {
        // открытое замыкание
    };

})(); // сразу же выполнить функцию

Безымянные функции являются выражениями; поэтому, чтобы вы имели возможность их выполнить, они сперва должны быть разобраны.

( // разобрать функцию внутри скобок
function() {}
) // и вернуть объект функции
() // вызвать результат разбора

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

// Два других способа
+function(){}();
(function(){}());

Заключение

Рекомендуется всегда использовать анонимную обёртку для заключения кода в его собственное пространство имён. Это не только защищает код от совпадений имён, но и позволяет создавать более модульные программы.

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

Массивы

Итерации по массивам и свойства

Несмотря на то, что массивы в JavaScript являются объектами, нет достаточных оснований для использования цикла for in для итерации по элементам массива. Фактически, существует несколько весомых причин против использования for in в массивах.

Во время выполнения for in циклически перебираются все свойства объекта, находящиеся в цепочке прототипов. Единственный способ исключить ненужные свойства — использовать hasOwnProperty, а это в 20 раз медленнее обычного цикла for.

Итерирование

Для достижения лучшей производительности при итерации по массивам, лучше всего использовать обычный цикл for.

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

В примере выше есть один дополнительный приём, с помощью которого кэшируется величина длины массива: l = list.length.

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

Фактически, отсутствие кэширования может привести к выполнению цикла в два раза медленнее, чем при кэшировании длины

Свойство length

Хотя геттер свойства length просто возвращает количество элементов содержащихся в массиве, сеттер можно использовать для обрезания массива.

var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo; // [1, 2, 3]

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

Заключение

Для оптимальной работы кода рекомендуется всегда использовать простой цикл for и кэшировать свойство length. Использование for in с массивами является признаком плохого кода, обладающего предпосылками к ошибкам и может привести к низкой скорости его выполнения.

Конструктор Array

Так как в конструкторе Array есть некоторая двусмысленность, касающаяся его параметров, настоятельно рекомендуется при создании массивов всегда использовать синтаксис литеральной нотации — [].

[1, 2, 3]; // Результат: [1, 2, 3]
new Array(1, 2, 3); // Результат: [1, 2, 3]

[3]; // Результат: [3]
new Array(3); // Результат: []
new Array('3') // Результат: ['3']

В случае, когда в конструктор Array передаётся один аргумент и этот аргумент имеет тип Number, конструктор возвращает новый, заполненный случайными значениями, массив, имеющий длину равную значению переданного аргумента. Стоит заметить, что в этом случае будет установлено только свойство length нового массива, индексы массива фактически не будут проинициализированы.

var arr = new Array(3);
arr[1]; // не определён, undefined
1 in arr; // false, индекс не был установлен

Поведение, которое позволяет изначально установить только размер массива, может пригодиться лишь в нескольких случаях, таких как повторение строк, за счёт чего избегается использование цикла for.

new Array(count + 1).join(stringToRepeat);

Заключение

Использование конструктора Array нужно избегать, насколько это возможно. Литералы определённо предпочтительнее — это краткая запись и она имеет более понятный синтаксис, так что при этом даже улучшается читабельность кода.

Типы

Равенство и сравнение

JavaScript имеет 2 различных способа сравнения значений объектов на равенство.

Оператор сравнения

Оператор сравнения состоит из двух символов равенства: ==

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

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

В таблице выше показаны результаты приведения типов и это главная причина, почему использование == повсеместно считается плохой практикой: оно приводит к трудностям в отслеживании ошибок из-за сложных правил преобразования типов.

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

Оператор строгого равенства

Оператор строгого равенства состоит из трёх символов равенства: ===

В отличие от обычного оператора равенства, оператор строгого равенства не выполняет приведение типов между операндами.

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

Результаты выше более понятны и позволяют быстрее выявлять ошибки в коде. Это в определённой степени улучшает код, а также дает прирост производительности в случае, если операнды имеют различные типы.

Сравнение объектов

Хотя оба оператора == и === заявлены как операторы равенства, они ведут себя по-разному, когда хотя бы один из операндов является Object.

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

Здесь оба операнда сравниваются на идентичность, а не на равенство; то есть будет проверяться, являются ли операнды одним экземпляром объекта, так же как делает is в Python и сравниваются указатели в С.

Заключение

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

Оператор typeof

Оператор typeof (вместе с instanceof) — это, вероятно, самая большая недоделка в JavaScript, поскольку, похоже, он поломан более, чем полностью.

Хотя instanceof еще имеет ограниченное применение, typeof на самом деле имеет только один практический случай применения, который при всём при этом не является проверкой типа объекта.

Таблица типов JavaScript

Значение            Класс      Тип
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function в Nitro/V8)
new RegExp("meow")  RegExp     object (function в Nitro/V8)
{}                  Object     object
new Object()        Object     object

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

Класс представляет собой значение внутреннего свойства [[Class]] объекта.

Для того, чтобы получить значение [[Class]], необходимо вызвать метод toString у Object.prototype.

Класс объекта

Спецификация предоставляет только один способ доступа к значению [[Class]] — используя Object.prototype.toString.

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

В примере выше Object.prototype.toString вызывается со значением this, являющимся объектом, значение [[Class]] которого нужно получить.

Проверка переменных на определённость

typeof foo !== 'undefined'

Выше проверяется, было ли foo действительно объявлено или нет; просто обращение к переменной приведёт к ReferenceError. Это единственное, чем на самом деле полезен typeof.

Заключение

Для проверки типа объекта настоятельно рекомендуется использоватьObject.prototype.toString — это единственный надежный способ. Как показано выше в таблице типов, некоторые возвращаемые typeof значения не определены в спецификации: таким образом, они могут отличаться в различных реализациях.

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

Оператор instanceof

Оператор instanceof сравнивает конструкторы двух операндов. Это полезно только когда сравниваются пользовательские объекты. Использование на встроенных типах почти так же бесполезно, как и оператор typeof.

Сравнение пользовательских объектов

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// Всего лишь присваиваем Bar.prototype объект функции Foo,
// но не экземпляра Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

Использование instanceof со встроенными типами

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

Здесь надо отметить одну важную вещь: instanceof не работает на объектах, которые происходят из разных контекстов JavaScript (например, из различных документов в web-браузере), так как их конструкторы и правда не будут конструкторами тех самых объектов.

Заключение

Оператор instanceof должен использоваться только при обращении к пользовательским объектам, происходящим из одного контекста JavaScript. Так же, как и в случае оператора typeof, любого другого использования необходимо избегать.

Приведение типов

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

// Эти равенства — истинны
new Number(10) == 10; // объект типа Number преобразуется
                      // в числовой примитив в результате неявного вызова
                      // метода Number.prototype.valueOf

10 == '10';           // Strings преобразуется в Number
10 == '+10 ';         // Ещё чуток строко-безумия
10 == '010';          // и ещё
isNaN(null) == false; // null преобразуется в 0,
                      // который конечно же не NaN

// Эти равенства — ложь
10 == 010;
10 == '-10';

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

Конструкторы встроенных типов

Конструкторы встроенных типов, например, Number и String ведут себя различным образом, в зависимости от того, вызываются они с ключевым словом new или без.

new Number(10) === 10;     // False, Object и Number
Number(10) === 10;         // True, Number и Number
new Number(10) + 0 === 10; // True, из-за неявного преобразования

Использование встроенного типа, такого как Number, в качестве конструктора создаёт новый экземпляр объекта Number, но при использовании без ключевого слова new функция Number будет вести себя как конвертер.

Кроме того, присутствие литералов или переменных, которые не являются объектами, приведет к еще большему насилию над типами.

Лучший вариант — это явное приведение к одному из трех возможных типов.

Приведение к строке

'' + 10 === '10'; // true

Путём добавления в начале пустой строки, значение легко приводится к строке.

Приведение к числовому типу

+'10' === 10; // true

Используя унарный оператор плюс, можно преобразовать значение в число.

Приведение к булеву типу

Используя оператор not (!) дважды, значение может быть приведено к логическому (булеву) типу.

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

Нативности

Почему нельзя использовать eval

Функция eval выполняет строку кода JavaScript в локальной области видимости.

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

Но eval исполняется в локальной области видимости только тогда, когда он вызывается напрямую и при этом имя вызываемой функции именно eval.

var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

Любой ценой избегайте использования функции eval. 99.9% случаев её "использования" могут достигаться без её участия.

eval под прикрытием

Обе функции работы с интервалами времени setTimeout и setInterval могут принимать строку в качестве первого аргумента. Эта строка всегда будет выполняться в глобальной области видимости, поскольку eval в этом случае вызывается не напрямую.

Проблемы с безопасностью

Кроме всего прочего, функция eval — это проблема в безопасности, поскольку исполняется любой переданный в неё код; никогда не следует использовать её со строками из неизвестных или недоверенных источников.

Заключение

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

undefined и null

В JavaScript есть два отдельных типа для представления ничего, при этом более полезным из них является undefined.

Тип undefined

undefined — это тип с единственным возможным значением: undefined.

Кроме этого, в языке определена глобальная переменная со значением undefined, и эта переменная так и называется — undefined. Не являясь константой, она не является и ключевым словом. Из этого следует, что её значение можно с лёгкостью переопределить.

Несколько случаев, когда возвращается undefined:

  • При попытке доступа к глобальной переменной undefined (если она не изменена).
  • Неявный возврат из функции при отсутствии в ней оператора return.
  • Из операторов return, которые ничего не возвращают.
  • В результате поиска несуществующего свойства у объекта (и доступа к нему).
  • Параметры, которые не были переданы в функцию явно.
  • При доступе ко всему, чьим значением является undefined.

Обработка изменений значения undefined

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

Но при этом, чтобы сравнить что-либо со значением undefined, прежде нужно получить значение самой переменной undefined.

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

var undefined = 123;
(function(something, foo, undefined) {
    // в локальной области видимости `undefined`
    // снова ссылается на правильное значене.

})('Hello World', 42);

Другой способ достичь того же эффекта — использовать определение внутри обёртки.

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

Единственная разница между этими вариантами в том, что последняя версия будет больше на 4 байта при минификации, а в первом случае внутри анонимной обёртки нет дополнительного оператора var.

Использование null

Хотя undefined в контексте языка JavaScript чаще используется в качестве традиционного null, настоящий null (и тип и литерал) является в большей или меньшей степени просто другим типом данных.

Он используется во внутренних механизмах JavaScript (например для определения конца цепочки прототипов за счёт присваивания Foo.prototype = null). Но в большинстве случаев тип null может быть заменён на undefined.

Автоматическая вставка точек с запятой

Хоть JavaScript и имеет синтаксис, подобный языкам семейства C, он при этом не принуждает вас ставить точки с запятой в исходном коде — вы всегда можете их опустить.

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

var foo = function() {
} // ошибка разбора, ожидается точка с запятой
test()

Происходит вставка и парсер пытается снова.

var foo = function() {
}; // ошибки нет, парсер продолжает
test()

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

Как это работает

Приведённый код не содержит точек с запятой, так что места для их вставки остаются на совести парсера:

(function(window, undefined) {
    function test(options) {
        log('тестируем!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'здесь передадим длинную строчку',
            'и ещё одну на всякий случай'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}

})(window)

Ниже представлен результат игры парсера в "угадалки".

(function(window, undefined) {
    function test(options) {

        // не вставлена точка с запятой, строки были объединены
        log('тестируем!')(options.list || []).forEach(function(i) {

        }); // <- вставлена

        options.value.test(
            'здесь передадим длинную строчку',
            'и ещё одну на всякий случай'
        ); // <- вставлена

        return; // <- вставлена, в результате 
                //    оператор return разбит на два блока
        { // теперь парсер считает этот блок отдельным

            // метка и одинокое выражение
            foo: function() {}
        }; // <- вставлена
    }
    window.test = test; // <- вставлена

// снова объединились строки
})(window)(function(window) {
    window.someLibrary = {}; // <- вставлена

})(window); //<- вставлена

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

"Висящие" скобки

Если парсер встречает "висящую" скобку, то он не вставляет точку с запятой.

log('тестируем!')
(options.list || []).forEach(function(i) {})

Такой код трансформируется в строку

log('тестируем!')(options.list || []).forEach(function(i) {})

Чрезвычайно высоки шансы, что log возвращает не функцию; таким образом, эта строка вызовет TypeError с сообщением о том, что undefined не является функцией.

Заключение

Настоятельно рекомендуем никогда не забывать ставить точку с запятой; также рекомендуется оставлять скобки на одной строке с соответствующим оператором и никогда не опускать их для выражений с использованием if / else. Оба этих совета не только повысят читабельность вашего кода, но и предотвратят от изменения поведения кода, произведённого парсером втихую.

Другое

setTimeout и setInterval

Поскольку JavaScript поддерживает асинхронность, есть возможность запланировать выполнение функции, используя функции setTimeout и setInterval.

function foo() {}
var id = setTimeout(foo, 1000); // возвращает число > 0

Функция setTimeout возвращает идентификатор таймаута и планирует вызвать foo через, примерно, тысячу миллисекунд. Функция foo при этом будет вызвана ровно один раз.

В зависимости от разрешения таймера в используемом для запуска кода движке JavaScript, а также с учётом того, что JavaScript является однопоточным языком и посторонний код может заблокировать выполнение потока, нет никакой гарантии, что переданный код будет выполнен ровно через указанное в вызове setTimeout время.

Переданная первым параметром функция будет вызвана как глобальный объект — это значит, что оператор this в вызываемой функции будет ссылаться на этот самый объект.

function Foo() {
    this.value = 42;
    this.method = function() {
        // this ссылается на глобальный объект
        console.log(this.value); // выведет в лог undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

Поочерёдные вызовы с использованием setInterval

setTimeout вызывает функцию единожды; setInterval — как и предполагает название — вызывает функцию каждые X миллисекунд. И его использование не рекомендуется.

В то время, когда исполняющийся код будет блокироваться во время вызова с таймаутом, setInterval будет продолжать планировать последующие вызовы переданной функции. Это может (особенно в случае небольших интервалов) повлечь за собой выстраивание вызовов функций в очередь.

function foo(){
    // что-то, что выполняется одну секунду
}
setInterval(foo, 100);

В приведённом коде foo выполнится один раз и заблокирует этим главный поток на одну секунду.

Пока foo блокирует код, setInterval продолжает планировать последующие её вызовы. Теперь, когда первая foo закончила выполнение, в очереди будут уже десять ожидающих выполнения вызовов foo.

Разбираемся с потенциальной блокировкой кода

Самый простой и контролируемый способ — использовать setTimeout внутри самой функции.

function foo(){
    // что-то, выполняющееся одну секунду
    setTimeout(foo, 100);
}
foo();

Такой способ не только инкапсулирует вызов setTimeout, но и предотвращает от очередей блокирующих вызовов и при этом обеспечивает дополнительный контроль. Сама функция foo теперь принимает решение, хочет ли она запускаться ещё раз или нет.

Очистка таймаутов вручную

Удаление таймаутов и интервалов работает через передачу соответствующего идентификатора либо в функцию clearTimeout, либо в функцию clearInterval — в зависимости от того, какая функция set... использовалась для его получения.

var id = setTimeout(foo, 1000);
clearTimeout(id);

Очистка всех таймаутов

Из-за того, что встроенного метода для удаления всех таймаутов и/или интервалов не существует, для достижения этой цели приходится использовать брутфорс.

// удаляем "все" таймауты
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

Вполне могут остаться таймауты, которые не будут захвачены этим произвольным числом; так что всё же рекомендуется следить за идентификаторами всех создающихся таймаутов, за счёт чего их можно будет удалять индивидуально.

Скрытое использование eval

setTimeout и setInterval могут принимать строку в качестве первого параметра. Эту возможность не следует использовать никогда, поскольку изнутри при этом производится скрытый вызов eval.

function foo() {
    // будет вызвана
}

function bar() {
    function foo() {
        // никогда не будет вызывана
    }
    setTimeout('foo()', 1000);
}
bar();

Поскольку eval в этом случае не вызывается напрямую, переданная в setTimeout строка будет выполнена в глобальной области видимости; так что локальная переменная foo из области видимости bar не будет выполнена.

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

function foo(a, b, c) {}

// НИКОГДА не делайте такого
setTimeout('foo(1,2, 3)', 1000)

// Вместо этого используйте анонимную функцию
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

Заключение

Никогда не используйте строки как параметры setTimeout или setInterval. Это явный признак действительно плохого кода. Если вызываемой функции необходимо передавать аргументы, лучше передавать анонимную функцию, которая самостоятельно будет отвечать за сам вызов.

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

Пояснения

От переводчиков

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

К примеру, в статье про сравнение объектов авторы настоятельно рекомендуют использовать только оператор строгого неравенства ===. Но мы считаем, что если вы уверены и осознали, что оба сравниваемых операнда имеют один тип, вы имеете право опустить последний символ =. Вы вольны применять строгое неравенство только в случаях, когда вы не уверены в типах операндов (!== undefined — это полезный приём). Так в вашем коде будут опасные и безопасные области, но при этом по коду будет явно видно, где вы рассчитываете на переменные одинаковых типов, а где позволяете пользователю вольности.

Функцию setInterval тоже можно использовать, если вы стопроцентно уверены, что код внутри неё будет исполняться как минимум в три раза быстрее переданного ей интервала.

С другой стороны, использование var и грамотная расстановка точек с запятой — обязательные вещи, халатное отношение к которым никак не может быть оправдано — в осознанном пропуске var (если только вы не переопределяете глобальный объект браузера... хотя зачем?) или точки с запятой нет никакого смысла.

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

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