"Clase-campos-propuesta" o "¿Qué salió mal en tc39 commit"

Todos nosotros hace mucho tiempo queríamos la encapsulación normal en JS, que podría usarse sin gestos innecesarios. También queremos construcciones convenientes para declarar propiedades de clase. Y finalmente, queremos que todas estas características en el lenguaje aparezcan de manera que no rompan las aplicaciones existentes.


Parece que aquí está la felicidad: propuesta de campos de clase , que después de muchos años de tormento del comité tc39 aún llegó a la stage 3 e incluso se implementó en Chrome .


Honestamente, realmente me gustaría escribir un artículo sobre por qué debería usar la nueva función de lenguaje y cómo hacerlo, pero, desafortunadamente, el artículo no tratará sobre eso en absoluto.


Descripción de la falta actual


No repetiré la descripción original , las preguntas frecuentes y los cambios en las especificaciones aquí , sino que resumiré brevemente los puntos principales.


Campos de clase


Declarando campos y usándolos dentro de una clase:


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

Acceso a campos fuera de la clase:


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

Todo parecía ser obvio y durante muchos años hemos estado usando esta sintaxis con Babel y TypeScript .


Solo hay un matiz. Esta nueva sintaxis utiliza [[Define]] , y no [[Set]] semántica con la que vivimos todo este tiempo.


En la práctica, esto significa que el código anterior no es igual a esto:


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

Pero en realidad es equivalente a esto:


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

Y, aunque para el ejemplo anterior, ambos enfoques hacen esencialmente lo mismo, esta es una diferencia MUY GRAVE , y he aquí por qué:


Digamos que tenemos una clase para padres como esta:


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

En base a esto, creamos otro:


 class B extends A { x = 2; } 

Y lo usaron:


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

Luego, por alguna razón, la clase A se cambió de una manera aparentemente compatible con versiones anteriores:


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

Y para la semántica de [[Set]] , este es realmente un cambio compatible con versiones anteriores, pero no para [[Define]] . Ahora la llamada a b.method() saldrá a la consola 1 lugar de 2 . Y esto sucederá porque Object.defineProperty redefine el descriptor de propiedad y, en consecuencia, no se llamará a getter / setter de la clase A De hecho, en la clase secundaria, oscurecimos la propiedad x del padre, de forma similar a cómo podemos hacer esto en el ámbito léxico:


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

Es cierto que, en este caso, el linter con sus reglas no-shadowed-variable / no-shadow nos salvará, pero la probabilidad de que alguien haga un no-shadowed-class-field tiende a cero.


Por cierto, agradeceré el término ruso más exitoso para shadowed .

A pesar de todo lo anterior, no soy un oponente inaceptable de la nueva semántica (aunque preferiría otra), porque tiene sus propios aspectos positivos. Pero, desafortunadamente, estas ventajas no superan al menos más importante: hemos estado usando la semántica [[Set]] durante muchos años, porque se usa en babel6 y TypeScript de forma predeterminada.


Es cierto que vale la pena señalar que en babel7 se ha cambiado el valor predeterminado .

Más discusiones originales sobre este tema se pueden leer aquí y aquí .


Campos privados


Y ahora pasaremos a la parte más controvertida de este. Tan controvertido que:


  1. a pesar de que ya está implementado en Chrome Canary y los campos públicos ya están habilitados de forma predeterminada, los campos privados todavía están detrás de la bandera;
  2. a pesar de que la prozal inicial para campos privados se fusionó con la actual, todavía se están creando solicitudes para la separación de estas dos características (por ejemplo, una , dos , tres y cuatro );
  3. incluso algunos miembros del comité (como Allen Wirfs-Brock y Kevin Smith ) hablan y ofrecen alternativas , a pesar de la etapa 3 ;
  4. este fallo estableció un récord para el número de problemas: 129 en el repositorio actual + 96 en el original , versus 126 para BigInt , y el titular del registro tiene comentarios en su mayoría negativos ;
  5. Tuve que crear un hilo separado con el intento de resumir de alguna manera todos los reclamos en su contra;
  6. Tuve que escribir una pregunta frecuente por separado que cubre esta parte
    sin embargo, debido a una argumentación bastante débil, tales discusiones aparecieron ( uno , dos )
  7. Yo, personalmente, pasé todo mi tiempo libre (ya veces trabajando) durante un largo período de tiempo para resolver todo e incluso encontrar una explicación de por qué era así u ofrecer una alternativa adecuada ;
  8. Al final, decidí escribir este artículo de revisión.

