Введение
Сад JavaScript – коллекция документации по самым странным особенностям языка JavaScript. Тут собраны советы по тому, как избежать распространённых ошибок и малозаметных багов, а также проблем с быстродействием и неправильного стиля программирования.
Это не учебник по языку. Предполагается, что вы уже предварительно знаете язык. Для обучения языку рекомендую воспользоваться этим великолепным переводом чудесной книги "Выразительный JavaScript".
Объекты
Использование и свойства объектов
В JS все сущности ведут себя, как объекты, за двумя исключениями: null и undefined.
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'
function Foo(){}
Foo.bar = 1;
Foo.bar; // 1
Распространено ошибочное мнение, что числа невозможно использовать в качестве объектов. Из-за несовершенства парсера он пытается разобрать точку после числа как запись числа с плавающей точкой.
2.toString(); // вызывает SyntaxError
Это можно обойти по-разному:
2..toString(); // вторая точка срабатывает
2 .toString(); // пробел перед точкой
(2).toString(); // 2 вычисляется в первую очередь
Объекты как тип данных
Объекты можно использовать и как хэши (списки типа ключ-значение). Они в основном состоят из именованных свойств, к которым привязаны значения.
Используя {} можно создать пустой объект. Он наследует от Object.prototype и у него нет своих собственных свойств.
var foo = {}; // новый пустой объект
// новый объект со свойством 'test' , значение которого равно 12
var bar = {test: 12};
Доступ к свойствам
Доступ можно получить двумя путями – через точку или через квадратные скобки.
var foo = {name: 'котёнок'}
foo.name; // котёнок
foo['name']; // котёнок
var get = 'name';
foo[get]; // котёнок
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
};
Имена свойств объектов можно записывать и как простые переменные, и как строки. Из-за ещё одного недочёта парсера, код вызывает ошибку SyntaxError (для парсеров до версии ECMAScript 5). delete – это зарезервированное слово, поэтому его надо записывать как строку.
Прототипы
У JS нет классической модели наследования, вместо неё есть прототипирование. Это часто рассматривают как недостаток языка, но на самом деле эта модель мощнее классической. Например, достаточно просто построить классическую модель на базе прототипирования, но не наоборот.
JS – единственный популярный язык с прототипным наследованием, поэтому при переходе с классического подхода может потребоваться время на привыкание. Первое отличие: наследование в JS использует цепочки прототипов.
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Присвоить прототипу Bar новый экземпляр Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// удостовериться, что Bar указан как конструктор
Bar.prototype.constructor = Bar;
var test = new Bar(); // создать новый экземпляр Bar
// Итоговая цепь прототипов
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World' }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* etc. */ }
Примечание: если использовать Bar.prototype = Foo.prototype, тогда у обоих объектов будет единый прототип. Тогда изменения прототипа любого объекта приведут к изменениям другого, а обычно это не то, чего добиваются.
В приведённом коде объект test унаследован как от Bar.prototype, так и от Foo.prototype, поэтому у него будет доступ к функции method, определённой для Foo. А также доступ к свойству value экземпляра Foo, который является его прототипом. Важно отметить, что новый Bar() не создаёт новый экземпляр Foo, но использует повторно тот, что назначен его прототипу. Поэтому все экземпляры Bar совместно используют одно свойство value.
Не используйте Bar.prototype = Foo, он будет указывать не на прототип Foo, а на объект Foo. Так что цепь прототипов пройдёт через Function.prototype, а не через Foo.prototype. И тогда method не будет находиться в цепи прототипов.
Поиск свойства
При доступе к свойствам объекта JS проходит по цепи вверх, пока не находит свойство с заданным именем.
Если он дойдёт до верха, т.е. до Object.prototype, и не найдёт его, то вернёт undefined.
Свойство prototype
Хотя оно используется для построения цепочек, ему всё равно можно присвоить любое значение. Присвоение примитивов, тем не менее, будет проигнорировано.
function Foo() {}
Foo.prototype = 1; // да пофиг, ничего не будет
А вот присвоение объектов сработает, и приведёт к динамическому созданию цепочек прототипов.
Быстродействие
Поиск свойств, находящихся наверху цепочки, могут отнимать время. Кроме того, попытка достучаться до несуществующего свойства всегда будет приводить к проходу всей цепочки. Кроме того, при проходе свойство объекта каждому свойству, находящемуся в цепочке, будет присвоен номер.
Расширение встроенных прототипов
Иногда неправомерно используется расширение Object.prototype или какого-то другого встроенного прототипа. Это называется «обезьяньими заплатками» и такая техника нарушает инкапсуляцию. Хотя она и используется популярными фреймворками типа Prototype, особого оправдания такому засорению встроенных типов нет.
Единственная причина для возможного расширения встроенных прототипов – встроить в язык элементы новых движков JS, к примеру Array.forEach.
Итог
Важно понять модель прототипного наследования перед тем, как писать сложный код с её использованием. Помните о длине цепочек и разбивайте их при необходимости. Не надо расширять встроенные прототипы, если только это не делается для совместимости с новыми свойствами JS.
hasOwnProperty
Используется для проверки наличия свойства непосредственно у объекта, а не у одного из его прототипов. Наследуется всеми объектами от Object.prototype.
Недостаточно проверить свойство на undefined. Свойство может существовать, и при этом быть undefined.
hasOwnProperty – единственная вещь языка, которая работает со свойствами и не проходит по цепочке прототипов.
// Ломаем 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 для получения корректного результата.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Там живут драконы'
};
foo.hasOwnProperty('bar'); // всегда будет false
// Используйте hasOwnProperty от другого объекта и вызывайте его, когда 'this' равно foo
({}).hasOwnProperty.call(foo, 'bar'); // true
// Также можно использовать hasOwnProperty от Object prototype
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
Итог
Использование hasOwnProperty – единственный надёжный метод определения наличия свойства у объекта. Его использование рекомендуется во многих случаях при обходе свойство объектов.
Цикл for in
Как и оператор in, цикл for in проходит по цепочке прототипов при обходе свойств объекта. Цикл не будет идти по свойствам, у которых атрибут enumerable равен false – например, как у свойства length массивов.
// Испортим Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // выводит и bar и moo
}
Т.к. невозможно поменять работу for in, необходимо фильтровать ненужные свойства в теле цикла. С ECMAScript 3 для этого используется метод hasOwnProperty от Object.prototype
С ECMAScript 5 Object.defineProperty можно использовать, если enumerable установлено в false, чтобы добавлять объектам свойства без их нумерации. В этом случае разумно предполагать в коде, что любые нумеруемые свойства добавляются не просто так, и опускать hasOwnProperty, поскольку она уменьшает читаемость кода. В библиотечном коде hasOwnProperty использовать надо, потому что там нельзя делать предположения по поводу того, какие нумеруемые свойства могут находиться в цепочке прототипов.
Поскольку for in проходит по всей цепочке, с каждым слоем наследования он будет работать медленнее.
Использование hasOwnProperty для фильтрации
// foo из предыдущего примера
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Такой код можно использовать только со старыми версиями. Из-за использования hasOwnProperty будет выведено только moo. Если убрать hasOwnProperty, то в коде могут появиться ошибки из-за возможного расширения прототипов.
В новых версиях ECMAScript ненумеруемые свойства можно задавать через Object.defineProperty, и уменьшать риск прохода по свойствам без использования hasOwnProperty. И всё равно надо быть аккуратным, используя старые библиотеки типа Prototype, которые не используют возможности новых ECMAScript.
Итог
Рекомендуется всегда использовать hasOwnProperty в ECMAScript 3 и ниже, а также в коде библиотек. Начиная с ECMAScript 5, Object.defineProperty делает возможным определение ненумеруемых свойств, вследствие чего hasOwnProperty можно опускать.
Автор: SLY_G