Używanie natywnych funkcji JSON

Język ActionScript 3.0 oferuje natywny interfejs API służący do kodowania i dekodowania obiektów ActionScript za pomocą formatu JSON (JavaScript Object Notation). Klasa JSON i funkcje pomocnicze elementów składowych są zgodne z piątym wydaniem specyfikacji ECMA-262 (z kilkoma różnicami).

Omówienie interfejsu API mechanizmu JSON

Interfejs API mechanizmu JSON w języku ActionScript obejmuje klasę JSON i funkcje składowe toJSON() w kilku klasach natywnych. Na potrzeby aplikacji wymagających własnego kodowania JSON w dowolnej klasie architektura ActionScript udostępnia sposoby przesłaniania kodowania domyślnego.

Klasa JSON wewnętrznie obsługuje importowanie i eksportowanie dla dowolnej klasy ActionScript, która nie udostępnia elementu toJSON() . W takich przypadkach klasa JSON przegląda właściwości publiczne każdego napotkanego obiektu. Jeśli obiekt zawiera inne obiekty, klasa JSON przechodzi rekurencyjnie do obiektów zagnieżdżonych i wykonuje takie samo przeglądanie. Jeśli dowolny obiekt udostępnia metodę toJSON() , klasa JSON używa tej własnej metody zamiast algorytmu wewnętrznego.

Interfejs JSON składa się z metody kodującej stringify() i metody dekodującej parse() . Każda z tych metod udostępnia parametr umożliwiający wstawienie własnej logiki do przepływu pracy kodowania lub dekodowania danych w formacie JSON. W metodzie stringify() jest to parametr replacer . W metodzie parse() jest to parametr reviver . W tych parametrach jest przekazywana definicja funkcji z dwoma argumentami o następującym zapisie:

function(k, v):*

Metody toJSON()

Zapis dotyczący metod toJSON() :

public function toJSON(k:String):*

Metoda JSON.stringify() wywołuje metodę toJSON() , jeśli ona istnieje, dla każdej właściwości publicznej napotkanej podczas przeglądania obiektu. Właściwość składa się z pary klucz-wartość. Gdy metoda stringify() wywołuje metodę toJSON() , przekazuje klucz k właściwości, którą bada w danej chwili. Typowa implementacja metody toJSON() oblicza każdą nazwę właściwości i zwraca żądane kodowanie wartości.

Metoda toJSON() może zwrócić wartość dowolnego typu (oznaczoną jako *) — nie tylko typu String. Zmienny typ zwracanej wartości pozwala metodzie toJSON() zwrócić obiekt, jeśli jest to wymagane. Jeśli na przykład właściwość własnej klasy użytkownika zawiera obiekt z biblioteki innej firmy, można zwrócić ten obiekt, gdy metoda toJSON() napotka tę właściwość. Obiekt JSON przechodzi wtedy rekurencyjnie do obiektu pochodzącego z biblioteki innej firmy. Przepływ procesu kodowania zachowuje się następująco:

  • Jeśli metoda toJSON() zwraca obiekt, którego wartość nie jest ciągiem, metoda stringify() przechodzi rekurencyjnie do tego obiektu.

  • Jeśli metoda toJSON() zwraca ciąg, metoda stringify() opakowuje tę wartość w inny ciąg, zwraca opakowany ciąg i przechodzi do kolejnej wartości.

Często zwrócenie obiektu jest lepszym rozwiązaniem niż zwrócenie ciągu JSON utworzonego przez aplikację. Zwrócenie obiektu pozwala użyć wbudowanego algorytmu kodowania JSON i pozwala mechanizmowi JSON przejść rekurencyjnie do zagnieżdżonych obiektów.

Metoda toJSON() nie jest zdefiniowana w klasie Object ani w większości innych klas natywnych. W przypadku braku tej metody klasa JSON w standardowy sposób przegląda właściwości publiczne obiektu. Można również użyć metody toJSON() do udostępnienia właściwości prywatnych obiektu.

Niektóre klasy natywne mają wymagania, których biblioteki ActionScript nie mogą efektywnie obsłużyć w każdym przypadku użycia. Dla takich klas język ActionScript udostępnia trywialną implementację, którą klienci mogą zastąpić w celu realizacji określonych potrzeb. Do klas udostępniających trywialne elementy toJSON() należą:

  • ByteArray

  • Date

  • Dictionary

  • XML

