Как найти прототип объекта

Object.getPrototypeOf()

Метод Object.getPrototypeOf() возвращает прототип (т.е. значение внутреннего свойства [[Prototype]] ) указанного объекта.

Try it

Syntax

Object.getPrototypeOf(obj)

Parameters

obj

Объект,прототип которого должен быть возвращен.

Return value

Прототип данного объекта, который может быть null .

Examples

Using getPrototypeOf

const proto = {};
const obj = Object.create(proto);
Object.getPrototypeOf(obj) === proto; 

Non-object coercion

В ES5 будет выдано исключение TypeError, TypeError параметр obj не является объектом. В ES2015 параметр будет приведен к Object .

Object.getPrototypeOf('foo');

Object.getPrototypeOf('foo');

Specifications

Browser compatibility

Desktop Mobile Server
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox для Android Opera Android Safari на IOS Samsung Internet Deno Node.js
getPrototypeOf

5

12

3.5

9

12.1

5

4.4

18

4

12.1

5

1.0

1.0

0.10.0

See also

  • Полифил Object.getPrototypeOf в core-js
  • Object.prototype.isPrototypeOf()
  • Object.setPrototypeOf()
  • Object.prototype.__proto__
  • Сообщение Джона Резига на getPrototypeOf
  • Reflect.getPrototypeOf()


JavaScript

  • Object.getOwnPropertyNames()

    Метод Object.getOwnPropertyNames()возвращает массив всех свойств (включая неперечислимые,за исключением тех,которые используют Symbol),найденных непосредственно

  • Object.getOwnPropertySymbols()

    Метод Object.getOwnPropertySymbols()возвращает массив всех свойств,найденных непосредственно у данного объекта The,свойства символов которого должны быть возвращены.

  • Object.hasOwn()

    Статический метод Object.hasOwn()возвращает true,если указанное свойство его Примечание:Object.hasOwn()предназначен для замены Object.hasOwnProperty().

  • Object.prototype.hasOwnProperty()

    Метод hasOwnProperty()возвращает булево значение,указывающее,указал ли объект свою противоположность наследованию Примечание:Object.hasOwn()рекомендуется использовать вместо hasOwnProperty(),

Время на прочтение
7 мин

Количество просмотров 32K

Лирическое вступление 

Получив в очередной раз кучу вопросов про прототипы на очередном собеседовании, я понял, что слегка подзабыл тонкости работы прототипов, и решил освежить знания. Я наткнулся на кучу статей, которые были написаны либо по наитию автора, как он “чувствует” прототипы, либо статья была про отдельную часть темы и не давала полной картины происходящего. 

Оказалось, что есть много неочевидных вещей из старых времён ES5 и даже ES6, о которых я не слышал. А еще оказалось, что вывод консоли браузера может не соответствовать действительности.

Что такое прототип

Объект в JS имеет собственные и унаследованные свойства, например, в этом коде:

var foo = { bar: 1 };
foo.bar === 1 // true
typeof foo.toString === "function" // true

у объекта foo имеется собственное свойство bar со значением 1, но также имеются и другие свойства, такие как toString. Чтобы понять, как объект foo получает новое свойство toString, посмотрим на то, из чего состоит объект:

Дело в том, что у объекта есть ссылка на другой объект-прототип. При доступе к полю foo.toString сначала выполняется поиск такого свойства у самого объекта, а потом у его прототипа, прототипа его прототипа, и так пока цепочка прототипов не закончится. Это похоже на односвязный список объектов, где поочередно проверяется объект и его объекты-прототипы. Так реализовано наследование свойств, например, у (почти, но об этом позже) любого объекта есть методы valueOf и toString.

Как выглядит прототип 

У всех прототипов имеются два общих свойства, constructor и __proto__. Свойство constructor указывает на функцию-конструктор, с помощью которой создавался объект, а свойство __proto__ указывает на следующий прототип в цепочке (либо null, если это последний прототип). Остальные свойства доступны через ., как в примере выше.

Да кто такой этот ваш constructor 

constructor – это ссылка на функцию, с помощью которой был создан объект: 

const a = {};
a.constructor === Object // true

Не совсем понятна идея зачем он был нужен, возможно, как способ клонирования объекта: 

object.constructor(object.arg)

Но я не нашел подходящий пример его использования, если у Вас есть примеры проектов, где это использовалось, то напишите об этом. В остальном же использовать constructor лучше не стоит, так как это writable свойство, которое можно случайно перезаписать, работая с прототипом, и сломать часть логики.

Где живёт прототип 

На самом деле, объекты представляют собой не только поля, доступные для JS кода. Интерпретатор также сохраняет некоторые приватные данные объекта для работы с ним, для этого в стандарте определено понятие внутренних слотов, которые обозначены как имя в квадратных скобках [[SlotName]]. Для прототипов отведен приватный слот [[Prototype]] содержащий ссылку на объект-прототип (либо null, если прототипа нет).

Из-за того, что [[Prototype]] предназначался исключительно для самого JS движка, получить доступ к прототипу объекта было невозможно. Для случаев когда это было нужно, ввели нестандартное свойство __proto__, которое поддержали многие браузеры и которое по итогу попало в сам стандарт, но как опциональное и стандартизированное только для обратной совместимости с существующим JS кодом.

О чем вам недоговаривает дебаггер, или он вам не прототип

Свойство __proto__ является геттером и сеттером для внутреннего слота [[Prototype]] и находится в Object.prototype:

Из-за этого я избегал записи __proto__ для обозначения прототипа. __proto__ находится не в самом объекте, что приводит к неожиданным результатам. Для демонстрации попробуем через __proto__ удалить прототип объекта и затем восстановить его:

const foo = {};
foo.toString(); // метод toString() берется из Object.prototype и вернет '[object Object]', пока все хорошо
foo.__proto__ = null; // делаем прототип объекта null
foo.toString(); // как и ожидалось появилась ошибка TypeError: foo.toString is not a function
foo.__proto__ = Object.prototype; // восстанавливаем прототип обратно
foo.toString(); // прототип не вернулся, ошибка TypeError: foo.toString is not a function

Как так получилось? Дело в том, что __proto__ – это унаследованное свойство Object.prototype, а не самого объекта foo. Из-за этого в момент когда в цепочке прототипов пропадает ссылка на Object.prototype, __proto__ превращается в тыкву и перестает работать с прототипом.
А теперь отработаем кликбейт из введения. Представим следующую цепочку прототипов:

var baz = { test: "test" };
var foo = { bar: 1 };
foo.__proto__ = baz;

В консоли Chrome foo будет выглядеть следующим образом:

А теперь уберем связь между baz и Object.prototype:

baz.__proto__ = null;

И теперь в консоли Chrome видим следующий результат:

Связь с Object.prototype разорвана у baz и __proto__ возвращает undefined даже у дочернего объекта foo, однако Chrome все равно показывает что __proto__ есть. Скорее всего тут имеется в виду внутренний слот [[Prototype]], но для простоты это было изменено на __proto__, ведь если не извращаться с цепочкой прототипов, это будет верно.

Как работать с прототипом объекта

Рассмотрим основные способы работы с прототипом: изменение прототипа и создание нового объекта с указанным прототипом.

Для изменения прототипа у существующего объекта есть всего два метода: использование сеттера __proto__ и метод Object.setPrototypeOf.

var myProto = { name: "Jake" };
var foo = {};
Object.setPrototypeOf(foo, myProto);
foo.__proto__ = myProto;

Если браузер не поддерживает ни один из этих методов, то изменить прототип объекта невозможно, можно только создать его копию с новым прототипом.
Но есть один нюанс с внутренним слотом [[Extensible]] который указывает на то, возможно ли добавлять к нему новые поля и менять его прототип. Есть несколько функций, которые выставляют этот флаг в false и предотвращают смену прототипа: Object.freeze, Object.seal, Object.preventExtensions. Пример:

const obj = {};
Object.preventExtensions(obj);
Object.setPrototypeOf(obj, Function.prototype); // TypeError: #<Object> is not extensible

А теперь менее категоричный вопрос создания нового объекта с прототипом. Для этого есть следующие способы.
Стандартный способ:

const foo = Object.create(myPrototype);

Если нет поддержки Object.create, но есть __proto__:

const foo = { __proto__: myPrototype };

И в случае если отсутствует поддержка всего вышеперечисленного:

const f = function () {}
f.prototype = myPrototype;
const foo = new f();

Способ основан на логике работы оператора new, о которой поговорим чуть ниже. Но сам способ основан на том, что оператор new берет свойство prototype функции и использует его в качестве прототипа, т.е. устанавливает объект в [[Prototype]], что нам и нужно.

Функции и конструкторы

А теперь поговорим про функции и как они работают в качестве конструкторов.

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

const user = new Person('John', 'Doe');

Функция Person тут является конструктором и создает два поля в новом объекте, а цепочка прототипов выглядит так:

Откуда взялся Person.prototype? При объявлении функции, у нее автоматически создается свойство prototype для того чтобы ее можно было использовать как конструктор (note 3), таким образом свойство prototype функции не имеет отношения к прототипу самой функции, а задает прототипы для дочерних объектов. Это позволит реализовывать наследование и добавлять новые методы, например так:

Person.prototype.fullName = function () {
    return this.firstName + ' ' + this.lastName;
}

И теперь вызов user.fullName() вернет строку “John Doe”.

Что такое new 

На самом деле оператор new не таит в себе никакой магии. При вызове new выполняет несколько действий:

  1. Создает новый объект self
  2. Записывает свойство prototype функции конструктора в прототип объекта self
  3. Вызывает функцию конструктор с объектом self в качестве аргумента this
  4. Возвращает self если конструктор вернул примитивное значение, иначе возвращает значение из конструктора

Все эти действия можно сделать силами самого языка, поэтому можно написать свой собственный оператор new в виде функции:

function custom_new(constructor, args) {
    // https://stackoverflow.com/questions/31538010/test-if-a-variable-is-a-primitive-rather-than-an-object
    function isPrimitive(val) {
        return val !== Object(val);
    }
    const self = Object.create({});
    const constructorValue = constructor.apply(self, args) || self;
    return isPrimitive(constructorValue) ? self : constructorValue;
}
custom_new(Person, ['John', 'Doe'])

Но начиная с ES6 волшебство пришло и к new в виде свойства new.target, которое позволяет определить, была ли вызвана функция как конструктор с new, или как обычная функция:

function Foo() {
    console.log(new.target === Foo);
}
Foo(); // false
new Foo(); // true

new.target будет undefined для обычного вызова функции, и ссылкой на саму функцию в случае вызова через new;

Наследование

Зная все вышеперечисленное, можно сделать классическое наследование дочернего класса Student от класса Person. Для этого нужно

  1. Создать конструктор Student с вызовом логики конструктора Person
  2. Задать объекту `Student.prototype` прототип от `Person`
  3. Добавить новые методы к `Student.prototype`

function Student(firstName, lastName, grade) {
    Person.call(this, firstName, lastName);
    this.grade = grade;
}

// вариант 1
Student.prototype = Object.create(Person.prototype, {
    constructor: {
        value:Student,
        enumerable: false,
        writable: true
    }
});
// вариант 2
Object.setPrototypeOf(Student.prototype, Person.prototype);

Student.prototype.isGraduated = function() {
    return this.grade === 0;
}

const student = new Student('Judy', 'Doe', 7);

Фиолетовым цветом обозначены поля объекта (они все находятся в самом объекте, т.к. this у всей цепочки прототипов один), а методы желтым (находятся в прототипах соответствующих функций)
Вариант 1 предпочтительнее, т.к. Object.setPrototypeOf может привести к проблемам с производительностью.

Сколько вам сахара к классу 

Для того чтобы облегчить классическую схему наследование и предоставить более привычный синтаксис, были представлены классы, просто сравним код с примерами Person и Student: 

class Person {
    constructor(firstName, lastName) {  
        this.firstName = firstName; 
        this.lastName = lastName;
    }

    fullName() {
        return this.firstName + ' ' + this.lastName;
    }
}

class Student extends Person {
    constructor(firstName, lastName, grade) {
        super(firstName, lastName);
        this.grade = grade;
    }

    isGraduated() {
        return this.grade === 0;
    }
}

Уменьшился не только бойлерплейт, но и поддерживаемость: 

  • В отличие от функции конструктора, при вызове конструктора без new выпадет ошибка
  • Родительский класс указывается ровно один раз при объявлении

При этом цепочка прототипов получается идентичной примеру с явным указанием prototype у функций конструкторов.

P. S.

Наивно было бы ожидать, что одна статья ответит на все вопросы. Если у Вас есть интересные вопросы, экскурсы в историю, аргументированные или беспочвенные заявления о том, что я сделал все не так, либо правки по ошибкам, пишите в комментарии. 

P. P. S.

К сожалению главный кликбейт статьи перестал быть актуальным. В данный момент Chrome (версия 93, на момент обновления статьи) перестал использовать __proto__ для обозначения прототипа, и теперь отображает его как слот [[Prototype]]:

image

Справедливости ради хочу отметить что в Firefox (92) также не используется обозначение __proto__:

image

В этой статье мы изучим всё что касается прототипов в JavaScript. Разберём зачем они нужны, что такое наследование и цепочка прототипов, как работает this внутри методов, рассмотрим пример расширения классов и многое другое.

Что такое прототипы?

При создании объектов, например, с помощью конструктора, каждый из них будет содержать специальное внутреннее свойство [[Prototype]], указывающее на его прототип. В JavaScript прототипы используются для организации наследования.

Допустим у нас имеется конструктор Box:

// конструктор Box
function Box(width, height) {
  this.width = width;
  this.height = height;
}

При объявлении конструктора или класса у него автоматически появится свойство prototype. Оно содержит прототип. Прототип – это объект. В данном случае им будет являться Box.prototype. Это очень важный момент.

Этот прототип будет автоматически назначаться всем объектам, которые будут создаваться с помощью этого конструктора:

// создание объекта с помощью конструктора Box
const box1 = new Box(25, 30);

Таким образом при создании объекта, в данном случае, box1, он автоматически будет иметь ссылку на прототип, то есть на свойство Box.prototype.

Это очень легко проверить:

Object.getPrototypeOf(box1) === Box.prototype // true
box1.__proto__ === Box.prototype // true

Прототип объекта в JavaScript

Получить прототип объекта в JavaScript можно с помощью статического метода Object.getPrototypeOf или специального свойства __proto__. В этом примере показаны два этих способа. Кстати, свойство __proto__ не является стандартным, но оно поддерживается всеми браузерами.

