Trying to grasp let vs var a little better - javascript

Trying to grasp the differences between the two and I am experimenting with different examples to ensure I understand properly.
I have already looked at this question on Stack Overflow:
What's the difference between using "let" and "var" to declare a variable?
It seemed to make sense until I stumbled across this code segment:
http://jsbin.com/xepovisesi/edit?html,output
const buttons = document.getElementsByTagName("button");
for(var i = 0; i < buttons.length; i++) {
const button = buttons[i];
button.addEventListener("click", function() {
alert("Button " + i + " Pressed");
});
}
<button />
<button />
<button />
<button />
If you change the first for loop statement to a let, the functionality works as expected... It alerts which button was pressed. However var just alerts "Button 10 was pressed" every time. Why is it doing this?
Thanks,
James.

It's explained in the first paragraph of the accepted answer for the linked question:
The difference is scoping. var is scoped to the nearest function block
and let is scoped to the nearest enclosing block (both are global if
outside any block), which can be smaller than a function block.
var creates a variable in the scope outside for statement block. The equivalent code with let looks like this:
let i = 0;
for(i = 0; i < buttons.length; i++) {
const button = buttons[i];
button.addEventListener("click", function() {
alert("Button " + i + " Pressed");
});
}
Event listener for every button is bound to a single instance of i, which has value 10 after for loop ends, and you see that when you press each button.
This code
for(let i = 0; i < buttons.length; i++) {
const button = buttons[i];
button.addEventListener("click", function() {
alert("Button " + i + " Pressed");
});
}
has i in the scope of the of the for statement block. That means that for each iteration, new instance of i is created and bound in the event handler, so each handler shows different value for i.
Without let you can create a scope by introducing a function. The common way to write such code before let was
const buttons = document.getElementsByTagName("button");
for(var i = 0; i < buttons.length; i++) {
addButtonListener(i);
}
function addButtonListener(i) {
const button = buttons[i];
button.addEventListener("click", function() {
alert("Button " + i + " Pressed");
});
}

This is not indeed obvious and needs to be specified.
The reason is that let in a for(...) statement creates a new "binding" at each iteration. It's this way because the standard says so, but it could have been different without changing the meaning of let in other cases.
For example in Common Lisp it is unspecified (is implementation dependent) if a (dotimes ...) form will create a new binding at each iteration or not.
In other words Javascript code like:
for (let i=0; i<10; i++) {
...
}
is equivalent to
{
let __hidden__ = 0;
for (__hidden__ = 0; __hidden__ < 10; __hidden__++) {
let i = __hidden__;
...
}
}
and it's NOT the same as
{
let i = 0;
for (i = 0; i<10; i++) {
...
}
}
I personally think that this is indeed the most useful semantic (a new binding at each iteration) as reusing the same binding can be surprising in case of captures. It's also much easier to implement the other semantic when you need it (the second snipped is simpler and shorter than the first).
Reusing the same binding is instead for example what Python 3 does for variables in comprehensions:
[(lambda : i) for i in range(10)][3]() # ==> 9
Finally in my opinion the worst possible choice is leaving it implementation-dependent ... what Common Lisp surprisingly did...
(let ((L (list)))
(dotimes (i 10)
(push (lambda () i) L))
;; May be the following will print 10 times 10, or may
;; be the numbers from 9 to 0
(dolist (f L)
(print (funcall f))))

The difference is scope. In this example, you are creating a javascript closure (By having the event listener inside the for loop). Javascript does not write the value into the lexical scope of the function at the time the function is defined, rather, it looks up the value when the function is executed. When you use var, it will define the variable i inside the scope of whatever function it is inside of. In this case, the global scope. So each loop iteration refers to the same i variable, which gets incremented to 10 rather instantaneously, and the variable lives on past the for loop. So, when the event listener fires, it references the i variable on the global scope, whose value remains 10.
On the other hand, using let creates a LOCAL copy of i, which is scoped to its respective loop iteration. When the event listener is fired, it checks its own scope for the variable i which it obviously doesn't find, and then it moves outward, with the next step being the scope of the for loop to which it finds the local copy of i which retained its value for that specific iteration of the loop - hence you get the correct alert message for each button that is pressed.
Summary / TLDR;
var scopes variables to the function in which it is defined
let scopes variables to the block in which it is defined
Variables referenced in closures work their way outward through the scope chain when searching for a definition of a variable, stopping at the global scope.
Hope this helps!

