Javascript-форум (https://javascript.ru/forum/)
-   Оффтопик (https://javascript.ru/forum/offtopic/)
-   -   Множественное наследование, супер методы, наследование дескрипторов (https://javascript.ru/forum/offtopic/45583-mnozhestvennoe-nasledovanie-super-metody-nasledovanie-deskriptorov.html)

nerv_ 06.03.2014 20:17

Многоуровневое наследование, супер методы, наследование дескрипторов
 
Всем привет :)

Этот пост отвечает на следующие вопросы:
1. Как написать наследование на чистом javascript
2. Как написать многоуровневое наследование
3. Как написать наследование дескрипторов
4. Как написать многоуровневое наследование с наследованием дескрипторов
5. Как вызывать супер методы

Никаких библиотек не подключено, весь используемый код расположен ниже.
В качестве "многоуровневое наследования" выступают три класса:
Animal -> Mammal - Cat.
В качестве супер-метода выступает метод destroy().

Данный пост отражает точку зрения автора и носит рекомендательный характер.

/**********************
 * HELPERS
 * *********************/
/**
 * @borrows toString as toString
 */
Object.toString = Object.prototype.toString;
/**
 * Returns "true" if a value is date
 * @param {*} v A value
 * @return {Boolean}
 */
Date.isDate = function(v) {
    return Object.toString.call(v) === '[object Date]';
};
/**
 * Returns "true" if a value is regular expression
 * @param {*} v A value
 * @return {Boolean}
 */
RegExp.isRegExp = function(v) {
    return Object.toString.call(v) === '[object RegExp]';
};
/**
 * Returns "true" if a value is array
 * @param {*} v A value
 * @return {Boolean}
 */
Array.isArray = Array.isArray || function(v) {
    return Object.toString.call(v) === '[object Array]';
};
/**
 * Creates clone of object
 * Not working with DOM elements
 * [url]http://stackoverflow.com/a/728694[/url]
 * [url]https://github.com/andrewplummer/Sugar/blob/master/lib/object.js#L328[/url]
 * @param {Object} obj
 * @return {Object}
 */
Object.clone = function(obj) {
    // Number, String, Boolean, Function, null, undefined
    if (null === obj || 'object' !== typeof obj) {
        return obj;
    }

    // Date and RegExp
    if (Date.isDate(obj) || RegExp.isRegExp(obj)) {
        return new obj.constructor(obj);
        // Array and Object
    } else {
        var copy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                copy[key] = this.clone(obj[key]);
            }
        }
        return copy;
    }
};
/**
 * Inherits a target (Class_1) by a source (Class_2)
 * @param {Object} target
 * @param {Object} source
 */
Object.inherit = function(target, source) {
    target._super = source;
    target.prototype = Object.create(target._super.prototype, target._super.descriptor);
    target.prototype.constructor = target;
};


/**********************
 * CLASSES
 * *********************/

/**
 * Class Animal
 * @param {Number} legs
 * @constructor
 */
function Animal(legs) {
    var descriptor = Object.clone(Animal.descriptor);
    descriptor.legs.value = legs;
    Object.defineProperties(this, descriptor);
}

Animal.prototype.destroy = function() {
    console.log('Animal.destroy()');
    for(var key in Animal.descriptor) {
        delete this[key];
    }
};
Animal.prototype.run = function() {
    console.log('Animal.run()');
};

Animal.descriptor = {
    legs: {
        value: null,
        enumerable: true,
        configurable: true
    }
};

// ------------------------

// Inheritance
Object.inherit(Mammal, Animal);

/**
 * Class Mammal
 * @param {Number} legs
 * @param {Number} teeth
 * @constructor
 */
function Mammal(legs, teeth) {
    // call the Animal constructor
    Mammal._super.apply(this, arguments);

    var descriptor = Object.clone(Mammal.descriptor);
    descriptor.teeth.value = teeth;
    Object.defineProperties(this, descriptor);
}

