Principais Novidades do ES6 e Suas Aplicações

  1. Diferenças entre var, let e const

As declarações de variáveis em JavaScript evoluíram com o ES6, introduzindo let e const para oferecer um controle mais granular sobre o escopo e o comportamento das variáveis.

Comportamentos Distintos:

  • var: Permite o "hoisting" (elevação) de variáveis, onde a declaração é movida para o topo do escopo, resultando em undefined se acessada antes da atribuição. Não possui escopo de bloco e permite a redeclaração.
  • let e const: Não realizam hoisting da mesma forma que var. O acesso a elas antes da sua declaração resulta em um ReferenceError devido à "zona de morte temporal" (Temporal Dead Zone - TDZ). Possuem escopo de bloco e const impede a redeclaração e a reatribuição.

Exemplos de Código:


// Exemplo com var (hoisting)
console.log(varVariable); // undefined
var varVariable = 100;

// Exemplo com let/const (TDZ)
try {
   console.log(letVariable);
} catch (error) {
   console.error(error.message); // Cannot access 'letVariable' before initialization
}
let letVariable = 100;

// Exemplo de escopo de bloco
if (true) {
   var blockVar = "Dentro do bloco";
}
console.log(blockVar); // Dentro do bloco

if (true) {
   let blockLet = "Bloco Let";
}
// console.log(blockLet); // blockLet is not defined

// Exemplo de redeclaração
var redeclaredVar = 50;
var redeclaredVar = 25;
console.log(redeclaredVar); // 25

// let redeclaredLet = 50;
// let redeclaredLet = 25; // Identifier 'redeclaredLet' has already been declared

// Exemplo de reatribuição
let mutableVar = 10;
mutableVar = 5;
console.log(mutableVar); // 5

const immutableVar = 10;
// immutableVar = 5; // Uncaught TypeError: Assignment to constant variable
 
  1. Estruturas de Dados: Set e Map no ES6

O ES6 introduziu Set e Map como estruturas de dados poderosas para gerenciar coleções de dados de forma mais eficiente e flexível.

Características Principais:

  • Set: Armazena valores únicos. Elementos podem ser de qualquer tipo. A ordem de inserção é mantida.
  • Map: Armazena pares chave-valor. As chaves podem ser de qualquer tipo (incluindo objetos), e os valores também. Diferente de um objeto literal, Map permite chaves não-string e mantém a ordem de inserção.

Exemplos de Uso:


const objectList = [
 { id: 1, name: "Item A" },
 { id: 2, name: "Item B" }
];

const mapEntries = [
 ['chaveUnica', 'valorAssociado'],
 [123, { id: 1, name: "Objeto Mapeado" }]
];

// Criando um Set
const uniqueSet = new Set(objectList);
console.log("Set:", uniqueSet);
// Set: Set(2) { { id: 1, name: 'Item A' }, { id: 2, name: 'Item B' } }

// Criando um Map
const dataMap = new Map(mapEntries);
console.log("Map:", dataMap);
// Map(2) { 'chaveUnica' => 'valorAssociado', 123 => { id: 1, name: 'Objeto Mapeado' } }

// Adicionando e verificando elementos
uniqueSet.add({ id: 1, name: "Item A" }); // Não adicionará, pois é um objeto diferente mas com conteúdo igual
console.log("Set após adicionar duplicado:", uniqueSet); // O tamanho não muda se o objeto for diferente, mesmo com mesmo conteúdo

dataMap.set('novaChave', 'novoValor');
console.log("Map após adicionar:", dataMap);
 
  1. Compreendendo o Promise no ES6

Promise é uma maneira moderna e elegante de lidar com operações assíncronas em JavaScript, evitando o "callback hell" e melhorando a legibilidade do código.

