Fortgeschrittenes Wissen
Der Abschnitt befasst sich eingehender mit den String-Interna. Dieses Wissen wird Ihnen nützlich sein, wenn Sie vorhaben, sich mit Emojis, seltenen mathematischen oder hieroglyphischen Zeichen oder anderen seltenen Symbolen zu befassen.
Wie wir bereits wissen, basieren JavaScript-Strings auf Unicode: Jedes Zeichen wird durch eine Bytefolge von 1–4 Bytes dargestellt.
Mit JavaScript können wir ein Zeichen in eine Zeichenfolge einfügen, indem wir seinen hexadezimalen Unicode-Code mit einer dieser drei Notationen angeben:
xXX
XX
muss aus zwei hexadezimalen Ziffern mit einem Wert zwischen 00
und FF
bestehen, dann ist xXX
das Zeichen, dessen Unicode-Code XX
ist.
Da die xXX
Notation nur zwei hexadezimale Ziffern unterstützt, kann sie nur für die ersten 256 Unicode-Zeichen verwendet werden.
Zu diesen ersten 256 Zeichen gehören das lateinische Alphabet, die meisten grundlegenden Syntaxzeichen und einige andere. Beispielsweise ist "x7A"
dasselbe wie "z"
(Unicode U+007A
).
alarm( "x7A" ); // z alarm( "xA9" ); // ©, das Copyright-Symbol
uXXXX
XXXX
muss aus genau 4 Hexadezimalziffern mit einem Wert zwischen 0000
und FFFF
bestehen, dann ist uXXXX
das Zeichen, dessen Unicode-Code XXXX
ist.
Zeichen mit Unicode-Werten größer als U+FFFF
können ebenfalls mit dieser Notation dargestellt werden, aber in diesem Fall müssen wir ein sogenanntes Ersatzpaar verwenden (wir werden später in diesem Kapitel über Ersatzpaare sprechen).
alarm( "u00A9" ); // ©, dasselbe wie xA9, unter Verwendung der 4-stelligen Hexadezimalschreibweise alarm( "u044F" ); // я, der kyrillische Buchstabe alarm( "u2191" ); // ↑, das Pfeil-nach-oben-Symbol
u{X…XXXXXX}
X…XXXXXX
muss ein Hexadezimalwert von 1 bis 6 Bytes zwischen 0
und 10FFFF
sein (der höchste von Unicode definierte Codepunkt). Mit dieser Notation können wir alle vorhandenen Unicode-Zeichen einfach darstellen.
Alert( "u{20331}" ); // 佫, ein seltenes chinesisches Zeichen (langer Unicode) alarm( "u{1F60D}" ); // ?, ein lächelndes Gesichtssymbol (ein weiterer langer Unicode)
Alle häufig verwendeten Zeichen haben 2-Byte-Codes (4 Hex-Ziffern). Buchstaben in den meisten europäischen Sprachen, Zahlen und die grundlegenden einheitlichen CJK-Ideografiesätze (CJK – aus chinesischen, japanischen und koreanischen Schriftsystemen) haben eine 2-Byte-Darstellung.
Ursprünglich basierte JavaScript auf der UTF-16-Kodierung, die nur 2 Bytes pro Zeichen zuließ. Aber 2 Bytes erlauben nur 65536 Kombinationen und das reicht nicht für jedes mögliche Symbol von Unicode.
Daher werden seltene Symbole, die mehr als 2 Bytes erfordern, mit einem Paar von 2-Byte-Zeichen codiert, die als „Ersatzpaar“ bezeichnet werden.
Als Nebeneffekt beträgt die Länge solcher Symbole 2
:
alarm( '?'.length ); // 2, MATHEMATISCHES SCHRIFTKAPITAL X alarm( '?'.length ); // 2, Gesicht mit Freudentränen alarm( '?'.length ); // 2, ein seltenes chinesisches Schriftzeichen
Das liegt daran, dass es zum Zeitpunkt der JavaScript-Erstellung noch keine Ersatzpaare gab und diese daher von der Sprache nicht korrekt verarbeitet werden!
Tatsächlich haben wir in jeder der oben genannten Zeichenfolgen ein einzelnes Symbol, aber die length
zeigt eine Länge von 2
an.
Es kann auch schwierig sein, ein Symbol zu erhalten, da die meisten Sprachfunktionen Ersatzpaare als zwei Zeichen behandeln.
Hier sehen wir beispielsweise zwei ungerade Zeichen in der Ausgabe:
alarm( '?'[0] ); // zeigt seltsame Symbole... alarm( '?'[1] ); // ...Teile des Ersatzpaares
Teile eines Ersatzpaares haben ohne einander keine Bedeutung. Die Warnungen im obigen Beispiel zeigen also tatsächlich Müll an.
Technisch gesehen sind Surrogatpaare auch an ihren Codes erkennbar: Wenn ein Zeichen den Code im Intervall von 0xd800..0xdbff
hat, dann ist es der erste Teil des Surrogatpaares. Das nächste Zeichen (zweiter Teil) muss den Code im Intervall 0xdc00..0xdfff
haben. Diese Intervalle sind vom Standard ausschließlich für Ersatzpaare reserviert.
Daher wurden in JavaScript die Methoden String.fromCodePoint und str.codePointAt hinzugefügt, um mit Ersatzpaaren umzugehen.
Sie sind im Wesentlichen mit String.fromCharCode und str.charCodeAt identisch, behandeln Ersatzpaare jedoch korrekt.
Den Unterschied sieht man hier:
// charCodeAt ist nicht ersatzpaarfähig und gibt daher Codes für den 1. Teil von ?: alarm( '?'.charCodeAt(0).toString(16) ); // d835 // codePointAt erkennt Ersatzpaare alarm( '?'.codePointAt(0).toString(16) ); // 1d4b3, liest beide Teile des Ersatzpaares
Wenn wir jedoch von Position 1 ausgehen (und das ist hier ziemlich falsch), dann geben beide nur den 2. Teil des Paares zurück:
alarm( '?'.charCodeAt(1).toString(16) ); // dcb3 alarm( '?'.codePointAt(1).toString(16) ); // dcb3 // bedeutungslose 2. Hälfte des Paares
Weitere Möglichkeiten zum Umgang mit Ersatzpaaren finden Sie später im Kapitel Iterables. Dafür gibt es wahrscheinlich auch spezielle Bibliotheken, aber nichts ist berühmt genug, um es hier vorschlagen zu können.
Fazit: Das Teilen von Zeichenfolgen an einer beliebigen Stelle ist gefährlich
Wir können einen String nicht einfach an einer beliebigen Position teilen, z. B. nehmen wir str.slice(0, 4)
und erwarten, dass es sich um einen gültigen String handelt, z. B.:
alarm( 'hi ?'.slice(0, 4) ); // Hi [?]
Hier sehen wir ein Müllzeichen (erste Hälfte des Smile-Ersatzpaars) in der Ausgabe.
Seien Sie sich dessen nur bewusst, wenn Sie zuverlässig mit Ersatzpaaren arbeiten möchten. Das ist vielleicht kein großes Problem, aber Sie sollten zumindest verstehen, was passiert.
In vielen Sprachen gibt es Symbole, die aus dem Grundzeichen und einer Markierung darüber/darunter bestehen.
Beispielsweise kann der Buchstabe a
das Basiszeichen für diese Zeichen sein: àáâäãåā
.
Die meisten gängigen „zusammengesetzten“ Zeichen haben ihren eigenen Code in der Unicode-Tabelle. Aber nicht alle, denn es gibt zu viele Kombinationsmöglichkeiten.
Um beliebige Kompositionen zu unterstützen, erlaubt uns der Unicode-Standard die Verwendung mehrerer Unicode-Zeichen: das Basiszeichen, gefolgt von einem oder mehreren „Markierungs“-Zeichen, die es „verzieren“.
Wenn wir beispielsweise S
gefolgt von dem Sonderzeichen „Punkt darüber“ (Code u0307
) haben, wird es als Ṡ angezeigt.
alarm( 'Su0307' ); // S
Wenn wir über dem Buchstaben (oder darunter) eine zusätzliche Markierung benötigen – kein Problem, fügen Sie einfach das erforderliche Markierungszeichen hinzu.
Wenn wir beispielsweise ein Zeichen „Punkt unten“ anhängen (Code u0323
), erhalten wir „S mit Punkten oben und unten“: Ṩ
.
Zum Beispiel:
alarm( 'Su0307u0323' ); // S
Dies bietet große Flexibilität, stellt aber auch ein interessantes Problem dar: Zwei Zeichen können optisch gleich aussehen, aber mit unterschiedlichen Unicode-Zusammensetzungen dargestellt werden.
Zum Beispiel:
sei s1 = 'Su0307u0323'; // Ṩ, S + Punkt oben + Punkt unten sei s2 = 'Su0323u0307'; // Ṩ, S + Punkt unten + Punkt oben alarm( `s1: ${s1}, s2: ${s2}` ); alarm( s1 == s2 ); // false, obwohl die Zeichen identisch aussehen (?!)
Um dieses Problem zu lösen, gibt es einen „Unicode-Normalisierungsalgorithmus“, der jede Zeichenfolge in die einzelne „normale“ Form bringt.
Es wird durch str.normalize() implementiert.
alarm( "Su0307u0323".normalize() == "Su0323u0307".normalize() ); // WAHR
Es ist lustig, dass normalize()
in unserer Situation tatsächlich eine Folge von 3 Zeichen zu einem zusammenfügt: u1e68
(S mit zwei Punkten).
alarm( "Su0307u0323".normalize().length ); // 1 alarm( "Su0307u0323".normalize() == "u1e68" ); // WAHR
In der Realität ist dies nicht immer der Fall. Der Grund dafür ist, dass das Symbol Ṩ
„häufig genug“ ist, sodass die Unicode-Ersteller es in die Haupttabelle aufgenommen und ihm den Code gegeben haben.
Wenn Sie mehr über Normalisierungsregeln und -varianten erfahren möchten – diese werden im Anhang des Unicode-Standards beschrieben: Unicode-Normalisierungsformen, aber für die meisten praktischen Zwecke reichen die Informationen aus diesem Abschnitt aus.