JavaScript orienté objet en termes simples





Bonne journée, mes amis!



Il existe 4 façons en JavaScript de créer un objet:



  • Fonction constructeur
  • Classe (classe)
  • Objet lié à un autre objet (OLOO)
  • Fonction d'usine


Quelle méthode devriez-vous utiliser? Lequel est le meilleur?



Afin de répondre à ces questions, nous allons non seulement considérer chaque approche séparément, mais également comparer les classes et les fonctions de fabrique selon les critères suivants: héritage, encapsulation, le mot-clé "this", gestionnaires d'événements.



Commençons par ce qu'est la programmation orientée objet (POO).



Qu'est-ce que la POO?



Essentiellement, la POO est un moyen d'écrire du code qui vous permet de créer des objets à l'aide d'un seul objet. C'est également l'essence du modèle de conception Constructor. Un objet partagé est généralement appelé blueprint, blueprint ou blueprint, et les objets qu'il crée sont des instances.



Chaque instance possède à la fois des propriétés héritées du parent et des propriétés qui lui sont propres. Par exemple, si nous avons un projet humain, nous pouvons créer des instances avec des noms différents basés sur celui-ci.



Le deuxième aspect de la POO est la structuration du code lorsque nous avons plusieurs projets de niveaux différents. C'est ce qu'on appelle l'héritage ou le sous-classement.



Le troisième aspect de la POO est l'encapsulation, lorsque nous cachons les détails d'implémentation aux étrangers, rendant les variables et les fonctions inaccessibles de l'extérieur. C'est l'essence des modèles de conception de module et de façade.



Passons aux méthodes de création d'objets.



Méthodes de création d'objets



Fonction constructeur


Les constructeurs sont des fonctions qui utilisent le mot clé "this".



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


cela vous permet de stocker et d'accéder aux valeurs uniques de l'instance en cours de création. Les instances sont créées à l'aide du mot-clé "new".



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


Classe


Les classes sont une abstraction ("sucre syntaxique") sur les fonctions constructeurs. Ils facilitent la création d'instances.



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


Notez que le constructeur contient le même code que la fonction constructeur ci-dessus. Nous devons le faire pour initialiser cela. Nous pouvons omettre le constructeur si nous n'avons pas besoin d'attribuer des valeurs initiales.



À première vue, les classes semblent être plus complexes que les constructeurs - vous devez écrire plus de code. Tenez vos chevaux et ne sautez pas aux conclusions. Les cours sont cool. Vous comprendrez pourquoi un peu plus tard.



Les instances sont également créées à l'aide du mot-clé "new".



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

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


Lier des objets


Cette méthode de création d'objets a été proposée par Kyle Simpson. Dans cette approche, nous définissons le projet comme un objet ordinaire. Ensuite, en utilisant une méthode (qui est généralement appelée init, mais ce n'est pas obligatoire, contrairement au constructeur de la classe), nous initialisons l'instance.



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


Object.create est utilisé pour créer une instance. Après l'instanciation, init est appelé.



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

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


Le code peut être amélioré un peu en renvoyant ceci à init.



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

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


Fonction d'usine


Une fonction de fabrique est une fonction qui renvoie un objet. Tout objet peut être retourné. Vous pouvez même renvoyer une instance d'une classe ou des liaisons d'objet.



Voici un exemple simple de fonction d'usine.



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


Nous n'avons pas besoin du mot clé "this" pour créer une instance. Nous appelons simplement la fonction.



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

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


Voyons maintenant comment ajouter des propriétés et des méthodes.



Définition des propriétés et des méthodes



Les méthodes sont des fonctions déclarées comme propriétés d'un objet.



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


En POO, il existe deux façons de définir des propriétés et des méthodes:



  • Dans une instance
  • En prototype


Définition des propriétés et des méthodes dans le constructeur


Pour définir une propriété sur une instance, vous devez l'ajouter à la fonction constructeur. Assurez-vous d'ajouter la propriété à cela.



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)






Les méthodes sont généralement définies dans le prototype, car cela évite de créer une fonction pour chaque instance, i.e. Permet à toutes les instances de partager une seule fonction (appelée fonction partagée ou distribuée).



