1

Consider a pattern like this, where we want to define a nice getter/setter interface for a property to hide some internal validation:

var validThings = {
    'pretty name a': 3293293,
    'pretty name b': 8275850,
    'pretty name c': 2983855
};


function Constructor() {
    var internalThing = {
        name: 'pretty name a',
        id: '3293293'
    };

    Object.defineProperty(this, 'thing', {
        get: function() { return internalThing.name; },
        set: function(val) {
            var id = validThings[val];
            if (id) internalThing = { name: val, id: id };
        }
    });
}

This lets us avoid worrying about the ids so we can just set 'thing' like this:

var constructoid = new Constructor();
constructoid.thing = 'pretty name b';
constructoid.thing; // 'pretty name b';

And of course it prevents us from setting it to an invalid value:

constructoid.thing = 'pretty name d';
constructoid.thing; // 'pretty name b';

But let's say we do want to be able to access the ID from outside the object as well. The most natural interface for that would be as a property of thing

constructoid.thing.id

But how would one go about defining such a property when 'thing' is itself a getter/setter? I am used to being able to throw properties on anything if needed, in JS -- functions, arrays, ice cream cones, whatever. But it seems to not be possible in this case, at least not in any way I've been able to think of.

Object.defineProperty(this.thing, 'id', {...}) // <-- error

Of course, I can simply define a property on the Constructor object itself, something like 'thingID', or I could return both the name and ID from the thing getter. I'm not looking for a solution like that, however obvious; this is a hypothetical question about whether it's actually possible to define a property on a defined property.

Semicolon
  • 6,793
  • 2
  • 30
  • 38
  • Well, you cannot make the same property return different values. You have to use two different property names. – Felix Kling Mar 26 '14 at 22:52
  • The question was about assigning a child property to an accessor property -- which in this case is a string, adding to the trouble. Note that you actually can just do defineProperty(this.thing, ...) if the defined property is a value rather than an accessor. I did find a roundabout way to do it with an accessor, though (see below). – Semicolon Mar 26 '14 at 23:13

1 Answers1

0

I found a way:

    Object.defineProperty(this, 'thing', {
        get: function() {
            var name = internalThing.name;
            if (!name) return;
            name = new String(name);
            Object.defineProperty(name, 'id', {
                get: function() { return internalThing.id; }
            });
            return name;
        },
        set: function(val) {
            var id = validThings[val];
            if (id) internalThing = { name: val, id: id };
        }
    });

I've never had an occasion to use the String constructor before, but it turns out it allows you to add properties to a specific string. If anyone has a better approach, I'm still curious about other ways to achieve this effect.


Edit:

As Felix Kling pointed out, String as a constructor is too much trouble. The above pattern would work without caveats if the value returned by the first getter weren't primitive, so it could be useful in some situations, but for primitives I don't think there's any good solution. The object requirement is true for value properties as well -- although unlike accessors, defineProperty will accept a value-type previously defined property as its first argument. Presumably a value property is considered a real object, while an accessor property is a ... phantom abstraction? Whatever it is, you can't pin things to 'it' until 'it' exists (i.e., within a get function).

Semicolon
  • 6,793
  • 2
  • 30
  • 38
  • This can create problems in the long run. For example, given `constructoid.thing = 'foo';`, `constructoid.thing === 'foo'` will return `false`. In general you should avoid using the object equivalent to primitive data types. – Felix Kling Mar 26 '14 at 23:27
  • Agreed. This is mostly a can-it-be-done question. That said, the actual bit of code I was writing that made me wonder (not the contrived example in the question) will actually benefit from this solution, and I am solidly in the "== unless you don't want coercion" camp as opposed to the "=== unless you do want coercion" camp when it comes to code I write for myself. – Semicolon Mar 26 '14 at 23:47
  • (Not that my equality preference eliminates the potential problem, but it does make it less likely to be an issue. But I should ask: are there other downsides to new String() aside from failing strict equality with primitive strings?) – Semicolon Mar 26 '14 at 23:50
  • 1
    Mmh. `typeof` checks would also fail. Aside from that I cannot think of anything else off the top of my head. – Felix Kling Mar 27 '14 at 01:22
  • Good point. And I suppose instanceof WOULD work ... I do wonder why String and Number are accessible as constructors given their inherent incompatibility. I don't think I've ever seen them used before. I probably won't use it either -- even if it doesn't cause me problems, it's probably too idiosyncratic to justify. – Semicolon Mar 27 '14 at 01:38