adrianmcli
adrianmcli

Reputation: 1996

How to make a Javascript class with methods and an array I can push to?

I want to define a Javascript object which manages messages. Within this object, I'll need an array that I can do a push() to:

MsgObjCollection.push(MsgObj)

Essentially I am trying to fill the MsgObjCollection object with a bunch of MsgObjs. Each MsgObj has the 3 variables messagesText, timeStamp, source (sent or received).

Also, I'll need to have some methods like:

MsgObjCollection.Sent.Count       // Counts the number of sent messages
MsgObjCollection.Received.Count   // Counts the number of received messages
MsgObjCollection.Count            // Counts the total number of messages in the object

I'm not sure how to approach this in the simplest, cleanest manner.

NOTE: In case there's any confusion, these are not static methods. I'll be creating instances of these objects using the new operator. So I will need multiple instances.

Upvotes: 1

Views: 139

Answers (3)

GameAlchemist
GameAlchemist

Reputation: 19294

You need push, count, you might want to have all arrays methods / accesssors / iterators. What's more, you 'll get some speed boost if you let your collection be an array.

So best solution is to inherit from array, and to have your objects be just real arrays : nothing should be defined on the object, everything on its prototype.

-->> You'll get the speed and all features of arrays for free.

The function looks like :

function MsgObjCollection() { /* nothing */ };
var SO_pr =  ( MsgObjCollection.prototype = [] ) ;

And then, to define count, sent and received on the prototype, use Object.defineProperty not to pollute enumeration, and also to have getters/setters :

Object.defineProperty(SO_pr, 'sent', { get : function() { 
                          var cnt = 0; 
                          this.forEach( function(x) { if (x.source == 'Sent') cnt++; }); 
                          return cnt; } } );
Object.defineProperty(SO_pr, 'received', { get : function() { 
                           var cnt = 0; 
                           this.forEach( function(x) { if (x.source == 'Received') cnt++; });
                           return cnt; } } );
Object.defineProperty(SO_pr, 'count', { get  : function()   { return this.length } , 
                                        set  : function (x) { this.length = x    } });

Notice that since the Msg collection's prototype is a new array, you do not pollute array's prototype when changing MsgObjCollection's prototype.

The Sent and Received property you wish are more complex : they act as a view on the underlying object.
One thing you can do is to have them return a new array built out of the right items of the original array.
I prefer, though, to build a wrapper around the original array 1) to allow modification through this view and 2) to avoid garbage creation.

The fiddle is here : http://jsfiddle.net/cjQFj/1/

Object.defineProperty(SO_pr, 'Sent',    
                      { get : function() { return getWrapper('Sent', this); } } ) ;
Object.defineProperty(SO_pr, 'Received', 
                      { get : function() { return getWrapper('Received', this); } } ) ;

function getWrapper(wrappedProp, wrappedThis) {
   var indx = 0, wp=null;
   // try to find a cached wrapper
   while (wp = getWrapper.wrappers[indx++] ) { 
           if (wp.wthis === this && wp.wprop==wrappedProp) return wp.wrapper;
   };
  // no wrapper : now build, store, then return a new one
  var newWrapper = { 
       get count() { return (wrappedProp=='Sent') ? wrappedThis.sent : wrappedThis.received },
       unshift : function () {  if (this.count == 0) return null;
                         var indx=0; 
                         while (wrappedThis[indx].source != wrappedProp ) indx++; 
                         var popped = wrappedThis[indx];
          while (indx<wrappedThis.length-1) {wrappedThis[indx]=wrappedThis[indx+1]; indx++; }
                         wrappedThis.length--;
                         return popped;
                       }
                 };
  getWrapper.wrappers.push({ wthis : wrappedThis, wprop : wrappedProp, wrapper :  newWrapper }); 
  return newWrapper;
};
getWrapper.wrappers = [];

Now just a little test :

var myColl = new MsgObjCollection();
myColl.push({ source : 'Sent', message : 'hello to Jhon' });
myColl.push({ source : 'Received' , message : 'hi from Kate' });
myColl.push({ source : 'Sent', message : 'hello to Kate' });
myColl.push({ source : 'Received' , message : 'Reply from Jhon' });
myColl.push({ source : 'Received' , message : 'Ho, i forgot from Jhon' });

console.log('total number of messages : ' + myColl.count);
console.log('sent : ' + myColl.sent + '  Sent.count ' + myColl.Sent.count);
console.log('received : ' + myColl.received + '  Received.count ' + myColl.Received.count);
console.log('removing oldest sent message ');
var myLastSent = myColl.Sent.unshift();
console.log ('oldest sent message content : ' + myLastSent.message);
console.log('total number of messages : ' + myColl.count);
console.log('sent : ' + myColl.sent + '  Sent.count ' + myColl.Sent.count);
console.log('received : ' + myColl.received + '  Received.count ' + myColl.Received.count);

Output : >>

total number of messages : 5 
sent : 2  Sent.count 2 
received : 3  Received.count 3 
removing oldest sent message  
oldest sent message content : hello to Jhon
total number of messages : 4 
sent : 1  Sent.count 1 
received : 3  Received.count 3 

The annoying part is that those view properties are not arrays, but since you cannot overload [] operator, you cannot have a fully transparent view on the original array, (i.e. : myBox.Sent[i] that would be exactly the i-th sent message ) so at some point you might want to create arrays on the fly for some operations.

Upvotes: 1

Dan Tao
Dan Tao

Reputation: 128317

Here's a tweak on bfavaretto's answer that should get you closer to what you want:

function MsgObjCollection() {
    this.sent = [];
    this.received = [];
    this.total = [];

    this.push = function(msg) {
        // Assuming msg.source is either 'sent' or 'received',
        // this will push to the appropriate array.
        this[msg.source].push(msg);

        // Always push to the 'total' array.
        this.total.push(msg);
    };
};

You would use this as follows:

var coll = new MsgObjCollection();
coll.push(/* whatever */);

var sent = coll.sent.length;
var received = coll.received.length;

If you wanted, you could wrap the sent and received arrays with objects that expose a Count function instead of a length property; but that strikes me as unnecessary.

Upvotes: 2

bfavaretto
bfavaretto

Reputation: 71908

There are several ways to do that. One of the simplest, if you only need one instance, is an object literal:

var MsgObjCollection = {
    _arr : [],
    push : function(val) {
        return this._arr.push(val);
    },

    Sent : {
        Count : function() {
           // ...
        }
    },

    // etc.
};

If you need multiple instances, use a constructor, and add methods to its prototype property:

function MsgObjCollection() {
    this._arr = [];
}
MsgObjCollection.prototype.push = function(val) {
    return this._arr.push(val);
}
MsgObjCollection.prototype.get = function(index) {
    return this._arr[index];
}
// and so on...

// USAGE:
var collection = new MsgObjCollection();
collection.push('foo');
console.log(collection.get(0));

Upvotes: 0

Related Questions