JavaScript, encode_utf8, decode_utf8, Bytesequenzen und Zeichenketten

Äquivalente JS-Klasse zur Perl-Klasse Encode welche den UTF-8-Algorithmus implementiert und der Spread-Operator

Um die Zeichenkodierung in JavaScript müssen wir uns i.d.R. nicht weiter kümmern. Die *.js-Datei wird mit der Kodierung UTF-8 gespeichert und mit derselben Kodierung wird das HTML ausgegeben. Wenn jedoch mit Codepoints, ArrayBuffer und Binaries gearbeitet wird, ja dann gehts in Fragen Zeichenkodierung tiefer rein. Um diese Dinge geht es hier.

Betrachte den Code untenstehend:

// ArrayBuffer zu UTF-8-kodierten String
ClassEncode.prototype.buffer2ustr = function(buffer){
    var uha = new Uint8Array(buffer);
    return this.decode_utf8(String.fromCharCode(...uha));
};

Erläuterungen

Der spread-Operator (spread-Syntax) erlaubt es, der String-Funktion String.fromCharCode() ein ganzes Array mit einem Rutsch zu übergeben. Das Array mit den übergebenen Bytewertigkeiten wird expandiert und im Ergebnis dessen werden die einzelnen Bytes wiederhergestellt. Über ClassEncode.decode_utf8() wird der UTF-8-Algorithmus angewandt, d.h. aus den Bytewertigkeiten werden Codepoints und aus den Codepoints schließlich werden UTF-8-kodierte Zeichen, die sodann im DOM Verwendung finden können. Zum besseren Verständnis lassen wir das Dekodieren einmal weg und erzeugen die Bytesequenz für das EURO-Zeichen:

var binary = String.fromCharCode(0xE2, 0x82, 0xAC);
console.log( btoa(binary) ); // 4oKs

Wie wir sehen, funktioniert eine Base64-Kodierung einfandfrei weil eine Binary übergeben wird. Andernfalls, also bei Übergabe des Eurozeichen btoa("€")erhalten wir eine Fehlermeldung: InvalidCharacterError: String contains an invalid character weil JavaScript das "€" eben als Zeichen betrachtet und nicht als eine Binary bzw. Bytesequenz. Wenn wir jedoch eine Zeichenkette benötigen, beispielsweise zum Befüllen eines Eingebefeldes, muss das DOM eine Solche als UTF-8-kodierte Zeichenkette auffassen. Um den Unterschied zu verdeutlichen:

var binary = String.fromCharCode(0xE2, 0x82, 0xAC);
document.getElementById('binary').value = binary;
Binary:  
Decodet: 
var en = new ClassEncode();
document.getElementById('decodet').value = en.decode_utf8(binary);

Und damit sehen wir auch das Eurozeichen so wie es auszusehen hat.

Bytesequenzen und String.fromCharCode()

Wie eine Bytesequenz erzeugt werden kann, zeigt der Code obenstehend, d.h., unter Verwendung der Funktion String.fromCharCode(). Hierbei ist sicherzustellen, daß die übergebenen Bytewertigkeiten stets kleiner oder gleich 256 sind (Maximalwert einer Bytewertigkeit). Sobald wir jedoch beispielsweise notieren:

var binary = String.fromCharCode(0x20AC);

Wird JavaScript den übergebenen Wert > 127 als Codepoint auffassen und das erzeugte Zeichen intern als UTF-16-kodiert ablegen. Ansonsten wird das Zeichen im DOM als ein UTF-8-kodiertes Zeichen aufgefasst und auch so dargestellt. Dieser Sachverhalt führt oft zu Missverständnissen:

var binary = String.fromCharCode(0xE2, 0x82, 0xAC);
var b = new Blob([binary]);
document.getElementById('in').value = binary.length+", "+b.size; // 3, 6

Während also die Länge der Binary mit 3 exakt ausgegeben wird, hat der mit dieser Binary erzeugte Blob eine Größe von 6 Bytes! Wird der Blob jedoch mit new Blob(["€"]) erstellt, ergibt sich dieser mit der exakten Größe von 3 Bytes.

Kodierte Zeichenketten und String.fromCodePoint()

Eine einfache Regel vermeidet diese Missverständnisse: Wenn utf-8-kodierte Zeichen erzeugt werden sollen, benutze die Funktion String.fromCodePoint(). Es versteht sich, dass hierzu die Codepoints zu übergeben sind und nicht etwa die Oktettenwertigkeiten.

ClassEncode vermittelt zwischen Bytesequenzen, Zeichenketten und ArrayBuffer

Ganz im Sinne des Perl-Moduls Encode.pm stellt meine JavaScript-Klasse ClassEncode die Methoden encode_utf8() und decode_utf8() bereit sowie weitere Methoden:

en = new ClassEncode();
// UTF8-Algorithmus anwenden
bytesequenz = en.encode_utf8(utf8string);
utf8string  = en.decode_utf8(bytesequenz);

// Arraybuffer und Strings
bytesequenz = en.buffer2binary(arraybuffer);
utf8string  = en.buffer2ustr(arrybuffer);
arraybuffer = en.binary2buffer(bytesequenz);

// Aliasdefinitionen
ClassEncode.prototype.encode = ClassEncode.prototype.encode_utf8;
ClassEncode.prototype.decode = ClassEncode.prototype.decode_utf8;

Die Aliasdefinitionen deswegen, weil in JavaScript meist ohnehin nur mit der Kodierung UTF-8 gearbeitet wird.

Binaries und AJAX Responses

Mit einem XMLHttpRequest-Objekt (XHR) ist es möglich, die Response als ArrayBuffer aufzufassen, dafür wird xhr.responseType = "arraybuffer" gesetzt. Andernseits wird mit dem Default (xhr.responseType = "text") in xhr.response stets eine UTF-8-kodierte Zeichenkette erwartet, auch dann wenn im Responseheader ein anderer Charset, bspw. als Charset = "ISO-8859-1" angegeben ist. Mit den Methoden von ClassEncode ist es möglich, eine ArrayBuffer-Response in lesbaren Text umzuwandeln, Beispiel siehe untenstehend.

Die Response wird als ArrayBuffer angefordert. Im Fehlerfall, also wenn das serverseitige Erstellen der Binary fehlschlägt, ist davon auszugehen, daß die Response den Fehlertext enthält und der Server einen Status != 200 sendet. Von daher muss aus der Response als ArrayBuffer ein lesbarer Text hergestellt werden, hierzu wird der HTTP-Status herangezogen:

var xhr = new XMLHttpRequest();
xhr.open('GET','/jsencode.html?u=1');
xhr.responseType = "arraybuffer";
xhr.onload = function(){
    var e = new ClassEncode();
    if(this.status != 200){
        pretext( e.buffer2ustr(this.response) );
        return;
    }
};

Die Methode buffer2ustr() ist ganz oben beschrieben siehe ebenda.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de