Свойство prototype имеется у каждой функции за исключением стрелочных. Это свойство как мы уже отмечали выше в качестве значения имеет объект. По умолчанию в нём находится только одно свойство constructor, которое содержит ссылку на саму эту функцию:

Box.prototype.constructor = Box // true

То есть Box.prototype.constructor – это сам конструктор Box.

Если создать ещё один объект класса Box, то он тоже будет иметь точно такой же прототип. Как мы уже отмечали выше прототипы в JavaScript используются для организации наследования. То есть, если мы сейчас в Box.prototype добавим какой-нибудь, например, метод, то он будет доступен для всех экземпляров класса Box:

// создадим конструктор Box
function Box(width, height) {
  this.width = width;
  this.height = height;
}
// добавим метод print в прототип Box
Box.prototype.print = function () {
  return `Box Size: ${this.width} x ${this.height}`
}
// создадим объекты
const box1 = new Box(25, 30);
const box2 = new Box(50, 70);
// выведем размеры ящиков в консоль
console.log(box1.print()); // Box Size: 25 x 30
console.log(box2.print()); // Box Size: 50 x 70

Прототип объекта на уровне родительского класса в JavaScript

Обратите внимание, что метода print нет у объектов box1 и box2. Но если раскрыть значение свойства [[Prototype]] в консоли в веб-браузере, то вы увидите его. То есть этот метод находится на уровне класса Box и наследуется всеми его экземплярами.

Соответственно получается, что мы можем вызвать print как метод объектов box1 и box2. Таким образом нам доступны не только собственные свойства и методы, но также наследуемые. А наследование, как вы уже понимаете, осуществляется в JavaScript на основе прототипов.

Так что же такое прототип? Прототип в JavaScript – это просто ссылка на объект, который используется для наследования.

Наследование

Что такое наследование? Если после переменной, содержащей некоторый объект поставить точку, то вы увидите все доступные для него свойства и методы:

Список доступных свойств и методов на уровне объекта box1

Здесь width и height – это его собственные свойства. Далее на уровне родительского класса находятся методы constructor и print. Т.е. вы можете вызвать метод print, потому что он наследуется всеми экземплярами класса Box. Кроме этого, здесь имеются методы класса Object, такие как hasOwnProperty, isPrototypeOf, toString и так далее. Эти методы тоже доступны, потому что Box.prototype наследует все свойства и методы Object.prototype.

Таким образом, кроме собственных свойств и методов объекту также доступны свойства и методы из прототипов. Такие свойства и методы называются наследованными.

Следовательно, в этом примере объект box1 имеет свои собственные свойства width и height, а также наследует все свойства и методы Box.prototype и Object.prototype.

Цепочка прототипов

В JavaScript наследование осуществляется только на уровне объектов через прототипы. То есть один объект имеет ссылку на другой через специальное внутреннее свойство [[Prototype]]. Тот в свою очередь тоже имеет ссылку и т.д. В результате получается цепочка прототипов.

Таким образом наследование, которые мы рассмотрели выше на примере объекта box1 происходит благодаря существованию следующей цепочки прототипов:

box1 -> Box.prototype -> Object.prototype

Заканчивается цепочка на прототипе глобального класса Object, потому что он не имеет прототипа, то есть его значение __proto__ равно null.

При этом, когда мы пытаемся получить доступ к некоторому свойству или методу этого объекта, поиск всегда начинается с самого объекта. Если данного свойства или метода у него нет, то поиск перемещается в прототип, потом в прототип прототипа и так далее.

Если указанное свойство или метод не найден, то возвращается undefined.

Например, если метод print мы добавим в сам объект box1, то будет использоваться уже он, а не тот, который находится в прототипе Box.prototype:

box1.print = function() {
  return `Размеры коробки: ${this.width} x ${this height}`;
}

Почему? Потому что поиск сразу прекращается, как только указанный метод будет найден. А в данном случае он будет найден сразу в объекте, поэтому переход в прототип не осуществится.

Значение this внутри методов

Значение this внутри методов определяется только тем для какого объекта мы его вызываем.

Рассмотрим следующий пример:

function Counter() {
  this.value = 0;
}
Counter.prototype.up = function() {
  this.value++;
  return this.value;
}
const counter1 = new Counter();
const counter2 = new Counter();
counter1.up(); // 1
counter1.up(); // 2
counter2.up(); // 1

Здесь мы вызываем up как метод объектов counter1 и counter2. Данный метод не является собственным для этих объектов, он наследуется и находится на уровне класса Counter. Но на самом деле это не имеет значения. Единственное, что важно для this – это только то, для какого объекта мы вызываем этот метод, то есть что стоит перед точкой. Это и будет this.

При вызове counter1.up(), this внутри этого метода будет указывать на counter1:

counter1.up();

На строчке перед точкой стоит counter2, значит this внутри up будет указывать на него:

counter2.up();

Таким образом, не важно, где находится метод, this внутри него всегда будет указывать на объект, для которого он вызывается, и, следовательно, с помощью this мы всегда сначала будем получать доступ к собственным свойствам и методам этого объекта.

Установка прототипа объекту

Установить прототип объекту можно с помощью статического метода Object.setPrototypeOf() или свойства __proto__.

Например, создадим два объекта и установим для второго объекта в качестве прототипа первый объект с помощью __proto__:

const person1 = {
  name: 'Tom',
  printName() {
    return `Name: ${this.name}`
  }
}
const person2 = {
  name: 'Bob',
  __proto__: person1
}

Проверить что прототипом для person2 является person1 очень просто:

person2.__proto__ === person1 // true
Object.getPrototypeOf(person2) === person1 // true

Установка объекта прототипа с помощью специального свойства __proto__

При этом метод printName становится наследуемым, то есть доступным для объекта person2:

person2.printName(); // "Name: Bob"

Пример установки прототипа с помощью Object.setPrototypeOf():

const message = {
  text: 'Сообщение 1...',
  color: 'black',
  getText() {
    return `<div style="color: ${this.color};">${this.text}</div>`;
  }
}
const errorMessage = {
  text: 'Сообщение 2...',
  color: 'red'
}
Object.setPrototypeOf(errorMessage, message);

Установка объекта прототипа с помощью Object.setPrototypeOf()

В этом примере мы в качестве прототипа для errorMessage установили message.

Чтобы было более понятно как работает метод Object.setPrototypeOf, рассмотрим его синтаксис:

Object.setPrototypeOf(obj, prototype)

Где:

  • obj – объект, для которого необходимо установить прототип;
  • prototype – объект, который будет использоваться в качестве прототипа для obj, или null, если у obj не должно быть прототипа.

Очень важный момент заключается в том, что мы не можем указать в качестве прототипа объект, который уже имеется в цепочке, то есть замкнуть её.

Следовательно, мы получим ошибку, если попытаемся для message установить в качестве прототипа errorMessage:

Object.setPrototypeOf(message, errorMessage);

Кроме этого, в JavaScript нет множественного наследования, то есть нельзя одному объекту назначить несколько прототипов.

Наследование классов

Допустим у нас имеется конструктор Person:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
person.prototype.getName = function() {
  return this.name;
}

Создадим конструктор Student, который будет расширять класс Person:

function Student(name, age, schoolName) {
  // вызываем функцию, передавая ей в качестве this текущий объект
  Person.call(this, name, age);
  this.schoolName = schoolName;
}
Student.prototype.getSchoolName = function() {
  return this.schoolName;
}

