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

الكائنات في جافاسكربت


Emq Mohammed

يوجد سبعة أنواع للبيانات في JavaScript كما رأينا في فصل أنواع البيانات. ستة من هذه الأنواع تُدعى "أساسية" (primitive) لأنها تحوي قيمة شيء واحد فقط (سواء كان نصًا أو رقمًا أو أي شيء آخر).

في المُقابل، تُستخدم الكائنات لتخزين مجموعة من البيانات المتنوعة والوحدات المعقدة المُرَمَّزة بمفاتيح. تُضمَّن الكائنات في ما يقارب جميع نواحي JavaScript، لذا يجب علينا أن نفهمها قبل التعمق في أي شيء آخر.

يمكن إنشاء أي كائن باستخدام الأقواس المعقوصة {…} مع قائمة اختيارية بالخاصيات. الخاصية هي زوج من "مفتاح: قيمة" (key: value) إذ يكون المفتاح عبارة عن نص (يُدعى "اسم الخاصية")، والقيمة يمكن أن تكون أي شيء.

يمكننا تخيل الكائن كخزانة تحوي ملفات. يُخزن كل جزء من هذه البيانات في الملف الخاص به باستخدام المفتاح. يمكن إيجاد، أو إضافة، أو حذف ملف باستخدام اسمه.

1.png

يمكن إنشاء كائن فارغ (خزانة فارغة) باستخدام إحدى الصيغتين التاليتين:

let user = new Object(); // (object constructor) صياغة باني كائن
let user = {};  // (object literal) صياغة مختصرة لكائن عبر الأقواس

 

2.png

تُستخدم الأقواس المعقوصة {...} عادة، وهذا النوع من التصريح يُسمى «الصياغة المختصرة لتعريف كائن» (object literal).

القيم المُجرَّدة والخاصيات

يمكننا إضافة بعض الخاصيات (properties) إلى الكائن المعرَّف بالأقواس {...} مباشرة بشكل أزواج "مفتاح: قيمة":

let user = {     // كائن
  name: "John",  // name عبر المفتاح John خزِّن القيمة 
  age: 30        // age خزِّن القيمة 30 عبر المفتاح 
};

لدى كل خاصية مفتاح (يُدعى أيضًا "اسم " أو "مُعَرِّف") قبل النقطتين ":" وقيمة لهذه الخاصية بعد النقطتين.

يوجد خاصيتين في الكائن user:

  1. اسم الخاصية الأولى هو "name" وقيمتها هي "John".
  2. اسم الخاصية الثانية هو "age" وقيمتها هي "30".

يمكن تخيل الكائن السابق user كخزانة بملفين مُسَمَّيان "name" و "age".

3.png

يمكننا إضافة، وحذف، وقراءة الملفات من الخزانة في أي وقت. يمكن الوصول إلى قيم الخاصيات باستخدام الصيغة النُقَطية (dot notation):

// الحصول على قيم خاصيات الكائن:
alert( user.name ); // John
alert( user.age ); // 30

يمكن للقيمة أن تكون من أي نوع، لِنُضِف قيمة من نوع بيانات منطقية (boolean):

user.isAdmin = true;

 

4.png

يمكننا استخدام المُعامِل delete لحذف خاصية:

delete user.age;

 

5.png

يمكننا أيضا استخدام خاصيات بأسماء تحوي أكثر من كلمة، لكن يجب وضعها بين علامات الاقتباس "":

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // يجب أن تكون الخاصية ذات الاسم المُحتوي على أكثر من كلمة بين علامتي اقتباس
};

 

6.png

يمكن إضافة فاصلة بعد آخر خاصية في القائمة:

let user = {
  name: "John",
  age: 30,
}

وتُسمى بفاصلة "مُعَلِّقَة" أو "زائدة" فهي تجعل إضافة أو حذف أو التنقل بين الخاصيات أسهل لأن جميع الأسطر تُصبِح متشابهة.

الأقواس المعقوفة

لا تعمل طريقة الوصول إلى الخاصيات ذات الأسماء المحتوية على أكثر من كلمة باستخدام الصيغة النُقَطية:

// تعرض هذه التعليمة وجود خطأ في الصياغة
user.likes birds = true

