Die Geschmacksrichtungen der objektorientierten Programmierung (in JavaScript)

Avatar of Zell Liew
Zell Liew am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Bei meiner Recherche habe ich festgestellt, dass es vier Ansätze für objektorientierte Programmierung in JavaScript gibt

Welche Methoden sollte ich verwenden? Welche ist der "beste" Weg? Hier stelle ich meine Ergebnisse vor, zusammen mit Informationen, die Ihnen bei der Entscheidung helfen können, welche für Sie die richtige ist.

Um diese Entscheidung zu treffen, werden wir uns nicht nur die verschiedenen Geschmacksrichtungen ansehen, sondern auch konzeptionelle Aspekte zwischen ihnen vergleichen

Was ist objektorientierte Programmierung?

Objektorientierte Programmierung ist eine Methode zum Schreiben von Code, die es Ihnen ermöglicht, verschiedene Objekte aus einem gemeinsamen Objekt zu erstellen. Das gemeinsame Objekt wird normalerweise als Blaupause bezeichnet, während die erstellten Objekte als Instanzen bezeichnet werden.

Jede Instanz hat Eigenschaften, die nicht mit anderen Instanzen geteilt werden. Wenn Sie beispielsweise eine menschliche Blaupause haben, können Sie menschliche Instanzen mit unterschiedlichen Namen erstellen.

Der zweite Aspekt der objektorientierten Programmierung betrifft die Strukturierung von Code, wenn Sie mehrere Ebenen von Blaupausen haben. Dies wird üblicherweise als Vererbung oder Unterklassifizierung bezeichnet.

Der dritte Aspekt der objektorientierten Programmierung betrifft die Kapselung, bei der Sie bestimmte Informationen innerhalb des Objekts verbergen, sodass sie nicht zugänglich sind.

Wenn Sie mehr als diese kurze Einführung benötigen, finden Sie hier einen Artikel, der diesen Aspekt der objektorientierten Programmierung einführt, falls Sie Hilfe benötigen.

Beginnen wir mit den Grundlagen – einer Einführung in die vier Geschmacksrichtungen der objektorientierten Programmierung.

Die vier Geschmacksrichtungen der objektorientierten Programmierung

Es gibt vier Möglichkeiten, objektorientierte Programmierung in JavaScript zu schreiben. Diese sind

Verwendung von Konstruktorfunktionen

Konstruktoren sind Funktionen, die ein this-Schlüsselwort enthalten.

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

this ermöglicht es Ihnen, eindeutige Werte zu speichern (und darauf zuzugreifen), die für jede Instanz erstellt werden. Sie können eine Instanz mit dem Schlüsselwort new erstellen.

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

Klassensyntax

Klassen werden als „syntaktischer Zucker“ für Konstruktorfunktionen bezeichnet. Das heißt, Klassen sind eine einfachere Art, Konstruktorfunktionen zu schreiben.

Es gibt ernsthafte Meinungsverschiedenheiten darüber, ob Klassen schlecht sind (wie dies und dies). Wir werden diese Argumente hier nicht vertiefen. Stattdessen werden wir uns nur ansehen, wie man Code mit Klassen schreibt, und entscheiden, ob Klassen besser sind als Konstruktoren, basierend auf dem Code, den wir schreiben.

Klassen können mit folgender Syntax geschrieben werden

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

Beachten Sie, dass die constructor-Funktion denselben Code enthält wie die obige Konstruktorsyntax? Das müssen wir tun, da wir Werte in this initialisieren wollen. (Wir können constructor überspringen, wenn wir keine Werte initialisieren müssen. Mehr dazu später unter Vererbung).

Auf den ersten Blick scheinen Klassen Konstruktoren unterlegen zu sein – es gibt mehr Code zu schreiben! Halten Sie Ihre Pferde und ziehen Sie noch keine Schlussfolgerung. Wir haben noch viel zu behandeln. Klassen beginnen später zu glänzen.

Wie zuvor können Sie eine Instanz mit dem Schlüsselwort new erstellen.

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

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

Objekte, die auf andere Objekte verweisen (OLOO)

OLOO wurde von Kyle Simpson geprägt und populär gemacht. In OLOO definieren Sie die Blaupause als normales Objekt. Dann verwenden Sie eine Methode (oft init genannt, aber das ist nicht erforderlich, wie es constructor für eine Klasse ist), um die Instanz zu initialisieren.

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

Sie verwenden Object.create, um eine Instanz zu erstellen. Nach der Erstellung der Instanz müssen Sie Ihre init-Funktion ausführen.

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

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

Sie können init nach Object.create verketteten, wenn Sie this innerhalb von init zurückgegeben haben.

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

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

Factory-Funktionen

Factory-Funktionen sind Funktionen, die ein Objekt zurückgeben. Sie können jedes Objekt zurückgeben. Sie können sogar eine Klasseninstanz oder eine OLOO-Instanz zurückgeben – und es bleibt eine gültige Factory-Funktion.

Hier ist die einfachste Art, Factory-Funktionen zu erstellen

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

Sie benötigen kein new, um Instanzen mit Factory-Funktionen zu erstellen. Sie rufen einfach die Funktion auf.

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

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

Nachdem wir diese vier Möglichkeiten der OOP-Einrichtung gesehen haben, sehen wir uns an, wie Eigenschaften und Methoden auf jeder einzelnen deklariert werden, damit wir ein besseres Verständnis für die Arbeit damit bekommen, bevor wir zu den größeren Vergleichen kommen, die wir machen wollen.


Deklaration von Eigenschaften und Methoden

Methoden sind Funktionen, die als Eigenschaft eines Objekts deklariert werden.

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

In der objektorientierten Programmierung gibt es zwei Möglichkeiten, Eigenschaften und Methoden zu deklarieren

  1. Direkt auf der Instanz
  2. Im Prototyp

Lassen Sie uns beides lernen.

Deklaration von Eigenschaften und Methoden mit Konstruktoren

Wenn Sie eine Eigenschaft direkt auf einer Instanz deklarieren möchten, können Sie die Eigenschaft innerhalb der Konstruktorfunktion schreiben. Stellen Sie sicher, dass sie als Eigenschaft für this festgelegt wird.

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

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

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