Но на данном этапе, они сейчас полностью независимы. Но для того чтобы класс Student расширял Person нужно указать, что прототипом для Student.prototype является Person.prototype. Например, выполним это с помощью свойства __proto__:

Student.prototype.__proto__ = Person.prototype;

Создадим новый экземпляр класса Student:

const student = new Student('Bob', 15, 'ABC School');

Для этого объекта будет доступен как метод getSchoolName, так и getName.

Рассмотрим ещё один очень интересный пример с наследованием классов:

// конструктор для создания объектов класса Rectangle
function Rectangle(x1, y1, x2, y2, bgColor) {
  Rectangle.counter++;
  this.id = `${this.constructor.name.toLowerCase()}-${Rectangle.counter}`;
  [this.left, this.top] = [Math.min(x1, x2), Math.min(y1, y2)];
  [this.width, this.height] = [Math.max(x1, x2) - this.left, Math.max(y1, y2) - this.top];
  this.bgColor = bgColor;
}
// конструктор для создания объектов класса Square
function Square(x, y, side, bgColor) {
  // вызываем конструктор Rectangle в контексте текущего создаваемого объекта
  Rectangle.call(this, x, y, x + side, y + side, bgColor);
}
// конструктор для создания объектов класса Circle
function Circle(x, y, r, bgColor) {
  Square.call(this, x, y, r, bgColor);
  this.radius = r;
}
// счетчик количества созданных объектов
Rectangle.counter = 0;
// установим для Square.prototype прототип Rectangle.prototype
Square.prototype.__proto__ = Rectangle.prototype;
// установим для Circle.prototype прототип Square.prototype
Circle.prototype.__proto__ = Square.prototype;
// объявляем метод draw в прототипе Rectangle
Rectangle.prototype.draw = function() {
  document.body.insertAdjacentHTML('beforeend', `<div id="${this.id}" style="position: absolute; left: ${this.left}px; top: ${this.top}px; width: ${this.width}px; height: ${this.height}px; background-color: ${this.bgColor};"></div>`);
}
// переопределяем метод draw в прототипе Circle
Circle.prototype.draw = function() {
  const el = Rectangle.prototype.draw.call(this);
  document.querySelector(`#${this.id}`).style.borderRadius = `${this.radius}px`;
}
// создадим новые объекты
const rectangle = new Rectangle(70, 80, 200, 300, '#673ab7');
const square = new Square(250, 50, 150, '#ff9800');
const circle = new Circle(450, 140, 90, '#4caf50');
// вызовем для каждого из них метод draw()
rectangle.draw();
square.draw();
circle.draw();

Здесь у нас имеются 3 класса: Rectangle, Square и Circle. Для того чтобы объекты класса Square наследовали свойства и методы Rectangle.prototype мы прописали следующую связь:

Square.prototype.__proto__ = Rectangle.prototype;

Похожим образом мы это сделали также для объектов класса Circle:

Circle.prototype.__proto__ = Square.prototype;

Таким образом, объекты, являющиеся экземплярами класса Circle наследуют свойства и методы Circle.prototype, Square.prototype, Rectangle.prototype и Object.prototype. Это происходит благодаря следующей цепочки прототипов:

circle -> Circle.prototype -> Square.prototype -> Rectangle.prototype -> Object.prototype

Для вызова в Square родительского конструктора мы используем метод call. С помощью call мы задаём контекст, в котором нам нужно вызвать функцию. В данном случае мы вызываем Rectangle в контексте текущего создаваемого объекта.

Свойство constructor

У каждой функции, кроме стрелочных, как уже отмечали выше по умолчанию, имеется свойство prototype. Как мы уже отмечали выше это прототип, который автоматически будут иметь все объекты, если мы эту функцию будет использовать как конструктор, то есть для создания объектов.

По умолчанию свойство prototype функции содержит следующий объект:

function Article(title) {
  this.title = title;
}
Article.prototype = {
  constructor: Article
}

Здесь мы свойству prototype присвоили объект вручную, но точно такой же генерируется автоматически. Этот объект изначально содержит только свойство constructor, которое указывает на сам конструктор, то есть на функцию.

Свойство constructor можно использовать для создания объектов:

const article1 = new Article('Про витамины');
const article2 = new article1.constructor('Про мёд');

Это может пригодиться, когда, например, объект был сделан вне вашего кода и вам необходимо создать новый подобный ему, то есть с использованием этого же конструктора.

Не рекомендуется полностью перезаписывать значение свойства prototype, потому что в этом случае вы потеряете constructor и его придётся добавлять вручную:

// так делать не нужно
Article.prototype = {
  getTitle() {
    return this.title;
  }
}

Если нужно что-то добавить в prototype, то делайте это как в примерах выше, то есть посредством простого добавления ему нужных свойств и методов:

Article.prototype.getTitle = function() {
  return this.title;
}

Встроенные прототипы

В JavaScript практически всё является объектами. То есть функции, массивы, даты и так далее. Исключением являются только примитивные типы данных: строка, число и так далее.

Например, при создании объекта { name: 'Tom' } внутренне используется конструктор Object:

const person = { name: 'Tom' }

Прототипом такого объекта соответственно становится Object.prototype и в этом легко убедиться:

person.__proto__ === Object.prototype // true

Поэтому нам на уровне этого объекта доступны различные методы, они берутся из Object.prototype. Например, метод hasOwnProperty:

person.hasOwnProperty('name'); // true

Этот метод возвращает true, когда указанное свойство является для этого объекта родным. В противном случае – false.

При этом Object.prototype является корнем иерархии других встроенных прототипов. Но при этом он сам не имеет прототипа.

JavaScript - Object.prototype

На рисунке видно, что конструктор Object имеет по умолчанию свойство prototype. Это значение будет автоматически записываться в свойство [[Prototype]] объектов, которые будет создаваться с помощью этого конструктора. В Object.prototype имеется свойство constructor, которые указывает на сам конструктор. Эти связи между Object и Object.prototype показаны на схеме. Кроме этого Object.prototype не имеет прототипа. То есть его значение [[Prototype]] содержит null.

Теперь давайте рассмотрим, как выполняется создание даты в JavaScript. Осуществляется это очень просто посредством конструктора Date:

const now = new Date();

Методы прототипа конструктора Date

Следовательно, прототипом даты является Date.prototype:

now.__proto__ === Date.prototype // true

Этот прототип содержит большое количество методов для работы с датой, например, такие как getDate, getHours и так далее. Их нет в now, но они доступны нам посредством наследования.

Объект Date.prototype имеет в качестве прототипа Object.prototype:

Date.prototype.__proto__ === Object.prototype // true
// или так
now.__proto__.__proto__ === Object.prototype // true

Следовательно, методы Object.prototype, которых нет в Date.prototype также доступны для now. Например, hasOwnProperty:

now.hasOwnProperty('getYear') // false

Таким образом можно нарисовать следующую схему:

JavaScript - Date.prototype

Другие встроенные объекты устроены подобным образом.

Метод Object.create

Object.create предназначен для создания нового объекта, который будет иметь в качестве прототипа объект, переданный в этот метод в качестве аргумента:

const rect1 = {
  a: 8,
  b: 5,
  calcArea() {
    return this.a * this.b
  }
}
// создали новый объект, который будет иметь в качестве прототипа rect1
const rect2 = Object.create(rect1);
rect2.a = 10;
rect2.b = 5;
rect2.calcArea(); // 50

