Задача EXTENDS: макросы функций препроцессора и классоподобный oop

Фон

Я использовал препроцессор C для управления и «компиляции» полубольших проектов javascript с несколькими файлами и целями сборки. Это дает полный доступ к директивам препроцессора C, таким как #include, #define, #ifdef и т. д., из javascript. Вот пример сценария сборки, чтобы вы могли протестировать пример кода:

#!/bin/bash
export OPTS="-DDEBUG_MODE=1 -Isrc"
for FILE in `find src/ | egrep '\.js?$'`
do
  echo "Processing $FILE"
  cat $FILE  \
  | sed 's/^\s*\/\/#/#/'  \
  | cpp $OPTS  \
  | sed 's/^[#:<].*// ; /^$/d'  \
  > build/`basename $FILE`;
done

Создайте каталоги src и build и поместите файлы .js в src.


Удобные макросы

Первоначально я просто хотел использовать препроцессор для #include и, может быть, несколько #ifdef, но я подумал, а не было бы неплохо иметь еще и несколько удобных макросов? Пошли эксперименты.

#define EACH(o,k)     for (var k in o) if (o.hasOwnProperty(k))

Круто, теперь я могу написать что-то вроде этого:

EACH (location, prop) {
  console.log(prop + " : " location[prop]);
}

И он будет расширяться до:

for (var prop in location) if (location.hasOwnProperty(prop)) {
  console.log(prop + " : " location[prop]);
}

Как насчет foreach?

#define FOREACH(o,k,v)   var k,v; for(k in o) if (v=o[k], o.hasOwnProperty(k))
// ...
FOREACH (location, prop, val) { console.log(prop + " : " + val) }

Обратите внимание, как мы прокрадываем v=o[k] внутрь условия if, чтобы оно не мешало фигурным скобкам, которые должны следовать за вызовом этого макроса.


ООП класса

Давайте начнем с макроса NAMESPACE и неясного, но полезного шаблона js...

#define NAMESPACE(ns)    var ns = this.ns = new function()

new function(){ ... } делает несколько приятных вещей. Он вызывает анонимную функцию как конструктор, поэтому для ее вызова не требуется дополнительный () в конце, а внутри него this относится к объекту, создаваемому конструктором, другими словами, к самому пространству имен. Это также позволяет нам вкладывать пространства имен в пространства имен.

Вот мой полный набор классовых ООП-макросов:

#define NAMESPACE(ns) var ns=this.ns=new function()

#define CLASS(c)      var c=this;new function()

#define CTOR(c)       (c=c.c=this.constructor=$$ctor).prototype=this;\
                      function $$ctor

#define PUBLIC(fn)    this.fn=fn;function fn
#define PRIVATE(fn)   function fn
#define STATIC(fn)    $$ctor.fn=fn;function fn

Как видите, эти макросы многое определяют как в Variable Object (для удобства), так и в this (от необходимости). Вот пример кода:

NAMESPACE (Store) {

  CLASS (Cashier) {

    var nextId = 1000;

    this.fullName = "floater";

    CTOR (Cashier) (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }

    PUBLIC (sell) (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }

    STATIC (hire) (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }

  CLASS (Customer) {

    CTOR (Customer) (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }

    PUBLIC (buy) (item, cashier) {
      cashier.sell(this, item);
    }
  }
}

Как насчет РАСШИРЕНИЙ?

Итак, это подводит меня к вопросу ... как мы можем реализовать EXTENDS в качестве макроса, чтобы обернуть обычное наследование прототипа js "клонирование прототипа, копирование свойств конструктора"? Я не нашел способа сделать это, кроме требования, чтобы EXTENDS появлялись после определения класса, что глупо. Этот эксперимент требует EXTENDS, иначе он бесполезен. Не стесняйтесь изменять другие макросы, если они дают такие же результаты.

Изменить. Это может пригодиться для EXTENDS; перечислив их здесь для полноты.

#define EACH(o,k)   for(var k in o)if(o.hasOwnProperty(k))
#define MERGE(d,s)  EACH(s,$$i)d[$$i]=s[$$i]
#define CLONE(o)    (function(){$$C.prototype=o;return new $$C;function $$C(){}}())

Заранее спасибо за любую помощь, совет или живое обсуждение. :)


person Dagg Nabbit    schedule 25.07.2010    source источник


Ответы (1)


Кажется, я только что выполнил свой собственный вызов. Я добавил второй (необязательный) аргумент в макрос объявления CLASS для надкласса объявляемого класса.

Моя первоначальная реализация создавала много встроенного мусора вокруг конструктора, поэтому я решил обернуть некоторые удобные функции во вспомогательный объект макроса, чтобы избежать избыточности.

Вот текущие воплощения моих классовых ООП-макросов:

// class-like oo

#ifndef BASE
  #define BASE  $$_
#endif

#define COLLAPSE(code)      code

