Using the assignment operator instead of the equality operator

The previous version of the Find Position script didn't work quite correctly, since it often ignored the last step in position calculations: the one from the <body> to the <html> element. Part of the reason was that its code was too complicated.

The problems with this script used to generate a lot of comments. Eight months ago I changed the script somewhat, and comments dropped off to zero, meaning I'd done it right. Meanwhile I've taken another look at it and changed it a bit more.

In any case, the changed script now uses a new approach (that is, it was new to me eight months ago). It now uses the assignment operator = instead of the equality operator == that you'd expect:

while (obj = obj.offsetParent)

I always planned to write a blog entry about this approach, because I feel this little trick should become general knowledge. So here it is (eight months too late, but anyway):

Assignment versus equality

A mistake novel JavaScripters frequently make is confusing the assignment and equality operators = and ==. If you want to compare two values, you should use a double equal sign since that's how JavaScript works. If you accidentally use a single equal sign, you don't compare values but assign a value to a variable.

For example:

if (x == 3) {
	doSomething();
}

This is correct: you compare the value of x to 3. If x in fact has the value 3, the doSomething() function is executed.

This is wrong:

if (x = 3) {
	doSomething();
}

Now you do not compare x and 3, but instead you assign the value 3 to x. The doSomething() function is always executed, regardless of the original value of x. This can be quite confusing to novel scripters who think they've done it right.

Now why is doSomething() executed? That happens because the assignment operator = also returns a value: the value you just set the variable to. In our example, the x = 3 statement does two things:

  1. It assigns the value 3 to x.
  2. It then returns the same value 3 to whichever JavaScript construct asked for it. In this case, it's returned to the if () statement.

The if () statement expects to receive a boolean value true or false. If it receives true the statement doSomething() is executed. If it receives false the statement is not executed.

However, now if () receives the value 3, which is a number and not a boolean. That's perfectly OK to JavaScript. If any operator or statement receives a value of the wrong type, JavaScript silently converts the value to the correct type. Chapter 5 of the book treats these situations in detail.

The number 3 is converted to the boolean true and doSomething() is executed. In fact, almost every number is converted to true, and therefore doSomething() will be executed no matter which value you assign to x.

The only exceptions are 0 and the special value NaN (Not a Number). These two are converted to false.

Usually you don't want all this complicated stuff to happen. Usually you just want to compare two values and take action based on whether they're equal or not. That's why you generally use the equality operator == in such cases.

However, there are a few cases where deliberately substituting the assignment operator = is a good idea.

Using assignment instead of equality

Let's look at the relevant portion of the Find Position script:

do {
	curleft += obj.offsetLeft;
	curtop += obj.offsetTop;
} while (obj = obj.offsetParent);

This script takes an object, calculates its offset relative to its offsetParent, and then moves to this offsetParent to do the same. It continues doing this until there's no more offsetParent to be found. When it's done we've found the actual position of the initial object relative to the uppermost container in our page (usually the <html> element).

In order to do all this, we have to check continuously if the current object has an offsetParent. If there is, we have to repeat all actions; if there isn't, the function is ready.

Why does it work?

The while (obj = obj.offsetParent) line takes care of this. Since it uses the assignment operator =, obj receives a new value: it now points to the offsetParent of the previous object so that we can easily repeat the calculations in the body of the do {} statement.

As in the x = 3 example above, this new value of obj is also sent on to the while () statement. That statement now receives an object, but needs a boolean. That's no problem: any object is converted to boolean true.

Once that value has been received, the while () statement orders the do{} to run once more and add the offset of the new object. That's exactly what we want.

But what about the last step? Eventually obj is going to point to the <html> element, which is the topmost element in the document hierarchy and does not have an offsetParent.

That's no problem, either. If we reach the point where obj doesn't have an offsetParent any more, the statement obj = obj.offsetParent returns the value undefined, since all object properties that are not defined have this value.

