Javascript, lexical scopes and what your momma thought you about variables

time to read 3 min | 586 words

Let us assume that we have this amazing javascript function:

function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		if( nums[i] % 2 == 0)
		{
			alertLink.onclick = function() { alert('EVEN: '+ nums[i]); };
		}
		else
		{
			alertLink.onclick = function() { alert('ODD: ' + nums[i]); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Can you guess what it would generate? Quite a few undefines alerts, as a matter of fact. Why is that? Because the anonymous function is a closure, which capture not the value of i, but the i variable itself.

This means when we click on a link that this method has generated, we use the last known value of i. Since we have exited the loop, i is actually 8.

Now, in C# we have the same problem, and we can solve it by introducing a temporary variable in the loop, so we change the code to look like this:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		var tmpNum = nums[i];
		
		if( nums[i] % 2 == 0)
		{
			alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); };
		}
		else
		{
			alertLink.onclick = function() { alert('ODD: ' + tmpNum ); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Try to run it, and you'll get an.. interesting phenomenon. All the links will show tmpNum as 7. Again, we captured the variable itself, not its value. And in JS, it looks like you are getting the same variable in the loop, not a new one (this is absolutely the wrong way to describe it, but it is a good lie), like you would in C#.

What is even more interesting is that you would get the exact same result here:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
		
		if( nums[i] % 2 == 0)
		{
			var tmpNum = nums[i];
			alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); };
		}
		else
		{
			var tmpNum = nums[i];
			alertLink.onclick = function() { alert('ODD: ' + tmpNum ); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

Here we have two different lexical scopes, with respectively different variables. Looks like it should work. But the lexical scope of JS is the function, not the nearest set of curly. Both tmpNum refer to the same variable, and as such, are keeping the last value in it.

If the lexical scope is a function, we need to use a function then. Here is a version that works:

	
function test()
{
	var nums = [1,2,3,4,5,6,7];
	for(var i = 0; i<nums.length; i++)
	{
		var alertLink = document.createElement("A");
		alertLink.href = "#";
		alertLink.innerHTML = nums[i];
		
	
		if( nums[i] % 2 == 0)
		{
			var act = function(tmpEVEN)
			{
				alertLink.onclick = function() { alert('EVEN: '+tmpEVEN); };
			};
			act(nums[i]);
		}
		else
		{
			var tmpODD = nums[i];
			alertLink.onclick = function() { alert('ODD: ' + tmpODD); };
		}
		
		document.firstChild.appendChild(alertLink);
		document.firstChild.appendChild(document.createElement("BR"));
		
	}
}

And that is it for today's JS lesson.