Los campos privados se declaran de la siguiente manera:


 class A { #priv; } 

Y el acceso a ellos es el siguiente:


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

Ni siquiera mencionaré el tema de que el modelo mental detrás de esto no es muy intuitivo ( this.#priv !== this['#priv'] ), no usa las palabras private / protected ya reservadas (lo que necesariamente causará dolor adicional para desarrolladores de TypeScript), no está claro cómo extenderlo para otros modificadores de acceso , y la sintaxis en sí no es muy hermosa. Aunque todo esto fue la razón original que me empujó a un estudio más profundo y a participar en las discusiones.


Todo esto se relaciona con la sintaxis, donde las preferencias estéticas subjetivas son muy fuertes. Y uno podría vivir con él y acostumbrarse a él con el tiempo. Si no fuera por una cosa: hay un problema muy importante de semántica ...


Semántica WeakMap


Echemos un vistazo a lo que hay detrás de la propuesta existente. Podemos reescribir el ejemplo anterior con encapsulación y sin usar la nueva sintaxis, pero conservando la semántica de la actual:


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

Por cierto, sobre la base de esta semántica, uno de los miembros del comité incluso construyó una pequeña biblioteca de servicios públicos que le permite usar el estado privado en este momento, para mostrar que el comité sobrevalora esa funcionalidad. El código formateado solo toma 27 líneas.

En general, todo es bastante bueno, nos ponemos hard-private , que no pueden obtenerse / interceptarse / rastrearse desde el código externo de ninguna manera, y al mismo tiempo podemos acceder a los campos privados de otra instancia de la misma clase, por ejemplo así:


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

Bueno, esto es muy conveniente, excepto por el hecho de que esta semántica, además de la encapsulación en sí, también incluye brand-checking (no puede brand-checking Google lo que es, es poco probable que encuentre información relevante).
brand-checking es lo opuesto duck-typing de duck-typing , en el sentido de que no comprueba la interfaz pública del objeto, sino el hecho de que el objeto se construyó utilizando un código confiable.
Tal verificación, de hecho, tiene un cierto alcance: se asocia principalmente con la seguridad de llamar a un código no confiable en un solo espacio de direcciones con uno confiable y la capacidad de intercambiar objetos directamente sin serialización.


Aunque algunos ingenieros consideran esto una parte necesaria de la encapsulación adecuada.

A pesar de que esta es una oportunidad bastante curiosa, que está estrechamente relacionada con el patrón de (descripción corta y más larga ), Realms propaganda y trabajo científico en el campo de la informática, en el que Mark Samuel Miller (también es miembro del comité) participa, en mi experiencia. , en la práctica de la mayoría de los desarrolladores, esto casi nunca ocurre.


Por cierto, todavía me encontré con una membrana (aunque no sabía qué era entonces) cuando reescribí vm2 para satisfacer mis necesidades.

Problema de brand-checking


Como se mencionó anteriormente, brand-checking es lo opuesto duck-typing de duck-typing . En la práctica, esto significa que tener 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 solo se puede brandCheckedMethod con una instancia de clase A e incluso si el objetivo es un objeto que conserva los invariantes de esta clase, este método arrojará una excepción:


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

Obviamente, este ejemplo es bastante sintético y el uso de duckTypedObj como este duckTypedObj dudoso, hasta que pensemos en Proxy .
Uno de los escenarios de uso de proxy muy importantes es la metaprogramación. Para que el proxy realice todo el trabajo útil necesario, los métodos de los objetos que se envuelven usando un proxy deben ejecutarse en el contexto del proxy, y no en el contexto del objetivo, es decir:


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

Llame a proxy.method(); realizará un trabajo útil declarado en el proxy y devolverá 1 , mientras llama a proxy.brandCheckedMethod(); en lugar de hacer un trabajo útil dos veces desde el proxy, arrojará una excepción, porque a !== proxy , lo que significa que no se aprobó la brand-check .


Sí, podemos ejecutar métodos / funciones en el contexto de un objetivo real, no un proxy, y para algunos escenarios esto es suficiente (por ejemplo, para implementar el patrón de ), pero esto no es suficiente para todos los casos (por ejemplo, para implementar propiedades reactivas: MobX 5 ya usa un proxy para esto, Vue.js y Aurelia están experimentando con este enfoque para futuras versiones).


En general, siempre y cuando brand-check deba realizarse explícitamente, esto no es un problema: el desarrollador solo debe decidir conscientemente qué compensación realiza y si la necesita, además, en el caso de una brand-check explícita brand-check puede implementarla de tal manera que el error no se lanzaría en servidores proxy de confianza.


Desafortunadamente, el actual nos ha privado de esta flexibilidad:


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

Tal method siempre arrojará una excepción si no se invoca en el contexto de un objeto construido utilizando el constructor A Y la peor parte es que brand-check está implícito aquí y se combina con otra funcionalidad: la encapsulación.


Si bien la casi necesaria para cualquier código, brand-check tiene un alcance bastante limitado. Y combinarlos en una sintaxis conducirá al hecho de que aparecen muchas brand-check no intencionadas en el código del usuario, cuando el desarrollador solo pretendía ocultar los detalles de la implementación.
Y el eslogan que se usa para promover esto fue # is the new _ solo exacerbando la situación.


También puede leer una discusión detallada de cómo un prozal existente rompe un proxy . Uno de los desarrolladores y autor de Aurelia, Vue.js, habló en la discusión .

Además, mi comentario , que describe con más detalle la diferencia entre diferentes escenarios proxy, puede parecer interesante para alguien. En su conjunto, toda la discusión sobre la conexión de campos y membranas privadas .

Alternativas


Todas estas discusiones tendrían poco sentido si no hubiera alternativas. Desafortunadamente, ni un solo suplente llegó a la etapa 1 y, como resultado, ni siquiera tuvo la oportunidad de trabajar lo suficiente. Sin embargo, enumeraré aquí las alternativas que de alguna manera resuelven los problemas descritos anteriormente.


  1. Symbol.private - un prozazil alternativo uno de los miembros del comité.
    1. Resuelve todos los problemas anteriores (aunque puede tener los suyos, pero, debido a la falta de trabajo activo en él, es difícil encontrarlos)
    2. una vez más se rechazó en la última reunión del comité debido a la falta de un brand-check integrado, problemas con el patrón de membrana (aunque esto + esto ofrece una solución adecuada) y la falta de sintaxis conveniente
    3. La sintaxis conveniente se puede construir sobre la real, como he mostrado aquí y aquí
  2. Clases 1.1 - anterior posozal del mismo autor
  3. Usar privado como un objeto

En lugar de una conclusión


Por el tono del artículo, probablemente parezca que condeno al comité; esto no es así. Solo me parece que a lo largo de los años (dependiendo de cuál sea el punto de partida, incluso podría ser décadas) que el comité trabajó en la encapsulación en JS, mucho en la industria ha cambiado, y la apariencia podría haberse vuelto borrosa, lo que condujo a una falsa clasificación de prioridades .


Además, nosotros, como comunidad, presionamos tc39 forzándolos a lanzar funciones más rápido, mientras damos muy poca retroalimentación en las primeras etapas de prozos, reduciendo nuestra indignación solo en un momento en que poco se puede cambiar.


Se cree que en este caso, el proceso simplemente falló.


Después de sumergirlo en mi cabeza y hablar con algunos representantes, decidí que haría todo lo posible para evitar que se repita una situación similar, pero puedo hacer un poco (escribir un artículo de revisión, implementar la stage1 faltaba en babel y eso es todo).


Pero lo más importante son los comentarios, por lo que le pido que participe en esta pequeña encuesta. Y yo, a su vez, trataré de transmitirlo al comité.

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


All Articles