4

I have the following nested data structure:

{ x: { y: 'foo', z: 'bar' } }

This data is stored on a prototype object, which is the parent of both concrete objects and other prototypes.

I want to assign to the x.y property of inheriting objects without affecting the parent prototype. Also, I want accesses to x.z to be delegated to the prototype.

What is the best way to do this?

Here is an executable snippet which better illustrates what I want to accomplish:

// Prototype with nested properties.
var prototype = {
    question: 'Am I nested?',
    nested: {
        answer: 'Yes.',
        thoughts: 'I like being nested.'
    }
};

// Another prototype. Overrides properties.
var liar = Object.create(prototype);
liar.nested.answer = 'N-No...!'; // Modifies prototype since
                                 // liar.nested === prototype.nested

// I could do this, but then I lose the other nested properties.
var indecisive = Object.create(prototype);
indecisive.nested = { answer: 'I dunno?' }; // New object, won't delegate.

// Output some text on the snippet results panel.
function results(text) { results.element.appendChild(document.createTextNode(text + '\n')); }; results.element = document.getElementById('results'); results.json = function() { for(var i = 0, len = arguments.length; i < len; ++i) { results(JSON.stringify(arguments[i], results.json.replacer, results.json.indentation)); } }; results.json.replacer = function(k, v) { return typeof v === 'undefined' ? 'undefined' : v; }; results.json.indentation = 4;

results.json(
  prototype.nested.answer,    // Shouldn't have been changed.
  liar.nested.answer,         // As expected.
  indecisive.nested.answer,   // As expected.

  prototype.nested.thoughts,  // As expected.
  liar.nested.thoughts,       // As expected.
  indecisive.nested.thoughts, // Undefined, when it should delegate to the prototype.

  prototype,                  // It's been modified.
  liar,                       // It's empty. It should have an own property.
  indecisive                  // Has own property, but nested object does not delegate.
);
<html>
  <body>
    <pre id="results"></pre>
  </body>
</html>

This matter is complicated because the nested property refers to the same object in both the prototype and the objects linked to it. If I assign to object.nested.answer, it changes object.nested, which is the same object referenced by prototype.nested.

Such an assignment affects all objects in the prototype chain. I want it to be a local change. I want a new object.nested.answer hierarchy to be created as own properties of object.

I could assign a new object to object.nested. However, while that would have the intended effect of creating an own property on object, it would also override object.nested.thoughts. In order to work around that, I would have to repeat myself by supplying a copy of prototype.nested.thoughts as part of the new object.

That would break referential integrity. If I were to deliberately change prototype.nested.thoughts at runtime with the intention of changing all objects that did not deliberately override that property, object would not be changed; it would continue to reference its own local copy of nested.thoughts.

So, what I want to do is get the nested objects to delegate property access to their parent's prototype. Can this be done? If so, how?


The context which spawned this question

I was writing something akin to a small geometry library. I think I got too creative with my API design, ran into certain limitations, couldn't find any pattern that seemed to match what I was trying to do and finally asked this question to see if there was any known solution.

Here's my train of thought and an executable snippet at the end:

  1. In analytic geometry, ordered lists of numbers are common. So, I created a tuple prototype.
  2. I defined other geometric concepts as abstractions in terms of tuples.

    ┌─────────────┬────────┬────────────────────────┐
    │ Abstraction │  Pair  │      Description       │
    ├─────────────┼────────┼────────────────────────┤
    │ Coordinates │ (x, y) │ Cartesian coordinates. │
    │  Dimensions │ (w, h) │ Width and height.      │
    └─────────────┴────────┴────────────────────────┘
    
  3. I thought it would be nice to give each of those abstractions a different notation.

    1. In order to do that, I made toString depend on properties of the prototype.
    2. Coordinates are often denoted as (x, y), which is already the default tuple notation.
    3. I decided to represent dimensions as |w, h|.
    4. This led me to identify the following notational structure:

      ┌─────────────┬────────────────────┬───────────────────────────────────┐
      │ Abstraction │      Notation      │              Grammar              │
      ├─────────────┼────────────────────┼───────────────────────────────────┤
      │       Tuple │ (x₀, x₁, ... , xᵢ) │ '(' element ( ', ' element )* ')' │
      ╞═════════════╪════════════════════╪═══════════════════════════════════╡
      │ Coordinates │       (x, y)       │ Inherits the grammar of Tuple.    │
      ├─────────────┼────────────────────┼───────────────────────────────────┤
      │  Dimensions │       |w, h|       │ '|' element ( ', ' element )* '|' │
      └─────────────┴────────────────────┴───────────────────────────────────┘
      

