Your first use of buttonTitle is inside the string that you're assigning to the innerHTML of the id="buttons" element, inside the onclick attribute you're using there. The code in the onclick string is evaluated at global scope,¹ where you don't have a buttonTitle variable.
I strongly recommend not building code strings like that, not least for this reason. Instead, use a function:
for (var i = 0; i < arrayButtons.length; i++) {
    console.log("console! ", arrayButtons[i].title); // works!
    let buttonTitle = arrayButtons[i].title;
    const buttons = document.getElementById("buttons")
    buttons.insertAdjacentHTML(
        "beforeend",
        "<div><button class='changeButton'>" +
        "<i class=\"icon\"></i>"+ buttonTitle + "</button></div>"
    );
    // Get the button we just added (the last one)
    const buttonList = buttons.querySelectorAll("button");
    const newButton = buttonList[buttonList.length - 1];
    // Add the handler to it
    newButton.addEventListener("click", function() {
        document.getElementById("changeByButtonClick").innerHTML = buttonTitle;
    });
}
Live Example:
const arrayButtons = [
    {title: "first"},
    {title: "second"},
    {title: "third"},
    {title: "fourth"},
];
for (var i = 0; i < arrayButtons.length; i++) {
    console.log("console! ", arrayButtons[i].title); // works!
    let buttonTitle = arrayButtons[i].title;
    const buttons = document.getElementById("buttons")
    buttons.insertAdjacentHTML(
        "beforeend",
        "<div><button class='changeButton'>" +
        "<i class=\"icon\"></i>"+ buttonTitle + "</button></div>"
    );
    // Get the button we just added (the last one)
    const buttonList = buttons.querySelectorAll("button");
    const newButton = buttonList[buttonList.length - 1];
    // Add the handler to it
    newButton.addEventListener("click", function() {
        document.getElementById("changeByButtonClick").innerHTML = buttonTitle;
    });
}
#changeByButtonClick {
    min-height: 1em;
}
<div id="changeByButtonClick"></div>
<div id="buttons">
    This is the initial content that we don't remove.
</div>
 
 
Also note that I used insertAdjacentHTML instead of using += on innerHTML. (Thanks to Niet the Dark Absol for pointing it out, I missed it was a +=.) Never use += on innerHTML, when you do that, the browser has to do this:
- Recurse through the nodes in the target element building an HTML string.
- Pass that string to the JavaScript layer.
- Receive the new string from the JavaScript layer.
- Tear down the entire contents of the target element.
- Parse the string.
- Build new replacement nodes for the previous contents plus the new nodes for the new content.
In the process it loses event handlers and other non-HTML information. In contrast, with insertAdjacentHTML, all it has to do is parse the string and insert the new nodes.
¹ "...at global scope..." Actually, it's a bit more complicated than that, it's a nested scope using (effectively) with blocks. But for the purposes of the question, the details there aren't important; it's nearly at global scope.