Variable 'i' is being captured inside your button event handler.
1) If you don't have 'let' available, you can resolve this by scoping a new variable inside a IIFE like below:
const button = buttons[i];
(function() {
var j = i;
button.addEventListener("click", function() {
alert("Button " + j + " Pressed");
});
})()
2) Or, if you have 'let' available just do:
const button = buttons[i];
let j = i;
button.addEventListener("click", function() {
alert("Button " + j + " Pressed");
});

The reason for this is because the var keyword is hoisted to the top of scope, where as let is block scoped (closest curly brace). Let me explain by showing you how code is pseudo-interpreted by the JavaScript runtime.
Code using var...
function () {
for(var i = 0; i < buttons.length; i++) {
var button = buttons[i];
[...]
}
}
...gets converted to something like this by the JS runtime (due to hoisting):
function () {
var i, length, button;
for(i = 0; i < buttons.length; i++) {
button = buttons[i];
[...]
}
}
The thing to notice here is that the variables only exist once. These variables get updated on each iteration of the for loop. When the for loop is finished, all of the variables are holding the "last" value from the very last iteration.
Whereas the same code code using let...
function () {
for(let i = 0; i < buttons.length; i++) {
let button = buttons[i];
[...]
}
}
...gets treated differently. The variables are scoped to the curly braces around the for loop. Conceptually, you can think of it like a forEach statement:
function () {
buttons.forEach(function(item, i) {
var button = buttons[i]; // same as "item"
var length = buttons.length;
[...]
});
}
The thing to notice is that every iteration of the for loop gets its own variable i, length, and button - no sharing taking place. This is the distinction.

Related

Warning not to make function within a loop

