«Class-fields-proposition» ou «Qu'est-ce qui s'est mal passé dans tc39 commit»

Il y a longtemps, nous voulons tous une encapsulation normale dans JS, qui pourrait être utilisée sans gestes inutiles. Nous voulons également des constructions pratiques pour déclarer les propriétés de classe. Et enfin, nous voulons que toutes ces fonctionnalités du langage apparaissent de manière à ne pas casser les applications existantes.


Il semblerait qu'ici c'est le bonheur: classe-champs-proposition , qui après de nombreuses années de tourments du comité tc39 est encore parvenue à l' stage 3 et a même été mise en œuvre en chrome .


Honnêtement, j'aimerais vraiment écrire un article expliquant pourquoi vous devriez utiliser la nouvelle fonctionnalité de langue et comment le faire, mais, malheureusement, l'article ne parlera pas du tout de cela.


Description du courant manquant


Je ne répéterai pas ici la description d'origine , la FAQ et les modifications des spécifications , mais je ne résumerai que brièvement les principaux points.


Champs de classe


Déclarer des champs et les utiliser dans une classe:


 class A { x = 1; method() { console.log(this.x); } } 

Accès aux champs en dehors de la classe:


 const a = new A(); console.log(ax); 

Tout semblait évident et depuis de nombreuses années, nous utilisons cette syntaxe en utilisant Babel et TypeScript .


Seulement, il y a une nuance. Cette nouvelle syntaxe utilise [[Define]] , et non [[Set]] sémantique avec laquelle nous avons vécu tout ce temps.


En pratique, cela signifie que le code ci-dessus n'est pas égal à ceci:


 class A { constructor() { this.x = 1; } method() { console.log(this.x); } } 

Mais en fait, cela équivaut à ceci:


 class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } } 

Et, bien que pour l'exemple ci-dessus, les deux approches font essentiellement la même chose, c'est une différence TRÈS GRAVE , et voici pourquoi:


Disons que nous avons une classe parent comme celle-ci:


 class A { x = 1; method() { console.log(this.x); } } 

Sur cette base, nous en avons créé un autre:


 class B extends A { x = 2; } 

Et ils l'ont utilisé:


 const b = new B(); b.method(); //   2   