Создание объекта с прототипом Object.prototype:

const obj = Object.create(Object.prototype);

Данный пример аналогичен этому:

const obj = {};

Создание объекта без прототипа:

const obj = Object.create(null);

Во 2 аргументе мы можем объекту сразу передать необходимые свойства. Описываются они в полном формате с использованием специальных атрибутов как в Object.defineProperties:

const person = Object.create(null, {
  name: {
    value: 'John'
  },
  age: {
    value: '18',
    writable: true
  }
});

Здесь мы описали два свойства: name и age. С помощью value мы устанавливаем значение свойству, а посредством аргумента writable задаем доступно ли свойство для изменения.

Пример, в котором мы с помощью Object.create установим для Book.prototype в качестве прототипа объект Product.prototype:

// конструктор Product
function Product(params) {
  this.name = params.name;
  this.price = params.price;
  this.discount = params.discount;
}
// конструктор Book
function Book(params) {
  Product.call(this, params);
  this.isbn = params.isbn;
  this.author = params.author;
  this.totalPages = params.totalPages;
}
// устанавливаем для Book.prototype в качестве прототипа объект Product.prototype
Book.prototype = Object.create(Product.prototype, {
  constructor: {
    value: Book
  },
  // геттер
  fullName: {
    get() {
      return `${this.author}: ${this.name}`;
    }
  }
});
Product.prototype.calcDiscountedPrice = function() {
  return (this.price * (1 - this.discount / 100)).toFixed(2);
}

// создадим новый объект
const book = new Book({
  name: 'Краткая история времени',
  author: 'Стивен Хокинг',
  isbn: '978-5-17102-284-6',
  totalPages: 232,
  price: 611.00,
  discount: 5
});
// цена книги со скидкой
console.log(book.calcDiscountedPrice()); // 580.45

Здесь мы Object.create используем для создания нового объекта, который будет использоваться в качестве прототипа для Book.prototype. При этом свою очередь этот новый объект будет иметь в качестве прототипа Product.prototype. Для этого мы передаем его в качестве аргумента методу Object.create. Кроме этого к этому объекту мы сразу же добавили два свойства: constructor и fullName. С помощью constructor мы восстановили конструктор, который был в прототипе пока мы ему не присвоили новое значение. А динамическое свойство fullName, представляющее собой геттер, будем использовать для получения имени книги с автором.

Что такое Объекты в JavaScript

Объекты (object) – это особенный тип в JS. Остальные типы называются примитивными, потому что значения примитивных типов могут быть только простыми значениями, например, строка или число. В объектах хранятся коллекции или более сложные структуры.

Объект может быть создан с помощью фигурных скобок {...} с необязательным списком свойств. Свойство – пара ключ-значение, где ключ – строка (имя свойства), а значение может быть чем угодно.

Сравним объект, например, с бейсболкой. У нее есть цвет, форма, вес, материал и т. д. Также и объект JS, содержит свойства, которые определяют его характеристики.

        let cap = new Object(); //конструктор объекта
let cap = new {}; //литерал объекта
let cap = Object.create({}); //создание объекта определенное стандартом ECMAScript 5
    

Чаще используют вариант с фигурными скобками {..}. Такое объявление называют литералом объекта или литеральной нотацией.

Литералы и свойства

Литерал объекта – заключенный в фигурные скобки список свойств (пар имя/значение) через запятую, например: (число(1), строка("строка"), объект({a:1, b:2}))

        object.property
    

Имя объекта и имена его свойств чувствительны к регистру. Свойства могут определяться в момент указания их значений, а также с помощью скобочной записи. Неопределенные свойства объекта – undefined, не null. Например, создадим бейсболку и зададим ее свойства:

        var cap = new Object();
cap.color = "Red";
cap["volume"] = 300;
    

Для удаления свойств можно использовать оператор delete:

        delete cap.color;
    

Имя свойства может состоять из нескольких слов, заключенных в кавычки:

        let cap = {
	background: "Red",
	"has image": true
}
    

Объекты, объявленные как константы (const), не могут быть изменены. Свойства таких объектов можно менять.

Например:

        const cap = {
	color: "Red"
}
cap.color = "Green"; // допустимо
alert(cap.color); // Green
cap = new Object(); // Ошибка

    

Ошибки доступа к свойствам

Как писалось выше, попытка обратиться к несуществующему свойству не является ошибкой – будет просто получено значение undefined. Однако если обратиться к свойству несуществующего объекта, то это будет считаться ошибкой.

Пример:

        var cap = { Color: "Red", Volume: 300 };
var a = cap.Text; // undefined
var b = cap.Text.length; // Ошибка TypeError

    

Для того чтобы защититься от исключения, рассмотрим следующий пример:

        // Наглядный и понятный вариант
var b = undefined;
if (cap) {
	if (cap.Text)
	{
		b = cap.Text.length;
	}
}

// Краткая форма
var b = cap && cap.Text && cap.Text.length;

    

Запись значений в свойства, доступные только для чтения к исключению, как правило, не приводят.

Пример:

        Object.prototype = {}; // исключения не будет, прототип не изменится
    

Когда запись значения свойству объекта будет неудачной:

  • Объект имеет собственное свойство, доступное только для чтения.
  • Объект имеет унаследованное свойство, доступное только для чтения.
  • Объект не имеет ни собственного, ни унаследованного свойства, которое требует изменить атрибут объекта extensible и имеет значение false.

Квадратные скобки

Как описывалось выше, со свойствами можно работать с помощью квадратных скобок. Особенно это полезно для свойств, которые состоят из нескольких слов, например:

        cap.has image = true; // ошибка
cap["has image"] = true; // корректно

    

При обращении к свойствам объекта с помощью квадратных скобок, можно также задавать, получать и удалять свойства:

        cap["color"] = "Red"; // присвоить значение свойству
alert(cap["color"]); // получить значение свойства
delete cap["color"]; // удалить свойство

    

Имя свойства могут быть выражениями и могут храниться в переменных, например:

        let propertyName = "color";
cap[propertyName] = "Red";

    

Вычисляемые свойства

Квадратные скобки также используются в литеральной нотации для создания вычисляемого свойства: мы можем задать имя свойства через переменную и потом работать с ним. Например, добавим бейсболке какое-нибудь свойство:

        let newProperty = prompt("Задайте имя нового свойства", "property");
	
let cap = {
	[newProperty]: 1,
};

alert(cap.property);// 1 если newProperty = "property"

    

Если вы поэкспериментируете с примером выше и вместо property зададите какое-нибудь другое значение, то результат будет undefined.

Квадратные скобки позволяют больше, нежели точка: можно рассчитывать имена свойств во время выполнения кода или задать его извне, как в примере выше.

Свойство из переменной

Возникает необходимость использовать переменные как значения свойств с тем же именем.

Пример:

        function makeCap(color, volume){
	return {
		color: color,
		volume: volume	
	};
}

let cap = makeCap("Red", 500);
alert(cap.color); // Red

    

В примере имена свойств color и volume совпадают с именами переменных, подставляемых в значения свойств. Существует синтаксический сахар для упрощенной записи.

Пример:

        function makeCap(color, volume){
	return {
		color,
		volume	
	};
}

    

Можно и еще короче:

        let cap = {
	color,
	volume: 500
};

    

Ограничения на имена свойств

