Рецепт пуле-непробиваемого toJSON
Многие разработчики не раз за свою практику сталкивались с тем, что стандартная возможность упаковать объект в JSON падает с ошибкой, или, что хуже, уходит в вечный цикл. Причина тому цикличные ссылки. Пример:
const a = {};
const b = { a };
a.b = b;
JSON.stringify(a);
Получаем закономерный Uncaught TypeError: Converting circular structure to JSON. Но, что если вам, к примеру, для нужд debug-а, потребовался полунепробиваемый метод toJSON?
Хорошая новость! Мы можем его написать самостоятельно. Причём, благодаря наличию таких классов, как Map или Set, без особых ухищрений и извращений. Принцип действия алгоритма достаточно прост: всякий раз, когда мы собираемся упаковать очередной объект (а только они передаются по ссылке, и приводят к цикличности), мы должны проверить, а не упаковывали ли мы его ранее. А т.к. Set и Map в качестве ключа в Map и значения в Set понимают всё, что угодно, включая объекты, не приводя их к строковому виду, то и переложить эту работёнку можно и нужно именно на них.
Circular
Т.е. где-то внутри нашего toJSON будет:
if(map.has(obj))
return `$circular:${map.get(obj)}`;
map.set(obj, path);
Где path это путь к текущему итерируемому элементу. Таким образом, столкнувшись с объектом повторно, мы, вместо этого объекта, упакуем строку вида $circular:a.b.c. Что будет очень удобным подспорьем при разборе сложных багов.
Element-ы
Не будет лишним проверять — является ли объект DOMElement-ом, Document-ом или чем-нибудь подобным, т.к. эти сущности не просты внутри, но при этом, едва ли представляют для вас интерес:
if(
obj instanceof RegExp ||
obj instanceof Date ||
obj instanceof Element ||
obj instanceof Document
)
return obj.toString();
Получим строку вида [object HTMLDivElement].
jQuery
В случае, если вы используете jQuery, то стоит все порождённые ею jQuery-массивы превратить в обычные:
if(obj instanceof $) // jQuery-nodes
return _.map(obj, (v, idx) => { /* toJS */ });
В моём случае типовым результатом будет — ["[object HTMLDivElement]", "[object HTMLDivElement]"]. Но можно и усложнить схему, конвертируя DOM-ноды в их CSS-path (например: #section.cls[attr="value"]).
NaN & Null
Скорее всего, внутри своего toJSON вы будете использовать typeof obj === 'object'. Напоминаю о засаде — typeof null === 'object'; Не забудьте это учесть.
Про NaN — JSON.stringify(NaN) === "null". Не самый лучший вариант, не правда ли?
if(_.isNaN(obj))
return '$NaN$';
Дело в том, что JSON не умеет никаких NaN. Но для нужд дебага такие моменты могут быть критичными. Тоже самое и для undefined.