JavaScript Гарден — это постоянно обновляющаяся и растущая документация по самым заковыристым темам языка JavaScript. В ней вы найдёте советы о том, как избежать распространённых ошибок и предсказать появление тех или иных багов. В документации подробно освещены проблемы оптимизации и нерекомендуемые практики с которыми, продираясь к глубинам языка, могут столкнуться даже просвещённые JavaScript-программисты.
JavaScript Гарден не cтавит себе целью научить вас языку JavaScript. Вам понадобится реальный опыт работы с языком чтобы понимать темы, рассматриваемые в этом руководстве. Если вам требуется изучить основы языка, пожалуйста обратитесь к замечательному руководству на сайте Mozilla Developer Network.
Авторы
Это руководство является результатом работы двух заядлых пользователей Stack Overflow: Иво Ветцель /Ivo Wetzel/ (автора текста) и Чжан И Цзян /Zhang Yi Jiang/ (дизайнера).
Участники
- Кайо Ромао /Caio Romão/ (проверка орфографии)
- Андреас Бликст /Andreas Blixt/ (проверка грамматики)
Хостинг
JavaScript Garden хостится на GitHub, однако Cramer Development поддерживают наше зеркало на JavaScriptGarden.info.
Переводчики
- ‘shaman.sir’
- Антон Шевчук
- Максим Лозовой
- Елена Пашкова
- binariti очень помог со стилистикой текста
Лицензия
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
};
Ключи для свойств объектов могут записываться как посимвольно без кавычек, так и в виде закавыченных строк. В связи с другим упущением в парсере 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() {}
// Установим значением прототипа Bar новый экземпляр Foo
Bar.prototype = new Foo();
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' }
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
вашего проекта, дабы избежать потенциальных ошибок с неверным заимствованием свойств из прототипов встроенных объектов.
Цикл 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
будет ссылаться на этот самый экземпляр.
Замыкания и ссылки
Одним из самых мощных инструментов язка 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
.
В случае, когда функция в явном виде возвращает некое значение используя return
, в результате выполнения конструктора мы получим именно его, но только если возвращаемое значение представляет собой 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
и, возможно, благодаря ему вам станет легче работать с приватными переменными, у него есть несколько недостатков:
- Он использует больше памяти, поскольку созданные объекты не хранят методы в прототипе и соответственно для каждого нового объекта создаётся копия каждого метода.
- Чтобы эмулировать наследование, фабрике нужно скопировать все методы из другого объекта или установить прототипом нового объекта старый.
- Разрыв цепочки прототипов по мнимой необходимости избавиться от использования ключевого слова
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
в области видимости функции, он будет искать её по имени в такой последовательности:
- Если в текущей области видимости есть выражение
var foo
, использовать эту переменную. - Если один из параметров функции называется
foo
, использовать этот параметр. - Если функциия сама называется
foo
, использовать её. - Перейти на одну область видимости выше и повторить, начиная с п. 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 loop
.
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.toString() преобразуется
// обратно в число
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
(если она не была переопределена). - При попытке доступа к переменной, которая ещё не была инициализирована каким-либо значением.
- Неявный возврат из функции при отсутствии в ней оператора
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) она является свойством либо объекта 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
под видом его свойства, что позволяет нам её успешно удалить.
В IE (по крайней мере, с 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, 1000);
В приведённом примере foo
в первый же раз заблокирует своим процессом главный поток на одну секунду.
Пока foo
блокирует код, setInterval
продолжает планировать последующие её вызовы. Теперь, когда первая foo
закончила выполнение, в очереди будет уже десяток ожидающих выполнения вызовов foo
.
Разбираемся с потенциальной блокировкой кода
Самый простой и легко контролируемый способ — использовать setTimeout
внутри такой функции.
function foo(){
// что-то, выполняющееся одну секунду или более
setTimeout(foo, 1000);
}
foo();
Такой способ не только инкапсулирует вызов setTimeout
, но и предотвращает от очередей блокирующих вызовов и при этом обеспечивает дополнительный контроль: сама функция foo
теперь принимает решение, хочет ли она запускаться ещё раз или нет.
(прим. перев. — такая техника сейчас активно используется для привязывания браузерного метода requestAnimationFrame
к анимации, работающей на базе canvas
).
Сброс таймаутов вручную
Удалить таймаут или интервал можно посредством передачи соответствующего идентификатора либо в функцию 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(a, b, c);
}, 1000)
Заключение
Никогда не используйте строки как параметры для setTimeout
или setInterval
. Это явный признак действительно плохого кода. Если вызываемой функции необходимо передать аргументы, лучше передавать анонимную функцию, которая самостоятельно будет отвечать за сам вызов.
Кроме того, избегайте использования setInterval
, поскольку его планировщик не блокируется выполняемым кодом.
Пояснения
От переводчиков
Авторы этой документации требуют от читателя не совершать каких-либо ошибок и постоянно следить за качеством пишущегося кода. Мы, как переводчики и опытные программисты на JavaScript рекомендуем прислушиваться к этим советам, но при этом не делать из этого крайность. Опыт — сын ошибок трудных, и иногда в борьбе с ошибками зарождается намного более детальное понимание предмета. Да, нужно избегать ошибок, но допускать их неосознанно — вполне нормально.
К примеру, в статье про сравнение объектов авторы настоятельно рекомендуют использовать только оператор строгого неравенства ===
. Но мы считаем, что если вы уверены и осознали, что оба сравниваемых операнда имеют один тип, вы имеете право опустить последний символ =
. Вы вольны применять строгое неравенство только в случаях, когда вы не уверены в типах операндов (!== undefined
— это полезный приём). Так в вашем коде будут опасные и безопасные области, но при этом по коду будет явно видно, где вы рассчитываете на переменные одинаковых типов, а где позволяете пользователю вольности.
Функцию setInterval
тоже можно использовать, если вы стопроцентно уверены, что код внутри неё будет исполняться как минимум в три раза быстрее переданного ей интервала.
С другой стороны, использование var
и грамотная расстановка точек с запятой — обязательные вещи, халатное отношение к которым никак не может быть оправдано — в осознанном пропуске var
(если только вы не переопределяете глобальный объект браузера… хотя зачем?) или точки с запятой нет никакого смысла.
Относитесь с мудростью к тому, что вы пишете — важно знать как работает именно ваш код и как это соответствует приведённым в статье тезисам — и уже из этого вы сможете делать вывод, подходит ли вам тот или иной подход или нет. Важно знать как работает прототипное наследование, но это не так необходимо, если вы используете функциональный подход или пользуетесь какой-либо сторонней библиотекой. Важно помнить о том, что у вас недостаёт какого-либо конкретного знания и что пробел следует заполнить, но если вы не используете в работе эту часть, вы всё равно можете писать хороший код — ну, если у вас есть талант.
Гонка за оптимизацией — это драматично и правильно, но лучше написать работающий и понятный вам код, а потом уже его оптимизировать и искать узкие места, при необходимости. Оптимизацию необходимо делать, если вы видите явные неудобства для пользователя в тех или иных браузерах или у вас есть супер-крупный проект, которым никогда не мешает оптимизация, или вы работаете с какой-либо сверхтребовательной технологией типа WebGL. Данная документация очень поможет вам в определении этих узких мест.