Pull to refresh

Классы, объекты и наследование в JavaScript

Reading time 17 min
Views 27K
Недавно в офисе Хабра я хотел прочитать своим коллегам небольшой доклад об объектной ориентации и наследовании классов в JavaScript.

Дело в том, что в свое время я был в полном восторге, научившись создавать свои собственные объекты и выстраивать цепочки наследования, и решил, что называется, поделиться с другими своими находками и наблюдениями. (=

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

Пользуясь тем, что семинар все время откладывается «до следующей пятницы», я решил опубликовать тексты семинара в сети, дабы мои восторги оказались полезными еще кому-нибудь.

Весь текст подеён на 5 разделов:
  1. ООП в Java Script (1/5): Объекты
  2. ООП в Java Script (2/5): Классы
  3. ООП в Java Script (3/5): Свойства и методы класса
  4. ООП в Java Script (4/5): Наследование классов
  5. ООП в Java Script (5/5): Полезные ссылки


ООП в Java Script (1/5): Объекты


Все в JavaScript, на самом деле, является объектом. Массив — это объект. Функция — это объект. Объект — тоже объект. Так что такое объект? Объект — это коллекция свойств. Каждое свойство представляет собой пару имя-значение. Имя свойства — это строка, а значение свойства — строка, число, булево значение, или объект (включая массив и функцию).

Когда мы определяем какую-то переменную, например:

var s = 'hello world';
alert(typeof s); // выводит string

мы, в действительности, неявням образом задаем свойство какого-то объекта. В данном случае, таким объектом будет глобальный объект window:

alert (s == window.s); // выводит true
alert (typeof window); // выводит object

Более того, это свойство window.s само по себе является объектом, т.к. в нем уже изначально определена своя коллекция свойств:

alert(s.length); // выводит 11 (число символов в строке)

При всем при том, что это, на первый взгляд, обычный строковый литерал!

Если значение свойства — функция, мы можем назвать это свойство методом объекта. Чтобы вызвать метод объекта достаточно дописать после его имени две круглые скобки (). Когда метод объекта выполняется, переменная this внутри этой функции ссылается на сам объект. С помощью ключевого слова this метод объекта получает доступ ко всем остальным свойствам и методам объета.

var s = 'futurico'; // создаем новое свойство s объекта window (window.s)
var f = function(){ // создаем новый метод f объекта window (window.f) 
	alert(this == window); // выводит true
	alert(this.s); // выводит 'futurico'
}
f(); // вызываем метод f объекта window (window.f())

var o = {}; // создаем новое свойство o объекта window (window.o)
o.s = 'karaboz'; // создаем новое свойство s объекта window.o (window.o.s)
o.f = function(){ // создаем новый метод f объекта window.o (window.o.f)
	alert(this == o); // выводит true
	alert(this.s); // выводит 'karaboz'
}

o.f(); // вызываем метод f объекта window.o (window.o.f())

Объект создается с помощью функции-конструктора, инициализирующей объект, и ключевого слова new. Функция-конструктор предоставляет те же возможности, что и класс в других языках программирования: а именно, описывает шаблон, по которому будут создаваться объекты (экземпляры) класса. В основе такого шаблона лежит перечисление свойств и методов, которыми будет обладать объект, созданный на основе данного класса. Для всех встроенных типов данных в JavaScript существуют встроенные функции-конструткоры.

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

var str='karaboz';

мы неявным образом вызываем встроенную функцию-конструтор:

var str = new String('karaboz');

и тем самым создаем объект (экземпляр) класса String.

Это же утверждение верно и для всех остальных типов данных JavaScript:

// число
var num = 12345.6789; // var num = new Number(12345.6789);

// булево значение
var bul = true; // var c = new Boolean(true);

// функция
var fun = function(x){var p = x}; // var fun = new Function('x', 'var p = x');

// массив
var arr = ['a', 'b', 'c']; // var arr = new Array('a', 'b', 'c');

// объект
var obj = {}; // var obj = new Object();

У всех этих объектов сразу же после создания определены все свойства и методы, описанные в их функциях-конструкторах (классах):

alert(num.toFixed(1)); // выводит 12345.6
alert(arr.length); // выводит 3

На самом деле, интерпретатор JavaScript действует несколько хитрее, чем может показаться из предыдущего примера. Так, несмотря на то, что следующий код показывает равенство двух переменных (объектов класса String):

var str1 = 'karaboz';
var str2 = new String('karaboz');
alert(str1 == str2); // выводит true

при попытке определить новый пользовательский метод для str1 мы получим ошибку:

str1.tell = function(){
	alert(this);
}
str1.tell(); // выводит ошибку 'str1.tell is not a function'

При этом, для str2 все сработает, как мы и ожидаем:

str2.tell = function(){
	alert(this);
}
str2.tell(); // выводит 'karaboz'

Это ограничение, наложенное JavaScript на переменные (объекты), созданные через строковые, числовые и булевы литералы, тем не менее, не распространяется на объекты, созданные через литералы функции, массива или объекта. Т.е. переменным (объектам), содержащим в себе функцию, или массив, или объект можно напрямую присваивать пользовательские свойства и методы:

var s = 'futurico'; // создаем новое свойство s объекта window (window.s)
var f = function(){ // создаем новый метод f объекта window (window.f) 
	alert(this == window); // выводит true
	alert(this.s); // выводит 'futurico'
}
f(); // вызываем метод f объекта window (window.f())

f.s = 'karaboz'; // создаем новое свойство s объекта window.f (window.f.s)
f.m = function(){ // создаем новый метод m объекта window.f (window.f.m)
	alert(this == f); // выводит true
	alert(this.s); // выводит 'karaboz'
}
f.m(); // вызываем метод m объекта window.f (window.f.m())

Здесь мы наглядно убеждаемся, что функция f, созданная как метод глобального объекта window, сама оказывается объектом, у которого могут быть свои собственные свойства и методы!

ООП в Java Script (2/5): Классы


Итак, класс — это шаблон, описывающий свойства и методы, которыми будет обладать любой объект, созданный на основе этого класса. Чтобы создать свой собственный класс в JavaScript, мы должны написать функцию-конструктор:

// Функция-конструктор - это обычная функция
var Class = function(p){
	alert('My name is constructor');
	this.p = p; 
}

А чтобы создать объект этого нового класса, мы должны вызвать его как обычную функцию, используя при этом ключевое слово new. При этом ключевое слово this внутри функции-конструтора теперь будет указывать на вновь созданный объект:

var o = new Class('karaboz');
alert(o); // выводит [Object object]
alert(o.p); // выводит 'karaboz' - теперь это свойство объекта o

Если попытыться переменной o просто присвоить вызов функции Class() — без ключевого слова new, то никакого объекта создано не будет:

var o = Class('karaboz'); // эквивалентно вызову window.Class()
alert(o); // выводит undefined, а именно то, что вернула функция Class()
alert(window.p); // выводит 'karaboz' - теперь это свойство глобального объекта window

При создании функции, JavaScript автоматически создает для нее пустое свойство .prototype. Любые свойства и методы, записанные в .prototype функции-конструтора станут доступными как свойства и методы объектов, созданных на основе этой функции. Это является основой для описания шаблона (класса), по которому и будут создаваться объеты.

Class.prototype.method = function(){
	alert('my name is .method');
}

Теперь мы можем вызывать этот метод, как метод самого объекта:

o.method(); // работает!

При вызове свойства объекта, оно ищется сначала в самом объекте, и если его там не оказывается, то интепретатор смотрит в .prototype функции-конструтора, содавшей объект.

Так, при создании объекта, в нем уже существует свойство .constructor, которое указывает на функцию-конструктор, создавшую этот объект:

alert(o.constructor == Class); // выводит true 

Заметим, что мы не определяли такого свойства в самом объекте. Интерпретатор, не найдя свойство .constructor в объекте, берет его из .prototype функции-конструктора, создавшей объект. Проверим:

alert(Class.prototype.constructor == Class); // выводит true

Следует обратить внимание, что .prototype существует только для функции-конструктора, но не для самого объекта, созданного на его основе:

alert(o.prototype); // выводит undefined
alert(o.constructor.prototype); // выводит [Object object]

Доступ к .prototype функции-конструктора существует у всех объектов, в том числе и у объектов, встроенных в JavaScript, таких как строки, числа и т.п. Причем тут уже нет никаких ограничений в создании собсвенных свойств и методов (мы видели эти ограничения при попытке прямого присвоения свойств и методов строковой переменной — объекту, созданному через строковый литерал):

var s = 'karaboz';

s.constructor.prototype.tell = function(){
	alert(this);
}

s.tell(); // теперь это не выдает ошибку, а выводит 'karaboz'

Задать новое свойство или метод для встроенных типов объектов можно и напрямую — через встроенную функцию-конструтор этих объектов:

String.prototype.tell = function(){
	alert(this);
}

Кстати, мы в очередной раз подтвердили утверждение о том, что все в JavaScript есть объект (=

ООП в Java Script (3/5): Свойства и методы класса


Свойства и методы класса (члены класса) могут быть открытыми (public), закрытыми (private), привилегированными (privileged) и статическими (static).

Открытые (public) члены


Открытыми называют такие свойства и методы, которые могут быть напрямую прочитаны, изменены, удалены или добавлены любым кодом, находящимся за пределами самого объекта.
Открытые свойства задаются с помощью ключевого слова .this внутри функции-конструктора:

var Class = function(p){
	this.p = p;
}

var o = new Class('karaboz');
alert(o.p); // выводит 'karaboz'

o.p = 'mertas';
alert(o.p); // выводит 'mertas'

Открытые методы задаются с помощью .prototype функции-конструтора:

Class.prototype.method = function(){
	alert('my name is .method');
}
obj.method(); // выводит 'my name is .method'

obj.method = function(){
	alert('my name is .method, but I am new one!');
}
obj.method(); // выводит 'my name is .method, but I am new one!'

Присваивая объекту obj метод .method, мы не изменяем одноименный метод в .prototype функции-конструтора, а лишь закрываем его от интерпретатора, создавая в нашем объекте новое свойство с тем же именем. Т.е. все вновь создаваемые объекты будут по-прежнему обладать стандартным методом из .prototype.

Мы можем позволить объекту вновь видеть и пользоваться методом из .prototype. Для этого нужно просто удалить свойство .method самого объекта:

delete o.method;
o.method(); // вновь выводит 'my name is .method'

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

Закрытые (private) члены


Закрытые свойства и методы недоступны напрямую извне объекта. Они описываются прямо в функции-конструкторе класса и создаются при инициализации объекта. Такими свойствами обладают переменные, переданные в качестве параметров в функцию-конструтор, переменные, объявленные с помощью ключегого слова var, а также функции, объявленные как локальные внутри функции-конструтора.

var Class = function(p){
	var secret = p;
	var count = 3;

	var counter = function(){
		count –;
		if(count > 0){
			return true;
		} else {
			return false;
		}
	}
}

Свойствa secret, count и метод counter создаются в объекте при его инициализации. Они называются закрытыми, потому что к ним нет доступа как у кода извне объекта, так и у открытых методов самого объекта. Чтобы понять, как можно использовать эти закрытые свойства, нужно обратиться к привилегированным методам.

Привилегированные (privileged) методы


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

Привилегированный метод определяется в конструкторе с помощью ключевого слова this:

var Class = function(p){
	var secret = p;
	var count = 3;

	var counter = function(){
		if(count > 0){
			count –;
			return true;
		} else {
			return false;
		}
	}

	this.tellSecret = function(){
		if(counter()){
			return secret;
		} else {
			return null;
		}
	}
}
var o = new Class('12345');

alert(o.tellSecret()); // выводит '12345'
alert(o.tellSecret()); // выводит '12345'
alert(o.tellSecret()); // выводит '12345'
alert(o.tellSecret()); // выводит null

// безуспешно пытаемся переписать закрытый метод counter, 
// a на самом деле, просто создаем новый одноименный открытый метод
o.counter = function(){
	return true;
}
alert(o.tellSecret()); // все равно выводит null

.tellSecret и есть привилегированный метод. Он возвращает закрытое свойство secret при первых трех вызовах, а при всех последующих начинает возвращать null. Каждый раз .tellSecret вызывает закрытый метод counter, который сам обладает доступом к закрытым свойствам объекта. Любой код имеет доступ к методу .tellSecret, но это не дает прямого доступа к закрытым членам объекта.

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

Статические (static) члены


Статические свойства и методы — это свойства и методы, привязанные к самой функции-конструтору (к самому классу). Поэтому их еще называют свойствами и методами класса. Они доступны любому коду как внутри, так и за пределами объекта:

var Class = function(p){
	this.p = p;
}
Class.prototype.tell = function(word){
	alert(this.p + ' ' + word + ' ' + this.constructor.p);
	// alert(this.p + ' ' + word + ' ' + Class.p);
}
Class.p = 'futurico';

var o = new Class('karaboz');
o.tell('love'); // выводит 'karaboz loves futurico';


Замыкание (closure)


Закрытые и привилегированные методы возможны в JavaScript благодаря тому, что называется замыканием (closure). Замыкание — это функция, плюс все те лексические переменные из охватывающего контекста, которые она использует. Когда мы используем оператор function, мы всегда создаем не функцию, а именно замыкание. Замыкание 'помнит' значения всех переменых, которые существовали в контексте, создающем это замыкание, даже когда функция используется уже вне создавшего ее контекста.

var createFunc = function(param){
	var closureParam = param;
	// замыкание
	var returnedFunc = function(){alert(closureParam);}
	return returnedFunc;
}

var f = createFunc('karaboz');

Теперь, если мы посмотрим на переменную f, мы увидим, что это обычная функция, в теле которой заключен параметр closureParam, который неопределен нигде в окружающем f контектсе и по идее должен выдавать ошибку:

alert(f); // выведет: function(){alert(closureParam);}

Однако ошибки не будет, функция function(){alert(closureParam);} благодаря эффекту замыкания помнит closureParam из контекста, породившего ее:

f(); // выведет 'karaboz'

Если вспомнить описанный выше привилегированный метод .tellSecret, то теперь можно понять, как он работает. Метод помнит как закрытую функцию count(), так и закрытое свойство secret, объявленные в создающем .tellSecret контексте. При этом, когда внутри .tellSecret вызывается count(), эта последняя функция, в свою очередь, помнит использующуюся в ее теле переменую count.

ООП в Java Script (4/5): Наследование классов


Основные принципы наследования классов:

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

В JavaScript нет инструментов для создания классического наследования классов. Вместо этого есть наследование на основе свойства .prototype объекта: при вызове метода объекта, интерпретатор ищет этот метод в свойствах самого объекта, и если он не находит метод там, то продолжает поиск в свойстве (объекте) .prototype функции-конструтора данного объекта.

Зная о таком поведении JavaScript, попробуем создать наследование двух классов:

var Class = function(){ // функция-конструткор класса
	this.className = 'Class';
}
Class.prototype.method = function(){ // описываем открытый метод класса
	alert('method of ' +  this.className);
}

var ClassSub = function(){ // функция-конструткор подкласса
	this.className = 'ClassSub';
}
ClassSub.prototype = new Class(); // создаем объект надкласса в .prototype подкласса

var objSub = new ClassSub(); // создаем экземпляр класса ClassSub
objSub.method(); // работает! выводит 'method of ClassSub'

Видим, что подкласс унаследовал метод .method своего надкласса (выполняет его, как свой собственный). Как это происходит? Сначала интерпретатор ищет метод .method в самом объекте objSub и естественно не находит его там. Далее, интерпретатор обращается к ClassSub.prototype и ищет .method среди свойств этого объекта. Опять же — ничего не находит: мы нигде не задавали ничего похожего на ClassSub.prototype.method = function(){}. Но ведь сам объект ClassSub.prototype создан из функции-конструтора Class(). Поэтому, не найдя нужных свойств в самом ClassSub.prototype, интерпретатор обращается к .prototype функции-конструтора этого объекта. И уже здесь находит запрашиваемый метод: Class.prototype.method = function(){}.

Подтврдим это длинное рассуждение простым сравнением:

// .method объекта objSub вытаскивается из .method объекта ClassSub.prototype
alert(objSub.method == ClassSub.prototype.method); // true

// а .method объекта ClassSub.prototype вытаскивается из .method объекта Class.prototype
alert(ClassSub.prototype.method == Class.prototype.method); // true

Подобная цепочка прототипов может быть сколь угодно длинной, но поиск интепретатора в любом случае закончится в тот момент, когда он доберется до объета, созданного (явно или неявно) из встроенного класса Object. Если в Object.prototype он и теперь не найдет запрашиваемого метода, то вернет ошибку. Класс Object лежит в самом верху любой возможной иерархии классов, создаваемых в JavaScript.

Теперь попробуем переопределить этот унаследованный метод, а заодно расширить подкласс собственным дополнительным методом. Одновременно проверим, что методы надкласса остались прежними (помним, что открытые методы и свойства можно добавлять даже после создания экземпляра класса):

ClassSub.prototype.method = function(){ // переопределяем унаследованный метод надкласса
	alert('method of ' +  this.className + ' but new one');
}
ClassSub.prototype.methodSub = function(){ // создаем новый метод подкласса
	alert('methodSub of ' + this.className);
};

// вызываем переопределенный метод в объекте подкласса
objSub.method(); // выводит 'method of ClassSub but new one'

// вызываем новый метод в объекте подкласса
objSub.methodSub(); // выводит 'methodSub of ClassSub'

var obj = new Class(); // создаем экземпляр класса Class

// вызываем переопределенный метод в объекте надкласса
obj.method(); // выводит 'method of Class'

// вызываем новый метод в объекте надкласса
obj.methodSub(); // выводит ошибку 'obj.methodSub is not a function'

Итак, пока все идет нормально. Мы переопределили метод .method в подклассе, и экземпляр подкласса стал выполнять именно его. Одновремнно, экземпляр надкласса сохранил свой прежний одноименный метод. Мы создали новый метод подкласса, который успешно работает в экземпляре подкласса. При этом этот новый метод не стал методом надкласса — экземпляр надкласса не видит его и выдает ошибку.

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

var Animal = function(name){
	this.name = name;
	this.cry(); // при рождении особь должна крикнуть
}
Animal.prototype.cry = function(){
	alert('whoa!');
}

var animal_thing = new Animal('karaboz'); // кричит 'whoa!';


Теперь создадим подкласс Cat, экземпляры которого не кричат, а мяучат:

var Cat = function(name){
	this.name = name;
	this.cry();
}
Cat.prototype = new Animal(); // наследуем от класса Animal
Cat.prototype.cry = function(){ // переопределяем метод .cry
	alert('meow!'); 
}
var cat_thing = new Cat('mertas');

Запустив этот код, мы услышим не два крика (whoa!, meow!), а три! (whoa! ,whoa!, meow!) И понятно почему. Второй крик происходит в тот самый момент, когда мы делаем наследование Cat.prototype = new Animal(). Мы невольно создаем экземпляр класса Animal (и заставляем его кричать при рождении). Т.е. мы запускаем функцию-конструктор надкласса вхолостую еще до создания какого-либо экземпляра подкласса!

Кроме того, в подклассе мы полностью продублировали функцию-конструктор надкласса! Пока мы даже не видим, как иначе можно заставить подкласс присваивать объекту свойства, переданные через параметры функции-конструктора, и как по-другому заставить этот конструктор что-то делать вообще.

Решение проблемы холостого вызова функции-конструктора надкласса


Может попробовать не создавать экземпляр класса Animal, а просто указать на равенство прототипов двух классов? (ведь именно через свои прототипы они и связываются). Попробуем поменять эту строчку:

Cat.prototype = Animal.prototype;

Запустив код, услышим два ожидаемых крика! Но это только кажется, что проблема решена. Попробуем сразу после создания экземпляра подкласса Cat создать еще один экземпляр надкласса Animal

var animal_thing_new = new Animal('juks');
// кричит 'meow!', но это же не экземпляр Cat!

Этот экземпляр кричит голосом класса Cat! Получилось, что мы перезаписали одноименный метод родительского класса. Все дело в том, что когда мы пишем Cat.prototype = Animal.prototype, мы передаем объекту Cat.prototype объект Animal.prototype по ссылке (как всегда происходит, когда переменной присваивается объект). Поэтому любые изменеия первого небезосновательно ведут к изменению второго. Кoгда же мы писали Cat.prototype = new Animal(), мы создавали в Cat.prototype новый объект. Меняя его свойства, мы никак не затрагивали свойства .prototype самой функции-конструктора объекта.

Попробуем реализовать наследование — без создания экземпляра родительского класса — несколько иначе. Попробуем просто скопировать в .prototype подкласса все свойства и методы из .prototype надкласса. Перепишем проблемную строчку следующим образом:

for (var prop in Animal.prototype){
	Cat.prototype[prop] = Animal.prototype[prop];
}

Запустим код и увидим, что третья особь уже больше не мяучит, т.е. метод родительского класса отсался прежним! Но хорошо ли мы поступили? На самом деле, мы не унаследовали свойства надкласса, а просто создали еще одну их копию. Если объектов подкласса будет много — то для каждого объекта будет создана собственная полная копия всех свойств надкласса. Более того, если попытаться поменять методы класса после создания объектов подкласса, то эти изменеия никак не отразятся на объектах подкласса! Такой код кажется очень негибким и громоздким.

Прибегнем к следующему хитрому приему: создадим в .prototype подкласса новый объект, обладающий ссылкой на .prototype надкласса, но при этом не запускающий функцию-конструктор надкласса. Перепишем сложную строчку еще раз:

var Empty = function(){}; // создаем пустую функцию-конструтор
Empty.prototype = Animal.prototype; 
Cat.prototype = new Empty();

Мы создали в Cat.prototype объект искусственного класса Empty. При создании этого объекта ничего не происходит, потому что функция-конструтор Empty() пуста. Любые присваивания в Cat.prototype будут касаться только изменения свойств самого объекта Cat.prototype и не будут затрагивать функцию-конструтор надкласса Animal. Если интепретатор не найдет требуемого метода ни в экземпляре класса Cat, ни в свойствах Cat.prototype, он обратится к функции-конструтору объекта Cat.prototype (== new Empty()) и начнет искать в Empty.prototype, который ссылается напрямую на нужный нам Animal.prototype

Решение проблемы дублирования функции-конструктора надкласса


Нам бы хотелось сделать примерно следующее:

var Cat = function(name){
	Animal.apply(this, arguments);
}

Т.е. при инициализации каждого нового объекта подкласса Cat вызывать функцию-конструктор надкласса Animal в контексте объекта new Cat(). В принципе, наш код уже хорошо работает, но хотелось бы видеть его более универсальным — не привязанным к конкретным именам классов.

Сделаем одно лирическре отступление. Как мы помним, при создании любого объекта, у него образуется свойсво .constructor, берущееся из .prototype.constructor породившей его функции-конструктора. Однако, когда мы записали: Cat.prototype = new Empty(), мы создали в Cat.prototype новый объект. Если теперь попробовать обратиться к (new Cat()).constructor, интепретатор пойдет искать его в Cat.prototype.constructor, а значит в (new Empty().constructor) и найдет в результате это свойство в Empty.prototype.constructor ( == Animal.prototype.constructor). Т.е. наше свойство .constructor указывает теперь на функцию-конструктор надкласса, а не подкласса! Мы исковеркали это свойство. Зная все это, прямо сейчас можно было бы записать:

var Cat = function(name){
	this.constructor.apply(this, arguments);
}

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

var Empty = function(){}; // создаем пустую функцию-конструтор
Empty.prototype = Animal.prototype; 
Cat.prototype = new Empty();
Cat.prototype.constructor = Cat; // возвращаем ссылку на подлинную функцию-конструктор
Cat.superClass = Animal; // создаем ссылку на функцию-конструктор надкласса
В итоге, имеем следующий код нашей функции-конструктора подкласса:
var Cat = function(name){
	Cat.superClass.apply(this, arguments);
}

Если нам теперь захочется в подклассе поменять наш метод не целиком, а только расширить его, мы можем легко сделать это так:

Cat.prototype.cry = function(){
	Cat.superClass.prototype.cry.apply(this, arguments);
	alert('one more cat was born');
}

Наведем порядок в нашем коде и напишем универсальную функцию наследования. Запишем ее в .prototype встроенной функции-конструткора Function. Таким образом, мы создадим новый метод для всех возможных функций, в т.ч. и для наших пользовательских классов.

// Универсальная функция наследования
Function.prototype.inheritsFrom = function(superClass) {
	var Inheritance = function(){};
	Inheritance.prototype = superClass.prototype;

	this.prototype = new Inheritance();
	this.prototype.constructor = this;
	this.superClass = superClass;
}

// функция-конструктор класса
var Class = function(){}

// описание свойств и методов класса
Class.prototype.method = function(){};

// функция-конструктор подкласса
var ClassSub = function(){
	ClassSub.superClass.apply(this, arguments);
}
// определение наследования
ClassSub.inheritsFrom(Class); // sic!  

// описание свойств и методов подкласса
ClassSub.prototype.method = function(){ 
	ClassSub.superClass.prototype.method.apply(this, arguments);
}


ООП в Java Script (5/5): Полезные ссылки


  1. Private Members in JavaScript, Douglas Crockford
  2. Classical Inheritance in JavaScript, Douglas Crockford
  3. OOP in JS, Part 1: Public/Private Variables and Methods, Gavin Kistner
  4. OOP in JS, Part 2: Inheritance, Gavin Kistner
  5. Inheritance in JavaScript, Kevin Lindsey
  6. Маленькие хитрости JavaScript, или пишем скрипты по-новому, Дмитрий Котеров
  7. Большие хитрости JavaScript, Дмитрий Котеров
  8. Наследование в JavaScript, Дмитрий Котеров
Tags:
Hubs:
+17
Comments 54
Comments Comments 54

Articles