Авторы
Это руководство является результатом работы двух заядлых пользователей Stack Overflow: Иво Ветцель /Ivo Wetzel/ (автора текста) и Чжан И Цзян /Zhang Yi Jiang/ (дизайнера).
Это руководство является результатом работы двух заядлых пользователей 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
.
this;
Когда мы используем this
в глобальной области видимости, она просто ссылается на глобальный объект.
foo();
Внутри функции this
ссылается на глобальный объект.
test.foo();
Внутри метода this
ссылается на test
.
new foo();
Если перед вызовом функции присутствует ключевое слово new
, то данная функция будет действовать как конструктор. Внутри такой функции this
будет указывать на новый созданный Object
.
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
, да и благодаря ему вам действительно станет легче работать с приватными переменными, у него есть несколько недостатков:
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
, использовать её;Нередко можно столкнуться с таким неприятным последствием единого глобального пространства имён, как проблема с перекрытием имён переменных. В 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
вообще имеет один-единственный практический случай применения, который при всём при этом, неожиданно,.. не оказывается проверкой типа объекта.
Значение Класс Тип
-------------------------------------
"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. Данная документация очень поможет вам в определении этих узких мест.