Objektorientiertes JavaScript in einfachen Worten





Guten Tag, Freunde!



In JavaScript gibt es vier Möglichkeiten, ein Objekt zu erstellen:



  • Konstruktorfunktion
  • Klasse (Klasse)
  • Objektverknüpfung mit einem anderen Objekt (OLOO)
  • Werksfunktion


Welche Methode sollten Sie anwenden? Welches ist das beste?



Um diese Fragen zu beantworten, werden wir nicht nur jeden Ansatz einzeln betrachten, sondern auch Klassen und Factory-Funktionen anhand der folgenden Kriterien vergleichen: Vererbung, Kapselung, das Schlüsselwort "this", Ereignishandler.



Beginnen wir mit der objektorientierten Programmierung (OOP).



Was ist OOP?



Im Wesentlichen ist OOP eine Methode zum Schreiben von Code, mit der Sie Objekte mit einem einzelnen Objekt erstellen können. Dies ist auch die Essenz des Konstruktor-Entwurfsmusters. Ein freigegebenes Objekt wird normalerweise als Blaupause, Blaupause oder Blaupause bezeichnet, und die Objekte, die es erstellt, sind Instanzen.



Jede Instanz verfügt sowohl über vom übergeordneten Element geerbte als auch über eigene Eigenschaften. Wenn wir beispielsweise ein Human-Projekt haben, können wir darauf basierend Instanzen mit unterschiedlichen Namen erstellen.



Der zweite Aspekt von OOP ist die Strukturierung des Codes, wenn wir mehrere Projekte auf verschiedenen Ebenen haben. Dies wird als Vererbung oder Unterklasse bezeichnet.



Der dritte Aspekt von OOP ist die Kapselung, wenn wir Implementierungsdetails vor Außenstehenden verbergen und Variablen und Funktionen von außen unzugänglich machen. Dies ist die Essenz der Entwurfsmuster für Module und Fassaden.



Fahren wir mit den Methoden zum Erstellen von Objekten fort.



Objekterstellungsmethoden



Konstruktorfunktion


Konstruktoren sind Funktionen, die das Schlüsselwort "this" verwenden.



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


Auf diese Weise können Sie die eindeutigen Werte der zu erstellenden Instanz speichern und darauf zugreifen. Instanzen werden mit dem Schlüsselwort "new" erstellt.



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


Klasse


Klassen sind eine Abstraktion ("syntaktischer Zucker") über Konstruktorfunktionen. Sie erleichtern das Erstellen von Instanzen.



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


Beachten Sie, dass der Konstruktor denselben Code enthält wie die obige Konstruktorfunktion. Wir müssen dies tun, um dies zu initialisieren. Wir können den Konstruktor weglassen, wenn wir keine Anfangswerte zuweisen müssen.



Auf den ersten Blick scheinen Klassen komplexer zu sein als Konstruktoren - Sie müssen mehr Code schreiben. Halten Sie Ihre Pferde und springen Sie nicht zu Schlussfolgerungen. Der Unterricht ist cool. Sie werden etwas später verstehen, warum.



Instanzen werden auch mit dem Schlüsselwort "new" erstellt.



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

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


Objekte verknüpfen


Diese Methode zum Erstellen von Objekten wurde von Kyle Simpson vorgeschlagen. In diesem Ansatz definieren wir das Projekt als gewöhnliches Objekt. Anschließend initialisieren wir die Instanz mithilfe einer Methode (die normalerweise als init bezeichnet wird, aber im Gegensatz zum Konstruktor in der Klasse nicht erforderlich ist).



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


Mit Object.create wird eine Instanz erstellt. Nach der Instanziierung wird init aufgerufen.



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

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


Der Code kann ein wenig verbessert werden, indem er an init zurückgegeben wird.



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

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


Werksfunktion


Eine Factory-Funktion ist eine Funktion, die ein Objekt zurückgibt. Jedes Objekt kann zurückgegeben werden. Sie können sogar eine Instanz einer Klasse oder Objektbindungen zurückgeben.



Hier ist ein einfaches Beispiel für eine Factory-Funktion.



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


