Uso de la funcionalidad JSON nativa

ActionScript 3.0 proporciona una API nativa para codificar y descodificar objetos de ActionScript con formato JavaScript Object Notation (JSON). La clase JSON y las funciones de miembros admitidas siguen la especificación de la quinta edición del ECMA-262 con algunas variaciones.

Descripción de la API JSON

La API JSON de ActionScript consta de la clase JSON y de las funciones de miembros toJSON() en varias clases nativas. En el caso de aplicación que requieren una codificación JSON personalizada para alguna clase, la estructura de ActionScript proporciona alternativas para omitir la codificación predeterminada.

La clase JSON gestiona internamente la importación y la exportación de cualquier clase de ActionScript que no proporcione un miembro de toJSON() . En esos casos, JSON recorre las propiedades públicas de cada objeto que encuentra. Si un objeto contiene otros objetos, JSON lo recorre repetidamente los objetos anidados y hace su trabajo. Si algún objeto proporciona un método toJSON() , JSON utiliza dicho método personalizado en lugar de su algoritmo interno.

La interfaz JSON está formada por un método de codificación, stringify() , y uno de descodificación, parse() . Cada uno de ellos proporciona un parámetro que permite insertar su propia lógica en el flujo de trabajo de codificación y descodificación JSON. Para stringify() , este parámetro se llama replacer ; para parse() , se llama reviver . Estos parámetros toman una definición de función con dos argumentos y para ello utilizan la siguiente firma:

function(k, v):*

Métodos toJSON()

La firma de los métodos toJSON() es

public function toJSON(k:String):*

JSON.stringify() llama a toJSON() , si existe, en cada propiedad pública que encuentra durante su recorrido por el objeto. Una propiedad está formada por una pareja clave-valor. Si stringify( ) llama a toJSON() , transfiere la clave k de la propiedad que se está analizando. Una implementación típica de toJSON() calcula cada nombre y devuelve la codificación deseada de su valor.

El método toJSON() puede devolver un valor de cualquier tipo (indicado con *), no solo de tipo String. Este tipo devuelto variable permite que toJSON() devuelva un objeto, si procede. Por ejemplo, si una propiedad de la clase personalizada contiene un objeto de la biblioteca de un tercero, puede devolver dicho objeto cuando toJSON() encuentre su propiedad. Entonces JSON lo recorrerá repetidas veces el objeto del tercero. El flujo del proceso de codificación se comporta del modo siguiente:

  • Si toJSON() devuelve un objeto que no da como resultado una cadena, stringify() recorre repetidas veces dicho objeto.

  • Si toJSON() devuelve una cadena, stringify() resume dicho valor en otra cadena, devuelve la cadena resumida y, después, pasa al siguiente valor.

En muchos casos, es preferible devolver un objeto a devolver una cadena JSON creada por la aplicación. Devolver un objeto aprovecha el algoritmo de codificación JSON integrado y también permite que JSON pueda ejecutarse repetidas veces en objetos anidados.

El método toJSON() no se define en la clase Object ni en la mayor parte de clases nativas. Su ausencia hace que JSON lleve a cabo su recorrido habitual por las propiedades públicas del objeto. Si lo desea, también puede utilizar toJSON() para mostrar las propiedades privadas del objeto.

Algunas clases representan retos que las bibliotecas de ActionScript no son capaces de resolver de forma eficaz en todos los casos. Para estas clases, ActionScript proporciona una implementación trivial que los clientes pueden volver a implementar para ajustarla a sus necesidades. Las clases que proporcionan miembros de toJSON() triviales son:

  • ByteArray

  • Date

  • Dictionary

  • XML

Puede crear una subclase de la clase ByteArray para anular este método toJSON() , o puede redefinir su prototipo. Las clases Date y XML, que se declaran finales, requieren el uso del prototipo de la clase para poder redefinir toJSON() . La clase Dictionary se declara dinámica, y se obtiene así libertad adicional a la hora de anular toJSON() .

Definición de comportamiento JSON personalizado

Para implementar su propia codificación y descodificación JSON para clases nativas, puede elegir una de las siguientes opciones:

  • Definir o anular toJSON() en la subclase personalizada de una clase nativa no final

  • Definir o redefinir toJSON() en el prototipo de la clase

  • Definir una propiedad toJSON en una clase dinámica

  • Utilizar los parámetros JSON.stringify() replacer y JSON.parser() reviver

Definición de toJSON() en el prototipo de una clase integrada

La implementación nativa de JSON en ActionScript imita el mecanismo JSON de ECMAScript definido en ECMA-262, quinta edición. Dado que ECMAScript no admite clases, ActionScript define el comportamiento de JSON en términos de distribución basada en prototipos. Los prototipos son los precursores de las clases de ActionScript 3.0 y permiten herencia simulada, así como incorporaciones y nuevas definiciones de miembros.

ActionScript permite definir o redefinir toJSON() en el prototipo de cualquier clase. Este privilegio se aplica incluso a clases marcadas como finales. Cuando se define toJSON() en un prototipo de clase, la definición se aplica a todas las instancias de dicha clase dentro del ámbito de la aplicación. Por ejemplo, así puede definir un método toJSON() en el prototipo MovieClip:

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

Cuando la aplicación llame en ese momento a stringify() en cualquier instancia de MovieClip, stringify() devuelve la salida del método toJSON() :

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

También puede anular toJSON() en clases nativas que definan el método. Por ejemplo, el siguiente código anula 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" 

Definición o anulación de toJSON() en el nivel de clase

No siempre se necesitan aplicaciones para usar prototipos para redefinir toJSON() . También se puede definir toJSON() como miembro de una subclase si la clase principal no está marcada como final. Por ejemplo, puede ampliar la clase ByteArray y definir una función toJSON() pública:

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"

