PDA

View Full Version : jQuery $(this) usage



jscheuer1
02-20-2009, 05:51 PM
In jQuery, the element being acted upon is often passed to the function parameter of a jQuery function as this. It can be made into a jQuery element object by using $(this) in the function parameter, ex:


$('.menu a').each(function(){
$(this).animate({paddingLeft: $(this).width()}, 200);
});

My question is, since (I assume) at least the first instance of $(this) initializes the a element as a jQuery element object with all of the extended features of jQuery, does the second one do this all over again, or does it recognise that the element has already been initialized, and simply accesses it as the initialized jQuery object that it already is?

In other words, would it be any more efficient to do:


$('.menu a').each(function(){
var a = $(this);
a.animate({paddingLeft: a.width()}, 200);
});

?

Twey
02-20-2009, 07:06 PM
It is worth doing the latter, yes. The jQuery function, for which $ is an alias, actually calls a constructor which is prototyped with the appropriate properties, so the overhead isn't huge, but it is worthwhile to avoid doing it if possible.

Also, the character '$' is, according to ECMA-262, reserved for machine-generated identifiers. Using it as jQuery does is non-standard, and should be avoided. Luckily, jQuery, unlike Prototype, allows one to disable the $ alias entirely by running in 'compatibility mode', after which the function can be accessed as jQuery. I recommend doing this in every script, although unfortunately it does not fix such instances as appear in the jQuery source itself.

jscheuer1
02-20-2009, 07:26 PM
I came to the same conclusion after doing the following test:


$(function(){
var t = new Date().getTime();
for(var ar = [], i = 0; i < 10000; ++i)
ar.push($('#test'));
alert(ar.length + ' ' + (new Date().getTime() - t));
t = new Date().getTime();
var a = $('#test2');
for(var ar2 = [], i = 0; i < 10000; ++i)
ar2.push(a);
alert(ar2.length + ' ' + (new Date().getTime() - t));
}
);

But the reason I was wondering was is this more efficient:


function linkanimate(){
var a = $(this), w = a.width(); timer = (timer * multiplier + introspeed);
a.animate({marginLeft: '-=' + w}, 0).animate({marginLeft: '+=' + w}, timer)
.animate({marginLeft: '+=' + movement}, timer).animate({marginLeft: '-=' + movement}, timer, hoverapprove);
}

function hoverapprove(){
var a = $(this);
if(a.hasClass('selected') && extendselected == 'load') a.animate({paddingLeft: '+=' + movement}, timer);
else if(!a.hasClass('selected') || extendselected == 'hover'){
a.data('hoverinit', parseInt(a.css('paddingLeft')));
a.hover(hoverover, hoverout);
}
}

function hoverover(){
var a = $(this);
a.stop(true).animate({paddingLeft: a.data('hoverinit') + movement}, speed);
}

function hoverout(){
var a = $(this);
a.animate({paddingLeft: a.data('hoverinit')}, speed);
}

or this:


function linkanimate(){
var a = $(this), w = a.width(), pl; timer = (timer * multiplier + introspeed);
a.animate({marginLeft: '-=' + w}, 0).animate({marginLeft: '+=' + w}, timer)
.animate({marginLeft: '+=' + movement}, timer).animate({marginLeft: '-=' + movement}, timer, hoverapprove);
function hoverapprove(){
if(a.hasClass('selected') && extendselected == 'load') a.animate({paddingLeft: '+=' + movement}, timer);
else if(!a.hasClass('selected') || extendselected == 'hover'){pl = parseInt(a.css('paddingLeft')); a.hover(hoverover, hoverout);}
}
function hoverover(){a.stop(true).animate({paddingLeft: pl + movement}, speed);}
function hoverout(){a.animate({paddingLeft: pl}, speed);}
}

This also relates to our previous discussion about parsing the code in passes. In the second example, the hoverapprove, hoverover, and hoverout functions may have to be parsed with each a tag processed by linkanimate, whereas the first example parses them only once as the wrapper function (not shown) is run. But then each time they are used $(this) must be performed, whereas including them in linkanimate, they will be created knowing which $(this) they are running for. After laying all of this out, I think the second one would be more efficient because $() is a much longer function than those in this example. What do you think?

About the $ var, I like its economy, but usually wrap my code in something like
:


(function menuanimatewrapper($){
code goes here
})(jQuery);

Is that as good as your recommendation about $, at least as far as insulating its use from other code that may need it goes?