Wir brauchen das Schlüsselwort "this" nicht, um eine Instanz zu erstellen. Wir rufen einfach die Funktion auf.



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

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


Schauen wir uns nun Möglichkeiten zum Hinzufügen von Eigenschaften und Methoden an.



Eigenschaften und Methoden definieren



Methoden sind Funktionen, die als Eigenschaften eines Objekts deklariert sind.



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


In OOP gibt es zwei Möglichkeiten, Eigenschaften und Methoden zu definieren:



  • In einer Instanz
  • Im Prototyp


Eigenschaften und Methoden im Konstruktor definieren


Um eine Eigenschaft für eine Instanz zu definieren, müssen Sie sie der Konstruktorfunktion hinzufügen. Stellen Sie sicher, dass Sie die Eigenschaft hinzufügen.



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)






Methoden werden normalerweise im Prototyp definiert, da dadurch vermieden wird, dass für jede Instanz eine Funktion erstellt wird, d. H. Ermöglicht allen Instanzen die gemeinsame Nutzung einer einzelnen Funktion (als gemeinsam genutzte oder verteilte Funktion bezeichnet).



Verwenden Sie den Prototyp, um dem Prototyp eine Eigenschaft hinzuzufügen.



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

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






Das Erstellen mehrerer Methoden kann mühsam sein.



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


Mit Object.assign können Sie sich das Leben leichter machen.



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


Eigenschaften und Methoden in einer Klasse definieren


Instanzeigenschaften können im Konstruktor definiert werden.



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

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






Prototyp-Eigenschaften werden nach dem Konstruktor als normale Funktion definiert.



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

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






Das Erstellen mehrerer Methoden in einer Klasse ist einfacher als in einem Konstruktor. Wir brauchen dafür kein Object.assign. Wir fügen nur weitere Funktionen hinzu.



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

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


Definieren von Eigenschaften und Methoden beim Binden von Objekten


Um Eigenschaften für eine Instanz zu definieren, fügen wir dieser eine Eigenschaft hinzu.



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)






Die Prototypmethode wird als reguläres Objekt definiert.



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






Definieren von Eigenschaften und Methoden in Factory-Funktionen (FF)


Eigenschaften und Methoden können in das zurückgegebene Objekt aufgenommen werden.



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






Bei Verwendung von FF können Sie keine Prototyp-Eigenschaften definieren. Wenn Sie solche Eigenschaften benötigen, können Sie eine Instanz der Klassen-, Konstruktor- oder Objektbindung zurückgeben (dies ist jedoch nicht sinnvoll).



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


Wo werden Eigenschaften und Methoden definiert?



Wo sollten Sie Eigenschaften und Methoden definieren? Instanz oder Prototyp?



Viele Leute denken, dass Prototypen dafür besser sind.



Es ist jedoch wirklich nicht wirklich wichtig.



Durch das Definieren von Eigenschaften und Methoden für eine Instanz verbraucht jede Instanz mehr Speicher. Bei der Definition von Methoden in Prototypen wird weniger Speicher verbraucht, jedoch nicht wesentlich. Angesichts der Leistung moderner Computer ist dieser Unterschied nicht signifikant. Tun Sie also, was für Sie am besten funktioniert, und bevorzugen Sie dennoch Prototypen.



Wenn Sie beispielsweise Klassen oder Objektbindungen verwenden, ist es besser, Prototypen zu verwenden, da dies das Schreiben des Codes erleichtert. Im Fall von FF können keine Prototypen verwendet werden. Es können nur Eigenschaften von Instanzen definiert werden.



Ca. per .: Ich werde mir erlauben, mit dem Autor nicht einverstanden zu sein. Das Problem der Verwendung von Prototypen anstelle von Instanzen bei der Definition von Eigenschaften und Methoden hängt nicht nur vom Speicherverbrauch ab, sondern vor allem vom Zweck der zu definierenden Eigenschaft oder Methode. Wenn eine Eigenschaft oder Methode für jede Instanz eindeutig sein muss, muss sie für die Instanz definiert werden. Wenn eine Eigenschaft oder Methode für alle Instanzen gleich (gemeinsam) sein soll, muss sie im Prototyp definiert werden. Im letzteren Fall reicht es aus, wenn Sie Änderungen an einer Eigenschaft oder Methode vornehmen müssen, diese am Prototyp vorzunehmen, im Gegensatz zu Eigenschaften und Methoden von Instanzen, die individuell angepasst werden.



