JavaScript orientado a objetos en términos simples





¡Buen dia amigos!



Hay 4 formas en JavaScript para crear un objeto:



  • Función constructora
  • Clase (clase)
  • Objeto vinculado a otro objeto (OLOO)
  • Función de fábrica


¿Qué método deberías utilizar? ¿Cuál es el mejor?



Para responder a estas preguntas, no solo consideraremos cada enfoque por separado, sino que también compararemos clases y funciones de fábrica de acuerdo con los siguientes criterios: herencia, encapsulación, la palabra clave "esto", controladores de eventos.



Comencemos con lo que es la programación orientada a objetos (OOP).



¿Qué es POO?



Básicamente, OOP es una forma de escribir código que le permite crear objetos usando un solo objeto. Esta es también la esencia del patrón de diseño de Constructor. Un objeto compartido se suele denominar plano, plano o plano, y los objetos que crea son instancias.



Cada instancia tiene propiedades heredadas del padre y propiedades propias. Por ejemplo, si tenemos un proyecto humano, podemos crear instancias con diferentes nombres basados ​​en él.



El segundo aspecto de OOP es estructurar el código cuando tenemos varios proyectos de diferentes niveles. A esto se le llama herencia o subclasificación.



El tercer aspecto de la programación orientada a objetos es la encapsulación, cuando ocultamos los detalles de la implementación a los externos, lo que hace que las variables y funciones sean inaccesibles desde el exterior. Ésta es la esencia de los patrones de diseño de módulos y fachadas.



Sigamos con los métodos de creación de objetos.



Métodos de creación de objetos



Función constructora


Los constructores son funciones que utilizan la palabra clave "this".



    function Human(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }


esto le permite almacenar y acceder a los valores únicos de la instancia que se está creando. Las instancias se crean utilizando la palabra clave "nueva".



const chris = new Human('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier

const zell = new Human('Zell', 'Liew')
console.log(zell.firstName) // Zell
console.log(zell.lastName) // Liew


Clase


Las clases son una abstracción ("azúcar sintáctica") sobre funciones constructoras. Facilitan la creación de instancias.



    class Human {
        constructor(firstName, lastName) {
            this.firstName = firstName
            this.lastName = lastName
        }
    }


Tenga en cuenta que el constructor contiene el mismo código que la función de constructor anterior. Tenemos que hacer esto para inicializar esto. Podemos omitir el constructor si no necesitamos asignar valores iniciales.



A primera vista, las clases parecen ser más complejas que los constructores: tienes que escribir más código. Sostenga sus caballos y no saque conclusiones precipitadas. Las clases son geniales. Entenderás por qué un poco más tarde.



Las instancias también se crean utilizando la palabra clave "nueva".



const chris = new Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Vincular objetos


Este método de creación de objetos fue propuesto por Kyle Simpson. En este enfoque, definimos el proyecto como un objeto ordinario. Luego, usando un método (que generalmente se llama init, pero no es necesario, a diferencia del constructor de la clase), inicializamos la instancia.



const Human = {
    init(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
}


Object.create se usa para crear una instancia. Después de la instanciación, se llama a init.



const chris = Object.create(Human)
chris.init('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


El código se puede mejorar un poco devolviendo esto a init.



const Human = {
  init () {
    // ...
    return this
  }
}

const chris = Object.create(Human).init('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Función de fábrica


Una función de fábrica es una función que devuelve un objeto. Se puede devolver cualquier objeto. Incluso puede devolver una instancia de una clase o enlaces de objetos.



Aquí hay un ejemplo simple de una función de fábrica.



function Human(firstName, lastName) {
    return {
        firstName,
        lastName
    }
}


No necesitamos la palabra clave "this" para crear una instancia. Simplemente llamamos a la función.



const chris = Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


Ahora veamos formas de agregar propiedades y métodos.



Definición de propiedades y métodos



Los métodos son funciones declaradas como propiedades de un objeto.



    const someObject = {
        someMethod () { /* ... */ }
    }


En OOP, hay dos formas de definir propiedades y métodos:



  • En una instancia
  • En prototipo


Definición de propiedades y métodos en el constructor


Para definir una propiedad en una instancia, debe agregarla a la función constructora. Asegúrese de agregar la propiedad a esto.



function Human (firstName, lastName) {
  //  
  this.firstName = firstName
  this.lastname = lastName

  //  
  this.sayHello = function () {
    console.log(`Hello, I'm ${firstName}`)
  }
}

const chris = new Human('Chris', 'Coyier')
console.log(chris)






Los métodos generalmente se definen en el prototipo, ya que esto evita crear una función para cada instancia, es decir Permite que todas las instancias compartan una única función (denominada función compartida o distribuida).



Para agregar una propiedad al prototipo, use prototype.



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastname = lastName
}

//    
Human.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.firstName}`)
}






La creación de varios métodos puede resultar tediosa.



//    
Human.prototype.method1 = function () { /*...*/ }
Human.prototype.method2 = function () { /*...*/ }
Human.prototype.method3 = function () { /*...*/ }


Puede hacer su vida más fácil con Object.assign.



Object.assign(Human.prototype, {
  method1 () { /*...*/ },
  method2 () { /*...*/ },
  method3 () { /*...*/ }
})


Definición de propiedades y métodos en una clase


Las propiedades de la instancia se pueden definir en constructor.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
      this.lastname = lastName

      this.sayHello = function () {
        console.log(`Hello, I'm ${firstName}`)
      }
  }
}






