Авторы
Это руководство является результатом работы двух заядлых пользователей Stack Overflow: Иво Ветцель /Ivo Wetzel/ (автора текста) и Чжан И Цзян /Zhang Yi Jiang/ (дизайнера).
Это руководство является результатом работы двух заядлых пользователей Stack Overflow: Иво Ветцель /Ivo Wetzel/ (автора текста) и Чжан И Цзян /Zhang Yi Jiang/ (дизайнера).
JavaScript Гарден распространяется под лицензией MIT и располагается на GitHub. Если вы найдёте ошибку или опечатку, пожалуйста сообщите нам о ней или запросите права на загрузку в репозиторий. Кроме того, вы можете найти нас в комнате JavaScript среди чатов Stack Overflow.
В JavaScript всё ведет себя, как объект, лишь за двумя исключениями — null
и undefined
.
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'
function Foo(){}
Foo.bar = 1;
Foo.bar; // 1
Неверно считать, что числовые литералы нельзя использовать в качестве объектов — это распространённое заблуждение. Его причиной является упущение в парсере JavaScript, благодаря которому применение точечной нотации к числу воспринимается им как литерал числа с плавающей точкой.
2.toString(); // вызывает SyntaxError
Есть несколько способов обойти этот недостаток и любой из них можно использовать для того, чтобы работать с числами, как с объектами:
2..toString(); // вторая точка распознаётся корректно
2 .toString(); // обратите внимание на пробел перед точкой
(2).toString(); // двойка вычисляется заранее
Объекты в JavaScript могут использоваться как хеш-таблицы: подавляющей частью состоят из именованных свойств (ключей), привязанных к значениям.
Используя объектный литерал — нотацию {}
— можно создать простой объект. Новый объект наследуется от Object.prototype
и не имеет собственных свойств.
var foo = {}; // новый пустой объект
// новый объект со свойством 'test', имеющим значение 12
var bar = {test: 12};
Получить доступ к свойствам объекта можно двумя способами: используя либо точечную нотацию, либо запись квадратными скобками.
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // SyntaxError
foo['1234']; // работает
Обе нотации идентичны по принципу работы — одна лишь разница в том, что использование квадратных скобок позволяет устанавливать свойства динамически и использовать такие имена свойств, какие в других случаях могли бы привести к синтаксической ошибке.
Единственный способ удалить свойство у объекта — использовать оператор delete
; устанавливая свойство в undefined
или null
, вы только заменяете связанное с ним значение, но не удаляете ключ.
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
Приведённый код выведет две строки: bar undefined
и foo null
— на самом деле удалено было только свойство baz
и посему только оно будет отсутствовать в выводе.
var test = {
'case': 'Я — ключевое слово, поэтому меня надо записывать строкой',
delete: 'Я тоже ключевое слово, так что я' // не является ошибкой, бросает SyntaxError только в версиях ECMAScript ниже 5ой версии
};
Свойства объектов могут записываться как явно символами, так и в виде закавыченных строк. В связи с другим упущением в парсере JavaScript, этот код выбросит SyntaxError
во всех версиях ранее ECMAScript 5.
Источником ошибки является факт, что delete
— это ключевое слово и поэтому его необходимо записывать как строчный литерал: ради уверенности в том, что оно будет корректно опознано более старыми движками JavaScript.
От перев.: И еще один пример в пользу строковой нотации, это относится к JSON:
// валидный JavaScript и валидный JSON
{
"foo": "oof",
"bar": "rab"
}
// валидный JavaScript и НЕвалидный JSON
{
foo: "oof",
bar: "rab"
}
В JavaScript отсутствует классическая модель наследования — вместо неё используется прототипная модель.
Хотя её часто расценивают как один из недостатков JavaScript, на самом деле прототипная модель наследования намного мощнее классической. К примеру, поверх неё можно предельно легко реализовать классическое наследование, а попытки совершить обратное вынудят вас попотеть.
Из-за того, что JavaScript — практически единственный широко используемый язык с прототипным наследованием, придётся потратить некоторое время на осознание различий между этими двумя моделями.
Первое важное отличие заключается в том, что наследование в JavaScript выполняется с использованием так называемых цепочек прототипов.
function Foo() {
this.value = 42;
}
Foo.prototype.method = function() {}
function Bar() {}
// Зададим наследование от Foo
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.foo = 'Hello World';
// Убедимся, что Bar является действующим конструктором
Bar.prototype.constructor = Bar;
var test = new Bar() // создадим новый экземпляр bar
// Цепочка прототипов, которая получится в результате
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World', value: 42 }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* и т.д. */ }
В приведённом коде объект test
наследует оба прототипа: Bar.prototype
и Foo.prototype
; следовательно, он имеет доступ к функции method
которую мы определили в прототипе Foo
. Также у него есть доступ к свойству value
одного уникального экземпляра Foo
, который является его прототипом. Важно заметить, что код new Bar()
не создаёт новый экземпляр Foo
, а повторно вызывает функцию, которая была назначена его прототипом: таким образом все новые экземпляры Bar
будут иметь одинаковое свойство value
.
При обращении к какому-либо свойству объекта, JavaScript проходит вверх по цепочке прототипов этого объекта, пока не найдет свойство c запрашиваемым именем.
Если он достигнет верхушки этой цепочки (Object.prototype
) и при этом так и не найдёт указанное свойство, вместо него вернётся значение undefined.
prototype
То, что свойство prototype
используется языком для построения цепочек прототипов, даёт нам возможность присвоить любое значение этому свойству. Однако обычные примитивы, если назначать их в качестве прототипа, будут просто-напросто игнорироваться.
function Foo() {}
Foo.prototype = 1; // ничего не произойдёт
Foo.prototype = {
"foo":"bar"
};
При этом присвоение объектов, как в примере выше, позволит вам динамически создавать цепочки прототипов.
Поиск свойств, располагающихся относительно высоко по цепочке прототипов, может негативно сказаться на производительности, особенно в критических местах кода. Если же мы попытаемся найти несуществующее свойство, то поиск будет осуществлён вообще по всей цепочке, со всеми вытекающими последствиями.
Вдобавок, при циклическом переборе свойств объекта, будет обработано каждое свойство, существующее в цепочке прототипов.
Часто встречается неверное применение прототипов — расширение прототипа Object.prototype
или прототипов одного из встроенных объектов JavaScript.
Подобная практика нарушает принцип инкапсуляции и имеет соответствующее название — monkey patching. К сожалению, в основу многих широко распространенных фреймворков, например Prototype, положен принцип изменения базовых прототипов. Вам же стоит запомнить — от хорошей жизни прототипы встроенных объектов не меняют.
Единственным оправданием для расширения встроенных прототипов может быть только воссоздание возможностей более новых движков JavaScript, например функции Array.forEach
, которая появилась в версии 1.6.
Перед тем, как вы приступите к разработке сложных приложений на JavaScript, вы должны полностью осознать как работают прототипы, и как организовывать наследование на их основе. Также, помните о зависимости между длиной цепочек прототипов и производительностью — разрывайте их при необходимости. Кроме того — никогда не расширяйте прототипы встроенных объектов (ну, если только для совместимости с новыми возможностями Javascript).
hasOwnProperty
Если вам необходимо проверить, определено ли свойство у самого объекта, а не в его цепочке прототипов, вы можете использовать метод hasOwnProperty
, который все объекты наследуют от Object.prototype
.
hasOwnProperty
— единственная функция в JavaScript, которая позволяет получить свойства объекта без обращения к цепочке его прототипов.
// испортим Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
Только используя hasOwnProperty
можно гарантировать правильный результат при переборе свойств объекта. И нет иного способа для определения свойств, которые определены в самом объекте, а не где-то в цепочке его прототипов.
hasOwnProperty
как свойствоJavaScript не резервирует свойство с именем hasOwnProperty
. Так что, если есть потенциальная возможность, что объект может содержать свойство с таким именем, требуется использовать внешний вариант функции hasOwnProperty
чтобы получить корректные результаты.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Да прилетят драконы'
};
foo.hasOwnProperty('bar'); // всегда возвращает false
// Используем метод hasOwnProperty пустого объекта
// и передаём foo в качестве this
({}).hasOwnProperty.call(foo, 'bar'); // true
Единственным способом проверить существование свойства у объекта является использование метода hasOwnProperty
. При этом рекомендуется использовать этот метод в каждом цикле for in
вашего проекта, чтобы избежать возможных ошибок с ошибочным заимствованием свойств из прототипов родительских объектов. Также вы можете использовать конструкцию {}.hasOwnProperty.call(...)
на случай, если кто-то вздумает расширить прототипы встроенных объектов.
for in
Как и оператор in
, цикл for in
проходит по всей цепочке прототипов, обходя свойства объекта.
// Испортим Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // печатает и bar и moo
}
Так как изменить поведение цикла for in
как такового не представляется возможным, то для фильтрации нежелательных свойств объекта внутри этого цикла используют метод hasOwnProperty
из Object.prototype
.
hasOwnProperty
в качестве фильтра// возьмём foo из примера выше
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Это единственная версия правильного использования цикла. Благодаря использованию hasOwnProperty
будет выведено только свойство moo
. Если же убрать hasOwnProperty
, код становится нестабилен и могут возникнуть ошибки, особенно если кто-то изменил встроенные прототипы, такие как Object.prototype
.
Один из самых популярных фреймворков Prototype как раз этим и славится, и если вы его подключаете, то не забудьте использовать hasOwnProperty
внутри цикла for in
, иначе у вас гарантированно возникнут проблемы.
Рекомендация одна — всегда используйте hasOwnProperty
. Пишите код, который будет в наименьшей мере зависеть от окружения, в котором он будет запущен — не стоит гадать, расширял кто-то прототипы или нет и используется ли в ней та или иная библиотека.
Функции в JavaScript тоже являются объектами (шок, сенсация) — следовательно, их можно передавать и присваивать точно так же, как и любой другой объект. Одним из вариантов использования такой возможности является передача анонимной функции как функции обратного вызова в другую функцию — к примеру, для асинхронных вызовов.
function
// всё просто и привычно
function foo() {}
В следующем примере описанная функция резервируется перед запуском всего скрипта; за счёт этого она доступна в любом месте кода, вне зависимости от того, где она определена — даже если функция вызывается до её фактического объявления в коде.
foo(); // сработает, т.к. функция будет создана до выполнения кода
function foo() {}
function
как выражениеvar foo = function() {};
В этом примере безымянная и анонимная функция присваивается переменной foo
.
foo; // 'undefined'
foo(); // вызовет TypeError
var foo = function() {};
Так как в данном примере выражение var
— это определение функции, переменная с именем foo
будет заранее зарезервирована перед запуском скрипта (таким образом, foo
уже будет определена во время его работы).
Но поскольку присвоения исполняются непосредственно во время работы кода, foo
по умолчанию будет присвоено значение undefined
(до обработки строки с определением функции):
var foo; // переменная неявно резервируется
foo; // 'undefined'
foo(); // вызовет TypeError
foo = function() {};
Существует еще нюанс, касающийся именованных функций создающихся через присваивание:
var foo = function bar() {
bar(); // работает
}
bar(); // получим ReferenceError
Здесь объект bar
не доступен во внешней области, так как имя bar
используется только для присвоения переменной foo
; однако bar
можно вызвать внутри функции. Такое поведение связано с особенностью работы JavaScript с пространствами имен - имя функции всегда доступно в локальной области видимости самой функции.
this
В JavaScript область ответственности специальной переменной this
концептуально отличается от того, за что отвечает this
в других языках программирования. Различают ровно пять вариантов того, к чему привязывается this
в языке.
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]); // массив развернётся в a = 1, b = 2, c = 3
foo.call(bar, 1, 2, 3); // аналогично
Когда мы используем методы call
или apply
из Function.prototype
, то внутри вызываемой функции this
явным образом будет присвоено значение первого передаваемого параметра.
Исходя из этого, в предыдущем примере (строка с apply
) правило #3 вызов метода не будет применено, и this
внутри foo
будет присвоено bar
.
Хотя большинство из примеров ниже наполнены глубоким смыслом, первый из них можно считать ещё одним упущением в самом языке, поскольку он вообще не имеет практического применения.
Foo.method = function() {
function test() {
// this ссылается на глобальный объект
}
test();
};
Распространенным заблуждением будет то, что this
внутри test
ссылается на Foo
, но это не так.
Для того, чтобы получить доступ к Foo
внутри функции test
, необходимо создать локальную переменную внутри method
, которая и будет ссылаться на Foo
.
Foo.method = function() {
var that = this;
function test() {
// Здесь используем that вместо this
}
test();
};
Подходящее имя для переменной - that
, его часто используют для ссылки на внешний this
. В комбинации с замыканиями this
можно пробрасывать в глобальную область или в любой другой объект.
Еще одной фичей, которая не работает в JavaScript
, является создание псевдонимов для методов, т.е. присвоение метода объекта переменной.
var test = someObject.methodTest;
test();
Следуя первому правилу test
вызывается как обычная функция; следовательно this
внутри него больше не ссылается на someObject
.
Хотя позднее связывание this
на первый взгляд может показаться плохой идеей, но на самом деле именно благодаря этому работает наследование прототипов.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
В момент, когда будет вызван method
нового экземпляра Bar
, this
будет ссылаться на этот самый экземпляр.
Одним из самых мощных инструментов JavaScript'а считаются возможность создавать замыкания — это такой приём, когда наша область видимости всегда имеет доступ к внешней области, в которой она была объявлена. Собственно, единственный механизм работы с областями видимости в JavaScript — это функции: т.о. объявляя функцию, вы автоматически реализуете замыкания.
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
В данном примере Counter
возвращает два замыкания: функции increment
и get
. Обе эти функции сохраняют ссылку на область видимости Counter
и, соответственно, имеют доступ к переменной count
из этой самой области.
Поскольку в JavaScript нельзя присваивать или ссылаться на области видимости, заполучить count
извне не представляется возможным. Единственным способом взаимодействовать с ним остается использование двух замыканий.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
В приведенном примере мы не изменяем переменную count
в области видимости Counter
, т.к. foo.hack
не объявлен в данной области. Вместо этого будет создана или перезаписана глобальная переменная count
;
Часто встречается ошибка, когда замыкания используют внутри циклов, передавая переменную индекса внутрь.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Данный код не будет выводить числа с 0
до 9
, вместо этого число 10
будет выведено десять раз.
Анонимная функция сохраняет ссылку на i
и, когда будет вызвана функция console.log
, цикл for
уже закончит свою работу, а в i
будет содержаться 10
.
Для получения желаемого результата необходимо создать копию переменной i
.
Для того, чтобы скопировать значение индекса из цикла, лучше всего использовать анонимную функцию как обёртку.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Анонимная функция-обертка будет вызвана сразу же, и в качестве первого аргумента получит i
, значение которой будет скопировано в параметр e
.
Анонимная функция, которая передается в setTimeout
, теперь содержит ссылку на e
, значение которой не изменяется циклом.
Еще одним способом реализации является возврат функции из анонимной функции-обертки, поведение этого кода будет таким же, как и в коде из предыдущего примера.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
arguments
В области видимости любой функции в JavaScript есть доступ к специальной переменной arguments
. Эта переменная содержит в себе список всех аргументов, переданных данной функции.
Объект arguments
не является наследником Array
. Он, конечно же, очень похож на массив и даже содержит свойство length
— но он не наследует Array.prototype
, а представляет собой Object
.
По этой причине, у объекта arguments
отсутствуют стандартные методы массивов, такие как push
, pop
или slice
. Хотя итерация с использованием обычного цикла for
по аргументам работает вполне корректно, вам придётся конвертировать этот объект в настоящий массив типа Array
, чтобы применять к нему стандартные методы массивов.
Указанный код вернёт новый массив типа Array
, содержащий все элементы объекта arguments
.
Array.prototype.slice.call(arguments);
Эта конвертация занимает много времени и использовать её в критических частях кода не рекомендуется.
Ниже представлен рекомендуемый способ передачи аргументов из одной функции в другую.
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// делаем здесь что-нибудь
}
Другой трюк — использовать и call
и apply
вместе, чтобы быстро создать несвязанную обёртку:
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Создаём несвязанную версию "method"
// Она принимает параметры: this, arg1, arg2...argN
Foo.method = function() {
// Результат: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Объект arguments
создаёт по геттеру и сеттеру и для всех своих свойств и для формальных параметров функции.
В результате, изменение формального параметра также изменит значение соответствующего свойства объекта arguments
и наоборот.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Объект arguments
создаётся во всех случаях, лишь за двумя исключениями — когда он переопределён внутри функции (по имени) или когда одним из её параметров является переменная с таким именем. Неважно, используется при этом сам объект или нет.
Геттеры и сеттеры создаются всегда; так что их использование практически никак не влияет на производительность.
Однако, есть один момент, который может радикально понизить производительность современных движков JavaScript. Этот момент — использование arguments.callee
.
function foo() {
arguments.callee; // сделать что-либо с этим объектом функции
arguments.callee.caller; // и с вызвавшим его объектом функции
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // При обычных условиях должна бы была быть развёрнута...
}
}
В коде выше, функция foo
не может быть развёрнута (а могла бы), потому что для корректной работы ей необходима ссылка и на себя и на вызвавший её объект. Это не только кладёт на лопатки механизм развёртывания, но и нарушает принцип инкапсуляции, поскольку функция становится зависима от конкретного контекста вызова.
Крайне не рекомендуется использовать arguments.callee
или какое-либо из его свойств. Никогда.
Создание конструкторов в JavaScript также отличается от большинства других языков. Любая функция, вызванная с использованием ключевого слова new
, будет конструктором.
Внутри конструктора (вызываемой функции) this
будет указывать на новосозданный Object
. Прототипом этого нового объекта будет prototype
функции, которая была вызвана в качестве конструктора.
Если вызываемая функция не имеет явного возврата посредством return
, то вернётся this
— этот новый объект.
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
В этом примере Foo
вызывается в виде конструктора, следовательно прототип созданного объекта будет привязан к Foo.prototype
.
В случае, когда функция в явном виде возвращает некое значение используя return
, то в результате выполнения конструктора мы получим именно его, но только если возвращаемое значение представляет собой Object
.
function Bar() {
return 2;
}
new Bar(); // новый объект
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // возвращённый объект
Если же опустить ключевое слово new
, то функция не будет возвращать никаких объектов.
function Foo() {
this.bla = 1; // устанавливается глобальному объекту
}
Foo(); // undefined
Этот пример в некоторых случаях всё-таки может сработать: это связано с поведением this
в JavaScript — он будет восприниматься парсером как глобальный объект.
Если хотите избавится от необходимости использования new
, напишите конструктор, возвращающий значение посредством return
.
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
В обоих случаях при вызове Bar
мы получим один и тот же результат — новый объект со свойством method
(спасибо замыканию за это).
Также следует заметить, что вызов new Bar()
никак не связан с прототипом возвращаемого объекта. Хоть прототип и назначается всем новосозданным объектам, но Bar
никогда не возвращает этот новый объект.
В предыдущем примере нет функциональных отличий между вызовом конструктора с оператором new
или без него.
Часто не рекомендуют использовать new
, поскольку если вы его забудете, это может привести к ошибкам.
Чтобы создать новый объект, лучше использовать фабрику и создать новый объект внутри этой фабрики.
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.someMethod = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
Хотя данный пример и сработает, если вы забыли ключевое слово new
, и благодаря ему легче работать с приватными переменными, у него есть несколько недостатков
new
идёт вразрез с духом языка.Хотя забытое ключевое слово new
и может привести к багам, это точно не причина отказываться от использования прототипов. В конце концов, полезнее решить, какой из способов лучше совпадает с требованиями приложения: очень важно выбрать один из стилей создания объектов и после этого не изменять ему.
Хотя JavaScript нормально понимает синтаксис двух фигурных скобок, окружающих блок, он не поддерживает блочную область видимости; всё что остаётся на этот случай в языке — область видимости функций.
function test() { // область видимости
for(var i = 0; i < 10; i++) { // не область видимости
// считаем
}
console.log(i); // 10
}
Также JavaScript не знает ничего о различиях в пространствах имён: всё определяется в глобально доступном пространстве имён.
Каждый раз, когда JavaScript обнаруживает ссылку на переменную, он будет искать её всё выше и выше по областям видимости, пока не найдёт её. В случае, если он достигнет глобальной области видимости и не найдет запрошенное имя и там тоже, он ругнётся ReferenceError
.
// скрипт A
foo = '42';
// скрипт B
var foo = '42'
Вышеприведённые два скрипта не приводят к одному результату. Скрипт A определяет переменную по имени foo
в глобальной области видимости, а скрипт B определяет foo
в текущей области видимости.
Повторимся, это вообще не тот же самый эффект. Если вы не используете var
— то вы в большой опасности.
// глобальная область видимости
var foo = 42;
function test() {
// локальная область видимости
foo = 21;
}
test();
foo; // 21
Из-за того что оператор var
опущен внутри функции, фунция test
перезапишет значение foo
. Это поначалу может показаться не такой уж и большой проблемой, но если у вас имеется тысяча строк JavaScript-кода и вы не используете var
, то вам на пути встретятся страшные и трудноотлаживаемые ошибки — и это не шутка.
// глобальная область видимости
var items = [/* какой-то список */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// область видимости subLoop
for(i = 0; i < 10; i++) { // пропущенный оператор var
// делаем волшебные вещи!
}
}
Внешний цикл прекратит работу сразу после первого вызова subLoop
, поскольку subLoop
перезаписывает глобальное значение переменной i
. Использование var
во втором цикле for
могло бы вас легко избавить от этой ошибки. Никогда не забывайте использовать var
, если только влияние на внешнюю область видимости не является тем, что вы намерены получить.
Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var
.
// глобальная область видимости
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// локальная область видимости для функции test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
В то время как foo
и i
— локальные переменные в области видимости функции test
, присвоение bar
переопределит значение одноимённой глобальной переменной.
В JavaScript действует механизм всплытия определения. Это значит, что оба определения с использованием var
и определение function
будут перенесены наверх заключающей их области видимости.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
Этот код трансформируется ещё перед исполнением. JavaScript перемещает операторы var
и определение function
наверх ближайшей оборачивающей области видимости.
// выражения с var переместились сюда
var bar, someValue; // по умолчанию - 'undefined'
// определение функции тоже переместилось
function test(data) {
var goo, i, e; // потерянная блочная область видимости
// переместилась сюда
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // вылетает с ошибкой TypeError,
// поскольку bar всё ещё 'undefined'
someValue = 42; // присвоения не подвержены всплытию
bar = function() {};
test();
Потерянная область видимости блока не только переместит операторы var
вовне циклов и их тел, но и сделает результаты некоторых конструкций с if
неинтуитивными.
В исходном коде оператор if
изменял глобальную переменную goo
, когда, как оказалось, он изменяет локальную переменную — в результате работы всплытия.
Если вы не знакомы со всплытием, то можете посчитать, что нижеприведённый код должен породить
ReferenceError
.
// проверить, проинициализована ли SomeImportantThing
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Но, конечно же, этот код работает: из-за того, что оператор var
был перемещён наверх глобальной области видимости
var SomeImportantThing;
// другой код может инициализировать здесь переменную SomeImportantThing,
// а может и нет
// убедиться, что она всё ещё здесь
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Все области видимости в JavaScript, включая глобальную области видимости, содержат специальную, определённую внутри них, переменную this
, которая ссылается на текущий объект.
Области видимости функций также содержат внутри себя переменную arguments
, которая содержит аргументы, переданные в функцию.
Например, когда JavaScript пытается получить доступ к переменной foo
в области видимости функции, он будет искать её по имени в такой последовательности:
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 имеет 2 различных способа сравнения значений объектов на равенство.
Оператор сравнения состоит из двух символов равенства: ==
Слабая типизированность языка JavaScript подразумевает приведение обеих переменных к одному типу для того, чтобы произвести сравнение.
"" == "0" // false
0 == "" // true
0 == "0" // true
false == "false" // false
false == "0" // true
false == undefined // false
false == null // false
null == undefined // true
" \t\r\n" == 0 // true
В таблице выше показаны результаты приведения типов и это главная причина, почему использование ==
повсеместно считается плохой практикой: оно приводит к трудностям в отслеживании ошибок из-за сложных правил преобразования типов.
Кроме того, приведение типов во время сравнения также влияет на производительность; например, строка должна быть преобразована в число перед сравнением с другим числом.
Оператор строгого равенства состоит из трёх символов равенства: ===
В отличие от обычного оператора равенства, оператор строгого равенства не выполняет приведение типов между операндами.
"" === "0" // false
0 === "" // false
0 === "0" // false
false === "false" // false
false === "0" // false
false === undefined // false
false === null // false
null === undefined // false
" \t\r\n" === 0 // false
Результаты выше более понятны и позволяют быстрее выявлять ошибки в коде. Это в определённой степени улучшает код, а также дает прирост производительности в случае, если операнды имеют различные типы.
Хотя оба оператора ==
и ===
заявлены как операторы равенства, они ведут себя по-разному, когда хотя бы один из операндов является Object
.
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
Здесь оба операнда сравниваются на идентичность, а не на равенство; то есть будет проверяться, являются ли операнды одним экземпляром объекта, так же как делает is
в Python и сравниваются указатели в С.
Крайне рекомендуется использовать только операторы строгого равенства. В случае, когда намечается преобразование типов, нужно сделать явное приведение и не оставлять их на совести языковых хитростей с преобразованиями.
typeof
Оператор typeof
(вместе с instanceof
) — это, вероятно, самая большая недоделка в JavaScript, поскольку, похоже, он поломан более, чем полностью.
Хотя instanceof
еще имеет ограниченное применение, typeof
на самом деле имеет только один практический случай применения, который при всём при этом не является проверкой типа объекта.
Значение Класс Тип
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function в Nitro/V8)
new RegExp("meow") RegExp object (function в Nitro/V8)
{} Object object
new Object() Object object
В таблице выше Тип представляет собой значение, возвращаемое оператором typeof
. Как хорошо видно, это значение может быть абсолютно любым, но не логичным результатом.
Класс представляет собой значение внутреннего свойства [[Class]]
объекта.
Для того, чтобы получить значение [[Class]]
, необходимо вызвать метод toString
у Object.prototype
.
Спецификация предоставляет только один способ доступа к значению [[Class]]
— используя Object.prototype.toString
.
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
is('String', 'test'); // true
is('String', new String('test')); // true
В примере выше Object.prototype.toString
вызывается со значением this, являющимся объектом, значение [[Class]]
которого нужно получить.
typeof foo !== 'undefined'
Выше проверяется, было ли foo
действительно объявлено или нет; просто обращение к переменной приведёт к ReferenceError
. Это единственное, чем на самом деле полезен typeof
.
Для проверки типа объекта настоятельно рекомендуется использоватьObject.prototype.toString
— это единственный надежный способ. Как показано выше в таблице типов, некоторые возвращаемые typeof
значения не определены в спецификации: таким образом, они могут отличаться в различных реализациях.
Кроме случая проверки, была ли определена переменная, typeof
следует избегать во что бы то ни стало.
instanceof
Оператор instanceof
сравнивает конструкторы двух операндов. Это полезно только когда сравниваются пользовательские объекты. Использование на встроенных типах почти так же бесполезно, как и оператор typeof.
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// Всего лишь присваиваем Bar.prototype объект функции Foo,
// но не экземпляра Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
instanceof
со встроенными типамиnew String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
Здесь надо отметить одну важную вещь: instanceof
не работает на объектах, которые происходят из разных контекстов JavaScript (например, из различных документов в web-браузере), так как их конструкторы и правда не будут конструкторами тех самых объектов.
Оператор instanceof
должен использоваться только при обращении к пользовательским объектам, происходящим из одного контекста JavaScript. Так же, как и в случае оператора typeof
, любого другого использования необходимо избегать.
JavaScript — слабо типизированный язык, поэтому преобразование типов будет применяться везде, где возможно.
// Эти равенства — истинны
new Number(10) == 10; // объект типа Number преобразуется
// в числовой примитив в результате неявного вызова
// метода Number.prototype.valueOf
10 == '10'; // Strings преобразуется в Number
10 == '+10 '; // Ещё чуток строко-безумия
10 == '010'; // и ещё
isNaN(null) == false; // null преобразуется в 0,
// который конечно же не NaN
// Эти равенства — ложь
10 == 010;
10 == '-10';
Для того, чтобы избежать этого, настоятельно рекомендуется использовать оператор строгого равенства. Впрочем, хотя это и позволяет избежать многих распространенных ошибок, существует ещё много дополнительных вопросов, которые возникают из-за слабости типизации JavaScript.
Конструкторы встроенных типов, например, Number
и String
ведут себя различным образом, в зависимости от того, вызываются они с ключевым словом new
или без.
new Number(10) === 10; // False, Object и Number
Number(10) === 10; // True, Number и Number
new Number(10) + 0 === 10; // True, из-за неявного преобразования
Использование встроенного типа, такого как Number
, в качестве конструктора создаёт новый экземпляр объекта Number, но при использовании без ключевого слова new
функция Number
будет вести себя как конвертер.
Кроме того, присутствие литералов или переменных, которые не являются объектами, приведет к еще большему насилию над типами.
Лучший вариант — это явное приведение к одному из трех возможных типов.
'' + 10 === '10'; // true
Путём добавления в начале пустой строки, значение легко приводится к строке.
+'10' === 10; // true
Используя унарный оператор плюс, можно преобразовать значение в число.
Используя оператор not (!
) дважды, значение может быть приведено к логическому (булеву) типу.
!!'foo'; // true
!!''; // false
!!'0'; // true
!!'1'; // true
!!'-1' // true
!!{}; // true
!!true; // true
eval
Функция eval
выполняет строку кода JavaScript в локальной области видимости.
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
Но eval
исполняется в локальной области видимости только тогда, когда он вызывается напрямую и при этом имя вызываемой функции именно eval
.
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
Любой ценой избегайте использования функции eval
. 99.9% случаев её "использования" могут достигаться без её участия.
eval
под прикрытиемОбе функции работы с интервалами времени setTimeout
и setInterval
могут принимать строку в качестве первого аргумента. Эта строка всегда будет выполняться в глобальной области видимости, поскольку eval
в этом случае вызывается не напрямую.
Кроме всего прочего, функция eval
— это проблема в безопасности, поскольку исполняется любой переданный в неё код; никогда не следует использовать её со строками из неизвестных или недоверенных источников.
Никогда не стоит использовать eval
: любое применение такого кода поднимает вопросы о качестве его работы, производительности и безопасности. Если вдруг для работы вам необходима eval
, эта часть должна тут же ставиться под сомнение и не должна использоваться в первую очередь — необходимо найти лучший способ, которому не требуются вызовы eval
.
undefined
и null
В JavaScript есть два отдельных типа для представления ничего
, при этом более полезным из них является undefined
.
undefined
undefined
— это тип с единственным возможным значением: undefined
.
Кроме этого, в языке определена глобальная переменная со значением undefined
, и эта переменная так и называется — undefined
. Не являясь константой, она не является и ключевым словом. Из этого следует, что её значение можно с лёгкостью переопределить.
Несколько случаев, когда возвращается undefined
:
undefined
(если она не изменена).return
.return
, которые ничего не возвращают.undefined
.undefined
Поскольку глобальная переменная undefined
содержит копию настоящего значения undefined
, присвоение этой переменной нового значения не изменяет значения типа undefined
.
Но при этом, чтобы сравнить что-либо со значением undefined
, прежде нужно получить значение самой переменной undefined
.
Чтобы защитить код от переопределения переменной undefined
, часто используется техника анонимной обёртки, которая использует отсутствующий аргумент.
var undefined = 123;
(function(something, foo, undefined) {
// в локальной области видимости `undefined`
// снова ссылается на правильное значене.
})('Hello World', 42);
Другой способ достичь того же эффекта — использовать определение внутри обёртки.
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
Единственная разница между этими вариантами в том, что последняя версия будет больше на 4 байта при минификации, а в первом случае внутри анонимной обёртки нет дополнительного оператора var
.
null
Хотя undefined
в контексте языка JavaScript чаще используется в качестве традиционного null, настоящий null
(и тип и литерал) является в большей или меньшей степени просто другим типом данных.
Он используется во внутренних механизмах JavaScript (например для определения конца цепочки прототипов за счёт присваивания Foo.prototype = null
). Но в большинстве случаев тип null
может быть заменён на undefined
.
Хоть JavaScript и имеет синтаксис, подобный языкам семейства C, он при этом не принуждает вас ставить точки с запятой в исходном коде — вы всегда можете их опустить.
При этом JavaScript — не язык без точек с запятой, они на самом деле нужны ему, чтобы он мог разобраться в вашем коде. Поэтому парсер JavaScript автоматически вставляет их в те места, где сталкивается с ошибкой парсинга из-за их отсутствия.
var foo = function() {
} // ошибка разбора, ожидается точка с запятой
test()
Происходит вставка и парсер пытается снова.
var foo = function() {
}; // ошибки нет, парсер продолжает
test()
Автоматическая вставка точек с запятой считается одним из наибольших упущений в проекте языка, поскольку она может изменить поведение кода.
Приведённый код не содержит точек с запятой, так что места для их вставки остаются на совести парсера:
(function(window, undefined) {
function test(options) {
log('тестируем!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'здесь передадим длинную строчку',
'и ещё одну на всякий случай'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
Ниже представлен результат игры парсера в "угадалки".
(function(window, undefined) {
function test(options) {
// не вставлена точка с запятой, строки были объединены
log('тестируем!')(options.list || []).forEach(function(i) {
}); // <- вставлена
options.value.test(
'здесь передадим длинную строчку',
'и ещё одну на всякий случай'
); // <- вставлена
return; // <- вставлена, в результате
// оператор return разбит на два блока
{ // теперь парсер считает этот блок отдельным
// метка и одинокое выражение
foo: function() {}
}; // <- вставлена
}
window.test = test; // <- вставлена
// снова объединились строки
})(window)(function(window) {
window.someLibrary = {}; // <- вставлена
})(window); //<- вставлена
Парсер радикально подменил поведение изначального кода, а в определённых случаях он сделал абсолютно неправильные выводы.
Если парсер встречает "висящую" скобку, то он не вставляет точку с запятой.
log('тестируем!')
(options.list || []).forEach(function(i) {})
Такой код трансформируется в строку
log('тестируем!')(options.list || []).forEach(function(i) {})
Чрезвычайно высоки шансы, что log
возвращает не функцию; таким образом, эта строка вызовет TypeError
с сообщением о том, что undefined не является функцией
.
Настоятельно рекомендуем никогда не забывать ставить точку с запятой; также рекомендуется оставлять скобки на одной строке с соответствующим оператором и никогда не опускать их для выражений с использованием if
/ else
. Оба этих совета не только повысят читабельность вашего кода, но и предотвратят от изменения поведения кода, произведённого парсером втихую.
setTimeout
и setInterval
Поскольку JavaScript поддерживает асинхронность, есть возможность запланировать выполнение функции, используя функции setTimeout
и setInterval
.
function foo() {}
var id = setTimeout(foo, 1000); // возвращает число > 0
Функция setTimeout
возвращает идентификатор таймаута и планирует вызвать foo
через, примерно, тысячу миллисекунд. Функция foo
при этом будет вызвана ровно один раз.
В зависимости от разрешения таймера в используемом для запуска кода движке JavaScript, а также с учётом того, что JavaScript является однопоточным языком и посторонний код может заблокировать выполнение потока, нет никакой гарантии, что переданный код будет выполнен ровно через указанное в вызове setTimeout
время.
Переданная первым параметром функция будет вызвана как глобальный объект — это значит, что оператор this
в вызываемой функции будет ссылаться на этот самый объект.
function Foo() {
this.value = 42;
this.method = function() {
// this ссылается на глобальный объект
console.log(this.value); // выведет в лог undefined
};
setTimeout(this.method, 500);
}
new Foo();
setInterval
setTimeout
вызывает функцию единожды; setInterval
— как и предполагает название — вызывает функцию каждые X
миллисекунд. И его использование не рекомендуется.
В то время, когда исполняющийся код будет блокироваться во время вызова с таймаутом, setInterval
будет продолжать планировать последующие вызовы переданной функции. Это может (особенно в случае небольших интервалов) повлечь за собой выстраивание вызовов функций в очередь.
function foo(){
// что-то, что выполняется одну секунду
}
setInterval(foo, 100);
В приведённом коде foo
выполнится один раз и заблокирует этим главный поток на одну секунду.
Пока foo
блокирует код, setInterval
продолжает планировать последующие её вызовы. Теперь, когда первая foo
закончила выполнение, в очереди будут уже десять ожидающих выполнения вызовов foo
.
Самый простой и контролируемый способ — использовать setTimeout
внутри самой функции.
function foo(){
// что-то, выполняющееся одну секунду
setTimeout(foo, 100);
}
foo();
Такой способ не только инкапсулирует вызов setTimeout
, но и предотвращает от очередей блокирующих вызовов и при этом обеспечивает дополнительный контроль. Сама функция foo
теперь принимает решение, хочет ли она запускаться ещё раз или нет.
Удаление таймаутов и интервалов работает через передачу соответствующего идентификатора либо в функцию clearTimeout
, либо в функцию clearInterval
— в зависимости от того, какая функция set...
использовалась для его получения.
var id = setTimeout(foo, 1000);
clearTimeout(id);
Из-за того, что встроенного метода для удаления всех таймаутов и/или интервалов не существует, для достижения этой цели приходится использовать брутфорс.
// удаляем "все" таймауты
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
Вполне могут остаться таймауты, которые не будут захвачены этим произвольным числом; так что всё же рекомендуется следить за идентификаторами всех создающихся таймаутов, за счёт чего их можно будет удалять индивидуально.
eval
setTimeout
и setInterval
могут принимать строку в качестве первого параметра. Эту возможность не следует использовать никогда, поскольку изнутри при этом производится скрытый вызов eval
.
function foo() {
// будет вызвана
}
function bar() {
function foo() {
// никогда не будет вызывана
}
setTimeout('foo()', 1000);
}
bar();
Поскольку eval
в этом случае не вызывается напрямую, переданная в setTimeout
строка будет выполнена в глобальной области видимости; так что локальная переменная foo
из области видимости bar
не будет выполнена.
По этим же причинам рекомендуется не использовать строку для передачи аргументов в функцию, которая должна быть вызвана из одной из двух функций, работающих с таймаутами.
function foo(a, b, c) {}
// НИКОГДА не делайте такого
setTimeout('foo(1,2, 3)', 1000)
// Вместо этого используйте анонимную функцию
setTimeout(function() {
foo(1, 2, 3);
}, 1000)
Никогда не используйте строки как параметры setTimeout
или setInterval
. Это явный признак действительно плохого кода. Если вызываемой функции необходимо передавать аргументы, лучше передавать анонимную функцию, которая самостоятельно будет отвечать за сам вызов.
Кроме того, избегайте использования setInterval
в случаях, когда его планировщик может блокировать выполнение JavaScript.
Авторы этой документации требуют от читателя не совершать каких-либо ошибок и постоянно следить за качеством пишущегося кода. Мы, как переводчики и опытные программисты на JavaScript, рекомендуем прислушиваться к этим советам, но при этом не делать из этого крайность. Опыт — сын ошибок трудных, и иногда в борьбе с ошибками зарождается намного более детальное понимание предмета. Да, нужно избегать ошибок, но допускать их неосознанно — вполне нормально.
К примеру, в статье про сравнение объектов авторы настоятельно рекомендуют использовать только оператор строгого неравенства ===
. Но мы считаем, что если вы уверены и осознали, что оба сравниваемых операнда имеют один тип, вы имеете право опустить последний символ =
. Вы вольны применять строгое неравенство только в случаях, когда вы не уверены в типах операндов (!== undefined
— это полезный приём). Так в вашем коде будут опасные и безопасные области, но при этом по коду будет явно видно, где вы рассчитываете на переменные одинаковых типов, а где позволяете пользователю вольности.
Функцию setInterval
тоже можно использовать, если вы стопроцентно уверены, что код внутри неё будет исполняться как минимум в три раза быстрее переданного ей интервала.
С другой стороны, использование var
и грамотная расстановка точек с запятой — обязательные вещи, халатное отношение к которым никак не может быть оправдано — в осознанном пропуске var
(если только вы не переопределяете глобальный объект браузера... хотя зачем?) или точки с запятой нет никакого смысла.
Относитесь с мудростью к тому, что вы пишете — важно знать, как работает именно ваш код и как это соответствует приведённым в статье тезисам — и уже из этого вы сможете делать вывод, подходит ли вам тот или иной подход или нет. Важно знать, как работает прототипное наследование, но это не так необходимо, если вы используете функциональный подход или пользуетесь какой-либо сторонней библиотекой. Важно помнить о том, что у вас недостаёт какого-либо конкретного знания и что пробел следует заполнить, но если вы не используете в работе эту часть, вы всё равно можете писать хороший код — ну, если у вас есть талант.
Гонка за оптимизацией — это драматично и правильно, но лучше написать работающий и понятный вам код, а потом уже его оптимизировать и искать узкие места при необходимости. Оптимизацию необходимо делать, если вы видите явные неудобства для пользователя в тех или иных браузерах, или у вас один из тех супер-крупных проектов, которым никогда не помешает оптимизация, или вы работаете с какой-либо сверхтребовательной технологией типа WebGL. Данная документация очень поможет вам в определении этих узких мест.