Mammal.prototype.destroy = function() {
    console.log('Mammal.destroy()');
    Mammal._super.prototype.destroy.call(this);
    for(var key in Mammal.descriptor) {
        delete this[key];
    }
};
Mammal.prototype.walk = function() {
    console.log('Mammal.walk()');
};

Mammal.descriptor = {
    teeth: {
        value: null,
        enumerable: true,
        configurable: true
    }
};

// ------------------------

// Inheritance
Object.inherit(Cat, Mammal);

/**
 * Class Cat
 * @param {Number} legs
 * @param {Number} teeth
 * @param {Number} tails
 * @constructor
 */
function Cat(legs, teeth, tails) {
    // call the Mammal constructor
    Cat._super.apply(this, arguments);

    var descriptor = Object.clone(Cat.descriptor);
    descriptor.tails.value = tails;
    Object.defineProperties(this, descriptor);
}

Cat.prototype.destroy = function() {
    console.log('Cat.destroy()');
    Cat._super.prototype.destroy.call(this);
    for(var key in Cat.descriptor) {
        delete this[key];
    }
};
Cat.prototype.climb = function() {
    console.log('Cat.climb()');
};

Cat.descriptor = {
    tails: {
        value: null,
        enumerable: true,
        configurable: true
    }
};


/********************
 * USAGE / INSTANCE
 ********************/

var cat = new Cat(8, 100, 3);

alert(JSON.stringify(cat));
console.info('created', cat);

console.log('cat instanceof Cat', cat instanceof Cat);
console.log('cat instanceof Mammal', cat instanceof Mammal);
console.log('cat instanceof Animal', cat instanceof Animal);

cat.run(); // Animal.prototype.run
cat.walk(); // Mammal.prototype.walk
cat.climb(); // Cat.prototype.climb
cat.destroy(); // Cat -> Mammal -> Animal

alert(JSON.stringify(cat));
console.info('destroyed', cat);

Octane 14.03.2014 10:29

Цитата:

Сообщение от nerv_
Object.toString = Object.prototype.toString;

Нафига? В Object и так есть toString

Цитата:

Сообщение от nerv_
// Inheritance
Object.inherit(…, …);

Я бы подумал, что это какие-то манипуляции с __proto__, может быть лучше в Function или вообще отдельно?

Кстати, мне последнее время нравится идея создавать объекты, не описывая конструкторы:
var Animal = {

	destroy: function () {
		console.log('Animal.destroy()');
	},

	run: function () {
		console.log('Animal.run()');
	}

};

var Mammal = Object.create(Animal);

Object.assign(Mammal, {

	walk: function () {
		console.log('Mammal.walk()');
	}

});

var Cat = Object.create(Mammal);
как-то так… правда тут с instanceof непонятки могут возникнуть

melky 14.03.2014 10:43

Цитата:

Сообщение от Octane
Нафига? В Object и так есть toString

а что это за статический метод? откуда он взялся?

Octane, согласен, ES6 - крутая вещь

Octane 14.03.2014 10:53

Цитата:

Сообщение от melky
а что это за статический метод? откуда он взялся?

Object.toString === Function.prototype.toString // true

nerv_ 14.03.2014 12:30

Цитата:

Сообщение от Octane
Нафига? В Object и так есть toString

есть, только не работает =)

Цитата:

Сообщение от Octane
Я бы подумал, что это какие-то манипуляции с __proto__

Никакой черной магии, все законно :)

Немного переписал функцию наследования согласно тому, что есть в Node.js
Если точнее, то вместо Class._super стало Class.super_

Цитата:

Сообщение от Octane
Кстати, мне последнее время нравится идея создавать объекты, не описывая конструкторы

а еще нет прототипов и нет супер методов

Цитата:

Сообщение от Octane
может быть лучше в Function или вообще отдельно?

чего?

BallsShaped 14.03.2014 13:02

Цитата:

Сообщение от nerv_
чего?

Метод .inherit

Octane 15.03.2014 01:15

Цитата:

Сообщение от nerv_
а еще нет прототипов и нет супер методов