Conceitos Fundamentais:

  • Estados: Um Promise pode estar em um de três estados: pending (pendente), fulfilled (realizado) ou rejected (rejeitado).
  • Métodos:
    • .then(onFulfilled, onRejected): Executa callbacks quando o Promise é realizado ou rejeitado.
    • .catch(onRejected): Um atalho para .then(undefined, onRejected), útil para tratar erros.
    • .finally(onFinally): Executa um callback quando o Promise é finalmente liquidado (realizado ou rejeitado).
  • Promise.all(iterable): Retorna um novo Promise que se resolve quando todos os Promises no iterável se resolvem, ou rejeita se qualquer um dos Promises rejeitar.
  • Promise.race(iterable): Retorna um novo Promise que se resolve ou rejeita assim que qualquer um dos Promises no iterável se resolve ou rejeita.

Cenários de Uso:

  • Carregamento de recursos assíncronos (ex: imagens, scripts).
  • Requisições de rede (AJAX, Fetch API).
  • Operações que podem levar tempo e precisam de feedback (timeouts, progresso).

function simulateAsyncOperation(succeed = true) {
   return new Promise((resolve, reject) => {
       setTimeout(() => {
           if (succeed) {
               resolve("Operação concluída com sucesso!");
           } else {
               reject(new Error("Falha na operação."));
           }
       }, 1000);
   });
}

// Exemplo de uso básico
simulateAsyncOperation(true)
   .then(result => {
       console.log("Sucesso:", result);
   })
   .catch(error => {
       console.error("Erro:", error.message);
   })
   .finally(() => {
       console.log("Operação finalizada, independentemente do resultado.");
   });

// Exemplo com Promise.all
const promise1 = simulateAsyncOperation(true);
const promise2 = simulateAsyncOperation(true);

Promise.all([promise1, promise2])
   .then(results => {
       console.log("Todos os Promises foram resolvidos:", results);
   })
   .catch(error => {
       console.error("Um dos Promises falhou:", error.message);
   });

// Exemplo com Promise.race
const promise3 = new Promise((resolve) => setTimeout(() => resolve("Rápido!"), 500));
const promise4 = new Promise((resolve) => setTimeout(() => resolve("Lento."), 1500));

Promise.race([promise3, promise4])
   .then(result => {
       console.log("O primeiro Promise a completar:", result); // Output: O primeiro Promise a completar: Rápido!
   });
 
  1. Explorando Generator no ES6

Generators são uma funcionalidade poderosa para criar iteradores personalizados e gerenciar fluxos de controle assíncronos de forma mais síncrona.

Características Principais:

  • Sintaxe: Definidos com a palavra-chave function* e utilizam o operador yield para pausar a execução e retornar um valor.
  • Execução: Ao chamar uma função generator, ela retorna um objeto iterador. O código dentro do generator só é executado quando o método .next() do iterador é chamado.
  • yield: Pausa a execução do generator e retorna o valor especificado. O estado do generator é salvo.
  • .next(): Retoma a execução do generator a partir do último yield. Retorna um objeto com as propriedades value (o valor retornado pelo yield) e done (um booleano indicando se o generator terminou).
  • return: Encerra o generator e retorna um valor final.

Cenários de Uso:

  • Simplificar a escrita de código assíncrono, tornando-o mais parecido com código síncrono.
  • Implementar iteradores personalizados para estruturas de dados complexas.
  • Gerenciamento de estado em aplicações.

function* simpleGenerator() {
   let count = 0;
   console.log("Iniciando o generator...");
   yield ++count; // Retorna 1, pausa
   console.log("Retomando após o primeiro yield...");
   yield ++count; // Retorna 2, pausa
   console.log("Último yield...");
   return ++count; // Retorna 3, finaliza
}

const generatorInstance = simpleGenerator();

console.log("Primeira chamada next():", generatorInstance.next());
// Output:
// Iniciando o generator...
// Primeira chamada next(): { value: 1, done: false }

console.log("Segunda chamada next():", generatorInstance.next());
// Output:
// Retomando após o primeiro yield...
// Segunda chamada next(): { value: 2, done: false }

