В данном посте хочу рассказать как предпочитаю реализовывать наследование в объемном 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