Скажімо, у нас є складний об’єкт, і ми хотіли б перетворити його в рядок, щоб відправити його через мережу або просто вивести його для логування.
Безумовно, такий рядок повинен включати всі важливі властивості.
Ми могли б реалізувати перетворення наступним чином:
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
.