Why is this not an infinite loop? - javascript

I'm reading this this example and I'm stuck at understanding one line. I need to understand everything so I can't move on.
This function is supposed to hide all the elements inside an object. It's supposed to work. But to me, that for loop looks like an infinite one. Why is it not?
getChild: function (i) {
return this.children[i];
},
hide: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.hide();
}
this.element.hide(0);
},
From what I see, the function takes the first element of the object with getChild(0) and then calls hide again on that 0-dimension object. Then it resets the counter (i) and gets the first element of the 0-dimension object (which is the same 0-dim object) and calls the function again.
I know I'm mistaken but that's what I see. Please show me the light! Thanks

In a for loop like the one above, the first bit (var node, i = 0) is only executed once, at the beginning of the loop. The loop stops executing when the middle section (node = this.getChild(i);) returns false. getChild will return false when there isn't anything at index i. (Technically, it'll return undefined, but that equates to false in this instance).
Secondly, even though hide() is called in the for loop, i is not reset. Why? This recursive call creates a new instance of hide() separate from the original. All of the variables in this new hide() are separate from the original. (and so on, down the rabbit hole).
See http://www.tizag.com/javascriptT/javascriptfor.php for more information on for loops.

The variable i is not reset on each iteration. The only actions that are recurisvely executed are the boolean expression and i++. node.hide() is not the same as this.hide(). The latter is a different function being called. If it were the same function, then yes, there would be an infinite loop.
The "outer" hide function is being used to "hide" all the elements in this.getChild(i). node.hide() will call the hide() method on those elements so they are hidden. There is no infinite loop because node.hide(), although it has the same name as the function it's being used in, is not the same function.

The code
node.hide();
is still a member of the tree and still traversable. It is just hidden from being displayed.
The initialization part of the for loop
var node, i=0
is executed only once, before the looping begins.
The conditional
node = this.getChild(i)
evaluates to true (non-null) when there is a child node, and false (null) when it has run out of descendants, thereby breaking out of the loop.

If there is no child at i, getChild will return undefined and break out of the loop.
Consider the following text from the article:
Now create the GalleryImage class. Notice that it uses all of the exact same methods as the GalleryComposite. In other words, they implement the same interface, except that the image is a leaf so it doesn't actually do anything for the methods regarding children, as it cannot have any. Using the same interface is required for the composite to work because a composite element doesn't know whether it's adding another composite element or a leaf, so if it tries to call these methods on its children, it needs to work without any errors.
And consider the constructor for GalleryImage:
var GalleryImage = function (src, id) {
this.children = [];
this.element = $('<img />')
.attr('id', id)
.attr('src', src);
}
And how the images and composites are constructed:
var container = new GalleryComposite('', 'allgalleries');
var gallery1 = new GalleryComposite('Gallery 1', 'gallery1');
var gallery2 = new GalleryComposite('Gallery 2', 'gallery2');
var image1 = new GalleryImage('image1.jpg', 'img1');
var image2 = new GalleryImage('image2.jpg', 'img2');
var image3 = new GalleryImage('image3.jpg', 'img3');
var image4 = new GalleryImage('image4.jpg', 'img4');
gallery1.add(image1);
gallery1.add(image2);
gallery2.add(image3);
gallery2.add(image4);
container.add(gallery1);
container.add(gallery2);
Since an image cannot contain children, its this.children will remain an empty array. So, when the hide function finally gets called on an image (at one of the leaves of the composite tree), the loop will attempt to evaluate this.children[0] which will return undefined. This will cause the code node = this.getChild(i) to evaluate to a "false" value, and that particular for loop will terminate. Thus preventing an endless loop.

Related

Confusion around closure/scope

