Предыдущие части:
Итак, в предыдущих частях мы выяснили, что
- В JavaScript нет классов
- В JavaScript всё объект
Наша задача – сделать нормальное и понятное наследование.
Для начала нужно попытаться сделать такой псевдо-класс, чтобы можно было создавать из него экземпляры объектов с предопределёнными свойствами.
Вместо классов в JavaScript используются функции. Чтобы были понятны нюансы, рассмотрим такую функцию:
function MyClass() {
this.x = 0;
return 'test';
}
Мы можем сделать обычный вызов функции, например:
var t = MyClass();
console.log(t);
И получим обычный результат: 'test'.
Но также мы можем написать так:
var obj = new MyClass();
console.log(obj);
После запуска этого кода мы увидим, что создался объект obj, и у него есть свойство x.
Разберём, как это получилось.
Как мы видели ранее, инструкция new создаёт новый пустой объект, а к нему применяется конструктор. В данном случае в качестве конструктора вызывается функция MyClass(). И что она делает? Внутри неё написано:
this.x = 0;
Ключевое слово this обозначает тот объект, для которого применяется функция в данный момент. Мы как бы держим объект в одной руке, а функцию в другой. И прикладываем их друг к другу.
То есть, если мы создали пустой объект и приложили к нему конструктор-функцию, то this для этой функции и есть тот самый пустой объект.
Конструктор получает объект в виде this, и создаёт в нём свойство x.
Таким образом, наш сконструированный объект имеет свойство x. Мы можем создать много объектов, вызывая new MyClass(), и теперь нам не надо объявлять свойства в каждом объекте.
Хорошо, а когда функция вызывается без new, то есть ей не передаётся пустой объект, то что в ней является this? Это будет какой-нибудь глобальный объект, в контексте которого работает скрипт. Например, если скрипт работает в браузере, то это будет объект window, и значит, такой вызов:
var t = MyClass();
не только вернёт результат 'test' в переменую t, но и создаст свойство x в объекте window (совершенно без нашего ведома).
Как видим, здесь нет никакой особенной магии, связанной с классами. Это банальная функция, которую можно запускать с чем угодно. Она не несёт никакой ответственности ни за что. Чтобы запустить её именно с чем угодно, используем такой синтаксис:
var obj = { a: 1 };
MyClass.call(obj);
Мы сначала создали объект obj со свойством a, потом вызвали функцию MyClass с этим объектом в качестве this. В результате получили объект obj с двумя свойствами: a и x.
Теперь дополним функцию, чтобы она также создавала в объекте метод:
function MyClass() {
this.x = 0;
this.someMethod = function() { return true; }
}
Как и ранее, мы создали в объекте this свойство someMethod и назначили этому свойству ещё одну функцию – которая тоже объект.
Теперь, когда мы будем вызывать
var obj = new MyClass();
то будем получать объект не только со свойством x, но также и с методом someMethod, которым можем пользоваться:
obj.someMethod();
Но проблема так и не решена. Внешне мы избавили себя от лишней работы по созданию свойств и методов, но внутренне каждый созданный объект имеет своё собственное свойство someMethod, которому каждый раз присваивается новый экземпляр функции. Наследования всё ещё нет, есть лишь клонирование.
Как я говорил ранее, для нормального наследования нужен родитель. И на самом деле он уже есть.
У каждого объекта в JavaScript есть специальное свойство, которое называется prototype (прототип). Оно указывает на тот объект, который является "родителем".
Работает прототип так: если идёт обращение к свойству или методу объекта, но их нет в этом объекте, тогда берётся прототип. Если там есть эти свойства, то берутся они, а если нет, то у прототипа есть свой прототип, и тогда берётся он. И так далее, пока у самого последнего объекта в цепочке родителей прототип не окажется пустым.
То есть, это вроде как и есть рабочая схема наследования. Как нам её применить в своих целях?
Сейчас, возможно, будет сложный момент, но ради него и всё и писалось. У функции MyClass (как и у любой другой) есть свойство prototype. Потому что функция это не просто код, это реальный объект, помните? И поэтому она может иметь свойства. В свою очередь, prototype это тоже объект, у которого есть свойство constructor. А это свойство... равно функции MyClass.
Немного визуализации:
MyClass -> prototype -> constructor === MyClass
Эта схема создаётся автоматически, просто по факту объявления функции в коде. Что она нам даёт?
Отложим загадку prototype.constructor на потом и посмотрим на сам prototype. Когда вызывается конструктор над новым объектом, то есть только тогда, когда мы используем new, то в прототип объекта помещается ссылка на прототип конструктора. Не копия, а именно ссылка на объект-прототип в единственном экземпляре.
то есть у двух созданных объектов:
var obj1 = new MyClass();
var obj2 = new MyClass();
будет ссылка на один и тот же экземпляр прототипа. И значит, если объявить методы в прототипе, то эти методы будут существовать в единственном экземпляре прототипа, а не во всех созданных объектах.
Попробуем это сделать:
MyClass.prototype.test = function() { return true; }
Видите, мы как обычно добавили свойство в объект, но этот объект – прототип функции MyClass. Прототип унаследуется объектами:
var obj1 = new MyClass();
var obj2 = new MyClass();
И после этого мы сможем вызывать у них метод test():
obj1.test();
obj2.test();
Но этот метод будет содержаться только в прототипе, а не в самих объектах.
Фактически мы получили полноценный класс. Достаточно описать свойства в конструкторе, чтобы они создавались в каждом объекте (этого всё равно не избежать), а методы добавить в прототип функции-конструктора.
Это, однако, не спасает нас от того, что прототип можно изменить уже потом, когда мы отнаследовались от него. Например, добавить или убрать методы из него. Но... просто не будем так делать, потому что никто не заставляет.
С другой стороны, подумайте о перспективах: можно создать 100500 объектов одного класса, затем добавить в их прототип новый метод, и этот метод сразу же появится во всех 100500 объектах. Такие вещи могут быть очень удобны. Но вообще говоря, это грозит проблемами, особенно когда некоторые светлые головы меняют прототипы стандартных классов, таких как String, поэтому лучше не надо.
Ну вот и всё. Вроде бы и не сильно сложно, правда? Но мы ещё не рассмотрели "настоящее" наследование, то есть не просто когда объект наследует методы своего класса, а когда один класс наследуется от другого.
Читайте дальше: