Очередной сериализатор для JavaScript, но есть нюанс…
- четверг, 3 апреля 2025 г. в 00:00:07
Пару лет назад начал разрабатывать редактор текстовых квестов на JavaScript и обратил внимание на то, что неплохо было бы добавить в JSON-сериализатор поддержку ссылок на объекты. Чтобы можно было одним методом сохранить и загрузить состояние объекта, не нарушая его целостность и связь с внешним миром. Что-то подобное есть в PHP при работе метода serialize.
Спустя год начал разрабатывать пошаговую стратегию, в которой такой метод был бы идеальным для реализации сохранений и сетевого режима (пересылка сохранений от игрока к игроку, как это реализовано в Heroes of Might&Magic 3). Имея такой метод, можно было бы не заботиться о сохранении/загрузке объектов игрового мира при их изменениях. Например, добавим лучнику привязку его стрел к конкретному типу дерева. Или в морском пароме создадим массив перевозимых юнитов. При обычной тактике обработки данных это создало бы немало проблем для организации сохранения ссылок.
В итоге, кроме банальной организации внутренних ссылок, идея разрослась амбициозными планами, а именно:
Сохранять цепочку прототипов объекта со всеми их значениями;
Организовать связь с внешними статичными объектами (чтобы не тянуть их в сериализацию);
Сохранять методы объекта;
Сделать так, чтобы один объект, размещённый в нескольких местах, так и оставался одним объектом.
Этими идеями данный сериализатор отличается от имеющихся аналогов.
В итоге была разработана следующая утилита
https://github.com/nerd220/JSONext
Она содержит два метода - toLinkedJSON и fromLinkedJSON для запаковки и распаковки объекта в JSON.
fromLinkedJSON производит распаковку объекта из сериализации.
toLinkedJSON принимает три параметра:
Объект. Если нужно вставить не объект, необходимо обернуть его в объект, например {myData: array1, elseData: text1}.
Массив ссылок на внешние объекты и способ их восстановления, в формате
[ [объект, 'способ восстановления'], [объект2, 'способ 2']... ]
Пример:
[ [document.body, 'document.body'], [canvas1, 'document.getElementById("canvas1")'] ]
Если в исходном объекте будет найдена ссылка на document.body, она будет заменена скриптом, который при распаковке объекта присоединит ему ссылку на document.body.
Массив конструкторов (прототипов), объекты которых нужно пересоздать при распаковке.
Пример: ['employers', 'workers']
Простой пример:
var a={x: 1};
var b={y: 2, z: a};
var c={a: a, b: b};
var json=toLinkedJSON(c);
var x=fromLinkedJSON(json);
console.log(x.a == x.b.z); // true, потому что все ссылки были сохранены
Более сложный пример:
// создаём функции конструкторы
function constructAProto(){
this.i=1;
this.protoMethod=function(){
this.i+=2;
}
}
function constructA(){
// странный, но рабочий метод наследования
this.__proto__=new constructAProto();
this.constructor=constructA; // нужно указать конструктор объекта
// данные
this.body=document.body;
}
function constructB(link){
this.someMethod=function(){
this.l.i++;
}
this.l=link;
}
// создаём объекты и заполняем их данные
var a=new constructA();
var b=new constructB(a);
var c={linkA: a, linkB: b, method: (o)=>console.log(o.l.i)};
var o={a:a, b:b, c:c};
// сериализуем объект
var json=toLinkedJSON(o,[[document.body,'document.body']],['constructA','constructB']);
// десериализуем объект
var x=fromLinkedJSON(json);
// тестируем результат
x.b.someMethod(); // проверяем метод объекта B, увеличиваем i у связанной с ним A
x.a.protoMethod(); // вызываем метод прототипа объекта А, прибавляем к А ещё 2
x.c.method(x.b); // используя сериализованный метод объекта C, выводим A = 4
console.log(x.c.linkA === x.b.l); // true
console.log(x.a.body); // вернёт наш текущий document.body
Этот пример демонстрирует бесшовный перенос сложного связанного объекта в строку и обратно.
При сохранении всё довольно просто - метод рекурсивно обходит объект, собирая информацию об его содержимом (ссылки на внутренние и внешние объекты, принадлежность объектов к тем или иным прототипам), затем записывает в JSON формат с собственными добавлениями.
При этом исходный объект меняется. В связанные с ним объекты добавляются идентификаторы, которые удаляются после завершения работы метода.
При распаковке метод делает три вещи:
Восстанавливает ссылки на внутренние объекты, для этого осуществляется поиск точек монтирования внутреннего объекта;
Восстанавливает ссылки на внешние объекты, используя указанные пользователем директивы;
Пересоздаёт объекты так, чтобы они вновь принадлежали нужному прототипу, при этом вызывается функция-конструктор, в которую подбираются входные параметры исходя из данных объекта. После создания объекта в него загружаются сохранённые данные, а также восстанавливаются ссылки на него во всех точках монтирования внутри основного объекта;
Распаковывает методы объектов, не принадлежащих указанным прототипам.
Метод может нагружать процессор. Это связано с необходимостью переобхода объекта в поиске точек монтирования. Было бы значительно проще, если бы JavaScript позволял осуществлять доступ к внутреннему ID объекта;
Использование eval для пересоднания объектов гипотетически небезопасно (например, если пользователь имеет доступ к сериализации);
Необходимо использовать специфический способ наследования объектов (как в указанном примере);
Если какие-то внешние объекты ссылались на внутренние объекты нашей сериализации, эти связи необходимо обновлять самостоятельно.
Данный метод позволит разработчикам экономить время на разработке алгоритмов сохранения и загрузки данных. Фактически, можно сохранить всё приложение как есть, а затем развернуть его из строки без потери связности и функциональности.
Метод создаёт компактную сериализацию за счёт игнорирования содержимого указанных прототипов, однако восстанавливает значения не только самого объекта, но и всех его прототипов (как в указанном выше примере с значением i).
В данный момент метод успешно работает для сохранений в сетевых играх до 10мб (сектора поля, юниты, события и т.д.)