Вступление

Авторы

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

Участники

Хостинг

JavaScript Garden хостится на GitHub, однако Cramer Development поддерживают наше зеркало на JavaScriptGarden.info.

Переводчики

Лицензия

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 распростанено заблуждение, что числовые литералы нельзя использовать в роли объектов — оно является неверным и зародилось по причине известного упущения в парсере 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) прототипом: таким образом, все новые экземпляры Bar будут иметь одно и то же свойство value (прим. перев. — то есть все ссылки по имени value, во всех экземплярах Bar, будут указывать на одно и то же место в памяти).

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

При обращении к какому-либо свойству объекта, движок 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 из прототипа Object
Object.prototype.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 использует упомянутое расширение Object.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 is not defined» отсутствует).

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

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

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

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

Здесь фукнция 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]); // внутри foo массив развернётся в аргументы
foo.call(bar, 1, 2, 3); // аналогично: a = 1, b = 2, c = 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 будет ссылаться на этот самый экземпляр.

(прим. перев. — В случае, если вам необходимо передать this в функцию, вы можете использовать <function>.call, <function>.apply при непосредственно вызове или <function>.bind при отложенном — для того, чтобы вызвать эту функцию с указанным 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 (прим. перев. — замена кода внутри foo.hack на this.count = 1337, не поможет, конечно же, тоже, поскольку count никогда не был свойством объекта Counter, а был лишь внутренней переменной);

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

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

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)
}

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

Объект 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);
};

// Создаём несвязанную версию метода
// Она принимает параметры: 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 создаётся во всех случаях, за одним лишь исключением — когда он переопределён по имени внутри функции, или когда одним из её параметров является переменная с таким именем. При этом ееважно, используется ли сам объект в коде функции или нет.

Геттеры и сеттеры создаются всегда; так что их использование практически никак не влияет на производительность, и тем более никак не влияет на неё в реальном коде, где обычно происходят вещи посерьёзнее обычных прочтений и переопределений свойств объекта 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.

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

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

function Test() {
    this.value = 2;

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

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

function Foo() {
    this.bla = 1; // свойство bla устанавливается глобальному объекту
}
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 никогда не возвращает этот новый объект (прим. перев. — судя по всему, подразумевается, что код 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 вовне циклов и их тел, но и лишит смысла конструкцию c 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 умеет сравнивать значения объектов на равенство двумя различными способами.

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

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

Под слабой типизацией языка 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, любого другого использования instanceof необходимо избегать.

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

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

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

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

// Эти равенства возвращают false
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 используется два отдельных типа для описания ничегоnull и undefined, при этом более полезным из них оказывается undefined.

Значение undefined

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

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

Список случаев, когда код возвращает значение undefined:

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

Защита от потенциальных изменений значения 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. Оба этих совета не только повысят читабельность вашего кода, но и предотвратят вас от изменений в поведении кода, сделанных парсером без вашего ведома.

Оператор delete

Если говорить кратко — в JavaScript невозможно удалить глобальную переменную или функцию или любую другую сущность, у которой установлен атрибут DontDelete.

Глобальный код и код внутри функций

Когда переменная или функция определена в глобальной области видимости или в области видимости функции, её судьба предопределена: (прим. перев. — внутри движка JavaScript) она является свойством (property) либо объекта Activation, либо объекта Global. Подобные сущности имеют набор внутренних атрибутов, одним из которых и является упомянутый ранее DontDelete. Переменные и объявления функций, замеченные движком в глобальной области или в коде функций, создаются с атрибутом DontDelete и поэтому не могут быть удалены.

// глобальная переменная:
var a = 1; // установлен DontDelete
delete a; // false
a; // 1

// обычная функция:
function f() {} // установлен DontDelete
delete f; // false
typeof f; // "function"

// переопределение не помогает:
f = 1;
delete f; // false
f; // 1

Установленные пользователем свойства

Свойства, установленные явно, могут быть безпрепятственно удалены:

// явно установим свойства:
var obj = {x: 1};
obj.y = 2;
delete obj.x; // true
delete obj.y; // true
obj.x; // undefined
obj.y; // undefined

В приведённом примере свойства obj.x и obj.y могут быть удалены, поскольку у них отсутствует атрибут DontDelete. По этой же причине работает и пример ниже:

// работает нормально, но не в IE
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true - просто глобальная переменная
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined

Здесь, чтобы удалить a, мы используем один трюк. В этом коде this ссылается на объект Global и мы явно описываем переменную a под видом его свойства, что и позволяет нам её успешно удалить.

Internet Explorer (по крайней мере, с 6-го по 8-й) содержит баги, из-за которых такой код не сработает.

Аргументы функций и встроенные свойства

Обычные аргументы функций, объект arguments, а также встроенные свойства, объединены общей особенностью: у всех у них установен атрибут DontDelete.

// аргументы функций и свойства:
(function (x) {

  delete arguments; // false
  typeof arguments; // "object"

  delete x; // false
  x; // 1

  function f(){}
  delete f.length; // false
  typeof f.length; // "number"

})(1);

Хост-объекты

(прим. перев. — Хост-объекты — это объекты, которые, в неком окружении, дополняют функциональность языка JavaScript, не являясь частью его спецификации. В случае браузера это объекты window, document, setTimeout и т.п.)

Поведение оператора delete может быть полностью непредсказуемым, когда вы примененяете его к таким хост-объектам. С позволения спецификации, хост-объекты вольны вести себя как им только вздумается.

Заключение

Оператор delete часто ведёт себя непредсказуемо и может использоваться относительно безопасно только для удаления пользовательских свойств у обычных объектов.

Другое

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 миллисекунд. И использовать его не рекомендуется.

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

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

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

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

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

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

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

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

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

Сброс таймаутов вручную

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

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

Сброс всех таймаутов

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

// сбрасываем «все» таймауты
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

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

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

// сбрасываем «все» таймауты
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
    clearTimeout(i);
}

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

Скрытое использование 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, рекомендуем прислушиваться к этим советам, но при этом не делать из этого крайность. Опыт — сын ошибок трудных, и иногда в борьбе с ошибками зарождается намного более детальное понимание предмета. Да, нужно избегать ошибок, но допускать их неосознанно — вполне нормально.

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

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

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

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

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