JS ООП: виртуальные функции, наследование

whirlwind

TDD infected, paranoid
JS ООП: виртуальные функции, наследование

Дошли руки до большого кода - понял, что без классов будет бардак. Заморочился наследованием и прочими нужными вещами. Вот что получилось

PHP:
function BaseClass(){
	this.className = "BaseClass";
	this.someAttr = "hello";

	this.method = function(value){
		this.print(this.className + " says: " + value);
	};

	this.print = function(value){
		alert("Primary output driver >>" + value);
	};
}

instance = new BaseClass();
instance.method("Hi" + " [someAttr = " + instance.someAttr + "]");
Это пример базового класса, в котором у нас базовые реализации методов method и print. Атрибут className задавать не обязательно, если мы не хотим делать instanceof

Далее, первый наследник в иерархии некий MyClass
PHP:
function MyClass(){
	this.parent = new BaseClass();
	Object.extend(this,this.parent);

	this.className = "MyClass";

	this.method = function(value){
		value = "just " + value;
		this.parent.method.apply(this,arguments);
	}
}

instance = new MyClass();
instance.method("Hello" + " [someAttr = " + instance.someAttr + "]");
В этом классе мы частично изменяем поведение метода method. Так как у нас this.parent это экземпляр базового объекта, мы можем обратиться к базовой реализации этого метода. Главное - вызвать его в контексте текущего объекта. Это достигается путем вызова метода apply - расширения объекта function, предоставленное библиотекой prototype.

Как мы выдим, то что не перегружалось, то так и осталось - в данном случае это значение атрибута instance.someAttr и поведение метода print.

Теперь следующий уровень наследования
PHP:
function MyAnotherClass(){
	this.parent = new MyClass();
	Object.extend(this,this.parent);

	this.className = "MyAnotherClass";

	this.print = function(value){
		alert(":) I think... is another output driver >>" + value);
	}
}

instance = new MyAnotherClass();
instance.method("Wow" + " [someAttr = " + instance.someAttr + "]");
В этом классе переопределяется т.н. драйвер вывода. Я не стал сильно заморачиваться в этом примере, просто будем так считать :)
Как видим, после выполнения теста, у нас теперь полностью заменена реализация метода, который был в самом что ни на есть базовом класса. Собсно полагаю основные цели - наследование атрибутов и методов, перегрузка методов с возможностью вызова базовой реализации достигнуты. Что скажете по поводу такого ООП? Где я что упустил (что то подозрительно легко все получилось)?


PS. По поводу prototype. Вообще тут нет ничего такого, для чего нам так уж очень нужна библиотека prototype. apply - ничто иное как вызов с использованием замыкания, а Object.extend - это элементарное копирование свойств одного объекта в другой. Так что все это легко переписывается и без prototype.
 

Mich

Продвинутый новичёк
Рекомендую http://andrewsumin.livejournal.com/260.html
У вас практически то же самое, но вы зачем-то все усложняете с Object.extend...
 

whirlwind

TDD infected, paranoid
Да. В моем варианте проблемы как раз с конструкторами. Спасибо за линк. Возьму на заметку.

-~{}~ 14.08.06 12:14:

>но вы зачем-то все усложняете с Object.extend

Как это усложняю? Весь код должен выполняться в контексте this (что бы работать с атрибутами, относящимися к this, а не к parent) -> методы базового должны быть доступны в контексте this. Иначе например

this.method = function(value){
this.print(this.className + " says: " + value);
};

вызваная как this.parent.method.apply(this,arguments); обратится к методу print наследника, а там его нет, если не сделать extend. Для этого они и копируются.

-~{}~ 14.08.06 12:50:

Теперь далее - конструкторы. Допустим конструктор класса BaseClass принимает 1 аргумент, класса MyClass два аргумента (один из них пойдет в конструктор класса BaseClass), а класс MyAnotherClass не имеет собственной реализации конструктора. Переписываем наши классы следующим образом

PHP:
function BaseClass(){
	this.className = "BaseClass";
	this.someAttr = "hello";
    
    this.construct = function(a){
        alert("BaseClass constructor with a=" + a);
    };

	this.method = function(value){
		this.print(this.className + " says: " + value);
	};

	this.print = function(value){
		alert("Primary output driver >>" + value);
	};
    
    this.construct.apply(this,arguments);
}