В том и идея, что описываем прототипы, вместо конструкторов. Кроме как в конструкторе, ссылка super чаще всего бывает нужна только в выдуманных примерах, а тут нет конструкторов.
Единственное неудобство в instanceof, но можно воспользоваться isPrototypeOf
var barsic = Object.create(Cat);
if (Cat.isPrototypeOf(barsic)) {
   …
}

а еще getPrototypeOf (IE9+) есть, так что без super вполне можно обойтись
Object.getPrototypeOf(Cat) → Mammal

http://www.reddit.com/r/javascript/c...s_approach_is/

Не навязываю, что надо делать именно так, просто как пример интересного подхода


Цитата:

Сообщение от Octane
может быть лучше в Function или вообще отдельно?
Цитата:

Сообщение от nerv_
чего?
Цитата:

Сообщение от BallsShaped
Метод .inherit



Я про то, что в Object методу inherit не место.

nerv_ 16.03.2014 17:33

Цитата:

Сообщение от Octane
В том и идея, что описываем прототипы, вместо конструкторов

Куда писать те свойства, кот. должны быть созданы для каждого экземпляра, как не в конструктор?
Т.е. моя позиция в том, что придется описывать и то и другое.
А еще у нас есть дескрипторы.

Цитата:

Сообщение от Octane
Единственное неудобство в instanceof, но можно воспользоваться isPrototypeOf

я бы не стал так утверждать)

Цитата:

Сообщение от Octane
Кроме как в конструкторе, ссылка super чаще всего бывает нужна только в выдуманных примерах, а тут нет конструкторов.

Что то я тебя не понял :) Если есть необходимость в множественном наследовании, то ссылка нужна и на мой взгляд удобнее, чем
Object.getPrototypeOf(object)


Цитата:

Сообщение от Octane
Я про то, что в Object методу inherit не место.

не хотел расширять прототипы. В целом на усмотрение разработчика :)

Octane 16.03.2014 18:07

Кстати, тут ни разу не множественное наследование, у тебя всегда один суперкласс.


Цитата:

Сообщение от nerv_
Куда писать те свойства, кот. должны быть созданы для каждого экземпляра, как не в конструктор?

Либо инициализирующий метод, либо отдельно set-метод для каждого свойства/группы свойств, что мне кажется лучше. Это намного нагляднее, чем искать что там произошло после super.apply.


Цитата:

Сообщение от nerv_
Цитата:

Сообщение от Octane
Единственное неудобство в instanceof, но можно воспользоваться isPrototypeOf

я бы не стал так утверждать)

Можно подробнее в чем проблема?
var Animal = {};

var Mammal = Object.create(Animal);

var Cat = Object.create(Mammal);

var barsic = Object.create(Cat);

alert(Animal.isPrototypeOf(barsic)); // true
alert(Mammal.isPrototypeOf(barsic)); // true
alert(Cat.isPrototypeOf(barsic));    // true



Цитата:

Сообщение от nerv_
Цитата:

Сообщение от Octane
Кроме как в конструкторе, ссылка super чаще всего бывает нужна только в выдуманных примерах, а тут нет конструкторов.

Что то я тебя не понял Если есть необходимость в множественном наследовании, то ссылка нужна и на мой взгляд удобнее, чем Object.getPrototypeOf(object)

Вместо
Cat._super.prototype.destroy.call(this);

будет
Object.getPrototypeOf(Cat).destroy.call(this);
разве что в большем количестве символов неудобство


Цитата:

Сообщение от nerv_
А еще у нас есть дескрипторы.

Да дескрипторы у тебя это так наворот к классическому примеру с наследованием, для меня в этом нет никакого академического интереса.

nerv_ 16.03.2014 20:19

Цитата:

Сообщение от Octane
Кстати, тут ни разу не множественное наследование, у тебя всегда один суперкласс.

Согласен. Я выразился не совсем точно. Следовало бы переименовать в "многоуровневое" (например). А нативного множественного наследования в js нет, только mixin'ы.

Цитата:

Сообщение от Octane
Можно подробнее в чем проблема?

следуя общепринятым соглашениям по наименованию, с больших букв пишутся имена "классов". А раз это класс - это функция, следовательно можно использовать оператор new:
var Animal = {};