Methoden werden üblicherweise auf dem Prototyp deklariert, da der Prototyp es Instanzen ermöglicht, dieselbe Methode zu verwenden. Es ist eine kleinere „Code-Fußabdruck“.

Um Eigenschaften auf dem Prototyp zu deklarieren, müssen Sie die prototype-Eigenschaft verwenden.

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

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

Es kann umständlich sein, wenn Sie mehrere Methoden in einem Prototyp deklarieren möchten.

// Declaring methods on a prototype
Human.prototype.method1 = function () { /*...*/ }
Human.prototype.method2 = function () { /*...*/ }
Human.prototype.method3 = function () { /*...*/ }

Sie können die Dinge einfacher machen, indem Sie Funktionen wie Object.assign verwenden.

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

Object.assign unterstützt nicht das Zusammenführen von Getter- und Setter-Funktionen. Dazu benötigen Sie ein anderes Werkzeug. Hier erfahren Sie, warum. Und hier ist ein Werkzeug, das ich erstellt habe, um Objekte mit Gettern und Settern zusammenzuführen.

Deklaration von Eigenschaften und Methoden mit Klassen

Sie können Eigenschaften für jede Instanz innerhalb der constructor-Funktion deklarieren.

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

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

Es ist einfacher, Methoden auf dem Prototyp zu deklarieren. Sie schreiben die Methode nach constructor wie eine normale Funktion.

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

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

Es ist einfacher, mehrere Methoden in Klassen im Vergleich zu Konstruktoren zu deklarieren. Sie benötigen nicht die Object.assign-Syntax. Sie schreiben einfach mehr Funktionen.

Hinweis: Zwischen Methodendeklarationen in einer Klasse gibt es kein ,.

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

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

Deklaration von Eigenschaften und Methoden mit OLOO

Sie verwenden denselben Prozess für die Deklaration von Eigenschaften und Methoden auf einer Instanz. Sie weisen sie als Eigenschaft von this zu.

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)

Um Methoden im Prototyp zu deklarieren, schreiben Sie die Methode wie ein normales Objekt.

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

Deklaration von Eigenschaften und Methoden mit Factory-Funktionen

Sie können Eigenschaften und Methoden direkt deklarieren, indem Sie sie in das zurückgegebene Objekt aufnehmen.

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

Sie können keine Methoden auf dem Prototyp deklarieren, wenn Sie Factory-Funktionen verwenden. Wenn Sie wirklich Methoden auf dem Prototyp haben möchten, müssen Sie eine Konstruktor-, Klassen- oder OLOO-Instanz zurückgeben. (Tun Sie das nicht, da es keinen Sinn ergibt.)

// Do not do this
function createHuman (...args) {
  return new Human(...args)
}

Wo Eigenschaften und Methoden deklarieren

Sollten Sie Eigenschaften und Methoden direkt auf der Instanz deklarieren? Oder sollten Sie prototype so oft wie möglich verwenden?

Viele Leute sind stolz darauf, dass JavaScript eine „Prototyp-Sprache“ ist (was bedeutet, dass sie Prototypen verwendet). Aus dieser Aussage heraus könnten Sie annehmen, dass die Verwendung von „Prototypen“ besser ist.

Die wirkliche Antwort ist: Es spielt keine Rolle.

Wenn Sie Eigenschaften und Methoden auf Instanzen deklarieren, nimmt jede Instanz etwas mehr Speicher ein. Wenn Sie Methoden auf Prototypen deklarieren, verringert sich der von jeder Instanz verwendete Speicher, aber nicht viel. Dieser Unterschied ist bei der heutigen Computerleistung unbedeutend. Stattdessen möchten Sie sehen, wie einfach es ist, Code zu schreiben – und ob es überhaupt möglich ist, Prototypen zu verwenden.

Wenn Sie beispielsweise Klassen oder OLOO verwenden, ist es besser, Prototypen zu verwenden, da der Code einfacher zu schreiben ist. Wenn Sie Factory-Funktionen verwenden, können Sie keine Prototypen verwenden. Sie können Eigenschaften und Methoden nur direkt auf der Instanz erstellen.

Ich habe einen separaten Artikel über das Verständnis von JavaScript-Prototypen geschrieben, wenn Sie mehr erfahren möchten.

Vorläufiges Urteil

Aus dem obigen Code können wir einige Anmerkungen machen. Diese Meinungen sind meine eigenen!

  1. Klassen sind besser als Konstruktoren, da es einfacher ist, mehrere Methoden in Klassen zu schreiben.
  2. OLOO ist wegen des Teils Object.create seltsam. Ich habe OLOO eine Weile ausprobiert, aber ich vergesse immer, Object.create zu schreiben. Es ist seltsam genug, als dass ich es nicht verwenden würde.
  3. Klassen und Factory-Funktionen sind am einfachsten zu verwenden. Das Problem ist, dass Factory-Funktionen keine Prototypen unterstützen. Aber wie gesagt, das spielt in der Produktion keine wirkliche Rolle.

Wir sind bei zwei angelangt. Sollen wir dann Klassen oder Factory-Funktionen wählen? Lassen Sie uns sie vergleichen!


Klassen vs. Factory-Funktionen – Vererbung

Um die Diskussion über Klassen und Factory-Funktionen fortzusetzen, müssen wir drei weitere Konzepte verstehen, die eng mit der objektorientierten Programmierung verbunden sind.

  1. Vererbung
  2. Kapselung
  3. diesen

Beginnen wir mit der Vererbung.

Was ist Vererbung?

Vererbung ist ein aufgeladenes Wort. Viele Leute in der Branche verwenden Vererbung meiner Meinung nach falsch. Das Wort „Vererbung“ wird verwendet, wenn man Dinge von irgendwoher erhält. Zum Beispiel

  • Wenn Sie eine Erbschaft von Ihren Eltern erhalten, bedeutet dies, dass Sie Geld und Vermögenswerte von ihnen erhalten.
  • Wenn Sie Gene von Ihren Eltern erben, bedeutet dies, dass Sie Ihre Gene von ihnen erhalten.
  • Wenn Sie einen Prozess von Ihrem Lehrer erben, bedeutet dies, dass Sie diesen Prozess von ihnen erhalten.

Ziemlich einfach.

In JavaScript kann Vererbung dasselbe bedeuten: wo Sie Eigenschaften und Methoden von der Elternblaupause erhalten.

Das bedeutet, dass *alle* Instanzen tatsächlich von ihren Blaupausen erben. Sie erben Eigenschaften und Methoden auf zwei Arten

  1. durch Erstellung einer Eigenschaft oder Methode direkt bei Erstellung der Instanz
  2. über die Prototypenkette

Wir haben besprochen, wie beides in dem vorherigen Artikel gemacht wird, also beziehen Sie sich darauf zurück, wenn Sie Hilfe beim Ansehen dieser Prozesse im Code benötigen.

Es gibt eine *zweite* Bedeutung für Vererbung in JavaScript – bei der Sie eine abgeleitete Blaupause von der Elternblaupause erstellen. Dieser Prozess wird genauer als Unterklassifizierung bezeichnet, aber Leute nennen dies manchmal auch Vererbung.

Unterklassifizierung verstehen

Unterklassifizierung bedeutet, eine abgeleitete Blaupause von einer gemeinsamen Blaupause zu erstellen. Sie können jeden objektorientierten Programmiergeschmack verwenden, um die Unterklasse zu erstellen.

Wir werden dies zuerst mit der Klassensyntax besprechen, da sie leichter zu verstehen ist.

Unterklassifizierung mit Klassen

Beim Erstellen einer Unterklasse verwenden Sie das Schlüsselwort extends.

class Child extends Parent {
  // ... Stuff goes here
}

Nehmen wir zum Beispiel an, wir möchten eine Developer-Klasse aus einer Human-Klasse erstellen.

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

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

Die Developer-Klasse wird Human wie folgt erweitern

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

    // Add other methods
}

Hinweis: super ruft die Human-Klasse (auch „Elternklasse“ genannt) auf. Es initialisiert den constructor von Human. Wenn Sie keinen zusätzlichen Initialisierungscode benötigen, können Sie constructor vollständig weglassen.

class Developer extends Human {
  // Add other methods
}

Nehmen wir an, ein Developer kann codieren. Wir können die Methode code direkt zu Developer hinzufügen.

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

Hier ist ein Beispiel für eine Instanz von Developer

const chris = new Developer('Chris', 'Coyier')
console.log(chris)
Instance of a Developer class.

Unterklassifizierung mit Factory-Funktionen

Es gibt vier Schritte zur Erstellung von Unterklassen mit Factory-Funktionen

  1. Erstellen Sie eine neue Factory-Funktion
  2. Erstellen Sie eine Instanz der Elternblaupause
  3. Erstellen Sie eine neue Kopie dieser Instanz
  4. Fügen Sie dieser neuen Kopie Eigenschaften und Methoden hinzu

Der Prozess sieht so aus

function Subclass (...args) {
  const instance = ParentClass(...args)
  return Object.assign({}, instance, {
    // Properties and methods go here
  })
}

Wir verwenden dasselbe Beispiel – die Erstellung einer Developer-Unterklasse –, um diesen Prozess zu veranschaulichen. Hier ist die Human-Factory-Funktion

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

Wir können Developer wie folgt erstellen

function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    // Properties and methods go here
  })
}

Dann fügen wir die Methode code wie folgt hinzu

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

Hier ist ein Beispiel für eine Developer-Instanz

const chris = Developer('Chris', 'Coyier')
console.log(chris)
Example of a Developer instance with Factory functions.

Hinweis: Sie können Object.assign nicht verwenden, wenn Sie Getter und Setter verwenden. Sie benötigen ein anderes Werkzeug wie mix. Warum, erkläre ich in diesem Artikel.

Überschreiben der Methode des Elternteils

Manchmal müssen Sie die Methode des Elternteils in der Unterklasse überschreiben. Sie können dies tun, indem Sie

  1. Eine Methode mit demselben Namen erstellen
  2. Die Methode des Elternteils aufrufen (optional)
  3. Ändern Sie, was immer Sie in der Methode der Unterklasse benötigen

Der Prozess sieht mit Klassen so aus

class Developer extends Human {
  sayHello () {
    // Calls the parent method
    super.sayHello() 

    // Additional stuff to run
    console.log(`I'm a developer.`)
  }
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()
Overwriting a parent's method.

Der Prozess sieht mit Factory-Funktionen so aus

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

  return Object.assign({}, human, {
      sayHello () {
        // Calls the parent method
        human.sayHello() 

        // Additional stuff to run
        console.log(`I'm a developer.`)
      }
  })
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()
Overwriting a parent's method.

Vererbung vs. Komposition

Kein Gespräch über Vererbung kommt ohne die Erwähnung der Komposition aus. Experten wie Eric Elliot schlagen oft vor, dass wir Komposition gegenüber Vererbung bevorzugen sollten.

„Bevorzuge Objektkomposition gegenüber Klassenerbung“, Gang of Four, „Design Patterns: Elements of Reusable Object Oriented Software“

„In der Informatik ist ein zusammengesetzter Datentyp oder ein Verbunddatentyp jeder Datentyp, der in einem Programm mithilfe der primitiven Datentypen der Programmiersprache und anderer zusammengesetzter Typen konstruiert werden kann. […] Der Vorgang des Konstruierens eines zusammengesetzten Typs wird Komposition genannt.“ ~ Wikipedia

Lassen Sie uns also die Komposition genauer betrachten und verstehen, was sie ist.

Komposition verstehen

Komposition ist die Handlung, zwei Dinge zu einem zu kombinieren. Es geht darum, Dinge zusammenzuführen. Die gebräuchlichste (und einfachste) Methode zum Zusammenführen von Objekten ist Object.assign.

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

Der Einsatz von Komposition lässt sich besser anhand eines Beispiels erklären. Nehmen wir an, wir haben bereits zwei Unterklassen, einen Designer und einen Developer. Designer können entwerfen, während Entwickler codieren können. Sowohl Designer als auch Entwickler erben von der Human-Klasse.

Hier ist der bisherige Code

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

Nun wollen wir eine dritte Unterklasse erstellen. Diese Unterklasse ist eine Mischung aus Designer und Entwickler – sie können entwerfen und codieren. Nennen wir sie DesignerDeveloper (oder DeveloperDesigner, je nachdem, was Ihnen gefällt).

Wie würden Sie die dritte Unterklasse erstellen?

Wir können nicht gleichzeitig Designer und Developer-Klassen erweitern. Das ist unmöglich, weil wir nicht entscheiden können, welche Eigenschaften zuerst kommen. Das nennt man oft das Diamantenproblem.

Diamond problem.

Das Diamantenproblem kann leicht gelöst werden, wenn wir etwas wie Object.assign tun – wo wir einem Objekt Priorität vor dem anderen geben. Wenn wir den Ansatz Object.assign verwenden, könnten wir Klassen wie folgt erweitern. Aber das wird in JavaScript nicht unterstützt.

// Doesn't work
class DesignerDeveloper extends Developer, Designer {
  // ...
}

Also müssen wir uns auf Komposition verlassen.

Komposition besagt: Anstatt zu versuchen, DesignerDeveloper durch Unterklassifizierung zu erstellen, erstellen wir ein neues Objekt, das gemeinsame Funktionen speichert. Wir können diese Funktionen dann nach Bedarf einbeziehen.

In der Praxis kann es so aussehen

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

Wir können Human dann ganz weglassen und drei verschiedene Klassen basierend auf ihren Fähigkeiten erstellen.

Hier ist der Code für 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)
Composing methods into a class

