Dubrowsky
Хроники одного дупла
Блогово  →  WebDev  → 

JavaScript: прототипы, конструкторы, .prototype, .constructor, .__proto__

05 Мая 2012 года

JavaScript - язык загадочный. Очень загадочный. Одно из самых загадочных его мест - прототипы и конструкторы. Это - две сосны, в которых очень легко заблудиться. Я попробую описать, как это устроено.

Сразу после того, как мы создали функцию, у нее есть свойство prototype, куда записан некоторый объект:

var Foo = function() {}
alert(Foo.prototype) // [object Object]

Правда, это свойство как тот суслик, который есть несмотря на то, что его не видишь:

var Foo = function() {}
Foo.prop = 'ololo';
for (var i in Foo) {
   alert('Foo.'+i + ' = ' + Foo[i])
}
// выведет "Foo.prop = ololo" 
// и НЕ выведет никакого Foo.prototype

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

var Foo = function() {}
for (var i in Foo.prototype) {
   alert('Foo.prototype.'+i + ' = ' + Foo.prototype[i])
}
alert('КОНЕЦ!')
// покажет только "КОНЕЦ!"

На самом деле в нем тоже сидит невидимый суслик, в виде свойства Foo.prototype.constructor, куда записана ссылка на саму функцию:

function Foo() {}
alert(Foo.prototype.constructor === Foo) // true

Когда же может пригодиться свойство Foo.prototype? Тогда, когда Foo используется в качестве конструктора, то есть с оператором new. Конструктор создает объекты, все это знают. В JavaScript это тоже так, но логика создания слегка запутанная. Вот перед нами простой с виду код:

var Foo = function() {}
var a = new Foo;
alert(a)

Любой пыхарь с уверенностью скажет "Мы создали объект а, являющийся экземпляром класса Foo". На самом деле в 7 символах "new Foo" заключена следующая магия:

  1. Мы создаем пустой объект (как если бы написали a = {})
  2. Прототипом этого объекта назначается то, что сидит в Foo.prototype
  3. Этот объект привязывается к this в теле Foo
  4. Исполняется функция Foo
  5. Оператор new возвращает созданный объект, если Foo не вернула что-либо другое.

"Прототип" из пункта 2 - это такой объект, где будут искаться свойства созданного объекта a, которые он сам в явном виде не содержит. Код ниже иллюстрирует все это более развернуто:

var Foo = function() {
   alert('я - фу!')
   this.prop = 'lol'
}

// добавляем свойство к Foo.prototype
Foo.prototype.bar = 'trololo'

var a = new Foo // исполняется Foo, видим алерт "я - фу"
// в переменную a записывается созданный объект
// когда Foo исполнялась, this указывало на него

alert(a.prop) // например, через this-ссылку мы добавили свойство prop
alert(a.bar) // а свойство bar берется из прототипа

ОК, вроде разобрались, что такое "прототип объекта", откуда он берется и зачем нужен. А как его посмотреть? В идеальном мире мы могли бы обратиться к obj.prototype и получить прототип объекта. Но увы, как мы видели выше, свойство с таким названием используется иначе - хранит то, что функция-конструктор сделает прототипом объекта. Напрашивается вывод, что можно получить прототип через .prototype конструктора. Попробуем:

var Foo = function() {}
Foo.prototype.prop = 'ololo'
var a = new Foo()
alert(a.prop); // берем из прототипа неявно
alert(a.constructor.prototype.prop); // а теперь - явно!

Вроде бы работает. Но что, если мы пытались изобразить наследование, расширив Foo другим "классом"?

function Foo() {}
function Bar() {
   this.prop = 'ololo'
}

Foo.prototype = new Bar // типа наследуемся
var a = new Foo
alert(a.prop) // работает
alert(a.constructor.prototype.prop) // undefined - ой :(
// а кстати, что у нас в a.constructor?
alert(a.constructor) // Bar!

Самое время задуматься, куда, собственно, указывает a.constructor. А, собственно, никуда не указывает, потому что у a нету такого свойства, оно берется из цепочки прототипов. В примере выше "прототип a" - это экземпляр Bar (потому что в Foo.prototype результат выполнения new Bar). У экземпляра Bar тоже нету свойства constructor. Зато оно есть у прототипа экземпляра Bar - потому что Bar.prototype (прототип экземпляра Bar) имеет свойство .constructor, указывающее на Bar ("свойство-суслик", о котором я писал в самом начале).

Если подумать, факап в этом примере случился из-за того, что мы "расширили" "класс" "экземпляром" (да, все - в кавычках!). Логичнее "расширять" "класс" "классом", но сути это не меняет - на obj.constructor.prototype в поисках прототипа мы полагаться не можем. Потому что obj.constructor - это не "функция, создавшая obj", а в лучшем случае - "функция, создавшая прототип obj" (так сформулировано в доках на MDN). В лучшем случае - потому что, вообще-то, там может быть что угодно:

// "Класс", создающий компанию с ключевыми сотрудниками
function Company (team) {
   for (var profession in team) {
      this[profession] = team[profession]
   }
}

var AvtoVAZ = new Company({
   president:'Игорь Комаров',
   constructor:'Сергей Кудрюк' // главный конструктор АвтоВАЗа
})

alert(AvtoVAZ.constructor)