ذلك لأن الصيغة النُقطية تحتاج لاسم متغير صحيح. لا يحوي مسافات أو حدود أخرى. يوجد بديل يعمل مع أي نص "صيغة الأقواس المعقوفة" []:

let user = {};

// تخزين
user["likes birds"] = true;

// استرجاع
alert(user["likes birds"]); // true

// حذف
delete user["likes birds"];

يعمل كل شيء وفق المطلوب الآن. يُرجى ملاحظة أنَّ النص بداخل الأقواس مُحاط بعلامتي اقتباس (تعمل علامات التنصيص الأخرى بطريقة صحيحة أيضًا).

تتيح لنا الأقواس المعقوفة أيضًا جلب اسم خاصية ناتجة عن قيمة أي تعبير - بدلًا من اسم الخاصية الفعلي - مثل استعمال اسم من متغير كما يلي:

let key = "likes birds";

// user["likes birds"] = true; يماثل قول 
user[key] = true;

يمكن حساب قيمة المتغير key أثناء التنفيذ أو قد تعتمد قيمته على مدخلات المستخدمين ثم نستخدمه للوصول إلى الخاصية مما يعطي مرونة كبيرة في التعامل. إليك المثال التالي:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// الوصول باستخدام متغير
alert( user[key] ); // John (if enter "name")

لا يمكن استخدام الصيغة النُقطية بالطريقة نفسها:

let user = {
  name: "John",
  age: 30
};

let key = "name";
alert( user.key ) // غير معروف

الخاصيات المحسوبة

يمكن استخدام الأقواس المعقوفة في كائن معرَّف بالأقواس، وهذا ما يسمى بالخاصيات المحسوبة. إليك المثال التالي:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // fruit يؤخذ اسم الخاصية من المتغير 
};

alert( bag.apple ); // fruit="apple" قيمتها 5 إذا كانت

معنى الخاصية المحسوبة سهل: تعني [fruit] أنَّ اسم الخاصية يجب أن يُؤخذ من fruit؛ لذا، إن أدخل الزائر "apple"، ستصبح قيمة bag هي {apple: 5}.

يعمل الأمر السابق بالطريقة التالية ذاتها:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// fruit خُذ اسم الخاصية من المتغير 
bag[fruit] = 5;

لكن شكله يبدو أفضل، أليس كذلك؟! يمكن استخدام تعابير أكثر تعقيدًا داخل الأقواس المعقوفة:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

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

يمكن استخدام الأسماء المحجوزة مع أسماء الخاصيات

لا يمكن لمتغير أن يحمل اسم إحدى الكلمات المحجوزة في اللغة مثل "for"، أو "let"، أو "return"، …الخ. لكن لا تُطبَّق هذه القاعدة على أسماء خاصيات الكائنات. فيمكن استخدام أي اسم معها:

let obj = {
  for: 1,
  let: 2,
  return: 3
};

alert( obj.for + obj.let + obj.return );  // 6

عمومًا، يمكن استخدام أي اسم، لكن هناك استثناء: "__proto__" لهذا الاسم معاملة خاصة لأسباب تاريخية. مثلًا، لا يمكننا استخدام الاسم على أنَّه قيمة لغير كائن:

let obj = {};
obj.__proto__ = 5;
alert(obj.__proto__); // لا تعمل وفق المطلوب [object Object]

كما نرى في الشيفرة، تم تجاهل تخزين القيمة الأولية 5.

تخزين أزواج الخاصيات التحكمية في كائن وإتاحة تحديد مفاتيح هذه الأزواج للزائر قد يجعل الشيفرة مصدرًا للأخطاء ومليئًا بالثغرات.

في تلك الحالة، قد يختار الزائر - الخبير والماكر - الاسم __proto__ ليكون مفتاحًا ويخرِّب البنية المنطقية للشيفرة (كما في المثال أعلاه). يوجد طريقة لجعل الكائنات تتعامل مع __proto__ بِعدِّها خاصية عادية وسنتطرق لها لاحقًا بعد فهم الكائنات بشكل أعمق.

يوجد أيضا هيكل بيانات آخر يدعى Map، والذي ستتعلمه في القسم الذي يتحدث عن نوعي البيانات Map و Set، اللذين يدعمان استعمال أي نوع مع المفاتيح.

