JavaScript / Мой extend и стиль наследования классов

в 6:58, , рубрики: extend, inheritance, javascript, static, метки: , , , ,

В данном посте хочу рассказать как предпочитаю реализовывать наследование в объемном JavaScript приложении.
Допустим для проекта необходимо множество родственных и не очень классов.Если мы попытаемся каждый тип поведения описать в отдельном классе, то классов может стать очень много. И у финальных классов может быть с десяток предков. В таком случае обычного JavaScript наследования через prototype может оказаться не достаточно. Например мне понадобилась возможность из метода вызывать аналогичный метод класса-предка. И захотелось создавать и наследовать некоторые статические свойства и методы класса. Такую функциональность можно добавить, вызывая для каждого класса ниже изложенную ф-ию extend:

Функция extend

cSO = {}; // Просто для отдельного пространства имен. cSO.extend = function(child, parent, other) {   if(parent) {     var F = function() {     };     F.prototype = parent.prototype;     child.prototype = new F();     child.prototype.constructor = child;     child.prototype.proto = function() {       return parent.prototype;     }     // Пока все стандартно.   } else {     child.prototype.proto = function() {       return;     }   }   /*    * У классов есть параметр stat, предназначенный для статических ф-ий и данных.    * Он доступен через _class.stat или из объекта(экземпляра) класса через this.stat.    * Потомки могут обращаться к статическому методу предка, для этого их нужно    * объявлять так: _class.stat.prototype.myStaticMethod = function() {...<habracut>    * Можно запретить наследовать метод, объявляя его без prototype.    * Все данные в stat доступны наследникам, если не перекрываются другими данными.    * Для удаления переменной из stat - используем _class.stat.deleteStatVal("name");    */   child.statConstructor = function() {   };   if(parent && ("statConstructor" in parent) && parent.statConstructor && typeof (parent.statConstructor) === "function") {     var S = function() {     };     S.prototype = parent.statConstructor.prototype;     child.statConstructor.prototype = new S();     child.statConstructor.prototype.constructor = child.statConstructor;     child.statConstructor.prototype.proto = function() {       return parent.statConstructor.prototype;     }     child.statConstructor.prototype.protoStat = function() {       return parent.stat;     }   } else {     child.statConstructor.prototype.proto = function() {       return;     }     child.statConstructor.prototype.protoStat = function() {       return;     }   }   var oldChildStat = child.stat; // если в stat что-то уже добавляли...   child.stat = new child.statConstructor();   if(oldChildStat) { // не забываем перенести старые методы и свойства.     for(var k in oldChildStat) {       child.stat[k] = oldChildStat[k];     }   }   child.stat.prototype = child.statConstructor.prototype;   if(oldChildStat && oldChildStat.prototype) {      // не забываем перенести старые методы и свойства для прототипа.     for(var k in oldChildStat.prototype) {       child.stat.prototype[k] = oldChildStat.prototype[k];     }   }   child.prototype.stat = child.stat;   if(other) {     // Выполняем условленные действия по дополнительным параметрам.     if(other.statConstruct) {       child.stat.prototype.construct = other.statConstruct;     }   }   child.stat._class = child; // чтобы ссылаться на класс из статических методов.   child.stat.deleteStatVal = function(name) {     if( name in child.stat) {       try {         delete child.stat[name];       } catch(e) {        }       if(parent) {         child.stat[name] = child.stat.protoStat()[name];       }     }   }   child.prototype.protoFunc = child.statConstructor.prototype.protoFunc = function(callerFuncName, args, applyFuncName) {     /*      * Позволяет вызвать функцию более ранней версии в иерархии прототипов (Правильное имя      * вызывающей ф-ии - необходимо передать в первом параметре). Если установленна      * переменная applyFuncName - вместо callerFuncName будет вызываться другая ф-ия но из      * прототипа на один уровень старше, чем прототип обладающий вызывающей ф-ией.      */     if(!args) {       args = [];     }     if(applyFuncName) {       // Пока не стал заморачиваться, решил отложить на лучшие времена.     } else {       applyFuncName = callerFuncName;     }     var tProto = this;     var ok = false;     do {       if(ok && arguments.callee.caller !== tProto[applyFuncName]) {         if(( applyFuncName in tProto) && ( typeof (tProto[applyFuncName]) === "function")) {           return tProto[applyFuncName].apply(this, args);         }       } else if(arguments.callee.caller === tProto[callerFuncName]) {         ok = true;       }     } while(("proto" in tProto) && (tProto = tProto.proto()))     return;   }      if(child.stat.construct) {     // Вызывается при создании класса или создании потомка без stat.construct     child.stat.construct();   } } 