Pour ajouter une propriété au prototype, utilisez prototype.



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

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






La création de plusieurs méthodes peut être fastidieuse.



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


Vous pouvez vous simplifier la vie avec Object.assign.



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


Définition des propriétés et des méthodes dans une classe


Les propriétés d'instance peuvent être définies dans le constructeur.



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

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






Les propriétés du prototype sont définies après le constructeur comme une fonction normale.



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

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






La création de plusieurs méthodes dans une classe est plus facile que dans un constructeur. Nous n'avons pas besoin d'Object.assign pour cela. Nous ajoutons simplement d'autres fonctionnalités.



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

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


Définition des propriétés et des méthodes lors de la liaison d'objets


Pour définir les propriétés d'une instance, nous y ajoutons une propriété.



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)






La méthode prototype est définie comme un objet régulier.



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






Définition des propriétés et des méthodes dans les fonctions d'usine (FF)


Les propriétés et méthodes peuvent être incluses dans l'objet renvoyé.



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






Lorsque vous utilisez FF, vous ne pouvez pas définir les propriétés du prototype. Si vous avez besoin de propriétés comme celle-ci, vous pouvez renvoyer une instance de la classe, du constructeur ou des liaisons d'objet (mais cela n'a pas de sens).



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


Où définir les propriétés et les méthodes



Où devez-vous définir les propriétés et les méthodes? Instance ou prototype?



Beaucoup de gens pensent que les prototypes sont meilleurs pour cela.



Cependant, cela n'a pas vraiment d'importance.



En définissant des propriétés et des méthodes sur une instance, chaque instance consommera plus de mémoire. Lors de la définition de méthodes dans les prototypes, la mémoire sera moins consommée, mais de manière insignifiante. Compte tenu de la puissance des ordinateurs modernes, cette différence n'est pas significative. Alors faites ce qui vous convient le mieux, mais préférez toujours les prototypes.



Par exemple, lors de l'utilisation de classes ou de liaisons d'objets, il est préférable d'utiliser des prototypes car cela facilite l'écriture du code. Dans le cas de FF, les prototypes ne peuvent pas être utilisés. Seules les propriétés des instances peuvent être définies.



Environ. per.: Je me permettrai d'être en désaccord avec l'auteur. Le problème de l'utilisation de prototypes au lieu d'instances lors de la définition des propriétés et des méthodes n'est pas seulement une question de consommation de mémoire, mais surtout une question de finalité de la propriété ou de la méthode définie. Si une propriété ou une méthode doit être unique pour chaque instance, elle doit être définie sur l'instance. Si une propriété ou une méthode doit être la même (commune) pour toutes les instances, elle doit être définie dans le prototype. Dans ce dernier cas, si vous devez apporter des modifications à une propriété ou une méthode, il suffira de les apporter au prototype, contrairement aux propriétés et méthodes des instances, qui sont ajustées individuellement.



Conclusion préliminaire



Sur la base du matériel étudié, plusieurs conclusions peuvent être tirées. C'est mon opinion personnelle.



  • Les classes sont meilleures que les constructeurs car elles facilitent la définition de plusieurs méthodes.
  • La liaison d'objet semble étrange en raison de la nécessité d'utiliser Object.create. J'ai continué à oublier cela en étudiant cette approche. Pour moi, c'était une raison suffisante pour refuser une utilisation ultérieure.
  • Les classes et les FF sont les plus faciles à utiliser. Le problème est que les prototypes ne peuvent pas être utilisés dans FF. Mais, comme je l'ai noté plus tôt, cela n'a pas vraiment d'importance.


Ensuite, nous comparerons les classes et les FF comme les deux meilleurs moyens de créer des objets en JavaScript.



Classes vs FF - Héritage



Avant de passer à la comparaison des classes et des FF, vous devez vous familiariser avec les trois concepts sous-jacents à la POO:



  • héritage
  • encapsulation
  • cette


Commençons par l'héritage.



Qu'est-ce que l'héritage?


En JavaScript, l'héritage signifie passer des propriétés de parent à enfant, c'est-à-dire du projet à l'instance.