Vorläufige Schlussfolgerung



Basierend auf dem untersuchten Material können mehrere Schlussfolgerungen gezogen werden. Es ist meine persönliche Meinung.



  • Klassen sind besser als Konstruktoren, da sie das Definieren mehrerer Methoden erleichtern.
  • Die Objektbindung erscheint seltsam, da Object.create verwendet werden muss. Ich habe das immer wieder vergessen, als ich diesen Ansatz studierte. Für mich war dies Grund genug, die weitere Verwendung abzulehnen.
  • Klassen und FFs sind am einfachsten zu verwenden. Das Problem ist, dass Prototypen in FF nicht verwendet werden können. Aber wie ich bereits erwähnt habe, spielt es keine Rolle.


Als Nächstes werden Klassen und FFs als die beiden besten Möglichkeiten zum Erstellen von Objekten in JavaScript verglichen.



Klassen vs. FF - Vererbung



Bevor Sie mit dem Vergleichen von Klassen und FFs fortfahren, müssen Sie sich mit den drei Konzepten vertraut machen, die OOP zugrunde liegen:



  • Erbe
  • Verkapselung
  • Dies


Beginnen wir mit der Vererbung.



Was ist Vererbung?


In JavaScript bedeutet Vererbung das Übergeben von Eigenschaften vom übergeordneten zum untergeordneten Element, d. H. vom Projekt zur Instanz.



Dies geschieht auf zwei Arten:



  • Verwenden der Instanzinitialisierung
  • unter Verwendung einer Prototypkette


Im zweiten Fall wird das übergeordnete Projekt um ein untergeordnetes Projekt erweitert. Dies wird als Unterklasse bezeichnet, aber einige nennen es auch Vererbung.



Unterklassen verstehen


Unterklassen sind, wenn ein untergeordnetes Projekt das übergeordnete Projekt erweitert.



Schauen wir uns das Beispiel von Klassen an.



Unterklasse mit einer Klasse


Das Schlüsselwort "erweitert" wird verwendet, um die übergeordnete Klasse zu erweitern.



class Child extends Parent {
    // ...
}


Erstellen wir beispielsweise eine Developer-Klasse, die die Human-Klasse erweitert.



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

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


Die Entwicklerklasse erweitert Human wie folgt:



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

    // ...
}


Das Schlüsselwort "super" ruft den Konstruktor der Klasse "Human" auf. Wenn Sie dies nicht benötigen, kann Super weggelassen werden.



class Developer extends Human {
  // ...
}


Angenommen, Entwickler können Code schreiben (wer hätte das gedacht). Fügen wir eine entsprechende Methode hinzu.



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


Hier ist ein Beispiel für eine Instanz der Klasse "Developer".



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






Unterklasse mit FF


Um Unterklassen mit FF zu erstellen, müssen Sie 4 Schritte ausführen:



  • Erstelle einen neuen FF
  • Erstellen Sie eine Instanz des übergeordneten Projekts
  • Erstellen Sie eine Kopie dieser Instanz
  • Fügen Sie dieser Kopie Eigenschaften und Methoden hinzu


Dieser Prozess sieht so aus.



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


Erstellen wir eine Unterklasse "Entwickler". So sieht der FF "Human" aus.



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


Entwickler erstellen.



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


Fügen Sie die "Code" -Methode hinzu.



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


Wir erstellen eine Instanz von Developer.



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






Überschreiben der übergeordneten Methode


Manchmal ist es erforderlich, eine übergeordnete Methode innerhalb einer Unterklasse zu überschreiben. Dies kann wie folgt erfolgen:



  • Erstellen Sie eine Methode mit demselben Namen
  • Rufen Sie die übergeordnete Methode auf (optional)
  • Erstellen Sie eine neue Methode in der Unterklasse


Dieser Prozess sieht so aus.



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

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

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






