My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, June 24, 2015

Rethinking the $()

If any developer extends or modify native prototypes is doing it wrong ... unless you are Paul Irish?
( please bear with me Paul, this post has nothing against you or your snippet, it's rather on how developers react to whatever you propose since you are well known in the industry ;-) )

I am talking about his bling snippet, and the instant hype around it.

Nope, still doing it wrong

I haven't asked him directly but I believe Paul intent was simply to demonstrate that for a sketchy quick and dirty jQuery like functionality you really don't need to put 7 tools in your chain, a CDN, pick the right version or learn the whole API, you can just go with few lines of code.
The problem is the hype that will inevitably see developers start doing that in production or in their own libraries, using an obtrusive approach which is also not strictly necessary.

Did you know that queryAll returns an Array subclass?

The reason that snippets contains the following line:
NodeList.prototype.__proto__ = Array.prototype
is because querySelectorAll returns a NodeList collection, which is a static collection that does not extend nor inherit from Array.

W3C knows this has been inconvenient for just about everyone, so it's going to replace querySelector and querySelectorAll with query and queryAll, where latter one will return an Array subclass.
Elements is an ES6-style subclass of Array with two additional methods. It's the new NodeList / HTMLCollection.
Accordingly, the little snippet could be even smaller, and also more reliable in terms of results since these new methods not only return array like results, will also fix relative CSS selectors, meaning these are also more reliable/less surprise prone methods.
window.$ = document.queryAll.bind(document);

Node.prototype.on = window.on = function (name, fn) {
  this.addEventListener(name, fn);
}
 
Elements.prototype.on = Elements.prototype.addEventListener = function (name, fn) {
  this.forEach(function (elem, i) {
    elem.on(name, fn);
  });
};
If you are worried about query and queryAll compatibility, you can stop already since there is a poly for that, which will also normalize and fix much more, so nothing to lose.

Yet doing it wrong

No matter how small and pragmatic could be our snippet, we're playing dirty polluting both the global object and at least 2 prototypes. On top of this, add the usage of __proto__, which not only I'd like to not see anymore in favor of Object.setPrototypeOf, but it will also automatically break compatibility with IE < 11, and we realize that a little snippet, seen already as "cool one, will use it" from few developers, should probably stay confined in our browser console for debugging purpose and nothing else.
Accordingly, I think it would be wise not to start publishing libraries based on such snippet, and I also believe Paul would agree with me on this.

We still want the $ of jQuery without the jQuery.

Like the snippet says already, having a $ around is very handy indeed.
That could also be the only thing we need though, so that previous snippet could be simplified as such:
var $ = $ || (function (){
  function on(name, fn) {
    this.forEach(function (el) {
      el.addEventListener(name, fn);
    });
  }
  return function (css, parent) {
    var result = typeof css === 'string' ?
      (parent || document).queryAll(css) : [css];
    result.on = on;
    return result;
  };
}());
If we don't have queryAll or a polyfill in the page, we can simply use this line instead:
// change this
(parent || document).queryAll(css) : [css];

// with this
Array.prototype.slice.call(
  (parent || document).querySelectorAll(css), 0
) : [css];
The snippet gives us the ability to add listeners to window or any node simply doing this:
$(window).on('load', function () { /*...*/ });
See? No need to extend any global object, neither global prototypes ... Hoooorraaaaaay!

What else do we need?

Many already pointed out that having an off method would be handy too, I'd like to add on top the need for a dispatch method so that's also easy to dispatch events as well.
If we then consider that extendability could bring in basically anything else we think we might need, I can tell you I've already described the entire functionality of QueryResult, an utility already available via CDN with a deadly simple API you already know and a subclassing behavior which will gradually and natively integrate with modern browsers as soon as these will ship ES6 classes and subclassing in their engines.
So yes, we can have the $, and we can still have it in few lines of code, without being obtrusive, and via modern approaches such subclassing and queryAll.
Enjoy!

2 comments:

Paul Irish said...

Yeah looks good, man.

I probably shouldn't have given it a name, but I always wanted to use the name. It's an experiment and is mostly for me saving time when prototyping some non-production ready code.

I think using a $ wrapper for on is a decent concession to avoid augmenting native prototypes so I'd probably have to move to that to be real, just with the regression of calling `[0]` a lot.

And yeah, I didnt mention queryAll in the gist, but should have, as that fixes the array subclassing yes. But again this was me trying to do the quick and overly clever 10 line version instead of a more reliable polyfill. :)

Andrea Giammarchi said...

like I was thinking already :-)

> It's an experiment and is mostly for me saving time when prototyping some non-production ready code.

thanks for clarifying that. Cheers