console.log("Terceira chamada next():", generatorInstance.next());
// Output:
// Último yield...
// Terceira chamada next(): { value: 3, done: true }

console.log("Chamada após finalização:", generatorInstance.next());
// Output:
// Chamada após finalização: { value: undefined, done: true }

// Iterando com for...of
function* countTo(limit) {
 for (let i = 1; i <= limit; i++) {
   yield i;
 }
}

console.log("Iterando com for...of:");
for (const num of countTo(5)) {
 console.log(num); // 1, 2, 3, 4, 5
}
 
  1. O Tipo Symbol no ES6

Symbol é um novo tipo de dado primitivo introduzido no ES6, projetado para criar identificadores únicos e imutáveis, especialmente úteis para propriedades de objetos.

Propriedades e Benefícios:

  • Unicidade Garantida: Cada Symbol criado é garantidamente único, mesmo que possua a mesma descrição.
  • Oculto de Iterações Comuns: Propriedades com chaves Symbol não são listadas por Object.keys(), for...in ou Object.entries(). Elas são acessíveis via Object.getOwnPropertySymbols() ou Reflect.ownKeys().
  • Evita Conflitos: Ideal para adicionar propriedades a objetos existentes sem o risco de sobrescrever propriedades de terceiros (padrão Mixin).
  • Ignorado por JSON.stringify(): Propriedades Symbol não são serializadas em JSON.

Cenários de Uso:

  • Definir chaves de propriedades privadas ou protegidas em objetos.
  • Implementar padrões como Mixin onde a adição de métodos a objetos de terceiros pode causar conflitos de nome.
  • Criar identificadores únicos para constantes ou tipos.

const uniqueId = Symbol("identificadorUnico");
const anotherUniqueId = Symbol("identificadorUnico");

console.log(uniqueId === anotherUniqueId); // false

const myObject = {
   name: "Exemplo",
   [Symbol("internalData")]: "Informação Secreta",
   regularProperty: "Valor Comum"
};

console.log("Chaves comuns:", Object.keys(myObject));
// Chaves comuns: [ 'name', 'regularProperty' ]

const symbols = Object.getOwnPropertySymbols(myObject);
console.log("Symbols:", symbols);
// Symbols: [ Symbol(internalData) ]

console.log("Valor da propriedade Symbol:", myObject[symbols[0]]);
// Valor da propriedade Symbol: Informação Secreta

console.log("Todos as chaves (incluindo Symbols):", Reflect.ownKeys(myObject));
// Todos as chaves (incluindo Symbols): [ 'name', Symbol(internalData), 'regularProperty' ]

console.log("JSON.stringify:", JSON.stringify(myObject));
// JSON.stringify: {"name":"Exemplo","regularProperty":"Valor Comum"}
 
  1. Funções de Seta (Arrow Functions) vs. Funções Tradicionais

As funções de seta (=>) introduzidas no ES6 oferecem uma sintaxe mais concisa e um comportamento de this lexicalmente vinculado, diferenciando-se significativamente das funções tradicionais.

Principais Diferenças:

  • Sintaxe: Mais curta e expressiva.
  • this: Funções de seta não possuem seu próprio this. Elas herdam o this do escopo pai onde foram definidas. O this em funções de seta é lexical e não pode ser alterado com .call(), .apply() ou .bind(). Funções tradicionais têm seu próprio this, cujo valor é determinado por como a função é chamada.
  • arguments: Funções de seta não possuem o objeto arguments. Para acessar todos os argumentos, deve-se usar parâmetros rest (...args).
  • prototype: Funções de seta não possuem a propriedade prototype.
  • Construtores: Devido à ausência de this próprio e prototype, funções de seta não podem ser usadas como construtores com a palavra-chave new.
  • yield: Funções de seta não podem ser usadas como funções geradoras.

Arrow Functions Não Podem Ser Construtores:

A principal razão pela qual funções de seta não podem ser usadas como construtores é a forma como lidam com o this. O operador new espera que o construtor crie e vincule um novo objeto ao this. Como a função de seta herda o this do escopo externo e este não pode ser alterado, a tentativa de usar new com uma função de seta resultará em um erro.