Der gleiche Prozess mit 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()






Vererbung versus Zusammensetzung


Ein Gespräch über Vererbung geht selten ohne Erwähnung der Zusammensetzung. Experten wie Eric Elliot glauben, dass Komposition wann immer möglich verwendet werden sollte.



Was ist Zusammensetzung?



Komposition verstehen


Komposition ist im Grunde die Kombination mehrerer Dinge zu einem. Die häufigste und einfachste Möglichkeit, Objekte zu kombinieren, ist die Verwendung von Object.assign.



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


Die Zusammensetzung lässt sich am einfachsten anhand eines Beispiels erklären. Angenommen, wir haben zwei Unterklassen: Entwickler und Designer. Designer wissen, wie man entwirft, und Entwickler wissen, wie man Code schreibt. Beide erben von der Klasse "Mensch".



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


Angenommen, wir möchten eine dritte Unterklasse erstellen. Diese Unterklasse sollte eine Mischung aus Designer und Entwickler sein - sie sollte in der Lage sein, Code zu entwerfen und zu schreiben. Nennen wir es DesignerDeveloper (oder DeveloperDesigner, wenn Sie es vorziehen).



Wie schaffen wir es?



Wir können die Klassen "Designer" und "Entwickler" nicht gleichzeitig erweitern. Dies ist nicht möglich, da wir nicht entscheiden können, welche Eigenschaften zuerst kommen sollen. Dies wird als Diamantproblem (Diamantvererbung) bezeichnet .







Das Rautenproblem kann mit Object.assign gelöst werden, wenn wir einem Objekt Vorrang vor einem anderen geben. JavaScript unterstützt jedoch keine Mehrfachvererbung.



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


Hier bietet sich die Komposition an.



In diesem Ansatz wird Folgendes angegeben: Erstellen Sie anstelle von DesignerDeveloper ein Objekt, das Fähigkeiten enthält, die Sie nach Bedarf in Unterklassen unterteilen können.



Die Implementierung dieses Ansatzes führt zu Folgendem.



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


Wir brauchen die Human-Klasse nicht mehr, da wir mit dem angegebenen Objekt drei verschiedene Klassen erstellen können.



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)






Wir können dasselbe für Designer und Entwickler 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 für eine Instanz erstellen? Dies ist nur eine der möglichen Optionen. Wir können auch Methoden in den Prototyp einfügen, aber ich finde das unnötig (dieser Ansatz scheint, als wären wir wieder bei den Konstruktoren).



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

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






Verwenden Sie den Ansatz, den Sie für richtig halten. Das Ergebnis wird das gleiche sein.



Komposition mit FF


Bei der Komposition mit FF werden dem zurückgegebenen Objekt verteilte Methoden hinzugefügt.



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






Vererbung und Zusammensetzung


Niemand hat gesagt, dass wir Vererbung und Komposition nicht gleichzeitig verwenden können.



Wenn Sie zu den Beispielen Designer, Developer und DesignerDeveloper zurückkehren, sollten Sie beachten, dass sie auch menschlich sind. Daher können sie die menschliche Klasse erweitern.



Hier ist ein Beispiel für Vererbung und Komposition unter Verwendung der Klassensyntax.



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






Und hier ist das gleiche mit der Verwendung von 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
  })
}






Unterklassen in der realen Welt


Während viele Experten argumentieren, dass die Zusammensetzung flexibler (und daher nützlicher) ist als Unterklassen, sollten Unterklassen nicht diskontiert werden. Viele der Dinge, mit denen wir uns befassen, basieren auf dieser Strategie.



Beispiel: Das "Klick" -Ereignis ist ein MouseEvent. MouseEvent ist eine Unterklasse von UIEvent (Benutzeroberflächenereignis), die wiederum eine Unterklasse von Ereignis (Ereignis) ist.







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







Vorläufige Schlussfolgerung zur Vererbung


Vererbung und Zusammensetzung können sowohl in Klassen als auch in FF verwendet werden. In FF sieht die Komposition "sauberer" aus, aber dies ist ein kleiner Vorteil gegenüber Klassen.



Setzen wir den Vergleich fort.