Ensuite, pour une raison quelconque, la classe A été modifiée d'une manière apparemment rétrocompatible:


 class A { _x = 1; //  ,   ,        get x() { return this._x; }; set x(val) { return this._x = val; }; method() { console.log(this._x); } } 

Et pour la sémantique de [[Set]] , c'est vraiment un changement rétrocompatible, mais pas pour [[Define]] . Maintenant, l'appel à b.method() sortira sur la console 1 au lieu de 2 . Et cela se produira car Object.defineProperty redéfinit le descripteur de propriété et, par conséquent, getter / setter de la classe A ne sera pas appelé. En fait, dans la classe enfant, nous avons masqué la propriété x du parent, de la même manière que nous pouvons le faire dans la portée lexicale:


 const x = 1; { const x = 2; } 

Certes, dans ce cas, le linter avec ses règles no-shadow no-shadowed-variable / no-shadow nous sauvera, mais la probabilité que quelqu'un crée un no-shadowed-class-field tend à zéro.


Soit dit en passant, je serai reconnaissant pour le terme russe plus réussi pour l' shadowed .

Malgré tout ce qui précède, je ne suis pas un adversaire inacceptable de la nouvelle sémantique (bien que j'en préfère une autre), car elle a ses propres aspects positifs. Mais, malheureusement, ces avantages ne l'emportent pas sur les inconvénients les plus importants - nous utilisons la sémantique [[Set]] depuis de nombreuses années, car elle est utilisée par défaut dans babel6 et TypeScript .


Certes, il convient de noter que dans babel7 valeur par défaut a été modifiée .

Des discussions plus originales sur ce sujet peuvent être lues ici et ici .


Champs privés


Et maintenant, nous allons passer à la partie la plus controversée de celle-ci. Tellement controversé que:


  1. bien qu'il soit déjà implémenté dans Chrome Canary et que les champs publics soient déjà activés par défaut, les champs privés sont toujours derrière le drapeau;
  2. bien que le profil initial pour les champs privés ait été fusionné avec le profil actuel, des demandes sont toujours en cours pour la séparation de ces deux caractéristiques (par exemple, un , deux , trois et quatre );
  3. même certains membres du comité (comme Allen Wirfs-Brock et Kevin Smith ) s'expriment et proposent des alternatives , malgré l' étape3 ;
  4. cette erreur a établi un record pour le nombre de problèmes - 129 dans le référentiel actuel + 96 dans l' original , contre 126 pour BigInt , et le détenteur du record a surtout des commentaires négatifs ;
  5. J'ai dû créer un fil séparé avec une tentative de résumer en quelque sorte toutes les réclamations contre lui;
  6. J'ai dû écrire une FAQ séparée qui couvre cette partie
    cependant, en raison d'une argumentation plutôt faible, de telles discussions sont apparues ( un , deux )
  7. Personnellement, j'ai passé tout mon temps libre (et parfois travaillé) pendant une longue période pour tout comprendre et même trouver une explication de la raison pour laquelle il était comme ça ou proposer une alternative appropriée ;
  8. à la fin, j'ai décidé d'écrire cet article de synthèse.

Les champs privés sont déclarés comme suit:


 class A { #priv; } 

Et leur accès est le suivant:


 class A { #priv = 1; method() { console.log(this.#priv); } } 

Je ne soulèverai même pas le sujet selon lequel le modèle mental derrière cela n'est pas très intuitif ( this.#priv !== this['#priv'] ), n'utilise pas les mots private / protected déjà réservés (ce qui causera nécessairement une douleur supplémentaire pour les développeurs TypeScript), il n'est pas clair comment l'étendre à d' autres modificateurs d'accès , et la syntaxe elle-même n'est pas très belle. Bien que tout cela soit la raison originale qui m'a poussé à une étude plus approfondie et à une participation aux discussions.


Tout cela concerne la syntaxe, où les préférences esthétiques subjectives sont très fortes. Et on pourrait vivre avec et s'y habituer avec le temps. Sinon pour une chose: il y a un problème très important de sémantique ...


Sémantique WeakMap


Voyons ce qui se cache derrière la proposition existante. Nous pouvons réécrire l'exemple ci-dessus avec encapsulation et sans utiliser la nouvelle syntaxe, mais en préservant la sémantique de la syntaxe actuelle:


 const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } } 

Soit dit en passant, sur la base de cette sémantique, l'un des membres du comité a même construit une petite bibliothèque d'utilitaires qui vous permet d'utiliser l'état privé en ce moment, afin de montrer que cette fonctionnalité est trop surfaite par le comité. Le code formaté ne prend que 27 lignes.

En général, tout va bien, nous obtenons hard-private accès hard-private , qui ne peut en aucun cas être obtenu / intercepté / suivi à partir de code externe, et en même temps, nous pouvons accéder aux champs privés d'une autre instance de la même classe, par exemple comme ceci:


 isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; } 

Eh bien, c'est très pratique, sauf que cette sémantique, en plus de l'encapsulation elle-même, comprend également la brand-checking (vous ne pouvez pas google ce que c'est - il est peu probable que vous trouviez des informations pertinentes).
brand-checking est l'opposé de la duck-typing de duck-typing , en ce sens qu'elle ne vérifie pas l'interface publique de l'objet, mais le fait que l'objet a été construit à l'aide d'un code de confiance.
Une telle vérification, en fait, a une certaine portée - elle est principalement associée à la sécurité de l'appel de code non approuvé dans un seul espace d'adressage avec un espace de confiance et à la possibilité d'échanger des objets directement sans sérialisation.


Bien que certains ingénieurs considèrent cela comme une partie nécessaire d'une bonne encapsulation.

Malgré le fait qu'il s'agit d'une opportunité plutôt curieuse, qui est étroitement liée au modèle de (description courte et plus longue ), à Realms propagande et travaux scientifiques dans le domaine de l'informatique, dans lesquels Mark Samuel Miller est engagé (il est également membre du comité), selon mon expérience. , dans la pratique de la plupart des développeurs, cela ne se produit presque jamais.


Soit dit en passant , je suis toujours tombé sur une membrane (même si je ne savais pas ce que c'était alors) lorsque j'ai réécrit vm2 pour répondre à mes besoins.

Problème de brand-checking


Comme mentionné précédemment, la brand-checking est l'opposé de la duck-typing de duck-typing . En pratique, cela signifie qu'avoir ce code:


 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 ne peut être appelé qu'avec une instance de classe A et même si la cible est un objet qui conserve les invariants de cette classe, cette méthode lèvera une exception:


 const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method(); //        1 duckTypedObj.brandCheckedMethod(); //      

Évidemment, cet exemple est assez synthétique et l'utilisation de duckTypedObj comme celle-ci duckTypedObj douteuse, jusqu'à ce que nous pensions à Proxy .
La métaprogrammation est l'un des scénarios d'utilisation de proxy très importants. Pour que le proxy fasse tout le travail utile nécessaire, les méthodes des objets encapsulés à l'aide d'un proxy doivent être exécutées dans le contexte du proxy, et non dans le contexte de la cible, à savoir:


 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; } }); 