Sie können dasselbe mit Developer und Designer tun.

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

Haben Sie bemerkt, dass wir Methoden direkt auf der Instanz erstellen? Das ist nur eine Option. Wir können Methoden immer noch in den Prototyp legen, aber ich finde, der Code sieht umständlich aus. (Es ist, als würden wir wieder Konstruktorfunktionen schreiben.)

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

Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design,
  sayHello: skills.sayHello
})
Composition via Classes by putting methods into the Prototype.

Fühlen Sie sich frei, jede Code-Struktur zu verwenden, zu der Sie sich hingezogen fühlen. Die Ergebnisse sind sowieso ziemlich gleich.

Komposition mit Factory-Funktionen

Komposition mit Factory-Funktionen bedeutet im Wesentlichen, die gemeinsamen Methoden in das zurückgegebene Objekt aufzunehmen.

function DesignerDeveloper (firstName, lastName) {
  return {
    firstName,
    lastName,    
    code: skills.code,
    design: skills.design,
    sayHello: skills.sayHello
  }
}
Composing methods into a factory function

Vererbung und Komposition gleichzeitig

Niemand sagt, dass wir Vererbung und Komposition nicht gleichzeitig verwenden können. Wir können es!

Wenn wir das bisher entwickelte Beispiel verwenden, sind Designer, Developer und DesignerDeveloper Humans immer noch Menschen. Sie können das Human-Objekt erweitern.

Hier ist ein Beispiel, bei dem wir sowohl Vererbung als auch Komposition mit der Klassensyntax verwenden.

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
})
Subclassing and Composition at the same time.

Und hier ist dasselbe mit Factory-Funktionen

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
  }
}
Subclassing and Composition in Factory functions

Unterklassifizierung in der realen Welt

Ein letzter Punkt zur Unterklassifizierung vs. Komposition. Auch wenn Experten darauf hingewiesen haben, dass Komposition flexibler ist (und daher nützlicher), hat Unterklassifizierung immer noch ihre Vorteile. Viele Dinge, die wir heute verwenden, sind mit der Unterklassifizierungsstrategie aufgebaut.

Zum Beispiel: Das click-Ereignis, das wir kennen und lieben, ist ein MouseEvent. MouseEvent ist eine Unterklasse von UIEvent, das wiederum eine Unterklasse von Event ist.

MouseEvent is a subclass of UIEvent.

Ein weiteres Beispiel: HTML-Elemente sind Unterklassen von Nodes. Deshalb können sie alle Eigenschaften und Methoden von Nodes verwenden.

HTMLElement is a subclass of Node.

Vorläufiges Urteil

Klassen und Factory-Funktionen können sowohl Vererbung als auch Komposition nutzen. Komposition scheint bei Factory-Funktionen jedoch sauberer zu sein, aber das ist kein großer Vorteil gegenüber Klassen.

Wir werden Klassen und Factory-Funktionen im nächsten Abschnitt genauer untersuchen.


Klassen vs. Factory-Funktionen – Kapselung

Wir haben uns bisher die vier verschiedenen Geschmacksrichtungen der objektorientierten Programmierung angesehen. Zwei davon – Klassen und Factory-Funktionen – sind im Vergleich zu den anderen einfacher zu verwenden.

Aber die Fragen bleiben: Welche sollten Sie verwenden? Und warum?

Um die Diskussion über Klassen und Factory-Funktionen fortzusetzen, müssen wir drei Konzepte verstehen, die eng mit der objektorientierten Programmierung verbunden sind

  1. Vererbung
  2. Kapselung
  3. diesen

Wir haben gerade über Vererbung gesprochen. Lassen Sie uns nun über Kapselung sprechen.

Kapselung

Kapselung ist ein großes Wort, hat aber eine einfache Bedeutung. Kapselung ist die Handlung, eine Sache in einer anderen Sache einzuschließen, damit die Sache im Inneren nicht austritt. Denken Sie an das Aufbewahren von Wasser in einer Flasche. Die Flasche verhindert, dass Wasser austritt.

In JavaScript sind wir daran interessiert, Variablen (einschließlich Funktionen) einzuschließen, damit diese Variablen nicht in den externen Gültigkeitsbereich austreten. Das bedeutet, dass Sie den Gültigkeitsbereich verstehen müssen, um Kapselung zu verstehen. Wir werden eine Erklärung durchgehen, aber Sie können auch diesen Artikel verwenden, um Ihr Wissen über Gültigkeitsbereiche aufzufrischen.

Einfache Kapselung

Die einfachste Form der Kapselung ist ein Block-Gültigkeitsbereich.

{
  // Variables declared here won't leak out
}

Wenn Sie sich im Block befinden, können Sie auf Variablen zugreifen, die außerhalb des Blocks deklariert wurden.

const food = 'Hamburger'

{
  console.log(food)
}
Logs food from inside the blog. Result: Hamburger.

Wenn Sie sich jedoch außerhalb des Blocks befinden, können Sie nicht auf Variablen zugreifen, die innerhalb des Blocks deklariert wurden.

{
  const food = 'Hamburger'
}

