Реализация удобной модели наследования на JavaScript
kayo — Сб, 02/02/2013 - 12:42
Чтобы хоть как-то оживить обстановку на борту нашего дирижабля, решил написать таки эту небольшую статью про свою реализацию модели наследования на JavaScript.
Введение
JavaScript — очень простой язык, но простота ни в коем случае не означает примитивность. Многие не находят в нём привычной им реализации ООП, основанной на иерархии классов, и ошибочно полагают, что нечто подобное лежит за рамками возможностей этого языка. На самом деле модель наследования в JS имеет основанную на прототипах природу. Здесь нет никаких классов, в качестве основы для создания объектов используются функции, и потому прототипная модель работает именно с ними. В простейшем случае вам нужно лишь немного подшаманить над стандартным свойством prototype, имеющимся у любой функции, добавив в него функции-«методы», чтобы получить возможность их вызова из производных объектов. Однако, выглядит это не всегда эстетично, потому что JavaScript простой язык, поэтому мы перевернём всё с ног на голову, обеспечив так желанную традиционными ООП программистами простоту.
Снаружи
Вот то, что мы хотим получить:
var ClassA = Base({ /* создаём потомок от базового класса */ method1: function(){}, /* определяем метод 1 */ methid2: function(){} /* определяем метод 2 */ }); var ClassB = ClassA({ /* наследуем от класса A */ $init: function(){}, /* переопределяем конструктор */ method1: function(){} /* переопределяем метод 1 */ }); /* если нам нужна возможность вызова методов родителя, поступаем следующим образом */ var ClassC = ClassB({ method2: function(){ /* делаем что-то */ ClassB.method2.call(this); /* делаем что-то ещё */ } }); /* или так, если в момент определения класса предок недоступен */ var ClassD = ClassB(function(BaseClass){ return { method2: function(){ BaseClass.method2.call(this); /* делаем что-то ещё */ } }; }); /* а может быть мы хотим осуществить вызов с аргументами, переданными методу потомка, и вернуть результат */ var ClassE = ClassB(function(BaseClass){ return { method2: function(){ /* делаем что-то */ return BaseClass.method2.apply(this, arguments); } }; }); /* с наследованием разобрались, а создавать экземпляры ещё проще */ var inst_a = new ClassA; inst_a.method1(); /* вызываем метод 1 */ /* вызов конструктора с аргументами, пожалуйста */ var inst_b = new ClassB(1, 2); inst_b.method2(1, 2, 3); /* вызываем метод 2 */ /* и так далее */
То есть для создания «классов»-потомков, мы просто вызываем «родителя» как функцию, а для создания объектов-экземпляров, вызываем наши импровизированные «классы» оператором new. Как вы заметили, если при наследовании мы передаём не объект, а функцию, возвращающую объект, то у нас есть доступ к родителя через первый аргумент этой функции. Данная особенность полезна гораздо больше, чем вы себе сейчас можете представить, в Heliko-Framework, так реализуется «вынесенное наследование», когда заранее не известна цепочка наследования, и таковых может быть несколько. Например, этот принцип положен в основу конвейеров обработки запросов, которые можно конструировать по своему желанию для обслуживания нужд конкретного приложения.
Изнутри
А теперь поговорим о том, как это собственно работает. Реализация выжата из suit.js, я старался не менять структуры функций, чтобы не повредить работоспособности кода, однако взял лишь всё самое необходимое:
var voidy = {}; /* пустой объект */ function dummy(){} /* функция, которая ничего не делает */ /* проверка на экземпляр */ function iof(inst, base){ return (inst && inst.prototype || inst) instanceof (base.callee || base); } /* утилита, определения/проверки типа */ function is(arg, type){ /* в качестве типа используется первая буква имени типа, при этом объект (o) и массив (a) разные типы */ arg = arg === null ? '_' : (iof(arg, Array) ? 'a' : (typeof arg).charAt(0)); if(type){ return type.indexOf(arg) > -1; } return arg; } /* расширение копированием */ function mix(dst, src, force){ for(var i in src){ if(!(i in voidy) && (!(i in dst) || force)){ dst[i] = src[i]; } } return dst; } /* простое наследование через создание объекта-посредника */ function ext(C, B){ /* на самом деле здесь используется небольшой трюк, позволяющий избежать вызов конструктора при наследовании, для чего создаётся посредник с тем же прототипом, что и родитель */ function F(){} F.prototype = B.prototype; C.prototype = new F(); C.prototype.constructor = C; C.superbase = B.prototype; } /* расширение на основе прототипа */ function sub(base, data){ if(arguments.length < 2){ data = base; base = 0; } var self = is(this, 'f') ? this : function(){}; if(base){ ext(self, base); } if(is(data, 'f')){ data = data(base && base.prototype); } if(data){ mix(self.prototype, data, true); } self.prototype.constructor = self; return self; } /* функция-посредник, реализующая объектную модель */ function pro(self, args){ if(!iof(self, args)){ /* вызвана как функция */ var prot = args[0]; return sub.call(function(){ return pro(this, arguments); }, args.callee, prot); } /* вызвана как конструктор */ self.$init.apply(self, args); /* вызов "конструктора" */ return self; } /* Базовый "класс" */ function Base(){ return pro(this, arguments); } Base.prototype = { $init: dummy };
Вот в общем-то и всё. Если что-то вам покажется лишним или избыточным, всегда есть возможность оптимизировать, дерзайте ^^

Отправить комментарий