herencia constructores clases javascript ecmascript-6

constructores - ¿son las clases es6 solo azúcar sintáctico para el patrón prototípico en javascript?



javascript parent class (6)

¿Las nuevas clases de ES6 son simplemente azúcar sintáctica para el antiguo patrón prototípico?

Sí, son (casi por completo) una sintaxis de conveniencia, la semántica es casi idéntica. La respuesta de Traktor53 entra en las diferencias.

Source

El siguiente ejemplo de código corto muestra cómo se establecen las funciones en una class en el objeto prototype .

class Thing { someFunc() {} } console.log("someFunc" in Thing.prototype); // true

Después de jugar con ES6, realmente empecé a gustarme la nueva sintaxis y las funciones disponibles, pero tengo una pregunta sobre las clases.

¿Las nuevas clases ES6 son simplemente azúcar sintáctica para el antiguo patrón prototípico? o hay más pasando aquí detrás de escena? es decir:

class Thing { //... classy stuff doStuff(){} }

vs:

var Thing = function() { // ... setup stuff }; Thing.prototype.doStuff = function() {}; // etc


Sí, tal vez, pero parte del azúcar sintáctico tiene dientes.

La declaración de una clase crea un objeto de función que es el constructor de la clase, utilizando el código proporcionado para el constructor dentro del cuerpo de la clase, y para las clases con nombre, con el mismo nombre que la clase.

La función de constructor de clase tiene un objeto prototipo normal del que las instancias de clase heredan propiedades en forma JavaScript normal. Los métodos de instancia definidos dentro del cuerpo de la clase se agregan a este prototipo.

ES6 no proporciona un medio para declarar valores de propiedad predeterminados de instancia de clase (es decir, valores que no son métodos) dentro del cuerpo de la clase para ser almacenados en el prototipo y heredados. Para inicializar el valor de instancia, puede establecerlos como propiedades locales, no heredadas dentro del constructor, o agregarlos manualmente al objeto prototype del constructor de clase fuera de la definición de clase de la misma manera que para las funciones de constructor ordinarias. (No estoy discutiendo los méritos o de lo contrario de configurar propiedades heredadas para las clases de JavaScript).

Los métodos estáticos declarados dentro del cuerpo de la clase se agregan como propiedades de la función constructora de la clase. Evite el uso de nombres de métodos de clase estáticos que compitan con las propiedades de funciones estándar y los métodos heredados de Function.prototype como call , apply o length .

Menos azucarado es que las declaraciones y métodos de clase siempre se ejecutan en modo estricto, y una característica que recibe poca atención: la propiedad .prototype de las funciones del constructor de clases es de solo lectura: no puede establecerla en algún otro objeto que haya creado para Algún propósito especial.

Algunas cosas interesantes suceden cuando extiendes una clase:

  • La propiedad del objeto prototype constructor de la clase extendida se prototipa automáticamente en el objeto prototype de la clase que se está extendiendo. Esto no es particularmente nuevo y el efecto puede duplicarse usando Object.create .

  • la función de constructor de clase extendida (objeto) se crea automáticamente un prototipo en la función de constructor de la clase que se está extendiendo, no en Function . Si bien es posible replicar el efecto en una función de constructor ordinario usando Object.setPrototypeOf o incluso childClass.__proto__ = parentClass , esta sería una práctica de codificación extremadamente inusual y a menudo se desaconseja en la documentación de JavaScript.

Existen otras diferencias, como los objetos de clase que no se izan en la forma de funciones nombradas declaradas utilizando la palabra clave de function .

Creo que podría ser ingenuo pensar que las declaraciones y expresiones de clase permanecerán inalteradas en todas las versiones futuras de ECMA Script y será interesante ver si ocurren desarrollos y cuándo. Podría decirse que se ha convertido en una moda asociar el "azúcar sintáctico" con las clases introducidas en ES6 (ECMA-262 versión estándar 6), pero personalmente trato de evitar repetirlo.


Si casi.

Con es6 puede extender la clase Function y la clase Array, en es5 no puede tener el mismo comportamiento: extender Function no crea un objeto invocable y extender Array no hereda la propiedad automática .length en es5

Para el resto, la lógica del prototipo y las clases son las mismas en JavaScript

¿Las clases es6 son realmente azúcar semántica?


Si. Pero son más estrictos.

Hay dos diferencias principales en sus ejemplos.

En primer lugar, con la sintaxis de clase, no puede inicializar una instancia sin una new palabra clave.

class Thing{} Thing() //Uncaught TypeError: Class constructor Thing cannot be invoked without ''new'' var Thing = function() { if(!(this instanceof Thing)){ return new Thing(); } }; Thing(); //works

La segunda es que las clases definidas con sintaxis de clase tienen un alcance de bloque. Es similar a definir variables con la palabra clave let .

class Thing{} class Thing{} //Uncaught SyntaxError: Identifier ''Thing'' has already been declared { class Thing{} } console.log(Thing); //Uncaught ReferenceError: Thing is not defined

Editar

Como @zeroflagL mencionó en su comentario, las declaraciones de clase tampoco se izan.

console.log(Thing) //Uncaught ReferenceError: Thing is not defined class Thing{}


Son azúcares totalmente sintácticos. Lo nuevo de la herencia prototípica en ES6 es la redefinición de la propiedad __proto__ de los objetos. __proto__ es legal ahora y así es como se ha hecho posible la subclase de matrices con JS.


No, las clases ES6 no son solo azúcar sintáctico para el patrón prototípico.

Si bien lo contrario se puede leer en muchos lugares y, aunque parece ser cierto en la superficie, las cosas se vuelven más complejas cuando comienzas a profundizar en los detalles.

No estaba muy satisfecho con las respuestas existentes. Después de investigar un poco más, así es como clasifiqué las características de las clases de ES6 en mi mente:

  1. Azúcar sintáctico para el patrón de herencia pseudoclásica estándar ES5.
  2. Azúcar sintáctico para mejorar el patrón de herencia pseudoclásico disponible pero poco práctico o poco común en ES5.
  3. Azúcar sintáctico para mejoras en el patrón de herencia pseudoclásico no disponible en ES5, pero que puede implementarse en ES6 sin la sintaxis de clase.
  4. Características imposibles de implementar sin la sintaxis de class , incluso en ES6.

(He intentado que esta respuesta sea lo más completa posible y, como resultado, se hizo bastante larga. Aquellos más interesados ​​en una buena visión general deberían ver la respuesta de traktor53 ).

Permítanme ''desugar'' paso a paso (y en la medida de lo posible) las declaraciones de clase a continuación para ilustrar las cosas a medida que avanzamos:

// Class Declaration: class Vertebrate { constructor( name ) { this.name = name; this.hasVertebrae = true; this.isWalking = false; } walk() { this.isWalking = true; return this; } static isVertebrate( animal ) { return animal.hasVertebrae; } } // Derived Class Declaration: class Bird extends Vertebrate { constructor( name ) { super( name ) this.hasWings = true; } walk() { console.log( "Advancing on 2 legs..." ); return super.walk(); } static isBird( animal ) { return super.isVertebrate( animal ) && animal.hasWings; } }

1. Azúcar sintáctico para el patrón de herencia pseudoclásica estándar ES5

En esencia, las clases de ES6 de hecho proporcionan azúcar sintáctica para el patrón de herencia pseudoclásica estándar de ES5.

Declaraciones de clase / expresiones

En el fondo, una declaración de clase o una expresión de clase creará una función de constructor con el mismo nombre que la clase de manera que:

  1. La propiedad interna [[Construct]] del constructor se refiere al bloque de código adjunto al método de la clase '' constructor() .
  2. Los métodos de clase se definen en la propiedad prototype del constructor (no estamos incluyendo métodos estáticos por ahora).

Usando la sintaxis de ES5, la declaración de clase inicial es, por lo tanto, más o menos equivalente a la siguiente (dejando de lado los métodos estáticos):

function Vertebrate( name ) { // 1. A constructor function containing the code of the class''s constructor method is defined this.name = name; this.hasVertebrae = true; this.isWalking = false; } Object.assign( Vertebrate.prototype, { // 2. Class methods are defined on the constructor''s prototype property walk: function() { this.isWalking = true; return this; } } );

La declaración de clase inicial y el fragmento de código anterior producirán lo siguiente:

console.log( typeof Vertebrate ) // function console.log( typeof Vertebrate.prototype ) // object console.log( Object.getOwnPropertyNames( Vertebrate.prototype ) ) // [ ''constructor'', ''walk'' ] console.log( Vertebrate.prototype.constructor === Vertebrate ) // true console.log( Vertebrate.prototype.walk ) // [Function: walk] console.log( new Vertebrate( ''Bob'' ) ) // Vertebrate { name: ''Bob'', hasVertebrae: true, isWalking: false }

Declaraciones / expresiones de clase derivadas

Además de lo anterior, las declaraciones de clase derivadas o las expresiones de clase derivadas también establecerán una herencia entre las propiedades prototype los constructores y harán uso de la super sintaxis de manera que:

  1. La propiedad prototype del constructor hijo hereda de la propiedad prototype del constructor padre.
  2. La llamada super() equivale a llamar al constructor padre con this vinculado al contexto actual.
    • Esta es solo una aproximación aproximada de la funcionalidad proporcionada por super() , que también establecería el parámetro implícito new.target y new.target método interno [[Construct]] (en lugar del método [[Call]] ). La llamada super() se "desugared" completamente en la sección 3 .
  3. Las llamadas super[method]() equivalen a llamar al método en el objeto prototype del padre con this vinculado al contexto actual (no estamos incluyendo métodos estáticos por ahora).
    • Esto es solo una aproximación de super[method]() llamadas super[method]() que no se basan en una referencia directa a una clase principal. super[method]() llamadas super[method]() se replicarán completamente en la sección 3 .

Usando la sintaxis de ES5, la declaración de clase derivada inicial es, por lo tanto, más o menos equivalente a lo siguiente (dejando de lado los métodos estáticos):

function Bird( name ) { Vertebrate.call( this, name ) // 2. The super() call is approximated by directly calling the parent constructor this.hasWings = true; } Bird.prototype = Object.create( Vertebrate.prototype, { // 1. Inheritance is established between the constructors'' prototype properties constructor: { value: Bird, writable: true, configurable: true } } ); Object.assign( Bird.prototype, { walk: function() { console.log( "Advancing on 2 legs..." ); return Vertebrate.prototype.walk.call( this ); // 3. The super[method]() call is approximated by directly calling the method on the parent''s prototype object } })

La declaración de clase derivada inicial y el fragmento de código anterior producirán lo siguiente:

console.log( Object.getPrototypeOf( Bird.prototype ) ) // Vertebrate {} console.log( new Bird("Titi") ) // Bird { name: ''Titi'', hasVertebrae: true, isWalking: false, hasWings: true } console.log( new Bird( "Titi" ).walk().isWalking ) // true

2. Azúcar sintáctico para mejorar el patrón de herencia pseudoclásico disponible pero poco práctico o poco común en ES5

Las clases de ES6 proporcionan además mejoras al patrón de herencia pseudoclásico que ya podría haberse implementado en ES5, pero a menudo se omitieron, ya que podría ser un poco poco práctico configurarlo.

Declaraciones de clase / expresiones

Una declaración de clase o una expresión de clase configurará las cosas de la siguiente manera:

  1. Todo el código dentro de la declaración de clase o expresión de clase se ejecuta en modo estricto.
  2. Los métodos estáticos de la clase se definen en el propio constructor.
  3. Todos los métodos de clase (estáticos o no) no son enumerables.
  4. La propiedad prototipo del constructor no se puede escribir.

Usando la sintaxis de ES5, la declaración de clase inicial es, por lo tanto, más precisamente (pero solo parcialmente) equivalente a lo siguiente:

var Vertebrate = (function() { // 1. Code is wrapped in an IIFE that runs in strict mode ''use strict''; function Vertebrate( name ) { this.name = name; this.hasVertebrae = true; this.isWalking = false; } Object.defineProperty( Vertebrate.prototype, ''walk'', { // 3. Methods are defined to be non-enumerable value: function walk() { this.isWalking = true; return this; }, writable: true, configurable: true } ); Object.defineProperty( Vertebrate, ''isVertebrate'', { // 2. Static methods are defined on the constructor itself value: function isVertebrate( animal ) { // 3. Methods are defined to be non-enumerable return animal.hasVertebrae; }, writable: true, configurable: true } ); Object.defineProperty( Vertebrate, "prototype", { // 4. The constructor''s prototype property is defined to be non-writable: writable: false }); return Vertebrate })();

  • NB 1 : Si el código circundante ya se está ejecutando en modo estricto, por supuesto no hay necesidad de envolver todo en un IIFE.

  • Nota 2 : aunque fue posible definir propiedades estáticas sin problemas en ES5, esto no era muy común. La razón de esto puede ser que establecer la herencia de propiedades estáticas no era posible sin el uso de la propiedad __proto__ no estándar.

Ahora, la declaración de clase inicial y el fragmento de código anterior también producirán lo siguiente:

console.log( Object.getOwnPropertyDescriptor( Vertebrate.prototype, ''walk'' ) ) // { value: [Function: walk], // writable: true, // enumerable: false, // configurable: true } console.log( Object.getOwnPropertyDescriptor( Vertebrate, ''isVertebrate'' ) ) // { value: [Function: isVertebrate], // writable: true, // enumerable: false, // configurable: true } console.log( Object.getOwnPropertyDescriptor( Vertebrate, ''prototype'' ) ) // { value: Vertebrate {}, // writable: false, // enumerable: false, // configurable: false }

Declaraciones / expresiones de clase derivadas

Además de lo anterior, las declaraciones de clase derivadas o las expresiones de clase derivadas también harán uso de la super sintaxis de manera que:

  1. Las llamadas super[method]() dentro de los métodos estáticos equivalen a llamar al método en el constructor del padre con this vinculado al contexto actual.
    • Esto es solo una aproximación de super[method]() llamadas super[method]() que no se basan en una referencia directa a una clase principal. super[method]() llamadas super[method]() en métodos estáticos no pueden imitarse completamente sin el uso de la sintaxis de class y se enumeran en la sección 4.

Usando la sintaxis ES5, la declaración de clase derivada inicial es, por lo tanto, más precisamente (pero solo parcialmente) equivalente a lo siguiente:

function Bird( name ) { Vertebrate.call( this, name ) this.hasWings = true; } Bird.prototype = Object.create( Vertebrate.prototype, { constructor: { value: Bird, writable: true, configurable: true } } ); Object.defineProperty( Bird.prototype, ''walk'', { value: function walk( animal ) { return Vertebrate.prototype.walk.call( this ); }, writable: true, configurable: true } ); Object.defineProperty( Bird, ''isBird'', { value: function isBird( animal ) { return Vertebrate.isVertebrate.call( this, animal ) && animal.hasWings; // 1. The super[method]() call is approximated by directly calling the method on the parent''s constructor }, writable: true, configurable: true } ); Object.defineProperty( Bird, "prototype", { writable: false });

Ahora, la declaración de clase derivada inicial y el fragmento de código anterior también producirán lo siguiente:

console.log( Bird.isBird( new Bird("Titi") ) ) // true

3. Azúcar sintáctico para mejorar el patrón de herencia pseudoclásico no disponible en ES5

Las clases de ES6 proporcionan además mejoras al patrón de herencia pseudoclásico que no están disponibles en ES5, pero se pueden implementar en ES6 sin tener que usar la sintaxis de clase.

Declaraciones de clase / expresiones

Las características de ES6 que se encuentran en otros lugares también se convirtieron en clases, en particular:

  1. Las declaraciones de clase se comportan como declaraciones de let : no se inicializan cuando se izan y terminan en la Zona muerta temporal antes de la declaración. ( question relacionada)
  2. El nombre de la clase se comporta como un enlace const dentro de la declaración de la clase; no se puede sobrescribir dentro de un método de la clase; si intenta hacerlo, se producirá un TypeError .
  3. Los constructores de clase se deben invocar con el método interno [[Construct]] , se lanza un TypeError si se invocan como funciones ordinarias con el método interno [[Call]] .
  4. Los métodos de clase (con la excepción del método constructor() ), estáticos o no, se comportan como los métodos definidos a través de la sintaxis del método conciso, lo que significa que:
    • Pueden usar la palabra clave super través de super.prop o super[method] (esto se debe a que se les asigna una propiedad interna [[HomeObject]] ).
    • No se pueden usar como constructores; carecen de una propiedad prototype y una propiedad interna [[Construct]] .

Con la sintaxis de ES6, la declaración de clase inicial es, por lo tanto, aún más precisa (pero solo parcialmente) equivalente a lo siguiente:

let Vertebrate = (function() { // 1. The constructor is defined with a let declaration, it is thus not initialized when hoisted and ends up in the TDZ ''use strict''; const Vertebrate = function( name ) { // 2. Inside the IIFE, the constructor is defined with a const declaration, thus preventing an overwrite of the class name if( typeof new.target === ''undefined'' ) { // 3. A TypeError is thrown if the constructor is invoked as an ordinary function without new.target being set throw new TypeError( `Class constructor ${Vertebrate.name} cannot be invoked without ''new''` ); } this.name = name; this.hasVertebrae = true; this.isWalking = false; } Object.assign( Vertebrate, { isVertebrate( animal ) { // 4. Methods are defined using the concise method syntax return animal.hasVertebrae; }, } ); Object.defineProperty( Vertebrate, ''isVertebrate'', {enumerable: false} ); Vertebrate.prototype = { constructor: Vertebrate, walk() { // 4. Methods are defined using the concise method syntax this.isWalking = true; return this; }, }; Object.defineProperty( Vertebrate.prototype, ''constructor'', {enumerable: false} ); Object.defineProperty( Vertebrate.prototype, ''walk'', {enumerable: false} ); return Vertebrate; })();

  • Nota 1 : Aunque los métodos de instancia y estáticos se definen con la sintaxis del método conciso, las super no se comportarán como se espera en los métodos estáticos. De hecho, la propiedad interna [[HomeObject]] no es copiada por Object.assign() . Establecer la propiedad [[HomeObject]] correctamente en métodos estáticos requeriría que definamos un constructor de funciones usando un objeto literal, lo cual no es posible.

  • NB 2 : para evitar que los constructores se invoquen sin la new palabra clave, se podrían implementar salvaguardas similares en ES5 haciendo uso del operador instanceof . Sin embargo, esos no cubrían todos los casos (ver esta answer ).

Ahora, la declaración de clase inicial y el fragmento de código anterior también producirán lo siguiente:

Vertebrate( "Bob" ); // TypeError: Class constructor Vertebrate cannot be invoked without ''new'' console.log( Vertebrate.prototype.walk.hasOwnProperty( ''prototype'' ) ) // false new Vertebrate.prototype.walk() // TypeError: Vertebrate.prototype.walk is not a constructor console.log( Vertebrate.isVertebrate.hasOwnProperty( ''prototype'' ) ) // false new Vertebrate.isVertebrate() // TypeError: Vertebrate.isVertebrate is not a constructor

Declaraciones / expresiones de clase derivadas

Además de lo anterior, lo siguiente también se aplicará a una declaración de clase derivada o expresión de clase derivada:

  1. El constructor hijo hereda del constructor padre (es decir, las clases derivadas heredan miembros estáticos).
  2. Llamar a super() en el constructor de la clase derivada equivale a llamar al método interno [[Construct]] del constructor padre con el valor actual new.target y vincular this contexto al objeto devuelto.

Usando la sintaxis ES6, la declaración de clase derivada inicial es, por lo tanto, más precisamente (pero solo parcialmente) equivalente a lo siguiente:

let Bird = (function() { ''use strict''; const Bird = function( name ) { if( typeof new.target === ''undefined'' ) { throw new TypeError( `Class constructor ${Bird.name} cannot be invoked without ''new''` ); } const that = Reflect.construct( Vertebrate, [name], new.target ); // 2. super() calls amount to calling the parent constructor''s [[Construct]] method with the current new.target value and binding the ''this'' context to the returned value (see NB 2 below) that.hasWings = true; return that; } Bird.prototype = { constructor: Bird, walk() { console.log( "Advancing on 2 legs..." ); return super.walk(); // super[method]() calls can now be made using the concise method syntax (see 4. in Class Declarations / Expressions above) }, }; Object.defineProperty( Bird.prototype, ''constructor'', {enumerable: false} ); Object.defineProperty( Bird.prototype, ''walk'', {enumerable: false} ); Object.assign( Bird, { isBird: function( animal ) { return Vertebrate.isVertebrate( animal ) && animal.hasWings; // super[method]() calls can still not be made in static methods (see NB 1 in Class Declarations / Expressions above) } }) Object.defineProperty( Bird, ''isBird'', {enumerable: false} ); Object.setPrototypeOf( Bird, Vertebrate ); // 1. Inheritance is established between the constructors directly Object.setPrototypeOf( Bird.prototype, Vertebrate.prototype ); return Bird; })();

  • Nota 1 : Object.create() solo se puede usar para establecer el prototipo de un nuevo objeto sin función, la configuración de la herencia entre los propios constructores solo se puede implementar en ES5 manipulando la propiedad no estándar __proto__ .

  • NB 2 : No es posible imitar el efecto de super() usando el contexto this , por lo que tuvimos que devolver un objeto diferente explícitamente del constructor.

Ahora, la declaración de clase derivada inicial y el fragmento de código anterior también producirán lo siguiente:

console.log( Object.getPrototypeOf( Bird ) ) // [Function: Vertebrate] console.log( Bird.isVertebrate ) // [Function: isVertebrate]

4. Características imposibles de implementar sin la sintaxis de class

Las clases de ES6 proporcionan además las siguientes características que no se pueden implementar en absoluto sin usar realmente la sintaxis de class :

  1. La propiedad interna [[HomeObject]] de los métodos de clase estática apunta al constructor de la clase.
    • No hay forma de implementar esto para las funciones de constructor ordinarias, ya que requeriría definir una función a través de un objeto literal (ver también la sección 3 anterior). Esto es particularmente problemático para los métodos estáticos de clases derivadas que utilizan la palabra clave super como nuestro método Bird.isBird() .

Es posible solucionar parcialmente este problema si la clase principal se conoce de antemano.

Conclusión

Algunas características de las clases de ES6 son simplemente azúcar sintáctica para el patrón de herencia pseudoclásico estándar de ES5. Sin embargo, las clases de ES6 también vienen con características que solo se pueden implementar en ES6 y algunas características adicionales que ni siquiera se pueden imitar en ES6 (es decir, sin usar la sintaxis de clase).

Mirando lo anterior, creo que es justo decir que las clases ES6 son más concisas, más convenientes y más seguras de usar que el patrón de herencia pseudoclásico ES5. Como resultado, también son menos flexibles (vea esta pregunta, por ejemplo).

Notas al margen

Vale la pena señalar algunas peculiaridades más de clases que no encontraron un lugar en la clasificación anterior:

  1. super() solo es una sintaxis válida en constructores de clases derivadas y solo se puede llamar una vez.
  2. Intentar acceder a this en un constructor de clase derivado antes de que super() se llame resultados en un ReferenceError .
  3. super() debe llamar a super() en un constructor de clase derivado si no se devuelve ningún objeto explícitamente.
  4. eval y los arguments no son identificadores de clase válidos (mientras que son identificadores de función válidos en modo no estricto).
  5. Las clases derivadas configuran un método de constructor() predeterminado si no se proporciona ninguno (correspondiente al constructor( ...args ) { super( ...args ); } ).
  6. No es posible definir propiedades de datos en una clase con una declaración de clase o una expresión de clase (aunque puede agregar propiedades de datos en la clase manualmente después de su declaración).

Recursos adicionales

  • El capítulo Comprensión de las clases de ES6 en Comprensión de ES6 de Nicholas Zakas es el mejor artículo sobre las clases de ES6 que he encontrado.
  • El blog 2ality de Axel Rauschmayer tiene una post muy completa sobre las clases de ES6.
  • Object Playground tiene un excelente video que explica el patrón de herencia pseudoclásico (y lo compara con la sintaxis de la clase).
  • El transpilador de Babel es un buen lugar para explorar cosas por su cuenta.