JavaScript

индекс
246,38

Классы и метаклассы в Javascript

Хочу рассказать о решении, которое я использую для одиночного наследования в Javascript. Оно настолько маленькое, что наверняка в том или ином варианте встречается где-нибудь еще. Надеюсь, кому-то из читателей оно окажется полезным.

Это часть фреймворка, который я сделал для своей платформы Akshell, вот полный код решения и документация. Однако оно может пригодиться в любой Server-Side Javascript среде, на стороне сервера удобное создание иерархий классов особенно актуально. Его можно использовать и на клиенте, если заменить работу со свойством __proto__ оператором new.

Собственно, все решение заключается в методе subclass класса Function. Если позволяет среда, его стоит сделать non enumerable.

Function.prototype.subclass = function (/* [constructor] [, prototype] */) {
  var self = this;
  var constructor = (
    typeof(arguments[0]) == 'function'
    ? Array.prototype.shift.call(arguments)
    : (this === Object
       ? function () {}
       : function () { self.apply(this, arguments); }));
  if (arguments[0])
    constructor.prototype = arguments[0];
  constructor.prototype.__proto__ = this.prototype;
  constructor.__proto__ = this.__proto__;
  return constructor;
};

Функция принимает два опциональных аргумента: конструктор и прототип создаваемого класса. Семантика передачи аргументов нестандартная, но в данном случае удобная: если первый аргумент является функцией, это конструктор, в противном случае — прототип.

Игрушечный пример иерархии классов:

var Figure = Object.subclass(
  {
    getArea: function () {
      throw Error('Abstract');
    }
  });

var Rectangle = Figure.subclass(
  function (a, b) {
    this.a = a;
    this.b = b;
  },
  {
    getArea: function () {
      return this.a * this.b;
    }
  });

var Square = Rectangle.subclass(
  function (a) {
    Rectangle.call(this, a, a);
  });

Т.к. конструкторы являются объектами класса Function, то естественным образом подклассы Function являются классами классов, или метаклассами. Определяя их можно управлять инстанциированием классов, а также определять статические свойства и методы классов, наследуемые подклассами (подкласс получает метакласс от своего родителя в предпоследней строке метода subclass).

Например, мы хотим, чтобы подклассы Figure можно было инстанцировать без оператора и каждый из них имел свойство optionalNew равное true. Для этого нужно написать метакласс для Figure:

var FigureMeta = Function.subclass(
  {
    subclass: function () {
      var constructor = Function.prototype.subclass.apply(this, arguments);
      var result = function () {
        var self = (
          this instanceof arguments.callee
          ? this
          : {__proto__: arguments.callee.prototype});
        constructor.apply(self, arguments);
        return self;
      };
      result.prototype = constructor.prototype;
      result.__proto__ = this.__proto__;
      return result;
    },
    
    optionalNew: true
  });
      
Figure.__proto__ = FigureMeta.prototype;


Все подклассы Figure, определенные после установки метакласса, получат необходимые изменения.

Конечно, метаклассы не стоит применять слишком широко, в частности, FigureMeta является совершенно надуманным примером. Однако в некоторых случаях они могут сделать код гораздо короче и понятнее. Например, при адаптировании в Akshell библиотеки js-forms, порта Django forms в Javascript, метаклассы позволили создавать подклассы форм так же декларативно, как и в Django (там для этого тоже используются метаклассы, только питоновские). Код занимает всего лишь 30 строк.
+26
20 мая 2010, 15:33
50

комментарии (7)

+15
webdew #
очень красиво
Figure.__proto__ = FigureMeta.prototype;

очень удобно
this instanceof arguments.callee? this: {__proto__: arguments.callee.prototype});

и главное негде ошибиться
+3
shergin #
Раз уж пошла такая пьянка, вот моя реализация классового ООП на JS: github.com/shergin/legacy
В частности, там работают такие плюшки как this.$base и this.$super.
+4
vadimslav #
Эти плюшки, кажется, работают ещё у Дина Эдвардса в его Base2, у Дмитрия Филатова и ещё думаю у нескольких сотен людей ;) Хочется процитировать Дина:

Just what the world needs, another Javascript library.

=)
0
noRerih #
Эх, не обновлял страницу, опередили… Хотя плагинами доставлять наследование — совсем не то же самое что в рамках шибко популярного фреймворка;)
+1
shergin #
Да, мы это обсуждали, вот в этом треде (http://stdafx.ya.ru/replies.xml?item_no=1921), и в частности с Димой Филатовым, вот моя цитата оттуда:
Моя отличается простотой и логичностью, конечно-же! У меня для каждого метода не создается свой теневой this.base(), вместо чего один единственный base на лету определяет откуда он вызван (arguments.callee.caller — такого я не видел ни у кого!), затем, на основании этого, определяет базовый класс и ваще делает всю магию. Кроме этого есть метод this.super(), который примерно также возвращает ссылку на базовый прототип. И самое главное — все это основывается на том, что каждая функция помнит как ее зовут и какому классу она принадлежит, эти ссылки прямо прописываются в объект function (такого я тоже ни у кого не видел, хотя реализация стандарта в фаерфоксе прямо предполагает наличиет проперти name у функции). Мне кажется это более честно и в наибольшей мере соответствует логике работы «раннего связывания» в си++.
0
noRerih #
кто только этого не делает;)
www.prototypejs.org/api/class/create
0
Lemoor #
Спасибо, полезный материал.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.