When jQuery fires a callback function, whether it is an event handler, an each iterator, or a filter function, it will normally give you a DOM element as the function’s context, accessible via the this
keyword. It’s common practice to subsequently wrap this
in jQuery(...)
resulting in a newly constructed jQuery instance.
This is no fault of jQuery’s but this practice of redundant “wrapping” generally sucks.
I “tweeted” a while ago, complaining of this:
Constructing a new jq obj on each iteration just to access some text seems wrong. jQuery(‘a’).map(function(){ return $(this).text(); });
Paul Irish suggested that I use jQuery.text
instead of jQuery.fn.text
, meaning that I wouldn’t have to bother with constructing a new jQuery object every single time my function was called. This was a good suggestion but unfortunately not all of jQuery’s methods have corresponding single-node functions, and it would mean not being able to chain methods.
This is a growing problem, and is only worsened by developers’ insistence on constructing multiple jQuery objects with the same element! –
jQuery('a').click(function(){ if (jQuery(this).attr('href') === 'blah') { jQuery(this).next('.whatever').slideToggle(); } jQuery(this).fadeTo('slow', 0.4); return false; }); |
Eew! Not only are there multiple pointless constructors, but Mr. Uber Cool jQuery Developer isn’t accustomed to the DOM, so has absolutely no idea that, in most situations, this.href
would suffice for getting the href
property value. This kind of misuse has been discussed in step #3 here: http://encosia.com/2010/03/30/5-steps-toward-jquery-mastery/.
The real problem remains that there are three jQuery objects being constructed, — I feel that it is a library’s obligation to protect against misuse like this. Even if you’re only constructing one jQuery object in a callback it’s still somewhat pointless, in that you’re only doing so to call a couple of methods…
In light of my concerns I thought it would make sense to experiment with some ways to alleviate this troublesome situation. In the end, I landed on something so dead-simple that I feel silly even shining a spotlight on it:
jQuery.single=function(a){return function(b){a[0]=b;return a}}(jQuery([1])); |
76 characters. Here’s the readable version:
jQuery.single = (function(o){ var collection = jQuery([1]); // Fill with 1 item, to make sure length === 1 return function(element) { // Give collection the element: collection[0] = element; // Return the collection: return collection; }; }()); |
A single jQuery object is being created and is then used for every single call to jQuery.single
— so only one jQuery object is ever being created. If you think about it, we tend to wrap elements in jQuery(...)
(i.e. in a jQuery instance) so that we can have access to jQuery’s methods. Very rarely do we mutate the object itself — even when you call parent()
or children()
, the jQuery object itself isn’t being changed — a new object is being returned. It’s this non-mutability which makes this solution viable.
Obviously, you can’t use it in exactly the same was as jQuery()
; jQuery.single
will only work when you pass a single DOM element. Here’s an example of its usage:
jQuery('a').click(function(){ var html = jQuery.single(this).next().html(); // Method chaining works! alert(html); // etc. etc. }); |
Also, you can’t cache it and save it for later as you normally would… well, actually, you can — but the object will change when you next call jQuery.single
, so be careful! And please note that this is only meant to be used in situations where you have a DOM element that requires immediate wrapping.
You might be thinking that we could do away with this trickery by simply remembering to “cache” our jQuery objects, like so (using the example from before):
jQuery('a').click(function(){ var me = jQuery(this); if (me.attr('href') === 'blah') { me.next('.whatever').slideToggle(); } me.fadeTo('slow', 0.4); return false; }); |
This is a good practice, but is still not quite as speedy as jQuery.single
. I absolutely recommend making up your own mind by carrying out your own tests, but having tested jQuery.single()
myself I can tell you that it performs better.
You can make it even faster by assigning jQuery.single
to a (closely-scoped) variable:
var $_ = jQuery.single; // Use $_(this) ... |
Thanks for reading! Please share your thoughts with me on Twitter. Have a great day!
I like.
I’ve just glanced at this but it seems like there might be a memory leak here. It seems like objects in the single collection are stuck there unless you explicitly delete them. Maybe I’m wrong here but at first glance it kind of looks that way.
@Mike, Hmmm, I’m a bit of an n00b when it comes to memory leaks etc. … Could you help me out? Are you talking about the DOM node being assigned to
collection[0]
? It’ll persist until the next call ofjQuery.single
— is that the problem?James, If use it in event bind, it’ll cause conflict
below is an sample:
@richard, that’s a typical example of where you probably shouldn’t use it ๐ … As I mentioned:
Isn’t this a prefect example of where your JQueryLint validator should come in? i.e. Warning: unnecessary wrapping of JQuery element….
I’ve only starting using jQuery recently and was surprised callbacks were passed the node on its own, rather than inside a jQuery wrapper. It would have been trivial to do the above inside the jQuery core before executing each callback.
@Simon, good point, I’ll see if I can work something in.
@Graham, but there would still be implicit “wrapping”, which would cost just as much.
I would point to your first assertion: “Mr. Uber Cool jQuery Developer isnโt accustomed to the DOM“, and point out that while jQuery is a great tool, it’s not a substitute for learning the native API. If you don’t know the DOM properties and methods, then Please Learn Them Now; you’ll be better for it.
@Howard, I couldn’t agree more! ๐
James, I meant something like this (my own library does something similar internally):
Only one wrapper object gets created.
Isn’t this http://en.wikipedia.org/wiki/Flyweight_pattern?
Would be cool to see jQuery be meeting more of actual computer science.
Oh, and as to DOM scripting – I don’t agree. It’s good to be aware that there is a href and id and whatever property on a DOM element, yet to remember all quirks and cross-browser issues is pointless to me. If I end up with a low performance application, I’ll profile it and remove those extra jQuery instances as needed. Perfection kills, worrying about optimization too much and too early is the root of all evil, etc. On the other hand, if a library has this shared flyweight object implemented, it automatically encourages it’s use (like in Ext JS).
PS.
Since I hear more and more of you people yelling to know the DOM API by heart, I’m actually printing out a cheat sheet and maybe consider using it from time to time. Maybe. ๐
@Graham, that’s a good idea — so, essentially, instead of having to call
jQuery.single
, the wrapped object could be passed to theeach
callback? I think I’m gonna bring this up on the jQ forums when I get a chance. ๐@Marek, I had never heard of the “Flyweight pattern”, so thanks for bringing it up!
Well, obviously, knowing all quirks is out of the question, but I think at least a rudimentary understanding of potential points of unreliability is very important.
wow~~ learn more thing,mark
jQuery should care of cache carefully not completely, cos illegal usage I think probably cause slower or big list of cache items same using search list like binary list and whatever.
This may help better understand how your shortcut works. The following code doesn’t work with jQuery.single:
_$().each([1,2,3],function(){});
//TypeError: Object 1,2,3 has no method 'apply'
$().each([1,2,3],function(){});
//Empty object returned w/jQuery set to prototype
This also gives different results:
_$([1,2,3]).each(function(n,i){ console.log(n,i); });
> 0 [1,2,3]
> Object returned first element is an array
$([1,2,3]).each(function(n,i){ console.log(n,i); });
> 0 1
> 1 2
> 2 3
> Array returned, extending jQuery
> Object