JSON является одним из очень простых, но в то же время эффективных языков для хранения и передачи данных. Он настолько популярен, что, пожалуй, может считаться самым совместимым форматом представления данных в мире. Вероятно, что все современные языки программирования умеют работать с JSON практически из коробки.

Одновременно с этим, JavaScript является одним из наиболее популярных языков программирования и применяется практически везде, начиная от веб-браузеров, заканчивая серверами и инструментами разработки. Также, нужно понимать, что JSON появился напрямую из JavaScript и эти два языка просто созданы друг для друга.

Но что же может пойти не так, спросите Вы? Просто попробуйте распарсить следующий JSON-документ:

{ "foo": 123456789123456789123 }

Давайте попробуем самый простой способ:

const object = JSON.parse('{ "foo": 123456789123456789123 }');

console.log(object.foo);

// Outputs: 123456789123456800000

Что же здесь произошло? Очевидно результат выполнения: 123456789123456800000 отличается от оригинального значения из JSON-документа: 123456789123456789123

Дело в том, что указанное число слишком велико, чтобы быть представленным стандартными типами данных, которые можно найти в современных языках программирования. Обычно, самым большим типом данных, который часто поддерживается языками программирования, является unsigned int 64, т. е. целое 64-битное число без знака (что соответствует размерам регистра современного 64-битного процессора), тогда как наше число требует 67 битов для корректного представления. Это и приводит к потере точности при попытке записать число в память компьютера. И стоит понимать, что это происходит не только с JavaScript, но и со всеми языками программирования, которые имеют ограничение на размер хранимого числа.

Чтобы избежать этой проблемы, как правило, современные языки программирования имеют специальный тип данных, обычно называемый BigInt или каким-то похожим образом. Наличие такого типа очень важно для научных вычислений.

Приведем пример, как наш JSON-документ можно распарсить, например, на Go с использованием специального типа:

package main

import (
 "encoding/json"
 "fmt"
 "math/big"
)

func main() {

 var object struct {
  Foo big.Int `json:"foo"`
 }
 
 b := []byte(`{ "foo": 123456789123456789123 }`)

 if err := json.Unmarshal(b, &object); err != nil {
  panic(err)
 }
 
 fmt.Println(object.Foo.String())
 
 // Outputs: 123456789123456789123
 
}

Благодаря использованию типа big.Int вышеприведенный код отлично распарсит наше число без потери точности. Как правило, любой другой язык программирования также позволяет добиться аналогичного результата похожим образом.

Попробуем повторить это на JavaScript?

Обратим внимание на параметры JSON.parse. Вторым аргументом метода является специальная callback-функция называемая "reviver" (восстановитель). По-сути это функция десериализации, которая вызывается для каждого значения (value) из распарсиваемого JSON-документа и позволяет менять значение на усмотрение пользователя. Также, в JavaScript есть специальный тип BigInt, позволяющий работать с большими целыми числами.

Супер! Давайте теперь объединим это всё вместе:

const object = JSON.parse(
  '{ "foo": 123456789123456789123 }',
  (key, value) => (key === 'foo') ? BigInt(value) : value
);

console.log(object.foo);

// Outputs: 123456789123456794624n

В реализации выше мы проверяем, что ключ соответствует значению: "foo", и конвертируем значение из JSON-документа в BigInt.

Но подождите! Полученный результат: 123456789123456794624n снова отличается от ожидаемого нами: 123456789123456789123. В чем дело на этот раз?

Немного копнув глубже, оказывается, что в далеком 2009-ом году разработчики стандарта EcmaScript (JavaScript) проделали не самую хорошую работу. Дело в том, что значение (value), которое передается в пользовательскую функцию (reviver) уже является предварительно десериализованным. Фактически, это приводит к выполнению примерно следующего кода:

(key === 'foo') ? BigInt(123456789123456800000) : 123456789123456800000

Другими словами, как бы Вы того не хотели, не существует способа корректно распарсить это число используя нативный JSON-парсер на JavaScript.

Конечно, в защиту создателей EcmaScript можно сказать, что в то время типа BigInt не существовало и в помине, но, тем не менее, сейчас нам от этого не легче.

Hidden text

Извиняюсь, что перебил!

Недавно я запустил свой Telegram-канал по разработке на JavaScript, там я планирую делиться опытом, новостями и наработанными практиками из мира JS. Заходите не стесняйтесь :)

Но есть и хорошие новости!

Комитет TC39 (группа экспертов отвечающих за разработку современного стандарта EcmaScript) рассматривает предложение об улучшении языка (JSON.parse source text access proposal), которое добавляет возможность доступа к исходному тексту JSON-значения в reviver-функцию. Данный стандарт находится уже на третьей стадии (Stage 3) и даже реализован в движке V8 (на котором работает Chromium и Node.js). Достаточно включить флаг: harmony_json_parse_with_source.

Реализация этого функционала в V8 появилась относительно недавно: 10-го Декабря 2022 года, однако само предложение толком не двигается вперед уже более двух лет и, кажется, что мейнтейнеры не особо торопятся его финализировать. Конечно, такая вещь как изменение стандарта требует времени. Думаю пройдет еще не один год, прежде чем данный функционал будет полностью реализован и внедрен во все JS-движки.

Почему это важно?

Нынче я работаю с технологиями Web 3.0 и в этой сфере очень часто можно встретить не просто большие, но огромные числа. Возьмем к примеру Ethereum. Один эфир (Ether) в этой системе равен 1,000,000,000,000,000,000 WEI (1E18), согласитесь квинтиллион — это довольно большое число. В системах, которые работают с финансами принято хранить денежные значения как целое число условных "центов" (WEI в данном примере), а затем просто округлять такие значения при отображении пользователю. Это позволяет добиться максимальной точности при всех вычислениях.

В то же время формат JSON используется, наверное, в 99% различных API и нам нужен способ передавать такие большие числа между системами. Многие API представляют большие числа не как числа, а как строки, содержащие числа. Отчасти это решает проблему на стороне JS, но это достаточно коряво и нарушает семантику стандарта JSON. Должен быть способ лучше!

Способ лучше

Приняв все вышеизложенное, я решил разработать собственный JSON-парсер на JavaScript, который бы учел все имеющиеся проблемы с парсингом JSON на JS в одном готовом решении и мне удалось оформить это как полноценный полифил. Теперь не нужно ждать внедрения стандарта несколько лет перед его использованием. Возможно это даже поможет с внедрением самого стандарта.

Вот пример как можно использовать новый стандарт совместно с полифилом для нашего случая:

import '@ton.js/json-parse-polyfill';

const object = JSON.parse(
  '{ "foo": 123456789123456789123 }',
  (key, value, context) => (
    (key === 'foo' ? BigInt(context.source) : value)
  )
);

console.log(object.foo);

// Outputs: 123456789123456789123n

Как видите достаточно только импортировать полифил и немного поменять reviver-функцию. Наконец-то оно работает!

В завершение

Иногда, чтобы сделать правильную вещь — нужно сделать вещь не очень правильную, в моем случае это было переизобретением колеса в лице собственного JSON-парсера. Но мне хочется верить, что оно того стоит.

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

Ну, а если Вам нужно еще больше гибкости и удобства при парсинге JSON-документов, то обратите внимание на мою дополнительную библиотеку, которая позволяет матчить значения по их путям используя некое подобие глобов.

Ознакомиться с документацией и примерами всех библиотек можно в репозитории на GitHub. Также не стесняйтесь делиться собственным мнением и задавать вопросы в комментариях.

Удачного парсинга! Теперь ни одно подлое число от Вас не сбежит!

Понравилась статья?

Если статья показалась Вам полезной, пожалуйста, поставьте звезду на GitHub.

Также, приглашаю на мой новый канал по JavaScript-разработке в Telegram. Весь новый контент я выкладываю там.

Вопросы

  1. Какие есть недостатки при использовании этого полифила?

Сам полифил не очень большой, всего 8 КБ несжатого кода. Однако, нужно понимать, что он будет работать медленнее, чем нативная реализация. В 25 раз медленнее, если быть точным. Но, данный парсер позволяет пропускать через себя 1 МБ вложенных JSON-данных за ~40 миллисекунд. Этого должно быть вполне достаточно для большинства приложений.

Также, полифил использует кастомный парсер только в том случае, когда Вы применяете функционал из нового стандарта (аргумент context в reviver-функции), во всех остальных случаях используется обычная нативная реализация.

  1. Написать свой парсер не такая простая задача, он точно работает корректно?