So obj now becomes undefined. More to the point, this value is also returned to the while () statement, and it is converted to boolean false. (After all, undefined is a fancy way of saying "It's not there", and such a statement should obviously convert to false.)

Therefore, as soon as the function is unable to find an offsetParent of the current element, its calculations stop. That, too, is exactly what we want.

Advantages

I find this way of working quite elegant. We have to get the new offsetParent and we also have to check if there is a new offsetParent at all. The while (obj = obj.offsetParent) statement handles both jobs at once.

Of course we could do the same with the equality operator ==. For instance:

do {
	curleft += obj.offsetLeft;
	curtop += obj.offsetTop;
	obj = obj.offsetParent;
} while (obj.nodeName != 'HTML');

However, there are three problems with this code:

  1. It needs an extra line of code.
  2. You assume that the <html> element is the first one that doesn't have an offsetParent. That assumption is dangerous because some older browsers (notably IE5) consider the <body> element the topmost one.
  3. This function doesn't make the very last check. As soon as obj points to the <html> element it stops, and doesn't add the offsets of the <html> element itself. Usually this element doesn't have any offsets, but as CSS wizardry progresses it might acquire them.

The last problem could be solved:

do {
	curleft += obj.offsetLeft;
	curtop += obj.offsetTop;
	obj = obj.offsetParent;
} while (obj.nodeName != 'HTML');
curleft += obj.offsetLeft;
curtop += obj.offsetTop;

This is ugly coding, though. We need two more extra lines to make the effect work. Besides, the second problem remains unsolved.

All in all I vastly prefer the elegant obj = obj.offsetParent solution.

A similar case

Finally I'd like to point out a similar case. Suppose we want to go through all <div>s in the document. Traditionally we do something like this:

var x = document.getElementsByTagName('div');
for (var i=0;i<x.length;i++) {
	doSomething(x[i]);
}

Now this works fine; no problem. However, we could also do the following:

var x = document.getElementsByTagName('div');
for (var i=0,div;div=x[i];i++) {
	doSomething(div);
}

The div=x[i] behaves exactly like the obj = obj.offsetParent. It assigns a new value to div, except when there's no more value to be found (i.e. we've gone through all <div>s in the document). At that point the assignment operator = returns undefined, which is converted to false and stops the for () loop.

This way of working is supposed to have an advantage: the browser doesn't have to calculate x.length every time the loop restarts. Although I doubt whether the removal of this calculation saves a lot of processing time, it's nonetheless something to keep in mind.

All in all, you can use the assignment operator = for more than just assigning new values to variables. Try it in your scripts. If you do it right you've solved several problems at once.

This is the blog of Peter-Paul Koch, web developer, consultant, and trainer. You can also follow him on Twitter or Mastodon.
Atom RSS

If you like this blog, why not donate a little bit of money to help me pay my bills?

Categories:

Comments

Comments are closed.

1 Posted by Miquel Fire on 14 January 2008 | Permalink

This applies to a lot of other languages as well. I use this in PHP quite a bit.

2 Posted by Stewart Pratt on 14 January 2008 | Permalink

The method you describe is neat, but it has one significant flaw in that because it's also a very common typo, when reading the code it's not always clear whether the author intended it or not.

The following loop uses the same number of lines, is no less efficient, and is *much* more obvious in terms of the author's intent.

for (obj = foo; obj != null; obj = obj.offsetParent)
{
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
}

The 'obj = foo' can be omitted if 'obj' has already been assigned.

With the last example you give, ('div = x[i]') it works fine in this particular case, but will not work for arrays which have null entries since it will exit as soon as it hits a null. Writing loops in several different ways depending on the content of the arrays is, again, quite confusing when you come to read someone's code and work out what they were trying to do.

3 Posted by quentin on 14 January 2008 | Permalink

This practice is usually discouraged. The big problem is readability.
While working in team, other developpers may be confused by the "=" sign, and may even think it is a mistake and replace it with "=="...