Można utworzyć podklasę klasy ByteArray w celu przesłonięcia jej metody toJSON() albo zmienić definicję jej prototypu. W przypadku klas Date i XML, które są zadeklarowane jako ostateczne, zmiana definicji metody toJSON() wymaga użycia prototypu klasy. Klasa Dictionary jest zadeklarowana jako dynamiczna, co zapewnia większą swobodę przesłaniania metody toJSON() .

Definiowanie własnych zachowań JSON

Aby zaimplementować własne metody kodowania i dekodowania JSON dla klas natywnych, można wybrać jedną z kilku opcji:

  • Zdefiniowanie lub przesłonięcie metody toJSON() we własnej podklasie klasy natywnej, która nie jest ostateczna (nie ma modyfikatora final)

  • Zdefiniowanie lub zmiana definicji metody toJSON() w prototypie klasy

  • Zdefiniowanie właściwości toJSON w klasie dynamicznej

  • Użycie parametrów JSON.stringify() replacer i JSON.parser() reviver

Definiowanie metody toJSON() w prototypie klasy wbudowanej

Natywna implementacja mechanizmu JSON w języku ActionScript odpowiada mechanizmowi JSON ECMAScript zdefiniowanemu w wydaniu 5 specyfikacji ECMA-262. Mechanizm ECMAScript nie obsługuje klas, dlatego w języku ActionScript zachowanie mechanizmu JSON jest zdefiniowane w formie wywołań opartych na prototypach. Prototypy są pierwowzorami klas języka ActionScript 3.0. Umożliwiają one symulowanie dziedziczenia oraz dodawanie i zmienianie definicji elementów składowych.

Język ActionScript umożliwia zdefiniowanie lub zmianę definicji metody toJSON() w prototypie dowolnej klasy. Ta możliwość dotyczy nawet klas, które są oznaczone jako ostateczne. Zdefiniowanie metody toJSON() w prototypie klasy powoduje, że definicja staje się obowiązująca dla wszystkich wystąpień tej klasy w zasięgu aplikacji. Poniżej podano przykładowy kod definiujący metodę toJSON() w prototypie klasy MovieClip.

MovieClip.prototype.toJSON = function(k):* { 
    trace("prototype.toJSON() called."); 
    return "toJSON"; 
} 

Gdy następnie aplikacja wywoła metodę stringify() wystąpienia klasy MovieClip, metoda stringify() zwróci dane wyjściowe metody toJSON() użytkownika.

var mc:MovieClip = new MovieClip(); 
var js:String = JSON.stringify(mc); //"prototype toJSON() called." 
trace("js: " + js); //"js: toJSON"

Metodę toJSON() można też przesłonić w klasach natywnych, które ją definiują. Poniższy przykładowy kod przesłania metodę Date.toJSON() .

Date.prototype.toJSON = function (k):* { 
    return "any date format you like via toJSON: "+ 
        "this.time:"+this.time + " this.hours:"+this.hours; 
} 
var dt:Date = new Date(); 
trace(JSON.stringify(dt)); 
// "any date format you like via toJSON: this.time:1317244361947 this.hours:14" 

Definiowanie i przesłanianie metody toJSON() na poziomie klasy

Aplikacje nie muszą zawsze zmieniać definicji metody toJSON() za pomocą prototypów. Można także zdefiniować metodę toJSON() jako element podklasy, jeśli klasa macierzysta nie jest oznaczona jako ostateczna (nie ma modyfikatora final). Można na przykład rozszerzyć klasę ByteArray i zdefiniować publiczną funkcję toJSON() .

package { 
 
    import flash.utils.ByteArray; 
    public class MyByteArray extends ByteArray 
    { 
        public function MyByteArray() { 
        } 
         
        public function toJSON(s:String):* 
        { 
            return "MyByteArray"; 
        } 
 
    } 
} 
 
 
var ba:ByteArray = new ByteArray(); 
trace(JSON.stringify(ba)); //"ByteArray" 
var mba:MyByteArray = new MyByteArray(); //"MyByteArray" 
trace(JSON.stringify(mba)); //"MyByteArray"

Jeśli klasa jest dynamiczna, można dodać właściwość toJSON do obiektu tej klasy i przypisać do niej funkcję w następujący sposób:

var d:Dictionary = new Dictionary(); 
trace(JSON.stringify((d))); // "Dictionary" 
d.toJSON = function(){return {c : "toJSON override."};} // overrides existing function 
trace(JSON.stringify((d))); // {"c":"toJSON override."}