Cela se produit de deux manières:



  • utilisation de l'initialisation d'instance
  • en utilisant une chaîne prototype


Dans le second cas, le projet parent est développé avec un projet enfant. C'est ce qu'on appelle le sous-classement, mais certains l'appellent aussi l'héritage.



Comprendre le sous-classement


Le sous-classement se produit lorsqu'un projet enfant étend le parent.



Regardons l'exemple des classes.



Sous-classement avec une classe


Le mot clé "extend" est utilisé pour étendre la classe parente.



class Child extends Parent {
    // ...
}


Par exemple, créons une classe "Developer" qui étend la classe "Human".



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

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


La classe Developer étendra Human comme suit:



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

    // ...
}


Le mot clé "super" appelle le constructeur de la classe "Human". Si vous n'en avez pas besoin, super peut être omis.



class Developer extends Human {
  // ...
}


Disons que le développeur peut écrire du code (qui aurait pensé). Ajoutons-y une méthode correspondante.



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


Voici un exemple d'instance de la classe "Developer".



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






Sous-classement avec FF


Pour créer des sous-classes à l'aide de FF, vous devez effectuer 4 étapes:



  • créer un nouveau FF
  • créer une instance du projet parent
  • créer une copie de cette instance
  • ajouter des propriétés et des méthodes à cette copie


Ce processus ressemble à ceci.



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


Créons une sous-classe "Développeur". Voici à quoi ressemble le FF "Human".



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


Créer un développeur.



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


Ajoutez-y la méthode "code".



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


Nous créons une instance de Developer.



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






Écraser la méthode parente


Parfois, il devient nécessaire de remplacer une méthode parente dans une sous-classe. Cela peut être fait comme suit:



  • créer une méthode avec le même nom
  • appeler la méthode parent (facultatif)
  • créer une nouvelle méthode dans la sous-classe


Ce processus ressemble à ceci.



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

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

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






Le même processus en utilisant 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()






Héritage contre composition


Une conversation sur l'héritage va rarement sans mentionner la composition. Des experts comme Eric Elliot estiment que la composition doit être utilisée autant que possible.



Qu'est-ce que la composition?



Comprendre la composition


Fondamentalement, la composition est la combinaison de plusieurs choses en un seul. Le moyen le plus courant et le plus simple de combiner des objets consiste à utiliser Object.assign.



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


La composition est plus facile à expliquer avec un exemple. Disons que nous avons deux sous-classes, Developer et Designer. Les concepteurs savent concevoir et les développeurs savent écrire du code. Les deux héritent de la classe "Human".



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


Maintenant, supposons que nous voulions créer une troisième sous-classe. Cette sous-classe doit être un mélange de concepteur et de développeur - elle doit être capable à la fois de concevoir et d'écrire du code. Appelons-le DesignerDeveloper (ou DeveloperDesigner, si vous préférez).



Comment le créons-nous?



Nous ne pouvons pas étendre les classes "Designer" et "Developer" en même temps. Ce n'est pas possible car nous ne pouvons pas décider quelles propriétés doivent venir en premier. C'est ce qu'on appelle le problème du diamant (héritage du diamant) .







Le problème du diamant peut être résolu avec Object.assign si nous donnons la priorité à un objet sur l'autre. Cependant, JavaScript ne prend pas en charge l'héritage multiple.



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


C'est là que la composition est utile.



Cette approche indique ce qui suit: au lieu de sous-classer DesignerDeveloper, créez un objet contenant des compétences que vous pouvez sous-classer selon vos besoins.



La mise en œuvre de cette approche conduit à ce qui suit.



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


Nous n'avons plus besoin de la classe Human, car nous pouvons créer trois classes différentes en utilisant l'objet spécifié.



Voici le code de 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)






Nous pouvons faire de même pour le concepteur et le développeur.



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


Avez-vous remarqué que nous créons des méthodes sur une instance? Ce n'est qu'une des options possibles. Nous pouvons également mettre des méthodes dans le prototype, mais je trouve cela inutile (cette approche semble être de retour aux constructeurs).



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

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






Utilisez l'approche qui vous convient le mieux. Le résultat sera le même.



