Entendendo a especificação ECMAScript, parte 1
Neste artigo, analisamos uma função simples na especificação e tentamos entender a notação. Vamos lá!
Prefácio
Mesmo que você saiba JavaScript, ler sua especificação de linguagem, Especificação da Linguagem ECMAScript, ou apenas a especificação ECMAScript, pode ser bastante assustador. Pelo menos foi assim que me senti quando comecei a lê-la pela primeira vez.
Vamos começar com um exemplo concreto e percorrer a especificação para entendê-lo. O código a seguir demonstra o uso de Object.prototype.hasOwnProperty:
const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
No exemplo, o não possui uma propriedade chamada hasOwnProperty, então subimos na cadeia de protótipos para procurá-la. Nós a encontramos no protótipo de o, que é Object.prototype.
Para descrever como Object.prototype.hasOwnProperty funciona, a especificação usa descrições semelhantes a pseudocódigos:
Object.prototype.hasOwnProperty(V)Quando o método
hasOwnPropertyé chamado com o argumentoV, os seguintes passos são realizados:
- Deixe
Pser? ToPropertyKey(V).- Deixe
Oser? ToObject(this value).- Retorne
? HasOwnProperty(O, P).
…e…
A operação abstrata
HasOwnPropertyé usada para determinar se um objeto possui uma propriedade própria com a chave de propriedade especificada. Um valor booleano é retornado. A operação é chamada com os argumentosOeP, ondeOé o objeto ePé a chave de propriedade. Essa operação abstrata realiza os seguintes passos:
- Afirme:
Type(O)éObject.- Afirme:
IsPropertyKey(P)étrue.- Deixe
descser? O.[[GetOwnProperty]](P).- Se
descforundefined, retornefalse.- Retorne
true.
Mas o que é uma “operação abstrata”? O que são as coisas dentro de [[ ]]? Por que há um ? antes de uma função? O que as afirmações significam?
Vamos descobrir!
Tipos de linguagem e tipos de especificação
Vamos começar com algo que parece familiar. A especificação usa valores como undefined, true e false, que já conhecemos do JavaScript. Todos eles são valores de linguagem, valores de tipos de linguagem que a especificação também define.
A especificação também usa valores de linguagem internamente, por exemplo, um tipo de dado interno pode conter um campo cujos valores possíveis são true e false. Em contraste, os motores JavaScript geralmente não usam valores de linguagem internamente. Por exemplo, se o motor JavaScript for escrito em C++, ele tipicamente usará o true e false do C++ (e não suas representações internas do true e false do JavaScript).
Além dos tipos de linguagem, a especificação também usa tipos de especificação, que são tipos que ocorrem apenas na especificação, mas não na linguagem JavaScript. O motor JavaScript não precisa (mas é livre para) implementá-los. Neste post do blog, conheceremos o tipo de especificação Record (e seu subtipo Completion Record).
Operações abstratas
Operações abstratas são funções definidas na especificação ECMAScript; elas são definidas para o propósito de escrever a especificação de maneira concisa. Um motor JavaScript não precisa implementá-las como funções separadas dentro do motor. Elas não podem ser chamadas diretamente a partir do JavaScript.
Slots internos e métodos internos
Slots internos e métodos internos usam nomes entre [[ ]].
Slots internos são membros de dados de um objeto JavaScript ou de um tipo de especificação. Eles são usados para armazenar o estado do objeto. Métodos internos são funções membros de um objeto JavaScript.
Por exemplo, todo objeto JavaScript possui um slot interno [[Prototype]] e um método interno [[GetOwnProperty]].
Slots internos e métodos não são acessíveis a partir do JavaScript. Por exemplo, você não pode acessar o.[[Prototype]] ou chamar o.[[GetOwnProperty]](). Um motor JavaScript pode implementá-los para seu próprio uso interno, mas não é obrigatório.
Às vezes, métodos internos delegam para operações abstratas de nomes semelhantes, como no caso de objetos ordinários' [[GetOwnProperty]]:
Quando o método interno
[[GetOwnProperty]]deOé chamado com a chave de propriedadeP, os seguintes passos são realizados:
- Retorne
! OrdinaryGetOwnProperty(O, P).
(Vamos descobrir o que o ponto de exclamação significa no próximo capítulo.)
OrdinaryGetOwnProperty não é um método interno, já que não está associado a nenhum objeto; em vez disso, o objeto no qual opera é passado como um parâmetro.
OrdinaryGetOwnProperty é chamado de “ordinário” porque opera em objetos ordinários. Objetos ECMAScript podem ser ordinários ou exóticos. Objetos ordinários devem ter o comportamento padrão para um conjunto de métodos chamados métodos internos essenciais. Se um objeto se desviar do comportamento padrão, ele é exótico.
O objeto exótico mais conhecido é o Array, uma vez que sua propriedade length se comporta de uma maneira não padrão: configurar a propriedade length pode remover elementos do Array.
Métodos internos essenciais são os métodos listados aqui.
Registros de Conclusão
E quanto aos pontos de interrogação e de exclamação? Para entendê-los, precisamos analisar Registros de Conclusão!
O Registro de Conclusão é um tipo de especificação (definido apenas para propósitos de especificação). Um mecanismo JavaScript não precisa ter um tipo de dado interno correspondente.
Um Registro de Conclusão é um “registro” — um tipo de dado que possui um conjunto fixo de campos nomeados. Um Registro de Conclusão possui três campos:
| Nome | Descrição |
|---|---|
[[Type]] | Um dos seguintes: normal, break, continue, return ou throw. Todos os outros tipos, exceto normal, são conclusões abruptas. |
[[Value]] | O valor produzido quando a conclusão ocorreu, por exemplo, o valor de retorno de uma função ou a exceção (se uma foi lançada). |
[[Target]] | Usado para transferências de controle direcionadas (não relevante para este post). |
Toda operação abstrata implicitamente retorna um Registro de Conclusão. Mesmo que pareça que uma operação abstrata retornaria um tipo simples, como Booleano, ele é implicitamente envolvido em um Registro de Conclusão com o tipo normal (veja Valores de Conclusão Implícitos).
Nota 1: A especificação não é totalmente consistente nesse aspecto; há algumas funções auxiliares que retornam valores puros e cujos valores de retorno são usados como estão, sem extrair o valor do Registro de Conclusão. Isso geralmente é claro pelo contexto.
Nota 2: Os editores da especificação estão analisando maneiras de tornar o manuseio do Registro de Conclusão mais explícito.
Se um algoritmo lançar uma exceção, isso significa retornar um Registro de Conclusão com [[Type]] throw cujo [[Value]] é o objeto de exceção. Ignoraremos os tipos break, continue e return por enquanto.
ReturnIfAbrupt(argument) significa realizar os seguintes passos:
- Se
argumentfor abrupto, retorneargument- Defina
argumentcomoargument.[[Value]].
Ou seja, inspecionamos um Registro de Conclusão; se for uma conclusão abrupta, retornamos imediatamente. Caso contrário, extraímos o valor do Registro de Conclusão.
ReturnIfAbrupt pode parecer uma chamada de função, mas não é. Ele faz com que a função onde ReturnIfAbrupt() ocorre retorne, e não a própria função ReturnIfAbrupt. Comporta-se mais como uma macro em linguagens do tipo C.
ReturnIfAbrupt pode ser usado assim:
- Deixe
objserFoo(). (objé um Registro de Conclusão.)ReturnIfAbrupt(obj).Bar(obj). (Se ainda estamos aqui,objé o valor extraído do Registro de Conclusão.)
E agora o ponto de interrogação entra em cena: ? Foo() é equivalente a ReturnIfAbrupt(Foo()). Usar uma abreviação é prático: não precisamos escrever o código de tratamento de erros explicitamente a cada vez.
Da mesma forma, Deixe val ser ! Foo() é equivalente a:
- Deixe
valserFoo().- Afirme:
valnão é uma conclusão abrupta.- Defina
valcomoval.[[Value]].
Com esse conhecimento, podemos reescrever Object.prototype.hasOwnProperty assim:
Object.prototype.hasOwnProperty(V)
- Deixe
PserToPropertyKey(V).- Se
Pfor uma interrupção abrupta, retorneP- Configure
PparaP.[[Value]]- Deixe
OserToObject(this value).- Se
Ofor uma interrupção abrupta, retorneO- Configure
OparaO.[[Value]]- Deixe
tempserHasOwnProperty(O, P).- Se
tempfor uma interrupção abrupta, retornetemp- Configure
tempparatemp.[[Value]]- Retorne
NormalCompletion(temp)
…e podemos reescrever HasOwnProperty assim:
HasOwnProperty(O, P)
- Afirme:
Type(O)éObject.- Afirme:
IsPropertyKey(P)étrue.- Deixe
descserO.[[GetOwnProperty]](P).- Se
descfor uma interrupção abrupta, retornedesc- Configure
descparadesc.[[Value]]- Se
descforundefined, retorneNormalCompletion(false).- Retorne
NormalCompletion(true).
Também podemos reescrever o método interno [[GetOwnProperty]] sem o ponto de exclamação:
O.[[GetOwnProperty]]
- Deixe
tempserOrdinaryGetOwnProperty(O, P).- Afirme:
tempnão é uma interrupção abrupta.- Configure
tempparatemp.[[Value]].- Retorne
NormalCompletion(temp).
Aqui assumimos que temp é uma nova variável temporária que não colide com mais nada.
Também usamos o conhecimento de que, quando uma declaração de retorno retorna algo diferente de um Registro de Conclusão, ele é implicitamente envolto em um NormalCompletion.
Desvio lateral: Return ? Foo()
A especificação usa a notação Return ? Foo() — por que o ponto de interrogação?
Return ? Foo() expande-se para:
- Deixe
tempserFoo().- Se
tempfor uma interrupção abrupta, retornetemp.- Configure
tempparatemp.[[Value]].- Retorne
NormalCompletion(temp).
O que é o mesmo que Return Foo(); ele se comporta da mesma maneira para conclusões abruptas e normais.
Return ? Foo() é usado apenas por razões editoriais, para deixar mais explícito que Foo retorna um Registro de Conclusão.
Afirmações
As afirmações na especificação garantem condições invariáveis dos algoritmos. Elas são adicionadas para clareza, mas não adicionam nenhum requisito à implementação — a implementação não precisa verificá-las.
Avançando
As operações abstratas delegam a outras operações abstratas (veja a imagem abaixo), mas com base neste post do blog devemos ser capazes de descobrir o que elas fazem. Encontraremos Property Descriptors, que é apenas outro tipo de especificação.
Resumo
Lemos um método simples — Object.prototype.hasOwnProperty — e operações abstratas que ele invoca. Familiarizamo-nos com os atalhos ? e ! relacionados ao tratamento de erros. Encontramos tipos de linguagem, tipos de especificação, slots internos e métodos internos.
Links úteis
Como Ler a Especificação ECMAScript: um tutorial que cobre grande parte do material abordado neste post, de um ângulo ligeiramente diferente.