Цикл for…in
Цикл for..in
проходит не только по собственным, но и по унаследованным свойствам объекта.
Например:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys возвращает только собственные ключи
alert(Object.keys(rabbit)); // jumps
// for..in проходит и по своим, и по унаследованным ключам
for(let prop in rabbit) alert(prop); // jumps, затем eats
Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода obj.hasOwnProperty(key): он возвращает true
, если у obj
есть собственное, не унаследованное, свойство с именем key
.
Пример такой фильтрации:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
В этом примере цепочка наследования выглядит так: rabbit
наследует от animal
, который наследует от Object.prototype
(так как animal
– литеральный объект {...}
, то это по умолчанию), а затем null
на самом верху:
Заметим ещё одну деталь. Откуда взялся метод rabbit.hasOwnProperty
? Мы его явно не определяли. Если посмотреть на цепочку прототипов, то видно, что он берётся из Object.prototype.hasOwnProperty
. То есть, он унаследован.
…Но почему hasOwnProperty
не появляется в цикле for..in
в отличие от eats
и jumps
? Он ведь перечисляет все унаследованные свойства.
Ответ простой: оно не перечислимо. То есть, у него внутренний флаг enumerable
стоит false
, как и у других свойств Object.prototype
. Поэтому оно и не появляется в цикле.Почти все остальные методы получения ключей/значений игнорируют унаследованные свойства
Почти все остальные методы, получающие ключи/значения, такие как Object.keys
, Object.values
и другие – игнорируют унаследованные свойства.
Они учитывают только свойства самого объекта, не его прототипа.
Итого
- В JavaScript все объекты имеют скрытое свойство
[[Prototype]]
, которое является либо другим объектом, либоnull
. - Мы можем использовать
obj.__proto__
для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены). - Объект, на который ссылается
[[Prototype]]
, называется «прототипом». - Если мы хотим прочитать свойство
obj
или вызвать метод, которого не существует уobj
, тогда JavaScript попытается найти его в прототипе. - Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное свойство, а не сеттер).
- Если мы вызываем
obj.method()
, а метод при этом взят из прототипа, тоthis
всё равно ссылается наobj
. Таким образом, методы всегда работают с текущим объектом, даже если они наследуются. - Цикл
for..in
перебирает как свои, так и унаследованные свойства. Остальные методы получения ключей/значений работают только с собственными свойствами объекта.
Практика
Куда будет произведена запись?
важность: 5
Объект rabbit
наследует от объекта animal
.
Какой объект получит свойство full
при вызове rabbit.eat()
: animal
или rabbit
?
let animal = {
eat() {
this.full = true;
}
};
let rabbit = {
__proto__: animal
};
rabbit.eat();
Решение
Ответ: rabbit
.
Поскольку this
– это объект, который стоит перед точкой, rabbit.eat()
изменяет объект rabbit
.
Поиск свойства и исполнение кода – два разных процесса. Сначала осуществляется поиск метода rabbit.eat
в прототипе, а затем этот метод выполняется с this=rabbit
.
Почему наедаются оба хомяка?
важность: 5
У нас есть два хомяка: шустрый (speedy
) и ленивый (lazy
); оба наследуют от общего объекта hamster
.
Когда мы кормим одного хомяка, второй тоже наедается. Почему? Как это исправить?
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// Этот хомяк нашёл еду
speedy.eat("apple");
alert( speedy.stomach ); // apple
// У этого хомяка тоже есть еда. Почему? Исправьте
alert( lazy.stomach ); // apple