JavaScript: la guía definitiva de clases

¡Buen dia amigos!



JavaScript usa el modelo de herencia prototipo: cada objeto hereda campos (propiedades) y métodos del objeto prototipo.



Las clases que se utilizan en Java o Swift como plantillas o esquemas para crear objetos no existen en JavaScript. Solo hay objetos en herencia prototípica.



La herencia prototípica puede imitar el modelo clásico de herencia de clases. Para hacer esto, ES6 introdujo la palabra clave de clase: azúcar sintáctico para herencia de prototipos.



En este artículo, aprenderemos cómo trabajar con clases: definir clases, sus campos y métodos privados (privados) y públicos (públicos) y crear instancias.



1. Definición: la palabra clave de clase



La palabra clave class se usa para definir una clase:



class User {
    //  
}


Esta sintaxis se denomina declaración de clase.



Es posible que la clase no tenga nombre. Usando una expresión de clase, puede asignar una clase a una variable:



const UserClass = class {
    //  
}


Las clases se pueden exportar como módulos. A continuación, se muestra un ejemplo de la exportación predeterminada:



export default class User {
    //  
}


Y aquí hay un ejemplo de una exportación con nombre:



export class User {
    //  
}


Las clases se utilizan para crear instancias. Una instancia es un objeto que contiene los datos y la lógica de una clase.







Las instancias se crean utilizando el operador new: instance = new Class ().



A continuación, se explica cómo crear una instancia de la clase de usuario:



const myUser = new User()


2. Inicialización: constructor ()



constructor (param1, param2, ...) es un método especial dentro de una clase que inicializa una instancia. Aquí es donde se establecen y configuran los valores iniciales para los campos de instancia.



En el siguiente ejemplo, el constructor establece el valor inicial para el campo de nombre:



class User {
    constructor(name) {
        this.name = name
    }
}


El constructor toma un parámetro, name, que se usa para establecer el valor inicial del campo this.name.



esto en el constructor apunta a la instancia que se está creando.



El argumento utilizado para instanciar la clase se convierte en un parámetro para su constructor:



class User {
    constructor(name) {
        name // 
        this.name = name
    }
}

const user = new User('')


El parámetro de nombre dentro del constructor tiene el valor 'Pechorin'.



Si no define su propio constructor, se crea un constructor estándar, que es una función vacía que no afecta a la instancia.



3. Campos



Los campos de clase son variables que contienen información específica. Los campos se pueden dividir en dos grupos:



  1. Campos de instancia de clase
  2. Campos de la propia clase (estáticos)


Los campos también tienen dos niveles de acceso:



  1. Público (público): los campos están disponibles tanto dentro de la clase como en instancias
  2. Privado (privado): los campos solo son accesibles dentro de la clase


3.1. Campos públicos de instancias de clase



class User {
    constructor(name) {
        this.name = name
    }
}


La expresión this.name = name crea un nombre de campo de instancia y le asigna un valor inicial.



Se puede acceder a este campo utilizando un descriptor de acceso de propiedad:



const user = new User('')
user.name // 


En este caso, el nombre es un campo público porque es accesible fuera de la clase Usuario.



Al crear campos implícitamente dentro de un constructor, es difícil obtener una lista de todos los campos. Para ello, los campos deben recuperarse del constructor.



La mejor forma es definir explícitamente los campos de la clase. No importa lo que haga el constructor, la instancia siempre tiene el mismo conjunto de campos.



La propuesta para crear campos de clase te permite definir campos dentro de una clase. Además, aquí puede asignar valores iniciales a los campos:



class SomeClass {
    field1
    field2 = ' '

    // ...
}


Cambiemos el código de la clase User definiendo un campo de nombre público en él:



class User {
    name

    constructor(name) {
        this.name = name
    }
}

const user = new User('')
user.name // 


Estos campos públicos son muy descriptivos, un vistazo rápido a la clase le permite comprender la estructura de sus datos.



Además, un campo de clase se puede inicializar en el momento de la definición:



class User {
    name = ''

    constructor() {
        //  
    }
}

const user = new User()
user.name // 


No existen restricciones de acceso a los campos abiertos y su cambio. Puede leer y asignar valores a dichos campos en el constructor, métodos y fuera de la clase.



3.2. Campos privados de instancias de clase



La encapsulación le permite ocultar los detalles de implementación interna de una clase. Quien usa la clase encapsulada confía en la interfaz pública sin entrar en los detalles de la implementación de la clase.



Estas clases son más fáciles de actualizar cuando cambian los detalles de implementación.



Una buena forma de ocultar detalles es usar campos privados. Dichos campos solo se pueden leer y modificar dentro de la clase a la que pertenecen. Los campos privados no están disponibles fuera de la clase.



Para convertir un campo en privado, preceda su nombre con un símbolo #, por ejemplo, #myPrivateField. Cuando se hace referencia a un campo de este tipo, siempre se debe utilizar el prefijo especificado.



Hagamos que el campo de nombre sea privado:



class User {
    #name

    constructor(name) {
        this.#name = name
    }

