Использование встроенных функций JSON

ActionScript 3.0 предоставляет встроенный API-интерфейс для кодирования и декодирования объектов ActionScript с помощью формата JSON (JavaScript Object Notation). Класс JSON и входящие в него вспомогательные функции разработаны в соответствии с 5-й редакцией спецификации ECMA-262, за исключением нескольких расхождений.

Обзор API-интерфейса JSON

API-интерфейс ActionScript JSON включает класс JSON и функции toJSON() нескольких встроенных классов. Для приложений, требующих пользовательскую кодировку JSON для какого-либо класса, платформа ActionScript обеспечивает способы переопределения кодировки по умолчанию.

Класс JSON выполняет внутреннюю обработку импорта и экспорта любого класса ActionScript, в котором отсутствует toJSON() . В подобных случаях JSON проверяет общедоступные свойства каждого обнаруживаемого объекта. Если объект содержит другие объекты, JSON переходит к вложенным объектам и выполняет такой же обход. Если объект предоставляет метод toJSON() , JSON использует этот пользовательский метод вместо внутреннего алгоритма.

Интерфейс JSON включает метод кодирования, stringify() , и декодирования, parse() . Каждый из этих методов предоставляет параметр, который позволяет вставлять собственную логику в рабочий процесс кодировки или декодировки JSON. Для метода stringify() это параметр replacer ; для parse() reviver . Эти параметры принимают определение функции с двумя аргументами, используя следующую подпись:

function(k, v):*

Методы toJSON()

Для методов toJSON() используется подпись

public function toJSON(k:String):*

JSON.stringify() вызывает метод toJSON() , если он существует, для каждого общедоступного свойства, обнаруженного при прохождении объекта. Свойство содержит пару «ключ/значение». Когда stringify() вызывает метод toJSON() , он передает ключ, k , рассматриваемого в данный момент свойства. При типичной реализации метод toJSON() анализирует имя каждого свойства и возвращает желаемую кодировку его значения.

Метод toJSON() может возвращать значение любого типа (обозначенное как *), а не только строку. Это позволяет методу toJSON() возвращать объекты, если это допустимо. Например, если свойство пользовательского класса содержит объект из другой библиотеки стороннего поставщика, то когда метод toJSON() обнаруживает такое свойство, может возвращаться объект. В этом случае JSON переходит к объекту стороннего поставщика. Процесс кодировки проходит следующим образом.

  • Если toJSON() возвращает объект, не являющийся строкой, stringify() переходит к этому объекту.

  • Если toJSON() возвращает строку, stringify() упаковывает значение в другую строку, возвращает упакованную строку, а затем переходит к следующему значению.

Во многих случаях возвращение объекта предпочтительнее возвращения строки JSON, созданной приложением. При возвращении объекта используется встроенный алгоритм кодировки JSON. Это также позволяет JSON переходить к вложенных объектам.

Метод toJSON() не определен в классе Object, как и в большинстве других встроенных классов. Если метод отсутствует, JSON выполняет стандартный обход общедоступных свойств объекта. При желании можно также можно использовать метод toJSON() для отображения закрытых свойств объекта.

При работе с несколькими встроенными классами возникают трудности, которые библиотеки ActionScript не могут эффективно решить для всех случаев использования. Для таких классов в ActionScript предусмотрена простая реализация, которую пользователи могут настраивать в соответствии со своими потребностями. Ниже перечислены классы, предоставляющие тривиальные методы toJSON() :

  • ByteArray

  • Date

  • Dictionary

  • XML

Для класса ByteArray можно создать подкласс, чтобы переопределить его метод toJSON() . Также можно переопределить его прототип. Для классов Date и XML, объявленных как окончательные, требуется использовать прототип класса с целью переопределения toJSON() . Класс Dictionary объявляется как динамический, что предоставляет дополнительную свободу при переопределении toJSON() .

Определение пользовательского поведения JSON

Чтобы реализовать собственную кодировку и декодировку JSON для встроенных классов, можно выбрать один из нескольких вариантов:

  • Определение или переопределение toJSON() в пользовательском подклассе неокончательно встроенного класса

  • Определение или переопределение toJSON() в прототипе класса

  • Определение свойства toJSON в динамическом классе

  • Использование параметров JSON.stringify() replacer и JSON.parser() reviver

Определение метода toJSON() для прототипа встроенного класса

Встроенная реализация JSON в ActionScript отражает механизм ECMAScript JSON (см. 5-ю редакцию ECMA-262). Так как ECMAScript не поддерживает классы, ActionScript определяет поведение JSON с точки зрения организации на основе прототипов. Прототипы — это предшественники классов ActionScript 3.0, которые позволяют имитировать наследование, а также осуществлять добавление и переопределение элементов.

ActionScript позволяет определять или переопределять toJSON() для прототипа любого класса. Это преимущество применимо даже к классам, помеченным как окончательные. Когда метод toJSON() определяется для прототипа класса, это определение становится текущим для всех экземпляров этого класса, которые находятся в области действия приложения. Например, вот как можно определить метод toJSON() в прототипе класса MovieClip:

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

Когда после этого приложение вызывает метод stringify() для любого экземпляра MovieClip, stringify() возвращает вывод метода toJSON() :

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

Метод toJSON() также можно переопределить в четырех определяющих его встроенных классах. Например, следующий код переопределяет 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" 

Определение и переопределение toJSON() на уровне класса

