It's easiest to think of this in terms of a timeline.
i is a variable name that points to a value.  When you go through the loop (the first thing to happen), i is being constantly reset to a higher value (first 0, then 1 etc).  Each time it iterates you bind a function to a button.  That function is not called yet, and it references i.  
Later, when a button is clicked, the function executes and looks for the value of i.  Because the loop has completed at this point, i will be equal to the number of buttons (in this case 5).  
The timeline
So basically:
...
- iis set to n (the number of buttons)
 
- iis greater than the loop condition and the loop completes
 
- The user clicks a button 
- The callback is fired and references - iwhich is currently set to n
 
- the alert fires with n as its value. 
Fixing the issue
Lots of ways to fix it.  The easiest old (backwards compatible way) is to pass the current value to a function (so it is read right away), and then use that to return a new function which has saved the old value in a closure:
for (i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", createHandler(i));
}
function createHandler(val) {
  return function() {
    alert("You just clicked " + val);
  }
} 
the new (ES6) way that may not work in all browsers is to use let, which limits a variable to block scope.
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", function () {
    alert("You just clicked " + i);
  });
}