Klassen vs. FF - Kapselung



Grundsätzlich geht es bei der Einkapselung darum, eine Sache in einer anderen zu verstecken und die innere Essenz von außen unzugänglich zu machen.



In JavaScript sind versteckte Entitäten Variablen und Funktionen, die nur im aktuellen Kontext verfügbar sind. In diesem Fall entspricht der Kontext dem Gültigkeitsbereich.



Einfache Kapselung


Die einfachste Form der Kapselung ist ein Codeblock.



{
  // ,  ,     
}


In einem Block können Sie auf eine außerhalb des Blocks deklarierte Variable zugreifen.



const food = 'Hamburger'

{
  console.log(food)
}






Aber nicht umgekehrt.



{
  const food = 'Hamburger'
}

console.log(food)






Beachten Sie, dass Variablen, die mit dem Schlüsselwort "var" deklariert wurden, einen globalen oder funktionalen Bereich haben. Versuchen Sie, var nicht zum Deklarieren von Variablen zu verwenden.



Kapselung mit einer Funktion


Der Funktionsumfang ähnelt dem Blockumfang. In einer Funktion deklarierte Variablen sind nur innerhalb dieser Funktion zugänglich. Dies gilt für alle Variablen, auch für die mit var deklarierten.



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

sayFood()
console.log(food)






Wenn wir uns innerhalb einer Funktion befinden, haben wir Zugriff auf Variablen, die außerhalb dieser Funktion deklariert sind.



const food = 'Hamburger'

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

sayFood()






Funktionen können Werte zurückgeben, die später außerhalb der Funktion verwendet werden können.



function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())






Schließung


Das Schließen ist eine fortgeschrittene Form der Einkapselung. Es ist nur eine Funktion innerhalb einer anderen Funktion.



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




In outdoorFunction deklarierte Variablen 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)
  }
}

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






Kapselung und OOP


Beim Erstellen von Objekten möchten wir, dass einige Eigenschaften öffentlich (öffentlich) und andere privat (privat oder privat) sind.



Schauen wir uns ein Beispiel an. Nehmen wir an, wir haben ein Autoprojekt. Beim Erstellen einer neuen Instanz fügen wir eine "Treibstoff" -Eigenschaft mit einem Wert von 50 hinzu.



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




Benutzer können diese Eigenschaft verwenden, um die verbleibende Kraftstoffmenge zu bestimmen.



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




Benutzer können die Kraftstoffmenge auch selbst einstellen.



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


Fügen wir die Bedingung hinzu, dass der Tank des Autos maximal 100 Liter Kraftstoff fasst. Wir möchten nicht, dass Benutzer die Kraftstoffmenge selbst einstellen können, da sie das Auto kaputt machen können.



Es gibt zwei Möglichkeiten, dies zu tun:



  • Nutzung von Privateigentum durch Konvention
  • mit echten privaten Feldern


Privateigentum nach Vereinbarung


In JavaScript werden private Variablen und Eigenschaften normalerweise mit einem Unterstrich gekennzeichnet.



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


In der Regel erstellen wir Methoden zum Verwalten privater Eigenschaften.



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

  getFuel () {
    return this._fuel
  }

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


Benutzer müssen die Methoden getFuel und setFuel verwenden, um die Kraftstoffmenge zu bestimmen bzw. festzulegen.



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

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


Die Variable "_fuel" ist jedoch nicht wirklich privat. Es ist von außen zugänglich.



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

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


Verwenden Sie echte private Felder, um den Zugriff auf Variablen einzuschränken.



Wirklich private Felder


Felder ist der Begriff, der zum Kombinieren von Variablen, Eigenschaften und Methoden verwendet wird.



Private Klassenfelder


Mit Klassen können Sie private Variablen mit dem Präfix "#" erstellen.



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


Leider kann dieses Präfix im Konstruktor nicht verwendet werden.







Private Variablen müssen außerhalb des Konstruktors definiert werden.



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


In diesem Fall können wir die Variable bei der Definition initialisieren.



class Car {
  #fuel = 50
}