// Função Tradicional
function RegularFunction(name) {
   this.name = name;
   console.log("Regular:", this);
}
RegularFunction.prototype.greet = function() {
   console.log(`Olá, sou ${this.name}`);
};

const person1 = new RegularFunction("Alice");
person1.greet();

// Função de Seta
const ArrowFunction = (name) => {
   // this.name = name; // Isso causaria erro ou não funcionaria como esperado
   console.log("Arrow function this (contexto externo):", this);
   // Para acessar parâmetros, use parâmetros rest:
   // console.log("Arguments:", arguments); // Isso geraria um erro
};

// Tentativa de usar como construtor (não funciona)
try {
   // const person2 = new ArrowFunction("Bob");
   // console.log(person2);
} catch (error) {
   console.error("Erro ao usar Arrow Function como construtor:", error.message);
   // Erro ao usar Arrow Function como construtor: ArrowFunction is not a constructor
}

// Exemplo de this lexical em Arrow Function
const obj = {
   value: 42,
   getValueRegular: function() {
       setTimeout(function() {
           console.log("Regular this:", this.value); // undefined (this aponta para window/global ou undefined em strict mode)
       }, 100);
   },
   getValueArrow: function() {
       setTimeout(() => {
           console.log("Arrow this:", this.value); // 42 (this é herdado do obj)
       }, 100);
   }
};

obj.getValueRegular();
obj.getValueArrow();
 
  1. Módulos: CommonJS vs. ES6 Modules

A gestão de módulos é crucial para organizar o código. CommonJS (usado no Node.js) e ES6 Modules (padrão nativo do JavaScript) possuem abordagens distintas.

Diferenças Chave:

  • Exportação:
    • CommonJS: Exporta um único valor (ou um objeto contendo múltiplos valores) usando module.exports ou exports. O valor exportado é uma cópia.
    • ES6 Modules: Permite exportar múltiplos valores nomeados (export const ..., export function ...) ou um único valor padrão (export default ...). As exportações são referências dinâmicas.
  • Importação:
    • CommonJS: Usa require(), que é uma função síncrona executada em tempo de execução.
    • ES6 Modules: Usa import, que é uma declaração estática avaliada em tempo de compilação/análise. O carregamento é assíncrono e pode ser otimizado.
  • this: Em CommonJS, this se refere ao módulo atual. Em ES6 Modules, this no escopo de nível superior é undefined.
  • Dinamicidade vs. Estaticidade: CommonJS permite a importação dinâmica (dentro de condicionais, loops), enquanto ES6 Modules exigem que as declarações import estejam no nível superior do escopo.

// --- Arquivo: matematicaCommonJS.js ---
const soma = (a, b) => a + b;
const subtracao = (a, b) => a - b;
module.exports = { soma, subtracao };

// --- Arquivo: matematicaES6.js ---
export const soma = (a, b) => a + b;
export const subtracao = (a, b) => a - b;
export const PI = 3.14159;

// --- Arquivo: principalCommonJS.js ---
// const matematica = require('./matematicaCommonJS');
// console.log("CommonJS Soma:", matematica.soma(10, 5));

// --- Arquivo: principalES6.js ---
// import { soma, subtracao, PI } from './matematicaES6';
// console.log("ES6 Soma:", soma(10, 5));
// console.log("ES6 PI:", PI);

// Exemplo de diferença de referência vs cópia (simplificado)
// No CommonJS, se o módulo exportado for alterado depois, as importações antigas não refletem a mudança.
// No ES6, as importações são referências e refletem as mudanças.
 
  1. Object.defineProperty vs. Proxy

Ambas as APIs permitem interceptar e modificar operações em objetos, mas Proxy oferece uma abordagem mais poderosa e flexível.