var Cat = Object.create(Animal);
      
var obj = new Cat();

console.log(obj);

Конечно, тут можно заявлять, что встроенные операторы языка нам не нужны, но если они есть, почему бы ими не воспользоваться?
Т.е. ты ломаешь то, что заложено в язык и к чему привыкли js-разработчики.

Цитата:

Сообщение от Octane
Да дескрипторы у тебя это так наворот к классическому примеру с наследованием

скорее как сахар. Признаться, я их тоже не люблю, но в одном проекте потребовались :)
Вместе с тем, считаю очень удобным то, что, например, приватные свойства
this._property;

можно делать неперебираемыми.

Цитата:

Сообщение от Octane
Либо инициализирующий метод, либо отдельно set-метод для каждого свойства/группы свойств, что мне кажется лучше. Это намного нагляднее, чем искать что там произошло после super.apply.

1. "Либо инициализирующий метод" та же функция конструктор
2. "либо отдельно set-метод для каждого свойства/группы свойств, что мне кажется лучше" не вижу причин, по которым нельзя сделать те же самые сет-методы в "стандартной" реализации

Впрочем, сколько людей, столько и мнений :)

Octane 16.03.2014 20:30

честно говоря сразу хотел назвать прототипы с маленькой буквы, потом зачем-то оставил, как в примере по ссылке

nerv_ 16.03.2014 21:04

бывает :)

Я вот теперь не могу тему переименовать. Только название получилось :D

Octane 16.03.2014 21:17

с чпу наверное уже ничего не сделать

nerv_ 17.03.2014 21:46

Цитата:

Сообщение от Octane
с чпу наверное уже ничего не сделать

за ошибки приходиться платить. Ну да ладно :)

Maxmaxmaximus11 19.03.2014 13:18

Ща вам батя покажет класс

Octane 19.03.2014 14:26

о нееет, пощади!

Maxmaxmaximus11 19.03.2014 17:01

новая версия Class, к суперметодам обращаемся через $methodName. если свойство начинается или заканчивается на жетсткий побел, то оно становится не итерируемым. вот и вся суть

function Class(){

}

Class.extend = function (Prototype) {

    Prototype.prototype = this.prototype;
    constructor.prototype = new Prototype();
    constructor.extend = this.extend;

    
    function constructor() {
        if (this.constructor) {
            this.constructor.apply(this, arguments);
        }

        for (var key in this) if (this.hasOwnProperty(key) && /(^_)|(_$)/.test(key)) {
            Object.defineProperty(this, key, {
                value       : this[key],
                writable    : true,
                configurable: true,
                enumerable  : false
            });
        }
    }


    for (var key in constructor.prototype) if (constructor.prototype.hasOwnProperty(key)) {

        if (/(^_)|(_$)|(constructor)/.test(key)) {
            Object.defineProperty(constructor.prototype, key, {
                value       : constructor.prototype[key],
                writable    : true,
                configurable: true,
                enumerable  : false
            });
        }

        if (key in this.prototype && typeof this.prototype[key] === 'function') {
            Object.defineProperty(constructor.prototype, '$' + key, {
                value       : this.prototype[key],
                writable    : false,
                configurable: false,
                enumerable  : false
            });
        }

    }


    return constructor;
};


//##############################################################


// проверяем суперметоды
alert('проверяем суперметоды')


var Animal = Class.extend(function () {
    this.say = function () {
        alert('Animal');
    }
});


var Cat = Animal.extend(function () {
    this.say = function () {
        this.$say();
        alert('Cat');
    }
});


new Cat().say(); // Animal, Cat



// проверяем приватные
alert('проверяем приватные')

var Animal = Class.extend(function () {
    this.say = function () {

    };

    this._need = function () {

    }
});


for (var key in new Animal) alert(key) // say



// проверяем instanceof
alert('проверяем instanceof')

alert( new Cat() instanceof Cat ) // true

Maxmaxmaximus11 19.03.2014 17:54

Ну а дальше вы можете расширять этот каркас всякими деструкторами и евент эмиттерами:

var EventEmitter = Class.extend(function () {


    this.constructor = function () {
        this._handlersStore = {}
    };


    this.on = function (type, handler) {
        var handlersStore = this._handlersStore;
        var handlers = handlersStore[type] || (handlersStore[type] = []);

        handlers.push(handler);
    };

    this.emit = function (type) {
        var self = this;
        var eventArgs = [].slice.call(arguments, 1);
        var handlersStore = this._handlersStore;
        var handlers = handlersStore[type] || [];

        handlers.forEach(function (handler) {
            handler.apply(self, eventArgs);
        });
    };

});


var q = new EventEmitter();

q.on('click', function () {
    alert('on click!');
});

q.emit('click');

nerv_ 19.03.2014 18:35

1. Если я правильно понял, то это
function constructor() {
    if (this.constructor) {
        this.constructor.apply(this, arguments);
    }
}

правильней написать так
function constructor() {
    if (this.$constructor) this.$constructor.apply(this, arguments);
    if (this.constructor) this.constructor.apply(this, arguments);
}

2. Как быть, если потребуются уникальные функции для экземпляра, а не из прототипа?
3. Если уж так писать циклы, обертки и т.п. то можно и миксыны сделать
4. Нет той гибкости, кот. есть при использовании ссылки на СуперКласс (на мой взгляд)
5. Кроме того, стороннему разработчику придется разбираться в твоей капусте :)

Sweet 19.03.2014 18:44

Цитата:

Сообщение от Maxmaxmaximus11
к суперметодам обращаемся через $methodName

Обрати внимание на название темы. С твоим подходом не получится многоуровневое наследование.

Maxmaxmaximus11 19.03.2014 18:51

Цитата:

Сообщение от nerv_
правильней написать так

не, родительский конструктор виден будет по цепочке прототипов если он есть так что бакс не обязательно.

Цитата:

Сообщение от Sweet
Обрати внимание на название темы. С твоим подходом не получится многоуровневое наследование.

Почему? Ты имеешь ввиду нельзя делать цепочки классов? Ведь можно. К прапраметодам ты вообще можешь обратиться как $$methodName, если ты об этом) правда я не знаю зачем это может понадобиться.

Цитата:

Сообщение от nerv_
Нет той гибкости

ой а можно пример использования гибкости)? у нас по факту нет ни какого super обьекта) если что так. не к чему обращаться. у нас есть прототип в котором хранятся всякие функции. но это не super обьект) да и к томуже не правильно было бы из детей изменять методы суперкласса. имхо. родительский класс вот ДАЛ тебе кучу всего, вто что он дал ты можешь новое добавлять, но не менять или добавлять) это идеологически верно, даже по названию функции extend.

Цитата:

Сообщение от nerv_
то можно и миксыны сделать

это когда 2 конструктора применяешь к одному обьекту)?

Sweet 19.03.2014 19:31

Maxmaxmaximus11
var A = Class.extend(function () {
  this.method = function () { }
});

var B = A.extend(function () {
  this.method = function () {
    this.$method == B.prototype.method; // -> fail!
  }
});

var C = B.extend(function () {
  this.method = function () {
    this.$method == B.prototype.method;
  }
});

Maxmaxmaximus11 19.03.2014 19:47



спасибо, баг нашел, короч влом фиксить, все ровно это ни кто юзать не будет. но мой способ поддерживает множественное наследование)

function Class(){

}

Class.extend = function (Prototype) {

    Prototype.prototype = this.prototype;
    constructor.prototype = new Prototype();
    constructor.extend = this.extend;

    
    function constructor() {
        if (this.constructor) {
            this.constructor.apply(this, arguments);
        }

        for (var key in this) if (this.hasOwnProperty(key) && /(^_)|(_$)/.test(key)) {
            Object.defineProperty(this, key, {
                value       : this[key],
                writable    : true,
                configurable: true,
                enumerable  : false
            });
        }
    }


    for (var key in constructor.prototype) if (constructor.prototype.hasOwnProperty(key)) {

        if (/(^_)|(_$)|(constructor)/.test(key)) {
            Object.defineProperty(constructor.prototype, key, {
                value       : constructor.prototype[key],
                writable    : true,
                configurable: true,
                enumerable  : false
            });
        }

        if (key in this.prototype && typeof this.prototype[key] === 'function') {
            Object.defineProperty(constructor.prototype, '$' + key, {
                value       : this.prototype[key],
                writable    : false,
                configurable: false,
                enumerable  : false
            });
        }

    }


    return constructor;
};

