How can a temporary variable be "saved" inside an event handler

Hello, quick question, consider the following code:
<h3>Click these buttons (Created with 'let'):</h3>
<div id="let-container"></div>
<h3>Click these buttons (Created with 'let'):</h3>
<div id="let-container"></div>
const container = document.getElementById('let-container');

for (let i = 1; i <= 3; i++) {

const btn = document.createElement('button');
btn.innerText = `Button ${i}`;
btn.addEventListener('click', () => {
console.log(`You clicked the button that was created when 'i' was: ${i}`);
});

container.appendChild(btn);
}
const container = document.getElementById('let-container');

for (let i = 1; i <= 3; i++) {

const btn = document.createElement('button');
btn.innerText = `Button ${i}`;
btn.addEventListener('click', () => {
console.log(`You clicked the button that was created when 'i' was: ${i}`);
});

container.appendChild(btn);
}
My question is, when we add the event listener for each instance of the button, even though i is normally a temporary variable, it's value gets stored and can be referenced when we actually click on the button, can someone explain why pls.
11 Replies
Jochem
Jochem•6d ago
the handler is a closure. It inherits the scope of the parent at the moment of creation
Faker
FakerOP•6d ago
yeah, just have a read on closures, basically what happen when we use let is we have a separate i variable for each iteration? When i is being referenced into each button instance, the event handler still "remembers" the current value of i at that point even though the loop is terminated?
Jochem
Jochem•6d ago
let (or const) defines the variable in block scope, which gets copied along to the closure. If you'd use var it gets hoisted to the top of the file and declared in global scope, which doesn't get copied.
Faker
FakerOP•6d ago
yeahh make sense just tested out both with let and var, thanks it's clearer now that's a powerful way of making things remember particular states, no? 👀
Jochem
Jochem•6d ago
it is, yeah
StefanH
StefanH•6d ago
Closures and hoisting are such a powerful concept once you understand and use it consciously. You can basically model private class variables and construction / destruction lifetimes all just using functions
Faker
FakerOP•6d ago
yep, good to know that they exist, will surely figured out some of their use cases when playing around
Jochem
Jochem•6d ago
keep in mind though, if you want a variable to exist in global scope, it's much better to declare it with let in the global scope than it is to use var basically, just don't ever use var anymore
Faker
FakerOP•6d ago
noted, ty
StefanH
StefanH•6d ago
I did some cursed shit like this before and my linters all screamed at me :)
for(var i = 0; feed(i); i++);
return i;
for(var i = 0; feed(i); i++);
return i;
I did replace it with let declaration eventually https://github.com/source-lib/sourcelib/blob/master/packages%2Fkv%2Fsrc%2Ftokenizer.ts#L164
Rägnar O'ock
Rägnar O'ock•6d ago
Var are hoisted to the function level, they are only global when created outside of a function. BTW, the snippet posted in the original post will always log the same thing no matter what button is clicked. That's because the i used in the template literal is created with a let (block scope) at the global level and updated after each iteration. So when the template literal is evaluated and logged it points to the same variable for each of the buttons, and because it is incremented after each iteration of the loop all 3 buttons will log the message with i being 3 instead of the 1 2 and 3 you would probably expect. That's... Ew... That's because you can redeclare variables created with var while doing so with let results in an error (given the declaration is in the same scope)
var a = 1;
console.log(a)
var a = 2;
console.log(a)
// logs :
// 1
// 2
var a = 1;
console.log(a)
var a = 2;
console.log(a)
// logs :
// 1
// 2
Will works and results in a being 2 at the end while the following would error out with a syntax error :
let a = 1;
console.log(a);
let a = 2;
console.log(a);

// logs :
// 1
// SyntaxError: redeclaration of formal parameter "a"
let a = 1;
console.log(a);
let a = 2;
console.log(a);

// logs :
// 1
// SyntaxError: redeclaration of formal parameter "a"
You can however declare a new variable with the same name in a nested scope (by simply adding {} around the code you want to scope, thus creating an anonymous block scope).
let a = 1;
{
let a = 2;
console.log(a);
}
console.log(a);
// log :
// 2
// 1
let a = 1;
{
let a = 2;
console.log(a);
}
console.log(a);
// log :
// 2
// 1
That 3rd block does something called shadowing. The second declaration of a inside the block prevents access to the a declared at the root, casting a shadow on it and hiding it from view if you want.

Did you find this page helpful?