    getName() {
        return this.#name
    }
}

const user = new User('')
user.getName() // 
user.#name // SyntaxError


#name es un campo privado. Solo se puede acceder dentro de la clase Usuario. El método getName () hace esto.



Sin embargo, intentar acceder a #nombre fuera de la clase de Usuario arrojará un error de sintaxis: SyntaxError: El campo privado '#nombre' debe declararse en una clase adjunta.



3.3. Campos estáticos públicos



En una clase, puede definir campos que pertenecen a la propia clase: campos estáticos. Estos campos se utilizan para crear constantes que almacenan la información que necesita la clase.



Para crear campos estáticos, use la palabra clave estática antes del nombre del campo: static myStaticField.



Agreguemos un nuevo campo de tipo para definir el tipo de usuario: administrador o regular. Los campos estáticos TYPE_ADMIN y TYPE_REGULAR son constantes para cada tipo de usuario:



class User {
    static TYPE_ADMIN = 'admin'
    static TYPE_REGULAR = 'regular'

    name
    type

    constructor(name, type) {
        this.name = name
        this.type = type
    }
}

const admin = new User(' ', User.TYPE_ADMIN)
admin.type === User.TYPE_ADMIN // true


Para acceder a los campos estáticos, utilice el nombre de la clase y el nombre de la propiedad: User.TYPE_ADMIN y User.TYPE_REGULAR.



3.4. Campos estáticos privados



A veces, los campos estáticos también forman parte de la implementación interna de la clase. Para encapsular dichos campos, puede hacerlos privados.



Para hacer esto, anteponga el nombre del campo con #: static #myPrivateStaticFiled.



Digamos que queremos limitar el número de instancias de la clase User. Se pueden crear campos estáticos privados para ocultar información sobre la cantidad de instancias:



class User {
    static #MAX_INSTANCES = 2
    static #instances = 0
}

name

constructor(name) {
    User.#instances++
    if (User.#instances > User.#MAX_INSTANCES) {
        throw new Error('    User')
    }
    this.name = name
}

new User('')
new User('')
new User('') //     User


El campo estático Usuario. # MAX_INSTANCES define el número permitido de instancias, y Usuario. # Instancias define el número de instancias creadas.



Estos campos estáticos privados solo están disponibles dentro de la clase Usuario. Nada del mundo exterior puede influir en las limitaciones: esta es una de las ventajas de la encapsulación.



Aprox. carril: si limita el número de instancias a una, obtiene una implementación interesante del patrón de diseño Singleton.



4. Métodos



Los campos contienen datos. La capacidad de cambiar datos es proporcionada por funciones especiales que son parte de la clase: métodos.



JavaScript admite tanto métodos de instancia como métodos estáticos.



4.1. Métodos de instancia



Los métodos de una instancia de una clase pueden modificar sus datos. Los métodos de instancia pueden llamar a otros métodos de instancia, así como a métodos estáticos.



Por ejemplo, definamos un método getName () que devuelva el nombre del usuario:



class User {
    name = ''

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')
user.getName() // 


En un método de clase, así como en un constructor, esto apunta a la instancia que se está creando. Use esto para obtener datos de instancia: this.field, o para llamar a métodos: this.method ().



Agreguemos un nuevo método nameContains (str) que toma un argumento y llama a otro método:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }

    nameContains(str) {
        return this.getName().includes(str)
    }
}

const user = new User('')
user.nameContains('') // true
user.nameContains('') // false


nameContains (str) es un método de la clase User que toma un argumento. Llama a otro método de instancia getName () para obtener el nombre de usuario.



El método también puede ser privado. Para convertir un método en privado, use el prefijo #.



Hagamos privado el método getName ():



class User {
    #name

    constructor(name) {
        this.#name = name
    }

    #getName() {
        return this.#name
    }

    nameContains(str) {
        return this.#getName().includes(str)
    }
}

const user = new User('')
user.nameContains('') // true
user.nameContains('') // false

user.#getName // SyntaxError


#getName () es un método privado. Dentro del método nameContains (str), lo llamamos así. # GetName ().



Al ser privado, el método #getName () no se puede llamar fuera de la clase User.



4.2. Getters y Setters



Los captadores y definidores son descriptores de acceso o propiedades calculadas. Estos son métodos que imitan campos, pero le permiten leer y escribir datos.



Los captadores se utilizan para obtener datos, los definidores se utilizan para modificarlos.



Para evitar asignar una cadena vacía al campo de nombre, envuelva el campo privado #nameValue en un getter y setter:



class User {
    #nameValue

    constructor(name) {
        this.name = name
    }

    get name() {
        return this.#nameValue
    }

    set name(name) {
        if (name === '') {
            throw new Error('     ')
        }
        this.#nameValue = name
    }
}

const user = new User('')
user.name //  , 
user.name = '' //  

user.name = '' //      


4.3. Métodos estáticos



Los métodos estáticos son funciones que pertenecen a la propia clase. Definen la lógica de la clase, no sus instancias.



Para crear un método estático, use la palabra clave estática delante del nombre del método: static myStaticMethod ().