//#################################


var Animal = Class.extend(function () {
    this.say = function () {
        alert('Animal')
    }
});


var Cat = Animal.extend(function () {
    this.say = function () {
        this.$say();
        alert('Cat')
    }
});


var Ashot = Cat.extend(function () {
    this.say = function () {
        this.$say();
        alert('Ashot')
    }
});


new Ashot().say();

nerv_ 19.03.2014 19:48

Цитата:

Сообщение от Maxmaxmaximus11
не, родительский конструктор виден будет по цепочке прототипов если он есть так что бакс не обязательно.

допустим, есть объект с методом, внутри которого вызывается супер метод
var obj = {
    method: function() {
        // тут этот объект вызывает свой супер метод
    }
};

С твоим подходом проблемы начнутся, если вызвать метод в контексте другого объекта. Т.е. "цепочка" супер методов в лучшем случае не будет вызвана, в худшем нарушена.
<script src="http://nervgh.github.io/js/yum/yum.js"></script>
<script>
    /**********************
     * CLASSES
     * *********************/

    /**
     * Class Animal
     * @param {Number} legs
     * @constructor
     */
    function Animal(legs) {}

    Animal.prototype.destroy = function() {
        console.log('Animal.destroy()');
    };

    // ------------------------

    // Inheritance
    Object.inherit(Mammal, Animal);

    /**
     * Class Mammal
     * @constructor
     */
    function Mammal(legs, teeth) {
        // call the Animal constructor
        Mammal.super_.apply(this, arguments);
    }

    Mammal.prototype.destroy = function() {
        console.log('Mammal.destroy()');
        Mammal.super_.prototype.destroy.call(this);
    };

    // ------------------------

    // Inheritance
    Object.inherit(Cat, Mammal);

    /**
     * Class Cat
     * @constructor
     */
    function Cat(legs, teeth, tails) {
        // call the Mammal constructor
        Cat.super_.apply(this, arguments);
    }

    Cat.prototype.destroy = function() {
        console.log('Cat.destroy()');
        Cat.super_.prototype.destroy.call(this);
    };


    /********************
     * USAGE / INSTANCE
     ********************/

    var cat = new Cat();

    cat.destroy(); // Cat -> Mammal -> Animal;
    console.log('--------------------');
    cat.destroy.call(this); // Cat -> Mammal -> Animal;

</script>


Цитата:

Сообщение от Maxmaxmaximus11
но мой способ поддерживает множественное наследование)

нет. Почему, читай тему.

Maxmaxmaximus11 19.03.2014 19:52

ладно ладно) убедили, я просто щас другим занят)) наварганил прямо тут в текстэриа)

nerv_ 19.03.2014 20:04

Цитата:

this.$method == B.prototype.method; // -> fail!

Sweet, все правильно, потому, что метод с баксом - это метод A.

var A = Class.extend(function () {
  this.method = function () { }
});

var B = A.extend(function () {
  this.method = function () {
    return this.$method === A.prototype.method;
  }
});

var C = B.extend(function () {
  this.method = function () {
    return this.$method === B.prototype.method;
  }
});
      

var b = new B();
var c = new C();
console.log(b.method());
console.log(c.method());

Sweet 19.03.2014 20:24

nerv_, ты написал как должно быть, а я - как происходит при использовании реализации Class.extend Maxmaxmaximus11.

nerv_ 19.03.2014 20:35

Sweet, я говорю, что "fail" происходит правильно в твоем примере.
Или я опять тебя не понял :)

Sweet 19.03.2014 20:48

nerv_, :) Все всё поняли.


Часовой пояс GMT +3, время: 10:28.