Composition avec FF


La composition avec FF consiste à ajouter des méthodes distribuées à l'objet retourné.



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






Héritage et composition


Personne n'a dit que nous ne pouvions pas utiliser l'héritage et la composition en même temps.



Pour en revenir aux exemples Designer, Developer et DesignerDeveloper, il convient de noter qu'ils sont également humains. Par conséquent, ils peuvent étendre la classe humaine.



Voici un exemple d'héritage et de composition utilisant la syntaxe de classe.



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






Et voici la même chose avec l'utilisation 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
  })
}






Sous-classes dans le monde réel


Alors que de nombreux experts affirment que la composition est plus flexible (et donc plus utile) que les sous-classes, les sous-classes ne doivent pas être écartées. Beaucoup de choses que nous traitons sont basées sur cette stratégie.



Par exemple: l'événement "click" est un MouseEvent. MouseEvent est une sous-classe de UIEvent (événement d'interface utilisateur), qui à son tour est une sous-classe de Event (événement).







Autre exemple: les éléments HTML sont des sous-classes de nœuds. Par conséquent, ils peuvent utiliser toutes les propriétés et méthodes des nœuds.







Conclusion préliminaire concernant l'héritage


L'héritage et la composition peuvent être utilisés dans les deux classes et FF. En FF, la composition semble "plus propre", mais c'est un léger avantage par rapport aux classes.



Continuons la comparaison.



Classes vs FF - Encapsulation



Fondamentalement, l'encapsulation consiste à cacher une chose à l'intérieur d'une autre, rendant l'essence intérieure inaccessible de l'extérieur.



En JavaScript, les entités masquées sont des variables et des fonctions qui ne sont disponibles que dans le contexte actuel. Dans ce cas, le contexte est le même que la portée.



Encapsulation simple


La forme la plus simple d'encapsulation est un bloc de code.



{
  // ,  ,     
}


Dans un bloc, vous pouvez accéder à une variable déclarée en dehors de celui-ci.



const food = 'Hamburger'

{
  console.log(food)
}






Mais pas l'inverse.



{
  const food = 'Hamburger'
}

console.log(food)






Notez que les variables déclarées avec le mot-clé "var" ont une portée globale ou fonctionnelle. Essayez de ne pas utiliser var pour déclarer des variables.



Encapsulation avec une fonction


La portée fonctionnelle est similaire à la portée de bloc. Les variables déclarées dans une fonction ne sont accessibles qu'à l'intérieur de celle-ci. Cela s'applique à toutes les variables, même celles déclarées avec var.



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

sayFood()
console.log(food)






Lorsque nous sommes à l'intérieur d'une fonction, nous avons accès aux variables déclarées en dehors de celle-ci.



const food = 'Hamburger'

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

sayFood()






Les fonctions peuvent renvoyer des valeurs qui peuvent être utilisées ultérieurement en dehors de la fonction.



function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())






Fermeture


La fermeture est une forme avancée d'encapsulation. C'est juste une fonction à l'intérieur d'une autre fonction.



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




Les variables déclarées dans outsideFunction peuvent être utilisées dans 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()






Encapsulation et POO


Lors de la création d'objets, nous voulons que certaines propriétés soient publiques (publiques) et d'autres privées (privées ou privées).



Regardons un exemple. Disons que nous avons un projet de voiture. Lors de la création d'une nouvelle instance, nous lui ajoutons une propriété "fuel" avec une valeur de 50.



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




Les utilisateurs peuvent utiliser cette propriété pour déterminer la quantité de carburant restante.



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




Les utilisateurs peuvent également définir eux-mêmes la quantité de carburant.



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


Ajoutons la condition que le réservoir de la voiture contienne au maximum 100 litres de carburant. Nous ne voulons pas que les utilisateurs puissent régler eux-mêmes la quantité de carburant, car ils peuvent casser la voiture.



Il y a deux façons de faire ça:



  • utilisation de propriétés privées par convention
  • en utilisant de vrais champs privés


Propriétés privées par accord


En JavaScript, les variables privées et les propriétés sont généralement indiquées par un trait de soulignement.



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