Jetzt ist die Variable "#fuel" nur innerhalb der Klasse verfügbar. Der Versuch, außerhalb der Klasse darauf zuzugreifen, führt zu einem Fehler.



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






Wir brauchen geeignete Methoden, um die Variable zu manipulieren.



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


Ich persönlich bevorzuge es, dafür Getter und Setter zu verwenden. Ich finde diese Syntax besser lesbar.



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 FF-Felder


FFs erstellen automatisch private Felder. Wir müssen nur eine Variable deklarieren. Benutzer können von außen nicht auf diese Variable zugreifen. Dies ist auf die Tatsache zurückzuführen, dass Variablen einen Block- (oder Funktions-) Bereich haben, d.h. sind standardmäßig gekapselt.



function Car () {
  const fuel = 50
}

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


Getter und Setter werden auch verwendet, um die private Variable "Kraftstoff" zu steuern.



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


So. Einfach und leicht!



Vorläufige Schlussfolgerung zur Einkapselung


Die FF-Kapselung ist einfacher und verständlicher. Es basiert auf dem Umfang, der ein wichtiger Bestandteil von JavaScript ist.



Bei der Klassenkapselung wird das Präfix "#" verwendet, was etwas langwierig sein kann.



Klassen gegen FF - das



Dies ist das Hauptargument gegen die Verwendung von Klassen. Warum? Weil die Bedeutung davon abhängt, wo und wie dies verwendet wird. Dieses Verhalten ist nicht nur für Anfänger, sondern auch für erfahrene Entwickler oft verwirrend.



Das Konzept hierfür ist jedoch eigentlich gar nicht so schwierig. Insgesamt gibt es 6 Kontexte, in denen dies verwendet werden kann. Wenn Sie diese Zusammenhänge verstehen, sollten Sie damit keine Probleme haben.



Die genannten Kontexte sind:



  • globaler Kontext
  • Kontext des zu erstellenden Objekts
  • der Kontext einer Eigenschaft oder Methode eines Objekts
  • einfache Funktion
  • Pfeilfunktion
  • Event-Handler-Kontext


Aber zurück zum Artikel. Schauen wir uns die Besonderheiten der Verwendung in Klassen und FFs an.



Verwenden Sie dies in Klassen


Bei Verwendung in einer Klasse zeigt dies auf die Instanz, die erstellt wird (Eigenschafts- / Methodenkontext). Aus diesem Grund wird die Instanz im Konstruktor initialisiert.



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

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






Verwendung in Konstruktorfunktionen


Wenn Sie dies in einer Funktion verwenden und neu zum Erstellen einer Instanz, zeigt dies auf die Instanz.



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

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






Im Gegensatz zu FK in FF zeigt dies auf ein Fenster (im Kontext des Moduls hat dies im Allgemeinen den Wert "undefiniert").



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

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






Daher sollte dies nicht in FF verwendet werden. Dies ist einer der Hauptunterschiede zwischen FF und FC.



Verwenden Sie dies in FF


Um dies in FF verwenden zu können, muss ein Eigenschafts- / Methodenkontext erstellt werden.



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

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






Obwohl wir dies in FF verwenden können, brauchen wir es nicht. Wir können eine Variable erstellen, die auf die Instanz verweist. Stattdessen kann eine solche Variable verwendet werden.



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 genauer als this.firstName, da human explizit auf eine Instanz verweist.



Tatsächlich müssen wir nicht einmal human.firstName schreiben. Wir können uns auf Vorname beschränken, da diese Variable einen lexikalischen Bereich hat (dies ist der Zeitpunkt, an dem der Wert der Variablen aus der externen Umgebung übernommen wird).



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

  return human
}

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






Schauen wir uns ein komplexeres Beispiel an.



Komplexes Beispiel



Die Bedingungen sind wie folgt: Wir haben ein "Human" -Projekt mit den Eigenschaften "firstName" und "lastName" und einer "sayHello" -Methode.



Wir haben auch ein "Entwickler" -Projekt, das von Human erbt. Entwickler wissen, wie man Code schreibt, daher müssen sie eine "Code" -Methode haben. Außerdem müssen sie deklarieren, dass sie sich in der Entwicklerkaste befinden, sodass wir die sayHello-Methode überschreiben müssen.