Twey
02-20-2009, 08:15 PM
As you say, it does help, but on the other hand you're still using the $ character; if your code is being pre-parsed by a machine there's no telling where it might add in bits, so using scope to hide it is an imperfect solution. If it's economy you want, try another of the free symbols, like _, or a single-letter identifier, like j or even J or S (even though it's not technically a constructor, it does immediately call one, so I guess we could make an exception).

The chained version is mostly preferable. I doubt that there would be a big difference in speed, but the chained version is much more readable (if you put the formatting back in — of course if you cram whole blocks onto one line just about anything will be hideous).


function linkanimate() {
var a = $(this),
w = a.width(),
pl;

timer = (timer * multiplier + introspeed);

a.animate({marginLeft: '-=' + w}, 0)
.animate({marginLeft: '+=' + w}, timer)
.animate({marginLeft: '+=' + movement}, timer)
.animate({marginLeft: '-=' + movement}, timer, hoverapprove);

function hoverapprove() {
if (a.hasClass('selected') && extendselected === 'load')
a.animate({paddingLeft: '+=' + movement}, timer);
else if (!a.hasClass('selected') || extendselected === 'hover') {
pl = parseInt(a.css('paddingLeft'));
a.hover(hoverover, hoverout);
}
}

function hoverover() {
a.stop(true).animate({paddingLeft: pl + movement}, speed);
}

function hoverout() {
a.animate({paddingLeft: pl}, speed);
}
}FWIW those strings suggest that there's some eval() going on behind the scenes there. It would be vastly preferable to provide the operator and argument separately; that would allow much greater flexibility, and if a string were passed it could be used as keys to look up equivalent functions. It's not ideal, but it works, and it's the best we've got in a language that doesn't allow treating operators as functions.

jscheuer1
02-21-2009, 12:12 AM
I must admit a little of that was over my head at the moment, but I will revisit your last post in this thread as I'm sure I can grasp it. I'm responding now, perhaps prematurely, because after I last posted here, called away by the exigencies of life, I realized that a synthesis of the two approaches I outlined in my last post might be best for this particular application.

My thoughts were that since hoverover and hoverout are used when the mouse ventures over and out of the initialized links, those functions would best appear as immediate children of the main wrapper. The $(this) then required in them would most likely refer to the element moused over or out (rather than any passed object or element), and would be used only in the case of such events. Though more intensive at that point, this use would be distributed in an acceptable manner.

The hoverapprove function should lie within the linkanimate function because, regardless of where it is, it must be called for every a tag being considered and its code is less intensive if it can use the linkanimate variables rather than establishing its own via the $(this) construct.

How does that sit with you?


function linkanimate(){
var a = $(this), w = a.width(), pl; timer = (timer * multiplier + introspeed);
a.animate({marginLeft: '-=' + w}, 0).animate({marginLeft: '+=' + w}, timer)
.animate({marginLeft: '+=' + movement}, timer).animate({marginLeft: '-=' + movement}, timer, hoverapprove);
function hoverapprove(){
if(a.hasClass('selected') && extendselected == 'load')
a.animate({paddingLeft: '+=' + movement}, timer);
else if(!a.hasClass('selected') || extendselected == 'hover'){
a.data('hoverinit', parseInt(a.css('paddingLeft')));
a.hover(hoverover, hoverout);}
}
}

function hoverover(){
var a = $(this);
a.stop(true).animate({paddingLeft: a.data('hoverinit') + movement}, speed);
}

function hoverout(){
var a = $(this);
a.animate({paddingLeft: a.data('hoverinit')}, speed);
}

To see it in action:

http://home.comcast.net/~jscheuer1/side/hover_h/

jscheuer1
02-22-2009, 03:03 AM
About the eval business, if I do the code like so:


(function menuanimatewrapper($){
var introspeed = 150, multiplier = .8, movement = 15, speed = 200,
menuclass = '.menucontainer', extendselected = 'load', // false, 'load', or 'hover'
timer;
$(menuinit);
function menuinit(){$(menuclass).each(menustyle);}
function menustyle(){
var c = $(this), cf = c.find('div:first'); timer = 0;
c.css({position: 'relative', height: cf.height(), width: cf.width(), overflow: 'visible'});
cf.css({position: 'absolute', top: 0, left: 0}).find('a').each(linkanimate);
}
function linkanimate(){
var a = $(this), w = a.width(); timer = (timer * multiplier + introspeed);
a.animate({marginLeft: ['-=', w].join('')}, 10).animate({marginLeft: ['+=', w].join('')}, timer)
.animate({marginLeft: ['+=', movement].join('')}, timer).animate({marginLeft: ['-=', movement].join('')}, timer,
function hoverapprove(){
if(a.hasClass('selected') && extendselected == 'load')
a.animate({paddingLeft: ['+=', movement].join('')}, timer);
else if(!a.hasClass('selected') || extendselected == 'hover')
a.data('hoverinit', parseInt(a.css('paddingLeft'))).hover(hoverover, hoverout);
}
);
}
function hoverover(){
var a = $(this); a.stop(true).animate({paddingLeft: a.data('hoverinit') + movement}, speed);
}
function hoverout(){
var a = $(this); a.animate({paddingLeft: a.data('hoverinit')}, speed);
}
})(jQuery);