En règle générale, nous créons des méthodes pour gérer les propriétés privées.



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

  getFuel () {
    return this._fuel
  }

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


Les utilisateurs doivent utiliser les méthodes getFuel et setFuel pour déterminer et régler la quantité de carburant, respectivement.



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

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


Mais la variable "_fuel" n'est pas vraiment privée. Il est accessible de l'extérieur.



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

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


Utilisez de vrais champs privés pour restreindre l'accès aux variables.



Des champs vraiment privés


Les champs sont le terme utilisé pour combiner des variables, des propriétés et des méthodes.



Champs de cours privés


Les classes vous permettent de créer des variables privées en utilisant le préfixe "#".



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


Malheureusement, ce préfixe ne peut pas être utilisé dans le constructeur.







Les variables privées doivent être définies en dehors du constructeur.



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


Dans ce cas, nous pouvons initialiser la variable lorsqu'elle est définie.



class Car {
  #fuel = 50
}


Désormais, la variable "#fuel" n'est disponible qu'à l'intérieur de la classe. Essayer d'y accéder en dehors de la classe générera une erreur.



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






Nous avons besoin de méthodes appropriées pour manipuler 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


Personnellement, je préfère utiliser des getters et des setters pour cela. Je trouve cette syntaxe plus lisible.



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


Champs privés FF


Les FF créent automatiquement des champs privés. Nous avons juste besoin de déclarer une variable. Les utilisateurs ne pourront pas accéder à cette variable de l'extérieur. Cela est dû au fait que les variables ont une portée de bloc (ou fonctionnelle), c'est-à-dire sont encapsulés par défaut.



function Car () {
  const fuel = 50
}

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


Les getters et les setters sont également utilisés pour contrôler la variable privée «fuel».



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


Comme ça. Simplement et facilement!



Conclusion préliminaire concernant l'encapsulation


L'encapsulation FF est plus simple et plus facile à comprendre. Il est basé sur la portée, qui est une partie importante de JavaScript.



L'encapsulation de classe implique l'utilisation du préfixe "#", ce qui peut être quelque peu fastidieux.



Classes contre FF - ce



c'est le principal argument contre l'utilisation des classes. Pourquoi? Parce que la signification de cela dépend de l'endroit et de la manière dont cela est utilisé. Ce comportement est souvent déroutant non seulement pour les débutants, mais aussi pour les développeurs expérimentés.



Cependant, le concept de ceci n'est en fait pas si difficile. Il y a 6 contextes au total dans lesquels cela peut être utilisé. Si vous comprenez ces contextes, vous ne devriez pas avoir de problèmes avec cela.



Les contextes nommés sont:



  • contexte global
  • contexte de l'objet en cours de création
  • le contexte d'une propriété ou d'une méthode d'un objet
  • fonction simple
  • fonction flèche
  • contexte du gestionnaire d'événements


Mais revenons à l'article. Regardons les spécificités de son utilisation dans les classes et les FF.



Utiliser ceci dans les classes


Lorsqu'il est utilisé dans une classe, cela pointe vers l'instance en cours de création (contexte de propriété / méthode). C'est pourquoi l'instance est initialisée dans le constructeur.



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

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






Utilisation de ceci dans les fonctions de constructeur


Lorsque vous l'utilisez dans une fonction et new pour créer une instance, cela pointera vers l'instance.



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

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






Contrairement à FK dans FF, cela pointe vers window (dans le contexte du module, cela a généralement la valeur "indéfini").



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

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






Par conséquent, cela ne doit pas être utilisé dans FF. C'est l'une des principales différences entre FF et FC.



Utiliser ceci dans FF


Afin de pouvoir l'utiliser dans FF, il est nécessaire de créer un contexte propriété / méthode.



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

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






Même si nous pouvons l'utiliser dans FF, nous n'en avons pas besoin. Nous pouvons créer une variable pointant vers l'instance. Une telle variable peut être utilisée à la place de cela.



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 est plus précis que this.firstName car human pointe explicitement vers une instance.



