Thursday, June 20, 2013

jQuery's extend() Method and Prototypes - A Deadly Combination

I recently have been working with some code where an object hierarchy was useful to me, and I implemented it using this sort of pattern:

function ObjectParent () {
ObjectParent.prototype = {
    constructor: ObjectParent,
    method: function () { ... },
    property: true,
    objectProperty: {
        subMethod: function () { ... },
        subProperty: true
function ObjectChild () {
ObjectChild.prototype = new ObjectParent();
    $.extend(true, ObjectChild.prototype, {
        childMethod: function () { ... },
        childProperty: false
function ObjectGrandChild () {
ObjectGrandChild.prototype = new ObjectGrandChild();
    $.extend(true, ObjectGrandChild.prototype, {
        grandChildMethod: function () { ... },
        childProperty: 'Now this is just getting weird.',
        objectProperty: {
            grandchildSubProperty: true
And it worked ... kind of.   Mostly?  Sort of?
I was seeing stuff from the grand child in the children and in the parent.  Why?  Well, first I took off the deep copy flag $.extend(true, ... ) becomes $.extend(...) and that made everything work.... sort of.

I had those sub-objects that I wanted to be extending and they weren't making it through properly.  So I made an explicit shallow copy for them, which looks like this:

$.extend(ObjectGrandChild.prototype.objectProperty, { grandchildsubProperty: true });

The question becomes ... where is ObjectGrandChild.prototype.objectProperty located?  If that property is actually coming from some previous level of prototypal inheritance, when we extend it in this way, we extend the carrying prototype and tend to pollute our prototype chain.

What we're actually doing here is saying:  take the item referenced by X (ObjectGrandChild.prototype.objectProperty -> ObjectParent.prototype.objectProperty) and update it with values from Y

I like the pattern I wrote, but the consequences of using correct prototypal inheritance are not always obvious.  Remember that object properties coming from the prototype are references and so changes pass through.  There is no way to overlay string / primitive data type properties in the fashion I was attempting.  You will always have to make a 'clone' of the prototype in order to extend it.

Which is easy enough:

ObjectGrandChild.prototype.objectProperty = $.extend({},
    { grandchildsubProperty: true });

Note that what we're doing is this:  redefine the reference A (ObjectGrandChild.prototype.objectProperty) to point to the new object B ({}) which has been created by taking all the direct properties of X (ObjectGrandChild.prototype.objectProperty -> ObjectParent.prototype.objectProperty) and adding / replacing them with the direct properties of Y.

No comments:

Post a Comment