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
P
ser? ToPropertyKey(V)
.- Deixe
O
ser? 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 argumentosO
eP
, 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
desc
ser? O.[[GetOwnProperty]](P)
.- Se
desc
forundefined
, 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
argument
for abrupto, retorneargument
- Defina
argument
comoargument.[[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
obj
serFoo()
. (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
val
serFoo()
.- Afirme:
val
não é uma conclusão abrupta.- Defina
val
comoval.[[Value]]
.
Com esse conhecimento, podemos reescrever Object.prototype.hasOwnProperty
assim:
Object.prototype.hasOwnProperty(V)
- Deixe
P
serToPropertyKey(V)
.- Se
P
for uma interrupção abrupta, retorneP
- Configure
P
paraP.[[Value]]
- Deixe
O
serToObject(this value)
.- Se
O
for uma interrupção abrupta, retorneO
- Configure
O
paraO.[[Value]]
- Deixe
temp
serHasOwnProperty(O, P)
.- Se
temp
for uma interrupção abrupta, retornetemp
- Configure
temp
paratemp.[[Value]]
- Retorne
NormalCompletion(temp)
…e podemos reescrever HasOwnProperty
assim:
HasOwnProperty(O, P)
- Afirme:
Type(O)
éObject
.- Afirme:
IsPropertyKey(P)
étrue
.- Deixe
desc
serO.[[GetOwnProperty]](P)
.- Se
desc
for uma interrupção abrupta, retornedesc
- Configure
desc
paradesc.[[Value]]
- Se
desc
forundefined
, retorneNormalCompletion(false)
.- Retorne
NormalCompletion(true)
.
Também podemos reescrever o método interno [[GetOwnProperty]]
sem o ponto de exclamação:
O.[[GetOwnProperty]]
- Deixe
temp
serOrdinaryGetOwnProperty(O, P)
.- Afirme:
temp
não é uma interrupção abrupta.- Configure
temp
paratemp.[[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
temp
serFoo()
.- Se
temp
for uma interrupção abrupta, retornetemp
.- Configure
temp
paratemp.[[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.