Five Row Buttons That All Report Row 5
A reporting screen builds five row buttons in a for loop. The loop variable is declared with var, and each button gets the same click handler:
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.textContent = 'Row ' + i;
btn.addEventListener('click', function () {
console.log('Opening row ' + i);
});
table.appendChild(btn);
}
Clicking any button logs Opening row 5. A proposed fix wraps the handler in an IIFE, but a reviewer asks why a let declaration is enough and whether the two fixes behave identically. A hard constraint is that the page still supports an old embedded browser that has no let, so the team cannot simply switch syntax everywhere. The buttons must also remain able to read the row index at click time, not at creation time, if the data is reordered later.
1. Explain precisely why every handler logs `5`, including why the button text is different from the logged value.
Read the worked answer
var is function-scoped, not block-scoped. The loop creates one binding named i for the entire enclosing function. Each iteration assigns a new number to that same binding, and each click handler closes over that same binding, not over a snapshot of its value.
During the loop, btn.textContent = 'Row ' + i reads the current value immediately, so the visible labels are 0 through 4. The handler body does not read i until a click occurs. By then the loop has completed, i has been incremented to 5, and the loop condition fails. All five handlers therefore see 5.
The distinction is between capturing a variable and capturing a value. The handlers capture the variable i. The text assignment captured the value at that moment. This is also why moving the handler registration after the loop would not help; the binding is still shared.
2. Compare the IIFE fix and the `let` fix. Do they behave identically, and which should be used under the old-browser constraint?
Read the worked answer
The let fix changes the loop header to for (let i = 0; i < 5; i++). A let declaration in a for header creates a fresh lexical binding per iteration, so each handler closes over a different i. It is concise and correct.
The IIFE fix creates a new function scope per iteration and passes the current value as an argument:
for (var i = 0; i < 5; i++) {
(function (rowIndex) {
btn.addEventListener('click', function () {
console.log('Opening row ' + rowIndex);
});
})(i);
}
It must copy i into a parameter. An IIFE that only wraps the handler but still reads outer i inside the handler still logs 5.
For the old embedded browser, use the IIFE or an equivalent factory function. The two fixes are equivalent for this closure problem, but not identical in general: let is lexical scoping, while the IIFE is an explicit function boundary. If rows can be reordered later, neither index-based fix is ideal; store a stable row id in a data attribute or closure and read that at click time.
3. The requirement says handlers must read the row index at click time if data is reordered. How does that change the recommended design?
Read the worked answer
A captured index describes the row's position at render time. If rows are reordered, that index may no longer identify the same record. The handler should therefore close over a stable identifier, such as a row id, and look up the current position when clicked.
For example, set btn.dataset.rowId = row.id and attach one delegated listener on the table. On click, read event.target.closest('button').dataset.rowId, then find the current index in the current data array. This avoids per-button closures entirely and handles reordering, insertion, and removal consistently.
If per-button handlers are required by the old browser, capture the id rather than the index. The closure problem is the same, but the captured value is stable. The IIFE or factory pattern still applies. The important judgement is to separate identity from position. Fixing the original var bug without addressing reordering would make the log correct only until the first sort.