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:
- In analytic geometry, ordered lists of numbers are common. So, I created a
tupleprototype. 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. │ └─────────────┴────────┴────────────────────────┘I thought it would be nice to give each of those abstractions a different notation.
- In order to do that, I made
toStringdepend on properties of theprototype. - Coordinates are often denoted as
(x, y), which is already the defaulttuplenotation. - I decided to represent
dimensionsas|w, h|. 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 )* '|' │ └─────────────┴────────────────────┴───────────────────────────────────┘
- In order to do that, I made
// 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 ', '.