// 1. Tuple prototype.

var tuple = {
    prototype: {
        toString: function tuple_toString() {
            return '(' + this.elements.join(', ') + ')';
        }
    },
    new: function tuple_new() {
        var tuple = Object.create(this.prototype);
        tuple.elements = Array.prototype.slice.call(arguments);
        return tuple;
    }
};

// 2. Geometric concepts.

var coordinates = {
    prototype: Object.create(tuple.prototype),
    new: tuple.new
};

var dimensions = {
    prototype: Object.create(tuple.prototype),
    new: tuple.new
};

// 3.1 Prototype properties in the toString function.

tuple.prototype.toString = function tuple_toString() {
    var elements = this.elements,
        notation = this.notation, // Should be inherited from the prototype.
        join     = notation.join
        brackets = notation.brackets,
        open     = brackets.open,
        close    = brackets.close;

    return open + elements.join(join) + close;
};

// 3.4 Notational metadata in prototype.

tuple.prototype.notation = {
    brackets: {
        open: '(',
        close: ')'
    },
    join: ', '
};

dimensions.prototype.notation = {
    brackets: {
        open: '|',
        close: '|'
    }
    // Is the ', ' from tuple.prototype.notation.join inherited?
};

// Output some text on the snippet results panel.
function results(text) { results.element.appendChild(document.createTextNode(text + '\n')); }; results.element = document.getElementById('results'); results.json = function() { for(var i = 0, len = arguments.length; i < len; ++i) { results(JSON.stringify(arguments[i], results.json.replacer, results.json.indentation)); } }; results.json.replacer = function(k, v) { return typeof v === 'undefined' ? 'undefined' : v; }; results.json.indentation = 4;

var triplet = tuple.new(1, 2, 3);
var origin  = coordinates.new(0, 0);
var fullHD  = dimensions.new(1920, 1080);

results.json(
  triplet.toString(),    // (1, 2, 3) - As expected.
  origin.toString(),     // (0, 0) - As expected.
  fullHD.toString(),     // |1920,1080| - Where is the space?

  triplet.notation.join, // ', ' - As expected.
  origin.notation.join,  // ', ' - As expected.
  fullHD.notation.join   // undefined
);
<html>
  <body>
    <pre id="results"></pre>
  </body>
</html>

fullHD.notation.join is returning undefined. When that is passed as an argument to Array.prototype.join, it acts as if no argument was passed at all, resulting in the default behavior of the function.

I wanted it to delegate to tuple.prototype.notation.join, which would then return ', '.

