اذهب إلى المحتوى

صيغة JSON وتوابعها في جافاسكربت


صفا الفليج

لنقل بأنّ لدينا كائن معقّد البنية ونريد تحويله إلى سلسلة نصية؛ كي نُرسله عبر الشبكة أو نطبعه في الطرفية لتسجيل المخرجات. الطبيعي هو أن تكون في هذه السلسلة النصية كلّ الخاصيات المهمة. يمكننا إجراء هذا التحويل بهذه الطريقة:

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-meetup.png

الاستثناءات وتعديل الكائنات: آلة الاستبدال

إليك الصياغة الكاملة للتابِع 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

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...