console.log(food)
Logs food from outside the blog. Results: Error.

Hinweis: Variablen, die mit var deklariert wurden, respektieren keinen Block-Gültigkeitsbereich. Deshalb empfehle ich Ihnen, let oder const zum Deklarieren von Variablen zu verwenden.

Kapselung mit Funktionen

Funktionen verhalten sich wie Block-Gültigkeitsbereiche. Wenn Sie eine Variable innerhalb einer Funktion deklarieren, kann diese nicht aus der Funktion austreten. Das funktioniert für alle Variablen, selbst für die, die mit var deklariert wurden.

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

sayFood()
console.log(food)
Logs food from outside the function. Results: Error.

Ebenso können Sie, wenn Sie sich innerhalb der Funktion befinden, auf Variablen zugreifen, die außerhalb dieser Funktion deklariert wurden.

const food = 'Hamburger'

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


sayFood()
Logs food from inside the function. Result: Hamburger.

Funktionen können einen Wert zurückgeben. Dieser zurückgegebene Wert kann später außerhalb der Funktion verwendet werden.

function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())
Logs return value from function. Result: Hamburger.

Closures

Closures sind eine fortgeschrittene Form der Kapselung. Sie sind einfach Funktionen, die in Funktionen verschachtelt sind.

// Here's a closure
function outsideFunction () {
  function insideFunction () { /* ...*/ }
}

Variablen, die in outsideFunction deklariert wurden, können in insideFunction verwendet werden.

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

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

// Calls `outsideFunction`, which returns `insideFunction`
// Stores `insideFunction` as variable `fn`
const fn = outsideFunction() 

// Calls `insideFunction`
fn()
Closure logs.

Kapselung und objektorientierte Programmierung

Wenn Sie Objekte erstellen, möchten Sie einige Eigenschaften öffentlich zugänglich machen (damit Leute sie verwenden können). Aber Sie möchten auch einige Eigenschaften privat halten (damit andere Ihre Implementierung nicht zerstören können).

Lassen Sie uns dies anhand eines Beispiels verdeutlichen. Nehmen wir an, wir haben eine Car-Blaupause. Wenn wir neue Autos produzieren, füllen wir jedes Auto mit 50 Litern Kraftstoff.

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

Hier haben wir die Eigenschaft fuel offengelegt. Benutzer können fuel verwenden, um die Menge an Kraftstoff zu ermitteln, die in ihren Autos verbleibt.

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

Benutzer können auch die Eigenschaft fuel verwenden, um jede beliebige Kraftstoffmenge einzustellen.

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

Fügen wir eine Bedingung hinzu und sagen, dass jedes Auto eine maximale Kapazität von 100 Litern hat. Mit dieser Bedingung möchten wir Benutzern nicht erlauben, die fuel-Eigenschaft frei einzustellen, da sie das Auto beschädigen könnten.

Es gibt zwei Möglichkeiten, die Einstellung der fuel-Eigenschaft zu verhindern

  1. Privat per Konvention
  2. Echte private Mitglieder

Privat per Konvention

In JavaScript gibt es die Praxis, Unterstriche vor einen Variablennamen zu setzen. Dies kennzeichnet die Variable als privat und sollte nicht verwendet werden.

class Car {
  constructor () {
    // Denotes that `_fuel` is private. Don't use it!
    this._fuel = 50
  }
}

Wir erstellen oft Methoden, um diese „private“ _fuel-Variable abzurufen und festzulegen.

class Car {
  constructor () { 
    // Denotes that `_fuel` is private. Don't use it!
    this._fuel = 50
  }

  getFuel () {
    return this._fuel
  }

  setFuel (value) {
    this._fuel = value
    // Caps fuel at 100 liters
    if (value > 100) this._fuel = 100
  }
}

Benutzer sollten die Methoden getFuel und setFuel verwenden, um Kraftstoff abzurufen und festzulegen.

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

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

Aber _fuel ist nicht wirklich privat. Es ist immer noch eine öffentliche Variable. Sie können darauf zugreifen, sie verwenden und sie missbrauchen (auch wenn der Missbrauch versehentlich geschieht).

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

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

Wir müssen echte private Variablen verwenden, wenn wir verhindern wollen, dass Benutzer darauf zugreifen.

Echte private Mitglieder

Mitglieder bezieht sich hier auf Variablen, Funktionen und Methoden. Es ist ein Sammelbegriff.

Private Mitglieder mit Klassen

Klassen erlauben Ihnen, private Mitglieder zu erstellen, indem Sie # vor die Variable setzen.

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

Leider können Sie # nicht direkt innerhalb einer constructor-Funktion verwenden.

Error when declaring <code>#</code> directly in constructor function.

Sie müssen die private Variable zuerst außerhalb des Konstruktors deklarieren.

class Car {
  // Declares private variable
  #fuel 
  constructor () {
    // Use private variable
    this.#fuel = 50
  }
}

In diesem Fall können wir eine Kurzschreibweise verwenden und #fuel vorab deklarieren, da wir fuel auf 50 setzen.

class Car {
  #fuel = 50
}

Sie können auf #fuel außerhalb von Car nicht zugreifen. Sie erhalten einen Fehler.

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

Sie benötigen Methoden (wie getFuel oder setFuel), um die Variable #fuel zu verwenden.

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

Hinweis: Ich bevorzuge Getter und Setter anstelle von getFuel und setFuel. Die Syntax ist leichter zu lesen.

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

Private Mitglieder mit Factory-Funktionen

Factory-Funktionen erstellen automatisch private Mitglieder. Sie müssen nur eine Variable wie gewohnt deklarieren. Benutzer können diese Variable nirgendwo anders abrufen. Dies liegt daran, dass Variablen funktionsbezogen sind und daher standardmäßig gekapselt werden.

function Car () {
  const fuel = 50 
}

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

Wir können Getter- und Setter-Funktionen erstellen, um diese private fuel-Variable zu verwenden.

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

Das ist alles! Einfach und unkompliziert!

Urteil zur Kapselung

Die Kapselung mit Factory-Funktionen ist einfacher und leichter zu verstehen. Sie basieren auf den Gültigkeitsbereichen, die ein wichtiger Teil der JavaScript-Sprache sind.

Die Kapselung mit Klassen hingegen erfordert das Voranstellen von # vor die private Variable. Das kann die Dinge umständlich machen.

