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

    Недавно в офисе Хабра я хотел прочитать своим коллегам небольшой доклад об объектной ориентации и наследовании классов в 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, Дмитрий Котеров
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 54

      0
      очень интересно, спасибо ))
        0
        Изложение своеобразное у тебя) но полезность бесспорна)
          0
          Хвалю, хорошо написано (ну, за исключением мелких помарок).
          Да и оформлено отлично, приятно.
            0
            Про добавление метода примитивному значению.
            Это ограничение, наложенное JavaScript на переменные (объекты), созданные через строковые,

            Это не ограничение, это запаковка примитива в объект.

            var srt1 = "Литерал";
            str1.tell = "Свойство";

            При попытке доступа к свойству (как получения, так и установки) примитивного значения, создается объект, соответствующий этому примитиву и работа продолжается с ним. После нужных действий он удаляется. Значение же самой изначальной переменной не затрагивается.
              0
              В JavaScript нет встроенной поддержки наследования (как собсвенно нет в нем и классов). Однако наследование можно реализовать (эмулировать) вручную, пользуясь услугами свойства .prototype.

              В javascript есть полноценное встроенное наследование — наследование на основании цепочки прототипов. Если не пытаться подогнать JS-парадигму под более привычную большинству (с классами), то и эмулировать ничего не нужно.
                0
                Все таки, я бы не стал так смело говорить о существовании в JS "наследования на основании цепочки прототипов". Да конечно, наследование мы строим, пользуясь этой "всемогущей цепочкой", однако для этого приходится изрядно попотеть - например для того, чтобы решить две важнейшие проблемы:
                1. конструктор надкласса не должен вызываться еще до создания объекта подкласса
                2. методы надкласса не должны перезаписываться одноименными методами подкласса

                Если же пользоваться .prototype напрямую, как в книжках написано (да хоть в тойже "Javascript: The Definitive Guide, By David Flanagan"), то ничего не получится и более или менее интересных вещей реализовать не удастся
                  +2
                  Интересные вещи реализуются просто и безболезненно :) Только нужно понять саму суть JS-парадигмы и полюбить её )
                  Конструктор надкласса не должен вызываться до создания объекта подкласса, по одной только причине — тут не должно быть никаких "надклассов" и "подклассов". Есть только объекты. Чистые и незамутненные :-D
                    0
                    а пояснить не можете? по поводу чистых и незамутненных! (= может ссылкой какой-нибудь? (=

                    дело в том, что когда я залез в разные модные js библиотеки, то не нашел там нормальной реализации наследования =( Везде этот метод .extend, который тупо копирует свойства и методы родителя, а не пользуется этими методами напрямую. Вот в prototype.js только в последней версии кажется была предпринята попытка сделать наследование с человеческим лицом.

                    Не о таких вы говорите объектах? (=
                      0
                      Больших знатоков JS тут же минусовали на Хабре, после того, как они осмеливались высказать своё мнение о "модных js-библиотеках", в том числе и о prototype.js. Так что я промолчу, тем более знаток я не такой большой.
                      Единственное, эти библиотеки отнюдь не то, на чем нужно позновать суть JS.
                      "Наследование", в своей основе, если отрешиться от всех реализаций и тому подобного, это всего лишь удобный способ строить похожие объекты на основании существующего шаблона. По сути еще одна ипостась повторного использования кода и ничего больше.
                      Через связь с объектом-прототипом мы получаем доступ (наследуем) к определенном в нем данным и функциям. Вот и всё наследование.
                0
                Опять начинаются главные огрехи подобных статей:
                1. Говорить о классах, там где их нет. Да, вы их будете эмулировать, но об этом нужно сразу сказать читателю.
                2. Начинать рассказ о сути ООП с технических деталей, типа, конструкторов, относящихся только к одному этапу - созданию объекта.
                Самая суть ООП в JS, да и еще много чего в JS — цепочка областей видимости, это бы подробно для новичков и описать.
                  0
                  Да, суть и необходимость ООП в JS необходимо всякий раз прояснять, это точно, надо дорабатывать семинар (=
                  Однако простите, не понял, что вы имеете в виду под "Говорить о классах, там где их нет"?
                    0
                    Всё о том же. В JS нет классов. В перемешали вместе понятие "класс" и "прототип".
                      0
                      В JS классов нет. И не было никогда. Есть только объекты. Math - объект, String - объект, Array - объект. Вообще все есть объект, не являющийся экземпляром никакого класса.
                      Если Вы, скажем, "создали экземпляр" класса SomeClassLikeObject [var s = new SomeClassLikeObject()], то, будь это действительно класс, Вам бы не удалось выполнить что-нить вроде s.newMethod = function(arg){alert('this method does not exist in original "class"');};
                      Данный код вполне успешно выполняется и в результате получается, что s уже не экземпляр SomeClassLikeObject, так как в нем присутствует дополнительный метод.
                        0
                        Опять-таки что считать "классами" :)
                        Например, в PHP тоже, кажется, "классическое" ООП и классы, как у людей. Однако, объекту можно добавить новое свойство.
                          0
                          Ну, ООП в PHP классическим только считается (особенно в 4й версии). Подобные хаки, будучи преподнесенными, как достоинства, и использованными по назначению, до сих пор взрывают моск проблемами типа "ну вот, $%#$#, и откуда тут взялось это &^#$^%$# поле???".
                            0
                            А это не хак, уверяю вас :)
                            Просто нужно понимать как всё работает и "откуда взялось это поле".
                            ООП в PHP вполне годится, чтобы на средних проектах работать в ООП-стиле со всеми его преимуществами. А вся эта высокая болтовня о "стандартах ООП" и другом, дело десятое.
                              0
                              я под впечатлением пребывал от одного проекта, над которым в свое время работал. вот там фаршировка экземпляра класса полями в рантайме применялась на каждом шагу. И, уверяю Вас, я понимал и понимаю, как оно работает, но вот поиск ошибок и отладка при таком раскладе превращалась в сущий кошмар. Вот там-то моск и взрывало.
                                0
                                Ну, то что на языке можно писать отстойно, не значит, что нельзя лучше :)
                            0
                            fault-finding mode on
                            новое поле ;)
                            fault-finding mode off
                            +1
                            Все это ж называется динамическими классами. Т.е. классами, в объекты которых можно "налету" добавлять новые свойства и методы, не опредленные в самом конструкторе. Разве нет? (=
                              0
                              да нет... да и первый раз такое определение слышу.
                              Вот, нашел -
                              В итоге в языке Self были реализованы следующие концепции:
                              ...
                              * отсутствие классов; такие языки называются prototype-based; в них создание новых типов объектов осуществляется посредством клонирования имеющихся и внесения изменений непосредственно в структуру нового объекта...
                              [skipped]

                              Отсюда
                              Типичные симптомы JavaScript'a. ;)
                        0
                        Упущена важная часть в описании .consturctor и .prototype.
                        Объекты связаны с прототипами внутренним свойством [[prototype]], которое невозможно изменить и в большинстве реализаций даже получить.
                        А не по цепочке obj.constructor.prototype.
                        Многие пытаются по этой цепочки дойти до корневого объекта или даже динамически изменить прототип объекта и очень удивляются, когда не получается.
                          0
                          А как всё-таки дойти до корневого объекта если [[prototype]] не доступно?
                          0
                          статья хорошая.
                          типа "я напомню :), а дальше вы и сами разберётесь"
                          пару ссылок на тематические статьи не помешало бы.
                          например
                          - для новичков не совсем ясно как объявлять private или public элементы класса
                          - как именно делать наследование.
                          конечно есть гугл, но всё таки.
                            0
                            а как же - простите - ссылки имеются в нормальном количестве (=
                            ООП в JavaScript (5): Полезные ссылки
                              0
                              ой. простите. не заметил. забираю слова назад :)
                            0
                            classes are poor man's closures ©
                            ; )
                              0
                              Похоже на реферат статей Котерова :)
                                +1
                                нет, это конечно же реферерат, но из разных источников, и потом я пользовался собственной логикой изложения (исходя из собственного опыта понимания этого всего) (=
                                  0
                                  Нет здесь Котерова и близко. У Дмитрия говорится - "классов нет, есть только объекты" и "наши классы - эмуляция настоящих".
                                  0
                                  есть js-библиотеки, позволяющие описывать классы, цепочки наследования и интерфейсы. вот например одна из вариантов http://forum.agiledev.ru/index.php?t=msg&th=995
                                    +1
                                    Ну вообще автор не библиотеку писал, а пытался донести основы механизмов.
                                    0
                                    А, почему, кстати, не в блоге JavaScript?
                                    0
                                    Извините, а что тут нового относительно тех же наблов? Помоему наблы 38-40 это всё прекрасно освящают. Их не читал только самый ленивый.
                                      0
                                      К сожалению, не совсем прекрасно.
                                      Гораздо прекрасней освящают комментарии к ним.
                                        0
                                        например?
                                          0
                                          Почитайте комментарии к этим статьям. Особенно, к статье о наследовании.
                                            0
                                            Почитал, на первых страницах ДК говорит о том чтобы читали внимательней наблу, а потом жуткий оффтоп...
                                              0
                                              Среди этого офтопа изредка рассыпаны крупицы знания )
                                              Особенно в постах Zeroglif'а.
                                              Так же далеко не здорово написана набла по замыканиям с попытками связать с Перлом.
                                                0
                                                Согласен, с перлом я бы связывать не стал.
                                                Вообще Вы правы, нет ни одной статьи об ООП в JS, в которой было бы всё прекрасно.
                                                Лично мои знания в этой области основаны на совокупности многих статей и личного опыта.
                                                А интерес к этой тематики возбудили именно наблы.
                                                  0
                                                  А мой интерес во многом комментарии к ним :)
                                      +1
                                      Недописано.
                                      http://disaen.com/misc/inheritance/
                                        0
                                        Какое множественно наследование?
                                        __proto__ только для FF и то далеко не всегда.
                                          0
                                          __proto__ в статье используется только для демонстрации доводов. Всё остальное делается стандартными средствами. Почитайте, пожалуйста, чуть внимательнее :)
                                            0
                                            Перечитал. Да, действительно:
                                            По стандарту оно недоступно для просмотра и редактирования, но, к счастью, Firefox любезно предоставляет к нему доступ через __proto__

                                            извините.

                                            Но множественного наследования всё равно не увидел.
                                              0
                                              Несогласованность термина. Убрал, чтобы не смущало.
                                          +1
                                          В общем и целом - ясная и доходчивая статья. Дописывайте, линкуйте к хабру, будем сглаживать шероховатости. ;)
                                            0
                                            Спасибо за помощь, как карма позволит — выложу.
                                              0
                                              Теперь позволяет?
                                                0
                                                Ога, спасибо :)
                                          0
                                            0
                                            Спасибо большое, очень полезно!

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

                                            Самое читаемое