Idea: Extending native DOM prototypes without collisions

As I pointed out in yesterday’s blog post, one of the reasons why I don’t like using jQuery is its wrapper objects. For jQuery, this was a wise decision: Back in 2006 when it was first developed, IE releases had a pretty icky memory leak bug that could be easily triggered when one added properties to elements. Oh, and we also didn’t have access to element prototypes on IE back then, so we had to add these properties manually on every element. Prototype.js attempted to go that route and the result was such a mess that they decided to change their decision in Prototype 2.0 and go with wrapper objects too. There were even long essays being written back then about how much of a monumentally bad idea it was to extend DOM elements.

The first IE release that exposed element prototypes was IE8: We got access to Node.prototype, Element.prototype and a few more. Some were mutable, some were not. On IE9, we got the full bunch, including HTMLElement.prototype and its descendants, such as HTMLParagraphElement. The memory leak bugs were mitigated in IE8 and fixed in IE9. However, we still don’t extend native DOM elements, and for good reason: collisions are still a very real risk. No library wants to add a bunch of methods on elements, it’s just bad form. It’s like being invited in someone’s house and defecating all over the floor.

But what if we could add methods to elements without the chance of collisions? (well, technically, by minimizing said chance). We could only add one property to Element.prototype, and then hang all our methods on that. E.g. if our library was called yolo and had two methods, foo() and bar(), calls to it would look like:

var element = document.querySelector(".someclass");
element.yolo.foo();
element.yolo.bar();
// or you can even chain, if you return the element in each of them!
element.yolo.foo().yolo.bar();

Sure, it’s more awkward than wrapper objects, but the benefit of using native DOM elements is worth it if you ask me. Of course, YMMV.

It’s basically exactly the same thing we do with globals: We all know that adding tons of global variables is bad practice, so every library adds one global and hangs everything off of that.

However, if we try to implement something like this in the naïve way, we will find that it’s kind of hard to reference the element used from our namespaced functions:

Element.prototype.yolo = {
	foo: function () {
		console.log(this); 
	},
	
	bar: function () { /* ... */ }
};

someElement.yolo.foo(); // Object {foo: function, bar: function}

What happened here? this inside any of these functions refers to the object that they are called on, not the element that object is hanging on! We need to be a bit more clever to get around this issue.

Keep in mind that this in the object inside yolo would have access to the element we’re trying to hang these methods off of. But we’re not running any code there, so we’re not taking advantage of that. If only we could get a reference to that object’s context! However, running a function (e.g. element.yolo().foo()) would spoil our nice API.

Wait a second. We can run code on properties, via ES5 accessors! We could do something like this:

Object.defineProperty(Element.prototype, "yolo", {
	get: function () {
		return {
			element: this,
			foo: function() {
				console.log(this.element);
			},
			
			bar: function() { /* ... */ }
		}
	},
	configurable: true,
	writeable: false
});

someElement.yolo.foo(); // It works! (Logs our actual element)

This works, but there is a rather annoying issue here: We are generating this object and redefining our functions every single time this property is called. This is a rather bad idea for performance. Ideally, we want to generate this object once, and then return the generated object. We also don’t want every element to have its own completely separate instance of the functions we defined, we want to define these functions on a prototype, and use the wonderful JS inheritance for them, so that our library is also dynamically extensible. Luckily, there is a way to do all this too:

var Yolo = function(element) {
	this.element = element;
};

Yolo.prototype = {
	foo: function() {
		console.log(this.element);
	},
	
	bar: function() { /* ... */ }
};

Object.defineProperty(Element.prototype, "yolo", {
	get: function () {
		Object.defineProperty(this, "yolo", {
			value: new Yolo(this)
		});
		
		return this.yolo;
	},
	configurable: true,
	writeable: false
});

someElement.yolo.foo(); // It works! (Logs our actual element)

// And it’s dynamically extensible too!
Yolo.prototype.baz = function(color) {
	this.element.style.background = color;
};

someElement.yolo.baz("red") // Our element gets a red background

Note that in the above, the getter is only executed once. After that, it overwrites the yolo property with a static value: An instance of the Yolo object. Since we’re using Object.defineProperty() we also don’t run into the issue of breaking enumeration (for..in loops), since these properties have enumerable: false by default.

There is still the wart that these methods need to use this.element instead of this. We could fix this by wrapping them:

for (let method in Yolo.prototype) {
	Yolo.prototype[method] = function(){
		var callback = Yolo.prototype[method];
		
		Yolo.prototype[method] = function () {
			var ret = callback.apply(this.element, arguments);
			
			// Return the element, for chainability!
			return ret === undefined? this.element : ret;
		}
	}
}

However, now you can’t dynamically add methods to Yolo.prototype and have them automatically work like the native Yolo methods in element.yolo, so it kinda hurts extensibility (of course you could still add methods that use this.element and they would work).

Thoughts?

  • Miles

    I like this approach for polyfills. General usage seems like a step backward. At least with jQuery you know it’s there. This has the potential to hide and surprise when the maintenance dev comes in to fix a bug.

    • So your argument is “we haven’t been doing it this way in the past, it could confuse people!!”? If so, then following this argument, nothing should ever change.

  • 4esn0k

    custom elements + Symbols ?

    • Care to elaborate? I’ve just had a twitter discussion about how symbols + proxies could perhaps help (though not pragmatic right now, due to poor support, and probably not a good idea altogether) but how would custom elements help in executing helper methods on your existing elements?

      • 4esn0k

        @LeaVerou:disqus, well on existing elements – it will not help

        • WebReflection

          You can define a Symbol as Element.prototype getter, and do the magic. Yes, thanks to their unique-id like feature, I’d say to avoid names clashing Symbols are quite a good option indeed.

      • WebReflection

        about poor support … not so poor after all 😉 I grought them everywhere but IE <= 8 http://webreflection.blogspot.co.uk/2015/04/bringing-symbols-to-es5.html ( I guess you busy person stopped reading my blog 😛 )

        • WebReflection

          *brought

        • lozandier

          Kudos for the effort to bring Symbols in ES5; that’s pretty impressive…

  • Sylvain Pollet

    Element.prototype does not contain any namespace objects, only methods so far, so it would make sense to have a yolo() method that either takes command parameters or return a contextualised Yolo object. I believe that we should not add too much to the elements prototypes, at a pinch simple gateways to our API as syntactic sugar but no more. The lesson learned by Prototype.js is still relevant: you should not touch what you don’t own.

    • You don’t own the global scope either, but most libraries add at least one global. But agree that you shouldn’t clutter them, hence why I wrote this post.

      • Sylvain Pollet

        most of “good” libraries also provide a UMD wrapper 🙂

        • Michael Messing

          UMD adds ‘define’ to the global namespace… seems like they shouldn’t be messing with things they don’t own 😉

          but seriously, as a developer I want to have control over all types. This is Javascript, this is why the language is awesome. This is why Ruby is awesome too. We shouldn’t give up on writing elegant code just because of FUD and some made up rule. Also, wasn’t the lesson of Prototype.js not to extend Object.prototype because of for..in, not all native prototypes ever…

        • Sylvain Pollet

          I don’t know what UMD wrapper you use, but it should not add stuff to the global namespace if not necessary, otherwise it would make no sense. Personnally I use this one : https://www.snip2code.com/Snippet/319662/UMD-wrapper-for-Chosen

        • He’s referring to define() itself. Where do you think that is? Hint: It starts with “global” and ends with “scope”.

        • Sylvain Pollet

          I got it. But its not the UMD wrapper that adds ‘define’, it is the user who chose to use AMD. Also, I would appreciate if you avoid sarcasm

  • MaxArt

    Proxies can solve the problem of `this` in accessing methods of the `yolo` property of the element, using a getter that binds the function defined in `Yolo.prototype` (or any other object that serves as a prototype, just like `jQuery.fn` for jQuery objects) to the current element.
    Of course this is all only supported by Firefox at the moment, while the rest of the article is good for IE9+ (except that `let` that can be easily converted to `var`), so take it as a proof of concept. Also, I’m not sure about the performances.

    • lozandier

      I pointed this out myself on Twitter, but someone rightfully pointed out the performance costs may not be worth it.

      It’s similar to the performance concerns some have w/ Object.observe.

    • Simon Kc Leung

      I tried using Proxy.
      [code]
      (function(){
      “use strict”
      var element, style, cssppt=””;

      function css(val){
      if (cssppt) {
      if (arguments.length===1){
      style[cssppt]=val;
      cssppt=””;
      return cssproxy;
      } else {
      val=style[cssppt];
      element=style=null;
      cssppt=””;
      return val;
      }
      }
      val=element;
      element=style=null;
      return val;
      }

      css.toString=function toString(){
      return style[cssppt];
      }

      var cssproxy=new Proxy(css,{
      get:function(target,name){
      cssppt=name;
      return target;
      },
      set:function(target,name,val){
      style[name]=val;
      }
      });

      Object.defineProperty(HTMLElement.prototype,”css”,{
      get:function(){
      element=this;
      style=this.style;
      return cssproxy;
      }
      });
      })();

      document.body.css.color(“red”).backgroundColor=”blue”;
      document.body.appendChild(document.createElement(“div”).css.backgroundColor(“white”).border(“5px solid green”).width(“500px”).height(“500px”)());
      [/code]

  • Thanks to your sharing, this post is helpful to me. —- An usable but ugly solution…

  • WebReflection

    The idea is OK (I use that already in many cases) but lazy descriptor assignment, specially over a getter, is a dirty operation full of surprises in IE9 Mobile **and** basically all Android 2.X phones ( plus others oldies with buggy WebKit ). I’ve debugged all cases and never talked about this library called lazyval https://github.com/WebReflection/lazyval#lazyval which solves for both generic objects and prototype lazy assignment. I’ve talked about this bug in my third “What Books Didn’t Tell You About ES5 Descriptors” post http://webreflection.blogspot.co.uk/2014/03/what-books-didnt-tell-you-about-es5_28.html#getters-and-setters-bug

    • WebReflection

      forgot .. with lazyval your example would look like: `lazyval.proto(Element.prototype, ‘yolo’, function () { return new Yolo(this); });`

  • Jorge Callalle

    Thanks to your sharing Lea

  • Nathan Bubna

    One of the other things you lose with this approach is type-specific extensions. You can add your ‘yolo’ to Element.prototype, and then extend Yolo to give all your elements the same enhancement. But what if you want to only create an extension soley for HTMLParagraphElements? There’s no opportunity to exploit inheritance and polymorphism.

    I bring this up because i considered this very approach when working on my DOMx library. But it undercut my intention to embrace the native types fully. It suffers from the same problem as jQuery extensions. If you want to extend a single element type, you end up with that method on all elements, even those that have no business with such a method.

    To solve that, i actually pulled together code that walked the prototype chain for any element for which the ‘yolo’ (‘x’ in my experiment) property was accessed. It then created a parallel prototype chain. This chain was cached and there was a simple API to extend specific DOM element types that hooked into that chain. And i got it working in all major desktop browsers, but i abandoned it before moving on to mobile testing.

    It was too much, too complex, too clever for me to trust it. It wasn’t worth all of that just to avoid a bit of namespace pollution and keep a line between what was really native and what was not. The reality is that namespace collisions of this sort are not that scary. Extending Element.prototype is not all the same as polluting the global namespace. If people can (and do) regularly avoid namespace collisions with their jQuery extensions (which all drop on $), then they can (and will) just as easily avoid collisions on Element.prototype and friends.

    That’s not to say that collisions won’t happen. It’s just to say that all this convoluted effort to avoid them is far more trouble than is justified by the relative simplicity of resolving such collisions if and when they happen.

    • lozandier

      Regarding your paragraph element example, I would seriously create a custom element at that point that extends from a paragraph; using an ES5-centric, it’d be the following to avoid using a ES6 (or CoffeeScript) class for the sake of clarity.


      let CustomParagraphElementProto = Object.create(HTMLParagraphElement.proto);

      CustomParagraphProto.createdCallback = {
      // Custom properties defined here
      }

      // Other Callbacks defined like CustomParagraphElementProto.attributeChanged

      let CustomParagraphElement = document.registerElement('custom-paragraph', { prototype: CustomParagraphElementProto, extends: 'p'})

      From there, you can then take advantage of the following ways of accessing the custom element:

      // In HTML
      [p is="custom-paragraph"]I'm a custom element (brought to you by Talor Swift)[/p]

      //**Edit: Disqus doesn't let me use a p so I'm going to replace the brackets

      //JS ways
      let myCustomParagraph = document.createElement('p', 'custom-paragraph');
      document.body.appendChild(myCustomParagraph);

      OR

      document.body.innerHTML = '';

      • Nathan Bubna

        Custom elements are great and, yes, are a good solution for some element-specific needs, but not for all. Utility methods, in particular, should not require semantic markup changes just to have them available without making them available on every single method.

        Respecting the prototype chain and differences between elements also allows for polymorphic implementations of common functions, both for easier code maintenance and faster implementations. The jQuery extension model of make-every-extension-available-everywhere-regardless-of-whether-it-makes-sense is leads to ugly, slower, branching logic in implementations.

        • lozandier

          I meant to say you should seriously *consider* creating a custom element; I didn’t realize that omission till now.

          You’re totally spot on if the intended changes you want to make doesn’t create a situation where it changes the behavior of a paragraph to the extent it’s a spiritual successor to a paragraph because of the dramatically different behavior you want to realize but want to keep the semantics of a paragraph (which is what custom element inheritance allows if you did).

    • lozandier

      With my earlier example, you can create a suite of custom-elements that someone can just import (via HTML Import today, via ES6 modules potentially in the future) through a single-file using something like vulcanize.

  • Pingback: Bruce Lawson’s personal site  : Reading List()

  • Evan Wieland

    Very interesting Lea! Thanks for this post 🙂

  • Pingback: Revision 216: Working Draft considered harmful | Working Draft()

  • jannes_meyer

    I just wrote a blog post about an alternative approach of extending native dom prototypes locally instead of globally.

    http://jannesmeyer.com/blog/2015/extending-the-prototype-of-built-ins

    This would only work by adding a new functionality to the language (maybe ES8) and through type-aware transpilers, but it would be much cleaner.

    Let me know what you think!

  • felipealexander

    I have been working as a research paper writer in order research paper writing for the past 3 years and now, i have been searching for a topic related to the article title and i am happy to get some related content for my research.

  • Pingback: 【翻译】理念:无冲突的扩展本地DOM原型 – 剑客|关注科技互联网()

  • Nice, I defnitely prefer this way over wrapping elements. It’s less confusing and backwards-compatible. Sure, you’ll have a bunch of yolo’s laying around but you can easily begin to phase it out if for some reason the ES guys end up taking your idea and providing it natively. 🙂

  • Ido Ofir

    Thank you for this one Lea, you got me thinking..

    Since you define your methods directly on Yolo.prototype your context is bound to someElement.yolo.

    but if you change the way you define new methods, you could just apply them to the element:

    Yolo.define = function(key, method){
    Yolo.prototype[key] = function(){ method.apply(this.element, arguments); };
    };

    // if you don’t mind defining your methods like this:

    Yolo.define(‘baz’, function(color){ this.style.background = color; });

    someEl.yolo.baz(‘red’); // this would work..

    btw good luck at the conf tomorrow ; )

    • binjiwang

      hi I also think so.use call/apply to contact with someElement.

      • rahul bandi

        What if I don’t want method…I just want like..dropdown.selectedindex or some element.property..which need to give result from function…can u plz suggest

  • Glenn

    All the solutions proposed here include object creation – either upon ever reference to yolo (first one) or once per element, resulting in a new yolo object on every element (rather than on its prototype – which blocks inheritance).

    I think a better approach is to just define the methods directly on the prototype rather than in a single object property on the prototype – so you call with someElement.yolo_foo() and someElement.yolo_baz(). You could claim that pollutes the prototype much more than a single property, but I think the argument would be purely academic. No browser is likely to create a yolo_baz() method in their host objects. Just choose a distinct prefix.

  • Pingback: CSS conic-gradient() polyfil | Designer News()

  • dapinitial

    If you re-read this article where YMMV means ‘You make me vomit’ you’ll get a good chuckle, especially after the whole inviting me into your house to defecate on your floor bit. 😀

  • Pingback: 【通译】理念:无冲突的扩展本地DOM原型 – JavaScript-java知识分享()

  • Pingback: Introducing Bliss: A 3KB library for blissful Vanilla JS | Lea Verou()

  • Pingback: Google()

  • Pingback: temporomandibular joint disorders Ormond beach()

  • Pingback: patio furniture()

  • Pingback: Used cars()

  • Pingback: intermodal container manufacturer()

  • Pingback: AT&T GoPhone Microsoft Lumia 640 4G LTE()

  • Pingback: PROMOTORAS cancun()

  • Pingback: travel the world()

  • Pingback: Superhero()

  • Pingback: Todays news()

  • Pingback: friv()

  • Pingback: travel()

  • Pingback: see it here()

  • Pingback: mountain coffee classics()

  • Pingback: penis enlargement()

  • Pingback: how to make money on the internet()

  • Pingback: vegetable garden tower()

  • Pingback: Piece Of Heaven()

  • Pingback: viagra()

  • Pingback: gardens()

  • Pingback: equifax()

  • Pingback: Computer Software()

  • Pingback: Instalar kodi paso a paso()

  • Pingback: 福井脱毛()

  • Pingback: 福井脱毛()

  • Pingback: cheap soundcloud promotion()

  • Pingback: legitimate work at home jobs()

  • Pingback: Taxi Airport Zurich St. Anton()

  • Pingback: 마사지()

  • zaid sasa

    Hahaa i already started a project for doing that 2 or 3 yeas ago.

    https://bitbucket.org/eatjs/eatjs and am planning to rewrite the code with test and more abstracted way

  • Pingback: receive sms online virtual number()

  • Pingback: thread()

  • Pingback: Doctor()

  • Pingback: FP25R12KE3()

  • Pingback: life insurance lawyer()

  • Pingback: best 4k android tv box()

  • Pingback: Restaurants in Covent Garden()

  • Pingback: legitimate work from home jobs()

  • Pingback: downtown tampa magazine()

  • Pingback: افلام()

  • Pingback: oakley outlet()

  • Pingback: flashlight brands()

  • Pingback: free online phone number for receiving texts()

  • Pingback: Matka Result()

  • Pingback: Fenster()

  • Pingback: dab jars()

  • Pingback: Entry clearance visa()

  • Pingback: pittsburgh web design()

  • Pingback: gourmet hawaiian coffee()

  • Pingback: gourmet hawaiian coffee()

  • Pingback: gourmet hawaiian coffee()

  • Pingback: gourmet coffee bean()

  • Pingback: custom paint()

  • Pingback: mobile app builder()

  • Pingback: Home Surveillance()

  • Pingback: scandals()

  • Pingback: Home Surveillance()

  • Pingback: e-learning()

  • Pingback: awiz for love()

  • Pingback: http://www.adigalit.co.il/()

  • Pingback: more info()

  • Pingback: information()

  • Pingback: Sex with a Beaver()

  • Pingback: Jumpers for Women Online()

  • Pingback: uk auction()

  • Pingback: venice-hotel-guide.com()

  • Pingback: ski holidays()

  • Pingback: jobs you can do from home()

  • Pingback: Photography()

  • Pingback: регистрация ооо Киев()

  • Pingback: robert()

  • Pingback: vigra()

  • Pingback: black magic()

  • Pingback: 注管理システム()

  • Pingback: istikhara online()

  • Pingback: home improvement ideas()

  • Pingback: Paul Boogeyman()

  • Pingback: skin care()

  • Pingback: satta matka()

  • Pingback: SATTA KING()

  • Pingback: her latest blog()

  • Pingback: Phil Lubitski()

  • Pingback: wooden chairs()

  • Pingback: Free Games online()

  • Pingback: dinteventtal.xyz()

  • Pingback: casino gratis slot()

  • Pingback: Auto Protection Options()

  • Pingback: military soldier blog()

  • Pingback: at&t()

  • Pingback: SEO services in Lahore()

  • Pingback: piano now()

  • Pingback: Click here()

  • Pingback: create an app()

  • Pingback: SEO services in Lahore()

  • Pingback: Download PC Games()

  • Pingback: Download PC Games()

  • Pingback: her explanation()

  • Pingback: Mens Divorce Law Firm()

  • Pingback: here()

  • Pingback: satta matka()

  • Pingback: http://www.mypsychicadvice.com/()

  • Pingback: lava rock Veneers()

  • Pingback: surviving military deployments in afghanistan()

  • Pingback: light deprivation greenhouse()

  • Pingback: Contact us- youtubemp3download3()

  • Pingback: Contact- AtlantaPiano()

  • Pingback: Pinganillos()

  • Pingback: como fazer uma retrospectiva animada()

  • Pingback: read()

  • Pingback: ÇÄÅÑÜ ->>()

  • Pingback: SEO services in Lahore()

  • Pingback: T-Shirt Druck()

  • Pingback: cork coasters()

  • Pingback: Wood fired pizza oven Pizza Party()

  • Pingback: Jual PLTS ONGRID Dan OFFgrid Terpusat()

  • Pingback: Denver office space news()

  • Pingback: diuretic side effects weight loss()

  • Pingback: 受注管理システム()

  • Pingback: Cash for cars melbourne()

  • Pingback: read here()

  • Pingback: Business reading()

  • Pingback: more info()

  • Pingback: Divorce Filing()

  • Pingback: Turen()

  • Pingback: Fenster()

  • Pingback: senior care()

  • Pingback: see this here()

  • Pingback: free slot games book of ra()

  • Pingback: anabolic steroid induced hypogonadism()

  • Pingback: Self-cleaning strainer()

  • Pingback: recipes()

  • Pingback: kala jadu()