Wir betrachten das letzte Konzept – this –, um den Vergleich zwischen Klassen und Factory-Funktionen abzuschließen, im nächsten Abschnitt.


Klassen vs. Factory-Funktionen – Die this-Variable

this (ha!) ist eines der Hauptargumente gegen die Verwendung von Klassen für objektorientierte Programmierung. Warum? Weil sich der Wert von this je nach Verwendungsweise ändert. Das kann für viele Entwickler (sowohl neue als auch erfahrene) verwirrend sein.

Aber das Konzept von this ist in Wirklichkeit relativ einfach. Es gibt nur sechs Kontexte, in denen Sie this verwenden können. Wenn Sie diese sechs Kontexte beherrschen, werden Sie keine Probleme mit der Verwendung von this haben.

Die sechs Kontexte sind

  1. Im globalen Kontext
  2. Bei der Objektkonstruktion
  3. In einer Objekt-Eigenschaft / Methode
  4. In einer einfachen Funktion
  5. In einer Pfeilfunktion
  6. In einem Event-Listener

Ich habe diese sechs Kontexte im Detail behandelt. Lesen Sie es, wenn Sie Hilfe beim Verständnis von this benötigen.

Hinweis: Scheuen Sie sich nicht, das Erlernen von this. Es ist ein wichtiges Konzept, das Sie verstehen müssen, wenn Sie JavaScript beherrschen wollen.

Kommen Sie zu diesem Artikel zurück, nachdem Sie Ihr Wissen über this gefestigt haben. Wir werden eine tiefere Diskussion über die Verwendung von this in Klassen und Factory-Funktionen führen.

Zurück? Gut. Los geht's!

Verwendung von this in Klassen

this bezieht sich auf die Instanz, wenn es in einer Klasse verwendet wird. (Es verwendet den Kontext „In einer Objekt-Eigenschaft / Methode“.) Deshalb können Sie Eigenschaften und Methoden auf der Instanz innerhalb der constructor-Funktion festlegen.

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

const chris = new Human('Chris', 'Coyier')
<code>this</code> points to the instance

Verwendung von this in Konstruktorfunktionen

Wenn Sie this innerhalb einer Funktion und new verwenden, um eine Instanz zu erstellen, bezieht sich this auf die Instanz. So wird eine Konstruktorfunktion erstellt.

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

const chris = new Human('Chris', 'Coyier')
<code>this</code> points to the instance.

Ich habe Konstruktorfunktionen erwähnt, da Sie this in Factory-Funktionen verwenden können. Aber this verweist auf Window (oder undefined, wenn Sie ES6-Module verwenden, oder einen Bundler wie webpack).

// NOT a Constructor function because we did not create instances with the `new` keyword
function Human (firstName, lastName) {
  this.firstName = firstName 
  this.lastName = lastName
  console.log(this)  
}

const chris = Human('Chris', 'Coyier')
<code>this</code> points to Window.

Grundsätzlich sollten Sie bei der Erstellung einer Factory-Funktion this nicht so verwenden, als wäre es eine Konstruktorfunktion. Dies ist ein kleiner Stolperstein, den Leute mit this erleben. Ich wollte das Problem hervorheben und es klarstellen.

Verwendung von this in einer Factory-Funktion

Der richtige Weg, this in einer Factory-Funktion zu verwenden, ist, es im Kontext „Objekt-Eigenschaft / Methode“ zu verwenden.

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

const chris = Human('Chris', 'Coyier')
chris.sayThis()
<code>this</code> points to the instance.

Auch wenn Sie this in Factory-Funktionen verwenden können, müssen Sie es nicht verwenden. Sie können eine Variable erstellen, die auf die Instanz verweist. Sobald Sie dies tun, können Sie die Variable anstelle von this verwenden. Hier ist ein Beispiel in Aktion.

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 ist klarer als this.firstName, da human definitiv auf die Instanz verweist. Das sehen Sie, wenn Sie den Code sehen.

Wenn Sie mit JavaScript vertraut sind, werden Sie vielleicht auch bemerken, dass es gar nicht nötig ist, human.firstName überhaupt zu schreiben! Nur firstName reicht aus, da firstName im lexikalischen Gültigkeitsbereich liegt. (Lesen Sie diesen Artikel, wenn Sie Hilfe bei Gültigkeitsbereichen benötigen.)

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

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()
Runs <code>chris.sayHello</code>

Was wir bisher behandelt haben, ist einfach. Es ist nicht einfach zu entscheiden, ob this tatsächlich benötigt wird, bis wir ein ausreichend kompliziertes Beispiel erstellen. Machen wir das also.

Detailliertes Beispiel

Hier ist die Einrichtung. Nehmen wir an, wir haben eine Human-Blaupause. Diese Human hat firstName und lastName Eigenschaften sowie eine sayHello-Methode.

Wir haben eine Developer-Blaupause, die von Human abgeleitet ist. Entwickler können codieren, also haben sie eine code-Methode. Entwickler wollen auch verkünden, dass sie Entwickler sind, also müssen wir sayHello überschreiben und I'm a Developer zur Konsole hinzufügen.

Wir werden dieses Beispiel mit Klassen und Factory-Funktionen erstellen. (Wir werden ein Beispiel mit this und ein Beispiel ohne this für Factory-Funktionen erstellen).

Das Beispiel mit Klassen

Zuerst haben wir eine Human-Blaupause. Diese Human hat firstName und lastName Eigenschaften sowie eine sayHello-Methode.

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

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

Wir haben eine Developer-Blaupause, die von Human abgeleitet ist. Entwickler können codieren, also haben sie eine code-Methode.

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

Entwickler wollen auch verkünden, dass sie Entwickler sind. Wir müssen sayHello überschreiben und I'm a Developer zur Konsole hinzufügen. Dies tun wir, indem wir die sayHello-Methode von Human aufrufen. Wir können dies mit super tun.

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

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

Das Beispiel mit Factory-Funktionen (mit this)

Wieder haben wir zuerst eine Human-Blaupause. Diese Human hat firstName und lastName Eigenschaften sowie eine sayHello-Methode.

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

Als nächstes haben wir eine Developer-Blaupause, die von Human abgeleitet ist. Entwickler können codieren, also haben sie eine code-Methode.

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

