Todos nós, há muito tempo, queremos um encapsulamento normal em JS, que pode ser usado sem gestos desnecessários. Também queremos construções convenientes para declarar propriedades de classe. E, finalmente, queremos que todos esses recursos no idioma apareçam de forma a não quebrar os aplicativos existentes.
Parece que aqui está a felicidade: proposta de campos de classe que, após muitos anos de tormento do comitê tc39, ainda chegou ao stage 3
e chegou a ser implementada no chrome .
Honestamente, eu realmente gostaria de escrever um artigo sobre por que você deve usar o novo recurso de idioma e como fazê-lo, mas, infelizmente, o artigo não será sobre isso.
Descrição da falta atual
Não repetirei a descrição original , as perguntas frequentes e as alterações nas especificações aqui , mas apenas descreverei brevemente os pontos principais.
Campos de classe
Declarando campos e usando-os dentro de uma classe:
class A { x = 1; method() { console.log(this.x); } }
Acesso a campos fora da classe:
const a = new A(); console.log(ax);
Tudo parecia óbvio e, por muitos anos, usamos essa sintaxe usando Babel e TypeScript .
Só há uma nuance. Essa nova sintaxe usa [[Define]]
, e não [[Set]]
semântica com a qual vivemos todo esse tempo.
Na prática, isso significa que o código acima não é igual a isso:
class A { constructor() { this.x = 1; } method() { console.log(this.x); } }
Mas, na verdade, é equivalente a isso:
class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } }
E, embora, para o exemplo acima, ambas as abordagens façam essencialmente a mesma coisa, essa é uma diferença MUITO SÉRIA e aqui está o porquê:
Digamos que temos uma classe pai como esta:
class A { x = 1; method() { console.log(this.x); } }
Com base nisso, criamos outro:
class B extends A { x = 2; }
E eles usaram:
const b = new B(); b.method();
Então, por algum motivo, a classe A
foi alterada de uma maneira aparentemente compatível com versões anteriores:
class A { _x = 1;
E para a semântica [[Set]]
, essa é realmente uma alteração compatível com versões anteriores, mas não para [[Define]]
. Agora a chamada para b.method()
será b.method()
no console 1
vez de 2
. E isso acontecerá porque Object.defineProperty
redefine o descritor de propriedades e, consequentemente, getter / setter da classe A
não será chamado. De fato, na classe filho, ocultamos a propriedade x
do pai, semelhante à maneira como podemos fazer isso no escopo lexical:
const x = 1; { const x = 2; }
É verdade que, neste caso, o ponteiro com suas regras no-shadow
no-shadowed-variable
no-shadow
/ no-shadow
nos salvará, mas a probabilidade de alguém criar um no-shadowed-class-field
tende a zero.
A propósito, serei grato pelo termo russo mais bem-sucedido de shadowed
.
Apesar de tudo isso, não sou um oponente inaceitável da nova semântica (embora prefira outra), porque ela tem seus próprios aspectos positivos. Infelizmente, essas vantagens não superam as menos importantes - usamos a semântica [[Set]]
há muitos anos, porque é usada no babel6
e no TypeScript
por padrão.
É verdade que vale a pena notar que no babel7
valor padrão foi alterado .
Discussões mais originais sobre esse tópico podem ser lidas aqui e aqui .
Campos privados
E agora vamos passar para a parte mais controversa deste. Tão controverso que:
- apesar de já estar implementado no Chrome Canary e de os campos públicos já estarem ativados por padrão, os campos privados ainda estão atrás da bandeira;
- apesar do processo inicial de campos particulares ter sido mesclado com o atual, ainda estão sendo criados pedidos para a separação desses dois recursos (por exemplo, um , dois , três e quatro );
- até alguns membros do comitê (como Allen Wirfs-Brock e Kevin Smith ) se manifestam e oferecem alternativas , apesar do estágio3 ;
- isso perdeu um recorde de número de edições - 129 no repositório atual + 96 no original , contra 126 no BigInt , e o detentor do registro tem comentários negativos ;
- Eu tive que criar um segmento separado com uma tentativa de resumir de alguma forma todas as reivindicações contra ele;
- Eu tive que escrever uma FAQ separada que cubra esta parte
no entanto, devido a uma argumentação bastante fraca, essas discussões apareceram ( um , dois )
- Pessoalmente, passei todo o meu tempo livre (e às vezes trabalhando) por um longo período de tempo para descobrir tudo e até encontrar uma explicação de por que ele era assim ou oferecer uma alternativa adequada ;
- no final, decidi escrever este artigo de revisão.
Os campos particulares são declarados da seguinte maneira:
class A { #priv; }
E o acesso a eles é o seguinte:
class A { #priv = 1; method() { console.log(this.#priv); } }
Nem vou levantar o tópico de que o modelo mental por trás disso não é muito intuitivo ( this.#priv !== this['#priv']
), não usa as palavras private
/ protected
já reservadas (o que necessariamente causará dor adicional para desenvolvedores de TypeScript), não está claro como estendê-lo para outros modificadores de acesso , e a sintaxe em si não é muito bonita. Embora tudo isso tenha sido a razão original que me levou a um estudo mais profundo e à participação nas discussões.
Tudo isso se refere à sintaxe, onde as preferências estéticas subjetivas são muito fortes. E alguém poderia viver com isso e se acostumar com o tempo. Se não fosse por uma coisa: há um problema muito significativo de semântica ...
Semântica WeakMap
Vamos dar uma olhada no que está por trás da proposta existente. Podemos reescrever o exemplo acima com encapsulamento e sem usar a nova sintaxe, mas preservando a semântica da atual:
const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } }
A propósito, com base nessa semântica, um dos membros do comitê construiu uma pequena biblioteca de utilitários que permite o uso do estado privado agora, para mostrar que essa funcionalidade é superestimada pelo comitê. O código formatado leva apenas 27 linhas.
Em geral, tudo é muito bom, temos hard-private
, que não podem ser obtidos / interceptados / rastreados de código externo de nenhuma maneira e, ao mesmo tempo, podemos acessar os campos privados de outra instância da mesma classe, por exemplo:
isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; }
Bem, isso é muito conveniente, exceto pelo fato de que essa semântica, além do próprio encapsulamento, também inclui brand-checking
(você não pode pesquisar no Google o que é - é improvável que encontre informações relevantes).
brand-checking
é o oposto de duck-typing
de duck-typing
, no sentido de que não verifica a interface pública do objeto, mas o fato de o objeto ter sido construído usando código confiável.
Essa verificação, de fato, tem um certo escopo - está principalmente associada à segurança de chamar código não confiável em um único espaço de endereço com um confiável e a capacidade de trocar objetos diretamente sem serialização.
Embora alguns engenheiros considerem isso uma parte necessária do encapsulamento adequado.
Apesar de ser uma oportunidade bastante curiosa, que está intimamente relacionada ao padrão
(descrição curta e mais longa ), Realms
propaganda e trabalho científico no campo da Ciência da Computação, no qual Mark Samuel Miller está envolvido (ele também é membro do comitê), na minha experiência , na prática da maioria dos desenvolvedores, isso quase nunca ocorre.
Aliás, ainda me deparei com uma membrana (embora não soubesse o que era na época) quando reescrevi vm2 para atender às minhas necessidades.
Problema de brand-checking
Como mencionado anteriormente, brand-checking
é o oposto da duck-typing
de duck-typing
. Na prática, isso significa que ter este código:
const brands = new WeakMap(); class A { constructor() { brands.set(this, {}); } method() { return 1; } brandCheckedMethod() { if (!brands.has(this)) throw 'Brand-check failed'; console.log(this.method()); } }
brandCheckedMethod
só pode ser chamado com uma instância da classe A
e A
mesmo que o destino seja um objeto que preserve os invariantes dessa classe, esse método lançará uma exceção:
const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method();
Obviamente, este exemplo é bastante sintético e o uso de duckTypedObj
como esse duckTypedObj
duvidoso, até pensarmos em Proxy
.
Um dos cenários de uso de proxy muito importantes é a metaprogramação. Para que o proxy faça todo o trabalho útil necessário, os métodos dos objetos quebrados usando um proxy devem ser executados no contexto do proxy, e não no contexto do destino, ou seja:
const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); doSomethingUseful('get', retval, target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } });
Chame proxy.method();
fará um trabalho útil declarado no proxy e retornará 1
, enquanto chama proxy.brandCheckedMethod();
em vez de fazer um trabalho útil duas vezes a partir do proxy, lançará uma exceção, porque a !== proxy
, que significa que brand-check
não passou.
Sim, podemos executar métodos / funções no contexto de um destino real, não um proxy, e para alguns cenários isso é suficiente (por exemplo, para implementar o padrão
), mas não é suficiente para todos os casos (por exemplo, para implementar propriedades reativas: o MobX 5 já usa um proxy para isso, o Vue.js e o Aurelia estão testando essa abordagem em versões futuras).
Em geral, desde que brand-check
precise ser feita explicitamente, isso não é um problema - o desenvolvedor precisa conscientemente decidir qual trade-off ele faz e se precisa, além disso, no caso de uma verificação explícita da brand-check
você pode implementá-la de tal maneira que o erro não seria gerado em proxies confiáveis.
Infelizmente, o atual nos privou dessa flexibilidade:
class A { #priv; method() { this.#priv;
Esse method
sempre lançará uma exceção se não for chamado no contexto de um objeto construído usando o construtor A
E a pior parte é que brand-check
está implícita aqui e é misturada com outra funcionalidade - encapsulamento.
Embora o
quase necessário para qualquer código, brand-check
tem um escopo bastante restrito. E combiná-los em uma sintaxe levará ao fato de que muitas brand-check
não intencionais da brand-check
aparecem no código do usuário, quando o desenvolvedor pretendia apenas ocultar os detalhes da implementação.
E o slogan usado para promover isso foi # is the new _
apenas exacerbando a situação.
Você também pode ler uma discussão detalhada de como um prozal existente quebra um proxy . Um dos desenvolvedores e autores do Aurelia, Vue.js, falou na discussão .
Além disso, meu comentário , que descreve com mais detalhes a diferença entre diferentes cenários de proxy, pode parecer interessante para alguém. Como um todo, toda a discussão sobre a conexão de campos e membranas particulares .
Alternativas
Todas essas discussões fariam pouco sentido se não houvesse alternativas. Infelizmente, nem uma única alternativa chegou ao estágio1 e, como resultado, nem sequer teve a chance de ser elaborada o suficiente. No entanto, listarei aqui alternativas que de alguma forma resolvem os problemas descritos acima.
- Symbol.private - um prozazil alternativo, um dos membros do comitê.
- Resolve todos os problemas acima (embora possa ter seus próprios, mas, devido à falta de trabalho ativo, é difícil encontrá-los)
- foi novamente revertida na última reunião do comitê devido à falta de uma
brand-check
integrada, problemas com o padrão de membrana (embora isso + isso ofereça uma solução adequada) e a falta de sintaxe conveniente - sintaxe conveniente pode ser construída sobre a real, como mostrei aqui e aqui
- Classes 1.1 - posozal anterior do mesmo autor
- Usando private como um objeto
Em vez de uma conclusão
Pelo tom do artigo, pode parecer que eu condeno o comitê - não é assim. Parece-me apenas que, ao longo dos anos (dependendo de qual é o ponto de partida, pode até levar décadas) que o comitê trabalhou no encapsulamento em JS, muitas coisas na indústria mudaram e a aparência pode ficar embaçada, o que levou a um falso ranking de prioridades .
Além disso, nós, como comunidade, pressionamos o tc39 forçando-o a liberar recursos mais rapidamente, enquanto damos muito pouco feedback nos estágios iniciais do prozos, diminuindo nossa indignação apenas no momento em que pouco pode ser alterado.
Acredita-se que, neste caso, o processo simplesmente falhou.
Depois de mergulhar na minha cabeça e conversar com alguns representantes, decidi que faria o possível para evitar a recorrência de uma situação semelhante - mas posso fazer um pouco (escreva um artigo de revisão, faça a implementação do stage1
faltando em babel
e isso é tudo).
Mas o mais importante é o feedback - por isso, peço que você participe dessa pequena pesquisa. E eu, por sua vez, tentarei transmiti-lo ao comitê.