#define NAMESPACE(ns)       var ns=BASE._ns(this).ns=new function()

#define CLASS(c,__ARGS...)  var c=[BASE._class(this),[__ARGS][0]]; \
                            new function()

#define CTOR(c)             BASE._extend($$_##c,c[1],this); \
                            c=c[0].c=$$_##c; function $$_##c

#define PUBLIC(fn)          BASE._public(this).fn=fn;function fn

#define PRIVATE(fn)         function fn

#define STATIC(fn)          BASE._static(this).fn=fn;function fn

// macro helper object

COLLAPSE(var BASE=new function(){

  function Clone(){};

  function clone (obj) {
    Clone.prototype=obj; return new Clone;
  };

  function merge (sub, sup) { 
    for (var p in sup) if (sup.hasOwnProperty(p)) sub[p]=sup[p]; 
  };

  this._extend = function (sub, sup, decl) {
    if (sup) {
      merge(sub, sup);
      sub.prototype=clone(sup.prototype);
      sub.prototype.constructor=sub;
    };
    if (decl) {
      merge(sub.prototype, decl);
      decl._static=sub;
      decl._public=sub.prototype;
    };
  };

  this._static=this._ns=this._class=function (obj) {
    return (obj._static || obj); 
  };

  this._public=function (obj) {
    return (obj._public || obj); 
  };

})

... вот тестовое пространство имён...

//#include "macros.js"

NAMESPACE (Store) {

  CLASS (Cashier) {

    var nextId = 1000;

    this.fullName = "floater";

    CTOR (Cashier) (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }

    PUBLIC (sell) (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }

    STATIC (hire) (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }

  // Customer extends Cashier, just so we can test inheritance

  CLASS (Customer, Cashier) {

    CTOR (Customer) (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }

    PUBLIC (buy) (item, cashier) {
      cashier.sell(this, item);
    }

    CLASS (Cart) {

      CTOR (Cart) (customer) {
        this.customer = customer;
        this.items = [];
      }
    }

  }
}

...и вот результат...

var $$_=new function(){ function Clone(){}; function clone (obj) { Clone.prototype=obj; return new Clone; }; function merge (sub, sup) { for (var p in sup) if (sup.hasOwnProperty(p)) sub[p]=sup[p]; }; this._extend = function (sub, sup, decl) { if (sup) { merge(sub, sup); sub.prototype=clone(sup.prototype); sub.prototype.constructor=sub; }; if (decl) { merge(sub.prototype, decl); decl._static=sub; decl._public=sub.prototype; }; }; this._static=this._ns=this._class=function (obj) { return (obj._static || obj); }; this._public=function (obj) { return (obj._public || obj); }; }
var Store=$$_._ns(this).Store=new function() {
  var Cashier=[$$_._class(this),[][0]]; new function() {
    var nextId = 1000;
    this.fullName = "floater";
    $$_._extend($$_Cashier,Cashier[1],this); Cashier=Cashier[0].Cashier=$$_Cashier; function $$_Cashier (fullName) {
      if (fullName) this.fullName = fullName;
      this.id = ++nextId;
      this.transactions = 0;
    }
    $$_._public(this).sell=sell;function sell (item, customer) {
      this.transactions += 1;
      customer.inventory.push(item);
    }
    $$_._static(this).hire=hire;function hire (count) {
      var newCashiers = [];
      for (var i=count; i--;) {
        newCashiers.push(new Cashier());
      }
      return newCashiers;
    }
  }
  var Customer=[$$_._class(this),[Cashier][0]]; new function() {
    $$_._extend($$_Customer,Customer[1],this); Customer=Customer[0].Customer=$$_Customer; function $$_Customer (name) {
      this.name = name;
      this.inventory = [];
      this.transactions = 0;
    }
    $$_._public(this).buy=buy;function buy (item, cashier) {
      cashier.sell(this, item);
    }
    var Cart=[$$_._class(this),[][0]]; new function() {
      $$_._extend($$_Cart,Cart[1],this); Cart=Cart[0].Cart=$$_Cart; function $$_Cart (customer) {
        this.customer = customer;
        this.items = [];
      }
    }
  }
}

Наследование, внутренние классы и вложенные пространства имен работают нормально. Как вы думаете, это полезный подход к классовому ООП и повторному использованию кода в js? Дайте мне знать, если я что-то пропустил.

person Dagg Nabbit    schedule 26.07.2010
comment
Каковы преимущества по сравнению с GWT? - person user123444555621; 27.07.2010
comment
Ну, это все еще javascript, только дополненный макросами. Код GWT написан на java и «компилируется» в javascript. Итак, если вы знакомы с javascript и хотите иметь удобный способ использовать знакомые классовые конструкции, такие как пространства имен, объявления классов с конструкторами внутри них, наследование и т. д., все это можно эмулировать в javascript, эти макросы просто делают это удобнее. - person Dagg Nabbit; 27.07.2010