Небольшая проверка как работает, и какие результаты выдает ф-ия extend()

Создаем 3 класса, каждый — наследник предидущего.

cSO.class001 = function() { } cSO.extend(cSO.class001, 0, {"statConstruct":function(sc1) {   console.log("statConstruct001"); }}); cSO.class001.prototype.construct = function(c1) {   console.log('c1');   this.protoFunc("construct", arguments); } cSO.class001.prototype.alert = function(a1) {   console.log('a1'); } cSO.class001.stat.prototype.st = function(s1) {   console.log('st1');   this.protoFunc("st"); } cSO.class001.stat.dat = ["hello1"];   cSO.class002 = function() { } cSO.extend(cSO.class002, cSO.class001, {"statConstruct":function(sc2) {   console.log("statConstruct002");   this.protoFunc("construct", arguments); }}); cSO.class002.prototype.construct = function(c2) {   console.log('c2');   this.protoFunc("construct", arguments); } cSO.class002.prototype.alert = function(a2) {   console.log('a2');   this.protoFunc("alert"); } cSO.class002.stat.st = function(s2) {   console.log('st2');   this.protoFunc("st"); }   cSO.class003 = function() { } cSO.extend(cSO.class003, cSO.class002); cSO.class003.prototype.construct = function(c3) {   console.log('c3');   this.protoFunc("construct", arguments); } cSO.class003.prototype.alert = function(a3) {   console.log('a3');   this.protoFunc("alert"); } cSO.class003.stat.prototype.st = function(s3) {   console.log('st3');   this.protoFunc("st"); } cSO.class003.stat.dat = ["hello3"]; 

А теперь узнаем, в каком порядке выполняются их действия.

var obj001 = new cSO.class001(); // statConstruct001 var obj002 = new cSO.class002(); // statConstruct002 statConstruct001 var obj003 = new cSO.class003(); // statConstruct002 statConstruct001 obj003.construct(); // c3 c2 c1 obj002.construct(); // c2 c1 obj001.construct(); // c1 obj003.alert(); // a3 a2 a1 cSO.class001.stat.st(); // st1 cSO.class003.stat.st(); // st3 st1 console.log(obj003.stat.dat); // ["hello3"] obj002.stat.dat = ["world"]; console.log(obj002.stat.dat); // ["world"] cSO.class002.stat.deleteStatVal("dat"); console.log(obj002.stat.dat); // ["hello1"] console.log(obj001.stat.dat); // ["hello1"] 

Еще несколько штрихов, которые мне показались важными

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