Metodę toJSON() można przesłonić, zdefiniować lub zdefiniować ponownie w dowolnej klasie ActionScript. Metoda toJSON() nie jest zdefiniowana w większości wbudowanych klas ActionScript. Właściwość toJSON nie jest zdefiniowana w prototypie domyślnym klasy Object ani zadeklarowana jako element tej klasy. Omawiana metoda jest zdefiniowana jako funkcja prototypu tylko w kilku klasach natywnych. Z tego powodu najczęściej nie można przesłonić metody toJSON() w normalny sposób.

Klasy natywne, w których nie jest zdefiniowana metoda toJSON() , podlegają serializacji do formatu JSON przy użyciu wewnętrznej implementacji mechanizmu JSON. Jeśli to możliwe, należy unikać zastępowania tych wbudowanych funkcji. Jeśli zostanie zdefiniowany element składowy toJSON() , klasa JSON będzie używać logiki określonej w programie zamiast własnych funkcji.

Używanie parametru replacer metody JSON.stringify()

Przesłanianie metody toJSON() w prototypie jest przydatne w przypadku zmiany sposobu eksportowania klasy do formatu JSON w obrębie całej aplikacji. Czasami jednak logika eksportowania określona przez użytkownika może dotyczyć tylko specjalnych przypadków w zmiennych warunkach. Aby zastosować takie zmiany o małym zasięgu, można skorzystać z parametru replacer metody JSON.stringify() .

Metoda stringify() stosuje funkcję przekazaną w parametrze replacer do kodowanego obiektu. Zapis tej funkcji jest podobny do zapisu metody toJSON() .

function (k,v):* 

W przeciwieństwie do metody toJSON() funkcja replacer wymaga wartości v oraz klucza k . Ta różnica jest konieczna, ponieważ metoda stringify() jest zdefiniowana w statycznym obiekcie JSON, a nie w kodowanym obiekcie. Gdy metoda JSON.stringify() wywołuje funkcję replacer(k,v) , przegląda ona pierwotny obiekt wejściowy. Niejawny parametr this przekazywany do funkcji replacer odnosi się do obiektu zawierającego klucz i wartość. Metoda JSON.stringify() nie modyfikuje pierwotnego obiektu wejściowego — pozostaje on niezmieniony w przeglądanym kontenerze. Można dzięki temu użyć kodu this[k] w celu odczytania klucza z pierwotnego obiektu. Parametr v zawiera wartość konwertowaną przez metodę toJSON() .

Podobnie jak metoda toJSON() , funkcja replacer może zwrócić wartość dowolnego typu. Jeśli funkcja replacer zwróci ciąg znaków, mechanizm JSON oznaczy znaki specjalne zawartości w cudzysłowie, a następnie umieści tę zmodyfikowaną zawartość w cudzysłowie. Pozwala to zagwarantować, że metoda stringify() otrzyma prawidłowy obiekt ciągu znaków JSON, który pozostanie ciągiem znaków w wywoływanej następnie metodzie JSON.parse() .

W poniższym kodzie parametr replacer i niejawny parametr this służą do zwrócenia wartości time (czas) i hours (godziny) obiektu Date.

JSON.stringify(d, function (k,v):* { 
    return "any date format you like via replacer: "+ 
        "holder[k].time:"+this[k].time + " holder[k].hours:"+this[k].hours; 
});

Korzystanie z parametru reviver metody JSON.parse()

Parametr reviver metody JSON.parse() pozwala wykonywać operację odwrotną w stosunku do funkcji replacer — konwertowanie ciągu znaków JSON na użyteczny obiekt ActionScript. Argument reviver jest funkcją otrzymującą dwa parametry i zwracającą dowolny typ.

function (k,v):* 

W tej funkcji wartość k jest kluczem, a wartość v jest wartością klucza k . Podobnie jak metoda stringify() , metoda parse() przegląda pary klucz-wartość JSON i stosuje do każdej z nich funkcję reviver (jeśli taka funkcja istnieje). Potencjalnym problemem jest to, że klasa JSON nie przekazuje nazwy klasy ActionScript dotyczącej obiektu. Może to utrudniać ustalenie typu obiektu do przywrócenia. Ten problem może być szczególnie istotny, gdy obiekty są zagnieżdżone. Projektując funkcje replacer i reviver metody toJSON() , można opracować sposoby identyfikowania eksportowanych obiektów ActionScript przy jednoczesnym pozostawieniu oryginalnych obiektów bez zmian.

