At the time the function is called the loop will have completed. There is only one i variable and the function always sees its current value. So if you use i from inside the function you will see it with the value lis.length.
There are ways around it. If you can use ES2015 (possibly through a transpiler), then you can write:
const lis = document.querySelectorAll('li');
for(let i = 0; i < lis.length; i++){
    lis[i].addEventListener('mouseover', () => lis[i].style.color = 'green');
    lis[i].addEventListener('mouseout', () => lis[i].style.color ="black");
};
and now you have a different i for each time round the loop.
Or in older code you can push the loop body out to another function and pass i in as a parameter. That has the same effect of binding a different variable for each event:
var lis = document.querySelectorAll('li');
var _loop = function _loop(i) {
    lis[i].addEventListener('mouseover', function () {
        return lis[i].style.color = 'green';
    });
    lis[i].addEventListener('mouseout', function () {
        return lis[i].style.color = "black";
    });
};
for (var i = 0; i < lis.length; i++) {
    _loop(i);
}
(which is the code automatically produced by babel from the ES2015 example I gave above)