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
tuple
prototype. I defined other geometric concepts as abstractions in terms of
tuple
s.┌─────────────┬────────┬────────────────────────┐ │ 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
toString
depend on properties of theprototype
. - Coordinates are often denoted as
(x, y)
, which is already the defaulttuple
notation. - I decided to represent
dimensions
as|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 ', '
.