التخطي إلى المحتوى
Toova
جميع الأدوات

7 حيل JSON ستوفّر عليك ساعات من العمل

Toova

يستخدم كل مطور JavaScript دالتي JSON.parse وJSON.stringify عشرات المرات أسبوعياً. لكن معظمهم يتوقف عند الأساسيات — تحليل استجابات API وتسلسل الكائنات إلى سلاسل نصية. تقدم هذه الواجهة البرمجية أكثر من ذلك، وبعض الميزات الأقل استخداماً تحل مشكلات تستلزم عادةً مكتبات من طرف ثالث أو ساعات من التشخيص.

يغطي هذا الدليل سبع حيل تتجاوز الإعدادات الافتراضية. كل واحدة منها قابلة للتطبيق فوراً على قواعد كود حقيقية. لا تجريد ولا أمثلة مختلقة — هذه أنماط تظهر في أنظمة الإنتاج.

يمكنك التحقق من أمثلة الكود في هذا المقال واستكشافها باستخدام مُنسّق JSON من Toova، الذي يتحقق ويُنسّق JSON بالكامل في متصفحك.

1. النسخ العميق عبر رحلة JSON (وحدوده)

أقدم حيلة في قاموس JavaScript: استخدام JSON.parse(JSON.stringify(obj)) لإنشاء نسخة عميقة من كائن عادي.

const original = { a: 1, b: { c: 2 } };

// Naive approach — deeply nested objects are still shared
const shallowCopy = { ...original }; // b still points to same object

// JSON deep clone — creates a completely independent copy
const deepClone = JSON.parse(JSON.stringify(original));

deepClone.b.c = 99;
console.log(original.b.c); // 2 — original untouched

يعمل هذا لأن التسلسل إلى سلسلة نصية ثم التحليل مجدداً يُنشئ شجرة كائنات جديدة تماماً بدون مراجع مشتركة. وهو سريع ولا يتطلب تبعيات وهو متاح منذ ES5.

البديل الحديث للاستخدام في الذاكرة هو structuredClone():

// Modern alternative: structuredClone() — handles more types
// Supported in Node.js 17+ and all evergreen browsers
const clone = structuredClone(original);

اعرف حدود نهج JSON قبل الاعتماد عليه:

// JSON.parse/stringify CANNOT handle:
const broken = {
  date: new Date(),      // becomes a string — loses Date prototype
  fn: () => 'hello',    // dropped silently
  undef: undefined,     // dropped silently
  inf: Infinity,        // becomes null
  map: new Map(),       // becomes {}
  cycle: null,          // circular refs throw
};

إذا كان كائنك يحتوي فقط على بيانات عادية — سلاسل نصية وأرقام وقيم منطقية ومصفوفات وكائنات عادية متداخلة — فرحلة JSON آمنة وسريعة. لأي شيء آخر، استخدم structuredClone() أو مكتبة متخصصة.

2. الـ Replacer المخصص للتصفية والإخفاء

يعرف معظم المطورين أن JSON.stringify يأخذ وسيطاً ثانياً، لكن نادراً ما يستخدمونه. هذا الوسيط الثاني هو الـ replacer: إما مصفوفة من المفاتيح المراد تضمينها، أو دالة تتحكم في كيفية تسلسل كل قيمة بالضبط.

الـ replacer كمصفوفة — قائمة بيضاء لمفاتيح محددة:

const user = {
  id: 'u_001',
  name: 'Alice',
  password: 'hunter2',        // must not appear in logs
  creditCard: '4111111111111111', // must not appear in logs
  role: 'admin',
};

// Replacer as array: only include these keys
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}'

الـ replacer كدالة — تحويل القيم أو إخفاؤها:

// Replacer as function: full control over key/value
const masked = JSON.stringify(user, (key, value) => {
  if (key === 'password' || key === 'creditCard') return '[REDACTED]';
  if (key === '' ) return value; // root object — always return
  return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}'

نسخة أكثر تطوراً تُخفي القيم بناءً على شكلها وليس على اسم مفتاحها:

// Replacer for type-based masking
const sanitize = (key, value) => {
  if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
    return '****-****-****-' + value.slice(-4);
  }
  return value;
};

هذه التقنية لا غنى عنها لبرمجيات وسيطة التسجيل: تريد سجلات منظمة مع سياق كامل للكائن، لكن بعض الحقول يجب ألا تصل إلى نظام تجميع السجلات. يتيح لك الـ replacer معالجة هذا عند حدود التسلسل بدلاً من نثر منطق الإخفاء عبر قاعدة الكود.