Though perhaps less efficient considering that the concatenation now being done via the join() method is for only two elements of an array in each case, I think it demonstrates that no eval is required or used.

My point is that the '+=' and the '-=' are already required to be strings in jQuery when used in this fashion. For example, one may do:


$('#something').animate({height: '+=15'}, 200);

Translation - incrementally add a total of 15px to the existing height of the element over a period of 200 milliseconds using a 'swing' algorithm (I believe this is some sort of sine or cosine transition - the default for all jQuery animations). Whereas one cannot do:


$('#something').animate({height: +=15}, 200);

In other words, the string '+=' is only a token, not an operator. Don't feel bad, it took me a while to realize that.

Twey
02-22-2009, 11:56 AM
Though perhaps less efficient considering that the concatenation now being done via the join() method is for only two elements of an array in each case, I think it demonstrates that no eval is required or used.It does no such thing — at first glance it looks like the jQuery animate() function does something like this for each step:


window.curel = self;

for (var p in prop)
eval('curel.style.' + p + prop[p]);

As it turns out, I read through the jQuery animate() function and it seems in fact to use a regex to parse out the 'operator', which is presumably somewhat better than eval(), but I would still prefer a big operator lookup table, both for efficiency and flexibility.

Here's one:
var Operator = {
'+' : function(a, b) { return b === undefined ? +a : a + b; },
'-' : function(a, b) { return b === undefined ? -a : a - b; },
'/' : function(a, b) { return a / b; },
'*' : function(a, b) { return a * b; },
'%' : function(a, b) { return a % b; },
'>' : function(a, b) { return a > b; },
'<' : function(a, b) { return a < b; },
'>=' : function(a, b) { return a >= b; },
'<=' : function(a, b) { return a <= b; },
'==' : function(a, b) { return a == b; },
'!=' : function(a, b) { return a != b; },
'===' : function(a, b) { return a === b; },
'!==' : function(a, b) { return a !== b; },
'>>' : function(a, b) { return a >> b; },
'<<' : function(a, b) { return a << b; },
'>>>' : function(a, b) { return a >>> b; },
'&' : function(a, b) { return a & b; },
'|' : function(a, b) { return a | b; },
'^' : function(a, b) { return a ^ b; },
'&&' : function(a, b) { return a && b; },
'||' : function(a, b) { return a || b; },
'in' : function(a, b) { return a in b; },
'!' : function(a) { return !a; },
'~' : function(a) { return ~a; },
'void ' : function(a) { return void a; },
'typeof ' : function(a) { return typeof a; },
'instanceof' : function(a, b) { return a instanceof b; },
'.' : function(a, b) { return a[b]; },
'()' : function(f) { return f.call(this, Array.prototype.slice.call(arguments, 1)); }
};The user code would go something like:
foo.animate({'marginLeft' : ['+', 5]});and the jQuery code would look like:
jQuery.each(prop, function(name, val) {
var op = val[0],
args = val,
s = self.style;
args[0] = s[name];

if (typeof op !== 'function')
op = Operator[op];

s[name] = op.apply(self, args);
});That way we could use any simple operator in string form, for brevity, or, if we liked, create a more complex function to use to combine the values, and pass that instead. Also, there's no regex, so it's fast!

jscheuer1
02-22-2009, 01:51 PM
I see your point. However, in writing code using jQuery, I'm not worrying about how it works, other than how that impacts how I use it. That's the level at which I opened this thread. I believe though that the folks at jQuery are continually interested in any improvements, especially if they can be added without changing the syntax, but that minor changes are OK, they recently dropped support for the @attribute filter/selector in favor of just plain attribute. Perhaps you should contact them.

Twey
02-22-2009, 03:11 PM
Well, it can easily be added without breaking compatibility by having a non-array case:
if (!(val instanceof Array))
val = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/).slice(1);I'd rather implement and test it before submitting a patch (this is all simplified — the actual animate() function is quite large [too large, I'd say...]). Maybe sometime.