Today while working with some JS I had a bug in my code. I was able to resolve it but I don't really understand why the change I made works. All I can guess is that it comes down to either closure or variable scope.
I was trying to build up a nested hash of arrays like so:
var maxNumberOfPairs = 2;
var container = {};
var pairsHash = {};
$.each(["nurse", "doctor", "janitor", "chef", "surgeon"], function(index, role) {
for(var i = 0; i < maxNumberOfPairs; i++){
var pairIdSubString = "attribute_" + i + "_" + role;
pairsHash["attribute_" + i] = [pairIdSubString + "_night", pairIdSubString + "_day"];
}
container [role] = pairsHash;
});
If you run this you get a nice nested output inside container but when you look at each array in the hash you get a weird behaviour with the string produced.
Each one has the last role in each string like so:
"attribute_0_surgeon_night"
If you log out the variable pairIdSubString it correctly has the role in the string, but as soon as this is added to pairHash it just uses the last element in the $.each array.
I was able to fix it by moving pairsHash inside the $.each but outside the for loop.
Can anyone explain to my why the output was different after moving it inside the each?
Thanks
It actually has to do with reference vs value. When its outside the each you are operating on the same object over and over so every time you set it to the container you are just setting a reference to the same object that is constantly changing. So every reference in container after the loop is the last state of the pairsHash because they all point to the same object.
When you put the pairsHash in the each it is reinitialized every time so they all point to different memory addresses. Not the same one since a new one is created every loop.
To further clarify all objects are just references to a memory address In JavaScript so in order to get new one you need to initialize or to pass by value to a function clone it.

Iterating through a jQuery array