3. فرز المفاتيح بشكل حتمي

ترتيب مفاتيح الكائن في JavaScript هو ترتيب الإدراج (للمفاتيح النصية). كائنان بنفس المفاتيح لكن تم إنشاؤهما بترتيبات مختلفة ينتجان سلاسل JSON مختلفة، مما يكسر فحوصات المساواة البسيطة ومفاتيح التخزين المؤقت وتجزئات المحتوى.

function sortedStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };

sortedStringify(a) === sortedStringify(b); // true — key order normalized

للكائنات المتداخلة بعمق، طبّق الفرز بشكل تكراري:

// Recursive key sorting for nested objects
function sortKeys(value) {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
    );
  }
  return value;
}

const sorted = JSON.stringify(sortKeys(deepNested));

JSON المفروز ضروري عندما تقوم بـ:

  • توليد مفاتيح التخزين المؤقت من أجسام الطلبات
  • حساب مجاميع التحقق أو التوقيعات على حمولات JSON
  • مقارنة استجابات API في الاختبارات بصرف النظر عن ترتيب الحقول
  • تخزين كائنات الإعداد حيث لا يجب أن يؤثر الترتيب على المساواة

بعد الفرز والطباعة الجميلة، استخدم أداة مقارنة النصوص لمقارنة سلسلتي JSON منظمتين ورؤية القيم التي تغيرت بين الإصدارات بالضبط.

4. معالجة BigInt دون فقدان الدقة

يمكن لنوع Number في JavaScript تمثيل الأعداد الصحيحة بأمان حتى 253 − 1. للمعرّفات التي تُنشئها الأنظمة الموزعة، والمبالغ المالية بوحداتها الصغيرة، والطوابع الزمنية بالنانوثانية، هذا غير كافٍ. يغطي BigInt الأعداد الصحيحة ذات الدقة العشوائية، لكن JSON.stringify لا يعرف كيف يتعامل معها.

const data = {
  amount: 9007199254740993n, // larger than Number.MAX_SAFE_INTEGER
};

// This throws: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // ERROR

الحلول المعيارية:

// Solution 1: Convert to string with a replacer
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'

// Solution 2: toJSON() on the BigInt prototype (monkey-patch — use with caution)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}'

في جانب التحليل، يمكن لدالة reviver استعادة قيم BigInt من تمثيلها النصي:

// Reviver to restore BigInt on parse
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
  if (key === 'amount') return BigInt(value);
  return value;
});
console.log(typeof revived.amount); // 'bigint'

احتفظ بتحويل BigInt إلى سلسلة في طبقة تسلسل مشتركة لضمان تطبيقه بشكل متسق. السماح لقيم BigInt بالتسرب إلى استدعاءات JSON.stringify المتفرقة في أطراف قاعدة الكود يؤدي إلى أخطاء غير متوقعة يصعب تتبعها.

5. اكتشاف المراجع الدائرية

يحدث المرجع الدائري عندما يحتوي كائن على مرجع لنفسه أو لسلف في رسم بيان الكائن. وهي أكثر شيوعاً مما تظن: باعثات الأحداث وعقد DOM وعقد React fiber وكيانات ORM كثيراً ما تحتوي على مراجع عكسية.

// Circular reference example
const obj = { name: 'node' };
obj.self = obj; // obj references itself

JSON.stringify(obj); // throws: TypeError: Converting circular structure to JSON

تعامل معها باستخدام replacer مخصص يتتبع الكائنات التي تمت زيارتها:

// Manual circular reference handler
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

safeStringify(obj); // '{"name":"node","self":"[Circular]"}'

WeakSet هو بنية البيانات الصحيحة هنا: تحتفظ بمراجع كائنات دون منع جمع القمامة، والبحث فيها O(1). يعمل هذا النمط أيضاً في برمجيات وسيطة التسجيل حيث تريد من الأخطاء أن تتدهور بأمان بدلاً من الإطلاق أثناء التسلسل.

نوع شائع: بدلاً من وضع علامة [Circular] على المراجع الدائرية، استبدلها بسلسلة مسار مثل [Circular: $.config.parent] لجعل موقع المرجع صريحاً أثناء التشخيص.

6. طباعة المصفوفات في سطر واحد

الوسيط الثالث لـ JSON.stringify هو المسافة البادئة. تمرير 2 أو 4 يوسّع كل شيء على أسطر متعددة، وهو رائع للكائنات لكن يمكن أن يكون مطوّلاً لمصفوفات الأوليات كالعلامات والمعرّفات والإحداثيات.