В отличие от переменных, для свойств нет ограничений на именование, таких как for, let, return и т. д.

        let o = {
	for: "hello ",
	let: "proglib",
	return: ".io"
};
	
alert(o.for + o.let + o.return); // hello proglib.io

    

Ограничений для свойств нет также и по типу, т. е. они могут быть заданы строкой или символом, а типы, кроме строк, будут преобразованы к строке.

Пример:

        let o = {
	1: "Тест" // аналогично "1": "Тест"
};

alert(o["0"]); //Тест
alert(o[0]); //Тест
    

Исключением является специальное свойство __proto__. Ему нельзя установить не объектное значение:

        let o = {};
o.__proto__ = 1;
alert(o.__proto__); //[object Object]

    

В этом случае присвоение значения 1 игнорируется.

Проверка существования свойства – оператор in

JavaScript позволяет получить доступ к любому свойству, даже не существующему, в отличие от других языков – ошибки не будет.

При обращении к несуществующему свойству возвращается undefined. Проверить несуществующее свойство можно следующим образом:

        let cap = {};

alert(cap.color === undefined); //true – свойства нет

    

Для проверки существования свойства в объекте используется специальный оператор in.

Пример:

        let cap = { color: "Red", volume: 500 };

alert("volume" in cap); //true – свойство volume существует 
alert("weight" in cap); //false – свойство weight не существует

    

Слева от оператора in указывается имя свойства. Если в кавычках, то это имя свойства. Если без кавычек, то это имя переменной, содержащей имя свойства.

Пример:

        let cap = { color: "Red"};

let name = "color";
alert(name in cap); //true

    

Оператор in позволяет определить, что свойство у объекта именно есть, а не то, что его значение undefined.

Пример:

        let cap = {
	color: undefined
};

alert(cap.color); // undefined при использовании === было бы получено false
alert("color" in cap); // true

    

Данный пример – скорее исключение, в связи с тем, что undefined обычно не присваивают. Для пустых/неизвестных свойств используют null.

Цикл for…in

Цикл for...in используется для перебора всех свойств.

Пример:

        let cap = {
	color: "Red",
	volume: 300,
	hasImage: true
};

for (let name in cap)
{
	alert(name); //color, volume, hasImage
	alert(cap[name]); //Red. 300, true
}

    

В примере name – переменная, объявленная внутри цикла for.

Упорядочение свойств объекта

Свойства объекта упорядочены следующим образом: сначала идут целочисленные типы в порядке возрастания, остальные – в порядке создания.

Пример:

        let a = { "b":2, 3:"4", 1:"b"};
for(let c in a)
    	alert(a[c]); // b, 4, 2

    

Как видим в примере выше, сначала вывелись значения свойства 1 и 3, а потом значение свойства b.

Целочисленные свойства – такие свойства, которые могут быть преобразованы в число и обратно.

Пример:

        alert(String(Math.trunc(Number("1")))); // "1" целочисленный
alert(String(Math.trunc(Number("+1")))); // "1" != "+1" не целочисленное
alert(String(Math.trunc(Number("1.2")))); // "1" != "1.2" не целочисленное

    

Создание объектов в JavaScript

Создание объектов с помощью оператора new

Оператор new используется для создания и инициализации нового объекта. После оператора new указывается имя конструктора (функции, создающей объект).

Примеры встроенных конструкторов:

        var obj = new Object(); // {}
var arr = new Array(); // []
var date = new Date(); // объект Date

    

Также можно создать свой конструктор для инициализации своих объектов.

Пример:

        var cap = Object.create({color:"Red", volume: 300});

    

Пример:

        var obj = Object.create(Object.prototype);
    
        var obj = Object.create(null);

    

Прототипы

Прототип – объект, на основании которого создается другой объект, наследующий его свойства.

Наследование

Наследование – в JavaScript происходит от объекта прототипа. Для наследования свойств одного объекта другим применяется функция inherit().

При обращении к свойству объекта, поиск свойства сначала происходит по самому объекту. Если у объекта такого свойства нет, то производится попытка поиска в прототипе, далее в прототипе прототипа и так далее до тех пор, пока не будет найдено свойство, либо не достигнет прототипа, у которого нет прототипа.

Пример:

        var a = {}; // Прототипом объекта является Object.prototype
a.propA = 1; // Дадим ему собственное свойство propA

var b = inherit(a); //b наследует свойства a и Object.prototype
b.propB = 2; // Дадим ему собственное свойство propB

var c = inherit(b); // c наследует свойства a, b и Object.prototype
с.propC = 3; // Дадим ему собственное свойство propC

var str = c.toString(); // toString наследуется от Object.prototype
var sum = c.propA + c.propB; // 3 propA и propB наследуются от a и b

    

При присваивании значений свойству объекта проверяется, имеется ли это свойство у объекта или оно унаследовано. При наличии собственного свойства происходит запись значения, а при наличии унаследованного свойства происходит скрытие унаследованного свойства свойством с тем же именем.

При присвоении значения будут проверены все свойства в цепочке прототипов на допустимость. При наследовании свойств только для чтения, присвоение значения производиться для таких свойств не будет.

Пример:

        var a = { prop:1 }; 
var b = inherit(a); // наследует свойство prop
b.x = 1; 
b.y = 1; // определение собственных свойств
b.prop = 2; // переопределение унаследованного свойства

alert(a.prop); // 1, объект-прототип остался неизменным

    

Исключение

Если объект наследует свойство, доступ к которому осуществляется посредством метода, то вместо создания нового свойства производится вызов метода записи нового значения. Метод записи вызывается относительно объекта, но не его прототипа.

Атрибуты объекта

Объекты JavaScript имеют атрибуты prototype, class, extensible.

Атрибут prototype

Как уже рассматривалось выше, prototype объекта определяет объект, от которого наследуются свойства. Данный атрибут устанавливается в момент создания объекта.

Атрибут class

Пример:

        function classof(obj){
	if (obj === null) return "null";
	if (obj === undefined) return "undefined";
	return Object.prototype.toString.call(obj);
}

    

Функция из примера позволяет определить класс объекта.

Атрибут extensible

Данный атрибут отвечает за возможность добавлять объекту новые свойства. В ECMAScript 3 встроенные и определяемые пользователем объекты неявно допускали возможность расширения, расширяемость объектов среды выполнения определялась конкретной реализацией. В ECMAScript 5 встроенные и определяемые пользователем объекты являются расширяемыми до тех пор, пока не преобразованы в нерасширяемые объект. С объектами среды выполнения ситуация осталась той же.

Пример:

        var cap = { color: "Red", volume: 300 };
var json = JSON.stringify(cap); // json == '{"color":"Red","volume":300}'
var obj = JSON.parse(json); // Копия объекта cap

    

Формат JSON, к сожалению, не может применяться для представления всех возможных значений. Вот некоторые примечание касательно преобразуемых типов:

  • Объекты, массивы, строки, конечные числовые значения, true, false поддерживают двустороннее преобразование.
  • NaN, Infinity, -Infinity преобразуются в null.
  • Date преобразуются в строки с датами формата ISO, однако, обратно они восстанавливаются в виде строк.
  • Function, RegExp, Error, undefined – не могут быть сериализованы или восстановлены.

***

Материалы по теме

  • ☕ Учебник по JavaScript: ООП на простых примерах
  • ☕ Учебник по JavaScript: работа с датой и временем
  • ☕ Что в коробке? Как работают функции в JavaScript