Приложениям не всегда требуется использовать прототипы для переопределения toJSON() . Также можно определить toJSON() как элемент подкласса, если родительский класс не помечен как окончательный. Например, можно расширить класс ByteArray и определить общедоступную функцию 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"

Если класс динамический, можно добавить свойство toJSON в объект этого класса и назначить ему следующую функцию:

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

Метод toJSON() можно переопределить или определить для любого класса ActionScript. Однако большинство встроенных классов ActionScript не определяют метод toJSON() . Класс Object не определяет toJSON в своем прототипе по умолчанию и не объявляет его в качестве элемента класса. Только небольшое число встроенных классов определяют этот метод как функцию прототипа. Таким образом, в большинстве случаев toJSON() нельзя переопределить традиционными способами.

Встроенные классы, которые не определяют toJSON() , подвергаются сериализации в JSON посредством внутренней реализации JSON. По возможности не заменяйте эту встроенную функцию. Если определить элемент toJSON() , класс JSON использует пользовательскую логику вместо собственной.

Использование параметра replacer метода JSON.stringify()

Рекомендуется переопределять toJSON() в прототипе и для изменения поведения класса JSON при экспорте в рамках всего приложения. Однако в некоторых случаях логика экспорта может применяться только к особым случаям в переходных условиях. Чтобы применить изменения узкого плана, можно использовать параметр replacer метода JSON.stringify() .

Метод stringify() применяет функцию, переданную через параметр replacer в кодируемый объект. Подпись для данной функции напоминает подпись toJSON() :

function (k,v):* 

В отличие от toJSON() , функция replacer требует значение, v , а также ключ, k . Это отличие необходимо, так как метод stringify() определяется для статического объекта JSON, а не для кодируемого объекта. Когда метод JSON.stringify() вызывает replacer(k,v) , он проходит исходный введенный объект. Неявный параметр this , передаваемый функции replacer , указывает на объект, который содержит ключ и значение. Так как метод JSON.stringify() не затрагивает исходный введенный объект, он остается без изменений в контейнере, который проходится в данный момент. Таким образом можно использовать код this[k] с целью запроса ключа для исходного объекта. Параметр v содержит значение, которое преобразуется методом toJSON() .

Как toJSON() , функция replacer может возвращать значение любого типа. Если replacer возвращает строку, модуль JSON маскирует его содержимое с помощью кавычек, а затем заключает замаскированное содержимое также в кавычки. Такое заключение в кавычки гарантирует, что метод stringify() получит действительный объект строки JSON, который останется строкой при последующем вызове JSON.parse() .

В следующем коде используется параметр replacer и неявный параметр this для возврата значений time и hours объекта 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; 
});

Использование параметра reviver метода JSON.parse()

Параметр reviver метода JSON.parse() выполняет функцию, противоположную функции replacer : он преобразует строку JSON в пригодный для использования объект ActionScript. Аргумент reviver представляет собой функцию, которая принимает два параметра и возвращает значение любого типа:

function (k,v):* 

В этой функции k является ключом, а v reviver значением k . Как и stringify() , parse() обходит пары «ключ-значение» JSON и применяет к каждой паре функцию reviver , если она существует. Потенциальная проблема заключается в том, что класс JSON не выводит имя класса ActionScript объекта. Это создает трудности при определении типа объекта, который требуется регенерировать. Эта проблема встает особенно остро для вложенных объектов. При разработке функций toJSON() , replacer и reviver можно найти способы идентификации экспортируемых объектов ActionScript, при использовании которых не затрагиваются исходные объекты.

Пример разбора

Следующий пример демонстрирует стратегию регенерации объектов, полученных в результате разбора строк JSON. В этом примере определяется два класса: JSONGenericDictExample и JSONDictionaryExtnExample. Класс JSONGenericDictExample — это пользовательский класс словаря. Каждая запись содержит имя и день рождения человека, а также уникальный идентификатор. При каждом вызове конструктора JSONGenericDictExample вновь созданный объект добавляется во внутренний статический массив с уникальным идентификатором в виде статически увеличиваемого целого числа. Класс JSONGenericDictExample также определяет метод revive() , который извлекает только целую часть из длинного элемента id . Метод revive() использует это целое число для поиска и возврата правильного регенерируемого объекта.

Класс JSONDictionaryExtnExample расширяет класс ActionScript Dictionary. Его записи не имеют заданной структуры и могут содержать любые данные. Вместо использования свойств, определенных для класса, данные назначаются после создания объекта JSONDictionaryExtnExample. В записях JSONDictionaryExtnExample объекты JSONGenericDictExample используются как ключи. Когда объект JSONDictionaryExtnExample регенерируется, функция JSONGenericDictExample.revive() использует идентификатор, связанный с JSONDictionaryExtnExample для получения правильного объекта ключа.

Самое важное то, что вместе с объектом JSONDictionaryExtnExample метод JSONDictionaryExtnExample.toJSON() возвращает строку маркера. Эта строка идентифицирует вывод JSON как экземпляр класса JSONDictionaryExtnExample. Этот маркер позволяет точно определить тип объекта, обрабатываемого при вызове метода 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; 
        } 
    } 
} 

Следующий сценарий среды выполнения вызывает JSON.parse() для объекта JSONDictionaryExtnExample, функция reviver вызывает JSONGenericDictExample.revive() для каждого объекта в JSONDictionaryExtnExample. Этот вызов извлекает идентификатор, представляющий ключ объекта. Функция JSONGenericDictExample.revive() использует этот идентификатор для извлечения и возврата сохраненного объекта JSONDictionaryExtnExample из статического массива с ограниченным доступом.

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