Object.defineProperty:

  • Granularidade: Opera em propriedades individuais de um objeto.
  • Limitações:
    • Não é eficiente para observar mudanças em arrays (requer sobrescrita de métodos mutáveis).
    • Para observar todas as propriedades de um objeto, é necessário iterar sobre elas (Object.keys()) e aplicar defineProperty recursivamente para objetos aninhados.
  • Uso: Definir descritores de propriedades (writable, enumerable, configurable) e getters/setters.

Proxy:

  • Granularidade: Opera sobre o objeto inteiro, permitindo interceptar uma gama muito maior de operações (gets, sets, deleteProperty, apply, construct, etc.).
  • Vantagens:
    • Observa mudanças em arrays nativamente.
    • Não requer iteração manual para "envelopar" propriedades individuais ou aninhadas.
    • Oferece mais "traps" (interceptadores) para lidar com diferentes tipos de operações.
    • Potencialmente mais performático em cenários complexos devido a otimizações nativas.
  • Uso: Cria um objeto "proxy" que envolve o objeto original, permitindo executar código personalizado antes ou depois das operações padrão.

// --- Object.defineProperty ---
const targetObj = { data: "valor inicial" };
const observedObj = {};

Object.defineProperty(observedObj, 'data', {
   configurable: true,
   enumerable: true,
   get: function() {
       console.log("Acessando 'data' via getter...");
       return targetObj.data;
   },
   set: function(newValue) {
       console.log(`Definindo 'data' para: ${newValue}`);
       targetObj.data = newValue;
   }
});

console.log(observedObj.data); // Acessando 'data' via getter... valor inicial
observedObj.data = "novo valor"; // Definindo 'data' para: novo valor
console.log(targetObj.data); // novo valor

// --- Proxy ---
const targetProxy = { count: 0 };
const handler = {
   get: function(obj, prop) {
       console.log(`Proxy: Acessando propriedade '${prop}'`);
       return obj[prop];
   },
   set: function(obj, prop, value) {
       console.log(`Proxy: Definindo propriedade '${prop}' para '${value}'`);
       obj[prop] = value;
       return true; // Indica sucesso
   }
};

const proxiedObject = new Proxy(targetProxy, handler);

proxiedObject.count = 1; // Proxy: Definindo propriedade 'count' para '1'
console.log(proxiedObject.count); // Proxy: Acessando propriedade 'count' \n 1
 
  1. Cópia de Objetos: Object.assign vs. Operador de Expansão (...)

Tanto Object.assign() quanto o operador de expansão (...) são usados para criar cópias de objetos e arrays, mas ambos realizam uma cópia rasa (shallow copy).

Cópia Rasa (Shallow Copy):

Significa que apenas os valores das propriedades de nível superior são copiados. Se uma propriedade contiver um objeto ou array aninhado, a referência a esse objeto/array aninhado é copiada, não o objeto/array em si. Modificações em objetos aninhados na cópia afetarão o original.

Diferenças:

  • Object.assign(target, ...sources):
    • Copia as propriedades enumeráveis de um ou mais objetos fonte para um objeto alvo.
    • Modifica o objeto alvo (se for um objeto literal {}, ele cria um novo objeto).
    • Copia também propriedades de símbolos (Symbols).
    • Pode acionar setters no objeto alvo.
  • Operador de Expansão (...):
    • Cria um novo objeto ou array literal contendo os elementos ou propriedades do iterável original.
    • É uma sintaxe mais concisa para criar cópias rasas.
    • Não copia propriedades herdadas ou de classes (apenas as próprias do objeto/array).
    • Copia propriedades de símbolos (Symbols).

const originalObject = {
   a: 1,
   b: { c: 2 },
   [Symbol('id')]: 123
};

// Usando Object.assign
const copiedByAssign = Object.assign({}, originalObject);
copiedByAssign.a = 10;
copiedByAssign.b.c = 20; // Afeta o original!