Прототипы

  • Введение
  • Получение свойства объекта
    • [[Prototype]]
      • __proto__
      • Object.getPrototypeOf
      • Object.setPrototypeOf
    • Получение свойства из цепочки
    • Object.prototype
    • Итерация по объекту, проверка свойства, enumerable
      • Оператор in
      • Object.prototype.hasOwnProperty()
      • Цикл for..in
      • Итерация по собственным ключам
  • Установка свойства объекта, затенение свойств
    • Затенение свойств
    • Установка нового значения для свойства из цепочки

Введение ⇡

В этой лекции мы познакомимся с таким понятием как цепочка прототипов. Это мощный инструмент, используемый в JavaScript и позволяющий строить сложные программы.

Рассмотрим пример. Допустим, у нас есть необходимость хранить сущности студентов и лекторов.

Мы выделим сущность студента:

var student = {
    age: 20,
    name: 'John',
    sleep: function () {},
    getAge: function () {},
    getName: function () {}
}

А также сущность лектора:

var lecturer = {
    age: 33,
    name: 'Iwan',
    talk: function () {},
    getAge: function () {},
    getName: function () {}
}

У этих сущностей очень много общего, например, свойства getAge, getName. Но также есть и специфичные: sleep у студента и talk у лектора.

Обычно для выделения таких абстракций используются классы и их наследование друг от друга. Это отлично, за исключением одного маленького но. В JavaScript нет такого понятия, как классы. В нашем распоряжении только объекты и функции, которые тоже объекты.

Решить же эту задачу нам позволяет механизм цепочки прототипов. Цепочка прототипов позволяет делегировать свойства одного объекта другому. Мы выделяем абстракции, а потом делегируем свойства от частного объекта более общему.

Лекция будет состоять из двух основных блоков. Вначале мы познакомимся с тем, где нам могут встретиться цепочки прототипов и как работает этот механизм. Во второй части мы научимся строить эти цепочки, а также выделять абстракции, вынося в них общий повторяющийся код и общую функциональность.

Получение свойства объекта ⇡

Впервые, где мы сталкиваемся с работой прототипов, – это при обращении к свойствам или методам объекта.
Рассмотрим простой пример:

var student = { name: 'John' };
console.log(student.name);

Здесь мы хотим получить имя пользователя.

Когда мы обращаемся к свойству объекта, вызывается внутренний метод [[Get]], отвечающий за поиск и возврат значения.

В простом случае метод находит свойство на объекте и возвращает значение данного свойства. Например, в нашем примере у объекта student есть свойство name, поэтому вернется его значение и результат будет John.

Если же свойства нет, поиск на этом не останавливается. Тут-то на сцену и выходит цепочка прототипов.

[[Prototype]]

У каждого объекта в JavaScript есть внутренее свойство [[Prototype]], которое обычно указывает на другой объект или равно null. Это скрытое свойство и прямого доступа к нему нет.

var student = { age: 20 };
// student.[[Prototype]] -> Object.prototype

Как мы видим, по умолчанию при создании объекта свойство [[Prototype]] ссылается на объект Object.prototype, у которого в свою очередь тоже есть скрытое свойство [[Prototype]], равное null.

var student = { age: 20 };
// student.[[Prototype]] -> Object.prototype
// Object.prototype.[[Prototype]] -> null

Чтобы было проще, приму следующее обозначение. Если свойство [[Prototype]] объекта ссылается на другой объект, то я буду это показывать стрелкой с двумя дефисами:

var student = { age: 20 }; // student --> Object.prototype --> null

Есть несколько способов получить доступ к этому объекту.

__proto__

Ранее в спецификации ES3 не было описано прямого способа достучаться до этого свойства, поэтому некоторые браузеры изобрели не документированное свойство __proto__. Пользоваться им не рекомендуется, так как это свойство не поддерживается спецификацией и может отсутствовать в какой-нибудь среде.
Тем не менее, для отладки в консоли оно вполне подойдет.

var student = { age: 20 }; // student --> Object.prototype --> null

console.log(student.__proto__ === Object.prototype);   // true
console.log(Object.prototype.__proto__);            // null
console.log(student.__proto__.__proto__);              // null

Этот метод также является сеттером:

var person = { type: 'none' };  // person --> Object.prototype --> null
var student = { age: 20 };      // student --> Object.prototype --> null

student.__proto__ = person;     // student --> person --> Object.prototype --> null

console.log(student.__proto__ === person);   // true

Object.getPrototypeOf

В спецификации ES5 появился метод Object.getPrototypeOf для получения прототипа объекта:

var student = { age: 20 }; // student --> Object.prototype --> null

console.log(Object.getPrototypeOf(student) === Object.prototype);   // true
console.log(Object.getPrototypeOf(Object.prototype));               // null

Object.setPrototypeOf

А вот сеттер прототипа для объекта появился только в ES6, поэтому доступен не везде и пользоваться им не рекомендуется.
Однако, пока мы не познакомились с другими и более распротраненными способами управлять цепочкой прототипов, я буду использовать данный метод в примерах.

var person = { type: 'none' };  // person --> Object.prototype --> null
var student = { age: 20 };      // student --> Object.prototype --> null

Object.setPrototypeOf(student, person);  // student --> person --> Object.prototype --> null

console.log(Object.getPrototypeOf(student) === person);   // true

Получение свойства из цепочки ⇡

Итак, вернемся к методу [[Get]] и получению свойства объекта. Простой случай, когда свойство существует у объекта, мы уже рассмотрели. Теперь обратим внимание на случай, когда свойства нет.

Как я уже сказал, поиск не останавливается. И, наверно, раз уж мы столько времени уделили свойству [[Prototype]], как-то связан с ним.

Так и есть. Когда метод [[Get]] не находит свойства на объекте, то переходит к объекту, на который ссылается свойство [[Prototype]], и повторяет поиск. Проверяется, есть ли свойство на объекте (уже другом!), если есть – возвращается его значение, если нет – происходит переход к новому объекту через свойство [[Prototype]].

Процесс этот завершается либо когда свойство находится на каком-нибудь объекте, либо когда очередное свойство [[Prototype]] указывает на null. Если свойство не находится, то есть доходим до конца, метод [[Get]] возвращает undefined

Такая цепочка из ссылок [[Prototype]] называется цепочкой прототипов.

Примеры:

var person = { age: 0, type: 'none' };  // person --> Object.prototype --> null
var student = { age: 20 };              // student --> Object.prototype --> null

Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null

console.log(student.age);  // 20           (свойство найдено на самом объекте student)
console.log(student.type); // "none"       (свойство через цепочку объекте person)
console.log(student.some); // undefined    (свойство не найдено ни на объекте, ни в цепочке)

Object.prototype

Верхняя граница цепочки прототипов обычно объект Object.prototype. Свойство [[Prototype]] этого объекта указывает на null.

Объект Object.prototype имеет множество полезных методов: .toString(), .valueOf(), .hasOwnProperty(..), .isPrototypeOf(..). Если цепочка прототипов объекта содержит Object.prototype (а в большинстве случаев это именно так), то все эти методы доступны для объекта.

var student = { age: 20 }; // student --> Object.prototype --> null
var students = [];         // students --> Array.prototype --> Object.prototype --> null

console.log(student.hasOwnProperty('age'));        // true
console.log(students.hasOwnProperty('length'));    // true

