Hallo Ingrid
Wäre das nicht sauberer?
String.prototype.escapeHtml = function () {
return this
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
Und statt escapeHtml(visits)
dann visits.escapeHtml()
.
Ich halte das Verändern von eingebauten Objekten für schlechte Praxis.
Davon abgesehen, gibt es hier eine viel schönere Lösung.
Wenn wir Werte in ein Templateliteral einfügen, die escaped werden müssen, dann wollen wir die entsprechende Funktion doch eigentlich nicht für jeden dieser Werte einzeln aufrufen, egal ob als Funktions- oder als Methodenaufruf. Denn zum einen sind wir faul und zum anderen besteht dabei das Risiko, dass wir vergessen, einen der Werte zu escapen.
Wir könnten natürlich vor dem Zusammenbau über die Werte iterieren, aber das wäre unnötig umständlich. Außerdem ist der richtige Zeitpunkt für die kontextgerechte Behandlung, wenn die Werte in den neuen Kontext eingebracht werden.
Die schönere Lösung besteht darin, ein Tagged Template zu verwenden:
const createCounter = (online, visits) => {
document.body.insertAdjacentHTML(
'beforeend',
escapeHTML`
<aside id="counter">
<h2>User Counter</h2>
<label for="online">Online:</label>
<output id="online">${
online
}</output>
<label for="visits">Visits:</label>
<output id="visits">${
visits
}</output>
</aside>
`
);
};
Wird wie oben direkt vor dem Templateliteral ein Tag in Form einer Funktionsreferenz notiert, dann werden die innerhalb des Templates notierten Ausdrücke zwar ausgewertet, aber der String wird nicht konkateniert. Stattdessen wird die angegebene Funktion mit den Teilzeichenketten des Templatestrings und den ermittelten Werten als Argument aufgerufen. Was von der Funktion zurückgegeben wird ist dann das Ergebnis des Ausdrucks.
Betrachten wir zunächst ein einfacheres Beispiel:
const square = ([_before, operator, _after], a, b) => {
return `${a**2}${operator}${b**2}`;
};
const expression = square`${2} + ${3}`;
console.log(expression);
Die Funktion square
wird hier mit drei Argumenten aufgerufen. Das erste Argument ist ein Array, das die Teilstrings enthält. Die beiden anderen Argumente sind die Werte der Ausdrücke, die in dem Templateliteral notiert wurden. Ich destrukturiere hier das Array mit den Teilstrings, um an die Zeichenkette mit dem Operator zu kommen. Am Ende wird der Ausdruck mit den veränderten Werten neu zusammengesetzt.
Hier sollte zwei Aspekten besondere Aufmerksamkeit geschenkt werden:
-
In dem Ausdruck square`${2} + ${3}`
scheint der Templatestring mit einem Platzhalter zu beginnen und mit einem Platzhalter zu enden. Das ist aber nicht richtig. Ein Template beginnt und endet immer mit einem String – in diesem Fall dem leeren String.
-
Die Gesamtzahl der Argumente mit denen die Funktion aufgerufen wird hängt davon ab, wieviele Ausdrücke in dem Template notiert wurden. In diesem Beispiel ist die Anzahl der Ausdrücke bekannt, weshalb wir einzeln benannte Parameter verwenden können.
Wenn es wie in unserem Anwendungsfall darum geht, prinzipiell beliebig lange Zeichenketten mit beliebig vielen Platzhaltern zu verarbeiten, dann können wir uns die Erkenntnis zu nutze machen, dass die Anzahl der Teilstrings genau um eins größer ist als die Anzahl der variablen Werte, die in das Template eingefügt wurden.
Die Funktionssignatur könnte demnach wiefolgt aussehen:
const escapeHTML = ([x, ...xs], ...unsafe) => {};
Wir destrukturieren wieder das Array mit den Teilstrings. Dabei teilen wir unter Verwendung der Restsyntax die Liste der Strings in head und tail. Die zu escapenden Werte, deren Anzahl wir nicht kennen, werden ebenfalls mittels Restsyntax einem Array hinzugefügt.
Wir könnten nun den ersten Teilstring x
als Rückgabewert nehmen und mit einer Schleife über die beiden Arrays iterieren, wobei wir die unsicheren Werte escapen und sie zusammen mit dem jeweils nachfolgenden Teilstring ans Ende von x
anfügen. Ich finde es allerdings schöner, hierfür die Methode reduce
zu verwenden.
const escapeHTML = ([x, ...xs], ...unsafe) =>
xs.reduce((accumulator, safe, i) =>
`${accumulator}${
String(unsafe[i])
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}${safe}`, x);
Wir rufen reduce
auf dem Array mit den restlichen Teilstrings auf, wobei wir den ersten Teilstring als Initialwert übergeben. Unser Reducer ist eine Funktion die drei Argumente entgegennimmt. Das erste Argument ist der Akkumulator – beim ersten Aufruf der Initialwert, bei allen weiteren Aufrufen der Rückgabewert der Funktion. Das zweite Argument ist der Teilstring, der dem zu escapenden Wert nachfolgt. Das dritte und letzte Argument ist der Index.
Wir greifen in jedem Aufruf mit dem Index auf das Array mit den zu escapenden Werten zu. Da wir nicht wissen, ob es sich bei dem Wert wirklich um einen String handelt und wir Typfehler vermeiden wollen, casten wir den Wert, bevor wir die Methode replace
aufrufen. Schließlich verwenden wir wieder ein Templateliteral, um den bereits verarbeiteten String, den escapeten Wert und den nächsten Teilstring zusammenzufügen.
Das Ergebnis ist dann das ursprüngliche Template mit allen Werten kontextgerecht escaped.
Viele Grüße,
Orlok