Search code examples
javascriptdomfunction-prototypes

Strange attribute behaviour when using HTMLElement as prototype of custom Function


I'm trying to "extend" (not sure what is the correct term for this kind of extensions) a DOMElement to override certain properties (base element). To do so I dynamically create a function whose prototype is the DOMElement I want to extend, and then instance an object with that function (extended element).

When assigning a value to the extended element the expected outcome would be that a property with the given name is created in the extended element, leaving the base element untouched, even if it has a property with that same name. Doing so with a plain object as a base element works flawlessly, but when using a DOMElement as the base element, the base element is modified if a property by the same name exists (On Chrome and Firefox. On IE9 an "Invalid calling object" is thrown even when trying to get the property's value).

Test code:

var base = $("<input type='text' />")[0]; 
//Plain object example: var base = {value:null}
base.value = "original";

var MyClass = function(){ };
MyClass.prototype = base;

var obj = new MyClass();
obj.value = "modified";

obj.value    //"modified" (OK)
base.value   //"modified" (WRONG!) - "original" if plain object is used (OK)

Application:

What I intended to do was to create an extended instance of an input element with an override "value" attribute. This would allow passing this object to a validator plugin that would use the overridden value to run all of it's validation rules, and in case of any error the original object would remain intact, and the extended one with all the modifications done by the validator.

Due to the probably unsolvable exception thrown in IE we are not going to further apply this method, but I'm still intrigued by this strange behavior.

What the problem might be:

I think that this behavior comes from the fact than a DOMElement attributes and functions are not pure Javascript, but an exposition of an Interface of the browser implementation. Being so affects how the prototype "inheritance" in this case works, making the native setter implementation for the value to act instead of the creation of the property on the extended object. (This can be supported by the exception thrown in IE, stating that I'm trying to access a native function/property from a non-native element).


Solution

  • This seems to have done the trick:

    var base = $("<input type='text' />")[0];
    base.value = "original";
    
    function MyClass() {
      Object.defineProperty(this, "value", {
        value: null,
        configurable: true,
        enumerable: true,
        writable: true
      });
    }
    MyClass.prototype = base;
    
    var obj = new MyClass();
    obj.value = "modified";
    
    console.log(obj.value);    //"modified" (OK)
    console.log(base.value);   //"original" (OK)
    

    The problem is that your obj instance never had its own value: it was always delegating to the prototype's setter, i.e. to HTMLInputElement.prototype.value. So all instances of obj would share this same value property.

    In other words, saying obj.value = "modified" was the same as saying Object.getPrototypeOf(obj).value = "modified", and of course Object.getPrototypeOf(obj) === base.

    My first try was to give MyClass its own per-instance value property, like so:

    function MyClass() {
        this.value = null;
    }
    

    But this doesn't actually do what we wanted, because this.value just references the prototype's value: we're not creating a new property; we're setting the existing one. Object.defineProperty makes it very explicit to the runtime that we want to create a new one, though, so that does the trick.