Источник: http://bonsaiden.github.io/JavaScript-Garden/ru/#object
В 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
};
Свойства объектов могут записываться как явно символами, так и в виде закавыченных строк. В связи с другим упущением в парсере JavaScript, этот код выбросит SyntaxError
во всех версиях ранее ECMAScript 5.
Источником ошибки является факт, что delete
— это ключевое слово и поэтому его необходимо записывать как строчный литерал: ради уверенности в том, что оно будет корректно опознано более старыми движками JavaScript.
От перев.: И еще один пример в пользу строковой нотации, это относится к JSON. JSON (англ. JavaScript Object Notation) — текстовый формат обмена данными, основанный на JavaScript и обычно используемый именно с этим языком.
// валидный JavaScript и валидный JSON
{
"foo": "oof",
"bar": "rab"
}
// валидный JavaScript и НЕвалидный JSON
{
foo: "oof",
bar: "rab"
}
В JavaScript отсутствует классическая модель наследования — вместо неё используется прототипная модель.
Хотя её часто расценивают как один из недостатков JavaScript, на самом деле прототипная модель наследования намного мощнее классической. К примеру, поверх неё можно предельно легко реализовать классическое наследование, а попытки совершить обратное вынудят вас попотеть.
Из-за того, что JavaScript — практически единственный широко используемый язык с прототипным наследованием, придётся потратить некоторое время на осознание различий между этими двумя моделями.
Первое важное отличие заключается в том, что наследование в JavaScript выполняется с использованием так называемых цепочек прототипов.
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Установим значением прототипа Bar новый экземпляр Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// Убедимся, что Bar является действующим конструктором
Bar.prototype.constructor = Bar;
var test = new Bar() // создадим новый экземпляр bar
// Цепочка прототипов, которая получится в результате
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World' }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* и т.д. */ }
В приведённом коде объект test
наследует оба прототипа: Bar.prototype
и Foo.prototype
; следовательно, он имеет доступ к функции method
которую мы определили в прототипе Foo
. Также у него есть доступ к свойству value
одного уникального экземпляра Foo
, который является его прототипом. Важно заметить, что код new Bar()
не создаёт новый экземпляр Foo
, а повторно вызывает функцию, которая была назначена его прототипом: таким образом все новые экземпляры Bar
будут иметь одинаковое свойство value
.
Замечание: Никогда не используйте конструкцию Bar.prototype = Foo
, поскольку ссылка будет указывать не на прототип Foo
, а на объект функции Foo
. Из-за этого цепочка прототипов будет проходить через Function.prototype
, а не через Foo.prototype
и в результате функция method
не будет содержаться в цепочке прототипов.
При обращении к какому-либо свойству объекта, JavaScript проходит вверх по цепочке прототипов этого объекта, пока не найдет свойство c запрашиваемым именем.
Если он достигнет верхушки этой цепочки (Object.prototype
) и при этом так и не найдёт указанное свойство, вместо него вернётся значение undefined
.
То, что свойство 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
.
Примечание: Для проверки наличия свойства недостаточно проверять, эквивалентно ли оно undefined
. Свойство может вполне себе существовать, но при этом ему может быть присвоено значение undefined
.
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
проходит по всей цепочке прототипов, обходя свойства объекта.
Примечание: Цикл 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
как такового не представляется возможным, то для фильтрации нежелательных свойств объекта внутри этого цикла используют метод hasOwnProperty
из Object.prototype
.
Примечание: Цикл for in
всегда обходит всю цепочку прототипов полностью: таким образом, чем больше прототипов (слоёв наследования) в цепочке, тем медленнее работает цикл.
hasOwnProperty
в качестве фильтра// возьмём foo из примера выше
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Это единственная версия правильного использования цикла. Благодаря использованию hasOwnPropery
будет выведено только свойство moo
. Если же убрать hasOwnProperty
, код становится нестабилен и могут возникнуть ошибки, особенно если кто-то изменил встроенные прототипы, такие как Object.prototype
.
Один из самых популярных фреймворков Prototype как раз этим и славится, и если вы его подключаете, то не забудьте использовать hasOwnProperty
внутри цикла for in
, иначе у вас гарантированно возникнут проблемы.
Рекомендация одна — всегда используйте hasOwnProperty
. Пишите код, который будет в наименьшей мере зависеть от окружения, в котором он будет запущен — не стоит гадать, расширял кто-то прототипы или нет и используется ли в ней та или иная библиотека.