Waren Sie jemals in einer Situation, in der Sie sich gewünscht hätten, Sie könnten die Werte in einem Objekt oder einer Array kontrollieren? Vielleicht wollten Sie bestimmte Datentypen verhindern oder die Daten validieren, bevor Sie sie im Objekt speichern. Angenommen, Sie wollten auf eingehende Daten auf irgendeine Weise reagieren, oder sogar auf ausgehende Daten? Zum Beispiel wollten Sie vielleicht das DOM aktualisieren, indem Sie Ergebnisse anzeigen oder Klassen für Stiländerungen tauschen, wenn sich Daten ändern. Wollten Sie schon einmal an einer einfachen Idee oder einem Seitenabschnitt arbeiten, der einige der Funktionen eines Frameworks wie Vue oder React benötigte, aber Sie wollten keine neue App starten?
Dann ist JavaScript Proxy vielleicht genau das Richtige für Sie!
Eine kurze Einführung
Ich sage gleich vorweg: Wenn es um Front-End-Technologien geht, bin ich eher ein UI-Entwickler; sehr ähnlich wie die *nicht* auf JavaScript fokussierte Seite von The Great Divide beschrieben wird. Ich bin glücklich damit, einfach gut aussehende Projekte zu erstellen, die in Browsern konsistent sind, und all die Eigenheiten, die damit einhergehen. Wenn es also um reinere JavaScript-Features geht, neige ich dazu, nicht zu tief einzusteigen.
Dennoch recherchiere ich gerne und suche immer nach etwas, das meiner Liste neuer Dinge zum Lernen hinzugefügt werden kann. Es stellt sich heraus, dass JavaScript-Proxies ein interessantes Thema sind, denn schon das Überfliegen der Grundlagen eröffnet viele mögliche Ideen, wie man diese Funktion nutzen kann. Trotzdem kann der Code auf den ersten Blick schnell schwer werden. Das hängt natürlich davon ab, was Sie brauchen.
Das Konzept des Proxy-Objekts gibt es schon seit einiger Zeit. In meiner Recherche konnte ich Verweise darauf finden, die mehrere Jahre zurückreichen. Doch es stand nicht ganz oben auf meiner Liste, da es bisher keine Unterstützung im Internet Explorer hatte. Im Vergleich dazu hat es seit Jahren exzellente Unterstützung in allen anderen Browsern. Dies ist einer der Gründe, warum Vue 3 nicht mit Internet Explorer 11 kompatibel ist, da es den Proxy im neuesten Vue-Projekt verwendet.
Also, was genau ist das Proxy-Objekt?
Das Proxy-Objekt
MDN beschreibt das Proxy-Objekt als etwas, das
[…] es Ihnen ermöglicht, einen Proxy für ein anderes Objekt zu erstellen, der grundlegende Operationen für dieses Objekt abfangen und neu definieren kann.
Die allgemeine Idee ist, dass Sie ein Objekt mit Funktionalität erstellen können, die Ihnen die Kontrolle über typische Operationen ermöglicht, die bei der Verwendung eines Objekts auftreten. Die beiden häufigsten sind das Abrufen und das Setzen von Werten, die im Objekt gespeichert sind.
const myObj = {
mykey: 'value'
}
console.log(myObj.mykey); // "gets" value of the key, outputs 'value'
myObj.mykey = 'updated'; // "sets" value of the key, makes it 'updated'
In unserem Proxy-Objekt würden wir also „Traps“ (Fallen) erstellen, um diese Operationen abzufangen und die gewünschte Funktionalität auszuführen. Es gibt bis zu dreizehn solcher Traps. Ich werde nicht unbedingt alle Traps behandeln, da nicht alle für meine einfachen Beispiele notwendig sind. Auch hier hängt es davon ab, was Sie für den jeweiligen Kontext benötigen, den Sie erstellen möchten. Vertrauen Sie mir, mit den Grundlagen kann man schon sehr weit kommen.
Um unser obiges Beispiel zur Erstellung eines Proxys zu erweitern, würden wir etwas Ähnliches tun:
const myObj = {
mykey: 'value'
}
const handler = {
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
return true;
}
}
const proxy = new Proxy(myObj, handler);
console.log(proxy.mykey); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it 'updated'
Zuerst beginnen wir mit unserem Standardobjekt. Dann erstellen wir ein Handler-Objekt, das die **Handler-Funktionen**, oft Traps genannt, enthält. Diese repräsentieren die Operationen, die auf einem traditionellen Objekt ausgeführt werden können, in diesem Fall `get` und `set`, die Dinge ohne Änderungen weiterleiten. Danach erstellen wir unseren Proxy mit dem Konstruktor, unserem Zielobjekt und dem Handler-Objekt. Zu diesem Zeitpunkt können wir das Proxy-Objekt beim Abrufen und Setzen von Werten referenzieren, was ein Proxy für das ursprüngliche Zielobjekt `myObj` sein wird.
Beachten Sie `return true` am Ende des `set`-Traps. Das soll dem Proxy mitteilen, dass das Setzen des Werts als erfolgreich betrachtet werden soll. In einigen Situationen, in denen Sie verhindern möchten, dass ein Wert gesetzt wird (denken Sie an einen Validierungsfehler), würden Sie stattdessen `false` zurückgeben. Dies würde auch einen Konsolenfehler mit einem `TypeError` ausgeben.
Nun, eines ist bei diesem Muster zu beachten: Das ursprüngliche Zielobjekt ist immer noch verfügbar. Das bedeutet, Sie könnten den Proxy umgehen und Werte des Objekts ändern, ohne den Proxy zu verwenden. Beim Lesen über die Verwendung des `Proxy`-Objekts habe ich nützliche Muster gefunden, die dabei helfen können.
let myObj = {
mykey: 'value'
}
const handler = {
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
return true;
}
}
myObj = new Proxy(myObj, handler);
console.log(myObj.mykey); // "gets" value of the key, outputs 'value'
myObj.mykey = 'updated'; // "sets" value of the key, makes it 'updated'
In diesem Muster verwenden wir das Zielobjekt als Proxy-Objekt und referenzieren das Zielobjekt innerhalb des Proxy-Konstruktors. Ja, das ist passiert. Das funktioniert, aber ich fand es etwas leicht, sich darüber zu verwirren, was gerade passiert. Lassen Sie uns also das Zielobjekt stattdessen innerhalb des Proxy-Konstruktors erstellen.
const handler = {
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
return true;
}
}
const proxy = new Proxy({
mykey: 'value'
}, handler);
console.log(proxy.mykey); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it 'updated'
Aus diesem Grund könnten wir sowohl das Ziel- als auch das Handler-Objekt innerhalb des Konstruktors erstellen, wenn wir es bevorzugen.
const proxy = new Proxy({
mykey: 'value'
}, {
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
return true;
}
});
console.log(proxy.mykey); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it 'updated'
Tatsächlich ist dies das gängigste Muster, das ich in meinen folgenden Beispielen verwende. Glücklicherweise gibt es Flexibilität bei der Erstellung eines Proxy-Objekts. Verwenden Sie einfach das Muster, das Ihnen am besten passt.
Die folgenden Beispiele behandeln die Verwendung von JavaScript Proxy von grundlegender Datenvalidierung bis hin zur Aktualisierung von Formulardaten mit einem Fetch. Beachten Sie, dass diese Beispiele wirklich die Grundlagen von JavaScript Proxy abdecken; es kann schnell tiefer gehen, wenn Sie möchten. In einigen Fällen geht es nur darum, normalen JavaScript-Code zu schreiben, der normale JavaScript-Dinge innerhalb des Proxy-Objekts tut. Betrachten Sie sie als Möglichkeiten, einige gängige JavaScript-Aufgaben mit mehr Kontrolle über die Daten zu erweitern.
Ein einfaches Beispiel für eine einfache Frage
Mein erstes Beispiel behandelt eine Frage, die ich schon immer als eher simplistisch und seltsam bei Coding-Interviews empfand: einen String umkehren. Ich mochte sie nie und stelle sie nie, wenn ich ein Interview führe. Da ich gerne gegen den Strom schwimme, habe ich mit unkonventionellen Lösungen gespielt. Wissen Sie, nur um sie manchmal zum Spaß zu erwähnen, und eine dieser Lösungen ist ein gutes Stück Front-End-Spaß. Sie eignet sich auch gut als einfaches Beispiel für die Verwendung eines Proxys.
Wenn Sie in das Eingabefeld tippen, sehen Sie, was immer eingegeben wird, umgekehrt unten angezeigt. Offensichtlich könnten hier viele Wege zum Umkehren eines Strings verwendet werden. Lassen Sie uns jedoch meinen seltsamen Weg der Umkehrung durchgehen.
const reverse = new Proxy(
{
value: ''
},
{
set: function (target, prop, value) {
target[prop] = value;
document.querySelectorAll('[data-reverse]').forEach(item => {
let el = document.createElement('div');
el.innerHTML = '\u{202E}' + value;
item.innerText = el.innerHTML;
});
return true;
}
}
)
document.querySelector('input').addEventListener('input', e => {
reverse.value = e.target.value;
});
Zuerst erstellen wir unseren neuen Proxy und das Zielobjekt ist ein einzelner Schlüssel `value`, der das enthält, was immer in das Eingabefeld eingegeben wird. Der `get`-Trap ist nicht vorhanden, da wir nur einen einfachen Durchlauf benötigen, da keine wirkliche Funktionalität damit verbunden ist. In diesem Fall muss nichts getan werden. Dazu kommen wir später.
Für den `set`-Trap haben wir eine kleine Funktionalität auszuführen. Es gibt immer noch einen einfachen Durchlauf, bei dem der Wert wie üblich dem `value`-Schlüssel im Zielobjekt zugewiesen wird. Dann gibt es einen `querySelectorAll`, der alle Elemente auf der Seite mit einem `data-reverse`-Datenattribut findet. Dies ermöglicht es uns, mehrere Elemente auf der Seite anzusprechen und sie alle auf einmal zu aktualisieren. Dies gibt uns unsere Framework-ähnliche Bindungsaktion, die jeder gerne sehen möchte. Dies könnte auch erweitert werden, um Eingabefelder anzusprechen, um eine echte Zwei-Wege-Bindung zu ermöglichen.
Hier kommt mein kleiner, lustiger und eigenartiger Weg zum Umkehren eines Strings zum Einsatz. Ein `div` wird im Speicher erstellt und dann wird das `innerHTML` des Elements mit einem String aktualisiert. Der erste Teil des Strings verwendet einen speziellen Unicode-Dezimalcode, der tatsächlich alles danach umkehrt und es von rechts nach links anzeigt. Der `innerText` des tatsächlichen Elements auf der Seite wird dann mit dem `innerHTML` des `div` im Speicher gefüllt. Dies geschieht jedes Mal, wenn etwas in das Eingabefeld eingegeben wird; daher werden alle Elemente mit dem `data-reverse`-Attribut aktualisiert.
Schließlich richten wir einen Event-Listener am Eingabefeld ein, der den `value`-Schlüssel in unserem Zielobjekt mit dem Wert des Eingabefelds setzt, auf das das Ereignis abzielt.
Am Ende ein sehr einfaches Beispiel für die Ausführung eines Nebeneffekts auf dem DOM der Seite durch Setzen eines Werts im Objekt.
Live-Formatierung eines Eingabewerts
Ein gängiges UI-Muster ist die Formatierung des Werts eines Eingabefelds in eine genauere Sequenz als nur eine Zeichenkette aus Buchstaben und Zahlen. Ein Beispiel dafür ist ein Telefon-Eingabefeld. Manchmal sieht und fühlt es sich besser an, wenn die eingegebene Telefonnummer tatsächlich wie eine Telefonnummer aussieht. Der Trick ist jedoch, dass wir, wenn wir den Wert des Eingabefelds formatieren, wahrscheinlich immer noch eine unformatierte Version der Daten wünschen.
Dies ist eine einfache Aufgabe für einen JavaScript Proxy.
Während Sie Zahlen in das Eingabefeld tippen, werden sie in ein Standard-US-Telefonnummernformat (z. B. `(123) 456-7890`) formatiert. Beachten Sie auch, dass die Telefonnummer wie im Beispiel mit der umgekehrten Zeichenkette oben als Klartext unter dem Eingabefeld angezeigt wird. Die Schaltfläche gibt sowohl die formatierte als auch die unformatierte Version der Daten in die Konsole aus.
Hier ist also der Code für den Proxy:
const phone = new Proxy(
{
_clean: '',
number: '',
get clean() {
return this._clean;
}
},
{
get: function (target, prop) {
if (!prop.startsWith('_')) {
return target[prop];
} else {
return 'entry not found!'
}
},
set: function (target, prop, value) {
if (!prop.startsWith('_')) {
target._clean = value.replace(/\D/g, '').substring(0, 10);
const sections = {
area: target._clean.substring(0, 3),
prefix: target._clean.substring(3, 6),
line: target._clean.substring(6, 10)
}
target.number =
target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` :
target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` :
target._clean.length > 0 ? `(${sections.area}` : '';
document.querySelectorAll('[data-phone_number]').forEach(item => {
if (item.tagName === 'INPUT') {
item.value = target.number;
} else {
item.innerText = target.number;
}
});
return true;
} else {
return false;
}
}
}
);
Es gibt noch mehr Code in diesem Beispiel, also lassen Sie uns ihn aufschlüsseln. Der erste Teil ist das Zielobjekt, das wir innerhalb des Proxys selbst initialisieren. Dort passieren drei Dinge.
{
_clean: '',
number: '',
get clean() {
return this._clean;
}
},
Der erste Schlüssel, `_clean`, ist unsere Variable, die die unformatierte Version unserer Daten enthält. Sie beginnt mit einem Unterstrich, was ein traditionelles Namensmuster ist, um sie als „privat“ zu betrachten. Wir möchten sie unter normalen Umständen unzugänglich machen. Dazu wird später mehr.
Der zweite Schlüssel, `number`, enthält einfach den formatierten Wert der Telefonnummer.
Der dritte „Schlüssel“ ist eine `get`-Funktion mit dem Namen `clean`. Diese gibt den Wert unserer privaten `_clean`-Variable zurück. In diesem Fall geben wir einfach den Wert zurück, aber dies bietet die Möglichkeit, bei Bedarf auch andere Dinge damit zu tun. Dies ist wie ein Proxy-Getter für die `get`-Funktion des Proxys. Es erscheint seltsam, aber es ist eine einfache Möglichkeit, unsere Daten zu steuern. Je nach Ihren spezifischen Bedürfnissen kann dies eine recht simple Methode zur Bewältigung dieser Situation sein. Für unser einfaches Beispiel hier funktioniert es, aber es könnten auch andere Schritte unternommen werden.
Nun zum `get`-Trap des Proxys.
get: function (target, prop) {
if (!prop.startsWith('_')) {
return target[prop];
} else {
return 'entry not found!'
}
},
Zuerst prüfen wir, ob die eingehende `prop` (Eigenschaft) oder der Objekt-Schlüssel nicht mit einem Unterstrich beginnt. Wenn sie nicht mit einem Unterstrich beginnt, geben wir sie einfach zurück. Wenn sie es tut, geben wir einen String zurück, der besagt, dass der Eintrag nicht gefunden wurde. Diese Art von negativer Rückgabe könnte je nach Bedarf unterschiedlich gehandhabt werden. Geben Sie einen String zurück, geben Sie einen Fehler zurück oder führen Sie Code mit unterschiedlichen Nebeneffekten aus. Alles hängt von der Situation ab.
Eine Sache, die man in meinem Beispiel beachten sollte, ist, dass ich keine anderen Proxy-Traps behandle, die bei einer als privat betrachteten Variablen im Proxy ins Spiel kommen könnten. Für einen umfassenderen Schutz dieser Daten müssten Sie andere Traps berücksichtigen, wie z. B. `[defineProperty]`(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/defineProperty), deleteProperty oder ownKeys — typischerweise alles, was mit der Manipulation oder Referenzierung von Objekt-Schlüsseln zu tun hat. Ob Sie so weit gehen, könnte davon abhängen, wer den Proxy nutzt. Wenn er für Sie ist, dann wissen Sie, wie Sie den Proxy verwenden. Aber wenn er für jemand anderen ist, sollten Sie erwägen, die Dinge so weit wie möglich abzusichern.
Nun kommt der Teil, in dem der meiste Zauber für dieses Beispiel passiert – der `set`-Trap.
set: function (target, prop, value) {
if (!prop.startsWith('_')) {
target._clean = value.replace(/\D/g, '').substring(0, 10);
const sections = {
area: target._clean.substring(0, 3),
prefix: target._clean.substring(3, 6),
line: target._clean.substring(6, 10)
}
target.number =
target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` :
target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` :
target._clean.length > 0 ? `(${sections.area}` : '';
document.querySelectorAll('[data-phone_number]').forEach(item => {
if (item.tagName === 'INPUT') {
item.value = target.number;
} else {
item.innerText = target.number;
}
});
return true;
} else {
return false;
}
}
Zuerst die gleiche Prüfung gegen die private Variable, die wir im Proxy haben. Ich teste nicht wirklich auf andere Arten von Props, aber Sie könnten hier erwägen, das zu tun. Ich gehe nur davon aus, dass der `number`-Schlüssel im Proxy-Zielobjekt angepasst wird.
Der eingehende Wert, der Wert des Eingabefelds, wird von allem außer Zahlenzeichen befreit und dem `_clean`-Schlüssel zugewiesen. Dieser Wert wird dann verwendet, um den formatierten Wert neu zu erstellen. Im Grunde wird jedes Mal, wenn Sie tippen, die gesamte Zeichenkette im erwarteten Format neu aufgebaut, live. Die `substring`-Methode hält die Zahl auf zehn Ziffern beschränkt.
Dann wird ein `sections`-Objekt erstellt, um die verschiedenen Abschnitte unserer Telefonnummer basierend auf der Aufschlüsselung einer US-Telefonnummer zu speichern. Wenn die `_clean`-Variable länger wird, aktualisieren wir `number` zu einem Formatierungsmuster, das wir zu diesem Zeitpunkt sehen möchten.
Ein `querySelectorAll` sucht nach jedem Element, das das `data-phone_number`-Datenattribut hat, und durchläuft es mit einer `forEach`-Schleife. Wenn das Element ein Eingabefeld ist, wird der Wert aktualisiert, und `innerText` von allem anderen wird aktualisiert. So erscheint der Text unter dem Eingabefeld. Wenn wir ein weiteres Eingabefeld mit diesem Datenattribut platzieren würden, würden wir seinen Wert in Echtzeit aktualisiert sehen. Dies ist eine Möglichkeit, eine Ein-Wege- oder Zwei-Wege-Bindung zu erstellen, je nach Anforderungen.
Am Ende wird `true` zurückgegeben, um dem Proxy mitzuteilen, dass alles gut gelaufen ist. Wenn die eingehende `prop` oder der Schlüssel mit einem Unterstrich beginnt, wird stattdessen `false` zurückgegeben.
Schließlich die Event-Listener, die dies ermöglichen:
document.querySelectorAll('input[data-phone_number]').forEach(item => {
item.addEventListener('input', (e) => {
phone.number = e.target.value;
});
});
document.querySelector('#get_data').addEventListener('click', (e) => {
console.log(phone.number); // (123) 456-7890
console.log(phone.clean); // 1234567890
});
Das erste Set findet alle Eingabefelder mit unserem spezifischen Datenattribut und fügt ihnen einen Event-Listener hinzu. Für jedes Eingabeereignis wird der Wert des `number`-Schlüssels des Proxys mit dem aktuellen Wert des Eingabefelds aktualisiert. Da wir den Wert, der jedes Mal gesendet wird, formatieren, entfernen wir alle Zeichen, die keine Zahlen sind.
Das zweite Set findet die Schaltfläche, die beide Datensätze, wie gewünscht, in die Konsole ausgibt. Dies zeigt, wie wir Code schreiben könnten, der die benötigten Daten jederzeit abruft. Hoffentlich ist klar, dass `phone.clean` sich auf unsere `get`-Proxy-Funktion im Zielobjekt bezieht, die die `_clean`-Variable im Objekt zurückgibt. Beachten Sie, dass sie nicht als Funktion aufgerufen wird, wie z. B. `phone.clean()`, da sie als `get`-Proxy in unserem Proxy fungiert.
Speichern von Zahlen in einem Array
Anstelle eines Objekts könnten Sie ein Array als Ziel-„Objekt“ im Proxy verwenden. Da es sich um ein Array handelt, gibt es einige Dinge zu beachten. Funktionen eines Arrays wie `push()` würden im Setter-Trap des Proxys auf bestimmte Weise behandelt. Außerdem funktioniert die Erstellung einer benutzerdefinierten Funktion innerhalb des Zielobjektkonzepts in diesem Fall nicht wirklich. Dennoch gibt es einige nützliche Dinge, die durch die Verwendung eines Arrays als Ziel getan werden können.
Sicher, das Speichern von Zahlen in einem Array ist nichts Neues. Offensichtlich. Dennoch werde ich diesem Zahlen-Array ein paar Regeln auferlegen, wie z. B. keine sich wiederholenden Werte und nur Zahlen zulassen. Ich werde auch einige Ausgabeoptionen bereitstellen, wie z. B. Sortieren, Summieren, Mitteln und Löschen der Werte. Dann werde ich eine kleine Benutzeroberfläche aktualisieren, die alles steuert.
Hier ist das Proxy-Objekt:
const numbers = new Proxy([],
{
get: function (target, prop) {
message.classList.remove('error');
if (prop === 'sort') return [...target].sort((a, b) => a - b);
if (prop === 'sum') return [...target].reduce((a, b) => a + b);
if (prop === 'average') return [...target].reduce((a, b) => a + b) / target.length;
if (prop === 'clear') {
message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`;
target.splice(0, target.length);
collection.innerText = target;
}
return target[prop];
},
set: function (target, prop, value) {
if (prop === 'length') return true;
dataInput.value = '';
message.classList.remove('error');
if (!Number.isInteger(value)) {
console.error('Data provided is not a number!');
message.innerText = 'Data provided is not a number!';
message.classList.add('error');
return false;
}
if (target.includes(value)) {
console.error(`Number ${value} has already been submitted!`);
message.innerText = `Number ${value} has already been submitted!`;
message.classList.add('error');
return false;
}
target[prop] = value;
collection.innerText = target;
message.innerText = `Number ${value} added!`;
return true;
}
});
Bei diesem Beispiel beginne ich mit dem Setter-Trap.
Das Erste, was zu tun ist, ist die Prüfung auf die `length`-Eigenschaft, die dem Array zugewiesen wird. Sie gibt einfach `true` zurück, damit sie auf normale Weise erfolgt. Es könnte immer Code vorhanden sein, falls wir auf die Zuweisung der Länge reagieren müssen.
Die nächsten beiden Codezeilen beziehen sich auf zwei HTML-Elemente auf der Seite, die mit einem `querySelector` gespeichert sind. `dataInput` ist das Eingabeelement und wir möchten es bei jeder Eingabe leeren. `message` ist das Element, das Antworten auf Änderungen des Arrays enthält. Da es das Konzept eines Fehlerzustands hat, stellen wir sicher, dass es bei jeder Eingabe nicht in diesem Zustand ist.
Das erste `if` prüft, ob die Eingabe tatsächlich eine Zahl ist. Wenn nicht, tut es mehrere Dinge. Es gibt einen Konsolenfehler aus, der das Problem beschreibt. Das `message`-Element erhält die gleiche Aussage. Dann wird die Nachricht über eine CSS-Klasse in einen Fehlerzustand versetzt. Schließlich gibt es `false` zurück, was auch dazu führt, dass der Proxy seinen eigenen Fehler in die Konsole ausgibt.
Das zweite `if` prüft, ob die Eingabe bereits im Array vorhanden ist; denken Sie daran, dass wir keine Wiederholungen wünschen. Wenn es eine Wiederholung gibt, geschieht dieselbe Benachrichtigung wie im ersten `if`. Die Benachrichtigung ist etwas anders, da es sich um ein Template-Literal handelt, damit wir den wiederholten Wert sehen können.
Der letzte Abschnitt geht davon aus, dass alles gut gelaufen ist und die Dinge fortgesetzt werden können. Der Wert wird wie üblich gesetzt und dann aktualisieren wir die `collection`-Liste. `collection` bezieht sich auf ein weiteres Element auf der Seite, das uns die aktuelle Sammlung von Zahlen im Array anzeigt. Wieder wird die Nachricht mit der hinzugefügten Eingabe aktualisiert. Schließlich geben wir `true` zurück, um dem Proxy mitzuteilen, dass alles in Ordnung ist.
Nun ist der `get`-Trap etwas anders als in den vorherigen Beispielen.
get: function (target, prop) {
message.classList.remove('error');
if (prop === 'sort') return [...target].sort((a, b) => a - b);
if (prop === 'sum') return [...target].reduce((a, b) => a + b);
if (prop === 'average') return [...target].reduce((a, b) => a + b) / target.length;
if (prop === 'clear') {
message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`;
target.splice(0, target.length);
collection.innerText = target;
}
return target[prop];
},
Was hier passiert, ist die Ausnutzung einer „Prop“, die keine normale Array-Methode ist; sie wird als Prop an den `get`-Trap weitergegeben. Nehmen wir zum Beispiel den ersten „Prop“, der durch diesen Event-Listener ausgelöst wird:
dataSort.addEventListener('click', () => {
message.innerText = numbers.sort;
});
Wenn also die Sortier-Schaltfläche geklickt wird, wird der `innerText` des `message`-Elements mit dem aktualisiert, was `numbers.sort` zurückgibt. Es fungiert als Getter, den der Proxy abfängt und etwas anderes als typische Array-bezogene Ergebnisse zurückgibt.
Nachdem der potenzielle Fehlerzustand des `message`-Elements entfernt wurde, ermitteln wir, ob etwas anderes als eine Standard-Array-Get-Operation erwartet wird. Jede gibt eine Manipulation der ursprünglichen Array-Daten zurück, ohne das ursprüngliche Array zu verändern. Dies geschieht durch die Verwendung des Spread-Operators auf dem Ziel, um ein neues Array zu erstellen, und dann werden Standard-Array-Methoden verwendet. Jeder Name sollte andeuten, was er tut: sort, sum, average und clear. Nun, okay, `clear` ist keine exakte Standard-Array-Methode, aber es klingt gut. Da die Einträge in beliebiger Reihenfolge vorliegen können, können wir uns die sortierte Liste anzeigen lassen oder Berechnungen auf den Einträgen durchführen. Das Löschen löscht einfach das Array, wie Sie es erwarten würden.
Hier sind die anderen Event-Listener, die für die Schaltflächen verwendet werden:
dataForm.addEventListener('submit', (e) => {
e.preventDefault();
numbers.push(Number.parseInt(dataInput.value));
});
dataSubmit.addEventListener('click', () => {
numbers.push(Number.parseInt(dataInput.value));
});
dataSort.addEventListener('click', () => {
message.innerText = numbers.sort;
});
dataSum.addEventListener('click', () => {
message.innerText = numbers.sum;
});
dataAverage.addEventListener('click', () => {
message.innerText = numbers.average;
});
dataClear.addEventListener('click', () => {
numbers.clear;
});
Es gibt viele Möglichkeiten, wie wir ein Array erweitern und Funktionen hinzufügen könnten. Ich habe Beispiele für ein Array gesehen, das die Auswahl eines Eintrags mit einem negativen Index ermöglicht, der vom Ende zählt. Das Finden eines Eintrags in einem Array von Objekten basierend auf einem Eigenschaftswert innerhalb eines Objekts. Eine Meldung erhalten, wenn versucht wird, auf einen nicht vorhandenen Wert im Array zuzugreifen, anstatt auf `undefined`. Es gibt viele Ideen, die mit einem Proxy für ein Array genutzt und erforscht werden können.
Interaktives Adressformular
Ein Adressformular ist eine ziemlich übliche Sache auf einer Webseite. Fügen wir ihm zum Spaß (und aus nicht-standardmäßigen Gründen) etwas Interaktivität für die Bestätigung hinzu. Es kann auch als Datensammlung der Werte des Formulars innerhalb eines einzigen Objekts fungieren, das bei Bedarf abgerufen werden kann.
Hier ist das Proxy-Objekt:
const model = new Proxy(
{
name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
getData() {
return {
name: this.name || 'no entry!',
address1: this.address1 || 'no entry!',
address2: this.address2 || 'no entry!',
city: this.city || 'no entry!',
state: this.state || 'no entry!',
zip: this.zip || 'no entry!'
};
}
},
{
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
if (prop === 'zip' && value.length === 5) {
fetch(`https://api.zippopotam.us/us/${value}`)
.then(response => response.json())
.then(data => {
model.city = data.places[0]['place name'];
document.querySelector('[data-model="city"]').value = target.city;
model.state = data.places[0]['state abbreviation'];
document.querySelector('[data-model="state"]').value = target.state;
});
}
document.querySelectorAll(`[data-model="${prop}"]`).forEach(item => {
if (item.tagName === 'INPUT' || item.tagName === 'SELECT') {
item.value = value;
} else {
item.innerText = value;
}
})
return true;
}
}
);
Das Zielobjekt ist ziemlich einfach; die Einträge für jedes Eingabefeld im Formular. Die `getData`-Funktion gibt das Objekt zurück, aber wenn eine Eigenschaft einen leeren String als Wert hat, wird sie zu „kein Eintrag!“ geändert. Dies ist optional, aber die Funktion liefert ein saubereres Objekt, als wir es durch einfaches Abrufen des Zustands des Proxy-Objekts erhalten würden.
Die Getter-Funktion leitet die Dinge einfach wie gewohnt weiter. Sie könnten wahrscheinlich darauf verzichten, aber ich nehme sie zur Vollständigkeit auf.
Die Setter-Funktion setzt den Wert für die Prop. Das `if` prüft jedoch, ob die gesetzte Prop zufällig die Postleitzahl ist. Wenn dies der Fall ist, prüfen wir, ob die Länge des Werts fünf ist. Wenn die Auswertung `true` ist, führen wir einen Fetch durch, der eine Adressfinder-API mit der Postleitzahl anspricht. Alle zurückgegebenen Werte werden in die Objekteigenschaften, das Stadt-Eingabefeld, eingefügt und der Bundesstaat im Auswahlfeld ausgewählt. Dies ist ein Beispiel für eine praktische Abkürzung, damit die Leute diese Werte nicht eingeben müssen. Die Werte können bei Bedarf manuell geändert werden.
Für den nächsten Abschnitt sehen wir uns ein Beispiel für ein Eingabeelement an.
<input class="in__input" id="name" data-model="name" placeholder="name" />
Der Proxy hat einen `querySelectorAll`, der nach allen Elementen sucht, die ein passendes Datenattribut haben. Dies ist dasselbe wie im Beispiel mit der umgekehrten Zeichenkette, das wir zuvor gesehen haben. Wenn er eine Übereinstimmung findet, aktualisiert er entweder den Wert des Eingabefelds oder den `innerText` des Elements. So wird die gedrehte Karte in Echtzeit aktualisiert, um zu zeigen, wie die ausgefüllte Adresse aussehen wird.
Eine Sache, die man beachten sollte, ist das `data-model`-Attribut auf den Eingabefeldern. Der Wert dieses Datenattributs informiert den Proxy tatsächlich, an welchen Schlüssel er sich während seiner Operationen anheften soll. Der Proxy findet die beteiligten Elemente basierend auf diesem Schlüssel. Der Event-Listener tut im Wesentlichen dasselbe, indem er dem Proxy mitteilt, welcher Schlüssel im Spiel ist. So sieht das aus:
document.querySelector('main').addEventListener('input', (e) => {
model[e.target.dataset.model] = e.target.value;
});
Somit werden alle Eingabefelder innerhalb des Hauptelements angesprochen, und wenn das `input`-Ereignis ausgelöst wird, wird der Proxy aktualisiert. Der Wert des `data-model`-Attributs wird verwendet, um zu bestimmen, welcher Schlüssel im Proxy angesprochen werden soll. Im Wesentlichen haben wir ein modellähnliches System im Einsatz. Denken Sie darüber nach, wie so etwas weiter genutzt werden könnte.
Und was ist mit der „get data“-Schaltfläche? Es ist ein einfacher Konsolen-Log der `getData`-Funktion…
getDataBtn.addEventListener('click', () => {
console.log(model.getData());
});
Dies war ein unterhaltsames Beispiel zum Erstellen und Verwenden, um das Konzept zu erforschen. Dies ist die Art von Beispiel, die mich darüber nachdenken lässt, was ich mit dem JavaScript Proxy bauen könnte. Manchmal möchte man einfach ein kleines Widget, das etwas Datenerfassung/-schutz und die Möglichkeit hat, das DOM zu manipulieren, nur durch Interaktion mit Daten. Ja, man könnte Vue oder React verwenden, aber manchmal können selbst diese für so etwas Einfaches zu viel sein.
Das war's erstmal
„Vorerst“ bedeutet, dass das davon abhängen könnte, ob jeder von Ihnen etwas tiefer in den JavaScript Proxy eintauchen wird. Wie ich zu Beginn dieses Artikels sagte, behandle ich nur die Grundlagen dieser Funktion. Es gibt noch viel mehr, was sie bieten kann, und sie kann größer werden als die von mir bereitgestellten Beispiele. In einigen Fällen könnte sie die Grundlage für einen kleinen Helfer für eine Nischenlösung bilden. Es ist offensichtlich, dass die Beispiele leicht mit einfachen Funktionen erstellt werden könnten, die viel ähnliche Funktionalität bieten. Sogar die meisten meiner Beispielcodes sind reguläres JavaScript gemischt mit dem Proxy-Objekt.
Der Punkt ist jedoch, Beispiele für die Verwendung des Proxys anzubieten, um zu zeigen, wie man auf Dateninteraktionen reagieren könnte – sogar die Steuerung, wie auf diese Interaktionen reagiert werden kann, um Daten zu schützen, Daten zu validieren, das DOM zu manipulieren und neue Daten abzurufen – alles basierend darauf, dass jemand versucht, die Daten zu speichern oder abzurufen. Langfristig kann dies sehr mächtig sein und einfache Anwendungen ermöglichen, die keine größere Bibliothek oder Framework erfordern.
Wenn Sie also ein Front-End-Entwickler sind, der sich mehr auf die UI-Seite konzentriert, wie ich selbst, können Sie sich mit den Grundlagen beschäftigen, um zu sehen, ob es kleinere Projekte gibt, die von JavaScript Proxy profitieren könnten. Wenn Sie mehr ein JavaScript-Entwickler sind, dann können Sie tiefer in den Proxy für größere Projekte eintauchen. Vielleicht ein neues Framework oder eine neue Bibliothek?
Nur ein Gedanke...
Ich habe eine kleine Bibliothek mit Proxy geschrieben, um JS-Objekte mit einigen versteckten Metadaten zu annotieren, falls jemand interessiert ist: https://www.npmjs.com/package/proxy-extend
Danke für diesen Beitrag. Ich bin bis zum Teil „einfaches Beispiel für eine einfache Frage“ gekommen, wo es so aussieht, als würde die von rechts nach links gerichtete HTML-Entität genau das mit dem Code selbst machen. d.h. ihn rückwärts machen. Hat mich ein bisschen verwirrt!!!
el.innerHTML = ‘;eulav + ‘
Ja, das sah so aus, als würde die HTML-Entität ihre Arbeit im Codebeispiel verrichten. Sie sollte jetzt escaped sein oder Sie können sich das CodePen ansehen, um zu sehen, wie es aussieht.
Ich habe kürzlich damit begonnen, Proxies für zustandsgesteuerte DOM-Manipulationen zu verwenden. Hauptsächlich nützlich bei Projekten, die die Schwelle zum vollständigen Framework noch nicht ganz erreichen, aber dennoch von einem zustandsähnlichen Workflow profitieren könnten, um das jQuery-ähnliche Spaghetti-Chaos in Schach zu halten. (Wenn ich darüber nachdenke, könnte dies sogar zusammen mit jQuery verwendet werden, um viele Probleme zu lösen, auf die Menschen bei der Skalierung damit stoßen, denke ich.)
Ich habe eine Hilfsklasse geschrieben (nicht ganz eine Bibliothek, per se), die auch State-Batching und Debouncing von DOM-Operationen enthält. Sie aktualisieren Ihren Zustand basierend auf beliebigen Ereignissen in der
constructor-Methode und führen dann alle DOM-Manipulationsarbeiten in dersync-Methode durch. Mag nicht jedermanns Sache sein, aber ich mag diese Trennung wirklich, da sie mir hilft, in deklarierteren Begriffen zu denken (auch wenn es technisch gesehen immer noch "imperative Programmierung" ist).Falls es jemanden interessiert
Vue verwendet Proxies für sein Reaktivitätssystem.
Ich verstehe nicht, warum ein Proxy überhaupt verwendet werden muss, wenn man die Werte einfach erfassen und eine normale Funktion ausführen könnte, um diese Manipulationen durchzuführen (und die Browserkompatibilität wiederherstellen und die Komplexität reduzieren).
Das ist absolut richtig, für einige dieser einfachen Beispiele könnte man problemlos mit traditionellen Funktionen auskommen. Ein Unterschied ist, dass die Verwendung des Proxy Ihnen die Kontrolle über die Daten ermöglicht, die in Ihrem Zielobjekt enthalten sind, um unerwünschte Nebeneffekte zu verhindern. Wenn das Objekt auf traditionelle Weise verfügbar ist, sind diese Daten außerhalb Ihrer Kontrolle. Ein weiterer Unterschied könnte sein, dass die Funktionalität innerhalb desselben Proxy-Objekts enthalten ist, was in einigen Fällen nützlich sein könnte.
Es würde wirklich auf Ihre Bedürfnisse in der jeweiligen Situation ankommen.