В этом примере ни у объекта student, ни у students нет метода hasOwnProperty. Но они им доступны за счет того, что у них в цепочке есть объект Object.prototype. Это очень удобно!

Итерация по объекту, проверка свойства, enumerable ⇡

С правилами получения свойств объекта связаны такие операции, как проверка свойства через оператор in или итерация по ключаем объекта через цикл for..in

Оператор in

Для проверки наличия свойства у объекта используется оператор in. Данный оператор проверяет наличие свойства не только на самом объекте, но и в его цепочке прототипов.

var person = { type: 'none' };          // person --> Object.prototype --> null
var student = { age: 20 };              // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null

console.log('age' in student);             // true (из student)
console.log('type' in student);            // true (из person)
console.log('hasOwnProperty' in student);  // true (из Object.prototype)

Object.prototype.hasOwnProperty()

Как же проверить наличие свойства именно на самом объекте? Для этого служит функция Object.prototype.hasOwnProperty(). Она доступна объектам через цепочку прототипов.

var person = { type: 'none' };          // person --> Object.prototype --> null
var student = { age: 20 };              // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);    // student --> person --> Object.prototype --> null

console.log(student.hasOwnProperty('age'));            // true
console.log(student.hasOwnProperty('type'));           // false
console.log(student.hasOwnProperty('hasOwnProperty')); // false

Цикл for..in

Чтобы получить все свойства и методы объекта, используется for..in.

var person = { type: 'none' };              // person --> Object.prototype --> null
var student = { name: 'John', age: 20 };    // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);     // student --> person --> Object.prototype --> null

for(var key in student) {
    console.log(key);   // name, age, type
}

Как мы видим из примера, данный цикл также проходится по всей цепочке прототипов. Но почему же в выводе нет свойств из объекта Object.prototype? Ответ прост. Да, цикл for..in проходится по всей цепочке, но итерируется только по перечисляемым свойствам. Помните из лекции про объекты? Это те свойства, которые обладают свойством enumerable.

var student = { name: 'John', age: 20 };     // student --> Object.prototype --> null

// Объявляем неперечисляемое свойство
Object.defineProperty(student, 'type', {
  enumerable: false,
  value: 'admin'
});

for(var key in student) {
    console.log(key);   // name, age
}

Свойство type не вывелось, так как оно неперечисляемое.

Итерация по собственным ключам ⇡

Если смиксовать цикл for..in и метод Object.prototype.hasOwnProperty(), то можно добиться итерации только по собственным свойствам объекта:

var person = { type: 'none' };              // person --> Object.prototype --> null
var student = { name: 'John', age: 20 };    // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);     // student --> person --> Object.prototype --> null

for(var key in student) {
    if (student.hasOwnProperty(key)) {
        console.log(key);   // name, age
    }
}

Для облегчения этой задачи в ES5 появился метод Object.keys:

var person = { type: 'none' };              // person --> Object.prototype --> null
var student = { name: 'John', age: 20 };    // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);     // student --> person --> Object.prototype --> null

console.log(Object.keys(student))  // ["name", "age"]

Object.keys(student).forEach(function (key) {
    console.log(key);   // name, age
});

Установка свойства объекта, затенение свойств ⇡

Как работает механизм получения свойства, мы разобрались. Теперь разберемся с тем, как происходит установка значения. Присваиванием свойства занимается внутренний метод [[PUT]].

var student = { age: 20 };
student.age = 21;

console.log(student); // {age: 21}

В простом случае метод [[PUT]] находит свойство на объекте и устанавливает новое значение. Конечно, если это свойство не является read-only (writable: false).

var student = { name: 'John', age: 20 };     // person --> Object.prototype --> null

// Объявляем свойство read-only
Object.defineProperty(student, 'type', {
  writable: false,
  value: 'none'
});

student.type = 'admin';
console.log(student);  // {name: "John", age: 20, type: "none"}

Как видим в примере, свойство type не изменилось.

В строгом же режиме при присваивании значения read-only свойству выбросится исключение:
TypeError: Cannot assign to read only property 'type' of #<Object>

Если же свойства нет ни у объекта, ни в цепочке прототипов, то это свойство просто создается на объекте:

var student = { age: 20 };     // student --> Object.prototype --> null

student.name = 'John';

console.log(student);  // {age: 20, name: "John"}

Затенение свойств ⇡

Мы уже встречались с термином затенения. Сейчас познакомимся с ним в контексте цепочек прототипов. Рассмотрим пример, когда свйоство есть одновременно и у объекта, и в цепочке прототипов.

var person = { age: 0, type: 'none' };    // person --> Object.prototype --> null
var student = { age: 20 };                 // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);      // student --> person --> Object.prototype --> null

console.log(student.age);  // 20

В примере выше результатом будет 20. Это произошло, потому что метод [[GET]] возвращает первое найденое свойство. Получается, что свойство age объекта student собой заслоняет такое же свойство объекта person из цепочки прототипов. Это явление обычно называется затенением. Говорят, что свойство student.age затеняет свойство person.age.

Установка нового значения для свойства из цепочки ⇡

Вернемся к установке свойств. Мы рассмотрели два простых варианта, а именно: когда свойства не существует совсем и когда оно есть на объекте. Теперь рассмотрим более сложный случай, когда свойство есть в цепочке прототипов. Возникает три возможных сценария.

Свойство имеет стандартный сеттер и не помечено как read-only.
В этом случае создастся затеняющее свойство на объекте.

var person = { age: 0, type: 'none' };  // person --> Object.prototype --> null
var student = {};                       // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null

console.log(student.age);  // 0    (свойство объекта person, полученное по цепочке)

student.age = 20;
console.log(student.age);  // 20                       (свойство объекта student)
console.log(person);       // {age: 0, type: "none"}   (объект person не изменился)
console.log(student);      // {age: 20}                (у student создалось затеняющее свойство)

Свойство помечено как read-only
Поведение будет аналогичным тому, которое мы рассматривали c read-only свойством на объекте. В строгом режиме выбросится исключение, а в нестрогом просто ничего не произойдет.

var person = { type: 'none' };          // person --> Object.prototype --> null
var student = {};                       // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null

// Объявляем свойство read-only
Object.defineProperty(person, '__name__', {
  writable: false,
  value: 'person'
});

student.__name__ = 'non-person';   // В cтрогом режиме исключение TypeError
console.log(student);              // {}
console.log(student.__name__);     // "person"

В примере мы попытались изменить свойство __name__. Но у нас ничего не вышло: на объекте никаких свойств не появилось и объект в прототипе не изменился.

Свойство имеет кастомный сеттер
В этом случае вызовется кастомный сеттер, а свойства на объекте не создастся.

var person = { type: 'none' };              // person --> Object.prototype --> null
var student = {};                           // student --> Object.prototype --> null
Object.setPrototypeOf(student, person);     // student --> person --> Object.prototype --> null

// Объявляем кастомный сеттер
(function () {
    var NAME = 'person';

    Object.defineProperty(person, '__name__', {
        get: function() { return NAME; },
        set: function(newName) { NAME = newName }
    });
}());

student.__name__ = 'student';

console.log(student);           // {}
console.log(person.__name__);   // student

В примере выше мы попытались установить новое значение в свойство __name__. Вызвался наш кастомный сеттер, который исправил свойство __name__ у объекта person. Свойства же на объекте student не создалось.

Добавить комментарий