27 грудня 2021 р.

Методи 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.parse для перетворення JSON-тексту назад в об’єкт.

Наприклад, тут ми викликаємо JSON.stringify з об’єктом student:

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

let json = JSON.stringify(student);

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

alert(json);
/* JSON-кодований об’єкт:
{
  "name": "Іван",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "wife": 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 – це лише незалежна специфікація даних, тому деякі притаманні для JavaScript властивості об’єктів пропускаються в JSON.stringify.

А саме:

  • Функціональні властивості (методи).
  • Символьні ключі та значення.
  • Властивості, що зберігають undefined.
let user = {
  sayHi() { // ігнорується
    alert("Hello");
  },
  [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:

За винятком та трансформацією: Замінник

Повний синтаксис 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, серіалізується. Але список властивостей досить довгий.

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

Функція буде викликана для кожного (key, value), і повинна повернути значення “replaced”, яке буде використовуватися замість оригінального. Або 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 всередині raplacer – це об’єкт, який містить поточну властивість.

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

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

Форматування: пробіл

Третій аргумент 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) став рядком. Це тому, що всі дати мають вбудований метод 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

Or for nested objects:

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 є настільки строгим не тому, що її розробники ледачі, а тому, що дозволяє легко, надійно та дуже швидко реалізувати алгоритм кодування та читання.

Використання функції відновлення

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

Це виглядає так:

// 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() ); // Помилка!

Ой! Помилка!

Значення metaup.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.

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

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)