В примере с замыканием на самом деле все правильно работает, ошибка тут:
// console.log(`fib[${i}] = ${fib[i]}`)
console.log(`fib[${j}] = ${fib[j]}`)
У var область видимости функции или глобальная, у let и const – область видимости окружающего блока.
То есть пример можно переписать так, что будет равнозначно:
const fib = [1, 2, 3, 5, 8, 13];
var i = 0;
for(; i < fib.length; i++) {
setTimeout(function() {
console.log(`fib[${i}] = ${fib[i]}`)
}, 1500)
}
Видно, что на каждой итерации увеличивается значение одной и той же внешней переменной. Но когда счетчик i++ доходит до 6 и условие (i < 6) не выполняется, цикл завершает свою работу, однако значение переменной i теперь уже 6, а не 5.
Функция обратного вызова, переданная в setTimeout, ставится в очередь на выполнение. И эта очередь подойдет, только когда завершится выполнение основного потока. А к этому моменту i == 6. Дальше отложенные коллбэки начинают вызываться один за другим, и они берут это глобальное значение. Ну а элемента с индексом 6 в массиве нет, вот и undefined.
С замыканием или let или const, понятно, что такой проблемы не будет.