Si una clase es dinámica, puede añadir una propiedad toJSON a un objeto de dicha clase y asignarle una función del modo siguiente:

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."}

Puede anular, definir o redefinir toJSON() en cualquier clase de ActionScript. No obstante, la mayoría de las clases de ActionScript integradas no definen toJSON() . Sin embargo, la clase Object no define el método toJSON en su prototipo predeterminado ni lo declara como un miembro de clase. Solo unas pocas clases nativas definen el método como una función prototipo. Así, en la mayoría de los casos no es posible anular toJSON() en el sentido tradicional.

Las clases nativas que no definen toJSON() se serializan en JSON con una implementación interna de JSON. Evite sustituir esta funcionalidad integrada si es posible. Si define un miembro de toJSON() , la clase JSON usará su lógica en vez de su propia funcionalidad.

Uso del parámetro replacer de JSON.stringify()

Anular toJSON() en el prototipo resulta útil para cambiar el comportamiento de exportación JSON de una clase en toda la aplicación. En algunos casos, no obstante, la lógica de exportación es posible que se aplique solamente a casos especiales en condiciones transitorias. Para poder aceptar cambios con alcance tan limitado, puede utilizar el parámetro replacer del método JSON.stringify() .

El método stringify() aplica la función transferida a través del parámetro replacer al objeto que se está codificando. La firma de esta función es similar a la de toJSON() :

function (k,v):* 

Al contrario que toJSON() , la función replacer requiere el valor v así como la clave k . Esta diferencia es necesaria, ya que stringify() está definido en el objeto estático JSON y no en el objeto que se está codificando. Cuando JSON.stringify() llama a replacer(k,v) , su recorre el objeto de entrada original. El parámetro this implícito transferido a la función replacer hace referencia al objeto que contiene la clave y el valor. Como JSON.stringify() no modifica el objeto de entrada original, dicho objeto permanece intacto en el contenedor que se está recorriendo. De este modo, es posible utilizar el código this[k] para consultar la clave en el objeto original. El parámetro v aloja el valor que convierte toJSON() .

Al igual que toJSON() , la función replacer puede devolver cualquier tipo de valor. Si replacer devuelve una cadena, el motor JSON bloquea el contenido entre comillas y envuelve el contenido bloqueado entre comillas también. Esta envoltura garantiza que stringify() recibe un objeto de cadena JSON válido que seguirá siendo una cadena en la siguiente llamada a JSON.parse() .

El siguiente código utiliza el parámetro replacer y el parámetro implícito this para devolver los valores de time y hours de un objeto 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; 
});

Uso del parámetro reviver de JSON.parse()

El parámetro reviver del método JSON.parse() hace lo contrario que la función replacer : convierte a una cadena JSON en un objeto de ActionScript utilizable. El argumento reviver es una función que toma dos parámetros y devuelve cualquier tipo:

function (k,v):* 

En esta función, k es una clave y v es el valor de k . Del mismo modo que stringify() , parse() recorre los pares de clave/valor de JSON y aplica la función reviver a cada par (si existe). Un problema potencial es el hecho de que la clase de JSON no produce un nombre de clase de ActionScript de un objeto como salida. Por lo tanto, puede resultar complicado saber qué tipo de objeto se revive. Este problema es especialmente reseñable en objetos anidados. Al diseñar funciones toJSON() , replacer y reviver puede buscar formas de identificar los objetos de ActionScript exportados sin tocar los objetos originales.

Ejemplos de análisis

El siguiente ejemplo muestra una estrategia para revivir objetos analizados desde cadenas JSON. Este ejemplo define dos clases: JSONGenericDictExample y JSONDictionaryExtnExample. La clase JSONGenericDictExample es una clase personalizada de diccionario. Cada registro contiene el nombre de una persona y su cumpleaños, así como un identificador exclusivo. Cada vez que se llama al constructor JSONGenericDictExample, se añade un objeto recién creado a un conjunto estático interno con un entero que incrementa como su identificador. La clase JSONGenericDictExample también define un método revive() que extrae la porción entera del miembro de id más largo. El método revive() utiliza este entero para buscar y devolver el objeto adecuado.

La clase JSONDictionaryExtnExample amplía la clase Dictionary de ActionScript. Sus registros no tienen ninguna estructura definida y pueden contener cualquier dato. Los datos se asignan después de construir un objeto JSONDictionaryExtnExample, no como propiedades definidas por la clase. Los registros de JSONDictionaryExtnExample utilizan objetos JSONGenericDictExample como claves. Cuando se revive un objeto JSONDictionaryExtnExample, la función JSONGenericDictExample.revive() utiliza el identificador asociado a JSONDictionaryExtnExample para recuperar el objeto de clave correcto.

Lo que es más importante, el método JSONDictionaryExtnExample.toJSON() devuelve una cadena de marcador además del objeto JSONDictionaryExtnExample. Esta cadena identifica la salida JSON como perteneciente a la clase JSONDictionaryExtnExample. Este marcador no deja ninguna duda sobre el tipo de objeto que se está procesando durante 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; 
        } 
    } 
} 

En tiempo de ejecución, cuando el siguiente script llama a JSON.parse() en un objeto JSONDictionaryExtnExample, la función reviver llama a JSONGenericDictExample.revive() en cada objeto de JSONDictionaryExtnExample. Esta llamada extrae el identificador que representa a la clave del objeto. La función JSONGenericDictExample.revive() utiliza este identificador para recuperar y devolver el objeto JSONDictionaryExtnExample almacenado desde un conjunto estático privado.

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]>]