Pull to refresh

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

Reading time3 min
Views2.9K
Хочу рассказать о решении, которое я использую для одиночного наследования в 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 строк.
Tags:
Hubs:
+26
Comments7

Articles