Вся прелесть JSON в том, что этот формат имеет очень простую спецификацию. Также, парсер хорошо покрыт unit-тестами и протестирован на большом количестве реальных сложных документов. Код парсера написан на предельно строгом TypeScript, а сгенерированные бандлы проверяются перед каждым релизом как вручную, так и автоматически на предмет безопасности. Также, библиотека имеет ноль зависимостей и включает дополнительные фичи повышающие безопасность. Таким образом, библиотека может быть использована даже в проектах с достаточно высокими требованиями к безопасности.

Подробнее про безопасность в npm и выбор зависимостей Вы можете почитать в моем цикле статей на Хабре.

  1. Как лучше представлять большие числа в JSON-документах?

Стандарт JSON не накладывает каких-либо ограничений на размер чисел, однако, авторы некоторых API часто представляют большие числа как строки в своих JSON-документах:

{ "foo": "123456789123456789123" }

Данный подход позволяет JS парсить большие числа используя нативную реализацию.

Однако, я абсолютно убежден, что правильным решением является использование семантически-корректного типа для представления больших чисел в JSON — number. Это не вина стандарта JSON и других платформ в том, что разработчики EcmaScript оказались столь недальновидными. Ровно по этой причине комитет TC39 работает над внедрением нового стандарта, а я разработал описанные выше библиотеки.

P/S: в комментариях поделились интересным кейсом — если Ваш API имеет прослойки между сервером и клиентом, то представление данных стоит внимательно проверять, бывают случаи, когда какой-нибудь API Gateway может "портить" числа в документах.

