Question

NB: The material in the subsection titled Background is not essential. The full description of the question is fully contained in the preceding paragraphs.

I'd like to implement a universal, lightweight, and "unobtrusive" way to "tag" arbitrary objects.

More specifically, I want to define the equivalent of the (abstract) functions tag, isTagged, and getTagged, such that:

  1. isTagged(t) is true if and only if t was the value returned by tag(o), for some object o;
  2. getTagged(tag(o)) is identical to o, for every object o;
  3. if t = tag(o), then tag(t) should be identical to t;
  4. with the exception of the behaviors described in (1), (2), and (3) above, and strict identity tests involving ===, tag(o) and o should behave the same way.

[EDIT: One further requirement is that the implementation should not modify the Object class, nor any other standard class, in any way.]

For example:

>>> isTagged(o = "foo")
false
>>> isTagged(t = tag(o))
true
>>> getTagged(t) === o
true
>>> tag(t) === t
true
>>> t.length
3
>>> t.toUpperCase()
"FOO"

Below I give my best shot at solving this problem. It is (almost) universal, but, as it will soon be clear, it is anything but lightweight!!! (Also, it falls rather short of fully satisfying requirement 4 above, so it is not as "unobtrusive" as I'd like. Moreover, I have serious doubts as to its "semantic correctness".)

This solution consists of wrapping the object o to be tagged with a "proxy object" p, and copying all the properties of o (whether "owned" or "inherited") to p.

My question is:

is it possible to achieve the specifications given above without having to copy all the properties of the tagged object?


Background

Here's the implementation alluded to above. It relies on the utility function getProperties, whose definition (FWIW) is given at the very end.

function Proxy (o) { this.__obj = o }

function isTagged(t) {
  return t instanceof Proxy;
}

function getTagged(t) {
  return t.__obj;
}

var tag = (function () {
  function _proxy_property(o, pr) {
    return   (typeof pr === "function")
           ? function () { return pr.apply(o, arguments) }
           : pr;
  }

  return function (o) {
    if (isTagged(o)) return o;

    if (typeof o.__obj !== "undefined") {
      throw TypeError('object cannot be proxied ' +
                      '(already has an "__obj" property)');
    }
    var proxy = new Proxy(o);
    var props = getProperties(o); // definition of getProperties given below
    for (var i = 0; i < props.length; ++i) {
      proxy[props[i]] = _proxy_property(o, o[props[i]]);
    }
    return proxy;
  }
})();

This approach, ham-fisted though it is, at least seems to work:

// requirement 1
>>> isTagged(o = "foo")
false
>>> isTagged(p = tag(o))
true

// requirement 2
>>> getTagged(p) === o
true

// requirement 3
>>> tag(p) === p
true

// requirement 4
>>> p.length
3
>>> p.toUpperCase()
"FOO"

...well, almost; requirement (4) is not always satisfied:

>>> o == "foo"
true
>>> p == "foo"
false
>>> o == o
true
>>> p == o
false

FWIW, here's the definition of the function getProperties, which is used by the tag function. Criticisms welcome. (WARNING: I'm a completely clueless JS noob who doesn't know what he's doing! Use this function at your own risk!)

function getProperties(o) {
  var seen = {};
  function _properties(obj) {
    var ret = [];
    if (obj === null) {
      return ret;
    }
    try {
      var ps = Object.getOwnPropertyNames(obj);
    }
    catch (e if e instanceof TypeError &&
                e.message === "obj is not an object") {
      return _properties(obj.constructor);
    }
    for (var i = 0; i < ps.length; ++i) {
      if (typeof seen[ps[i]] === "undefined") {
        ret.push(ps[i]);
        seen[ps[i]] = true;
      }
    }
    return ret.concat(_properties(Object.getPrototypeOf(obj)));
  }
  return _properties(o);
}
Was it helpful?

Solution

I think you're overcomplicating all of this. There's no reason you need to store the tag on the object itself. If you create a separate object that uses the object's pointer as a key, not only will you conserve space, but you'll prevent any unintentional collisions should the arbitrary object happen to have a property named "_tagged".

var __tagged = {};

function tag(obj){
    __tagged[obj] = true;
    return obj;
}

function isTagged(obj){
    return __tagged.hasOwnProperty(obj);
}

function getTagged(obj){
    if(isTagged(obj)) return obj;
}

== EDIT ==

So I decided to take a minute to create a more robust tagging system. This is what I've created.

var tag = {
    _tagged: {},

    add: function(obj, tag){
        var tags = this._tagged[obj] || (this._tagged[obj] = []);
        if(tag) tags.push(tag);
        return obj;
    },

    remove: function(obj, tag){
        if(this.isTagged(obj)){
            if(tag === undefined) delete this._tagged[obj];
            else{
                var idx = this._tagged[obj].indexOf(tag);
                if(idx != -1) this._tagged[obj].splice(idx, 1);
            }
        }
    },

    isTagged: function(obj){
        return this._tagged.hasOwnProperty(obj);
    },

    get: function(tag){
        var objects = this._tagged
          , list = []
        ;//var

        for(var o in objects){
            if(objects.hasOwnProperty(o)){
                if(objects[o].indexOf(tag) != -1) list.push(o);
            }
        }

        return list;
    }
}

Not only can you tag an object, but you can actually specify different types of tags and retrieve objects with specific tags in the form of a list. Let me give you an example.

var a = 'foo'
  , b = 'bar'
  , c = 'baz'
;//var

tag.add(a);
tag.add(b, 'tag1');
tag.add(c, 'tag1');
tag.add(c, 'tag2');

tag.isTagged(a); // true
tag.isTagged(b); // true
tag.isTagged(c); // true

tag.remove(a);
tag.isTagged(a); // false

tag.get('tag1'); // [b, c]
tag.get('tag2'); // [c]
tag.get('blah'); // []

tag.remove(c, 'tag1');
tag.get('tag1'); // [b]

OTHER TIPS

You're over complicating it :

var tag = function(o) {
    Object.defineProperty(o, '__tagged', {
        enumerable: false,
        configurable: false,
        writable: false,
        value: "static"
    });
    return o;
}

var isTagged = function(o) {
    return Object.getOwnPropertyNames(o).indexOf('__tagged') > -1;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top