getElementsByTagName возвращает NodeList -
живую коллекцию элементов, не массив.
Соответственно:
код 1: всё ок, вы получаете коллекцию элементов уже после загрузки страницы когда все они уже присутствуют, берёте второй по счёту и передаёте на вывод console.log. console.log выводит его.
код 2: вы получаете коллекцию элементов до загрузки страницы, потому на момент вызова пустую, берёте второй элемент пустой коллекции(поскольку его нет то undefined) и передаёте на вывод console.log. console.log выводит undefined.
код 3: вы получаете коллекцию элементов до загрузки страницы, потому на момент вызова пустую, и передаёте оную на вывод console.log. Тут кроется подвох. Передаётся коллекция в console.log
ссылкой а не копией, и поскольку когда вы смотрите в консоль страница уже успела загрузиться - коллекция успевает к тому времени заполниться элементами загруженной страницы. Потому вы и видите в консоли её элементы.
На момент вызова же она также пуста как и в коде 2.
Чтобы это увидеть можно преобразовать код 3 к след. виду:
var a = Array.prototype.slice.call( document.getElementsByTagName ("h1") ); //преобразуем живой NodeList в статичный массив
console.log(a);
P.S. Лично мне не слишком нравится такой подход в реализации console.log - удобнее и логичнее было бы при выводе делать полные копии переданных объектов на момент передачи. Наверное такой поход приводил к излишним расходам памяти или просто противоречил стилю js. Так или иначе теперь уже ничего не попишешь, просто надо учитывать, что в консоли мы увидим самое последнее состояние объекта, а не промежуточное.