اختزال قيم الخاصيات

نستخدم غالبا قيم متغيرات موجودة مسبقًا لتكون قيمًا لأسماء الخاصيات في الشيفرات الحقيقية. اطلع مثلًا على الشيفرة التالية:

function makeUser(name, age) {
  return {
    name: name,
    age: age
    // ... خاصيات أخرى
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

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

function makeUser(name, age) {
  return {
    name, // name: name يماثل كتابة 
    age   // age: age يماثل كتابة
    // ...
  };
}

يمكننا استخدام كلًا من الخاصيات الاعتيادية والاختزال في الكائن ذاته:

let user = {
  name,  // name:name يماثل
  age: 30
};

فحص الكينونة

قابلية الوصول إلى أي خاصية في الكائن هي إحدى مميزات الكائنات، ولكن ألَا يوجد أي خطأ في حال لم تكن الخاصية موجودة؟! عند محاولة الوصول إلى خاصية غير موجودة، تُرجَع القيمة undefined. مما يُعطي طريقة متعارفة لفحص كينونة (وجود) خاصية ما من عدمه بموازنتها مع القيمة "undefined" ببساطة:

let user = {};

alert( user.noSuchProperty === undefined ); // تحقق هذه الموازنة يشير إلى عدم وجود الخاصية

يوجد أيضا مُعامل خاص "in" لفحص تواجد أي خاصية. طريقة استخدام هذا المعامل كالتالي:

"key" in object

مثلا:

let user = { name: "John", age: 30 };

alert( "age" in user ); // true, user.age موجود
alert( "blabla" in user ); // false, user.blabla غير موجود

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

let user = { age: 30 };

let key = "age";
alert( key in user ); // key إذ تؤخذ قيمة المتغير true تطبع القيمة
               // user ويُتحقق من وجود خاصية بذلك الاسم في الكائن

استخدام "in" مع الخاصيات التي تُخزن القيمة undefined

تفحص عملية الموازنة الصارمة "=== undefined" غالبًا وجود الخاصية وفق المطلوب. لكن يوجد حالة خاصة تفشل فيها هذه العملية، بينما لا يفشل المعامل in إن استعمل مكانها. هذه الحالة هي عند وجود الخاصية في الكائن لكنها تُخزن القيمة undefined:

let obj = {
  test: undefined
};

alert( obj.test ); // ولكن هل تُعدُّ الخاصية موجودة أم لا؟ undefined يطبع القيمة

alert( "test" in obj ); // وتَعدُّ الخاصية موجودة في الكائن true تُطبع القيمة

الخاصية obj.test موجودة فعليًا في الشيفرة أعلاه، لذا يعمل المُعامل in بصحة.

تحدث مثل هذه الحالات نادرًا فقط لأن القيمة undefined لا تُستخدَم بكثرة. نستخدم غالبا القيمة null للقيم الفارغة أو الغير معرفة، لذلك يُعد المُعامل in قليل الاستخدام في الشيفرات.

الحلقة for…in

يوجد شكل خاص للحلقة for..in للمرور خلال جميع مفاتيح كائنٍ ما. هذه الحلقة مختلفة تمامًا عما درسناه سابقًا، أي الحلقة for(;;)‎.

صياغة الحلقة تكون بالشكل التالي:

for (key in object) {
  // يتنفذ ما بداخل الحلقة لكل مفتاح ضمن خاصيات الكائن
}

مثلا، لنطبع جميع خاصيات الكائن user:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // المفاتيح
  alert( key );  // name, age, isAdmin
  // قيم المفاتيح
  alert( user[key] ); // John, 30, true
}

لاحظ أن جميع تراكيب "for" تتيح لنا تعريف متغير التكرار بِداخل الحلقة، مثل let key في المثال السابق. يمكننا أيضًا استخدام اسم متغير آخر بدلًا من key. إليك مثال يُستخدم بكثرة:

for (let prop in obj)

الترتيب في الكائنات

هل الكائنات مرتبة؟ بمعنى آخر، إن تنقلنا في حلقة خلال كائن، هل نحصل على جميع الخاصيات بنفس الترتيب الذي أُضيفت به؟ وهل يمكننا الاعتماد على هذا؟

الإجابة باختصار هي: "مرتب بطريقة خاصة": الخاصيات الرقمية يُعاد ترتيبها، تظهر باقي الخاصيات بترتيب الإنشاء ذاته كما في التفاصيل التالية.

لنرَ مثالًا لكائن بِرموز الهاتف:

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for (let code in codes) {
  alert(code); // 1, 41, 44, 49
}

قد تُستخدم الشيفرة لاقتراح قائمة من الخيارات للمستخدم. إن كنا نبني موقعًا لزوار من ألمانيا فقد نريد أن تظهر 49 أولا. لكن، عند تشغيل الشيفرة، نرى شيئا مختلفًا تماما:

  • تظهر USA (1)‎ أولًا
  • ثم Switzerland (41)‎ وهكذا.

تُستخدم رموز الهواتف بترتيب تصاعدي لأنها أعدادٌ، لذا نرى 1, 41, 44, 49.

خاصيات عددية؟ ما هذا؟

تعني "الخاصية العددية" (integer property) نصًا يمكن تحويله من وإلى عدد دون أن يتغير. لذا فإن 49 هو اسم خاصية عددي لأنه عند تحويله إلى عدد وإرجاعه لنص يبقى كما هو. لكن "1.2" و "‎+49" ليست كذلك:

// هي دالة تحذف الجزء العشري Math.trunc
alert( String(Math.trunc(Number("49"))) ); // "49", الخاصية العددية ذاتها
alert( String(Math.trunc(Number("+49"))) ); // ‏ "49" مختلفة عن "49+" => إذًا ليست خاصية عددية 
alert( String(Math.trunc(Number("1.2"))) ); //  ‏ "1" مختلفة عن "1.2" => إذًا ليست خاصية عددية 

في المقابل، إن كانت المفاتيح غير عددية، فتُعرَض بالترتيب الذي أُنشِئت به. إليك مثال على ذلك:

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // add one more

// تُعرض الخاصيات الغير رقمية بترتيب الإنشاء
for (let prop in user) {
  alert( prop ); // name, surname, age
}

لذا، لحل مشكلة رموز الهواتف يمكننا التحايل وجعلها غير عددية بإضافة "+" قبل كل رمز كما يلي:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for (let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

الآن تعمل وفق المطلوب!

النسخ بالمرجع

إحدى الاختلافات الرئيسية بين الكائنات والمتغيرات الأولية هي أنَّ الكائنات تُخزن وتُنسخ "بالمرجع". بينما تُخزن/تُنسخ القيم الأولية: النصوص، والأرقام، والقيم المنطقية بِعدِّها قيمةً كاملةً. إليك المثال التوضيحي التالي:

let message = "Hello!";
let phrase = message;

لدينا في هذه الشيفرة متغيرين مستقلين كلاهما يُخزن النص "Hello!‎ ".

7.png

الكائنات ليست كذلك.

لا يُخزِّن المتغير الكائن نفسه، وإنما "عنوانه في الذاكرة". بمعنى آخر، "مرجع للكائن"

هنا صورة للكائن:

let user = {
  name: "John"
};

 

8.png

كما نرى، يُخزَّن المتغير في مكان ما في الذاكرة ويُخزِّن المتغير user مرجعًا إليه.

عند نسخ متغير من نوع كائن، يُنسَخ المرجع الذي يشير إلى ذلك الكائن ولا يُنسَخ الكائن نفسه ويُكرَّر. إذا تخيلنا الكائن كخزانة، فإن المتغير هو مفتاح الخزانة ونسخ المتغير يكرِّر المفتاح وليس الخزانة ذاتها. إليك مثال يوضح ما ذكرناه:

let user = { name: "John" };

let admin = user; // يُنسَخ المرجع

الآن، أصبح لدينا متغيرين، كلاهما يحمل مرجعًا للكائن ذاته:

9.png

يمكننا استخدام كلا المتغيرين (المفتاحين) للوصول إلى الخزانة وتعديل محتواها:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // "admin" عُدِّلت باستخدام المرجع 

alert(user.name); // "user" أي يمكن رؤية التعديلات من المرجع 'Pete' تُعرَض القيمة

يوضح المثال أعلاه وجود كائن واحد فقط. كما لو كان لدينا خزامة بمفتاحين، واستخدمنا أحدهما (admin) للوصول إلى الخزانة. ثم إذا استخدمنا الآخر (user) لاحقًا، فسنرى إنعكاس التغييرات التي أجراها الأول.

الموازنة بالمرجع

يعمل مُعاملي المساواة == والمساواة الصارمة === بنفس الطريقة للكائنات. يكون الكائنان متساويان إذا كانَا الكائن نفسه فقط. أي، إذا كان متغيران يشيران للكائن ذاته، فهما متساويان:

let a = {};
let b = a; // نفس المرجع

alert( a == b ); // true, كلا المتغيرين يشيران للكائن نفسه
alert( a === b ); // true

وهنا متغيران مستقلان ليسا متساويين أي يشيران إلى كائنين منفصلين حتى وإن كانا متماثلين تمامًا:

let a = {};
let b = {}; // متغيران منفصلان

alert( a == b ); // خطأ

يُحوَّل الكائن إلى قيمة أولية (أساسية) في الموازنات مثل obj1 > obj2 أو obj == 5. سندرس كيفية تحويل الكائنات قريبًا، لكن، في الحقيقة، مثل هذه الموازنات تكون نادرة الضرورة وتنتج غالبًا من خطأ في كتابة الشيفرة.

الكائنات الثابتة

يمكن تغيير الكائن المُعَرَّف على أنَّه ثابت (أي وسم المتغير الذي يحوي الكائن بالكلمة المفتاحية const) دون حصول أي أخطاء. إليك الشيفرة التالية مثلًا:

const user = {
  name: "John"
};

user.age = 25; // (*)

alert(user.age); // 25

قد يبدو أن السطر (*) سيسبب خطأ، لكن لا يوجد أي مشكلة البتة. ذلك لأنَّ const تحافظ على قيمة user ذاتها وتمنع تغييرها. ويُحزِّن user المرجع للكائن نفسه دائمًا ولا يتغيِّر بتعديل الكائن نفسه. يدخل السطر (*) إلى الكائن ولا يعيد تعيين قيمة المتغير user.

يمكن أن يعطي const خطأ إن حاولنا تغيير قيمة المتغير user مثل:

const user = {
  name: "John"
};

// (user خطأ (لا يمكن تغيير قيمة المتغير
user = {
  name: "Pete"
};

لكن، ماذا إن أردنا إنشاء خاصيات ثابتة ضمن الكائن؟ سيُعطي user.age = 25 آنذاك خطأ، وذلك ممكن أيضًا. سيتم شرحه في الفصل رايات الخاصيات وواصفاتها.

الاستنساخ والدمج

إذًا، نسخ كائن يُنشِئ مرجعًا إضافيًّا له. لكن ماذا إن كنا نريد تكرار كائن فعليًّا أي إنشاء نسخة مستقلة منه؟ ذلك ممكن أيضًا، لكنه أصعب قليلًا لعدم وجود دالة تقوم بذلك في JavaScript ذلك لأنَّه لا حاجة لذلك بكثرة. النسخ بالمرجع كافٍ غالبًا.

لكن إن أردنا ذلك فعلًا، فإننا نحتاج لإنشاء كائن وتكرار هيكل الكائن الموجود عبر التنقل خلال خاصياته ونسخها على المستوى الأولي. وإليك مثال على ذلك:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // الكائن الجديد الفارغ

// إليه user ننسخ جميع خاصيات المتغير
for (let key in user) {
  clone[key] = user[key];
}

// الآن أصبحت النسخة مستقلة تماما
clone.name = "Pete"; // تغيير البيانات في النسخة

alert( user.name ); //  في الكائن الأصلي John تظل

يمكننا استخدام الدالة Object.assign للغرض ذاته.

الصياغة الدالة هي:

Object.assign(dest, [src1, src2, src3...])
  • تُعد المُعاملات dest، و src1، وحتى srcN كائنات (يمكن أن تكون بالعدد المُراد).
  • تنسخ الدالة خاصيات جميع الكائنات src1, ..., srcN إلى الكائن dest. بمعنى آخر، تُنسخ جميع الخاصيات لجميع المُعاملات بدءًا من المُعامل الثاني إلى المُعامل الأول. ثم يتم إرجاع dest.

مثلا، يمكننا استخدام الدالة لدمج عدة كائنات إلى كائن واحد:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// user إلى permissions2 و permissions1 تنسخ جميع الخاصيات من 
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

إن كان الكائن user يحوي أحد أسماء الخاصيات مسبقًا، فسيتم إعادة كتابة محتواها:

let user = { name: "John" };

// isAdmin وإضافة name إعادة كتابة  
Object.assign(user, { name: "Pete", isAdmin: true });

// now user = { name: "Pete", isAdmin: true }

يمكننا أيضًا استخدام الدالة Object.assign بدلًا من الحلقة للاستنساخ البسيط:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

تنسخ الدالة جميع خاصيات الكائن user إلى الكائن الفارغ وتُرجِعه كما في الحلقة لكن بشكل أقصر. حتى الآن، عدَدْنا جميع خاصيات user أولية (أساسية)، لكن قد تكون بعض الخاصيات مرجعًا لكائن آخر مثل الشيفرة التالية فما العمل؟

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

في هذه الحالة نسخ clone.sizes = user.sizes ليس كافيًا لأنَّ user.sizes عبارة عن كائن، فسيُنسَخ على أنَّه مرجعٌ. هكذا، سيصبح لدى clone و user الحجم ذاته:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // صحيح،  الكائن ذاته

// sizes الكائن الفرعي clone و user يتشارك الكائنان
user.sizes.width++;       // تغيير خاصية من مكان ما
alert(clone.sizes.width); // 51, يعرض النتيجة من مكان آخر

لإصلاح هذا، يجب استخدام حلقة الاستنساخ التي تفحص كل قيمة في user[key]‎، وإن كان كائنًا نستبدل الهيكل الخاص به أيضًا. هذه الطريقة تُسمى "استنساخ عميق" (deep cloning).

يوجد خوارزمية عامة للاستنساخ العميق تنفِّذ الحالة السابقة بشكل صحيح، بالإضافة إلى حالات أكثر تعقيدًا. تُدعى هذه الخوارزمية خوارزمية الاستنساخ المُهيكلة. حتى لا نُعيد اختراع العجلة مجدَّدًا، يمكننا استخدام تنفيذ جاهز للحالة من مكتبة JavaScript lodash، تُدعى الدالة _.cloneDeep(obj).

الخلاصة

الكائنات عبارة عن مصفوفات ترابطية بميزات خاصة عديدة. تُخزن الكائنات خاصيات (أزواج مفتاح-قيمة)، بشرط أنه:

  • يجب أن تكون مفاتيح الخاصيات نصوصًا أو رموزًا (غالبًا نصوص).
  • يمكن أن تكون القيم من أي نوع.

يمكننا استخدام ما يلي للوصول إلى خاصية:

  • الصيغة النُقَطِيَّة: obj.property.
  • صيغة الأقواس المعقوفة obj["property"]‎. تتيح لنا الأقواس المعقوفة أخذ مفتاح من متغير، مثل obj[varWithKey]‎.

عمليات أخرى:

  • لِحذف خاصية: delete obj.prop.
  • لِفحص تواجد خاصية بمفتاح معين: "key" in obj.
  • للتنقل خلال كائن: الحلقة for (let key in obj)‎.

تُخَزَّن الكائنات وتُنسخ باستخدام المرجع. بمعنىً آخر، لا يُخزن المتغير قيمة الكائن (object value) لكنه يُخزن مرجعًا (reference) يمثِّل موقع قيمة الكائن في الذاكرة. لذا فإن نسخ هذا المتغير أو تمريره إلى دالة سَينسخ هذا المرجع وليس الكائن ككُل. جميع العمليات (مثل إضافة أو حذف خاصيات) المُنفَّذة على مرجع منسوخ تُنفَّذ على الكائن نفسه.

لعمل نسخة حقيقية (الاستنساخ) يمكننا استخدام Object.assign أو _.cloneDeep(obj).

يُسمى ما درسناه في هذا الفصل "كائن بسيط" أو كائن فقط. يوجد العديد من أنواع الكائنات الأخرى في JavaScript:

  • الكائن Array (مصفوفة): لتخزين مجموعة البيانات المرتبة،
  • الكائن Date (تاريخ): لتخزين معلومات عن الوقت والتاريخ،
  • الكائن Error (خطأ): لتخزين معلومات عن خطأ ما.
  • وغيرها من أنواع الكائنات.

لدى هذه الأنواع ميزاتها الخاصة التي سيتم دراستها لاحقًا. يقول بعض الأشخاص أحيانًا شيئًا مثل "نوع مصفوفة" أو "نوع تاريخ" (الاسم الذي وضعته بين قوسين بجانب نوع الكائن)، لكن هذه الأنواع ليست أنواعًا مستقلة بحد ذاتها، إنما تنتمي إلى نوع البيانات Object (كائن) وتتفرع عنه بأشكال مختلفة.

تُعد الكائنات في JavaScript قوية جدًا. درسنا في هذا الفصل جزءًا بسيطًا من موضوع هائل جدًا. سنتعامل مع الكائنات لاحقًا بصورة أقرب وسَنتعلم أكثر عنها في فصول أخرى.

تمارين

مرحبًا، بالكائن

الأهمية: 5

اكتب الشيفرة البرمجية، سطر لكل متطلب:

  1. أنشئ كائنًا فارغًا باسم user.
  2. أضف الخاصية name بالقيمة John.
  3. أضف الخاصية surname بالقيمة Smith.
  4. غير قيمة الخاصية name إلى Pete.
  5. احذف الخاصية name من الكائن.

الحل

let user = {};
user.name = "John";
user.surname = "Smith";
user.name = "Pete";
delete user.name;

التحقق من الفراغ

اكتب الدالة isEmpty(obj)‎ التي تُرجع القيمة true إن كان الكائن فارغًا، وتُرجِع القيمة false في الحالات الأخرى. يجب أن تعمل كالتالي:

let schedule = {};

alert( isEmpty(schedule) ); // true

schedule["8:30"] = "get up";

alert( isEmpty(schedule) ); // false

إليك تجربة حية للمثال.

الحل

قم بالمرور خلال الكائن ونفذ الأمر return false مباشرة إن عثرت على أي خاصية:

function isEmpty(obj) {
  for (let key in obj) {
    // إن بدأت الحلقة بالعمل، فهناك خاصية في الكائن
    return false;
  }
  return true;
}

كائنات ثابتة؟

الأهمية: 5

هل من الممكن تغيير كائن صُرِّح عنه بالكلمة المفتاحية const؟ ما رأيك؟

const user = {
  name: "John"
};

// هل تعمل؟
user.name = "Pete";

الحل

بالفعل ستعمل بدون مشاكل. تحمي الكلمة المفتاحية const المتغير نفسه من التغيير فقط. بمعنى آخر، يخزن user مرجعًا للكائن ولا يمكن تغييره مع وجود التصريح عنه بالكلمة المفتاحية const لكن يمكن تغيير محتوى الكائن.

const user = {
  name: "John"
};

// تعمل
user.name = "Pete";

// خطأ
user = 123;

جمع خاصيات الكائن

لدينا كائن يُخزن رواتب الفريق:

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
}

اكتب الشيفرة التي تجمع الرواتب وتُخزنها في المتغير sum. يجب أن يكون مجموع المثال أعلاه 390. إن كان salaries فارغًا، فإن الناتج سيكون 0.

الحل

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
};

let sum = 0;
for (let key in salaries) {
  sum += salaries[key];
}

alert(sum); // 390

ضرب الخاصيات العددية بالقيمة 2

الأهمية: 3

أنشئ دالةً باسم multiplyNumeric(obj)‎ تضرب جميع الخاصيات العددية في الكائن obj في العدد 2.

مثلا:

// قبل الاستدعاء
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// بعد الاستدعاء
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

لاحظ أنَّ الدالة multiplyNumeric لا يجب أن تُرجِع أي شيء. يجب أن تُعدِّل القيم بداخل الكائن.

ملاحظة: استخدام typeof لفحص الأعداد.

إليك تجربة حية للتمرين.

الحل

function multiplyNumeric(obj) {
  for (let key in obj) {
    if (typeof obj[key] == 'number') {
      obj[key] *= 2;
    }
  }
}

ترجمة -وبتصرف- للفصل Objects من كتاب 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.


×
×
  • أضف...