Lassen Sie uns die angegebene Logik mithilfe von Klassen und FF implementieren.



Klassen


Wir erstellen ein Projekt "Human".



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

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


Erstellen Sie ein "Entwickler" -Projekt mit der "Code" -Methode.



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


Wir überschreiben die "sayHello" -Methode.



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

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


FF (mit diesem)


Wir erstellen ein Projekt "Human".



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


Erstellen Sie ein "Entwickler" -Projekt mit der "Code" -Methode.



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


Wir überschreiben die "sayHello" -Methode.



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 (ohne das)


Da Vorname direkt lexikalisch festgelegt ist, können wir dies weglassen.



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


Vorläufige Schlussfolgerung dazu


In einfachen Worten, Klassen erfordern die Verwendung davon, FFs jedoch nicht. In diesem Fall bevorzuge ich FF, weil:



  • Dieser Kontext kann sich ändern
  • Der mit FF geschriebene Code ist kürzer und sauberer (auch aufgrund der automatischen Kapselung von Variablen).


Klassen vs. FF - Event Handler



Viele Artikel über OOP übersehen die Tatsache, dass wir als Frontend-Entwickler ständig mit Event-Handlern zu tun haben. Sie bieten Interaktion mit Benutzern.



Da Ereignishandler diesen Kontext ändern, kann die Arbeit mit ihnen in Klassen problematisch sein. Gleichzeitig treten solche Probleme in FF nicht auf.



Das Ändern dieses Kontexts spielt jedoch keine Rolle, wenn wir wissen, wie wir damit umgehen sollen. Schauen wir uns ein einfaches Beispiel an.



Zähler erstellen


Um einen Zähler zu erstellen, verwenden wir das gewonnene Wissen, einschließlich privater Variablen.



Unser Zähler enthält zwei Dinge:



  • der Zähler selbst
  • Taste, um den Wert zu erhöhen






So könnte das Markup aussehen:



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


Erstellen eines Zählers mit einer Klasse


Bitten Sie den Benutzer zur Vereinfachung, das Zähler-Markup zu finden und an die Zähler-Klasse zu übergeben:



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

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


Sie müssen 2 Elemente in der Klasse erhalten:



  • <span> enthält den Zählerwert - wir müssen diesen Wert aktualisieren, wenn sich der Zähler erhöht
  • <Button> - Wir müssen einen Ereignishandler für dieses Element hinzufügen


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


Als nächstes initialisieren wir die Variable "count" mit dem Textinhalt von countElement. Die angegebene Variable muss privat sein.



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

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


Wenn die Taste gedrückt wird, muss sich der Wert des Zählers um 1 erhöhen. Wir implementieren dies mit der Methode "growCount".



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

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


Jetzt müssen wir das DOM aktualisieren. Implementieren wir dies mit der "updateCount" -Methode, die in growCount aufgerufen wird:



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

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

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


Es bleibt noch ein Event-Handler hinzuzufügen.



Hinzufügen eines Ereignishandlers


Fügen wir diesem.buttonElement einen Handler hinzu. Leider können wir growCount nicht als Rückruffunktion verwenden. Dies führt zu einem Fehler.



class Counter {
  // ...

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

  // 
}






Die Ausnahme wird ausgelöst, da dies auf buttonElement (Event-Handler-Kontext) verweist. Sie können dies überprüfen, indem Sie diesen Wert auf die Konsole drucken.







Dieser Wert muss geändert werden, um auf die Instanz zu verweisen. Dies kann auf zwei Arten erfolgen:



  • mit bind
  • mit der Pfeilfunktion


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



Hinzufügen eines Ereignishandlers mit bind


bind gibt eine neue Funktion zurück. Als erstes Argument wird ein Objekt übergeben, auf das dies verweist (an das dies gebunden wird).



class Counter {
  // ...

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

  // ...
}


Es funktioniert, aber es sieht nicht gut aus. Darüber hinaus ist Binden eine erweiterte Funktion, mit der Anfänger nur schwer umgehen können.



Pfeilfunktionen