En fait, nous n'avons même pas besoin d'écrire human.firstName. On peut se limiter à firstName, puisque cette variable a une portée lexicale (c'est à ce moment que la valeur de la variable est extraite de l'environnement externe).



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

  return human
}

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






Regardons un exemple plus complexe.



Exemple complexe



Les conditions sont les suivantes: nous avons un projet «Human» avec les propriétés «firstName» et «lastName» et une méthode «sayHello».



Nous avons également un projet "Developer" qui hérite de Human. Les développeurs savent écrire du code, ils doivent donc avoir une méthode "code". De plus, ils doivent déclarer qu'ils sont dans la caste des développeurs, nous devons donc remplacer la méthode sayHello.



Implémentons la logique spécifiée en utilisant des classes et FF.



Des classes


Nous créons un projet "Humain".



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

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


Créez un projet "Développeur" avec la méthode "code".



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


Nous écrasons la méthode "sayHello".



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

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


FF (en utilisant ceci)


Nous créons un projet "Humain".



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


Créez un projet "Développeur" avec la méthode "code".



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


Nous écrasons la méthode "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 (sans cela)


Puisque firstName a une portée lexicale directe, nous pouvons l'omettre.



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 () { /* ... */ }
  })
}


Conclusion préliminaire à ce sujet


En termes simples, les classes nécessitent l'utilisation de cela, mais pas les FF. Dans ce cas, je préfère utiliser FF car:



  • ce contexte peut changer
  • le code écrit avec FF est plus court et plus propre (également grâce à l'encapsulation automatique des variables)


Classes vs FF - Gestionnaires d'événements



De nombreux articles sur la POO négligent le fait qu'en tant que développeurs frontaux, nous avons constamment affaire à des gestionnaires d'événements. Ils fournissent une interaction avec les utilisateurs.



Étant donné que les gestionnaires d'événements modifient ce contexte, travailler avec eux dans les classes peut être problématique. Dans le même temps, de tels problèmes ne se posent pas dans FF.



Cependant, changer ce contexte n'a pas d'importance si nous savons comment le gérer. Regardons un exemple simple.



Créer un compteur


Pour créer un compteur, nous utiliserons les connaissances acquises, y compris les variables privées.



Notre compteur contiendra deux choses:



  • le compteur lui-même
  • bouton pour augmenter sa valeur






Voici à quoi pourrait ressembler le balisage:



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


Créer un compteur à l'aide d'une classe


Pour faciliter les choses, demandez à l'utilisateur de trouver et de transmettre le balisage de compteur à la classe Counter:



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

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


Vous devez obtenir 2 éléments dans la classe:



  • <span> contenant la valeur du compteur - nous devons mettre à jour cette valeur lorsque le compteur augmente
  • <button> - nous devons ajouter un gestionnaire pour les événements appelés par cet élément


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


Ensuite, nous initialisons la variable "count" avec le contenu textuel de countElement. La variable spécifiée doit être privée.



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

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


Lorsque le bouton est enfoncé, la valeur du compteur doit augmenter de 1. Nous implémentons cela en utilisant la méthode "AugmenterCount".



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

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


Nous devons maintenant mettre à jour le DOM. Implémentons cela en utilisant la méthode "updateCount" appelée dans augmentationCount:



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

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

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


Il reste à ajouter un gestionnaire d'événements.



Ajouter un gestionnaire d'événements


Ajoutons un gestionnaire à this.buttonElement. Malheureusement, nous ne pouvons pas utiliser augmenteCount comme fonction de rappel. Cela entraînera une erreur.



class Counter {
  // ...

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

  // 
}






L'exception est levée car elle pointe vers buttonElement (contexte du gestionnaire d'événements). Vous pouvez le vérifier en imprimant la valeur this sur la console.







La valeur this doit être modifiée pour pointer vers l'instance. Ceci peut être fait de deux façons:



  • en utilisant bind
  • en utilisant la fonction flèche


La plupart utilisent la première méthode (mais la seconde est plus simple).



Ajout d'un gestionnaire d'événements avec bind


bind renvoie une nouvelle fonction. En tant que premier argument, il est passé un objet vers lequel cela pointera (auquel cela sera lié).



class Counter {
  // ...

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

  // ...
}