Las propiedades del prototipo se definen después del constructor como una función normal.



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}






Crear varios métodos en una clase es más fácil que en un constructor. No necesitamos Object.assign para esto. Solo estamos agregando otras funciones.



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  method1 () { /*...*/ }
  method2 () { /*...*/ }
  method3 () { /*...*/ }
}


Definición de propiedades y métodos al vincular objetos


Para definir propiedades para una instancia, agregamos una propiedad a esto.



const Human = {
  init (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    this.sayHello = function () {
      console.log(`Hello, I'm ${firstName}`)
    }

    return this
  }
}

const chris = Object.create(Human).init('Chris', 'Coyier')
console.log(chris)






El método de prototipo se define como un objeto regular.



const Human = {
  init () { /*...*/ },
  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}






Definición de propiedades y métodos en funciones de fábrica (FF)


Las propiedades y los métodos se pueden incluir en el objeto devuelto.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}






Cuando usa FF, no puede definir propiedades de prototipo. Si necesita propiedades como esta, puede devolver una instancia de la clase, constructor o enlace de objeto (pero eso no tiene sentido).



//   
function createHuman (...args) {
  return new Human(...args)
}


Dónde definir propiedades y métodos



¿Dónde debería definir propiedades y métodos? ¿Instancia o prototipo?



Mucha gente piensa que los prototipos son mejores para esto.



Sin embargo, realmente no importa.



Al definir propiedades y métodos en una instancia, cada instancia consumirá más memoria. Al definir métodos en prototipos, la memoria se consumirá menos, pero de manera insignificante. Dado el poder de las computadoras modernas, esta diferencia no es significativa. Así que haga lo que le funcione mejor, pero prefiera los prototipos.



Por ejemplo, cuando se utilizan clases o enlaces de objetos, es mejor utilizar prototipos porque facilita la escritura del código. En el caso de FF, no se pueden utilizar prototipos. Solo se pueden definir las propiedades de las instancias.



Aprox. per .: permítanme estar en desacuerdo con el autor. La cuestión de utilizar prototipos en lugar de instancias al definir propiedades y métodos no es solo una cuestión de consumo de memoria, sino sobre todo una cuestión del propósito de la propiedad o método que se está definiendo. Si una propiedad o método debe ser único para cada instancia, debe definirse en la instancia. Si una propiedad o método va a ser el mismo (común) para todas las instancias, debe definirse en el prototipo. En este último caso, si necesita realizar cambios en una propiedad o método, bastará con realizarlos en el prototipo, a diferencia de las propiedades y métodos de instancias, que se ajustan individualmente.



Conclusión preliminar