Pfeilfunktionen haben unter anderem keine eigene Funktion. Sie leihen es aus der lexikalischen (externen) Umgebung. Daher kann der Zählercode wie folgt umgeschrieben werden:



class Counter {
  // ...

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

  // 
}


Es gibt einen noch einfacheren Weg. Wir können Erhöhen der Anzahl als Pfeilfunktion erstellen. In diesem Fall zeigt dies auf die Instanz.



class Counter {
  // ...

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

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

  // ...
}


Der Code


Hier ist der vollständige Beispielcode:







Erstellen eines Zählers mit FF


Der Anfang ist ähnlich - wir bitten den Benutzer, das Zähler-Markup zu finden und zu übergeben:



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

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


Wir erhalten die erforderlichen Elemente, die standardmäßig privat sind:



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


Initialisieren wir die Variable "count":



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

  let count = parseInt(countElement.textContext)
}


Der Zählerwert wird mit der Methode "growCount" erhöht. Sie können eine reguläre Funktion verwenden, aber ich bevorzuge einen anderen Ansatz:



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


Das DOM wird mit der "updateCount" -Methode aktualisiert, die in "growCount" aufgerufen wird:



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

    updateCount () {
      increaseCount()
    }
  }
}


Beachten Sie, dass wir counter.updateCount anstelle von this.updateCount verwenden.



Hinzufügen eines Ereignishandlers


Wir können dem buttonElement einen Ereignishandler hinzufügen, indem wir counter.increaseCount als Rückruf verwenden.



Dies funktioniert, da wir dies nicht verwenden. Daher ist es uns egal, dass der Handler den Kontext ändert.



function Counter (counterElement) {
  // 

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

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


Das erste Merkmal davon


Sie können dies in FF verwenden, jedoch nur im Kontext einer Methode.



Im folgenden Beispiel ruft der Aufruf von counter.increaseCount counter.updateCount auf, da dies auf counter zeigt:



function Counter (counterElement) {
  // 

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

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


Der Ereignishandler funktioniert jedoch nicht, da sich dieser Wert geändert hat. Dieses Problem kann mit Binden gelöst werden, nicht jedoch mit Pfeilfunktionen.



Das zweite Merkmal davon


Bei Verwendung der FF-Syntax können keine Methoden in Form von Pfeilfunktionen erstellt werden, da Methoden im Kontext einer Funktion erstellt werden, d. H. Dies zeigt auf das Fenster:



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


Daher empfehle ich bei der Verwendung von FF dringend, dies zu vermeiden.



Der Code








Event-Handler-Urteil


Event-Handler ändern den Wert davon. Gehen Sie daher sehr vorsichtig damit um. Bei der Verwendung von Klassen empfehle ich Ihnen, Event-Handler-Rückrufe in Form von Pfeilfunktionen zu erstellen. Dann müssen Sie keine Bindungsdienste verwenden.



Bei der Verwendung von FF empfehle ich, überhaupt darauf zu verzichten.



Fazit



In diesem Artikel haben wir uns vier Möglichkeiten angesehen, um Objekte in JavaScript zu erstellen:



  • Konstruktorfunktionen
  • Klassen
  • Objekte verknüpfen
  • Werksfunktionen


Zunächst kamen wir zu dem Schluss, dass Klassen und FFs die optimalsten Methoden zum Erstellen von Objekten sind.



Zweitens haben wir gesehen, dass Unterklassen mit Klassen einfacher zu erstellen sind. Bei der Komposition ist es jedoch besser, FF zu verwenden.



Drittens haben wir zusammengefasst, dass FFs bei der Kapselung einen Vorteil gegenüber Klassen haben, da letztere die Verwendung eines speziellen "#" -Präfixes erfordern und FFs Variablen automatisch privat machen.



Viertens können Sie mit FFs darauf verzichten, dies als Instanzreferenz zu verwenden. In Klassen müssen Sie auf einige Tricks zurückgreifen, um diese auf den ursprünglichen Kontext zurückzusetzen, der vom Ereignishandler geändert wurde.



Das ist alles für mich. Ich hoffe dir hat der Artikel gefallen. Danke für die Aufmerksamkeit.



All Articles