A resposta envolve a implementação interna do motor V8, especialmente os conceitos de propriedades indexadas por arrays, propriedades nomeadas, propriedades in-object, classes ocultas e arrays de descritores.
Exemplo prático
Considere este trecho de código:
function testV8() {
this[100] = 'test-100'
this[1] = 'test-1'
this["D"] = 'foo-D'
this["B"] = 'foo-B'
this[50] = 'test-50'
this[9] = 'test-9'
this[8] = 'test-8'
this[3] = 'test-3'
this[5] = 'test-5'
this["4"] = 'test-4'
this["A"] = 'foo-A'
this["C"] = 'foo-C'
this[4.5] = "foo-4.5"
}
const testObj = new testV8()
for (const key in testObj) {
console.log(`key:${key}, value:${testObj[key]}`)
}
O resultado da execução mostra que a ordem das chaves é consistente entre as execuções, não havendo aleatoriedade. A análise revela duas regras importantes:
- Chaves numéricas são listadas em ordem crescente.
- Chaves não numéricas seguem a ordem de definição.
Tipos de propriedades no V8
No V8, existem dois tipos principais de armazenamento para propriedades:
- Propriedades indexadas por arrays: Armazenadas em uma estrutura linear com base em índices numéricos.
- Propriedades nomeadas: Armazenadas em outra estrutura linear com base em nomes de chave.
Cada tipo é mantido separadamente em estruturas distintas chamadsa properties e elements. No entanto, quando há menos de 10 propriedades nomeadas, estas são armazenadas diretamente no objeto, conhecidas como propriedades in-object, evitando uma etapa extra de acesso.
Snapshot no Chrome DevTools
Para inspecionar o comportamento real, utilize o DevTools do Chrome:
- Acesse a aba Memory.
- Clique em Take snapshot.
- Filtre pelo nome da função
testV8.
Isso permite observar como as propriedades são realmente armazenadas.
Propriedades in-object vs. normais
Quando o número de propriedades nomeadas ultrapassa dez, elas são movidas para a estrutura properties. Essas são chamadas de propriedades normais e exigem uma etapa adicional para acesso.
Veja um exemplo:
function Foo(properties, elements) {
// Adiciona propriedades indexadas
for (let i = 0; i < elements; i++) {
this[i] = `element${i}`
}
// Adiciona propriedades nomeadas
for (let i = 0; i < properties; i++) {
const prop = `property${i}`
this[prop] = prop
}
}
const foo = new Foo(12, 12)
Classes Ocultas (Hidden Classes)
Em linguagens estáticas, o layout de objetos é fixo após a criação. Em JavaScript, isso não ocorre, então o V8 usa classes ocultas para otimizar o acesso às propriedades.
Cada objeto possui uma classe oculta que contém metadados sobre suas propriedades. Quando uma propriedade é adicionada ou removida, uma nova classe oculta é criada e vinculada à anterior via ponteiro back_pointer.
Estrutura de uma Classe Oculta
Uma classe oculta contém:
- Informações sobre o número de propriedades nomeadas.
- Ponteiro para um array de descritores.
Os arrays de descritores guardam informações como nomes e localizações das propriedades nomeadas. Propriedades indexadas por array não são incluídas aqui.
Árvore de transição
Quando novas propriedades são adicionadas, o V8 cria novas classes ocultas e conecta-as formando uma árvore de transição, permitindo identificar objetos com estruturas semelhantes.
Propriedades rápidas e lentas
Propriedades armazenadas em estruturas lineares são chamadas de rápidas, pois permitem acesso direto com complexidade O(1). Por outro lado, quando há muitas propriedades, o V8 pode optar por usar uma estrutura de dicionário (dictionary), resultando em propriedades lentas.
Essas propriedades lentas não utilizam o array de descritores e armazenam seus dados em uma estrutura de dicionário interna, evitando atualizações na classe oculta, mas reduzindo a eficiência de cache inline.
Resumo
- Propriedades indexadas são armazenadas em estruturas lineares.
- Propriedades nomeadas até dez são armazenadas diretamente no objeto.
- Classes ocultas ajudam a manter o desempenho ao rastrear mudanças nas propriedades.
- Propriedades rápidas oferecem acesso rápido, enquanto propriedades lentas usam dicionários.