Комментарии (59)


  1. funca
    25.01.2023 00:17
    +10

    Внезапно неперевод. Очень крутая работа в плане проекта, документации и продвижения.


  1. incogn1too
    25.01.2023 00:19
    +13

    "современные языки программирования" - улыбнуло. Внезапно в 70-ых lisp был в состоянии решить такие проблемы.


    1. funca
      25.01.2023 01:03
      +3

      Внезапно в 70-ых перед lisp такой проблемы даже не стояло: формат JSON (JavaScript Object Notation) появился только спустя 30 с лишним лет, - когда lisp уже успел обрести просветление и превратиться в миф.


      1. incogn1too
        25.01.2023 01:13
        +8

        Не будьте столь категоричны. Во первых - есть реализации json-a под lisp. Во вторых - проблема не в джейсоне, как в таковом. Просто, в случае lispa - математику не хипстеры писали.


        1. fominslava Автор
          25.01.2023 01:17
          +1

          А как в Lisp представляются большие числа? Python вот не имеет ограничений в размере чисел, но нужно понимать, что это всегда компромисс, ведь такая реализация всегда будет работать существенно медленнее, чем когда число целиком влезает в регистр процессора.


          1. incogn1too
            25.01.2023 01:24

            Мы говорим про 60-ые или про наше время? =)
            https://en.wikipedia.org/wiki/Lisp_machine


        1. klimkinMD
          26.01.2023 09:36

          За хипстеров не скажу, но недавно в обсуждении провели проверку вычисления современными средствами (языками, калькуляторами и т.д.) тангенса гугола (tg(10^100)), порядка десятка инструментов бодро выдали ответ(!), результаты получились от -5 до +6. И расстраивает то, что ни один не намекнул, что с такими числами обращаться не умеет.


          1. DWM
            27.01.2023 19:32

            А тангенсы в градусах или в радианах брали и возвращали значения?

            А что на это говорила документация (языков, калькуляторов и т.д.)?

            В JS говорит что нельзя смешивать BigInt с другими типами. Или говорит что нельзя конвертировать нецелые числа в BigInt.(чтобы конвертировать градусы в радианы).


        1. Dimaasik
          27.01.2023 14:05

          Мне кажется что в мире программирования есть реализация чего угодно под что угодно)


        1. demimurych
          27.01.2023 15:58
          +2

          Не несите чуши. Математика в JS как и в большинстве современных языков программирования реализована согласно стандарту IEEE 754.


    1. PlatinumKiller
      27.01.2023 12:21

      Есть еще Fortran)


  1. funca
    25.01.2023 00:34
    +12

    Я в свое время обжёгся с этим bigint. Клиент и сервер на python где поддержка больших чисел уже много лет из коробки. Сериализация и десериализация JSON на тестах работает четко. Но в продакшн между ними оказался API Gateway от известного вендора, со скриптигном на JS, вытворявший все те непотребства, что описаны в статье. В итоге все переделали на строки.

    В спецификации JSON не уточняются ограничения на размер целых чисел - интерпретация отдана на откуп реализации. Пройдет ещё куча времени, пока все о чем-нибудь договорятся и подстроятся.

    Как PoC библиотека просто класс и думаю ей можно найти массу применений. Но гонять bigint в JSON я бы не рекомендовал.


    1. fominslava Автор
      25.01.2023 00:38
      +2

      Отличный пример, спасибо, что поделились! Еще интересный момент, что Dev Tools в Google Chrome также показывает значения в отформатироанном JSON уже некорректно, что иногда сильно сбивает с толку :)


    1. slonopotamus
      25.01.2023 09:35
      -1

      Но гонять bigint в JSON я бы не рекомендовал.

      Допустим, мы делаем приложение в котором юеры могут лайкать фотки котиков. Какой тип данный вы будете использовать для подсчёта количества лайков? Строку, что ли?


      1. KivApple
        25.01.2023 09:47

        Так там и bigint не пригодится. Всё влезет в обычный number.


        1. Homiakin
          25.01.2023 11:45
          +9

          ок, ОЧЕНЬ милых котиков :)


          1. Nikita22007
            26.01.2023 16:16

            Верхний предел int64 со знаком примерно 10^19. Число людей в обозримом будущем врятли превысит 10^11. условимся, что на каждого человека приходится в среднем не более 100 аккаунтов. Тогда нам потребуется хранить число максимум 10^13, что даёт нам запас ещё на 6 порядков.


      1. fireSparrow
        25.01.2023 16:00
        +2

        Кстати, в некоторых случаях вполне прагматичным костылём будет именно хранение числа в строке, и преобразавоние его в число только там, где с ним непосредственно нужно производить числовые операции. Нужно всегда смотреть по ситуации.


  1. hellamps
    25.01.2023 05:22
    +2

    из рфц: This specification allows implementations to set limits on the range and precision of numbers accepted


    1. fominslava Автор
      25.01.2023 12:14
      +1

      Справедливо, но дело в том, что другие ЯП дают возможность разработчику самому решить как парсить JSON, в этом случае "implementation" (из вашей отсылки) это код разработчика. А авторы EcmaScript решили это за нас и не оставили нам никакого выбора (строго наложив ограничения IEEE 754). Собственно по этому мы и работает над расширением стандарта :)


      1. funca
        25.01.2023 16:15
        +1

        По идее аналогичная проблема со множеством вариантов интерпретации существует и для строк. Но решается она сейчас указанием encoding. Возможно аналогичный механизм для number - с возможностью указать название формата представления чисел - больше бы соответствал общей концепции.

        Да, доступ к результатам работы токенайзера даёт ещё больше свободы. Например, я могу поставить себе парсер для которого правила интерпретации задаются какой-нибудь JSON Schema. Но это довольно специфическая задача, и мне кажется будет лучше иметь это в виде отдельного API, а не костылей к JSON.parse(). За одно там можно было бы предложить решение для парсинга потоковых форматов типа json-lines.


        1. fominslava Автор
          25.01.2023 18:13
          +1

          Ну, костылём я бы это не назвал. Вполне себе логичное (и ожидаемое) расширение нативного API.

          А в чем сложность с json-lines? Сканируешь себе поток и скармливаешь строки парсеру постепенно. Не уверен что эту задачу в библиотеку стоит выделять, хотя, конечно, можно это сделать.


          1. funca
            25.01.2023 19:43
            +1

            Сканируешь себе поток и скармливаешь строки парсеру постепенно.

            А можно было бы проходить json-lines поток в один проход - без загрузки и буферизации каждой отдельной строки в памяти (они могут быть довольно длинными, что сказывается как на потреблении памяти так и на latency). Вот ещё кейс, когда по сути нужен лишь токенайзер, без парсинга: https://habr.com/ru/company/quadcode/blog/660229/ .

            Вариант, предложенный в статье, не выглядит эффектным: он все равно делает парсинг, даже когда это фактически не требуется. К тому же создаёт массу новых временных объектов. Поэтому я за отдельное API.


            1. fominslava Автор
              25.01.2023 21:04
              +1

              В целом внутрянка парсера позволяет реализовать механизм потокового чтения, думаю это не так сложно сделать.

              А что Вы имеете ввиду под "все равно делает парсинг, даже когда это фактически не требуется" и "создаёт массу новых временных объектов"? Не совсем понятно.


              1. funca
                25.01.2023 23:10
                +2

                JSON.parse(

                '{ "foo": 123456789123456789123 }',

                (key, value, context) => (

                (key === 'foo' ? BigInt(context.source) : value)

                )

                );

                Здесь для "foo" value не используется, однако парсер его все равно парсит, создаёт и передаёт в колбек, как велит интерфейс функции JSON.parse(). А если мы пишем, например, санитайзер - как делали ребята по ссылке в комменте выше, когда интересуют только ключи - то парсить value не требуется вообще - нужны лишь данные от токенайзера (в вашем примере key и context.source). Получается парсер делает кучу ненужной работы.

                Чтобы передать в callback значение context, в вашей редакции каждый раз создаётся новый объект. По одному разу на каждую ноду.

                Причина у проблем общая - добавление ортогональной по сути фичи в интерфейс JSON.parse().


                1. fominslava Автор
                  26.01.2023 21:11
                  +1

                  Ну да, накладные расходы, конечно, есть. Хотя мне сложно представить ситуацию и объемы данных когда это было бы критично. Вероятно для таких исключительных ситуаций можно написать/форкнуть собственное супер-оптимизированное решение.


                  1. funca
                    26.01.2023 23:17

                    Интересно, в каких ещё ситуациях может потребоваться доступ к сырым строкам? В JSON разных типов данных не много. Тема с number / bigint, как видно, отпадает - надёжнее передавать сразу строкой. string и так строки. Из примитивных остаются только "true", "false" и null.


  1. AlexXYZ
    25.01.2023 07:39
    -1

    А можно ли передать в JSON зацикленные структуры или когда некоторые структуры являются ссылочными, а JSON стрингует их как отдельные? Есть какие то методы?


    1. aamonster
      25.01.2023 08:53

      Нельзя (по стандарту). Но есть библиотеки для сериализации (только это уже не JSON).


    1. Alexandroppolus
      25.01.2023 08:57

      Из коробки - нет. Придется вручную велосипедить, например, нормализовывать объект, придумывать некую систему идентификаторов (внешних ключей), и т.д.


      1. AlexXYZ
        25.01.2023 10:32
        -5

        Уже пришлось поколхозить. Сколько лет JSON существует, а без колхоза никуда.


        1. dopusteam
          25.01.2023 18:02

          Он просто не для циклических структур, берите что то подходящее и не колхозьте


    1. aamonster
      25.01.2023 09:53

      Пример либы (в описании как раз излагается принцип): https://github.com/WebReflection/flatted


      1. AlexXYZ
        25.01.2023 10:28
        -1

        Спасибо, глянул. Но только потом подумал, что приёмниками такого «нового» формата могут быть только клиенты JS. C# NewtonSoft JSON не поддерживает ссылки внутри Json. Шьорт побьери. Эта их жажда угодить клиенту, сделав ключ .Parent, чего в обычном JS точно нет, сломала возможность делать множественные ссылки.


        1. aamonster
          25.01.2023 11:38

          Ну, портировать либу на другие языки нетрудно.

          Но если у вас проблема только с ключом .Parent – то проще всего сериализовать без него (на js – в JSON.serialize передать скипающую его функцию), а после десериализации обходить дерево, восстанавливая его.

          Но JSON – это всё-таки для "чистых данных", а не формат сериализации общего назначения.


        1. buldo
          25.01.2023 22:46
          +1

          В каком смысле newtonsoft не поддерживает ссылки? Мы гоняем через него объекты с циклическими ссылками внутри


    1. fominslava Автор
      25.01.2023 11:57
      +1

      Наверное это не совсем соотносится с тематикой статьи, но, тем не менее, рекомендую посмотреть как этот вопрос решается в стандарте JSON Schema (https://json-schema.org/understanding-json-schema/structuring.html) возможно будет полезно.


    1. sinkapoy
      25.01.2023 11:58
      +1

      Нельзя передать зацикленную структуру, т.к. сама спецификация не поддерживает ссылки. Также нельзя просто скопировать объект, т.к. это ведет к бесконечному копированию.

      Если бы у меня стояла ОСТРАЯ нужда, то можно сделать проход по дереву объектов в поисках ссылок на элементы структуры, после этого создать отдельное поле в JSON с иерархией и путем до оригиналов. После десериализации просто восстанавливать полное представление. У Articy Draft в экспорте json есть модели объектов и иерархия, можно попробовать такой подход тоже.
      Как вариант, можно добавить префикс для строк, после которого вставлять путь до оригинала. В любом случае так лучше не делать)


    1. mekegi
      25.01.2023 14:34
      +5

      Уже было в симпсонах. Авторы YAML решили предусмотреть и зацикленность ссылки и еще тысячу других няшных фич. В итоге вырастили кактус который теперь приходится всем жрать https://noyaml.com/


  1. PsyHaSTe
    25.01.2023 16:07
    +5

    Для себя решил просто передавать цифры строками и не париться на тему полифиллов и не париться. Вот что на эту тему говорит стандарт:


    This specification allows implementations to set limits on the range
    and precision of numbers accepted. Since software that implements
    IEEE 754-2008 binary64 (double precision) numbers [IEEE754] is
    generally available and widely used, good interoperability can be
    achieved by implementations that expect no more precision or range
    than these provide, in the sense that implementations will
    approximate JSON numbers within the expected precision. A JSON
    number such as 1E400 or 3.141592653589793238462643383279 may indicate
    potential interoperability problems, since it suggests that the
    software that created it expects receiving software to have greater
    capabilities for numeric magnitude and precision than is widely
    available.

    То есть, хотя стандарт прямо не запрещает, он не рекомендуют использовать числа которые не могут быть точно представлены как IEE754 double.




    Хех, funca описывает ровно то что я хотел избежать, но у меня это удалось и я с проблемами не столкнулся.


    1. formerchild
      26.01.2023 15:37
      +1

      Наверное, все так и делают. Мы тоже бигнамбы строками передаем


  1. sshmakov
    25.01.2023 18:08
    +3

    Забыли упомянуть, что в JavaScript под integer не 64 бита, а 53.

    64 - это в Java.


  1. EvgeniiKh
    25.01.2023 18:16

    JSON является одним из очень простых, но в то же время эффективных языков для хранения и передачи данных.

    JSON - это текстовый формат, а не язык.


    1. fominslava Автор
      25.01.2023 18:16

      https://www.json.org/json-en.html

      These properties make JSON an ideal data-interchange language.


    1. Arqwer
      25.01.2023 19:06
      +3

      Формальный язык - множество конечных слов над конечным алфавитом. Это гораздо более широкое понятие, чем вам кажется. Например, множество всех полиндромов это тоже язык. И множество всех простых чисел в двоичном представлении - это язык. Если вы можете к чему-то составить грамматику, то это точно язык. Любой текстовый формат это тоже язык.

      Формальный язык, Википедия


    1. kisskin
      25.01.2023 19:32
      +1

      И в добавок - эффективным будет бинарный формат, а json явно на порядок медленнее и обьемнее


  1. FlashManiac
    26.01.2023 10:52

    Именно по этому я тоже написал свою библиотеку. Но я пошел дальше - JS/JSON объекты кодируются/декодируются в бинарный формат с различными оптимизациями. Есть поддержка всех базовых JS типов включая новые BigInt/Symbol. Все работает как в браузере так и на nodeJS, а так же покрыто 100% тестами. Если нужно добавить уникальные кастомные типы - это тоже есть.
    https://github.com/superman2211/xobj/tree/master/packages/core


    1. Alexandroppolus
      26.01.2023 11:20
      +2

      А во что сериализуется symbol?

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


      1. mayorovp
        26.01.2023 11:51

        Судя по его тестам, символ сериализуется во что-то вроде уникального идентификатора символа в пределах сообщения. После десериализации сохраняется взаимная идентичность символов, но теряется связь символов с другими частями программы. Думаю, такое поведение можно назвать ожидаемым и очевидным, хотя я не представляю зачем вообще оно нужно.


        А вот с функциями вопрос интереснее...


        1. Alexandroppolus
          26.01.2023 12:15

          хотя я не представляю зачем вообще оно нужно

          Оно имело бы некоторый смысл, если бы в сериализации объектов поддерживались ключи-символы. Но автор случайно забыл про Object.getOwnPropertySymbols...

          А то что при десереализации создаются новые значения символов, прямо такое себе. Всё-таки символы - примитивные значения, и по аналогии с другими примитивами должны восстанавливаться. Возможно, стоит добавить некий глобальный словарь символов для подстановки.


          1. mayorovp
            26.01.2023 12:31

            Вот как раз ключи-символы при сериализации должны игнорироваться, на неперечислимость символов полагается слишком много библиотек. Сериализую какую-нибудь вью-модель вы, как правило, не хотите включать в сериализацию всех её наблюдателей включая DOM.


            Всё-таки символы — примитивные значения, и по аналогии с другими примитивами должны восстанавливаться.

            Но невозможность восстановить уникальный символ — это ключевое и общеизвестное свойство символов.


          1. FlashManiac
            26.01.2023 13:59

            Да идея со словарем отличная!


        1. FlashManiac
          26.01.2023 13:54
          +1

          Да вы правы - символ уникален в пределах сообщения. Symbol как значение передать в принципе нельзя.


        1. FlashManiac
          26.01.2023 13:56

          Функции так же добавлены в пределах контекста. И они должны быть строго анонимные ) Но была идея задавать идентификатор функции что бы при декодировании подставить ее аналог на другой стороне.


          1. FlashManiac
            26.01.2023 13:57

            Аналогично можно и символами сделать ) Создавать некую таблицу.


          1. Alexandroppolus
            26.01.2023 16:17
            +1

            И они должны быть строго анонимные

            Причем тут анонимность? Проблема в другом. Допустим, функция создана так:

            const func = ((x) => () => x)(123);

            func() всегда возвращает 123

            func.toString() дает строку "() => x". Очевидно, что десериализованная из такой строки функция совсем необязательно будет возвращать 123.


            1. funca
              26.01.2023 23:37

              Честно говоря удивлен, но оказалось что-то подобное уже есть https://www.npmjs.com/package/serialize-closures. Причем сделано снаружи, а не изнутри движка (так-то DevTools показывают внутренности объектов, поэтому вопрос сериализации замыканий это лишь вопрос доступа к соответствующим API).


              1. mayorovp
                27.01.2023 09:04

                Не, движок и его отладочные интерфейсы тут вообще ни при чём:


                Note: only functions whose code has first been processed by ts-closure-transform are eligible for serialization.


  1. demimurych
    27.01.2023 18:11

    Начать следовало бы с того - как расшифровывается JSON: JavaScript Object Notation

    Уже с єтого момента, должен был бы сработать маячек на тему, а не связаны ли особенности, которые я обсуждаю в рамках своей заметки чем-то фундаментальным, что связано с самим языком?

    Открыв официальную спецификаци, языка JavaScript, а именно главу касающуюся парсинга переданной строки в метод обьекта JSON https://tc39.es/ecma262/multipage/structured-data.html#sec-json.parse, мы должны были бы обратить внимание на строку алгоритма парсинга:

    Let completion be Completion(Evaluation of script).

    которая снимает ровно все вопросы, которые бы мог бы себе задать программист языка JS, а именно что будет если я подсуну некорректные с точки зрения синтаксиса языка данные и получить испчерпывающий ответ на єтот вопрос - произойдет ровно тоже самое, что должно произойти былобы если бы Вы тоже самое сделали используя обычный (не json) код JS.

    Я єто к тому, что зачастую, как в єтой статье, неожиданное ыоведение тех или иных частей языка связано не с "особенностями" языка, а с тем, что мы про него не знаем, так как учим его по mdm, блог постам и прочей "фигне" отличающейся от первоисточника (спецификации) как єскимос от папуаса.

    То есть: JSON.parse работает строго в рамках того как он и доажен работать и решение "заявлрнной" проблемы лежит не в плоскости изменения метода parse, который не является ничем иным как официальным (єталонным) алгоритмом парсинга JS кода), но только в введение отдельного апи в рамках host системы которая сделает работу за Вас.