لنقل بأنّ لدينا كائن معقّد البنية ونريد تحويله إلى سلسلة نصية؛ كي نُرسله عبر الشبكة أو نطبعه في الطرفية لتسجيل المخرجات. الطبيعي هو أن تكون في هذه السلسلة النصية كلّ الخاصيات المهمة. يمكننا إجراء هذا التحويل بهذه الطريقة:
let user = { name: "John", age: 30, toString() { return `{name: "${this.name}", age: ${this.age}}`; } }; alert(user); // {name: "John", age: 30}
ولكن… أثناء التطوير، نُضيف خاصيات جديدة ونُغيّر أسماء القديمة أو نحذفها حتّى. تحديث ذلك، مثل التابع toString
، كلّ مرّة سيكون جحيمًا حقيقيًا. يمكن أن نمرّ على الخاصيات في الكائن، ولكن ماذا لو كان معقّدًا وفيه كائنات وخاصيات متداخلة؟ حينها سنحتاج إجراء تحويل لتلك أيضًا.
لحسن حظّنا فكتابة تلك الشيفرة لهذه المعضلة ليس له داعٍ، فهناك من حلّها بالفعل.
JSON.stringify
نسق JSON (صيغة كائنات جافاسكربت JavaScript Object Notation) هو نسق عام لتمثيل القيم والكائنات، ويوثّقه المعيار RFC 4627. في بادئ الأمر كان غرض كتابته هو لاستعماله في جافاسكربت، ولكن رويدًا رويدًا بدأت اللغات الأخرى صناعة مكتبات تتعامل معه أيضًا. لهذا يسهل لنا استعمال JSON لتبادل البيانات حين يستعمل جهاز العميل جافاسكربت بينما الخادوم مكتوب بلغة روبي/PHP/جافا/أي لغة خنفشارية أخرى.
تقدّم جافاسكربت التوابِع الآتية:
-
JSON.stringify
لتحويل الكائنات إلى صياغة JSON. -
JSON.parse
لإرجاع بيانات مصاغة بصياغة JSON إلى كائن كما كان.
فمثلًا هنا نستعمل JSON.stringify
على طالب student
:
let student = { name: 'John', age: 30, isAdmin: false, courses: ['html', 'css', 'js'], wife: null }; let json = JSON.stringify(student); alert(typeof json); // حصلنا على سلسلة نصيةّ alert(json); /* كائن مرمّز بِـJSON: { "name": "John", "age": 30, "isAdmin": false, "courses": ["html", "css", "js"], "wife": null } */
يأخذ التابِع JSON.stringify(student)
الكائن ويحوّله إلى سلسلة نصية. تُسمّى سلسلة json
النصية الناتج بكائن مرمّز بِـJSON (JSON-encoded) أو مُسلسل (serialized) أو stringified أو marshalled. صرنا مستعدّين لإرسال الكائن عبر الوِب أو تخزينه في مخزن بيانات خام.
لاحظ من فضلك الاختلافات المهمة بين الكائن المرمّز بِـJSON من الكائن العادي الحرفي:
-
تستعمل السلاسل النصية علامات اقتباس مزدوجة. لا مكان لعلامات الاقتباس المفردة أو الفواصل
`
في JSON. بهذا يصير'John'
هكذا"John"
. -
حتّى خاصيات الكائنات تُحاط بعلامات اقتباس مزدوجة، ولا مناص من ذلك. بهذا يصير
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
خاصيات الكائنات الخاصّة بجافاسكربت.
نذكر منها:
- خاصيات الدوال (التوابِع).
- الخاصيات الرمزية.
-
الخاصيات التي تُخزّن
undefined
.
let user = { sayHi() { // ignored alert("Hello"); }, [Symbol("id")]: 123, // يتجاهلها something: undefined // يتجاهلها }; alert( JSON.stringify(user) ); // {} (كائن فارغ)
غالبًا، لا مانع من ذلك. لو كان هناك مانع فسنرى قريبًا طريقة تخصيص عملية السَلسلة هذه. بفضل دهاء المبرمجين، فالكائنات المتداخلة مدعومة وستُحوَّل تلقائيًا. مثال:
let meetup = { title: "Conference", room: { number: 23, participants: ["john", "ann"] } }; alert( JSON.stringify(meetup) ); /* البنية كاملة تتحوّل إلى سلسلة نصية: { "title":"Conference", "room":{"number":23,"participants":["john","ann"]}, } */
إليك التقييد: وجود الإشارات التعاودية (circular references) ممنوع. مثال:
let room = { number: 23 }; let meetup = { title: "Conference", participants: ["john", "ann"] }; meetup.place = room; // يُشير الاجتماع إلى الغرفة (meetup -> room) room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع (room -> meetup) JSON.stringify(meetup); // خطأ تحاول تحويل بنية تعاوية إلى JSON
هنا فشل التحويل بسبب الإشارات التعاودية: فتُشير room.occupiedBy
إلى meetup
وmeetup.place
إلى room
:
الاستثناءات وتعديل الكائنات: آلة الاستبدال
إليك الصياغة الكاملة للتابِع JSON.stringify
:
let json = JSON.stringify(value[, replacer, space])
المعاملات:
-
value
: القيمة التي ستُرمّز. -
replacer
: : مصفوفة من الخاصيات لترميزها، أو دالة ربط (mapping) بالشكلfunction(key, value)
. -
space
: عدد المسافات لاستعمالها لتنسيق السلسلة النصية.
في أغلب الوقت نستعمل JSON.stringify
بتمرير المُعامل الأول فقط. ولكن لو أردنا تعديل عملية الاستبدال مثل تعديل الإشارات التعاودية، فيمكننا استعمال المُعامل الثاني للتابِع.
لو مرّرنا مصفوفة فيها خاصيات، فستُرمّز تلك الخاصيات فقط. مثال:
let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // room references meetup alert( JSON.stringify(meetup, ['title', 'participants']) ); // {"title":"Conference","participants":[{},{}]}
ربّما نكون هنا صارمين كثيرًا، فقائمة الخاصيات تُطبّق على كامل بنية الكائن، بهذا الكائنات في participants
فارغة إذ أنّ name
ليست في القائمة.
لنضمّن في تلك القائمة كلّ خاصية عدا room.occupiedBy
إذ ستتسبّب بإشارة تعاودية:
let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) ); /* { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */
الآن سَلسلنا كلّ ما في occupiedBy
، ولكن قائمة الخاصيات صارت طويلة. لحسن حظّنا يمكننا استعمال دالة بدل المصفوفة لتكون آلة الاستبدال replacer
. ستُستدعى الدالة لكلّ زوج (key, value)
ويجب أن تُعيد القيمة ”المُستبدَلة“ التي ستحلّ مكان الأصلية، أو undefined
لو أردنا إهمال الخاصية.
في حالتنا هذه سنُعيد القيمة value
”كما هي“ لكل الخاصيات باستثناء occupiedBy
. لنُهمل occupiedBy
ستُعيد الشيفرة undefined
:
let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // يُشير الاجتماع إلى الغرفة }; room.occupiedBy = meetup; // تُشير الغرفة إلى الاجتماع alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); /* أزواج key:value التي تدخل آلة الاستبدال: : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 */
لاحِظ بأنّ الدالة replacer
تأخذ كلّ زوج ”مفتاح/قيمة“ بما في ذلك الكائنات المتداخلة وعناصر المصفوفات، فهي تتطبّق تكراريًا. وقيمة this
داخل replacer
هي الكائن الذي يحتوي على الخاصية الحالية.
الاستدعاء الأوّل خاصّ قليلًا، فهو يستلم ”كائن تغليف“: {"": meetup}
. بعبارة أخرى فأوّل زوج (key, value)
يكون مفتاحه فارغًا وقيمته هي الكائن الهدف كلّه. لهذا نرى السطر الأول في المثال أعلاه: ":[object Object]"
.
الغرض هو تقديم كلّ ما أمكن من ”تسلّط“ لأداة الاستبدال، بهذا يمكنها تحليل الكائنات كاملةً واستبدالها أو إهمالها لو تطلّب الأمر.
التنسيق: المسافات
المُعامل الثالث للتابِع JSON.stringify(value, replacer, space)
هو عدد المسافات التي ستُستعمل لتنسيقها تنسيقًا جميلًا (Pretty format).
في المثال السابق، لم يكن للكائنات المُسلسلة (stringified objects) أيّة مسافات أو مسافات بادئة. لا بأس لو كنّا سنرسل الكائن عبر الشبكة، فالمُعامل space
يُستعمل فقط لتجميل الناتج.
هنا بعبارة space = 2
نقول لجافاسكربت بأن تعرض الكائنات المتداخلة على عدّة أسطر، بمسافتين بادئتين داخل كل كائن:
let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } }; alert(JSON.stringify(user, null, 2)); /* إزاحة بمسافتين: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ /* بينما JSON.stringify(user, null, 4) يعطينا إزاحة أكبر: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */
نستعمل المُعامل space
فقط لغرض الناتج الجميل وعمليات تسجيل المخرجات.
تابِع ”toJSON“ مخصّص
كما يوجد toString
للتحويل إلى سلاسل نصية، يمكن للكائنات أيضًا تقديم تابِع toJSON
للتحويل إلى JSON. تستدعي JSON.stringify
ذاك التابِع تلقائيًا لو وجدته. مثال:
let room = { number: 23 }; let meetup = { title: "Conference", date: new Date(Date.UTC(2017, 0, 1)), room }; alert( JSON.stringify(meetup) ); /* { "title":"Conference", "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: "Conference", room }; alert( JSON.stringify(room) ); // 23 alert( JSON.stringify(meetup) ); /* { "title":"Conference", "room": 23 } */
كما نرى، استُعمِل التابِع toJSON
مرتين، مرة حين استدعاه JSON.stringify(room)
مباشرةً، ومرة حين كانت الخاصية room
داخل كائن مرمّز آخر.
التابع JSON.parse
لنفكّ ترميز سلسلة JSON نصية، سنحتاج تابِعًا آخر بالاسم JSON.parse. صياغته هي:
let value = JSON.parse(str, [reviver]);
المعاملات:
-
str
: سلسلة JSON النصية التي سيُحلّلها. -
reviver
: دالة اختياريةfunction(key,value)
تُستدعى لكلّ زوج(key, value)
ويمكن لها تعديل القيمة.
مثال:
// مصفوفة مُسلسلة (stringified array) let numbers = "[0, 1, 2, 3]"; numbers = JSON.parse(numbers); alert( numbers[1] ); // 1
أو حين استعمالها للكائنات المتداخلة:
let userData = '{ "name": "John", "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: "John", // خطأ: اسم خاصية بدون علامات اقتباس "surname": 'Smith', // خطأ: علامات اقتباس مُفردة في القيمة (يجب أن تكون مزودجة) 'isAdmin': false // خطأ: علامات اقتباس مُفردة في المفتاح (يجب أن تكون مزدوجة) "birthday": new Date(2000, 2, 3), // خطأ: استعمال "new" ممنوع، فقط وفقط قيم "friends": [0,1,2,3] // هنا لا بأس }`;
وأجل، لا تدعم JSON التعليقات، فلو أضفتها سيتحوّل الكائن إلى كائن غير صالح.
هناك نسق آخر بالاسم JSON5 ويُتيح لنا عدم إحاطة المفاتيح بعلامات اقتباس، وكتابة التعليقات وغيرها. إلّا أنّها مكتبة مستقلة وليست في مواصفة لغة جافاسكربت. لم يصنع المطوّرون كائنات JSON العادية لتكون بهذه الصرامة لأنّهم كسالى، بل لنُعوّل على شيفرات خوارزميات التحليل، إضافة إلى عملها بسرعة فائقة.
استعمال آلة الإحياء
تخيّل أنّنا استلمنا كائن meetup
مُسلسل من الخادوم، وهذا شكله:
// title: (meetup title), date: (meetup date) let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
…نريد الآن فكّ ترميزه، أي إعادته إلى كائن جافاسكربت عادي. يكون ذلك باستدعاء JSON.parse
:
let str = '{"title":"Conference","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
ستعدّلها لتكون تاريخًا:
let str = '{"title":"Conference","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":"Conference","date":"2017-11-30T12:00:00.000Z"}, {"title":"Birthday","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
. - تقدّم جافاسكربت التوابِع JSON.stringify لسَلسلة الكائنات إلى JSON، وJSON.parse للقراءة من JSON.
- يدعم كلا التابِعين دوال تعديل لتكون القراءة والكتابة ”ذكيّة“.
-
لو كان في الكائن تابِع
toJSON
، فسيستدعيهJSON.stringify
.
تمارين
تحويل الكائن إلى JSON وتحويله كما كان
الأهمية: 5
حوّل الكائن user
إلى JSON واقرأه ثانيةً ليكون متغيرًا آخرًا.
let user = { name: "John Smith", age: 35 };
الحل
let user = { name: "John Smith", age: 35 }; let user2 = JSON.parse(JSON.stringify(user));
استثناء الإشارات السابقة
الأهمية: 5
يمكننا في الحالات العادية من الإشارات التعاودية استثناء خاصية محدّدة لألا تُسلسل، حسب اسمها. ولكن أحيانًا لا نستطيع استعمال الاسم إذ يُستعمل في الإشارات التعاودية زائدًا الخاصيات العادية. يمكننا هنا فحص الخاصية حسب قيمتها.
اكتب دالة replacer
تُسلسل كل شيء ولكن تُزيل الخاصيات التي تُشير إلى meetup
:
let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; // إشارات تعاودية room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { /* شيفرتك هنا*/ })); /* هكذا النتيجة المطلوبة: { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */
الحل
let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { return (key != "" && value == meetup) ? undefined : value; })); /* { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */
علينا هنا (أيضًا) فحص key==""
لنستثني أوّل نداء إذ لا مشكلة بأن تكون القيمة value
هي meetup
.
ترجمة -وبتصرف- للفصل JSON methods, toJSON من كتاب The JavaScript language
اقرأ أيضًا
- المقال التالي: توابع الأنواع الأولية
- المقال السابق: النوع Date: تمثيل التاريخ والوقت
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.