20 лютого 2024 р.

Методи JSON, toJSON

Скажімо, у нас є складний об’єкт, і ми хотіли б перетворити його в рядок, щоб відправити його через мережу або просто вивести його для логування.

Безумовно, такий рядок повинен включати всі важливі властивості.

Ми могли б реалізувати перетворення наступним чином:

let user = {
  name: "Іван",
  age: 30,

  toString() {
    return `{name: "${this.name}", age: ${this.age}}`;
  }
};

alert(user); // {name: "Іван", age: 30}

… Але в процесі розробки додаються нові властивості, старі властивості перейменовуються та видаляються. Оновлення такого toString кожен раз може стати проблемою. Ми могли б спробувати перебрати властивості в ньому, але що, якщо об’єкт є складним і в його властивостях є вкладені об’єкти? Ми також повинні реалізувати їх перетворення.

На щастя, нема потреби писати код для обробки всього цього. У цьго завдання є просте рішення.

JSON.stringify

JSON (JavaScript Object Notation) – це загальний формат, який представляє значення та об’єкти. Він описується у стандарті RFC 4627. Спочатку він був розроблений для JavaScript, але багато інших мов мають бібліотеки, для його обробки. Тому легко використовувати JSON для обміну даними, коли клієнт використовує JavaScript, а сервер написаний на Ruby/PHP/Java тощо.

JavaScript надає методи:

  • JSON.stringify для перетворення об’єктів в JSON (він повертає JSON-рядок).
  • JSON.parse для перетворення JSON-рядка назад в об’єкт.

Наприклад, тут ми трансформуємо дані студента за домогою JSON.stringify:

let student = {
  name: 'Іван',
  age: 30,
  isAdmin: false,
  courses: ['html', 'css', 'js'],
  spouse: null
};

let json = JSON.stringify(student);

alert(typeof json); // ми отримали рядок!

alert(json);
/* JSON-кодований об’єкт:
{
  "name": "Іван",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "spouse": null
}
*/

Метод JSON.stringify(student) бере об’єкт і перетворює його в рядок.

Отриманий json рядок називається JSON-кодованим або серіалізованим об’єктом. Ми готові відправити його по мережі або покласти в звичайне сховище даних.

Будь ласка, зверніть увагу, що JSON-кодований об’єкт має кілька важливих відмінностей від літерального об’єкта:

  • Рядки використовують подвійні лапки. Ніяких одинарних або зворотніх лапок у JSON. Тобто 'Іван' стає "Іван".
  • Назви властивостей об’єкта також обертаються в подвійні лапки. Це обов’язково. Тобто age:30 стає "age":30.

JSON.stringify можна застосувати до примітивів.

JSON підтримує наступні типи даних:

  • Об’єкти { ... }
  • Масиви [ ... ]
  • Примітиви:
    • рядки,
    • числа,
    • логічні значення true/false,
    • null.

Наприклад:

// число JSON це просто число
alert( JSON.stringify(1) ) // 1

// рядок в JSON -- це ще рядок, але обернутий в подвійні лапки
alert( JSON.stringify('test') ) // "test"

alert( JSON.stringify(true) ); // true

alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSON – незалежна від мови специфікація даних, тому JSON.stringify пропускає деякі специфічні для JavaScript властивості об’єктів.

А саме:

  • Функціональні властивості (методи).
  • Символьні ключі та значення.
  • Властивості, що мають undefined.
let user = {
  sayHi() { // ігнорується
    alert("Привіт");
  },
  [Symbol("id")]: 123, // ігнорується
  something: undefined // ігнорується
};

alert( JSON.stringify(user) ); // {} (порожній об’єкт)

Зазвичай це добре. Якщо це не те, чого ми хочемо, то скоро ми побачимо, як налаштувати процес.

Чудово, що вкладені об’єкти підтримуються та перетворюються автоматично.

Наприклад:

let meetup = {
  title: "Конференція",
  room: {
    number: 23,
    participants: ["Іван", "Анна"]
  }
};

alert( JSON.stringify(meetup) );
/* Вся структура перетворена на рядок:
{
  "title":"Конференція",
  "room":{"number":23,"participants":["Іван","Анна"]},
}
*/

Важливі обмеження: не повинно бути жодних циклічних посилань.

Наприклад:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  participants: ["Іван", "Анна"]
};

meetup.place = room;      // meetup посилається на room
room.occupiedBy = meetup; // room посилається на meetup

JSON.stringify(meetup); // Помилка: Конвертування циклічних структур в JSON

Тут перетворення не вдається через циклічні посилання: room.occupiedBy, яке посилається на meetup, і metup.place, яке посилається на room:

Виключення та перетворення: replacer

Повний синтаксис JSON.stringify:

let json = JSON.stringify(value[, replacer, space])
value
Значення для кодування.
replacer
Масив властивостей для кодування або функція відображення function(key, value).
space
Кількість пробілів для форматування

Зазвичай, JSON.stringify використовується лише з першим аргументом. Але якщо нам потрібно налаштувати процес заміни, наприклад, відфільтрувати циклічні посилання, то ми можемо використовувати другий аргумент JSON.stringify.

Якщо ми передаємо йому масив властивостей, то будуть закодовані лише ці властивості.

Наприклад:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  participants: [{name: "Іван"}, {name: "Аліна"}],
  place: room // meetup посилається на room
};

room.occupiedBy = meetup; // room посилається на meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Конференція","participants":[{},{}]}

Тут ми, мабуть, занадто суворі. Список властивостей застосовується до всієї структури об’єкта. Тому об’єкти в participants будуть порожніми, бо name немає в списку.

Включімо в список всі властивості, окрім room.occupiedBy, яка призводить до циклічного посилання:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  participants: [{name: "Іван"}, {name: "Аліна"}],
  place: room // meetup посилається на room
};

room.occupiedBy = meetup; // room посилається на meetup

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Конференція",
  "participants":[{"name":"Іван"},{"name":"Аліна"}],
  "place":{"number":23}
}
*/

Тепер все, крім occupiedBy, серіалізується. Але список властивостей досить довгий.

На щастя, в якості replacer ми можемо використовувати функцію замість масиву.

Функція буде викликана для кожного (key, value), і повинна повернути замінене значення, яке буде використовуватися замість оригінального. Або undefined, якщо значення буде пропущено.

У нашому випадку ми можемо повернути value “як є” для всього, крім occupiedBy. Щоб ігнорувати occupiedBy, код нижче повертає undefined:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  participants: [{name: "Іван"}, {name: "Аліна"}],
  place: room // meetup посилається на room
};

room.occupiedBy = meetup; // room посилається на meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Конференція
participants: [object Object],[object Object]
0:            [object Object]
name:         Іван
1:            [object Object]
name:         Аліна
place:        [object Object]
number:       23
occupiedBy:   [object Object]
*/

Будь ласка, зверніть увагу, що функція replacer отримує кожну пару ключ/значення, включаючи вкладені об’єкти та елементи масиву. І вона застосовується рекурсивно. Значення this всередині replacer – це об’єкт, який містить поточну властивість.

Перший виклик особливий. Він зроблений з використанням спеціального “об’єкта обгортки”: {"": meetup}. Іншими словами, перша пара (key, value) має порожній ключ, а значення є цільовим об’єктом загалом. Ось чому перший рядок в прикладі вище буде ":[object Object]" .

Ідея полягає в тому, щоб забезпечити якомога більше можливостей для функції replacer – вона має можливість аналізувати та замінити/пропустити навіть весь об’єкт, якщо це необхідно.

Форматування: space

Третій аргумент JSON.stringify(value, replacer, space) – це кількість пробілів, що використовуються для гарного форматування.

Раніше всі розтягнуті об’єкти не мали відступу та додаткових пробілів. Це добре, якщо ми хочемо надіслати об’єкт через мережу. Аргумент space використовується виключно для виводу в зручному для читання вигляді.

Тут space = 2 указує JavaScript показати вкладені об’єкти на декількох рядках, з відступом у 2 пробіли всередині об’єкта:

let user = {
  name: "Іван",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

alert(JSON.stringify(user, null, 2));
/* відступ в 2 пробіли:
{
  "name": "Іван",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* для JSON.stringify(user, null, 4) результат містить більше пробілів:
{
    "name": "Іван",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

Третій аргумент також може бути рядок. У цьому випадку рядок використовується для відступу замість ряда пробілів.

Параметр space використовується виключно для логування та гарного виводу.

Спеціальний “toJSON”

Подібно до методу toString для перетворення в рядок, об’єкт також може мати метод toJSON для перетворення в JSON. Функція JSON.stringify автоматично викликає цей метод, якщо він є.

Наприклад:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Конференція",
    "date":"2017-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

Тут ми бачимо, що date (1) став рядком. Це тому, що всі об’єкти типу Date мають вбудований метод toJSON, який повертає такий рядок.

Тепер додамо власну реалізацію методу toJSON в наш об’єкт room (2):

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Конференція",
  room
};

alert( JSON.stringify(room) ); // 23

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Конференція",
    "room": 23
  }
*/

Як ми можемо побачити, toJSON використовується як при прямому виклику JSON.stringify(room), так і коли room вкладений в іншому серіалізованому об’єкті.

JSON.parse

Щоб декодувати JSON-рядок, нам потрібен інший метод, що називається JSON.parse.

Синтаксис:

let value = JSON.parse(str[, reviver]);
str
JSON-рядок для перетворення в об’єкт.
reviver
Необов’язкова функція, яка буде викликана для кожного (key, value) та може перетворювати значення.

Наприклад:

// масив у вигляді рядка
let numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

Або для вкладених об’єктів:

let userData = '{ "name": "Іван", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

let user = JSON.parse(userData);

alert( user.friends[1] ); // 1

JSON може бути настільки складним, наскільки це необхідно, об’єкти та масиви можуть включати інші об’єкти та масиви. Але вони повинні дотримуватися того ж формату JSON.

Ось типові помилки в рукописному JSON (іноді ми повинні писати його для знаходження помилок):

let json = `{
  name: "Іван",                     // помилка: ім'я властивості без лапок
  "surname": 'Smith',               // помилка: одинарні лапки для значень (повинні бути подвійними)
  'isAdmin': false                  // помилка: одинарні лапки для ключів (повинні бути подвійними)
  "birthday": new Date(2000, 2, 3), // помилка: не дозволяється конструктор "new", тільки значення
  "friends": [0,1,2,3]              // тут все добре
}`;

Крім того, JSON не підтримує коментарі. Додавання коментаря до JSON робить його недійсним.

Існує інший формат, який називається JSON5, що підтримує ключі не обернені в лапки, коментарі тощо. Але це окрема бібліотека, а не частина специфікації мови.

Звичайний JSON є настільки строгим не тому, що його розробники ледачі, а тому, що дозволяє легко, надійно та дуже швидко реалізувати алгоритм кодування та читання.

Використання reviver

Уявіть, що ми отримали серіалізований об’єкт meetup з сервера.

Ось такий:

// title: (meetup title), date: (meetup date)
let str = '{"title":"Конференція","date":"2017-11-30T12:00:00.000Z"}';

…І тепер нам потрібно десеріалізувати цей об’єкт, тобто знову перетворити його в об’єкт JavaScript.

Зробімо це, викликавши JSON.parse:

let str = '{"title":"Конференція","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str);

alert( meetup.date.getDate() ); // Помилка!

Ой! Помилка!

Значення meetup.date – це рядок, а не об’єкт Date. Як JSON.parse міг знати, що він повинен перетворити цей рядок на об’єкт Date?

Передаймо до JSON.parse функції відновлення як другий аргумент, який повертає всі значення “як є”, але date стане об’єктом Date:

let str = '{"title":"Конференція","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() ); // зараз працює!

До речі, це також працює для вкладених об’єктів:

let schedule = `{
  "meetups": [
    {"title":"Конференція","date":"2017-11-30T12:00:00.000Z"},
    {"title":"День народження","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.meetups[1].date.getDate() ); // працює!

Підсумки

  • JSON – це формат даних, який має власний незалежний стандарт та бібліотеки для більшості мов програмування.
  • JSON підтримує прості об’єкти, масиви, рядки, цифри, булеві значення та null.
  • JavaScript надає методи JSON.stringify для серіалізування в JSON і JSON.parse, щоб зчитати данні з JSON.
  • Обидва методи підтримують функції трансформації для інтелектуального читання/запису.
  • Якщо об’єкт має метод toJSON, то він викликається при виконанні JSON.stringify.

Завдання

важливість: 5

Перетворіть user на JSON, після цього зробіть з нього знову об’єкт і запишіть його в іншу змінну.

let user = {
  name: "Іван Іванов",
  age: 35
};
let user = {
  name: "Іван Іванов",
  age: 35
};

let user2 = JSON.parse(JSON.stringify(user));
важливість: 5

У простих випадках циклічних посиланнь ми можемо виключити від серіалізації властивість, через яку воно виникло, за її ім’ям.

Але іноді ми не можемо просто використовувати назву, оскільки вона може використовуватися як і у циклічних посиланнях, так і в звичайних, потрібних нам, властивостях. Таким чином, ми можемо перевірити властивість за її значенням.

Напишіть функцію replacer, щоб серіалізувати все, але видалити властивості, які посилаються на meetup:

let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  occupiedBy: [{name: "Іван"}, {name: "Аліса"}],
  place: room
};

// циклічне посилання
room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  /* ваш код */
}));

/* результат повинен бути:
{
  "title":"Конференція",
  "occupiedBy":[{"name":"Іван"},{"name":"Аліса"}],
  "place":{"number":23}
}
*/
let room = {
  number: 23
};

let meetup = {
  title: "Конференція",
  occupiedBy: [{name: "Іван"}, {name: "Аліса"}],
  place: room
};

room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  return (key != "" && value == meetup) ? undefined : value;
}));

/*
{
  "title":"Конференція",
  "occupiedBy":[{"name":"Іван"},{"name":"Аліса"}],
  "place":{"number":23}
}
*/

Тут нам також потрібно перевірити key=="", щоб виключити перший виклик, де значення value рівне meetup.

Навчальна карта