console.log("Original após assign:", originalObject);
// Original após assign: { a: 1, b: { c: 20 }, [Symbol(id)]: 123 }
console.log("Copiado por assign:", copiedByAssign);
// Copiado por assign: { a: 10, b: { c: 20 } } (Note que o Symbol não é mostrado aqui por padrão)

// Usando Operador de Expansão
const copiedBySpread = { ...originalObject };
copiedBySpread.a = 100;
copiedBySpread.b.c = 200; // Afeta o original!

console.log("Original após spread:", originalObject);
// Original após spread: { a: 1, b: { c: 200 }, [Symbol(id)]: 123 }
console.log("Copiado por spread:", copiedBySpread);
// Copiado por spread: { a: 100, b: { c: 200 } }

// Verificando Symbols em cópias (acesso via Reflect.ownKeys)
console.log("Symbols em copiedByAssign:", Reflect.ownKeys(copiedByAssign)); // Pode não incluir Symbols dependendo da implementação exata do assign
console.log("Symbols em copiedBySpread:", Reflect.ownKeys(copiedBySpread)); // Deve incluir o Symbol

// Para Deep Copy (cópia profunda), seria necessário uma função recursiva ou bibliotecas como Lodash.
 
  1. Visão Geral das Novidades do ES6

O ECMAScript 2015 (ES6) foi uma das atualizações mais significativas do JavaScript, introduzindo uma vasta gama de novas funcionalidades e melhorias sintáticas.

Categorias Principais de Novidades:

  • Melhorias de Sintaxe e Escopo:
    • let e const para gerenciamento de escopo de bloco.
    • Funções de seta (=>) com this lexical.
    • Template Literals (``) para strings mais flexíveis.
    • Desestruturação de arrays e objetos.
    • Parâmetros padrão para funções.
    • Operadores de rest (...) e spread (...).
  • Novos Objetos e Métodos:
    • Promise para gerenciamento de assincronismo.
    • Proxy e Reflect para metaprogramação.
    • Object.assign() para mesclagem de objetos.
    • Object.is() para comparação de valores mais precisa.
    • Métodos estáticos em Strings (startsWith, endsWith, includes, repeat).
    • Métodos de Array (find, findIndex, fill, copyWithin).
    • Number.isNaN(), Number.isInteger().
  • Novos Tipos de Dados e Estruturas:
    • Symbol para identificadores únicos.
    • Set para coleções de valores únicos.
    • Map para coleções de pares chave-valor.
  • Módulos:
    • Sistema de módulos nativo com import e export.
  • Funcionalidades Avançadas:
    • Generators (function*) para fluxos de controle complexos e assincronismo.
  1. Parâmetros Rest (...)

O parâmetro rest (...) em funções permite que uma função aceite um número indefinido de argumentos como um array.

Como Funciona:

  • Permite representar um número "ilimitado" de argumentos como um array.
  • Deve ser o último parâmetro na definição da função. Se houver outros parâmetros após o rest, ocorrerá um erro de sintaxe.
  • Dentro da função, o parâmetro rest se comporta como um array normal, permitindo o uso de todos os métodos de array.

Exemplo:


function processItems(initialValue, ...remainingItems) {
   console.log("Valor inicial:", initialValue);
   console.log("Itens restantes (array):", remainingItems);

   // Usando métodos de array com remainingItems
   const sumOfRemaining = remainingItems.reduce((acc, current) => acc + current, 0);
   console.log("Soma dos itens restantes:", sumOfRemaining);

   // Usando o operador spread para passar os itens como argumentos individuais
   console.log("Itens passados individualmente:", ...remainingItems);
}

processItems(10, 20, 30, 40, 50);
// Output:
// Valor inicial: 10
// Itens restantes (array): [ 20, 30, 40, 50 ]
// Soma dos itens restantes: 140
// Itens passados individualmente: 20 30 40 50

// Exemplo de erro com parâmetro rest não sendo o último
// function invalidFunction(...args, lastArg) { } // SyntaxError: Rest parameter must be last in the parameter list.
 

Tags: ES6 javascript var let const

Publicado em 8-29 09:09