I'm trying to iterate through an array and assign values to an element as such:
<tool>hammer</tool>
var tools = ["screwdriver", "wrench", "saw"];
var i;
for (i=0; i < tools.length; ++i){
$("tool").delay(300).fadeOut().delay(100).html(tools[i]).fadeIn();
};
However this doesn't seem to work as only "saw" is assigned as the html value and keeps fading in and out.
What am I doing wrong here?
jQuery.delay() pauses between effects queued items, so your .html() is being set instantaneously. Hence, you only see saw.
A solution is to scrap the for loop and "loop" against the length of the array, setting the tool text to the next first item in the array (as you remove it). However, you need to do this inside the context of the queue, so you can use the .fadeOut() callback to do this.
Wrap all this in a function (here, I immediately invoke it but give it a label, a, so it can be referenced, it's not anonymous) and pass that to .fadeIn() at the end so it continues the loop until the array is empty.
var tools = ["screwdriver", "wrench", "saw"];
(function a(){
if (tools.length) {
$("tool").delay(300).fadeOut(0, function(){
$(this).html(tools.shift());
}).delay(100).fadeIn(a);
}
})();
http://jsfiddle.net/tc1q1vv7/1/

Adding a listener to an array of elements, need unique arguments for function on each item

I have a bit of HTML generated by PHP in the format of:
<div class=zoomButton>
<input type=hidden name=zoomURL value=*different per instance*>
</div>
I am trying to attach a listener (imageZoom(event, url)) to each of the class "zoomButton" elements, but call it with different arguments for each instance.
i.e.
var zoomButtonArray = document.getElementsByClassName('zoomButton');
for (i=0; i<zoomButtonArray.length; i++)
{
var zoomURL = zoomButtonArray[i].children[0].value;
zoomButtonArray[i].addEventListener("mousedown", function(){imageZoom(event,zoomURL);});
}
however it seems that zoomURL is always the value of the very last element. How can I change my code/approach so that the argument passed to the listener is the correct one, and not the last one in the "zoomButtonArray" array?
Thanks
You need to wrap the event listener in a closure:
function makeEventListenerForZoomURL(zoomURL) {
return function(event) {
imageZoom(event, zoomURL);
}
}
var zoomButtonArray = document.getElementsByClassName('zoomButton');
for (i=0; i<zoomButtonArray.length; i++)
{
zoomButtonArray[i].addEventListener(
"mousedown",
makeEventListenerForZoomURL(zoomButtonArray[i].children[0].value)
);
}
This can also be simplified using the ECMAScript5 forEach:
var zoomButtonArray = document.getElementsByClassName('zoomButton');
zoomButtonArray = Array.prototype.slice.call(zoomButtonArray, 0);
zoomButtonArray.forEach(function(node) {
node.addEventListener("mousedown", function(event) {
imageZoom(event node.children[0].value);
});
});
The reason is that each time the for loop executes a new function is created, this new scope references the variable i but i changes each time the loop iterates. So by the time the event listener runs it looks at the value of i only to find that it is the last value when the for loop ended. By using a closure described above the scope created is unique to each iteration of the loop so that when the event listener finally executes the value of the wrapped variable (zoomURL or node in the examples above) will not have changed.
Here is a good article explaining closures in for loops: http://trephine.org/t/index.php?title=JavaScript_loop_closures
I think you are missing quotes around attributes. I just added quotes and the tested at jsFiddle (Fiddle link in comments) and it's working see to console in developer tool. it is iterating through each element as desired. Console screen shot

JavaScript returning var in for loop

I am trying to understand code which implements canvas/context objects. This code returns an object if the sprite of that object is encountered on the canvas at a specified set of coordinates provided by a mouse button down event (as far as I can tell).
Does the following code create an array of objects?
var selObj = getObjectByPixel(mx,my);
and
function getObjectByPixel(x,y) {
gctx.clearRect(0,0,MaxX,MaxY);
//alert(levelData.world['ExtraBlockTNT_1'].name);
for (var objname in levelData.world) {
var obj = levelData.world[objname];
var sprd = spriteData[obj.definition];
if(!sprd) continue;
var tr = transform(obj.x, obj.y, sprd.data.width, sprd.data.height);
gctx.save();
gctx.translate(tr.x,tr.y);
gctx.rotate(obj.angle);
gctx.fillRect(-tr.w/2, -tr.h/2, tr.w, tr.h);
gctx.restore();
//console.info(x,y);
var imageData = gctx.getImageData(x, y, 1, 1);
if (imageData.data[3] > 0) {
return obj;
}
}
return null;
}
It would seem to me that the first object in the loop will return if pixel data is encountered. If that is the case, does the loop end (which is what I assume will happen) or does it keep returning objects and store them in selObj
I'm quite confused by this code but the app runs without error so I must not be fully understanding it.
Thanks.
It does not return an array. It returns an object, see: return obj;. You can only return from a function once.
p.s. if the author of this code was to return an array he would have probably called it: getObjectsByPixel (note the s).
return always ends the execution and returns to the stack at the point the function was entered.
So that means it is only returning a single object. In order to return an array, the function would have to first create the array, and then return it after the loop has finished.
I finally worked out the dynamic of the block. The loop does only return a single obj (which is what I knew anyway). The logic is, for every object sprite on the canvas, an invisible filled rectangle is created in a overlayed canvas until the mouse click coordinates are within the bounds of one of the rectangles. Then the object that that rectangle was generated from is returned.

JavaScript function offsetLeft - slow to return value (mainly IE9)

I've had a hard time debugging a news ticker - which I wrote from scratch using JavaScript.
It works fine on most browsers apart from IE9 (and some mobile browsers - Opera Mobile) where it is moving very slowly.
Using Developer Tools > Profiler enabled me to find the root cause of the problem.
It's a call to offsetLeft to determine whether to rotate the ticker i.e. 1st element becomes the last element.
function NeedsRotating() {
var ul = GetList();
if (!ul) {
return false;
}
var li = GetListItem(ul, 1);
if (!li) {
return false;
}
if (li.offsetLeft > ul.offsetLeft) {
return false;
}
return true;
}
function MoveLeft(px) {
var ul = GetList();
if (!ul) {
return false;
}
var li = GetListItem(ul, 0);
if (!li) {
return false;
}
var m = li.style.marginLeft;
var n = 0;
if (m.length != 0) {
n = parseInt(m);
}
n -= px;
li.style.marginLeft = n + "px";
li.style.zoom = "1";
return true;
}
It seems to be taking over 300ms to return the value, whereas the ticker is suppose to be moving left 1 pixel every 10ms.
Is there a known fix for this?
Thanks
DOM operations
I agree with #samccone that if GetList() and GetListItem() are performing DOM operations each time, you should try to save references to the elements retrieved by those calls as much as possible and reduce the DOM operations.
then I can just manipulate that variable and hopefully it won't go out of sync with the "real" value by calling offsetLeft.
You'll just be storing a reference to the DOM element in a variable. Since it's a reference, it is the real value. It is the same exact object. E.g.:
var li = ul.getElementsByTagName( "li" )[ index ];
That stores a reference to the DOM object. You can read offsetLeft from that object anytime, without performing another DOM operation (like getElementsByTagName) to retrieve the object.
On the other hand, the following would just store the value and would not stay in sync:
var offsetLeft = ul.getElementsByTagName( "li" )[ index ].offsetLeft;
offsetLeft
If offsetLeft really is a bottleneck, is it possible you could rework this to just read it a lot less? In this case, each time you rotate out the first item could you read offsetLeft once for the new first item, then just decrement that value in each call to MoveLeft() until it reaches 0 (or whatever)? E.g.
function MoveLeft( px ) {
current_offset -= px;
If you want to get even more aggressive about avoiding offsetLeft, maybe you could do something where you read the width of each list item once, and the offsetLeft of the first item once, then just use those values to determine when to rotate, without ever calling offsetLeft again.
Global Variables
I think I get it... so elms["foo"] would have to be a global variable?
I think really I just need to use global variables instead of calling offsetLeft every 10 ms.
You don't need to use global variables, and in fact you should avoid it -- it's bad design. There are at least a couple of good approaches you could take without using global variables:
You can wrap your whole program in a closure:
( function () {
var elements = {};
function NeedsRotating() {
...
}
function example() {
// The follow var declaration will block access
// to the outer `elements`
var elements;
}
// Rest of your code here
} )();
There elements is scoped to the anonymous function that contains it. It's not a global variable and won't be visible outside the anonymous function. It will be visible to any code, including functions (such as NeedsRotating() in this case), within the anonymous function, as long as you don't declare a variable of the same name in your inner functions.
You can encapsulate everything in an object:
( function () {
var ticker = {};
ticker.elements = {};
// Assign a method to a property of `ticker`
ticker.NeedsRotating = function () {
// All methods called on `ticker` can access its
// props (e.g. `elements`) via `this`
var ul = this.elements.list;
var li = this.elements.list_item;
// Example of calling another method on `ticker`
this.do_something();
} ;
// Rest of your code here
// Something like this maybe
ticker.start();
} )();
Here I've wrapped everything in an anonymous function again so that even ticker is not a global variable.
Response to Comments
First of all, regarding setTimeout, you're better off doing this:
t = setTimeout( TickerLoop, i );
rather than:
t = setTimeout("TickerLoop();", i);
In JS, functions are first-class objects, so you can pass the actual function object as an argument to setTimeout, instead of passing a string, which is like using eval.
You may want to consider setInterval instead of setTimeout.
Because surely any code executed in setTimeout would be out of scope of the closure?
That's actually not the case. The closure is formed when the function is defined. So calling the function via setTimeout does not interfere with the function's access to the closed variables. Here is a simple demo snippet:
( function () {
var offset = 100;
var whatever = function () {
console.log( offset );
};
setTimeout( whatever, 10 );
} )();
setTimeout will, however, interfere with the binding of this in your methods, which will be an issue if you encapsulate everything in an object. The following will not work:
( function () {
var ticker = {};
ticker.offset = 100;
ticker.whatever = function () {
console.log( this.offset );
};
setTimeout( ticker.whatever, 10 );
} )();
Inside ticker.whatever, this would not refer to ticker. However, here you can use an anonymous function to form a closure to solve the problem:
setTimeout( function () { ticker.whatever(); }, 10 );
Should I store it in a class variable i.e. var ticker.SecondLiOffsetLeft = GetListItem(ul, 1).offsetLeft then I would only have to call offsetLeft again when I rotate the list.
I think that's the best alternative to a global variable?
The key things are:
Access offsetLeft once each time you rotate the list.
If you store the list items in a variable, you can access their offsetLeft property without having to repeatedly perform DOM operations like getElementsByTagName() to get the list objects.
The variable in #2 can either be an object property, if you wrap everything up in an object, or just a variable that is accessible to your functions via their closure scope. I'd probably wrap this up in an object.
I updated the "DOM operations" section to clarify that if you store the reference to the DOM object, it will be the exact same object. You don't want to store offsetLeft directly, as that would just be storing the value and it wouldn't stay in sync.
However you decide to store them (e.g. object property or variable), you should probably retrieve all of the li objects once and store them in an array-like structure. E.g.
this.li = ul.getElementsByTagName( "li" );
Each time you rotate, indicate the current item somehow, e.g.:
this.current_item = ###;
// or
this.li.current = this.li[ ### ];
// Then
this.li[ this.current_item ].offsetLeft
// or
this.li.current.offsetLeft
Or if you want you could store the li objects in an array and do this for each rotation:
this.li.push( this.li.shift() );
// then
this.li[0].offsetLeft
if you dont cache your selectors in var li = GetListItem(ul, 1);
then performance will suffer greatly.. and that is what you are seeing because you are firing up a new selector every 10ms
you should cache the selector in a hash like
elms["foo"] = elms["foo"] || selectElm(foo);
elms["foo"].actionHere(...)
your code is slow because reading offsetLeft will force the browser to do a reflow. the reflow is the part that is slowing you down. the browser is typically smart enough to queue changes to reduce the number of reflows. however, given that you want the most up to date value when access offsetLeft, you're forcing the browser to flush that queue and do a reflow in order to calculate the correct value for you.
without knowing all the details of what you're trying to do, it's hard to know what to recommend to improve performance. http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/ explains this problem in more detail and offers some advice about minimizing reflows.

Categories