A partir del material estudiado, se pueden sacar varias conclusiones. Es mi opinion personal.



  • Las clases son mejores que los constructores porque facilitan la definición de varios métodos.
  • El enlace de objetos parece extraño debido a la necesidad de usar Object.create. Seguí olvidándome de esto al estudiar este enfoque. Para mí, esta fue razón suficiente para rechazar un uso posterior.
  • Las clases y los FF son los más fáciles de usar. El problema es que los prototipos no se pueden utilizar en FF. Pero, como señalé antes, realmente no importa.


A continuación, compararemos clases y FF como las dos mejores formas de crear objetos en JavaScript.



Clases vs.FF - Herencia



Antes de pasar a comparar clases y FF, debe familiarizarse con los tres conceptos subyacentes de OOP:



  • herencia
  • encapsulamiento
  • esta


Empecemos por la herencia.



¿Qué es la herencia?


En JavaScript, la herencia significa pasar propiedades de padres a hijos, es decir, de proyecto a instancia.



Esto sucede de dos formas:



  • usando la inicialización de la instancia
  • usando una cadena prototipo


En el segundo caso, el proyecto principal se expande con un proyecto secundario. Esto se llama subclasificación, pero algunos también lo llaman herencia.



Entendiendo las subclases


La subclasificación es cuando un proyecto hijo extiende al padre.



Veamos el ejemplo de clases.



Subclasificar con una clase


La palabra clave "extiende" se usa para extender la clase principal.



class Child extends Parent {
    // ...
}


Por ejemplo, creemos una clase "Desarrollador" que amplíe la clase "Humano".



//  Human
class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


La clase Developer extenderá Human de la siguiente manera:



class Developer extends Human {
  constructor(firstName, lastName) {
    super(firstName, lastName)
  }

    // ...
}


La palabra clave super llama al constructor de la clase Human. Si no lo necesita, se puede omitir super.



class Developer extends Human {
  // ...
}


Digamos que el desarrollador puede escribir código (quién lo hubiera pensado). Agreguemos un método correspondiente.



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


A continuación, se muestra un ejemplo de una instancia de la clase "Desarrollador".



const chris = new Developer('Chris', 'Coyier')
console.log(chris)






Subclasificación con FF


Para crear subclases usando FF, debe realizar 4 pasos:



  • crear un nuevo FF
  • crear una instancia del proyecto principal
  • crear una copia de esta instancia
  • agregar propiedades y métodos a esta copia


Este proceso se ve así.



function Subclass (...args) {
  const instance = ParentClass(...args)
  return Object.assign({}, instance, {
    //   
  })
}


Creemos una subclase "Desarrollador". Así es como se ve el FF "Humano".



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}


Crear desarrollador.



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    //   
  })
}


Agregue el método de "código".



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


Creamos una instancia de Developer.



const chris = Developer('Chris', 'Coyier')
console.log(chris)






Sobrescribir el método principal


A veces es necesario sobrescribir un método principal dentro de una subclase. Esto puede hacerse de la siguiente manera:



  • crea un método con el mismo nombre
  • llamar al método principal (opcional)
  • crear un nuevo método en la subclase


Este proceso se ve así.



class Developer extends Human {
  sayHello () {
    //   
    super.sayHello()

    //   
    console.log(`I'm a developer.`)
  }
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()






El mismo proceso usando FF.



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)

  return Object.assign({}, human, {
      sayHello () {
        //   
        human.sayHello()

        //   
        console.log(`I'm a developer.`)
      }
  })
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()






Herencia versus composición


Una conversación sobre la herencia rara vez pasa sin mencionar la composición. Expertos como Eric Elliot creen que la composición debe usarse siempre que sea posible.



¿Qué es la composición?



Comprender la composición


Básicamente, la composición es la combinación de varias cosas en una. La forma más común y sencilla de combinar objetos es mediante Object.assign.



const one = { one: 'one' }
const two = { two: 'two' }
const combined = Object.assign({}, one, two)


La composición es más fácil de explicar con un ejemplo. Digamos que tenemos dos subclases, Desarrollador y Diseñador. Los diseñadores saben cómo diseñar y los desarrolladores saben cómo escribir código. Ambos heredan de la clase "Humano".



