[JavaScript] Прототипы в JS и малоизвестные факты
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Лирическое вступление
Получив в очередной раз кучу вопросов про прототипы на очередном собеседовании, я понял, что слегка подзабыл тонкости работы прототипов, и решил освежить знания. Я наткнулся на кучу статей, которые были написаны либо по наитию автора, как он "чувствует" прототипы, либо статья была про отдельную часть темы и не давала полной картины происходящего.
Оказалось, что есть много неочевидных вещей из старых времён 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.firstName;
}
И теперь вызов user.fullName() вернет строку "John Doe".
Что такое new
На самом деле оператор new не таит в себе никакой магии. При вызове new выполняет несколько действий:
- Создает новый объект
- Записывает свойство prototype функции конструктора в прототип объекта
- Вызывает функцию конструктор с объектом в качестве аргумента this
- Возвращает объект
Все эти действия можно сделать силами самого языка, поэтому можно написать свой собственный оператор new в виде функции:
function custom_new(constructor, args) {
const self = {};
Object.setPrototypeOf(self, constructor.prototype);
return constructor.apply(self, args) || self;
}
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. Для этого нужно
- Создать конструктор Student с вызовом логики конструктора Person
- Задать объекту `Student.prototype` прототип от `Person`
- Добавить новые методы к `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.
Наивно было бы ожидать, что одна статья ответит на все вопросы. Если у Вас есть интересные вопросы, экскурсы в историю, аргументированные или беспочвенные заявления о том, что я сделал все не так, либо правки по ошибкам, пишите в комментарии.
===========
Источник:
habr.com
===========
Похожие новости:
- [1С-Битрикс, JavaScript, PHP] Меняем страницу просмотра элемента универсальных списков в коробочном Битрикс24
- [Nginx, JavaScript, DevOps] Я сделал свой PyPI-репозитарий с авторизацией и S3. На Nginx
- [Разработка веб-сайтов, JavaScript, Математика] Математика верстальщику не нужна, или Временные функции и траектории для покадровых 2D анимаций на сайтах
- [JavaScript, Программирование, Разработка веб-сайтов] JavaScript: делегирование событий простыми словами (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, Отладка] Обработка ошибок в JavaScript
- [JavaScript, TypeScript] SEO npm-пакета: почему важно правильно настраивать конфиг и писать тесты
- [JavaScript, Браузеры, Разработка веб-сайтов] Про Shadow DOM
- [Google Cloud Platform, Google Chrome, Google Web Toolkit, Google API, JavaScript] Экстренная психологическая помощь | Prototyping Weekend
- [JavaScript, Node.JS, Программирование, Разработка веб-сайтов] Руководство по Express.js. Часть 2 (перевод)
- [JavaScript, Программирование] Как я искал работу в Берлине
Теги для поиска: #_javascript, #_javascript, #_prototype, #___proto__, #_class, #_nasledovanie_v_javascript (наследование в javascript), #_javascript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:53
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Лирическое вступление Получив в очередной раз кучу вопросов про прототипы на очередном собеседовании, я понял, что слегка подзабыл тонкости работы прототипов, и решил освежить знания. Я наткнулся на кучу статей, которые были написаны либо по наитию автора, как он "чувствует" прототипы, либо статья была про отдельную часть темы и не давала полной картины происходящего. Оказалось, что есть много неочевидных вещей из старых времён 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.firstName; } И теперь вызов user.fullName() вернет строку "John Doe". Что такое new На самом деле оператор new не таит в себе никакой магии. При вызове new выполняет несколько действий:
Все эти действия можно сделать силами самого языка, поэтому можно написать свой собственный оператор new в виде функции: function custom_new(constructor, args) {
const self = {}; Object.setPrototypeOf(self, constructor.prototype); return constructor.apply(self, args) || self; } 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. Для этого нужно
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; } } Уменьшился не только бойлерплейт, но и поддерживаемость:
При этом цепочка прототипов получается идентичной примеру с явным указанием prototype у функций конструкторов. P.S. Наивно было бы ожидать, что одна статья ответит на все вопросы. Если у Вас есть интересные вопросы, экскурсы в историю, аргументированные или беспочвенные заявления о том, что я сделал все не так, либо правки по ошибкам, пишите в комментарии. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:53
Часовой пояс: UTC + 5