Entwickler wollen auch verkünden, dass sie Entwickler sind. Wir müssen sayHello überschreiben und I'm a Developer zur Konsole hinzufügen.
Das tun wir, indem wir die sayHello-Methode von Human aufrufen. Wir können dies mithilfe der human-Instanz tun.

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

Das Beispiel mit Factory-Funktionen (ohne this)

Hier ist der vollständige Code, der Factory-Funktionen (mit this) verwendet

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

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

Haben Sie bemerkt, dass firstName sowohl in Human als auch in Developer innerhalb des lexikalischen Gültigkeitsbereichs verfügbar ist? Das bedeutet, dass wir this weglassen und firstName direkt in beiden Blaupausen verwenden können.

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

Sehen Sie das? Das bedeutet, dass Sie this sicher aus Ihrem Code weglassen können, wenn Sie Factory-Funktionen verwenden.

Urteil für this

Einfach ausgedrückt, Klassen erfordern this, während Factory-Funktionen dies nicht tun. Ich bevorzuge hier Factory-Funktionen, weil

  1. Der Kontext von this kann sich ändern (was verwirrend sein kann)
  2. Der mit Factory-Funktionen geschriebene Code ist kürzer und sauberer (da wir gekapselte Variablen verwenden können, ohne this.#variable zu schreiben).

Als Nächstes kommt der letzte Abschnitt, in dem wir eine einfache Komponente mit Klassen und Factory-Funktionen zusammen aufbauen. Sie sehen, wie sie sich unterscheiden und wie man Event-Listener mit jedem Geschmack verwendet.

Klassen vs. Factory-Funktionen – Event-Listener

Die meisten Artikel über objektorientierte Programmierung zeigen Beispiele ohne Event-Listener. Diese Beispiele sind leichter zu verstehen, spiegeln aber nicht die Arbeit wider, die wir als Frontend-Entwickler leisten. Die Arbeit, die wir leisten, erfordert Event-Listener – aus einem einfachen Grund – weil wir Dinge bauen müssen, die auf Benutzereingaben basieren.

Da Event-Listener den Kontext von this ändern, können sie Klassen problematisch machen. Gleichzeitig machen sie Factory-Funktionen attraktiver.

Aber das ist nicht wirklich der Fall.

Die Änderung von this spielt keine Rolle, wenn Sie wissen, wie Sie this sowohl in Klassen als auch in Factory-Funktionen behandeln. Wenige Artikel behandeln dieses Thema, daher dachte ich, es wäre gut, diesen Artikel mit einer einfachen Komponente abzuschließen, die objektorientierte Programmiergeschmacksrichtungen verwendet.

Erstellen eines Zählers

Wir werden in diesem Artikel einen einfachen Zähler erstellen. Wir werden alles verwenden, was Sie in diesem Artikel gelernt haben – einschließlich privater Variablen.

Nehmen wir an, der Zähler enthält zwei Dinge

  1. Die Zählung selbst
  2. Eine Schaltfläche zum Erhöhen der Zählung

Hier ist das einfachst mögliche HTML für den Zähler

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

Erstellen des Zählers mit Klassen

Um die Dinge einfach zu halten, bitten wir die Benutzer, das HTML des Zählers zu finden und es in eine Counter-Klasse zu übergeben.

class Counter () {
  constructor (counter) {
    // Do stuff 
  } 
}

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

Wir müssen zwei Elemente in der Counter-Klasse abrufen

  1. Die <span>, die die Zählung enthält – wir müssen dieses Element aktualisieren, wenn die Zählung steigt
  2. Die <button> – wir müssen dieser Klasse ein Event-Listener hinzufügen
Counter () {
  constructor (counter) {
    this.countElement = counter.querySelector('span')
    this.buttonElement = counter.querySelector('button')
  }
}

Wir initialisieren eine count-Variable und setzen sie auf das, was countElement anzeigt. Wir verwenden eine private #count-Variable, da die Zählung nicht woanders offengelegt werden sollte.

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

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

Wenn ein Benutzer auf die <button> klickt, möchten wir #count erhöhen. Dies können wir mit einer weiteren Methode tun. Wir werden diese Methode increaseCount nennen.

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

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

Als Nächstes müssen wir das DOM mit der neuen #count aktualisieren. Lassen Sie uns eine Methode namens updateCount erstellen, um dies zu tun. Wir werden updateCount von increaseCount aufrufen

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

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

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

Wir sind bereit, den Event-Listener hinzuzufügen.

Hinzufügen des Event-Listeners

Wir fügen den Event-Listener zu this.buttonElement hinzu. Leider können wir increaseCount nicht direkt als Callback verwenden. Sie erhalten einen Fehler, wenn Sie es versuchen.

class Counter () {
  // ...

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

  // Methods
}
Error accessing #count because this doesn't point to the instance

Sie erhalten einen Fehler, weil this auf buttonElement verweist. (Das ist der Kontext des Event-Listeners.) Sie sehen das buttonElement, wenn Sie this in die Konsole geloggt haben.

this points to the button element

Wir müssen den Wert von this wieder auf die Instanz für increaseCount ändern, damit die Dinge funktionieren. Es gibt zwei Möglichkeiten, dies zu tun

  1. Verwenden von bind
  2. Verwenden von Pfeilfunktionen

Die meisten Leute verwenden die erste Methode (aber die zweite ist einfacher).

Hinzufügen des Event-Listeners mit bind

bind gibt eine neue Funktion zurück. Sie ermöglicht es Ihnen, this auf das erste Argument zu ändern, das übergeben wird. Leute erstellen normalerweise Event-Listener, indem sie bind(this) aufrufen.

class Counter () {
  // ...

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

  // ...
}

Das funktioniert, ist aber nicht sehr gut lesbar. Es ist auch nicht anfängerfreundlich, da bind als fortgeschrittene JavaScript-Funktion gilt.

Pfeilfunktionen

Die zweite Methode ist die Verwendung von Pfeilfunktionen. Pfeilfunktionen funktionieren, weil sie den this-Wert für den lexikalischen Kontext beibehalten.

Die meisten Leute schreiben Methoden innerhalb des Pfeilfunktions-Callbacks, so

class Counter () {
  // ...

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

  // Methods
}

Das funktioniert, ist aber ein langer Weg. Es gibt tatsächlich eine Abkürzung.

Sie können increaseCount mit Pfeilfunktionen erstellen. Wenn Sie dies tun, wird der this-Wert für increaseCount sofort an den Wert der Instanz gebunden.

Hier ist also der benötigte Code

class Counter () {
  // ...

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

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

  // ...
}

Der Code

Hier ist eine vollständige Version des klassenbasierten Codes (mit Pfeilfunktionen).

Erstellen des Zählers mit Factory-Funktionen

Wir machen dasselbe hier. Wir lassen die Benutzer das Counter-HTML in die Counter-Factory übergeben.

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

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

Wir müssen zwei Elemente aus counter abrufen – die <span> und die <button>. Wir können hier normale Variablen (ohne this) verwenden, da sie bereits private Variablen sind. Wir werden sie nicht offenlegen.

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

Wir initialisieren eine Zählervariable mit dem Wert, der im HTML vorhanden ist.

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

  let count = parseInt(countElement.textContext)
}

