Guten Tag, Freunde!
Heute möchte ich mit Ihnen über drei Vorschläge sprechen, die sich auf JavaScript-Klassen beziehen und sich in drei Phasen befinden:
- Definition von Klassenfeldern
- private Methoden und Class Getter / Setter
- Funktionen für statische Klassen: statische öffentliche Felder, statische private Felder und statische private Methoden
Wenn man bedenkt, dass diese Vorschläge vollständig der Logik der Weiterentwicklung von Klassen entsprechen und die vorhandene Syntax verwenden, können Sie sicher sein, dass sie ohne größere Änderungen standardisiert werden. Dies wird auch durch die Implementierung der genannten "Funktionen" in modernen Browsern belegt.
Denken wir daran, welche Klassen in JavaScript enthalten sind.
Klassen sind größtenteils sogenannter "syntaktischer Zucker" (Abstraktion oder einfacher ein Wrapper) für Konstruktorfunktionen. Solche Funktionen werden verwendet, um das Konstruktorentwurfsmuster zu implementieren. Dieses Muster wird wiederum (in JavaScript) unter Verwendung des prototypischen Vererbungsmodells implementiert. Das prototypische Vererbungsmodell wird manchmal als eigenständiges "Prototyp" -Muster definiert. Weitere Informationen zu Entwurfsmustern finden Sie hier .
Was ist ein Prototyp? Es ist ein Objekt, das als Blaupause oder Blaupause für andere Objekte - Instanzen - fungiert. Ein Konstruktor ist eine Funktion, mit der Sie Instanzobjekte basierend auf einem Prototyp (Klasse, Oberklasse, abstrakte Klasse usw.) erstellen können. Der Prozess der Übergabe von Eigenschaften und Funktionen vom Prototyp an die Instanz wird als Vererbung bezeichnet. Eigenschaften und Funktionen in der Klassenterminologie werden normalerweise als Felder und Methoden bezeichnet, aber de facto sind sie ein und dasselbe.
Wie sieht eine Konstruktorfunktion aus?
//
'use strict'
function Counter(initialValue = 0) {
this.count = initialValue
// , this
console.log(this)
}
Wir definieren eine "Counter" -Funktion, die einen "initialValue" -Parameter mit einem Standardwert von 0 verwendet. Dieser Parameter wird der Instanzeigenschaft "count" zugewiesen, wenn die Instanz initialisiert wird. Der Kontext "this" ist in diesem Fall das von der Funktion erstellte (zurückgegebene) Objekt. Um JavaScript anzuweisen, nicht nur eine Funktion, sondern auch eine Konstruktorfunktion aufzurufen, müssen Sie das Schlüsselwort "new" verwenden:
const counter = new Counter() // { count: 0, __proto__: Object }
Wie wir sehen können, gibt die Konstruktorfunktion ein Objekt mit einer Eigenschaft zurück, die wir als "count" definiert haben, und einen Prototyp (__proto__) als globales Objekt "Object", auf das Prototypketten fast aller Typen (Daten) in JavaScript zurückgehen (außer für Objekte ohne Prototyp, die mit Object.create (null) erstellt wurden. Deshalb sagen sie, dass in JavaScript "alles ein Objekt ist".
Wenn Sie eine Konstruktorfunktion ohne "new" aufrufen, wird ein "TypeError" (Typfehler) ausgelöst, der angibt, dass "die Eigenschaft 'count' nicht undefiniert zugewiesen werden kann":
const counter = Counter() // TypeError: Cannot set property 'count' of undefined
//
const counter = Counter() // Window
Dies liegt daran, dass der Wert "this" innerhalb einer Funktion im strengen Modus "undefiniert" und das globale Objekt "Window" im nicht strengen Modus "undefiniert" ist.
Fügen wir der Konstruktorfunktion verteilte (gemeinsam genutzte, allen Instanzen gemeinsame) Methoden hinzu, um den Zählerwert zu erhöhen, zu verringern, zurückzusetzen und abzurufen:
Counter.prototype.increment = function () {
this.count += 1
// this,
return this
}
Counter.prototype.decrement = function () {
this.count -= 1
return this
}
Counter.prototype.reset = function () {
this.count = 0
return this
}
Counter.prototype.getInfo = function () {
console.log(this.count)
return this
}
Wenn Sie Methoden in der Konstruktorfunktion selbst und nicht in ihrem Prototyp definieren, werden für jede Instanz eigene Methoden erstellt, was es schwierig machen kann, die Funktionalität der Instanzen anschließend zu ändern. Bisher konnte dies auch zu Leistungsproblemen führen.
Das Hinzufügen mehrerer Methoden zum Prototyp einer Konstruktorfunktion kann wie folgt optimiert werden:
;(function () {
this.increment = function () {
this.count += 1
return this
}
this.decrement = function () {
this.count -= 1
return this
}
this.reset = function () {
this.count = 0
return this
}
this.getInfo = function () {
console.log(this.count)
return this
}
// -
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/call
}.call(Counter.prototype))
Oder Sie können es noch einfacher machen:
// ,
Object.assign(Counter.prototype, {
increment() {
this.count += 1
return this
},
decrement() {
this.count -= 1
return this
},
reset() {
this.count = 0
return this
},
getInfo() {
console.log(this.count)
return this
}
})
Verwenden wir unsere Methoden:
counter
.increment()
.increment()
.getInfo() // 2
.decrement()
.getInfo() // 1
.reset()
.getInfo() // 0
Die Klassensyntax ist prägnanter:
class _Counter {
constructor(initialValue = 0) {
this.count = initialValue
}
increment() {
this.count += 1
return this
}
decrement() {
this.count -= 1
return this
}
reset() {
this.count = 0
return this
}
getInfo() {
console.log(this.count)
return this
}
}
const _counter = new _Counter()
_counter
.increment()
.increment()
.getInfo() // 2
.decrement()
.getInfo() // 1
.reset()
.getInfo() // 0
Schauen wir uns ein komplexeres Beispiel an, um zu demonstrieren, wie die JavaScript-Vererbung funktioniert. Erstellen wir eine Klasse "Person" und ihre Unterklasse "SubPerson".
Die Person-Klasse definiert die Eigenschaften Vorname, Nachname und Alter sowie getFullName (Vor- und Nachnamen abrufen), getAge (Alter abrufen) und saySomething “(eine Phrase).
Die SubPerson-Unterklasse erbt alle Eigenschaften und Methoden von Person und definiert außerdem neue Felder für Lebensstil, Fähigkeiten und Interesse sowie neue getInfo-Methoden zum Abrufen des vollständigen Namens, indem die von den Eltern geerbte Methode "getFullName" und "Lebensstil" aufgerufen wird. getSkill "(eine Fertigkeit bekommen)," getLike "(ein Hobby bekommen) und" setLike "(ein Hobby definieren).
Konstruktorfunktion:
const log = console.log
function Person({ firstName, lastName, age }) {
this.firstName = firstName
this.lastName = lastName
this.age = age
}
;(function () {
this.getFullName = function () {
log(` ${this.firstName} ${this.lastName}`)
return this
}
this.getAge = function () {
log(` ${this.age} `)
return this
}
this.saySomething = function (phrase) {
log(` : "${phrase}"`)
return this
}
}.call(Person.prototype))
const person = new Person({
firstName: '',
lastName: '',
age: 30
})
person.getFullName().getAge().saySomething('!')
/*
30
: "!"
*/
function SubPerson({ lifestyle, skill, ...rest }) {
// Person SubPerson
Person.call(this, rest)
this.lifestyle = lifestyle
this.skill = skill
this.interest = null
}
// Person SubPerson
SubPerson.prototype = Object.create(Person.prototype)
//
Object.assign(SubPerson.prototype, {
getInfo() {
this.getFullName()
log(` ${this.lifestyle}`)
return this
},
getSkill() {
log(` ${this.lifestyle} ${this.skill}`)
return this
},
getLike() {
log(
` ${this.lifestyle} ${
this.interest ? ` ${this.interest}` : ' '
}`
)
return this
},
setLike(value) {
this.interest = value
return this
}
})
const developer = new SubPerson({
firstName: '',
lastName: '',
age: 25,
lifestyle: '',
skill: ' JavaScript'
})
developer
.getInfo()
.getAge()
.saySomething(' - !')
.getSkill()
.getLike()
/*
25
: " - !"
JavaScript
*/
developer.setLike(' ').getLike()
//
Klasse:
const log = console.log
class _Person {
constructor({ firstName, lastName, age }) {
this.firstName = firstName
this.lastName = lastName
this.age = age
}
getFullName() {
log(` ${this.firstName} ${this.lastName}`)
return this
}
getAge() {
log(` ${this.age} `)
return this
}
saySomething(phrase) {
log(` : "${phrase}"`)
return this
}
}
const _person = new Person({
firstName: '',
lastName: '',
age: 30
})
_person.getFullName().getAge().saySomething('!')
/*
30
: "!"
*/
class _SubPerson extends _Person {
constructor({ lifestyle, skill /*, ...rest*/ }) {
// super() Person.call(this, rest)
// super(rest)
super()
this.lifestyle = lifestyle
this.skill = skill
this.interest = null
}
getInfo() {
// super.getFullName()
this.getFullName()
log(` ${this.lifestyle}`)
return this
}
getSkill() {
log(` ${this.lifestyle} ${this.skill}`)
return this
}
get like() {
log(
` ${this.lifestyle} ${
this.interest ? ` ${this.interest}` : ' '
}`
)
}
set like(value) {
this.interest = value
}
}
const _developer = new SubPerson({
firstName: '',
lastName: '',
age: 25,
lifestyle: '',
skill: ' JavaScript'
})
_developer
.getInfo()
.getAge()
.saySomething(' - !')
.getSkill().like
/*
25
: " - !"
JavaScript
*/
developer.like = ' '
developer.like
//
Ich denke hier ist alles klar. Weitermachen.
Das Hauptproblem der Vererbung in JavaScript war und ist das Fehlen einer integrierten Mehrfachvererbung, d. H. die Fähigkeit einer Unterklasse, Eigenschaften und Methoden mehrerer Klassen gleichzeitig zu erben. Da in JavaScript alles möglich ist, können wir natürlich mehrere Vererbungen simulieren, zum Beispiel mit diesem Mixin:
// https://www.typescriptlang.org/docs/handbook/mixins.html
function applyMixins(derivedCtor, constructors) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
)
})
})
}
class A {
sayHi() {
console.log(`${this.name} : "!"`)
}
sameName() {
console.log(' ')
}
}
class B {
sayBye() {
console.log(`${this.name} : "!"`)
}
sameName() {
console.log(' B')
}
}
class C {
name = ''
}
applyMixins(C, [A, B])
const c = new C()
// , A
c.sayHi() // : "!"
// , B
c.sayBye() // : "!"
//
c.sameName() // B
Dies ist jedoch keine vollständige Lösung und nur ein Hack, um JavaScript in das Framework der objektorientierten Programmierung zu integrieren.
Gehen wir direkt zu den Innovationen, die die am Anfang des Artikels angegebenen Vorschläge bieten.
Angesichts der standardisierten Funktionen sieht die Klassensyntax heute folgendermaßen aus:
const log = console.log
class C {
constructor() {
this.publicInstanceField = ' '
this.#privateInstanceField = ' '
}
publicInstanceMethod() {
log(' ')
}
//
getPrivateInstanceField() {
log(this.#privateInstanceField)
}
static publicClassMethod() {
log(' ')
}
}
const c = new C()
console.log(c.publicInstanceField) //
//
// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing class
c.getPrivateInstanceField() //
c.publicInstanceMethod() //
C.publicClassMethod() //
Es stellt sich heraus, dass wir öffentliche und private Felder und öffentliche Methoden von Instanzen sowie öffentliche Methoden einer Klasse definieren können, aber wir können keine privaten Methoden von Instanzen sowie öffentliche und private Felder einer Klasse definieren. Tatsächlich ist es immer noch möglich, ein öffentliches Feld einer Klasse zu definieren:
C.publicClassField = ' '
console.log(C.publicClassField) //
Aber Sie müssen zugeben, dass es nicht sehr gut aussieht. Es scheint, dass wir wieder mit Prototypen arbeiten.
Mit dem ersten Vorschlag können Sie öffentliche und private Instanzfelder definieren, ohne einen Konstruktor zu verwenden:
publicInstanceField = ' '
#privateInstanceField = ' '
Mit dem zweiten Vorschlag können Sie Methoden für private Instanzen definieren:
#privateInstanceMethod() {
log(' ')
}
//
getPrivateInstanceMethod() {
this.#privateInstanceMethod()
}
Und schließlich können Sie mit dem dritten Vorschlag öffentliche und private (statische) Felder sowie private (statische) Methoden einer Klasse definieren:
static publicClassField = ' '
static #privateClassField = ' '
static #privateClassMethod() {
log(' ')
}
//
static getPrivateClassField() {
log(C.#privateClassField)
}
//
static getPrivateClassMethod() {
C.#privateClassMethod()
}
So wird das komplette Set aussehen (tatsächlich sieht es bereits aus):
const log = console.log
class C {
// class field declarations
// https://github.com/tc39/proposal-class-fields
publicInstanceField = ' '
#privateInstanceField = ' '
publicInstanceMethod() {
log(' ')
}
// private methods and getter/setters
// https://github.com/tc39/proposal-private-methods
#privateInstanceMethod() {
log(' ')
}
//
getPrivateInstanceField() {
log(this.#privateInstanceField)
}
//
getPrivateInstanceMethod() {
this.#privateInstanceMethod()
}
// static class features
// https://github.com/tc39/proposal-static-class-features
static publicClassField = ' '
static #privateClassField = ' '
static publicClassMethod() {
log(' ')
}
static #privateClassMethod() {
log(' ')
}
//
static getPrivateClassField() {
log(C.#privateClassField)
}
//
static getPrivateClassMethod() {
C.#privateClassMethod()
}
//
getPublicAndPrivateClassFieldsFromInstance() {
log(C.publicClassField)
log(C.#privateClassField)
}
//
static getPublicAndPrivateInstanceFieldsFromClass() {
log(this.publicInstanceField)
log(this.#privateInstanceField)
}
}
const c = new C()
console.log(c.publicInstanceField) //
//
// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing class
c.getPrivateInstanceField() //
c.publicInstanceMethod() //
//
// c.#privateInstanceMethod() // Error
c.getPrivateInstanceMethod() //
console.log(C.publicClassField) //
// console.log(C.#privateClassField) // Error
C.getPrivateClassField() //
C.publicClassMethod() //
// C.#privateClassMethod() // Error
C.getPrivateClassMethod() //
c.getPublicAndPrivateClassFieldsFromInstance()
//
//
// ,
//
// C.getPublicAndPrivateInstanceFieldsFromClass()
// undefined
// TypeError: Cannot read private member #privateInstanceField from an object whose class did not declare it
Alles wäre in Ordnung, nur es gibt eine interessante Nuance: Private Felder werden nicht vererbt. In TypeScript und anderen Programmiersprachen gibt es eine spezielle Eigenschaft, die normalerweise als "geschützt" bezeichnet wird und auf die nicht direkt zugegriffen werden kann, die jedoch zusammen mit öffentlichen Eigenschaften vererbt werden kann.
Es ist erwähnenswert, dass die Wörter "privat", "öffentlich" und "geschützt" reservierte Wörter in JavaScript sind. Wenn Sie versuchen, sie im strengen Modus zu verwenden, wird eine Ausnahme ausgelöst:
const private = '' // SyntaxError: Unexpected strict mode reserved word
const public = '' // Error
const protected = '' // Error
Daher bleibt die Hoffnung auf die Implementierung geschützter Klassenfelder in ferner Zukunft bestehen.
Ich mache Sie auf die Tatsache aufmerksam, dass die Technik der Kapselung von Variablen, d.h. Ihr Schutz vor Zugriff von außen ist so alt wie JavaScript. Vor der Standardisierung privater Klassenfelder wurden häufig Verschlüsse verwendet, um Variablen sowie die Entwurfsmuster Factory und Module auszublenden. Schauen wir uns diese Muster am Beispiel eines Einkaufswagens an.
Modul:
const products = [
{
id: '1',
title: '',
price: 50
},
{
id: '2',
title: '',
price: 150
},
{
id: '3',
title: '',
price: 100
}
]
const cartModule = (() => {
let cart = []
function getProductCount() {
return cart.length
}
function getTotalPrice() {
return cart.reduce((total, { price }) => (total += price), 0)
}
return {
addProducts(products) {
products.forEach((product) => {
cart.push(product)
})
},
removeProduct(obj) {
for (const key in obj) {
cart = cart.filter((prod) => prod[key] !== obj[key])
}
},
getInfo() {
console.log(
` ${getProductCount()} () ${
getProductCount() > 1 ? ' ' : ''
} ${getTotalPrice()} `
)
}
}
})()
//
console.log(cartModule) // { addProducts: ƒ, removeProduct: ƒ, getInfo: ƒ }
//
cartModule.addProducts(products)
cartModule.getInfo()
// 3 () 300
// 2
cartModule.removeProduct({ id: '2' })
cartModule.getInfo()
// 2 () 150
//
console.log(cartModule.cart) // undefined
// cartModule.getProductCount() // TypeError: cartModule.getProductCount is not a function
Fabrik:
function cartFactory() {
let cart = []
function getProductCount() {
return cart.length
}
function getTotalPrice() {
return cart.reduce((total, { price }) => (total += price), 0)
}
return {
addProducts(products) {
products.forEach((product) => {
cart.push(product)
})
},
removeProduct(obj) {
for (const key in obj) {
cart = cart.filter((prod) => prod[key] !== obj[key])
}
},
getInfo() {
console.log(
` ${getProductCount()} () ${
getProductCount() > 1 ? ' ' : ''
} ${getTotalPrice()} `
)
}
}
}
const cart = cartFactory()
cart.addProducts(products)
cart.getInfo()
// 3 () 300
cart.removeProduct({ title: '' })
cart.getInfo()
// 2 () 200
console.log(cart.cart) // undefined
// cart.getProductCount() // TypeError: cart.getProductCount is not a function
Klasse:
class Cart {
#cart = []
#getProductCount() {
return this.#cart.length
}
#getTotalPrice() {
return this.#cart.reduce((total, { price }) => (total += price), 0)
}
addProducts(products) {
this.#cart.push(...products)
}
removeProduct(obj) {
for (const key in obj) {
this.#cart = this.#cart.filter((prod) => prod[key] !== obj[key])
}
}
getInfo() {
console.log(
` ${this.#getProductCount()} () ${
this.#getProductCount() > 1 ? ' ' : ''
} ${this.#getTotalPrice()} `
)
}
}
const _cart = new Cart()
_cart.addProducts(products)
_cart.getInfo()
// 3 () 300
_cart.removeProduct({ id: '1', price: 100 })
_cart.getInfo()
// 1 () 150
console.log(_cart.cart) // undefined
// console.log(_cart.#cart) // SyntaxError: Private field '#cart' must be declared in an enclosing class
// _cart.getTotalPrice() // TypeError: cart.getTotalPrice is not a function
// _cart.#getTotalPrice() // Error
Wie wir sehen können, sind die Muster "Module" und "Factory" der Klasse in keiner Weise unterlegen, außer dass die Syntax der letzteren etwas prägnanter ist, aber Sie die Verwendung des Schlüsselworts "this" vollständig aufgeben können. Das Hauptproblem ist der Kontextverlust bei Verwendung in Pfeilfunktionen und Ereignishandlern. Dies erfordert das Binden an eine Instanz im Konstruktor.
Schauen wir uns zum Schluss ein Beispiel für das Erstellen einer Schaltflächen-Webkomponente mithilfe der Klassensyntax an (aus dem Text eines der Sätze mit einer geringfügigen Änderung).
Unsere Komponente erweitert das integrierte HTML-Element der Schaltfläche um Folgendes: Wenn Sie mit der linken Maustaste auf die Schaltfläche klicken, wird der Zählerwert um 1 erhöht. Wenn Sie mit der rechten Maustaste auf die Schaltfläche klicken, wird der Zählerwert um 1 verringert 1. Gleichzeitig können wir beliebig viele Schaltflächen mit eigenem Kontext und Status verwenden:
// https://developer.mozilla.org/ru/docs/Web/Web_Components
class Counter extends HTMLButtonElement {
#xValue = 0
get #x() {
return this.#xValue
}
set #x(value) {
this.#xValue = value
//
// https://developer.mozilla.org/ru/docs/DOM/window.requestAnimationFrame
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
requestAnimationFrame(this.#render.bind(this))
}
#increment() {
this.#x++
}
#decrement(e) {
//
e.preventDefault()
this.#x--
}
constructor() {
super()
//
this.onclick = this.#increment.bind(this)
this.oncontextmenu = this.#decrement.bind(this)
}
// React/Vue , , DOM
connectedCallback() {
this.#render()
}
#render() {
// , 0 -
this.textContent = `${this.#x} - ${
this.#x < 0 ? '' : ''
} ${this.#x & 1 ? '' : ''} `
}
}
// -
customElements.define('btn-counter', Counter, { extends: 'button' })
Ergebnis:
Es scheint, dass Klassen einerseits erst dann eine breite Akzeptanz in der Entwicklergemeinde finden, wenn sie gelöst sind. Nennen wir es „dieses Problem“. Es ist kein Zufall, dass das React-Team nach langer Zeit mit Klassen (Klassenkomponenten) diese zugunsten von Funktionen (Hooks) fallen ließ. Ein ähnlicher Trend ist in der Vue Composition API zu beobachten. Auf der anderen Seite arbeiten viele der ECMAScript-Entwickler, Webkomponenteningenieure bei Google und das TypeScript-Team aktiv an der Entwicklung der "objektorientierten" Komponente von JavaScript, sodass Sie in den nächsten Jahren keine Rabatte auf Klassen gewähren sollten.
Der gesamte Code im Artikel ist hier .
Weitere Informationen zu objektorientiertem JavaScript finden Sie hier .
Der Artikel war etwas länger als geplant, aber ich hoffe, Sie waren interessiert. Vielen Dank für Ihre Aufmerksamkeit und einen schönen Tag.