Al trabajar con métodos estáticos, hay que tener en cuenta dos reglas simples:



  1. Un método estático tiene acceso a campos estáticos.
  2. No tiene acceso a los campos de instancia.


Creemos un método estático para comprobar que ya se ha creado un usuario con el nombre especificado:



class User {
    static #takenNames = []

    static isNameTaken(name) {
        return User.#takenNames.includes(name)
    }

    name = ''

    constructor(name) {
        this.name = name
        User.#takenNames.push(name)
    }
}

const user = new User('')

User.isNameTaken('') // true
User.isNameTaken('') // false


isNameTaken () es un método estático que usa el campo estático privado User. # takeNames para determinar qué nombres se usaron.



Los métodos estáticos también pueden ser privados: static #myPrivateStaticMethod (). Estos métodos solo se pueden llamar dentro de la clase.



5. Herencia: se extiende



Las clases en JavaScript admiten la herencia utilizando la palabra clave extiende.



En la expresión, la clase Child extiende a Parent {}, la clase Child hereda los constructores, campos y métodos de Parent.



Creemos un ContentWriter de clase secundaria que amplíe la clase padre User:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []
}

const writer = new ContentWriter('')

writer.name // 
writer.getName() // 
writer.posts // []


ContentWriter hereda del usuario un constructor, un método getName () y un campo de nombre. El propio ContentWriter define un nuevo campo de publicaciones.



Tenga en cuenta que los campos privados y los métodos de la clase principal no son heredados por las clases secundarias.



5.1. Constructor padre: super () en constructor ()


Para llamar al constructor de la clase principal en la clase secundaria, use la función especial super () disponible en el constructor de la clase secundaria.



Deje que el constructor ContentWriter llame al constructor padre e inicialice el campo de publicaciones:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('', ['  '])
writer.name // 
writer.posts // ['  ']


super (nombre) en la clase secundaria ContentWriter llama al constructor de la clase principal User.



Tenga en cuenta que se llama a super () en el constructor hijo antes de usar la palabra clave this. La llamada super () "vincula" el constructor padre a la instancia.



class Child extends Parent {
    constructor(value1, value2) {
        //  !
        this.prop2 = value2
        super(value1)
    }
}


5.2. Instancia principal: super en métodos


Para acceder a un método principal dentro de una clase secundaria, use la super taquigrafía especial:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }

    getName() {
        const name = super.getName()
        if (name === '') {
            return ''
        }
        return name
    }
}

const writer = new ContentWriter('', ['  '])
writer.getName() // 


getName () de la clase ContentWriter secundaria llama al método getName () de la clase principal User.



A esto se le llama anulación de método.



Tenga en cuenta que super también se puede utilizar para métodos estáticos de la clase principal.



6. Comprobación del tipo de objeto: instancia de



La expresión del objeto instancia de Clase determina si un objeto es una instancia de la clase especificada.



Consideremos un ejemplo:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')
const obj = {}

user instanceof User // true
obj instanceof User // false


El operador instanceof es polimórfico: examina toda la cadena de clases.



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('', ['  '])

writer instanceof ContentWriter // true
writer instanceof User // true


¿Qué pasa si necesitamos definir una clase de instancia específica? La propiedad del constructor se puede utilizar para esto:



writer.constructor === ContentWriter // true
writer.constructor === User // false
// 
writer.__proto__ === ContentWriter.prototype // true
writer.__proto__ === User.prototype // false


7. Clases y prototipos



Debe decirse que la sintaxis de la clase es una buena abstracción sobre la herencia prototípica. No necesita hacer referencia a prototipos para usar clases.



Sin embargo, las clases son solo una superestructura de la herencia de prototipos. Cualquier clase es una función que crea una instancia cuando se llama a un constructor.



Los siguientes dos ejemplos son idénticos.



Clases:



class User {
    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')

user.getName() // 
user instanceof User // true


Prototipos:



function User(name) {
    this.name = name
}

User.prototype.getName = function () {
    return this.name
}

const user = new User('')

user.getName() // 
user instanceof User // true


Por tanto, la comprensión de las clases requiere un buen conocimiento de la herencia de prototipos.



8. Disponibilidad de capacidades de clase



Las capacidades de clase presentadas en este artículo se dividen entre la especificación ES6 y las propuestas en la tercera etapa de consideración:







Aprox. Por: de acuerdo con Puedo usar, el soporte para campos de clases privadas es actualmente del 68%.



9. Conclusión



Las clases en JavaScript se usan para inicializar instancias usando un constructor y definir sus campos y métodos. La palabra clave estática se puede utilizar para definir los campos y métodos de la propia clase.



La herencia se implementa mediante la palabra clave extensions. La palabra clave super le permite acceder a la clase principal desde la secundaria.



Para aprovechar la encapsulación, es decir ocultar detalles de implementación interna, hacer que los campos y métodos sean privados. Los nombres de dichos campos y métodos deben comenzar con un símbolo #.



Las clases son omnipresentes en JavaScript moderno.



Espero que el artículo te haya sido útil. Gracias por su atención.



All Articles