Przykład analizy

Poniższy przykład ilustruje strategię przywracania obiektów uzyskanych w wyniku analizy ciągów znaków JSON. W tym przykładzie są zdefiniowane dwie klasy: JSONGenericDictExample i JSONDictionaryExtnExample. Klasa JSONGenericDictExample jest własną klasą słownika. Każdy rekord zawiera imię i datę urodzenia osoby, a także unikatowy identyfikator. Przy każdym wywołaniu konstruktor klasy JSONGenericDictExample dodaje nowo utworzony obiekt do wewnętrznej tablicy statycznej ze statycznie zwiększaną wartością całkowitą będącą identyfikatorem obiektu. Klasa JSONGenericDictExample ma również zdefiniowaną metodę revive() , która wyodrębnia tylko część całkowitą z dłuższego elementu id . Metoda revive() używa tej wartości całkowitej do wyszukania i zwrócenia poprawnego obiektu, który można przywrócić.

Klasa JSONDictionaryExtnExample rozszerza klasę Dictionary języka ActionScript. Jej rekordy nie mają ustawionej struktury i mogą zawierać dowolne dane. Dane są przypisywane po skonstruowaniu obiektu klasy JSONDictionaryExtnExample, a nie jako właściwości zdefiniowane w klasie. Rekordy w klasie JSONDictionaryExtnExample używają obiektów JSONGenericDictExample jako kluczy. Podczas przywracania obiektu klasy JSONDictionaryExtnExample funkcja JSONGenericDictExample.revive() używa identyfikatora skojarzonego z klasą JSONDictionaryExtnExample, aby pobrać poprawny obiekt klucza.

Co najważniejsze, oprócz obiektu klasy JSONDictionaryExtnExample metoda JSONDictionaryExtnExample.toJSON() zwraca ciąg znaków będący znacznikiem. Ten ciąg znaków identyfikuje dane wyjściowe JSON jako należące do klasy JSONDictionaryExtnExample. Ten znacznik jednoznacznie określa, jaki typ obiektu jest przetwarzany w metodzie JSON.parse() .

package { 
    // Generic dictionary example: 
    public class JSONGenericDictExample { 
        static var revivableObjects = []; 
        static var nextId = 10000; 
        public var id; 
        public var dname:String; 
        public var birthday; 
 
        public function JSONGenericDictExample(name, birthday) { 
            revivableObjects[nextId] = this; 
            this.id       = "id_class_JSONGenericDictExample_" + nextId; 
            this.dname     = name; 
            this.birthday = birthday; 
            nextId++; 
        } 
        public function toString():String { return this.dname; } 
        public static function revive(id:String):JSONGenericDictExample { 
            var r:RegExp = /^id_class_JSONGenericDictExample_([0-9]*)$/; 
            var res = r.exec(id); 
            return JSONGenericDictExample.revivableObjects[res[1]]; 
        } 
    } 
} 
 
package { 
    import flash.utils.Dictionary; 
    import flash.utils.ByteArray; 
 
    // For this extension of dictionary, we serialize the contents of the 
    // dictionary by using toJSON 
    public final class JSONDictionaryExtnExample extends Dictionary { 
        public function toJSON(k):* { 
            var contents = {}; 
            for (var a in this) { 
                contents[a.id] = this[a]; 
            } 
     
            // We also wrap the contents in an object so that we can 
            // identify it by looking for the marking property "class E" 
            // while in the midst of JSON.parse. 
            return {"class JSONDictionaryExtnExample": contents}; 
        } 
 
        // This is just here for debugging and for illustration 
        public function toString():String { 
            var retval = "[JSONDictionaryExtnExample <"; 
            var printed_any = false; 
            for (var k in this) { 
                retval += k.toString() + "=" + 
                "[e="+this[k].earnings + 
                ",v="+this[k].violations + "], " 
                printed_any = true; 
            } 
            if (printed_any) 
                retval = retval.substring(0, retval.length-2); 
            retval += ">]" 
            return retval; 
        } 
    } 
} 