Cela fonctionne, mais cela n'a pas l'air bien. De plus, bind est une fonctionnalité avancée difficile à gérer pour les débutants.



Fonctions fléchées


Les fonctions de flèche, entre autres, n'ont pas le leur. Ils l'empruntent à l'environnement lexical (externe). Par conséquent, le code de compteur peut être réécrit comme suit:



class Counter {
  // ...

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

  // 
}


Il existe un moyen encore plus simple. Nous pouvons créer augmenterCount comme une fonction de flèche. Dans ce cas, cela pointera vers l'instance.



class Counter {
  // ...

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

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

  // ...
}


Le code


Voici l'exemple de code complet:







Créer un compteur avec FF


Le début est similaire - nous demandons à l'utilisateur de trouver et de transmettre le balisage du compteur:



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

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


Nous obtenons les éléments nécessaires, qui seront privés par défaut:



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


Initialisons la variable "count":



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

  let count = parseInt(countElement.textContext)
}


La valeur du compteur sera augmentée en utilisant la méthode "AugmenterCompte". Vous pouvez utiliser une fonction régulière, mais je préfère une approche différente:



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


Le DOM sera mis à jour en utilisant la méthode "updateCount" qui est appelée à l'intérieur de scaleCount:



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

    updateCount () {
      increaseCount()
    }
  }
}


Notez que nous utilisons counter.updateCount au lieu de this.updateCount.



Ajouter un gestionnaire d'événements


Nous pouvons ajouter un gestionnaire d'événements au buttonElement en utilisant counter.increaseCount comme rappel.



Cela fonctionnera puisque nous n'utilisons pas ceci, donc cela n'a pas d'importance pour nous que le gestionnaire change le contexte de ceci.



function Counter (counterElement) {
  // 

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

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


La première caractéristique de ce


Vous pouvez l'utiliser dans FF, mais uniquement dans le contexte d'une méthode.



Dans l'exemple suivant, l'appel de counter.increaseCount appellera counter.updateCount car cela pointe vers counter:



function Counter (counterElement) {
  // 

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

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


Toutefois, le gestionnaire d'événements ne fonctionnera pas car cette valeur a changé. Ce problème peut être résolu avec bind, mais pas avec les fonctions fléchées.



La deuxième caractéristique de ce


Lors de l'utilisation de la syntaxe FF, nous ne pouvons pas créer de méthodes sous la forme de fonctions fléchées, car les méthodes sont créées dans le contexte d'une fonction, c'est-à-dire cela pointera vers la fenêtre:



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


Par conséquent, lors de l'utilisation de FF, je recommande vivement d'éviter de l'utiliser.



Le code








Verdict du gestionnaire d'événements


Les gestionnaires d'événements modifient la valeur de ceci, utilisez-le donc très soigneusement. Lors de l'utilisation de classes, je vous conseille de créer des rappels de gestionnaire d'événements sous la forme de fonctions fléchées. Ensuite, vous n'avez pas à utiliser les services de liaison.



Lorsque vous utilisez FF, je recommande de s'en passer du tout.



Conclusion



Donc, dans cet article, nous avons examiné quatre façons de créer des objets en JavaScript:



  • Fonctions du constructeur
  • Des classes
  • Lier des objets
  • Fonctions d'usine


Tout d'abord, nous sommes arrivés à la conclusion que les classes et les FF sont les moyens les plus optimaux pour créer des objets.



Deuxièmement, nous avons vu que les sous-classes sont plus faciles à créer avec des classes. Cependant, dans le cas de la composition, il est préférable d'utiliser FF.



Troisièmement, nous avons résumé qu'en ce qui concerne l'encapsulation, les FF ont un avantage sur les classes, car ces dernières nécessitent l'utilisation d'un préfixe spécial "#", et les FF rendent les variables privées automatiquement.



Quatrièmement, les FF vous permettent de vous passer de cela comme référence d'instance. Dans les classes, vous devez recourir à une astuce pour ramener cela au contexte d'origine modifié par le gestionnaire d'événements.



C'est tout pour moi. J'espère que vous avez apprécié l'article. Merci de votre attention.



All Articles