function MyClass(){
	this.parent = new BaseClass();
	Object.extend(this,this.parent);
    
    this.construct = function(a,b){
        this.parent.construct.apply(this,arguments);
        alert("MyClass constructor with b=" + b);
    }

	this.className = "MyClass";

	this.method = function(value){
		value = "just " + value;
		this.parent.method.apply(this,arguments);
	}
    
    this.construct.apply(this,arguments);
}

function MyAnotherClass(){
	this.parent = new MyClass();
	Object.extend(this,this.parent);

	this.className = "MyAnotherClass";

	this.print = function(value){
		alert(":) I think... is another output driver >>" + value);
	}

    this.construct.apply(this,arguments);
}
а тест упрощаем, что бы не путаться среди кучи сообщений
PHP:
var instance = null;

//instance = new BaseClass();
//instance.method("Hi" + " [someAttr = " + instance.someAttr + "]");

//instance = new MyClass();
//instance.method("Hello" + " [someAttr = " + instance.someAttr + "]");

instance = new MyAnotherClass("1","2");
instance.method("Wow" + " [someAttr = " + instance.someAttr + "]");
Получим по очереди следующие алерты

1. BaseClass constructor with a=undefined
2. BaseClass constructor with a=undefined
3. MyClass constructor with b=undefined
4. BaseClass constructor with a=1
5. MyClass constructor with b=2
6. :) I think... is another output driver >>MyAnotherClass says: just Wow [someAttr = hello]


1 алерт - это
PHP:
function MyClass(){
	this.parent = new BaseClass();
то есть совершенно правильно undefined, так как без аргументов. но нам и не нужны аргументы, ведь мы хотим получить только методы, а не рабочий объект.

Алерты 2 и 3 это результат отработки
PHP:
function MyAnotherClass(){
	this.parent = new MyClass();
Так как конструкция MyClass так же конструирует BaseClass, то имеем 2 совершенно справедливых вызова конструктора соответственно BaseClass и MyClass без аргументов. Опять же нам не нужен сам объект, нам нужны лишь базовые реализации методов MyClass.

А вот 4 и 5 - это уже конструкция ожидаемого нами объекта. Здесь мы видим что аргументы конструктора совершенно корректно получены конструкторами базовых классов.

На основе данного примера можно выделить следующий протокол:

1. Для работы конструкторов последней строкой декларации класса должен быть вызов конструктора

this.construct.apply(this,arguments);

это должно быть сделано, даже если у текущий класс не объявляет собственного конструктора.

2. Менее явно просматривается порядок следования аргументов. Так как мы используем arguments для вызова конструктора базового класса, очень важно сохранять порядок задания аргументов. Это означает что аргументы, предназначенные для наследников, всегда должны передаваться после аргументов, предназначенных конструктору базового класса.

-~{}~ 14.08.06 13:21:

3. Не забываем в конструкторе первой строкой вызывать конструктор базового класса!
PHP:
this.construct = function(a,b){ 
        this.parent.construct.apply(this,arguments); 
        // current class construction code
}
 

SelenIT

IT-лунатик :)
whirlwind
Прошу прощения: this.construct - это разве обращение к конструктору текущего объекта? Почему-то в гугле я не нашел такого синтаксиса...
 

whirlwind

TDD infected, paranoid
это не конструктор в понимании JS. это произвольный метод, который запускается после конструкции объекта, т.е. конструктор, такой как мы привыкли в ООП.

PS. короче, чето глючит на 3 уровне иерархии. интерпретатор орет что стек оверфлоу - не могу выцепить где у него рекурсия получается.
 

BRat

o_0
Не совсем красиво в набле сделано.
Как раз сейчас изучаю тему наследования, нарыл отличную библиотеку для него (рекомендую, мелкая, но полезная)
http://www.nczonline.net/downloads - zInherit
И вот такой синтаксис для наследования:

PHP:
        function Polygon(iSides)
        {
            this.sides = iSides;
            if (typeof Polygon._initialized == "undefined")
            {
                Polygon.prototype.getArea = function() { return 0;};
                Polygon._initialized = true;
            }
        }


        function Triangle(fBase, fHeight) {
            Polygon.call(this, 3);
            this.base   = fBase;
            this.height = fHeight;

            if (typeof Triangle._initialized == "undefined")
            {
                Triangle.inheritFrom('Polygon');
                Triangle.prototype.getArea = function() { return 0.5*this.base*this.height;};
            }
        }
inheritFrom - из библиотеки zInherit.
 
Сверху