Gdy następujący skrypt środowiska wykonawczego wywołuje metodę JSON.parse() dla obiektu JSONDictionaryExtnExample, funkcja reviver wywołuje metodę JSONGenericDictExample.revive() dla każdego obiektu JSONDictionaryExtnExample. To wywołanie pobiera identyfikator reprezentujący klucz obiektu. Funkcja JSONGenericDictExample.revive() za pomocą tego identyfikatora pobiera przechowywany obiekt JSONDictionaryExtnExample z prywatnej statycznej tablicy i zwraca go.

import flash.display.MovieClip; 
import flash.text.TextField; 
 
var a_bob1:JSONGenericDictExample = new JSONGenericDictExample("Bob", new Date(Date.parse("01/02/1934"))); 
var a_bob2:JSONGenericDictExample = new JSONGenericDictExample("Bob", new Date(Date.parse("05/06/1978"))); 
var a_jen:JSONGenericDictExample = new JSONGenericDictExample("Jen", new Date(Date.parse("09/09/1999"))); 
 
var e = new JSONDictionaryExtnExample(); 
e[a_bob1] = {earnings: 40, violations: 2}; 
e[a_bob2] = {earnings: 10, violations: 1}; 
e[a_jen]  = {earnings: 25, violations: 3}; 
 
trace("JSON.stringify(e): " + JSON.stringify(e)); // {"class JSONDictionaryExtnExample": 
                        //{"id_class_JSONGenericDictExample_10001": 
                        //{"earnings":10,"violations":1}, 
                        //"id_class_JSONGenericDictExample_10002": 
                        //{"earnings":25,"violations":3}, 
                        //"id_class_JSONGenericDictExample_10000": 
                        // {"earnings":40,"violations":2}}} 
 
var e_result = JSON.stringify(e); 
 
var e1 = new JSONDictionaryExtnExample(); 
var e2 = new JSONDictionaryExtnExample(); 
 
// It's somewhat easy to convert the string from JSON.stringify(e) back 
// into a dictionary (turn it into an object via JSON.parse, then loop 
// over that object's properties to construct a fresh dictionary). 
// 
// The harder exercise is to handle situations where the dictionaries 
// themselves are nested in the object passed to JSON.stringify and 
// thus does not occur at the topmost level of the resulting string. 
// 
// (For example: consider roundtripping something like 
//   var tricky_array = [e1, [[4, e2, 6]], {table:e3}] 
// where e1, e2, e3 are all dictionaries.  Furthermore, consider 
// dictionaries that contain references to dictionaries.) 
// 
// This parsing (or at least some instances of it) can be done via 
// JSON.parse, but it's not necessarily trivial.  Careful consideration 
// of how toJSON, replacer, and reviver can work together is 
// necessary. 
 
var e_roundtrip = 
    JSON.parse(e_result, 
               // This is a reviver that is focused on rebuilding JSONDictionaryExtnExample objects. 
               function (k, v) { 
                    if ("class JSONDictionaryExtnExample" in v) { // special marker tag; 
                        //see JSONDictionaryExtnExample.toJSON(). 
                       var e = new JSONDictionaryExtnExample(); 
                       var contents = v["class JSONDictionaryExtnExample"]; 
                       for (var i in contents) { 
                          // Reviving JSONGenericDictExample objects from string 
                          // identifiers is also special; 
                          // see JSONGenericDictExample constructor and 
                          // JSONGenericDictExample's revive() method. 
                           e[JSONGenericDictExample.revive(i)] = contents[i]; 
                       } 
                       return e; 
                   } else { 
                       return v; 
                   } 
               }); 
 
trace("// == Here is an extended Dictionary that has been round-tripped  =="); 
trace("// == Note that we have revived Jen/Jan during the roundtrip.    =="); 
trace("e:           " + e); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], Bob=[e=10,v=1], 
                           //Jen=[e=25,v=3]>] 
trace("e_roundtrip: " + e_roundtrip); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], 
                                     //Bob=[e=10,v=1], Jen=[e=25,v=3]>] 
trace("Is e_roundtrip a JSONDictionaryExtnExample? " + (e_roundtrip is JSONDictionaryExtnExample)); //true 
trace("Name change: Jen is now Jan"); 
a_jen.dname = "Jan" 
 
trace("e:           " + e); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], Bob=[e=10,v=1], 
                           //Jan=[e=25,v=3]>] 
trace("e_roundtrip: " + e_roundtrip); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], 
                                     //Bob=[e=10,v=1], Jan=[e=25,v=3]>]