class Human {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class Designer extends Human {
  design (thing) {
    console.log(`${this.firstName} designed ${thing}`)
  }
}

class Developer extends Designer {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


Ahora, suponga que queremos crear una tercera subclase. Esta subclase debe ser una mezcla de diseñador y desarrollador: debe poder diseñar y escribir código. Llamémoslo DesignerDeveloper (o DeveloperDesigner, si lo prefiere).



¿Cómo lo creamos?



No podemos extender las clases "Diseñador" y "Desarrollador" al mismo tiempo. Esto no es posible porque no podemos decidir qué propiedades deben ir primero. A esto se le llama el problema del diamante (herencia del diamante) .







El problema del diamante se puede resolver con Object.assign si le damos prioridad a un objeto sobre el otro. Sin embargo, JavaScript no admite herencia múltiple.



//  
class DesignerDeveloper extends Developer, Designer {
  // ...
}


Aquí es donde la composición resulta útil.



Este enfoque establece lo siguiente: en lugar de subclasificar DesignerDeveloper, cree un objeto que contenga habilidades que pueda subclasificar según sea necesario.



La implementación de este enfoque conduce a lo siguiente.



const skills = {
    code (thing) { /* ... */ },
    design (thing) { /* ... */ },
    sayHello () { /* ... */ }
}


Ya no necesitamos la clase Human, porque podemos crear tres clases diferentes usando el objeto especificado.



Aquí está el código para DesignerDeveloper.



class DesignerDeveloper {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      code: skills.code,
      design: skills.design,
      sayHello: skills.sayHello
    })
  }
}

const chris = new DesignerDeveloper('Chris', 'Coyier')
console.log(chris)






Podemos hacer lo mismo para Designer y Developer.



class Designer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      design: skills.design,
      sayHello: skills.sayHello
    })
  }
}

class Developer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      code: skills.code,
      sayHello: skills.sayHello
    })
  }
}


¿Ha notado que creamos métodos en una instancia? Ésta es solo una de las posibles opciones. También podemos poner métodos en el prototipo, pero lo encuentro innecesario (este enfoque parece que volvemos a los constructores).



class DesignerDeveloper {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design,
  sayHello: skills.sayHello
})






Utilice el enfoque que más le convenga. El resultado será el mismo.



Composición con FF


La composición con FF consiste en agregar métodos distribuidos al objeto devuelto.



function DesignerDeveloper (firstName, lastName) {
  return {
    firstName,
    lastName,
    code: skills.code,
    design: skills.design,
    sayHello: skills.sayHello
  }
}






Herencia y composición


Nadie dijo que no podemos usar herencia y composición al mismo tiempo.



Volviendo a los ejemplos de Designer, Developer y DesignerDeveloper, cabe señalar que también son humanos. Por lo tanto, pueden extender la clase humana.



A continuación, se muestra un ejemplo de herencia y composición utilizando la sintaxis de clase.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class DesignerDeveloper extends Human {}
Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design
})






Y aquí ocurre lo mismo con el uso de FF.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}

function DesignerDeveloper (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code: skills.code,
    design: skills.design
  })
}






Subclases en el mundo real


Si bien muchos expertos sostienen que la composición es más flexible (y por lo tanto más útil) que las subclases, las subclases no deben descartarse. Muchas de las cosas con las que nos enfrentamos se basan en esta estrategia.



Por ejemplo: el evento "click" es un MouseEvent. MouseEvent es una subclase de UIEvent (evento de interfaz de usuario), que a su vez es una subclase de Event (evento).







Otro ejemplo: los elementos HTML son subclases de nodos. Por tanto, pueden utilizar todas las propiedades y métodos de los nodos.







Conclusión preliminar sobre herencia


La herencia y la composición se pueden usar en ambas clases y FF. En FF, la composición parece "más limpia", pero esto es una ligera ventaja sobre las clases.



Continuemos la comparación.



Clases vs.FF - Encapsulación



Básicamente, la encapsulación consiste en esconder una cosa dentro de otra, haciendo que la esencia interior sea inaccesible desde el exterior.



En JavaScript, las entidades ocultas son variables y funciones que solo están disponibles en el contexto actual. En este caso, el contexto es lo mismo que el alcance.



