Apêndice

Herança de prototipagem

Com exceção de null e undefined, cada tipo de dados primitivo tem um protótipo, um wrapper de objeto correspondente que fornece métodos para trabalhar com valores. Quando uma pesquisa de método ou propriedade é invocada em um primitivo, o JavaScript envolve o primitivo em segundo plano e chama o método ou executa a pesquisa de propriedade no objeto wrapper.

Por exemplo, um literal de string não tem métodos próprios, mas é possível chamar o método .toUpperCase() nele graças ao wrapper de objeto String correspondente:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Isso é chamado de herança de protótipo, que herda propriedades e métodos do construtor correspondente de um valor.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object { … }

Você pode criar primitivos usando esses construtores, em vez de apenas defini-los pelo valor deles. Por exemplo, usar o construtor String cria um objeto de string, não um literal de string: um objeto que contém o valor do nosso string, mas também todas as propriedades e métodos herdados do construtor.

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

Na maioria das vezes, os objetos resultantes se comportam como os valores usados para defini-los. Por exemplo, mesmo que a definição de um valor numérico usando o construtor new Number resulte em um objeto contendo todos os métodos e propriedades do protótipo Number, é possível usar operadores matemáticos nesses objetos da mesma forma que faria com literais numéricos:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

Muito raramente você precisará usar esses construtores, porque a herança de protótipo integrada do JavaScript significa que eles não oferecem benefícios práticos. A criação de primitivos usando construtores também pode levar a resultados inesperados, porque o resultado é um objeto, não um literal simples:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Isso pode complicar o uso de operadores de comparação rigorosos:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Inserção automática de ponto e vírgula (ASI)

Ao analisar um script, os intérpretes de JavaScript podem usar um recurso chamado inserção automática de ponto e vírgula (ASI, na sigla em inglês) para tentar corrigir instâncias de pontos e vírgulas omitidos. Se o analisador do JavaScript encontrar um token não permitido, ele vai tentar adicionar um ponto e vírgula antes dele para corrigir o possível erro de sintaxe, desde que uma ou mais das seguintes condições sejam verdadeiras:

  • Esse token é separado do token anterior por uma quebra de linha.
  • Esse token é }.
  • O token anterior é ), e o ponto e vírgula inserido seria o ponto e vírgula final de uma instrução do...while.

Para mais informações, consulte as regras de ASI.

Por exemplo, omitir o ponto e vírgula depois das instruções abaixo não causa um erro de sintaxe devido ao ASI:

const myVariable = 2
myVariable + 3
> 5

No entanto, o ASI não pode contabilizar várias instruções na mesma linha. Se você escrever mais de uma instrução na mesma linha, separe-as com ponto e vírgula:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI é uma tentativa de correção de erros, não um tipo de flexibilidade sintática integrada ao JavaScript. Use ponto e vírgula quando apropriado, para não depender dele para produzir um código correto.

Modo restrito

Os padrões que regem como o JavaScript é escrito evoluíram muito além de tudo que foi considerado durante o design inicial da linguagem. Cada nova mudança no comportamento esperado do JavaScript precisa evitar erros em sites mais antigos.

O ES5 resolve alguns problemas de longa data com a semântica do JavaScript sem interromper as implementações existentes introduzindo o "modo restrito", uma maneira de ativar um conjunto mais restritivo de regras de linguagem para um script inteiro ou uma função individual. Para ativar o modo restrito, use o literal de string "use strict", seguido por ponto e vírgula, na primeira linha de um script ou função:

"use strict";
function myFunction() {
  "use strict";
}

O modo estrito impede determinadas ações "não seguras" ou recursos descontinuados, gera erros explícitos em vez de "silenciosos" comuns e proíbe o uso de sintaxes que possam entrar em conflito com futuros recursos de linguagem. Por exemplo, as decisões de design iniciais sobre escopo variável aumentaram as chances dos desenvolvedores "poluirem" por engano o escopo global ao declarar uma variável, independentemente do contexto que a contém, omitindo a palavra-chave var:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

Os ambientes de execução de JavaScript moderno não podem corrigir esse comportamento sem o risco de interromper os sites que dependem dele, por engano ou deliberadamente. Em vez disso, o JavaScript moderno impede isso, permitindo que os desenvolvedores ativem o modo estrito para novos trabalhos e ativando esse modo por padrão somente no contexto de novos recursos de linguagem em que eles não corrompem implementações legadas:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Escreva "use strict" como um literal de string. Um literal de modelo (use strict) não vai funcionar. Também é preciso incluir "use strict" antes de qualquer código executável no contexto pretendido. Caso contrário, ela é ignorada pelo intérprete.

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

Por referência, por valor

Qualquer variável, incluindo propriedades de um objeto, parâmetros de função e elementos em uma matriz, conjunto ou mapa, pode conter um valor primitivo ou um valor de referência.

Quando um valor primitivo é atribuído de uma variável a outra, o mecanismo JavaScript cria uma cópia desse valor e a atribui à variável.

