Em 2015, com o lançamento do ES6, muitas mudanças começaram a acontecer no JavaScript. Quais foram as mudanças abaixo?

Hoje em dia, funções possem ser definidas com valores padrões de forma muito mais fácil graças a implementação dos "default parameters".

A declaração desses parâmetros é feita no próprio campo de parâmetros,com uma atribuição realizada na hora da declaração, como visto a seguir:

// Sintaxe básica

function multiply(a, b = 2) {
return a * b;
}

multiply(5); // 10

// Default parameters também funcionam com "later default parameters", ou seja, parâmetros que são lidos depois (mais para direita)

function foo (num = 1, multi = multiply(num)) {
return [num, multi];
}
foo(); // [1, 2]
foo(6); // [6, 12]

Mas, para exemplificar como essa mudança impactou a criação de funções com parâmetros padrões, vamos ver o seguinte exemplo:

// Queremos um elemento <p>, com um conteúdo definido e 2 classes atribuídas. Para isso, utilizaremos a função
                                    
createElement('p', {
    content: 'Um texto super único',
    classNames: ['texto-muito-especial', 'muito-grande']
});

// Retorna <p class="texto-muito-especial muito-grande">Um texto super único</p>

// Para esse método ser mais útil, ele deve sempre retornar um elemento padrão quando qualquer um dos argumentos não for passado.

createElement(); 

// Retorna <div class="texto-modulo padrao">Padrãozinho</div>
                            

Antes de existirem os default parameters, uma possível implementação dessa função era a seguinte:

// Sem os default parameters, fica desnecessariamente grande

function createElement (tag, config) {
    tag = tag || 'div';
    config = config || {};

    const element = document.createElement(tag);
    const content = config.content || 'Padrãozinho';
    const text = document.createTextNode(content);
    let classNames = config.classNames;

    if (classNames === undefined) {
    classNames = ['modulo-texto', 'padrao'];
    }

    element.classList.add(...classNames);
    element.appendChild(text);

    return element;
}
                            

E assim fica a mesma função com os default parameters:

function createElement (tag = 'div', {
    content = 'Padrãozinho',
    classNames = ['modulo-texto', 'especial']
} = {}) {
    const element = document.createElement(tag);
    const text = document.createTextNode(content);

    element.classList.add(...classNames);
    element.appendChild(text);

    return element;
}
                            

Sabe aquela ideia de de ter que digitar console.log("Oi" + nome + "! Como você está?") utilizando o símbolo "+" para concactenar os argumentos passados?

O ES6 resolveu muito esse problema, trazendo as template strings pro Javascript.

A sintaxe das template strings é a seguinte:

Para gerar uma string:
`string text`
Para gerar uma string de múltiplas linhas:
`string text line 1
string text line 2`
Interpolando valores diretamente dentro da string:
`string text ${expression} string text`
Templates reutilizáveis:
const templateFn = expression => `string text ${expression} string text`;

Porém, ainda existe uma outra feature bacana das template strings, que são chamadas de "tagged template literals", que tem a capacidade de chamar funções (chamadas de tag functions). Veremos a seguir:

Vamos supor a função mensagemExemplo():

function mensagemExemplo(mensagemParte1, nome, mensagemParte2) {
    console.log(`${mensagemParte1} ${nome}! ${mensagemParte2}`)
}

Ela poderia ser chamada em modo de tagged functions utilizando as template strings na seguinte sintaxe:

function exemploMelhorado(strings, ...expressoes) {
    return expressoes.reduce((acumulator, expressao, index) => {
        return acumulator + expressao + strings[index + 1]
    }, strings[0])}

Executando a função:

nome = "Caio"
let mensagemMelhoradaFinal = exemploMelhorado`Olá ${nome}! Tudo bem?`

console.log(mensagemMelhoradaFinal)
                                    

E como isso acontece? Vamos modificar a função e pedir pra ele retornar apenas os argumentos (que são arrays):

Resultado: é claro que parece que mais trabalhoso o exemplo melhorado, mas imagina o caso de uma função com muitos argumentos? Sem falar na flexibilidade dada ao código, já que agora é possível manipular os itens passados da forma que quiser.

Destructring é a técnica implementada no ES6 que possibilita a extração de dados de arrays ou objetos em diversas variáveis.

É possível, por exemplo, declarar arrays e objetos utilizando o rest operator da seguinte maneira:

let a, b, rest;
[a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20

[a, b, ...rest] = [10, 20, 30, 40, 50];
console.log(a); // 10
console.log(b); // 20
console.log(rest); // [30, 40, 50]

({ a, b } = { a: 10, b: 20 });
console.log(a); // 10
console.log(b); // 20

// Stage 4(finished) proposal
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a); // 10
console.log(b); // 20
console.log(rest); // {c: 30, d: 40}

Repare que é possível desestruturar (destructure) o array para os elementos do lado esquerdo da atribuição.

Outro exemplo que mostra bem o que acontece:

const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1
console.log(z); // 2

Porém é necessário ficar atento ao fato de que, se o elemento passado na atribuição do lado direito tiver seu comprimento (length) menor que o elemento do lado esqurdo, apenas serão passados os valores existentes, enquanto os outros valores serão redefinidos para "undefined":

const foo = ['one', 'two'];

const [red, yellow, green, blue] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // undefined
console.log(blue);  //undefined

Também é possível trocar variáveis de lugar sem precisar de uma temporária!

let a = 1;
let b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

A forma mais fácil de observar destructuring básico de objetos pode ser vista no seguinte exemplo, que funciona de forma parecida com o Array:

const user = {
id: 42,
isVerified: true
};

const {id, isVerified} = user;

console.log(id); // 42
console.log(isVerified); // true

Porém, uma das funcionalidades mais interessantes do destructuring em objetos é a possibilidade de deixar o código mais enxuto e ainda assim, bastante legível (talvez até mais). Veremos o exemplo.

Considere o seguinte objeto com várias informações sobre o usuário.

const user = {
    id: 42,
    displayName: 'lemoscaio',
    fullName: {
    firstName: 'Caio',
    lastName: 'Lemos'
    }
};

Sem a possibilidade de object destructuring, caso precisássemos de uma função que retorna o "id" do usuário, precisamos de algo como:

function userId(user) {
    return user.id;
}

console.log(userId(user)); // 42

Porém, a partir do ES6, é possível trabalhar com a seguinte estrutura, deixando implícito o objeto na função:

function userId({id}) {
    return id;
}

console.log(userId(user)); // 42

Também é possível atribuir nomes para as propriedades, que funcionam como se fossem variáveis dentro da função, usar destructuring em objetos aninhados e até mesmo utilizar a funcionalidade para declarar default parameters, como visto no próprio subtópico.