Encapsulación simple


La forma más simple de encapsulación es un bloque de código.



{
  // ,  ,     
}


Mientras está en un bloque, puede acceder a una variable declarada fuera de él.



const food = 'Hamburger'

{
  console.log(food)
}






Pero no al revés.



{
  const food = 'Hamburger'
}

console.log(food)






Tenga en cuenta que las variables declaradas con la palabra clave "var" tienen un alcance global o funcional. Intente no usar var para declarar variables.



Encapsulación con función


El alcance funcional es similar al alcance del bloque. Las variables declaradas en una función son accesibles solo dentro de ella. Esto se aplica a todas las variables, incluso a las declaradas con var.



function sayFood () {
  const food = 'Hamburger'
}

sayFood()
console.log(food)






Cuando estamos dentro de una función, tenemos acceso a las variables declaradas fuera de ella.



const food = 'Hamburger'

function sayFood () {
  console.log(food)
}

sayFood()






Las funciones pueden devolver valores que se pueden usar más adelante fuera de la función.



function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())






Cierre


El cierre es una forma avanzada de encapsulación. Es solo una función dentro de otra función.



//  
function outsideFunction () {
  function insideFunction () { /* ... */ }
}




Las variables declaradas en outsideFunction se pueden usar en insideFunction.



function outsideFunction () {
  const food = 'Hamburger'
  console.log('Called outside')

  return function insideFunction () {
    console.log('Called inside')
    console.log(food)
  }
}

//  outsideFunction,   insideFunction
//  insideFunction   "fn"
const fn = outsideFunction()






Encapsulación y OOP


Al crear objetos, queremos que algunas propiedades sean públicas (públicas) y otras privadas (privadas o privadas).



Veamos un ejemplo. Digamos que tenemos un proyecto de automóvil. Al crear una nueva instancia, le agregamos una propiedad "combustible" con un valor de 50.



class Car {
  constructor () {
    this.fuel = 50
  }
}




Los usuarios pueden utilizar esta propiedad para determinar la cantidad de combustible restante.



const car = new Car()
console.log(car.fuel) // 50




Los usuarios también pueden establecer la cantidad de combustible por sí mismos.



const car = new Car()
car.fuel = 3000
console.log(car.fuel) // 3000


Agreguemos la condición de que el tanque del automóvil contenga un máximo de 100 litros de combustible. No queremos que los usuarios puedan configurar la cantidad de combustible por sí mismos, porque pueden romper el automóvil.



Hay dos maneras de hacer esto:



  • uso de propiedades privadas por convención
  • usando campos privados reales


Propiedades privadas por convenio


En JavaScript, las propiedades y las variables privadas generalmente se indican con un guión bajo.



class Car {
  constructor () {
    //   "fuel"  ,       
    this._fuel = 50
  }
}


Normalmente, creamos métodos para administrar propiedades privadas.



class Car {
  constructor () {
    this._fuel = 50
  }

  getFuel () {
    return this._fuel
  }

  setFuel (value) {
    this._fuel = value
    //   
    if (value > 100) this._fuel = 100
  }
}


Los usuarios deben utilizar los métodos getFuel y setFuel para determinar y establecer la cantidad de combustible, respectivamente.



const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


Pero la variable "_fuel" no es realmente privada. Es accesible desde el exterior.



const car = new Car()
console.log(car.getFuel()) // 50

car._fuel = 3000
console.log(car.getFuel()) // 3000


Utilice campos privados reales para restringir el acceso a las variables.



Campos verdaderamente privados


Campos es el término utilizado para combinar variables, propiedades y métodos.



Campos de clases privadas


Las clases te permiten crear variables privadas usando el prefijo "#".



class Car {
  constructor () {
    this.#fuel = 50
  }
}


Desafortunadamente, este prefijo no se puede usar en el constructor.







Las variables privadas deben definirse fuera del constructor.



class Car {
  //   
  #fuel
  constructor () {
    //  
    this.#fuel = 50
  }
}


En este caso, podemos inicializar la variable cuando se defina.



class Car {
  #fuel = 50
}


Ahora la variable "#fuel" solo está disponible dentro de la clase. Intentar acceder a él fuera de la clase arrojará un error.