const mixed = {
  title: 'Report',
  tags: ['json', 'api', 'debug'],
  config: { indent: 2, sortKeys: true },
  count: 42,
};

// Default: everything multi-line
JSON.stringify(mixed, null, 2);
// {
//   "title": "Report",
//   "tags": [
//     "json",
//     "api",
//     "debug"
//   ],
//   "config": {
//     "indent": 2,
//     "sortKeys": true
//   },
//   "count": 42
// }

يمكنك معالجة المخرجات لاحقاً لطيّ المصفوفات البسيطة في سطر واحد:

// Custom replacer for single-line arrays
function prettyMixed(obj) {
  const raw = JSON.stringify(obj, null, 2);
  // Collapse arrays that contain only primitives onto one line
  return raw.replace(
    /\[\n\s+([\s\S]*?)\n\s+\]/g,
    (match, inner) => {
      const items = inner.split(',\n').map((s) => s.trim());
      if (items.every((s) => !/^[{\[]/.test(s))) {
        return '[' + items.join(', ') + ']';
      }
      return match;
    }
  );
}

النتيجة: تبقى الكائنات على أسطر متعددة للقراءة، وتنطوي مصفوفات الأوليات على سطر واحد للإيجاز. هذا هو التنسيق المستخدم في كثير من ملفات الإعداد ومخرجات السجلات المنظمة حيث يحتاج البشر والآلات معاً إلى قراءة نفس البيانات. يتعامل مُنسّق JSON ومحوّل JSON إلى YAML مع هذا الطيّ تلقائياً.

7. تدفق JSON الكبير

تحميل ملف JSON كبير بالكامل في الذاكرة قبل التحليل هو أكثر أخطاء الأداء شيوعاً في خطوط معالجة JSON. استجابة JSON بحجم 100 ميغابايت تخصص على الأقل 100 ميغابايت من الـ heap للسلسلة الخام، ثم تخصيصاً ثانياً للكائن المحلّل. لملفات أكبر من بضعة ميغابايتات، يعالج محلل التدفق البيانات بشكل تدريجي.

على Node.js مع مكتبة تدفق:

// node --experimental-vm-modules (or use a streaming JSON lib)
// Native streaming with the JSON Source Map proposal (Stage 3, 2026):

// For now, use a library like @streamparser/json or jsonstream2
import { createReadStream } from 'fs';
import parser from '@streamparser/json';

const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
  if (key === 'id') {
    console.log('Found id:', value);
  }
};

createReadStream('large.json').pipe(jsonParser);

في المتصفح باستخدام Fetch Streams API:

// Browser-side streaming with the Fetch + JSON stream decoder:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  // Process complete JSON objects from buffer...
}

التدفق أفيد في خطوط المعالجة الدُفعية ونقاط نهاية التصدير الكبيرة ونصوص تحليل السجلات. لاستجابات API النموذجية التي تقل عن 1 ميغابايت، يكفي JSON.parse القياسي وهو أبسط بكثير. العتبة التي يبدأ فيها التدفق بالإفادة عملياً هي حوالي 5-10 ميغابايت، اعتماداً على تعقيد البنية المحللة وحجم الـ heap في البيئة المستهدفة.

لاستكشاف بنية استجابات JSON الكبيرة قبل بناء محلل تدفق، الصق نموذجاً في مُنسّق JSON لترى المسارات التي تحتوي على البيانات التي تحتاجها، ثم حوّلها إلى CSV أو YAML لمزيد من التحليل.

الخلاصة

تغطي هذه الحيل السبع أكثر جوانب واجهة JSON API تأثيراً:

  • النسخ العميق عبر رحلة JSON — سريع للبيانات العادية، اعرف الحدود
  • الـ Replacer — تصفية المفاتيح وإخفاء القيم الحساسة عند حدود التسلسل
  • فرز المفاتيح — مخرجات حتمية لمفاتيح التخزين المؤقت والتوقيعات وتأكيدات الاختبار
  • معالجة BigInt — تسلسل إلى سلسلة نصية، استعادة عند التحليل
  • اكتشاف المراجع الدائرية — replacer مبني على WeakSet للتسلسل الآمن
  • الطباعة الجميلة مع مصفوفات مدمجة — مخرجات مقروءة بدون مسافات بيضاء زائدة
  • تدفق JSON الكبير — تحليل تدريجي للملفات الكبيرة واستجابات API

للتوثيق الكامل لوسيطات replacer وspace في JSON.stringify، راجع مرجع JSON.stringify في MDN. للبديل الحديث للنسخ العميق، راجع توثيق structuredClone() الذي يغطي جميع الأنواع المدعومة والحالات الطرفية.