Apêndice

Herança prototípica

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

Por exemplo, um literal de string não tem métodos próprios, mas você pode 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 prototípica, 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 {  }

É possível criar primitivos usando esses construtores, em vez de apenas defini-los pelo valor. Por exemplo, o uso do construtor String cria um objeto de string, não um literal de string: um objeto que não contém apenas o valor da string, mas 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 dos casos, os objetos resultantes se comportam como os valores que usamos para defini-los. Por exemplo, embora a definição de um valor numérico usando o construtor new Number resulte em um objeto que contém todos os métodos e propriedades do protótipo Number, é possível usar operadores matemáticos nesses objetos da mesma forma que em 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

Você raramente precisará usar esses construtores, porque a herança prototípica integrada do JavaScript significa que eles não oferecem nenhum benefício prático. 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 estritos:

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 ponto e vírgula omitidos. Se o analisador JavaScript encontrar um token que não é permitido, ele vai tentar adicionar um ponto e vírgula antes desse token para corrigir o possível erro de sintaxe, desde que uma ou mais das seguintes condições seja verdadeira:

  • Esse token é separado do anterior por um retorno de carro.
  • Esse token é }.
  • O token anterior é ), e o ponto e vírgula inserido seria o ponto e vírgula final de uma instrução dowhile.

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

Por exemplo, omitir pontos e vírgulas após as instruções a seguir não causará um erro de sintaxe devido ao ASI:

const myVariable = 2
myVariable + 3
> 5

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

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

const myVariable = 2; myVariable + 3;
> 5

A ASI é uma tentativa de correção de erro, não um tipo de flexibilidade sintática integrada ao JavaScript. Use ponto e vírgula sempre que necessário para não depender dele para produzir o código correto.

Modo restrito

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

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

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

O modo restrito impede determinadas ações "inseguras" ou recursos descontinuados, gera erros explícitos em vez de erros "silenciosos" comuns e proíbe o uso de sintaxe que possa colidir com recursos futuros da linguagem. Por exemplo, as primeiras decisões de design em relação ao escopo da variável aumentaram a probabilidade de os desenvolvedores "poluírem" o escopo global por engano ao declarar uma variável, independentemente do contexto que a contém, omitindo a palavra-chave var:

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

mySloppyGlobal;
> true

Os runtimes modernos do JavaScript não podem corrigir esse comportamento sem o risco de quebrar qualquer site que dependa dele, seja por engano ou deliberadamente. Em vez disso, o JavaScript moderno evita isso permitindo que os desenvolvedores ativem o modo rígido para novos trabalhos e ativando o modo estrito por padrão apenas no contexto de novos recursos de linguagem em que eles não quebram implementações legadas:

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

É preciso escrever "use strict" como um literal de string. Um template literal (use strict) não vai funcionar. Você também precisa incluir "use strict" antes de qualquer código executável no contexto pretendido. Caso contrário, o intérprete vai ignorá-la.

(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 primário 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, mudar um objeto referenciado por uma variável muda 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 contenha 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 rígidas, porque a igualdade estrita entre objetos exige que ambas as variáveis se refiram ao mesmo objeto para avaliar true. Eles não podem fazer referência a 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, o que 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 fora 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 o "monte". A pilha armazena dados estáticos, como valores primitivos e referências a objetos, porque a quantidade fixa de espaço necessária para armazenar esses dados pode ser alocada antes da execução do script. A pilha armazena objetos que precisam de espaço alocado dinamicamente porque o tamanho deles 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

O JavaScript é uma linguagem com linha de execução única com um modelo de execução "síncrono", o que 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 variam de simples (como destacar texto) a complexas (como interagir com elementos de formulário). Os fornecedores de navegadores encontraram maneiras de otimizar as tarefas executadas pela linha de execução principal, mas scripts mais complexos ainda podem usar muitos recursos da linha de execução principal e afetar a performance geral da página.

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

  • As linhas de execução do worker só podem atuar em arquivos JavaScript independentes.
  • Eles têm acesso limitado ou nenhum acesso à janela do navegador e à interface.
  • Elas são limitadas em como podem se comunicar com a linha de execução principal.

Essas limitações tornam os threads ideais para tarefas focadas e que exigem muitos recursos e que podem ocupar a linha de execução principal.

A pilha de chamadas

A estrutura de dados usada para gerenciar "contextos de execução", ou seja, o código que está ativamente sendo executado, é uma lista chamada de pilha de chamadas (frequentemente chamada apenas de "pilha"). Quando um script é executado pela primeira vez, o interpretador 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 interpretador encontra uma chamada de função ao executar o contexto global, ele envia um "contexto de execução de função" para essa chamada na parte de cima da pilha, pausa o contexto de execução global e executa o contexto de execução da função.

Toda vez que uma função é chamada, o contexto de execução da função para essa chamada é enviado para a parte de cima da pilha, logo acima do contexto de execução atual. A pilha de chamadas opera com base no princípio "último a entrar, primeiro a sair", o que significa que a chamada de função mais recente, que está mais alta na pilha, é executada e continua até ser resolvida. Quando essa função é concluída, o interpretador 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 na pilha novamente e retoma a execução.

Esses contextos de execução capturam todos os valores necessários para a execução. Elas 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.

O loop de eventos e a fila de callbacks

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, bloqueariam a linha de execução principal até que a tarefa fosse concluída ou interromperiam inesperadamente o contexto de execução atual no momento em que o contexto de execução da função de callback fosse adicionado à pilha. Para resolver isso, o JavaScript gerencia tarefas assíncronas usando um "modelo de concorrência" orientado a eventos composto pelo "loop de eventos" e a "fila de callbacks" (às vezes chamada de "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 callbacks, e não na parte de cima da pilha de chamadas. O loop de eventos é um padrão que às vezes é chamado de reator, que continuamente verifica o status da pilha de chamadas e da fila de callbacks. Se houver tarefas na fila de callbacks e o loop de eventos determinar que a pilha de chamadas está vazia, as tarefas da fila de callbacks serão enviadas para a pilha uma por vez para serem executadas.