const car = new Car()
console.log(car.#fuel)






Necesitamos métodos apropiados para manipular la variable.



class Car {
  #fuel = 50

  getFuel () {
    return this.#fuel
  }

  setFuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


Personalmente, prefiero usar getters y setters para esto. Encuentro esta sintaxis más legible.



class Car {
  #fuel = 50

  get fuel () {
    return this.#fuel
  }

  set fuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


Campos privados FF


Los FF crean campos privados automáticamente. Solo necesitamos declarar una variable. Los usuarios no podrán acceder a esta variable desde fuera. Esto se debe al hecho de que las variables tienen alcance de bloque (o funcional), es decir están encapsulados de forma predeterminada.



function Car () {
  const fuel = 50
}

const car = new Car()
console.log(car.fuel) // undefined
console.log(fuel) // Error: "fuel" is not defined


Los getters y setters también se utilizan para controlar la variable privada "combustible".



function Car () {
  const fuel = 50

  return {
    get fuel () {
      return fuel
    },

    set fuel (value) {
      fuel = value
      if (value > 100) fuel = 100
    }
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


Me gusta esto. ¡Simple y fácilmente!



Conclusión preliminar sobre encapsulación


La encapsulación FF es más simple y más fácil de entender. Se basa en el alcance, que es una parte importante de JavaScript.



La encapsulación de clases implica el uso del prefijo "#", que puede resultar algo tedioso.



Clases contra FF - esto



este es el principal argumento en contra del uso de clases. ¿Por qué? Porque el significado de esto depende de dónde y cómo se use. Este comportamiento a menudo es confuso no solo para los principiantes, sino también para los desarrolladores experimentados.



Sin embargo, el concepto de esto en realidad no es tan difícil. Hay 6 contextos en total en los que se puede utilizar. Si comprende estos contextos, no debería tener ningún problema con esto.



Los contextos nombrados son:



  • contexto global
  • contexto del objeto que se está creando
  • el contexto de una propiedad o método de un objeto
  • función simple
  • función de flecha
  • contexto del controlador de eventos


Pero volvamos al artículo. Veamos los detalles de usar esto en clases y FF.



Usando esto en clases


Cuando se usa en una clase, esto apunta a la instancia que se está creando (contexto de propiedad / método). Es por eso que la instancia se inicializa en constructor.



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    console.log(this)
  }
}

const chris = new Human('Chris', 'Coyier')






Usando esto en funciones constructoras


Cuando use esto dentro de una función y sea nuevo para crear una instancia, esto apuntará a la instancia.



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = new Human('Chris', 'Coyier')






A diferencia de FK en FF, esto apunta a la ventana (en el contexto del módulo, esto generalmente tiene el valor "indefinido").



//        "new"
function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = Human('Chris', 'Coyier')






Por lo tanto, esto no debe usarse en FF. Esta es una de las principales diferencias entre FF y FC.



Usando esto en FF


Para poder usar esto en FF, es necesario crear un contexto de propiedad / método.



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayThis () {
      console.log(this)
    }
  }
}

const chris = Human('Chris', 'Coyier')
chris.sayThis()






Aunque podemos usar esto en FF, no lo necesitamos. Podemos crear una variable que apunte a la instancia. Se puede utilizar una variable de este tipo en lugar de esto.



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${human.firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()


human.firstName es más preciso que this.firstName porque humano apunta explícitamente a una instancia.



De hecho, ni siquiera necesitamos escribir human.firstName. Podemos limitarnos a firstName, ya que esta variable tiene un alcance léxico (esto es cuando el valor de la variable se toma del entorno externo).



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()






Veamos un ejemplo más complejo.



Ejemplo complejo



Las condiciones son las siguientes: tenemos un proyecto "Human" con propiedades "firstName" y "lastName" y un método "sayHello".



También tenemos un proyecto "Desarrollador" que hereda de Human. Los desarrolladores saben cómo escribir código, por lo que deben tener un método de "código". Además, deben declarar que pertenecen a la casta de desarrolladores, por lo que debemos sobrescribir el método sayHello.



Implementemos la lógica especificada usando clases y FF.



Clases


Creamos un proyecto "Humano".



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastname = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


Cree un proyecto "Desarrollador" con el método "código".



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


Sobrescribimos el método "sayHello".



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }

  sayHello () {
    super.sayHello()
    console.log(`I'm a developer`)
  }
}