_class={   construct:function(){}, // Вызываем при создании каждого объекта-наследника.   destruct:function(){}, // Вызываем при удалении любого объекта-наследника.   // и т.д.   stat:{     create:function(){}, // Вызывается при создании класса или потомка класса.     collection:[], // В некоторых классах удобно журналировать все созданные экземпляры.     clearAll:function(){} // Иногда удобно иметь возможность удалить всю коллекцию.     // и т.д.   } } 

Сейчас объясню почему именно так.

Для создания класса необходимо вызывать конструктор, тот конструктор, что вызывается при new Foo() — не вызывается при создании объектов-потомков данного класса.Вот например:

var id = 0; var Foo = function() {   this.id = id++;   console.log("Вы создали объект, имеющий метод boom"); } foo.prototype.boom = function() {} var Bar = function() {    } Bar.prototype = new Foo(); var fooOb = new Foo(); // Вы создали объект, имеющий метод boom var barOb = new Bar(); var barOb2 = new Bar(); console.log(fooOb.id); // 1 console.log(barOb.id); // 0 console.log(barOb2.id); // 0 

А мне хочется, чтобы все объекты, наследники класса Foo имели уникальный id и предупреждали пользователя, что умеют взрываться.
Для реализации этого — я создаю специальный метод cnstruct (constructor — уже занято), и выполняю его при создании каждого объекта. Чтобы не забыть его выполнять, отказываюсь от создания объектов через new Foo() и создаю объекты через статический метод Foo.stat.create().
Далее представлена укороченная версия реально используемого класса, как пример того, какими получаются классы.

Реальный пример

Данный класс необходимо рассматривать как один из многих в цепочке прототипов от базового класса к финальному (скорее в обратную сторону).

(function() {   var _class = cSO.LocalStorageSyncDataType = function () {     /*      * Вообще класс описывает поведение объектов, которые      * сохраняются и загружаются с жесткого диска клиента.      * Но это только часть реализации, остальное вырезал.      */   }   cSO.extend(_class, cSO.ServerSyncDataType, {"statConstruct": function() {     this.protoFunc("construct", arguments); // Если конструктор объявлен, но в нем не вызывается protoFunc - цепочка предидущих конструкторов обрывается.     if("addToClassesForSnapshot" in this) { // Условие не удовлетворяется для cSO.LocalStorageSyncDataType, который по идее абстрактен, а только для его потомков.       this.addToClassesForSnapshot(this._class); // Все потомки по умолчанию будут регистрироваться.     }   }});   var _class_stat = _class.stat; // Такими присвоениями - позволяем минимизатору (компилятору) уменьшать размер скриптов на 15%-25% ежели без присвоений. Обычно устанавливаю подсветку этих слов как констант.   var _class_stat_prototype = _class_stat.prototype;   var _class_prototype = _class.prototype;   var cfs = _class_stat.classesForSnapshot = [];   _class_static.create = function(args) {     // Метод написан просто для примера, такой метод д.б. в финальных классах.     this.addedToLocalStorage = false;     if(args.addedToLocalStorage) {       this.addedToLocalStorage = true;     }     this.protoFunc("construct", arguments);   }   _class_prototype.construct = function(args) {     /*      * Конструктор достраивает созданный объект, но автоматически не вызывается.      * Он добавляет достоинства концепции фабрики объектов в данный стиль.      */     this.addedToLocalStorage = false;     if(args.addedToLocalStorage) {       this.addedToLocalStorage = true;     }     this.protoFunc("construct", arguments);   }   _class_prototype.setLoaded = function(val) {     this.protoFunc("setLoaded", arguments);     // знаю, что здесь будет код, но пока не знаю какой именно.   }   _class_stat.addToClassesForSnapshot = function(clas) {     clas = clas || this._class;     for(var i = 0; i < cfs.length; i++) {       if(cfs[i] === clas) return;     }     cfs.push(clas);   }   _class_stat.createAllSnapshots = function() {     for(var i = 0; i < cfs.length; i++) {       cfs[i].stat.createSnapshot();     }   }   _class_stat_prototype.createSnapshot = function() {     var co = this.collection;     var str = "";     for(var i in co) {       if(co[i]) {         if(!str) {           str = "[";         } else {           str += ",";         }         str += co[i].getJSON();       }     }     if(str) str += "]";     this.snapshot = str;   }   _class_stat.saveAllSnapshotsOnLocalStorage = function() {     for(var i = 0; i < cfs.length; i++) {       cfs[i].stat.saveSnapshotOnLocalStorage();     }   }   _class_stat_prototype.saveSnapshotOnLocalStorage = function() {     if(this.snapshot) {       cSO.localStorage.setItem(this.tableName, this.snapshot);     }   }   _class_stat.setAllBySnapshotsFromLocalStorage = function() {     for(var i = 0; i < cfs.length; i++) {       cfs[i].stat.setBySnapshotFromLocalStorage();     }   }   _class_stat_prototype.setBySnapshotFromLocalStorage = function() {     var arr = $.parseJSON(cSO.localStorage.getItem(this.tableName));     for(var i = 0; i < arr.length; i++) {       if(arr[i]) {         this.createOrGet({"cells":arr[i], "addedToLocalStorage":true});       }     }   } })(); 

Добавлю, что такой подход стоит использовать именно для классов объектов, а для «одиноких» объектов (например cSO.localStorage) стоит использовать традиционную фабрику объектов.
P.S. Понимаю, что большинство концепций программирования и скорость исполнения при таком подходе сильно страдают.Так же понимаю, что такой стиль не нов, и наверняка существуют другие, более подходящие(спасибо если укажете их).
P.S. Не ругайтесь сильно на код, моя проблема еще и в том, что я практически ни разу не показывал своего кода другим.

Автор: rogallic


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js