Appelez proxy.method(); fera un travail utile déclaré dans le proxy et retournera 1 , tout en appelant proxy.brandCheckedMethod(); au lieu de faire deux fois un travail utile à partir du proxy, il lèvera une exception, car a !== proxy , ce qui signifie que la brand-check n'a pas réussi.


Oui, nous pouvons exécuter des méthodes / fonctions dans le contexte d'une cible réelle, pas d'un proxy, et pour certains scénarios cela suffit (par exemple, pour implémenter le modèle ), mais cela ne suffit pas pour tous les cas (par exemple, pour implémenter des propriétés réactives: MobX 5 utilise déjà un proxy pour cela, Vue.js et Aurelia expérimentent cette approche pour les futures versions).


En général, tant que le brand-check de brand-check doit être fait explicitement, ce n'est pas un problème - le développeur doit juste consciemment décider quel compromis il fait et s'il en a besoin, de plus, dans le cas d'un brand-check explicite brand-check vous pouvez l'implémenter de telle manière que l'erreur ne serait pas renvoyée sur des proxys approuvés.


Malheureusement, l'actuel nous a privé de cette flexibilité:


 class A { #priv; method() { this.#priv; //    brand-check   } } 

Une telle method lèvera toujours une exception si elle n'est pas appelée dans le contexte d'un objet construit à l'aide du constructeur A Et le pire, c'est que la brand-check est implicite ici et est mélangée à une autre fonctionnalité - l'encapsulation.


Alors que l' presque nécessaire pour tout code, la brand-check a une portée plutôt étroite. Et les combiner en une seule syntaxe entraînera le fait que de nombreuses brand-check involontaires apparaissent dans le code utilisateur, lorsque le développeur avait uniquement l'intention de masquer les détails de l'implémentation.
Et le slogan qui est utilisé pour promouvoir cela était # is the new _ exacerbant seulement la situation.


Vous pouvez également lire une discussion détaillée sur la façon dont un proxy existant rompt un proxy . L'un des développeurs et auteur d'Aurelia Vue.js est intervenu dans la discussion .

De plus, mon commentaire , qui décrit plus en détail la différence entre les différents scénarios de proxy, peut sembler intéressant pour quelqu'un. Dans son ensemble, toute la discussion sur la connexion des champs privés et des membranes .

Alternatives


Toutes ces discussions auraient peu de sens s'il n'y avait pas d'alternatives. Malheureusement, pas un seul remplaçant n'est même arrivé à l' étape 1 , et, par conséquent, n'a même pas eu la chance d'être suffisamment élaboré. Cependant, je vais énumérer ici des alternatives qui résolvent en quelque sorte les problèmes décrits ci-dessus.


  1. Symbol.private - un prozazil alternatif l'un des membres du comité.
    1. Résout tous les problèmes ci-dessus (bien qu'il puisse avoir le sien, mais, compte tenu du manque de travail actif sur celui-ci, il est difficile de les trouver)
    2. il a de nouveau été rejeté lors de la dernière réunion du comité en raison de l'absence d'un brand-check intégré, de problèmes avec le motif de la membrane (bien que cela + cela offre une solution adéquate) et du manque de syntaxe pratique
    3. une syntaxe pratique peut être construite au-dessus de la réelle, comme je l'ai montré ici et ici
  2. Classes 1.1 - posozal antérieur du même auteur
  3. Utilisation de Private comme objet

Au lieu d'une conclusion


Par le ton de l'article, il peut probablement sembler que je condamne le comité - ce n'est pas le cas. Il me semble qu'au fil des années (selon le point de départ, cela pourrait même être des décennies) que le comité a travaillé sur l'encapsulation dans JS, beaucoup de choses dans l'industrie ont changé, et l'apparence pourrait être floue, ce qui a conduit à un faux classement des priorités .


De plus, en tant que communauté, nous poussons le tc39 à les forcer à publier des fonctionnalités plus rapidement, tout en donnant très peu de commentaires dans les premiers stades des prozos, ce qui ne fait baisser notre indignation qu'à un moment où peu de choses peuvent être modifiées.


On pense que dans ce cas, le processus a simplement échoué.


Après l'avoir plongé dans ma tête et avoir discuté avec certains représentants, j'ai décidé que je ferais de mon mieux pour éviter la répétition d'une situation similaire - mais je peux faire un peu (écrire un article de revue, faire en sorte que la mise en œuvre de l' stage1 ratée à babel et tout le babel ).


Mais la chose la plus importante est la rétroaction - je vous demanderais donc de participer à cette petite enquête. Et j'essaierai à mon tour de le transmettre au comité.

Source: https://habr.com/ru/post/fr428119/


All Articles