In Java, the compiler gives you a warning for that ! (in java, this only work with boolean as integer are never converted into boolean...)

Anyway in my sense, using the implicit conversion from integer to boolean should be avoided because it lacks clarity.

Personnaly i'd prefer something like :
while ((obj = obj.offsetParent) != null)


4 Posted by Pete B on 14 January 2008 | Permalink

glad you posted this.

I looked at your find position script before and thought the assignment/conditional looked odd

5 Posted by Mark Wubben on 14 January 2008 | Permalink

Your example:

do {
// do stuff
} while (obj = obj.offsetParent);

is equivalent to:

do {
// do stuff
obj = obj.offsetParent;
} while (obj);

So there's no need to check for obj.nodeName != 'HTML'.

Without using the assignment trick, this would be the best solution:

while(obj) {
// do stuff
obj = obj.offsetParent;
}

I agree with Stewart that the assignment trick will cause confusion if used like this.

6 Posted by Robbert Broersma on 14 January 2008 | Permalink

Quentin:
do { } while (!!(element = element.offsetParent))

7 Posted by Peter Siewert on 14 January 2008 | Permalink

I never thought to use the assignment opperator in the for() loop like that.

Generally for my versions of the findPosition functions, I just use recursion and sidestep the whole loop question:

function findLeft(obj) {
  if( !obj ) return 0;
  return obj.offsetLeft + findLeft( obj.offsetParent );
}
function findTop(obj) {
  if( !obj ) return 0;
  return obj.offsetTop + findTop( obj.offestParent );
}

It might have a slightly higher performance hit, but you cant really beat a 2 line function.

8 Posted by Lon on 15 January 2008 | Permalink

Hi Peter Paul,

your example using getElementsByTagName is dangerous.

It returns a "live NodeList". As a consequence if your 'doSomething' removes the node you will skip the next one due to the way you traverse the list.

Either traverse backwards (which is much cheaper as well) or do it different (conditional increment of counter for instance), but don't do it the way you are proposing.

9 Posted by Andreas Kalsch on 15 January 2008 | Permalink

This point isn't new. I think it's not confusing if you KNOW what the engine is doing (set the var value to a non boolean and then implicitly casting to boolean). So every good developer knows the difference between =, == (and ===).

But ...
"do {
// do stuff
} while (obj = obj.offsetParent);"
... sometimes throws errors if the value is undefined. So an own line with a check often is better.

10 Posted by Christopher Boomer on 15 January 2008 | Permalink

This technique is one I have always strategically avoided to avoid confusing myself.

However, @Lon: "traverse backwards (which is much cheaper as well)"

Could you elaborate on that?

11 Posted by Lon on 15 January 2008 | Permalink

@Christopher

Walking backwards through a live NodeList is cheaper because you don't have to access the length property, but just compare with 0.

Accessing the length property is very expensive in this case because the browser needs to re-compile the complete list (you never know if some node has appeared or disappeared) to calculate the length.

In general you should avoid using getElementsByTagName as much as you can. It's expensive and why would you want to find all elements with a certain nodeName? CSS can do that for you much faster and simpler.

12 Posted by Frederico Caldeira Knabben on 15 January 2008 | Permalink

Best practices:

- Surround the equality with parenthesis.

- Add a comment explicitly indicating your intentions.

E.g.:

while ((obj = obj.offsetParent)) // Only one "="

13 Posted by Sander Aarts on 15 January 2008 | Permalink

@Lon:
Of course CSS is faster, but can I use CSS selectors to select nodes _in JavaScript_ (without being translated to getElementsByTagName first)? Would hope so.

14 Posted by Lon on 15 January 2008 | Permalink

@Sander

My point is: CSS is faster. Why do you want to do this in JS? I can think of no valid reason that cannot be done better and faster using CSS and different JS.

Give me an example and I'll rewrite it so as not to use getElementsByTagName but CSS instead.

