Qual a diferença entre for, forEach e for...of?

Existem inúmeras maneiras de fazer laços em arrays e objetos no Javascript. Vamos ver algumas diferenças entre elas:

Diferenças de sintaxe
O que acontece com as construções for e for... in?

Esses loops fornecem acesso apenas ao índice do array, e não ao elemento em si. Vejamos os exemlos:

function loopComFor() {
    let numeros = [10, 20, 30, 40, 50]

    for (let i = 0; i < numeros.length; i++) {
        console.log(i) // 0, 1, 2, 3, 4
    }
}
function loopComForIn() {
    let numeros = [10, 20, 30, 40, 50]

    for (const numero in numeros) {
        console.log(numero) // 0, 1, 2, 3, 4
    }
}

Apesar de ter dado o nome "numero", um nome mais adequado seria "índice", já que o valor retornado não é o elemento.

O que acontece com as construções forEach e for... of?

Ao contrário das outras, essas te dão acesso ao próprio elemento do array. Com o forEach é até mesmo possível acessar também o índice, pasando um segundo parâmetro.

function loopComForIn() {
    let numeros = [10, 20, 30, 40, 50]

    for (const numero of numeros) {
        console.log(numero) // 10, 20, 30, 40, 50
    }
}
function loopComForEach() {
    let numeros = [10, 20, 30, 40, 50]

    numeros.forEach((numero, indice) => {
    console.log("Numero " + numero + " na posição " + indice)
    })
}
Porém, o que acontece caso o Array tenha uma chave não-numérica?

Considere o exemplo:

const arr = ['a', 'b', 'c'];
arr.test = 'bad';

Esse código retorna um array com a seguinte estrutura:

['a', 'b', 'c', test: 'bad'] //arr.length continua sendo 3

Nesse caso, o loop for... in é o único dos 4 que não ignora corretamente a chave não-numérica e, por isso, geralmente deve ser evitado ao percorrer um Array.

E se o elemento tiver espaços vazios?

Considerando o array com espaço vazio:

const arrayComVazio = ['a',, 'c'];

arrayComVazio.length; // 3

Vamos a mais um exemplo:

// Mostra "a, undefined, c"
for (let i = 0; i < arrayComVazio.length; ++i) {
    console.log(arr[i]);
}

// Mostra "a, c"
arrayComVazio.forEach(v => console.log(v));

// Mostra "a, c"
for (let i in arrayComVazio) {
    console.log(arrayComVazio[i]);
}

// Mostra "a, undefined, c"
for (const v of arrayComVazio) {
    console.log(v);
}

Dos 4, apenas o forEach e o for...in pulam o elemento vazio.

PS: Apesar de, por conta dos problemas listados dos outros loops, o o loop forEach parecer ser "o melhor", lembre-se que cada um possui sua aplicação. O loop forEach, por exemplo, não se dá muito bem com funções assíncronas.