Quando você atribui um objeto (instâncias de classe, matrizes e funções) a uma variável, em vez de criar uma nova cópia desse objeto, a variável contém uma referência à posição armazenada do objeto na memória. Por isso, alterar um objeto referenciado por uma variável modifica o objeto que está sendo referenciado, não apenas um valor contido por essa variável. Por exemplo, se você inicializar uma nova variável com uma variável que contém uma referência de objeto e usar a nova variável para adicionar uma propriedade a esse objeto, a propriedade e o valor dela serão adicionados ao objeto original:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

Isso é importante não apenas para alterar objetos, mas também para realizar comparações estritas, porque a igualdade estrita entre objetos exige que as duas variáveis façam referência ao mesmo objeto para avaliar como true. Eles não podem referenciar objetos diferentes, mesmo que sejam estruturalmente idênticos:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Alocação de memória

O JavaScript usa o gerenciamento automático de memória. Isso significa que a memória não precisa ser alocada ou desalocada explicitamente durante o desenvolvimento. Embora os detalhes das abordagens dos mecanismos JavaScript para o gerenciamento de memória estejam além do escopo deste módulo, entender como a memória é alocada fornece um contexto útil para trabalhar com valores de referência.

Há duas "áreas" na memória: a "pilha" e a "heap". A pilha armazena dados estáticos (valores primitivos e referências a objetos) porque a quantidade fixa de espaço necessário para armazenar esses dados pode ser alocada antes da execução do script. O heap armazena objetos que precisam de espaço alocado dinamicamente porque o tamanho pode mudar durante a execução. A memória é liberada por um processo chamado "coleta de lixo", que remove objetos sem referências da memória.

A linha de execução principal

Basicamente, o JavaScript é uma linguagem de linha de execução única com um modelo de execução "síncrona". Isso significa que ele pode executar apenas uma tarefa por vez. Esse contexto de execução sequencial é chamado de linha de execução principal.

A linha de execução principal é compartilhada por outras tarefas do navegador, como analisar HTML, renderizar e renderizar novamente partes da página, executar animações CSS e processar interações do usuário que vão da simples (como destacar texto) ao complexa (como interagir com elementos de formulário). Os fornecedores de navegadores descobriram maneiras de otimizar as tarefas realizadas pela linha de execução principal, mas scripts mais complexos ainda podem usar muito dos recursos da linha de execução principal e afetar o desempenho geral da página.

Algumas tarefas podem ser executadas em linhas de execução em segundo plano chamadas de Web Workers, com algumas limitações:

  • As linhas de execução de worker só podem atuar em arquivos JavaScript independentes.
  • Eles vão reduzir significativamente o acesso à janela e à interface do navegador.
  • Há uma limitação na forma como eles se comunicam com a linha de execução principal.

Essas limitações as tornam ideais para tarefas focadas e que consomem muitos recursos, que poderiam ocupar a linha de execução principal.

A pilha de chamadas

A estrutura de dados usada para gerenciar "contextos de execução" (o código que está sendo executado ativamente) é uma lista chamada pilha de chamadas (geralmente, apenas "a pilha"). Quando um script é executado pela primeira vez, o intérprete de JavaScript cria um "contexto de execução global" e o envia para a pilha de chamadas, com instruções dentro desse contexto global executadas uma por vez, de cima para baixo. Quando o intérprete encontra uma chamada de função durante a execução do contexto global, ele envia um "contexto de execução da função" para a parte de cima da pilha, pausa o contexto de execução global e executa o contexto de execução da função.

Sempre que uma função é chamada, o contexto de execução da função é enviado para a parte superior da pilha, logo acima do contexto de execução atual. A pilha de chamadas opera "primeiro a entrar, primeiro a sair", o que significa que a chamada de função mais recente, que fica no topo da pilha, é executada e continua até ser resolvida. Quando essa função é concluída, o intérprete a remove da pilha de chamadas, e o contexto de execução que contém essa chamada de função se torna o item mais alto da pilha novamente e retoma a execução.

Esses contextos de execução capturam todos os valores necessários para a execução. Eles também estabelecem as variáveis e funções disponíveis no escopo da função com base no contexto pai e determinam e definem o valor da palavra-chave this no contexto da função.

Loop de eventos e fila de callback

Essa execução sequencial significa que tarefas assíncronas que incluem funções de callback, como buscar dados de um servidor, responder à interação do usuário ou aguardar timers definidos com setTimeout ou setInterval, bloqueiam a linha de execução principal até que a tarefa seja concluída ou interrompem inesperadamente o contexto de execução atual no momento em que o contexto de execução da função de callback é adicionado à pilha. Para resolver isso, o JavaScript gerencia tarefas assíncronas usando um "modelo de simultaneidade" orientado a eventos, composto pelo "loop de eventos" e a "fila de callback" (às vezes conhecida como "fila de mensagens").

Quando uma tarefa assíncrona é executada na linha de execução principal, o contexto de execução da função de callback é colocado na fila de callback, e não na pilha de chamadas. O loop de eventos é um padrão às vezes chamado de reator, que pesquisa continuamente o status da pilha de chamadas e da fila de callback. Se houver tarefas na fila de callback e a repetição de eventos determinar que a pilha de chamadas está vazia, as tarefas da fila de callback serão enviadas para a pilha, uma de cada vez, para serem executadas.