15 Posted by Sander Aarts on 16 January 2008 | Permalink

@Lon
I'm not sure we're talking about the same thing.

What if I want to attach an event handler (say 'onclick') to all links in a menu (#menu)?
Of course I'm curious about your speed-of-light script ;-) but especially how you're going to use CSS for that.

16 Posted by Lon on 16 January 2008 | Permalink

@Sander: easy: attach the event handler to the menu itself (you only need to find one element and only need to attach one handler). Give all menu items some common className and there you are.

No need for getElementsByTagName.

17 Posted by Sander Aarts on 17 January 2008 | Permalink

@Lon
Thanks, but I fail to see where CSS is involved in this.

I was wondering: is it always faster to attach one handler to a parentNode and check for the node each time the event is triggered (with some W3C/IE incompatibilies) than to initially attach a handler to each specific node and not having to do the check later on, regardless of the number of nodes?

18 Posted by Lon on 17 January 2008 | Permalink

@Sander, becoming a bit off-topic:

I didn't say all solutions involve CSS, in this case you don't need CSS.

I don't know whether it's faster to attach at node or aggregate level, but it hardly matters when the user physically moves the mouse to click somewhere. The amount of time involved in the user causing the event itself is so much more than traversing a few parentNodes looking for some className...

Benefits are: no tedious administration, no detaching, no updating when new HTML is inserted, more extensible, easier, cleaner, whatever... Just better in my view.

I almost always attach only on body-level.

So once more then: why would you ever need to use getElementsByTagName? You don't if you think about what you want to do a bit more.

19 Posted by Harmen Janssen on 17 January 2008 | Permalink

Thanks for the interesting article.
I've used the "div=x[i]" method for a while (ha!) now in my loops and it strikes me as quite elegant, just as your findPosition script.

As for Lon's comments: you bring up very interesting points. Something to keep in mind :)

20 Posted by Arianne on 19 January 2008 | Permalink

The article explains a lot of why using only one '=' would give the correct result. But I feel that wouldn't it be more logical if it is written this way instead :

while( obj != null )
{
leftCoor += obj.offsetLeft ;
topCoor += obj.offsetTop ;
obj = obj.offsetParent ;
}

21 Posted by Mitch 74 on 21 January 2008 | Permalink

About the last example: the advantage of the method is hardly exclusive to this system. Another solution is to use:

if(i=array.length-1;i>=0;i--){...}

has the exact same effect, the length is read only once, and on top of that comparison with zero being equivalent to boolean testing, you have the same speed gain, without the inconvenient of stopping on empty array elements.

22 Posted by Lloyd on 22 January 2008 | Permalink

while, for and recursion have been mentioned and although I am a fan of recursion, in the case of the javascript engine, I find that something like "var i=length; while(--i) ... " to be ``fairly'' elegant and quite fast in comparison to ``for'' construct and recusion (in javascript)
Just my 2 centimos

23 Posted by jaco on 25 January 2008 | Permalink

I've been using this for if statements as well for quite some time, example:
if (element = document.getElementById('my-id')) element.className = 'my-class-name';

To distinguish between assignment and equality I leave out the spaces surrounding the '==':
if (otherElement==document.getElementById('my-id')) otherElement.className = 'my-class-name';

24 Posted by Ryan Cannon on 31 January 2008 | Permalink

I think your second example is a pretty bad habit to get into.

for (var i=0,div;div=x[i];i++) { doSomething(div) }

works fine when x is a nodeList. However, substitute an Array of values:

x = [1, 7, 0, 4, 2];
x = ["Yes", "No", "", "Maybe"];

and things break. A lot of things evaluate to false, and that bug seems like it be a tough one to track down. That said, you can do it even quicker:

for (var i = -1, div; div = x[++i];) doSomething(div) }

While still retaining the value of i and order of processing.

25 Posted by Joe on 6 February 2008 | Permalink

Also possible for arrays:

while(e = queue.shift()) {

}