Wir werden diese count-Variable mit einer increaseCount-Methode erhöhen. Sie können hier eine normale Funktion verwenden, aber ich erstelle gerne eine Methode, um die Dinge ordentlich und sauber zu halten.

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

Schließlich werden wir die Zählung mit einer updateCount-Methode aktualisieren. Wir werden auch updateCount von increaseCount aufrufen.

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

    updateCount () {
      increaseCount()
    }
  }
}

Haben Sie bemerkt, dass ich counter.updateCount anstelle von this.updateCount verwendet habe? Das gefällt mir, weil counter klarer ist als this. Ich mache das auch, weil Anfänger Fehler mit this in Factory-Funktionen machen können (was ich später behandeln werde).

Hinzufügen von Event-Listenern

Wir können Event-Listener zum buttonElement hinzufügen. Wenn wir das tun, können wir counter.increaseCount sofort als Callback verwenden.

Das können wir tun, weil wir this nicht verwendet haben, daher spielt es keine Rolle, selbst wenn Event-Listener den this-Wert ändern.

function Counter (counterElement) {
  // Variables 

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

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

Der this Stolperstein

Sie können this in Factory-Funktionen verwenden. Aber Sie müssen this in einem Methodenkontext verwenden.

Im folgenden Beispiel, wenn Sie counter.increaseCount aufrufen, ruft JavaScript auch counter.updateCount auf. Das funktioniert, weil this auf die counter-Variable verweist.

function Counter (counterElement) {
  // Variables 

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

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

Leider würde der Event-Listener nicht funktionieren, da der this-Wert geändert wurde. Sie benötigen die gleiche Behandlung wie Klassen – mit bind oder Pfeilfunktionen –, um den Event-Listener wieder zum Laufen zu bringen.

Und das bringt mich zum zweiten Stolperstein.

Zweiter this Stolperstein

Wenn Sie die Factory-Funktionssyntax verwenden, können Sie keine Methoden mit Pfeilfunktionen erstellen. Das liegt daran, dass die Methoden im function-Kontext erstellt werden. (Lesen Sie diesen Artikel.)

function Counter (counterElement) {
  // ...
  const counter = {
    // Do not do this. 
    // Doesn't work because `this` is `Window`
    increaseCount: () => {
      count = count + 1
      this.updateCount()
    }
  }
  // ...
}

Daher empfehle ich dringend, this bei der Verwendung von Factory-Funktionen vollständig zu überspringen. Das ist einfacher.

Der Code

Urteil für Event-Listener

Event-Listener ändern den Wert von this, daher müssen wir bei der Verwendung des this-Wertes sehr vorsichtig sein. Wenn Sie Klassen verwenden, empfehle ich, Event-Listener-Callbacks mit Pfeilfunktionen zu erstellen, damit Sie bind nicht verwenden müssen.

Wenn Sie Factory-Funktionen verwenden, empfehle ich, this vollständig zu überspringen, da es Sie verwirren kann. Das ist alles!


Fazit

Wir haben über die vier Geschmacksrichtungen der objektorientierten Programmierung gesprochen. Sie sind

  1. Konstruktorfunktionen
  2. Klassen
  3. OLOO
  4. Factory-Funktionen

Erstens sind wir zu dem Schluss gekommen, dass Klassen und Factory-Funktionen aus Sicht des Codes einfacher zu verwenden sind.

Zweitens haben wir verglichen, wie man Unterklassen mit Klassen und Factory-Funktionen verwendet. Hier sehen wir, dass die Erstellung von Unterklassen mit Klassen einfacher ist, aber die Komposition mit Factory-Funktionen einfacher ist.

Drittens haben wir die Kapselung mit Klassen und Factory-Funktionen verglichen. Hier sehen wir, dass die Kapselung mit Factory-Funktionen natürlich ist – wie JavaScript –, während die Kapselung mit Klassen erfordert, dass Sie eine # vor Variablen hinzufügen.

Viertens haben wir die Verwendung von this in Klassen und Factory-Funktionen verglichen. Ich finde, dass Factory-Funktionen hier gewinnen, da this mehrdeutig sein kann. Das Schreiben von this.#privateVariable erzeugt auch längeren Code im Vergleich zur Verwendung von privateVariable selbst.

Schließlich haben wir in diesem Artikel einen einfachen Zähler sowohl mit Klassen als auch mit Factory-Funktionen erstellt. Sie haben gelernt, wie Sie Event-Listener zu beiden objektorientierten Programmiergeschmacksrichtungen hinzufügen. Hier funktionieren beide Geschmacksrichtungen. Sie müssen nur vorsichtig sein, ob Sie this verwenden oder nicht.

Das ist alles!

Ich hoffe, das wirft etwas Licht auf die objektorientierte Programmierung in JavaScript für Sie. Wenn Ihnen dieser Artikel gefallen hat, gefällt Ihnen vielleicht mein JavaScript-Kurs, Learn JavaScript, in dem ich (fast) alles erkläre, was Sie über JavaScript wissen müssen, in einem so klaren und prägnanten Format wie diesem.

Wenn Sie Fragen zu JavaScript oder Frontend-Entwicklung im Allgemeinen haben, können Sie sich gerne an mich wenden. Ich werde sehen, wie ich helfen kann!