I've written a code to create modal windows for div container. Once the button is clicked, I get the button's number and display a related modal window. Tested, works on all browsers.
myModalContent = new tingle.modal();
var myBtn = document.querySelectorAll("button.project__btn");
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].addEventListener("click", function () {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
});
}
A js validator gives one warning "Don't make functions within a loop."
Read some posts related to this topic, especially that the function must be created outside of the loop, I created a function:
function handler(modalDiv, trigBtn, index){
modalDiv.open();
if (trigBtn[index].hasAttribute("data-btn")) {
modalDiv.setContent(document.querySelector(".project" + trigBtn[index].getAttribute("data-btn") + "-modal").innerHTML);
} else {
modalDiv.setContent(document.querySelector(".project1-modal").innerHTML);
}
}
Then called it from within a loop:
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].onclick = handler(myModalContent, myBtn, i);
}
It doesn't seem to work properly, it displays a last modal window right after the web page loads. My understanding that the function must be connected with the click event listener, ie when a button is clicked, the modal window should pop up. Now, the modal window pops up without any click event. Could you give me an idea how to properly write a function? Or if I should just simply ignore this js validation warning or not.
Keep it simple! You do not have to change anything about your code but to move the function expression to a named function declaration outside of the loop body:
var myModalContent = new tingle.modal();
var myBtn = document.querySelectorAll("button.project__btn");
function myHandler() {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
}
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].addEventListener("click", myHandler);
}
The warning is trying to prevent a problem with "modified closures". If your function did anything with the variable i, then you'd find that the value of the variable i at the time when users click the button is always myBtn.length because that's the value it ends up with at the end of the loop.
This:
for (var i = 0; i < myBtn.length; i++) {
...
Is treated like this:
var i;
for (i = 0; i < myBtn.length; i++) {
...
Since you don't use i anywhere in your function, you're technically safe, but there's a possibility that other developers in the future could change the code and end up running into this problem.
In order to fix this code in the way it looks like you're trying to fix it, you'd need to have the handler function return a function itself.
myBtn[i].addEventListener("click", createHandler());
function createHandler() {
return function() {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
};
}
This has the same effect as your working code, but prevents someone from trying to use i inside of the closure. If someone needs i there, they can add it to the createHandler's argument list, where it's not reusing the same variable for each pass through the loop.
Alternatively, if you can use modern versions of javascript, you can use the let keyword instead of var.
This:
for (let i = 0; i < myBtn.length; i++) {
...
Is treated more like how this code would work in a language like C#:
for (var _ = 0; _ < myBtn.length; _++) {
var i = _;
...
In other words, the scope of the i variable is internal to the for loop, rather than global to the function you're in.

A deep understanding of javascript closures [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 9 years ago.
I'm trying to grasp this bit of code, I know it's a closure but I'm not getting the result I think I should get.
This code returns me an [object MouseEvent], I can't understand why?
I'm adding a function call (updateProduct) to an .addEventListener using this code, and it returns an [object MouseEvent]
function addEventListenerToMinPlus(){
var x, y
for(var i = 0; i < productItemAll.length; i++){
x = productItemAll[i].querySelector(".boxNumbers-min")
x.addEventListener("click", function(i){return function(i){updateProduct(i)}}(i))
console.log(x)
}
}
function updateProduct(jow){
alert(jow)
}
jsFiddle
The browser invokes the event handler with an event object as the first parameter. Your function is declared to take a single parameter ("i"), so when you display it, that's what it is.
I suspect that what you meant was for the "i" inside the event handler to refer to the "i" in the outer function (the loop index). That also won't work, because the various handlers the loop creates will all refer to the same shared variable "i". See this old SO question.
The line
x.addEventListener("click", function(i) { return function(i) { updateProduct(i); }(i) }
produces a closure of the inner function
function(i) { updateProduct(i); }
The outer i is in the scope of this inner function, but it is shadowed by its parameter. So, in effect, the inner i represents the first argument passed to the click handler (the MouseEvent). If you want it to retain the value of the index, you have to change its name. Something like this:
x.addEventListener("click",
function(i) { return function(e) { updateProduct(i); }(i)
}
Now, in the inner function, e is the MouseEvent, and i is the outer index. I have updated the JSFiddle: http://jsfiddle.net/Cdedm/2/. Clicking the minus alerts 0 for the first item and 1 for the second, as expected.
that is because you are sending the element as parameter.
You should try doing this:
function addEventListenerToMinPlus(){
var x, y
for(var i = 0; i < productItemAll.length; i++){
x = productItemAll[i].querySelector(".boxNumbers-min")
x.addEventListener("click", function(){return updateProduct(i)})
console.log(x)
}
}
Hope this works,
Regards,
Marcelo
I think you are trying to do something like this. Your i will change by the time the click happens so it needs to be set to another local variable, in this case through the parameter on the new function. The click event handler will pass the event object you are getting currently
function addEventListenerToMinPlus() {
var x, y;
for(var i = 0; i < productItemAll.length; i++) {
x = productItemAll[i].querySelector(".boxNumbers-min");
x.addEventListener("click", function(i){return function(){updateProduct(i)}}(i));
}
}
function updateProduct(jow) {
alert(jow);
}
Unless you really really really know what you're doing, you can play about with closures all day and still not get it right with this sort of thing.
A more understandable appraoch by far, with more readable code for most people, is to use jQuery's .data() method to associate data (i in this case) with the element in question so it can be read back when the click event fires, for example :
function addEventListenerToMinPlus() {
var x, y;
for(var i = 0; i < productItemAll.length; i++) {
x = productItemAll[i].querySelector(".boxNumbers-min");
x.data('myIndex', i);//associate `i` with the element
x.addEventListener("click", function(i) {
var i = $(this).data('myIndex');//read `i` back from the element
updateProduct(i);
});
console.log(x);
}
}
For the record, a working closure would be as follows :
function addEventListenerToMinPlus() {
var x;
for(var i = 0; i < productItemAll.length; i++) {
x = productItemAll[i].querySelector(".boxNumbers-min");
x.addEventListener("click", function(i) {//<<<< this is the i you want
return function() {//<<<< NOTE: no formal variable i here. Include one and you're stuffed!
updateProduct(i);
}
}(i));
console.log(x);
}
}

generating dynamic onclick events with javascript

I am dynamically generating a series of onclick events where an alert() is associated with loop number of the pretended content. My problem is that currently the alerts outputs the 'i' value of the last loop rather than the i'th loop associated with the pretended content. Any thoughts?
JavaScript:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
Many thanks.
Try concatenating it like you do before:
for (i = 1; i < 4; i++) {
prepend_content = 'foo';
$('#dynamic_div').prepend(prepend_content);
}
You might want to declare i and prepend_content (with var) in case you already haven't, to make sure they don't leak into the global scope.
At the same time, I wouldn't suggest using or adding HTML with inline event handlers. Try creating the element like this:
prepend_content = $("<a>").attr({
href: "#",
id: "img1_link_" + i
}).text("foo").on("click", (function (i) {
return function () {
alert(i);
};
})(i));
DEMO: http://jsfiddle.net/ujv4y/
The extra use of the immediately invoked function for the click handler is to make a closure that captures the value of i in the loop.
You can create a function using currying for the alert (for more complex stuff):
function(i) {
return function(){alert(i);}
}

Javascript multiple dynamic addEventListener created in for loop - passing parameters not working

I want to use event listeners to prevent event bubbling on a div inside a div with onclick functions. This works, passing parameters how I intended:
<div onclick="doMouseClick(0, 'Dog', 'Cat');" id="button_id_0"></div>
<div onclick="doMouseClick(1, 'Dog', 'Cat');" id="button_id_1"></div>
<div onclick="doMouseClick(2, 'Dog', 'Cat');" id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
</script>
However, I tried to create multiple event listeners in a loop with this:
<div id="button_id_0"></div>
<div id="button_id_1"></div>
<div id="button_id_2"></div>
<script>
function doMouseClick(peram1, peram2, peram3){
alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", function(){
doMouseClick(i, "Dog", "Cat");
},false);
}
</script>
It correctly assigns the click function to each div, but the first parameter for each, peram1, is 3. I was expecting 3 different event handlers all passing different values of i for peram1.
Why is this happening? Are the event handlers not all separate?
Problem is closures, since JS doesn't have block scope (only function scope) i is not what you think because the event function creates another scope so by the time you use i it's already the latest value from the for loop. You need to keep the value of i.
Using an IIFE:
for (var i=0; i<names.length; i++) {
(function(i) {
// use i here
}(i));
}
Using forEach:
names.forEach(function( v,i ) {
// i can be used anywhere in this scope
});
2022 edit
As someone is still reading and upvoting this answer 9 years later, here is the modern way of doing it:
for (const [i, name] of names.entries()) {
document.getElementById(name).addEventListener("click", () => doMouseClick(i, "Dog", "Cat"), false);
}
Using const or let to define the variables gives them block-level scope and the value of i passed to the handler function is different for each iteration of the loop, as intended.
The old ways will still work but are no longer needed.
2013 answer
As pointed out already the problem is to do with closures and variable scope. One way to make sure the right value gets passed is to write another function that returns the desired function, holding the variables within the right scope. jsfiddle
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function getClickFunction(a, b, c) {
return function () {
doMouseClick(a, b, c)
}
}
for (var i = 0; i < names.length; i++) {
document.getElementById(names[i]).addEventListener("click", getClickFunction(i, "Dog", "Cat"), false);
}
And to illustrate one way you could do this with an object instead:
var names = ['button_id_0', 'button_id_1', 'button_id_2'];
function Button(id, number) {
var self = this;
this.number = number;
this.element = document.getElementById(id);
this.click = function() {
alert('My number is ' + self.number);
}
this.element.addEventListener('click', this.click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
or slightly differently:
function Button(id, number) {
var element = document.getElementById(id);
function click() {
alert('My number is ' + number);
}
element.addEventListener('click', click, false);
}
for (var i = 0; i < names.length; i++) {
new Button(names[i], i);
}
It's because of closures.
Check this out: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake
The sample code and your code is essentially the same, it's a common mistake for those don't know "closure".
To put it simple, when your create a handler function, it does not just accesses the variable i from the outer environment, but it also "remembers" i.
So when the handler is called, it will use the i but the variable i is now, after the for-loop, 2.
I've been struggling with this problem myself for a few hours and now I've just now managed to solve it. Here's my solution, using the function constructor:
function doMouseClickConstructor(peram1, peram2, peram3){
return new Function('alert("doMouseClick() called AND peram1 = ' + peram1 + ' AND peram2 = ' + peram2 + ' AND peram3 = ' + peram3 + ');');
}
for (var i=0; i<names.length; i++){
document.getElementById(names[i]).addEventListener("click", doMouseClickConstructor(i,"dog","cat"));
};
Note: I havn't actually tested this code. I have however tested this codepen which does all the important stuff, so if the code above doesn't work I've probably just made some spelling error. The concept should still work.
Happy coding!
Everything is global in javascript. It is calling the variable i which is set to 3 after your loop...if you set i to 1000 after the loop, then you would see each method call produce 1000 for i.
If you want to maintain state, then you should use objects. Have the object have a callback method that you assign to the click method.
You mentioned doing this for event bubbling...for stopping event bublling, you really do not need that, as it is built into the language. If you do want to prevent event bubbling, then you should use the stopPropagation() method of the event object passed to the callback.
function doStuff(event) {
//Do things
//stop bubbling
event.stopPropagation();
}

Losing Scope of Array on Click Event Loop [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Javascript closure inside loops - simple practical example
I have an array of 4 objects (that.pairs), and each object has a .t property which is a jQuery object/element. I'm trying to set an event on each t being clicked.
The problem is that when one of the them gets clicked, it's always the last pair (index 3) that gets passed into my doToggle() function.
Why is this happening? How can I fix it?
for (var i = 0; i < that.pairs.length; i++) {
var p = that.pairs[i];
p.t.click(function() {
that.doToggle(p);
});
}
It's because the p variable is shared by your closures, there's just one p variable. By the time your handlers are called, p has changed.
You have to use a technique I call freezing your closures
for (var i = 0; i < that.pairs.length; i++) {
// The extra function call creates a separate closure for each
// iteration of the loop
(function(p){
p.t.click(function() {
that.doToggle(p);
});
})(that.pairs[i]); //passing the variable to freeze, creating a new closure
}
A easier to understand way to accomplish this is the following
function createHandler(that, p) {
return function() {
that.doToggle(p);
}
}
for (var i = 0; i < that.pairs.length; i++) {
var p = that.pairs[i];
// Because we're calling a function that returns the handler
// a new closure is created that keeps the current value of that and p
p.t.click(createHandler(that, p));
}
Closures Optimization
Since there was a lot of talk about what a closure is in the comments, I decided to put up these two screen shots that show that closures get optimized and only the required variables are enclosed
This example http://jsfiddle.net/TnGxJ/2/ shows how only a is enclosed
In this example http://jsfiddle.net/TnGxJ/1/, since there's an eval, all the variables are enclosed.
Use $.each instead of a for loop so that you get a new variable scope with each iteration.
$.each(that.pairs, function(i, p) {
p.t.click(function() {
that.doToggle(p);
});
});
This way each click handler closes over a unique variable scope instead of the shared outer variable scope.
for (var i = 0; i < that.pairs.length; i++) {
var p = that.pairs[i];
(function(p){
p.t.click(function() {
that.doToggle(p);
});
}(p));
}
This trick with IIFE would solve the closure "issue" you're experiencing now.
for (var i = 0; i < that.pairs.length; i++) {
(function(num){
var p = that.pairs[num];
p.t.click(function() {
that.doToggle(p);
});
})(i)
}
Classic closure issue
Enclose them in an anonymous function and assign the current iteration in context. That should solve the problem..

Categories

Resources