Matheus Moreira
  • 17,106
  • 3
  • 68
  • 107
  • I'm having a hard time following what your question really is. Anything on the prototype is designed to be shared among all instances created from that prototype. That's what the prototype is for - shared properties. If you want it shared among all instances, put it on the prototype. If you don't want it shared, then assign/initialize the property in the constructor or some other method after the object is created or create it by making a copy of some existing object. – jfriend00 Nov 01 '15 at 07:03
  • @jfriend00, I have the prototype chain `A > B`. They have some metadata organized as a hierarchy of objects: `A.x = { y: { z: 'z', w: 'w' } }`. `B`'s metadata is slightly different than `A`'s, so I did `B.x.y.z = 'n'`, trusting that prototypal inheritance would look up `A` in order to provide the rest of the metadata to instances of `B`, but it didn't work due to the reference semantics of the nested objects. In `B.x.y.z`, the property lookup stops on `x`. It finds the `A.x` object and modifies it. I can't assign `B.x = { y: { z: 'n' } }` because then it won't find `A.x.y.w`. – Matheus Moreira Nov 01 '15 at 07:41
  • I still don't really follow. Prototypes are all references. If you do `A.x = {...}` then that replaces the entire `A.x` property with an entirely new object and it assigns that new property to the `A` instance directly (an own property), not to the prototype. Nothing that was previously in `A.x` will still be there. Everything is replaced with an entirely new object. FYI, what you have is nested objects stored in the prototype. It doesn't appear that the prototype does what you want it to. – jfriend00 Nov 01 '15 at 07:46
  • @jfriend00, yes. I want the objects on `B` to be empty save for the differences, so that it will look up the missing properties on `A`. But since the objects are nested, it's not doing that because `B.x.y.w` will try to look for `w` in `Object.prototype` instead of `A.x.y`. The question is about a way to get around that. If objects look like this: `A = { x: { y: { z: 'z', w: 'w' } } }; B = { x: { y: { z: 'n' } } };` with `A > B`, can I somehow make `B.x.y.w` try to find `w` in `A.x.y` even though `A.x !== B.x`? – Matheus Moreira Nov 01 '15 at 08:05
  • Maybe if you backed up and described a real world problem you're trying to solve rather than all this theoretical scenario, we might have a better idea what design pattern might work for you. I don't follow what actual problem you're trying to solve and don't know what `A > B` even means. `A` and `B` are two objects. Object comparisons don't have `>` or `<` operators, only identity. – jfriend00 Nov 01 '15 at 08:07
  • Sure, I can provide the context which spawned this question. I had the feeling it was too localized but kept the markdown just in case, so I'll just add it to the question. I used `A > B` to denote prototypal inheritance, as in `A.isPrototypeOf(B) === true`. – Matheus Moreira Nov 01 '15 at 08:18
  • So, Javascript does not have the ability to replace a parent object and have child property references on that object be inherited from some place else. It just doesn't do that. When doing `A.x.y`, the `y` property must be a property of `x`. It can be either an own property on `x` or on the prototype of `x`, but it MUST be on `x`. It can't be on `A` or A's prototype. So, it appears you're trying to replace `x` with some other object, but have `x.y` still be present from some other source that is not the new `x`. You just can't do that in Javascript. – jfriend00 Nov 01 '15 at 08:26
  • If you had specific properties, then you could create `get` methods for those properties and virtualize where the property actually comes from, but you'd have to do that for every individual property you want to have this special behavior. – jfriend00 Nov 01 '15 at 08:28
  • @jfriend00, I have added information to my question regarding the context which spawned it. Please take a look at the code I was developing at the time. – Matheus Moreira Nov 01 '15 at 08:44
  • `fullHD.notation.join` will only lookup `join` as an own property on `fullHD.notation` or on the `fullHD.notation` prototype. It will not look up a `notation.join` property on the `fullHD` prototype. Javascript just does not work that way. – jfriend00 Nov 01 '15 at 08:59
  • @jfriend00, can't we make something like that possible with code and complex prototype hierarchies? – Matheus Moreira Nov 01 '15 at 09:22
  • No, you can't use prototypes for that by themselves. As I said earlier, you could use getters for each property to try to virtualize where the actual property value comes from (your getter could manually look in some other prototype) and you might be able to do something with ES6 proxy objects (I'm not quite sure). Javascript prototypes only work one level at a time. You can't look up `notation.join` in a prototype. You'd have to lookup `notation` and then that `notation` object would have to then have a `.join` property. – jfriend00 Nov 01 '15 at 09:27
  • @jfriend00, what if we made all of `B`'s nested objects inherit from `A`'s nested objects as their prototypes? That way, `B.x.y.w` will resolve all the way to `B.x.y` and will ask its prototype `A.x.y` for the `w` property. – Matheus Moreira Nov 01 '15 at 09:40
  • @MatheusMoreira: Yes, that's the solution. Let `dimensions.prototype.notation = Object.create(tuple.prototype.notation)` before you overwrite the `.brackets` property on it. You could even go as far as letting `dimensions.prototype.notation.brackets` inherit from `tuple.prototype.notation.brackets`. – Bergi Nov 01 '15 at 11:00

0 Answers0