FF (usando esto)


Creamos un proyecto "Humano".



function Human () {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}


Cree un proyecto "Desarrollador" con el método "código".



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


Sobrescribimos el método "sayHello".



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    },

    sayHello () {
      human.sayHello()
      console.log('I\'m a developer')
    }
  })
}


Ff (sin esto)


Dado que firstName tiene un ámbito léxico directo, podemos omitir esto.



function Human (firstName, lastName) {
  return {
    // ...
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}

function Developer (firstName, lastName) {
  // ...
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${firstName} coded ${thing}`)
    },

    sayHello () { /* ... */ }
  })
}


Conclusión preliminar sobre este


En palabras simples, las clases requieren el uso de esto, pero los FF no. En este caso, prefiero usar FF porque:



  • este contexto puede cambiar
  • el código escrito con FF es más corto y más limpio (también debido a la encapsulación automática de variables)


Clases vs.FF - Controladores de eventos



Muchos artículos sobre OOP pasan por alto el hecho de que, como desarrolladores frontend, estamos constantemente lidiando con controladores de eventos. Proporcionan interacción con los usuarios.



Dado que los controladores de eventos cambian este contexto, trabajar con ellos en clases puede ser problemático. Al mismo tiempo, tales problemas no surgen en FF.



Sin embargo, cambiar este contexto no importa si sabemos cómo manejarlo. Veamos un ejemplo sencillo.



Crear contador


Para crear un contador, utilizaremos el conocimiento adquirido, incluidas las variables privadas.



Nuestro contador contendrá dos cosas:



  • el contador en sí
  • botón para aumentar su valor






Así es como podría verse el marcado:



<div class="counter">
  <p>Count: <span>0</span></p>
  <button>Increase Count</button>
</div>


Creando un contador usando una clase


Para facilitar las cosas, pida al usuario que busque y pase el marcado de contador a la clase Contador:



class Counter {
  constructor (counter) {
    // ...
  }
}

// 
const counter = new Counter(document.querySelector('.counter'))


Necesitas obtener 2 elementos en la clase:



  • <span> que contiene el valor del contador; necesitamos actualizar este valor cuando el contador aumenta
  • <botón>: necesitamos agregar un controlador para los eventos llamados por este elemento


class Counter {
  constructor (counter) {
    this.countElement = counter.querySelector('span')
    this.buttonElement = counter.querySelector('button')
  }
}


A continuación, inicializamos la variable "count" con el contenido de texto de countElement. La variable especificada debe ser privada.



class Counter {
  #count
  constructor (counter) {
    // ...

    this.#count = parseInt(countElement.textContent)
  }
}


Cuando se presiona el botón, el valor del contador debe aumentar en 1. Implementamos esto usando el método "IncrementarCount".



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
  }
}


Ahora necesitamos actualizar el DOM. Implementemos esto usando el método "updateCount" llamado dentro de enlargeCount:



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
    this.updateCount()
  }

  updateCount () {
    this.countElement.textContent = this.#count
  }
}


Queda por agregar un controlador de eventos.



Agregar un controlador de eventos


Agreguemos un controlador a this.buttonElement. Desafortunadamente, no podemos usar IncrementCount como función de devolución de llamada. Esto resultará en un error.



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  // 
}






Se lanza la excepción porque apunta a buttonElement (contexto del controlador de eventos). Puede verificar esto imprimiendo este valor en la consola.







Este valor debe cambiarse para que apunte a la instancia. Esto se puede hacer de dos maneras:



  • usando bind
  • usando la función de flecha


La mayoría usa el primer método (pero el segundo es más simple).



Agregar un controlador de eventos con bind


bind devuelve una nueva función. Como primer argumento, se pasa un objeto al que apuntará (al que estará vinculado).



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount.bind(this))
  }

  // ...
}


Funciona, pero no se ve bien. Además, vincular es una función avanzada que es difícil de manejar para los principiantes.



Funciones de flecha


Las funciones de flecha, entre otras cosas, no tienen esto propio. Lo toman prestado del entorno léxico (externo). Por lo tanto, el código del contador se puede reescribir de la siguiente manera:



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', () => {
      this.increaseCount()
    })
  }

  // 
}


Existe una forma aún más sencilla. Podemos crear growthCount como una función de flecha. En este caso, apuntará a la instancia.



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  increaseCount = () => {
    this.#count = this.#count + 1
    this.updateCounter()
  }

  // ...
}


El código


Aquí está el código de ejemplo completo:







Creando un contador usando FF


El comienzo es similar: le pedimos al usuario que busque y pase el marcado del contador:



function Counter (counter) {
  // ...
}

const counter = Counter(document.querySelector('.counter'))


Obtenemos los elementos necesarios, que serán privados por defecto:



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')
}


Inicialicemos la variable "count":



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')

  let count = parseInt(countElement.textContext)
}


El valor del contador se incrementará utilizando el método "IncrementCount". Puede usar una función regular, pero prefiero un enfoque diferente:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
    }
  }
}


El DOM se actualizará utilizando el método "updateCount" que se llama dentro de enlargeCount:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
      counter.updateCount()
    },

    updateCount () {
      increaseCount()
    }
  }
}


Tenga en cuenta que estamos usando counter.updateCount en lugar de this.updateCount.



Agregar un controlador de eventos


Podemos agregar un controlador de eventos al buttonElement usando counter.increaseCount como devolución de llamada.



Esto funcionará ya que no estamos usando esto, por lo que no nos importa que el controlador cambie el contexto de esto.



function Counter (counterElement) {
  // 

  // 
  const counter = { /* ... */ }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


La primera característica de este


Puede usar esto en FF, pero solo en el contexto de un método.



En el siguiente ejemplo, llamar a counter.increaseCount llamará a counter.updateCount porque esto apunta a counter:



function Counter (counterElement) {
  // 

  // 
  const counter = {
    increaseCount() {
      count = count + 1
      this.updateCount()
    }
  }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


Sin embargo, el controlador de eventos no funcionará porque este valor ha cambiado. Este problema se puede resolver con bind, pero no con las funciones de flecha.



La segunda característica de este


Cuando usamos la sintaxis FF, no podemos crear métodos en forma de funciones de flecha, porque los métodos se crean en el contexto de una función, es decir esto apuntará a la ventana:



function Counter (counterElement) {
  // ...
  const counter = {
    //   
    //  ,  this   window
    increaseCount: () => {
      count = count + 1
      this.updateCount()
    }
  }
  // ...
}


Por lo tanto, cuando use FF, le recomiendo que evite usarlo.



El código








Veredicto del controlador de eventos


Los controladores de eventos cambian el valor de esto, así que utilícelo con mucho cuidado. Cuando use clases, le aconsejo que cree devoluciones de llamada del controlador de eventos en forma de funciones de flecha. Entonces no tiene que utilizar los servicios de enlace.



Al usar FF, recomiendo prescindir de esto en absoluto.



Conclusión



Entonces, en este artículo, analizamos cuatro formas de crear objetos en JavaScript:



  • Funciones constructoras
  • Clases
  • Vincular objetos
  • Funciones de fábrica


Primero, llegamos a la conclusión de que las clases y los FF son las formas más óptimas de crear objetos.



En segundo lugar, vimos que las subclases son más fáciles de crear con clases. Sin embargo, en el caso de la composición, es mejor usar FF.



En tercer lugar, resumimos que cuando se trata de encapsulación, los FF tienen una ventaja sobre las clases, ya que estas últimas requieren el uso de un prefijo especial "#", y los FF hacen que las variables sean privadas automáticamente.



En cuarto lugar, los FF le permiten hacerlo sin usar esto como referencia de instancia. En las clases, debe recurrir a algunos trucos para devolver esto al contexto original cambiado por el controlador de eventos.



Eso es todo para mi. Espero que hayas disfrutado el artículo. Gracias por su atención.



All Articles