// упаси Бог пытаться создать новую компанию так:
// var KAMAZ = new AvtoVAZ.constructor()

В общем, догадаться самостоятельно не получается, надо лезть в доки, маны и прочую матчасть. Там мы обнаруживаем совершенно курьезную ситуацию: JavaScript, прототипно-ориентированный язык, обзавелся нормальным способом получения прототипа только на 14-м году своей жизни, в версии 1.8.1! Этот способ называется Object.getPrototypeOf() и не поддерживается IE младше 9.

function Foo() {}
function Bar() {
   this.prop = 'boo'
}

var b = new Bar
Foo.prototype = b

var a = new Foo

// получаем прототип
var aproto = Object.getPrototypeOf(a)
alert(aproto.prop) // boo из экземпляра Bar

// меняем в прототипе
aproto.prop = 'woo'
alert(a.prop) // woo - "меняется" и в экземпляре

// меняем в прототипе неявно
b.prop = 'zoo'
alert(a.prop) // zoo - работает!
// потому что Foo.prototype, aproto и b - все указывают на один и тот же объект

До появления этой штуки у разработчиков было только свойство .__proto__, которое работает только в Mozilla-образных браузерах.

function Foo() {}
var proto = {prop:'woo'}
Foo.prototype = proto

var a = new Foo
alert(a.__proto__.prop) // свойство есть в прототипе

proto.prop = 'zoo'
alert(a.__proto__.prop) // поменяли прототип, отразилось на экземпляре

a.__proto__.prop = 'boo'
alert(proto.prop) // и в обратную сторону работает

Настало время для типа-саммари.

Что такое "прототип объекта obj"?
Это объект, к которому JS обращается за свойствами, которых нету у самого obj.

Что такое obj.prototype?
Если obj - это функция, то obj.prototype - то, что станет прототипом объекта при вызове new obj().


Что такое obj.constructor?
Если очень повезет - ссылка на функцию, создавшую obj, или прототип obj. А так вообще - что угодно :)

Как кроссбраузерно получить ссылку на прототип объекта?
Никак :) благо не часто нужно.

Что такое obj.__proto__?
Это прототип объекта obj, если у вас мозилла

Что такое Object.getPrototypeOf(obj)
Это способ получить ссылку на прототип объекта obj, который станет кроссбраузерным после смерти IE8 (ну FF до 3.5)

Зачем мне все это знать? о_0
Если создание сайтов - ваша работа, то... вам это все знать нафиг не нужно :) Если только для общей образованности, ну и чтоб голову поломать на досуге.

Камменты

Григорий23.05.2014, 14:09#
Ёклмн, я мозг сломал! )
Дуброн самый26.05.2014, 05:09#
Григорий, это похвально тащемта )
iXplo12.11.2014, 14:04#
написано оч бодро и не занудно, спасибо. за "ололо" и "трололо" отдельный респект. спасибо за неформальное и весёлое программирование ))
Дуброн самый16.11.2014, 01:51#
iXplo, рад, если пригодилось =)
Николай21.02.2015, 14:42#
"JavaScript - язык загадочный. Очень загадочный. Одно из самых загадочных его мест - прототипы и конструкторы."

Я бы сказал правильнее так - загадочное описание спецификаций JS. И конечно добавьте сюда низкое качества перевода (за что кстати нельзя винить только переводчиков).

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

К новым и нестандартным , но логичным в среде ООП понятиям , следует на мой взгляд отнести - полиморфизм.
Николай21.02.2015, 14:58#
"Сразу после того, как мы создали функцию, у нее есть свойство prototype, куда записан некоторый объект:"

Свойство prototype функция имеет всегда. Не забываем , функция сама является объектом.

В свойстве prototype записывается НЕ САМ ОБЪЕКТ , а ССЫЛКА на объект.
Николай21.02.2015, 15:34#
"ОК, вроде разобрались, что такое "прототип объекта", откуда он берется и зачем нужен. А как его посмотреть? В идеальном мире мы могли бы обратиться к obj.prototype и получить прототип объекта. Но увы, как мы видели выше, свойство с таким названием используется иначе - хранит то, что функция-конструктор сделает прототипом объекта."

Не знаю описка здесь или ошибка. Но следует обязательно отметить - у созданного с помощью функции объекта свойство с именем prototype тоже есть. Но это свойство указывает уже на следующий вышестоящий объект в цепочке прототипов (как правило Object).

Ссылка же в созданном объекте, указывющая на его прототип носит другое название. Объект говорит функции спасибо за наводку о местоположении прототипа, но называет эту ссылку по своему.
Николай21.02.2015, 15:39#
Не знаю описка здесь или ошибка. Но следует обязательно отметить - у созданного с помощью функции объекта свойство с именем prototype тоже есть. Но это свойство указывает уже на следующий вышестоящий объект в цепочке прототипов (как правило Object).

Пожалуйста УБЕРИТЕ этот абзац из предыдущего , он конечно ошибочен и относится к объекту прототипа

Написать коммент: памятка постеру

 

Крутые посты wtf??? →

02.10.2012 · 94 каммента · рейтинг 13.21
28.04.2008 · 44 каммента · рейтинг 7.6
06.03.2008 · 29 камментов · рейтинг 6.14
29.08.2007 · 28 камментов · рейтинг 5.93
19.01.2008 · 20 камментов · рейтинг 5.08

Последне камменты

Статсы