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

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

كما عرفنا من د رسأنواع البيانات، هنالك ثماني أنواع للبيانات في الجافاسكريبت. سبعة منهم يسمون “أولية primitive”، لأن قيمهم تحتوي شيئا واحداً (لتكن سلسلة نصية أو رقم أو أي شيْء).

في المقابل، تستخدم الكائنات لحفظ مجموعات keyed collections من مختلف البيانات والكيانات المركبة.في الجافاسكريبت، تدخل الكائنات تقريبا في كل جانب من جوانب اللغة. لذا يتوجب علينا فهمها قبل التعمق في أي شيء آخر.

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

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

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

let user = new Object(); // "object constructor" syntax صيغة منشئ الكائن
let user = {};  // "object literal" syntax

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

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

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

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

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

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

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

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

يمكننا إضافة، وحذف، وقراءة الملفات من الخزانة في أي وقت.

يمكن الوصول إلى قيم الخاصيات باستخدام الصيغة النُقَطية (dot notation):

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

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

user.isAdmin = true;

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

delete user.age;

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

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

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

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

وهذا يسمى فاصلة “زائدة” أو “معلقة”. يجعل من السهل إضافة، إزالة، ونقل الخصائص، لأن جميع الأسطر تصبح متشابهة.

الكائن المعرف بأنه ثابت يمكنه أن يتغير

ملاحظة: يمكن تعديل كائن معلن على أنه ثابت.

مثلاً:

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

قد يبدو أن الخط (*) سيسبب خطأ، لكن ذلك لن يحدث. يُحدِّد “const” قيمة “user”، وليس إصلاح محتوياتها.

لن يُظهر “const” خطأ إلا إذا حاولنا تعيين "user = …` كله.

هناك طريقة أخرى لخلق خصائص كائن ثابتة، وسنتناولها لاحقًا في الفصل رايات الخصائص و واصفاتها.

الأقواس المربعة

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

// سيعطي هذا خطأ في الصياغة
user.likes birds = true

لا تفهم الجافاسكريبت هذه الصيغه. يعتقد أننا نتعامل مع “user.likes”، ثم تعطي خطأ في بناء الجملة عندما يصادف “birds” غير متوقعة.

تتطلب النقطة أن يكون المفتاح معرفًا متغيرًا صالحًا. هذا يعني: أنه لا يحتوي على مسافات، ولا يبدأ برقم ولا يتضمن أحرفًا خاصة (يُسمح بـ $ و_).

هناك بديل “رمز القوس المربع” الذي يعمل مع أي سلسلة:

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 يمكن حسابه وقت التنفيذ (run-time) أو اعتمادا على ما يدخله المستخدم. من ثم نستخدمها للوصول إلى الخاصية. وهذا يمنحنا قدراً كبيراً من المرونة.

مثلاً:

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 ) // undefined

خصائص محسوبة (computed properties)

يمكننا استخدام الأقواس المربعة في كائن حرفي object literal، عند إنشاء كائن. وهذا ما يسمى * الخصائص المحسوبة computed properties*.

مثلا:

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

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

alert( bag.apple ); // 5 if fruit="apple"

معنى computed property سهل: [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
};

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

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

اختصار قيمة الخاصية (Property value shorthand)

في الشيفرة الحقيقية، غالبًا ما نستخدم المتغيرات الموجودة بصفتها قيَمًا لأسماء الخصائص.

مثلاً:

function makeUser(name, age) {
  return {
    name: name,
    age: age,
    // ...other properties
  };
}

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

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

بدلاً من name:name يمكننا فقط كتابة name، كهذا المثال:

function makeUser(name, age) {
  return {
    name, // تماماً مثل name: name
    age,  // تماماً مثل age: age
    // ...
  };
}

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

let user = {
  name,  // same as name:name
  age: 30
};

قيود أسماء الخصائص Property names limitations

كما نعلم، لا يمكن للمتغير أن يمتلك اسماً يساوي واحداً من الكلمات المحفوظة للغة (language-reserved words) مثل “for”, “let”, “return” إلخ.

لكن بالنسبة لخاصية في كائن، لا توجد مثل هذه القيود:

// كل هذه الخصائص صحيحة
let obj = {
  for: 1,
  let: 2,
  return: 3
};

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

باختصار، ليس هناك أي قيود لأسماء الخصائص. يمكنها أن تكون أي حرف أو رمز (نوع مميز من identifiers، سيتم الحديث عنه لاحقاً).

الأنواع الأخرى تتحول تلقائياً لسلاسل نصية strings.

مثلاً, رقم 0 يتحول إلى حرف "0" عند استخدامه اسما لخاصية:

let obj = {
  0: "test" // same as "0": "test"
};

// كليهما يسمحان بالوصول إلى الخاصية (رقم 0 يتحول إلى حرف "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (same property)

There’s a minor gotcha with a special property named __proto__. لا يمكننا استخدام الاسم على أنَّه قيمة لغير كائن:

let obj = {};
obj.__proto__ = 5; // assign a number
alert(obj.__proto__); // [object Object] - the value is an object, didn't work as intended

كما نرى في الشيفرة أعلاه، إعطاء قيمة أولية 5 يتم تجاهلها.

سوف نغطي طبيعة __proto__ في subsequent chapters، واقتراح ways to fix مثل هذا السلوك.

فحص الكينونة، “in” معامل

ميزة ملحوظة للكائنات في الجافاسكريبت، مقارنة بالعديد من اللغات الأخرى، هي أنه من الممكن الوصول إلى أي خاصية. لن يكون هناك خطأ إذا كانت الخاصية غير موجودة!

قراءة خاصية غير موجودة تُرجع فقط “غير محدد”. لذا يمكننا بسهولة اختبار ما إذا كانت الخاصية موجودة:

let user = {};

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

يوجد أيضا مُعامل خاصة "in" لفحص وجود أي خاصية.

The syntax is:

"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 ); // true, خاصية "age" موجودة

لماذا يوجد المعامل in? أليس من الكافي المقارنة مقابل undefined?

حسناً، في معظم الأحيان المقارنة بـundefined تكون جيدة. ولكن هناك حالة خاصة عندما تفشل، لكن "in" تعمل بشكل صحيح.

يحدث ذلك عند وجود خاصية داخل كائن، ولكنها تخزن قيمة undefined:

let obj = {
  test: undefined
};

alert( obj.test ); // تعطي undefined, لذلك - ألا توجد هذه الخاصية؟

alert( "test" in obj ); // true, الخاصية موجودة بالفعل!

في الشيفرة السابقة، الخاصية obj.test موجودة بالفعل. لذا معامل inيعمل بشكل صحيح.

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

The “for…in” loop

للمرور على كل مفاتيح الكائن، يوجد شكل خاص آخر للحلقة loop: 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” هو اسم خاصية عددي, لأنه عند تحويله إلى عدد وإرجاعه لنص, يبقى كما هو. لكن “+49” و “1.2” are ليسا كذلك:

// هي دالة تحذف الجزء العشري 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
}

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

ملخص الدرس

الكائنات عبارة عن مصفوفات ترابطية بميزات خاصة عديدة.

تحفظ الخصائص ب (key-value pairs), حيث:

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

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

  • رمز النقطة: obj.property.
  • رمز الأقواس المربعة obj["property"]. تسمح الأقواس المربعة بأخذ المفتاح من متغير, مثل obj[varWithKey].

معاملات إضافية Additional operators:

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

ما درسناه في هذا الفصل يسمى “plain object”, أو Object.

هنالك أنواع عديدة من الكائنات في الجافا اسكريبت:

  • Array مصفوفة لتخزين مجموعة البيانات المرتبة,
  • Date تاريخ لتخزين معلومات عن الوقت والتاريخ,
  • Error خطأ لتخزين معلومات عن خطأ ما.
  • …وما إلى ذلك.

لدى هذه الأنواع ميزاتها الخاصة التي سيتم دراستها لاحقًا. يقول بعض الأشخاص أحيانًا شيئًا مثل “نوع مصفوفة” أو “نوع تاريخ”, لكن هذه الأنواع ليست أنواعًا مستقلة بحد ذاته, لكنها تنتمي “object” نوع البانات"كائن". وتتفرع عنه بأشكال مختلفة.

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

أمثله هامة

الأهمية: 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;
الأهمية: 5

اكتب دالة isEmpty(obj) و التى تقوم بارجاعtrueاذا كان الكائن لا يحتوى على خواص و ارجاعfalse` فى الحالات الأخرى

يجب أن تعمل بهذا الشكل :

let schedule = {};

alert(isEmpty(schedule)); // true

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

alert(isEmpty(schedule)); // false

افتح sandbox بالإختبارات.

ما عليك سوى التكرار فوق الكائن و “إرجاع false” على الفور إذا كان هناك خاصية واحدة على الأقل.

function isEmpty(obj) {
  for (let key in obj) {
    // if the loop has started, there is a property
    return false;
  }
  return true;
}

افتح الحل الإختبارات في sandbox.

الأهمية: 5

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

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
الأهمية: 3

أنشئ دالة multiplyNumeric (obj) تضرب جميع الخصائص الرقمية لـ obj بـ2.

على سبيل المثال:

إذا كانت “الرواتب” فارغة ، فيجب أن تكون النتيجة “0”.

// before the call
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// after the call
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

يرجى ملاحظة أن multiplyNumeric لا تحتاج إلى إرجاع أي شيء. يجب تعديل الكائن في مكانه.

ملاحظة. استخدم typeof للتحقق من وجود رقم هنا. s في 2

افتح sandbox بالإختبارات.

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

افتح الحل الإختبارات في sandbox.

نسخ الكائنات والمؤشرات

واحد من أكبر الإختلافات بين الكائنات والقيم primitives هو أنها تخزن وتنسخ عن طريق الإشارة إليها.

القيم Primitive: strings, numbers, booleans – تخزن وتنسخ كقيمة كاملة.

على سبيل المثال:

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

تكون النتيجة هي متغيران منفصلان كل منهما به كلمة "Hello!".

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

المتغير لا يحمل الكائن نفسه بل يحمل “عنوانه في الذاكرة” وبكلمات أخرى يحمل “مؤشر له”.

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

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

هنا يتم تخزين الكائن في مكان ما في الذاكرة والمتغير user لديه مؤشر لذلك المكان.

عندما يتم نسخ الكائن – يتم نسخ المؤشر ولا يتم تكرار الكائن.

For instance:

let user = { name: "John" };

let admin = user; // ينسخ المؤشر

الآن لدينا متغيرين كل منهما به مؤشر لنفس الكائن:

يمكننا استخدام أي متغير لنصل للكائن ونعدل فيه:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // تم تغييرها بواسطة المؤشر "admin"

alert(user.name); // 'Pete', التغيرات مرئية بواسطة مؤشر "user"

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

المقارنة بالمؤشرات

العامل == والعامل === هما نفس الشئ مع الكائنات.

الكائنان يكونان متساويان فقط إذا كانا يشيران لنفس الكائن.

هنا المتغيران يشيران لنفس الكائن لذا هما متساويان:

let a = {};
let b = a; // نسخ المؤشر

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

وهنا كائنان منفصلان غير متساويان حتى ولو كانا فارغين:

let a = {};
let b = {}; // كائنان منفصلان

alert(a == b); // false

مقارنة مثل obj1 > obj2 أو مقارنة كائن مع قيمة primitive obj == 5 يتم تحويل الكائنات إلى primitives. سنتكلم لاحقًا عن طريقة التحويل ولكن في الحقيقة هذه المقارنات نادرًا ما تحدث وفي الغالب تكون خطأ برمجي.

نسخ ودمج, Object.assign

نسخ المتغير ينشئ مؤشر آخر لنفس الكائن.

لكن ماذا إذا أردنا نسخ الكائن نفسه كنسخة منفصلة ؟

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

لكن إذا أردنا ذلك حقًا يمكننا فعل ذلك عن طريق عمل كائن آخر والمرور على خواص الكائن الحالي ونسخها واحدة تلو الأخرى.

كالتالي:

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 لذلك.

The syntax is:

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 };

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

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

إذا كانت الخاصية موجودة يتم استبدالها:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // now user = { name: "Pete" }

إيضًا يمكننا استخدام Object.assign لاستبدال الحلقة التكرارية for..in في النسخ البسيط:

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

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

تنسخ كل الخواص من user إلى كائن فارغ وترجعه.

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

حتى الآن إفترضنا أن كل خواص user هي primitive. ولكن الخواص يمكن أن تكون مؤشرات لكائنات أخرى فماذا سنفعل ؟

مثل هذا:

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

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

الآن ليس كافيًا نسخ clone.sizes = user.sizes لأن user.sizes هو كائن وسيتم نسخ المؤشر ويكون clone و user لهما نفس الخاصية sizes:

مثل هذا:

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

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

alert(user.sizes === clone.sizes); // true, نفس الكائن

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

لإصلاح ذلك يجب استخدام حلقة نسخ تفصل كل user[key] وإذا كان كائن يتم إيضًا نسخ بنيته وهذا يسمى النسخ العميق “deep cloning”.

هناك خوارزمية لذلك تتعامل مع ما رأيناه في الأعلى أو أكثر تعقيدًا وتسمى Structured cloning algorithm.

يمكننا كتابتها باستخدام الغستدعاء الذاتي Recursion أو لا نعيد اختراع العدلة ونستخدم الدالة الجاهزة مثل _.cloneDeep(obj) من مكتبة lodash.

ملخص الدرس

الكائنات توضع وتنسخ بالمؤشرات أو بمعنى آخر أن المتغير لا يحمل القيمة نفسها ولكنه يحمل مؤشر لها أي عنوان هذه القيمة في الذاكرة. لذلك نسخ هذا المتغير أو تمريره لدالة لا ينسخ القيمة نفسها بل المؤشر.

كل العمليات التي تتم بواسطة النسخة (مثل إضافة وحذف الخواص) تحدث على نفس الكائن.

لعمل نسخة حقيقية يمكننا استخدام Object.assign لما يسمى “shallow copy” (الكائنات الداخلية تنسخ بالمؤشر) أو دالة “deep cloning” مثل _.cloneDeep(obj).

كنس البيانات المهملة في جافاسكربت (Garbage Collection)

تتم عملية ادارة الذاكرة في الJavaScript بطريقة تلقائية غير مرئية نحن ننشئ لبقيم البسيطة, الكائنات , الدوال… و كل ذلك يستهلك من الذاكرة.

ما الذي سيحدث اذا لم نعد بحاجة لأحدهم ؟ كيف يكتشفها محرك ال JavaScript و يتخلص منها ؟

قابلية الوصول

المفهوم الأساسي لعملية ادارة الذاكرة في ال JavaScript هو قابلية الوصول.

ببساطة, القيم التي يمكن الوصول اليها هي القيم التي يمكن الولوج اليها و استخدامها بشكل ما, و تخزينها في الذاكرة شئ حتمي.

  1. هناك بعض القيم الأساسية التي يمكن الوصول اليها دائماو لا يمكن مسحها لأسباب واضحة. علي سبيل المثال:

    • المتغيرات المحلية و المعاملات للدالة التي يتم استخدامها
    • المتغيرات و معاملات الدوال في سلسلة متصلة من الاستدعاءات
    • المتغيرات العامة
    • (هنالك المزيد, بعضهم داخلي)

    هذه القيم تسمي ب الجذور (roots).

  2. أي قيمة اخري يمكن اعتبارها قابلة للوصول اليها اذا ما كان يمكن الوصول اليها بالفعل من جذر عن طريق مرجع(reference) او مجموعة من المراجع.

    علي سبيل المثال, اذا كان هناك كائن مخزن في متغير محلي, و الكائن به خاصية مرجه لكائن اخر, يمكن اعتبار هذا الكائن انه يمكن الوصول اليه, و يمكن الوصول ايضا الي كل مراجع هذا الكائن, كما يتم شرحه في المثال التالي.

تحدث عملية خلفية في محرك ال JavaScript يسمي ب جامع القمامة garbage collector يتم خلالها مراقبة كل الكائنات و ازالة الكائنات التي لا يمكن الوصول اليها.

مثـــال بـسـيـط

هذا هوا ابسط مثـال:

// user له مرجع الي الكائن
let user = {
  name: "John"
};

السهم هنا يمثل مرجع لكائن, المتغير العام "user" يرجع الي الكائن {name: "John"} (الذي سنطلق عليه جون اختصارا). خاصية "name" في كائن جون تخزن قيمة بسيطة لذا تم رسمها داخل الكائن.

اذا تم استخدام اسم المتغير user مرة اخري , هنا يتم فقدان مرجع الكائن:

user = null;

هنا لا يمكن الوصول الي كائن جون و بالتالي لا يمكن الدخول اليه لعدم وجود مرجع له, و هنا يأتي دور جامع القمامة للتخلص من بياناته و افراغ المساحة التخزينية.

مرجعين

تصور الأن اننا نسخنا مرجع user الي admin:

// user له مرجع الي الكائن
let user = {
  name: "John"
};

let admin = user;

Now if we do the same:

user = null;

…في هذه الحالة ما زال بالامكان الوصول الي الكائن عن طريق المتغير العام admin, لذا فهو مخزن بالذاكرة. اذا تم استخدام المتغير admin في مكان أخر ايضا هنا يمكن ازالة الكائن .

الكائنات المترابطة

Now a more complex example. The family: و الان الي مثال أكثر تعقيدا, كل العائلة:

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

الدالة marry “تزوج” كائنين عن طريق اعطاء كل منهم مرجع للأخر و ترجع كائن جديد يحتوي كليهما.

البناء الناتج في الذاكرة:

حتي الأن, كل الكائنات يمكن الوصول اليها. و الأن فلنزيل اثنين من المراجع:

delete family.father;
delete family.mother.husband;

الغاء مرجع واحد فقط من المرجعين لا يكفي ,ستظل امكانية الوصول الي الكائنين ممكنة.

و لكن اذا تم الغاء المرجعين, هنا يمكن ان نري ان جون لم يعد له اي مرجع:

Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible.

بعد عملية جمع القمامة:

الجزيرة التي لا يمكن الوصول اليها

هنالك امكانية ان تصبح جزيرة بأكملها من الكائنات المترابطة لا يمكن الوصول اليها و ان تمحي من الذاكرة.

الكائن هو كما بالمثال السابق, و بالتالي:

family = null;

الصورة من داخل الذاكرة تصبح كالتالي:

هذا المثال يشرح اهمية مفهوم قابلية الوصول

من الواضح ان جون و آن ما زالا متصلين ببعضهما بمراجع, لكن هذا غير كافي

كائن "family" السابق فقد اتصاله بالجذر, لا يوجد له اي مرجع الآن, لذا فالجزيرة باكملها لا يمكن الوصول اليها و تتم ازالتها.

الخوارزميات الداخلية

The basic garbage collection algorithm is called “mark-and-sweep”. الخوارزمية الأساسية لجمع القمامة تسمي ب "mark-and-sweep".

تحدث عملية جمع القمامة غالبا علي عدة خطوات:

  • جامع القمامة يأخذ الجذر و يضع عليه علامة
  • ثم يزور كل المراجع المرتبطة به و يضع علي كل منهم علامة
  • ثم يزور كل كل الكائنات التي عليها علامة و يضع علامة علي كل مراجعهم, كل الكائنات التي تمت زيارتها يتم تذكرها حتي لا تتم زيارة نفس الكائن مرتين.
  • … و هكذا الي ان يتم وضع علامة علي كل المراجع بداية من الجذر
  • كل الكائنات عدي التي تم وضع علامة عليها يتم ازالتها

كمثال, فلنفترض ان بناء الكائن لدينا كالتالي:

يمكننا بكل بساطة ملاحظة جزيرة لا يمكن الوصول اليها في الجانب الأيمن. و الآن نستطيع ان نري كيف يطبق جامع القمامة ال "mark-and-sweep".

أول خطوة هي وضع علامة عل الجذر:

ثم وضع علامة علي كل المراجع المرتبطة به:

…و مراجعم كذلك ان امكن

و الآن, كل الكائنات التي لم تتم زيارتها في هذه العملية تعتبر كائنات لا يمكن الوصول اليها و ستتم ازالتها:

يمكننا تخيل العملية كصب دلو كبير من الطلاء من الجذر و الذي سيسري خلال كل المراجع و يضع علامة علي كل الكائنات التي يمكن الوصول اليها, و الكائنات التي لا يمكن الوصول اليها يتم ازالتها.

تلك هي المبادئ التي يعمل علي اساسها جامع القمامة. محرك ال JavaScript يطبق العديد من التحسينات لجعله يعمل بشكل اسرع و الا يؤثر علي الآداء.

بعض هذه التحسينات:

  • مجموعة الأجيال – -- يتم تقسيم الكائنات الي مجموعتين “الجديدة”, “القديمة”, الكثير من الكائنات يتم انشائها و تؤدي وظيفتها و تموت بسرعة, فيمكن التخلص منها بسرعة. اما الكائنات التي تعيش لفترة طوية تصبح “قديمة” و لا يتم التحقق منها و ازالتها بنفس الكثافة.

  • المجموعة المتزايدة --في حالة وجود العديد من الكائنات, و حاولنا المرور ووضع علامة علي الكائن كله مرة واحدة, سيستهلك هذا بعضا من الوقت مما قد يؤدي الي بعض التأخير في عملية التنفيذ, لذلك يحاول جامع القمامة تقسيم نفسه الي اجزاء, كل الأجزاء يتم استخدامها وحدها واحدة تلو الأخري, مما قد يتطلب المزيد من الادارة لمتابعة التغييرات و تسجيلها, و لكن تأخيرات عديدة صغيرة افضل من تأخير واحد كبير.

  • مجموعة وقت الخمول – يحاول جامع القمامة ان يعمل في حالة ان وحدة المعالجة المركزية (CPU) في حالة خمول حتي لا يؤثر علي عملية التنفيذ.

هنالك العديد من التحسينات في خوارزميات جامع القمامة. و علي قدر ما اود ان اشرحها هنا,يجب ان نتوقف و ذلك لأن المحركات الختلفة تتبني طرق و حلول مختلفة و الأهم من ذلك ان الأشياء تتغير بتتطور المحركات, لذا ادرس أكثر “مقدما” فبدون الحاجة الحقيقية لمعرفتها فهي لا تستحق العناء الا ان كنت و بالطبع تمتلك الشغف للمعرفة فالروابط بالأسفل ستساعدك بالتأكيد.

ملخص الدرس

اهم النقاط لتعرفها:

  • جامع القمامة يعمل بشكل تلقائي, لا يمكن اجباره علي العمل او ايقافه
  • الكائنات تظل في الذاكرة طالما كان بالمكان الوصول اليها
  • كون الكائن له مرجع لا يعني بالضرورة ان يمكن الوصول اليه(من الجذر): قد تصبح مجموعة من الكائنات لا يمكن الوصول اليها

المحركات الحديثة تطور خوارزميات حديثة لعملية جمع القمامة

كتاب "The Garbage Collection Handbook: The Art of Automatic Memory Management"(R.Jones et al) يجمع بعضها

اذا كنت علي علم بالمستويات العميقة من البرمجيات , فهنالك المزيد من المعلومات عن جـامع القمامة V8 في هذا المقال

A tour of V8: Garbage Collection

تنشر ايضا V8 blog مقالات حول تنظيم الذاكرة من آن الي أخر, بطبيعة الحال, لتعلم جامع القمامة يفضل ان تتهيأ عن طريق تعلم مكونات V8 بشكل عام و قراءة مدونة Vyacheslav Egorov و الذي عمل كأحد مهندسي V8. استطيع ان أقول V8 تحديدا لأنه الأكثر تغطية عن طريق المقالات علي الانترنت. و بالنسبة للمحركات الأخري, العديد من الطرق متشابهة, و لكن جامع القمامة يختلف في نقاط عديدة.

المعرفة المتعمقة للمحركات ضرورية في حين الحاجة الي تحسينات ذات مستوي متطور, فأن تخطط لمعرفتها بعد ان تصبح علي معرفة جيدة باللغة لهي بالتأكيد خطوة حكيمة.

الدوال في الكائنات واستعمالها `this`

تُنشّأ الكائنات عادة لتُمَثِّل أشياء من العالم الحقيقي مثل المستخدمين، والطلبات، وغيرها:

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

يمكن للمستخدم في العالم الحقيقي أن يقوم بعدة تصرفات: مثل اختيار شيء من سلة التسوق، تسجيل الدخول، والخروج …إلخ.

تُمَثَّل هذه التصرفات في لغة JavaScript بإسناد دالة إلى خاصية وتدعى الدالة آنذاك بالتابع (method، أي دالة تابعة لكائن).

أمثلة على الدوال

بدايةً، لنجعل المستخدم user يقول مرحبًا:

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

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

استخدمنا هنا تعبير الدالة لإنشاء دالة تابع للكائن user وربطناها بالخاصية user.sayHi ثم استدعينا الدالة. هكذا أصبح بإمكان المستخدم التحدث! الآن أصبح لدى الكائن user الدالة sayHi.

يمكننا أيضًا استخدام دالة معرفة مسبقًا بدلًا من ذلك كما يلي:

let user = {
  // ...
};

//  أولا، نعرف دالة
function sayHi() {
  alert("Hello!");
};

// أضِف الدالة للخاصية لإنشاء تابع
user.sayHi = sayHi;

user.sayHi(); // Hello!
يسمى كتابة الشيفرة البرمجية باستخدام الكائنات للتعبير عن الاشياء «بالبرمجة الشيئية/كائنية» ([object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming)، تُختَصَر إلى "OOP").
OOP هو موضوع كبيرجدًا، فهو علم مشوق ومستقل بذاته. يعلمك كيف تختار الكائنات الصحيحة؟ كيف تنظم التفاعل فيما بينها؟ كما يعد علمًا للهيكلة ويوجد العديد من الكتب الأجنبية الجيدة عن هذا الموضوع مثل كتاب “Design Patterns: Elements of Reusable Object-Oriented Software” للمؤلفين E.Gamma، و R.Helm، و R.Johnson، و J.Vissides أو كتاب “Object-Oriented Analysis and Design with Applications” للمؤلف G.Booch، وغيرهما.

اختصار الدالة

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

// يتصرف الكائنان التاليان بالطريقة نفسها

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// يبدو شكل الدالة المختصر أفضل، أليس كذلك؟
user = {
  sayHi() { // same as "sayHi: function()"
    alert("Hello");
  }
};

يمكننا حذف الكلمة "function" وكتابة sayHi()‎ كما هو موضح. حقيقةً، التعبيرين ليسا متطابقين تمامًا، يوجد اختلافات خفية متعلقة بالوراثة في الكائنات (سيتم شرحها لاحقًا)، لكن لا يوجد مشكلة الآن. يفضل استخدام الصياغة الأقصر في كل الحالات تقريبًا.

الكلمة “this” في الدوال

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

يمكن للدالة استخدام الكلمة this للوصول إلى نسخة الكائن التي استدعتها

أي، قيمة this هي الكائن “قبل النقطة” الذي استُخدِم لاستدعاء الدالة.

مثلًا:

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

  sayHi() {
    // "this" هو الكائن الحالي"
    alert(this.name);
  }

};

user.sayHi(); // John

أثناء تنفيذ user.sayHi()‎ هنا، ستكون قيمة this هي الكائن user عمليًا، يمكن الوصول إلى الكائن بدون استخدام this بالرجوع إليه باستخدام اسم المتغير الخارجي:

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

  sayHi() {
    alert(user.name); // "user" يدلًا من "this"
  }

};

…لكن، لا يمكن الاعتماد على الطريقة السابقة. فإذا قررنا نسخ الكائن user إلى متغير آخر، مثلا: admin = user وغيرنا محتوى user لشيء آخر، فسيتم الدخول إلى الكائن الخطأ كما هو موضح في المثال التالي:

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

  sayHi() {
    alert( user.name ); // يتسبب في خطأ
  }

};


let admin = user;
user = null; // تغيير المحتوى لتوضيح الأمر

admin.sayHi(); //  يُرجِع خطأ sayHi() استخدام الاسم القديم بِداخل

إن استخدمنا this.name بدلًا من user.name بداخل alert، فستعمل الشيفرة عملًا صحيحًا.

“this” غير محدودة النطاق

الكلمة this في JavaScript تتصرف تصرفًا مختلفًا عن باقي اللغات البرمجية. فيمكن استخدامها في أي دالة. انظر إلى المثال التالي، إذ لا يوجد خطأ في الصياغة

function sayHi() {
  alert( this.name );
}

تُقَيَّم قيمة this أثناء تنفيذ الشيفرة بالاعتماد على السياق. مثلًا، في المثال التالي، تم تعيين الدالة ذاتها إلى كائنين مختلفين فيصبح لكل منهما قيمة مختلفة لـ “this” أثناء الاستدعاء:

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// استخدام الدالة ذاتها مع كائنين مختلفين
user.f = sayHi;
admin.f = sayHi;

// tلدى الاستدعائين قيمة مختلفة لـ
// "this"  التي بداخل الدالة تعني المتغير الذي قبل النقطة
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (يمكن الوصول إلى الدالة عبر الصيغة النقطية أو الأقواس المربعة – لا يوجد مشكلة في ذلك)

القاعدة ببساطة: إذا استُدعِيَت الدالة obj.f()‎، فإن this هي obj أثناء استدعاء f؛ أي إما user أو admin في المثال السابق.

استدعاءٌ دون كائن: this == undefined

يمكننا استدعاء الدالة دون كائن:

function sayHi() {
  alert(this);
}

sayHi(); // غير معرَّف

في هذه الحالة ستكون قيمة this هي undefined في الوضع الصارم. فإن حاولنا الوصول إلى this.name سيكون هناك خطأ.

في الوضع غير الصارم، فإن قيمة this في هذه الحالة ستكون المتغير العام (في المتصفح window والتي سَنشرحها في فصل المتغيرات العامة). هذا السلوك زمني يستخدم إصلاحات الوضع الصارم "use strict".

يُعد هذا الاستدعاء خطأً برمجيًا غالبًا. فإن وًجِدت this بداخل دالة، فمن المتوقع استدعاؤها من خلال كائن.

الأمور المترتبة على this الغير محدودة النطاق

إن أتيت من لغة برمجية أخرى، فمن المتوقع أنك معتاد على "this المحدودة" إذ يمكن لِلدوال المعرَّفة في الكائن استخدام this التي ترجع للكائن.

تستخدم this بحرية في JavaScript، وتُقَيَّم قيمتها أثناء التنفيذ ولا تعتمد على المكان حيث عُرِّفت فيه، بل على الكائن الذي قبل النقطة التي استدعت الدالة.

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

لدوال السهمية لا تحوي "this

الدوال السهمية (Arrow function) هي دوال خاصة: فهي لا تملك this مخصصة لها. إن وضعنا this في إحدى هذه الدوال فَستؤخذ قيمة this من الدالة الخارجية.

مثلًا، تحصل الدالة arrow()‎ على قيمة this من الدالة الخارجية user.sayHi()‎:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

يُعد ذلك إحدى ميزات دوال الدوال السهمية، وهي مفيدة عندما لا نريد استخدام this مستقلة، ونريد أخذها من السياق الخارجي بدلًا من ذلك. سَنتعمق في موضوع الدوال السهمية لاحقًا في فصل «إعادة النظر في الدوال السهمية».

ملخص الدرس

  • الدوال المخزنة في الكائنات تسمى «توابع» (methods).
  • تسمح هذه الكائنات باستدعائها بالشكل object.doSomething()‎.
  • يمكن للدوال الوصول إلى الكائن المعرفة فيه (أو النسخة التي استدعته المشتقة منه) باستخدام الكلمة المفتاحيةthis.
  • تُعَرَّف قيمة this أثناء التنفيذ.
  • قد نستخدم this عند تعريف دالة، لكنها لا تملك أي قيمة حتى استدعاء الدالة.
  • يمكن نسخ دالة بين الكائنات.
  • عند استدعاء دالة بالصيغة object.method()‎، فإن قيمة this أثناء الاستدعاء هي object.

لاحظ أن الدوال السهمية مختلفة تتعامل تعاملًا مختلفًا مع this إذ لا تملك قيمة لها. عند الوصول إلى this بداخل دالة سهمية فإن قيمتها تؤخذ من النطاق الموجودة فيه.

أمثلة هامةجدا

تُرجِع الدالة makeUser كائنًا هنا. ما النتيجة من الدخول إلى ref الخاص بها؟ ولماذا؟

What is the result of accessing its ref? Why?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // ما النتيجة؟

Aالإجابة: ظهور خطأ.

جربها:

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); //  ِلِقيمة غير معرفة 'name' خطأ: لا يمكن قراءة الخاصية

ذلك لأن القواعد التي تعين this لا تنظر إلى تعريف الكائن. ما يهم هو وقت الاستدعاء. قيمة this هنا بداخل makeUser()‎ هي undefined، لأنها استُدعيَت كدالة منفصلة، وليس كدالة بصياغة النقطة.

قيمة this هي واحدة للدالة ككل، ولا تؤثر عليها أجزاء الشيفرة ولا حتى الكائنات. لذا فإن ref: this تأخذ this الحالي للدالة

function makeUser(){
  return this; // this time there's no object literal
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

كما ترى فإن نتيجة alert( makeUser().name ) هي نفسها نتيجة alert( user.ref.name ) من المثال السابق هنا حالة معاكسة تمامًا:

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
};

let user = makeUser();

alert( user.ref().name ); // John

أصبحت تعمل هنا لأن user.ref()‎ هي دالة، وقيمة this تعَيَّن للكائن الذي قبل النقطة '.'

أنشئ كائنًا باسم calculator يحوي الدوال الثلاث التالية:

  • read()‎ تطلب قيمتين وتحفظها كخصائص الكائن.
  • sum()‎ تُرجِع مجموع القيم المحفوظة.
  • mul()‎ تضرب القيم المحفوظة وتُرجِع النتيجة.
let calculator = {
  // ... your code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

قم بتشغيل العرض التوضيحي

افتح sandbox بالإختبارات.

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

افتح الحل الإختبارات في sandbox.

لدينا الكائن ladder (سُلَّم) الذي يتيح الصعود والنزول:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // shows the current step
    alert( this.step );
  }
};

الآن، إن أردنا القيام بعدة استدعاءات متتالية، يمكننا القيام بما يلي:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

عَدِّل الشيفرة الخاصة بالدوال up، و down، و showStep لجعل الاستدعاءات متسلسلة كما يلي:

ladder.up().up().down().showStep(); // 1

يُستخدم هذا النمط بنطاق واسع في مكتبات JavaScript

افتح sandbox بالإختبارات.

الحل هو إرجاع الكائن نفسه من كل استدعاء.

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
}

ladder.up().up().down().up().down().showStep(); // 1

يمكننا أيضا كتابة استدعاء مستقل في كل سطر ليصبح سهل القراءة بالنسبة للسلاسل الأطول

ladder
  .up()
  .up()
  .down()
  .up()
  .down()
  .showStep(); // 1

افتح الحل الإختبارات في sandbox.

الباني والعامل "new"

نُنشِئ الكائنات باستخدام الصيغة الاعتيادية المختصرة {...}. لكننا نحتاج لإنشاء العديد من الكائنات المتشابهة غالبًا، مثل العديد من المستخدمين، أو عناصر لقائمة وهكذا. يمكن القيام بذلك باستخدام الدوال البانية (constructor functions) لكائن والمُعامِل "new".

الدالة البانية

تقنيًا، الدوال البانية هي دوال عادية، لكن يوجد فكرتين متفق عليها:

  1. أنها تبدأ بأحرف كبيرة.
  2. يجب تنفيذها مع المُعامِل "new" فقط. إليك المثال التالي:
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false

عند تنفيذ دالة مع الُعامِل new، تُنَفَّذ الخطوات التالية:

  1. يُنشَأ كائن فارغ ويُسنَد إلى this.
  2. يُنَفَّذ محتوى الدالة. تقوم غالبًا بتعديل this، وإضافة خاصيات إليه.
  3. تُرجَع قيمة this. بمعنى آخر، تقوم new User(...)‎ بشيء يشبه ما يلي:
function User(name) {
// this = {}; (implicitly)
// this إضافة خاصيات إلى
this.name = name;
this.isAdmin = false;
// this; (implicitly) إرجاع
}

إذًا، تُعطي let user = new User("Jack")‎ النتيجة التالية ذاتها:

let user = {
name: "Jack",

isAdmin: false
};

الآن، إن أردنا إنشاء مستخدمين آخرين، يمكننا استدعاء new User("Ann")‎، و new User("Alice‎")‎ وهكذا. تعدُّ هذه الطريقة في بناء الكائنات أقصر من الطريقة الاعتيادية عبر الأقواس فقط، وأيضًا أسهل للقراءة. هذا هو الغرض الرئيس للبانيات، وهي تطبيق شيفرة قابلة لإعادة الاستخدام لإنشاء الكائنات. لاحظ أنَّه يمكن استخدام أي دالة لتكون دالة بانية تقنيًا. يعني أنه يمكن تنفيذ أي دالة مع new، وستُنَفَّذ باستخدام الخوارزمية أعلاه. استخدام الأحرف الكبيرة في البداية هو اتفاق شائع لتمييز الدالة البانية من غيرها وأنَّه يجب استدعاؤها مع new.

**new function() { … }‎**

إن كان لدينا العديد من الأسطر البرمجية، وجميعها عن إنشاء كائن واحد مُعَقَّد، فبإمكاننا تضمينها في دالة بانية، هكذا:

let user = new function() {
this.name = "John";
this.isAdmin = false;
// ...شيفرة إضافية لإنشاء مستخدم
// ربما منطق معقد أو أي جمل
// متغيرات محلية وهكذا..
};

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

وضع اختبار الباني: new.target

ميزة متقدمة: تُستخدم الصيغة في هذا الجزء نادرًا، ويمكنك تخطيها إلا إن كنت تُريد الإلمام بكل شيء. يمكننا فحص ما إن كانت الدالة قد استدعيت باستخدام new أو دونه من داخل الدالة، وذلك باستخدام الخاصية الخاصة new.target. تكون الخاصية فارغة في الاستدعاءات العادية، وتساوي الدالة البانية إذا استُدعِيَت باستخدام new:

function User() {
alert(new.target);
}
// "new" بدون:
User(); // undefined
// باستخدام "new":
new User(); // function User { ... }

يمكن استخدام ذلك بداخل الدالة لمعرفة إن استُدعِيَت مع new، “في وضع بناء كائن”، أو دونه “في الوضع العادي”. يمكننا أيضًا جعل كلًا من الاستدعاء العادي وnew ينفِّذان الأمر ذاته -بناء كائن- هكذا:

function User(name) {
if (!new.target) { // new إن كنت تعمل بدون
return new User(name); // new ...سأضيف
}
this.name = name;
}
let john = User("John"); // new User تُوَجِّه الاستدعاء إلى
alert(john.name); // John

يستخدم هذا الأسلوب في بعض المكتبات أحيانًا لجعل الصيغة أكثر مرونة حتى يتمكن الأشخاص من استدعاء الدالة مع new أو دونه، وتظل تعمل. ربما ليس من الجيد استخدام ذلك في كل مكان، لأن حذف new يجعل ما يحدث أقل وضوحًا. لكن مع new، يعلم الجميع أن كائنًا جديدًا قد أُنشِئ.

ما تُرجِعه الدوال البانية

لا تملك الدوال البانية عادةً التعليمة return. فَمُهِمَتُهَا هي كتابة الأمور المهمة إلى this، وتصبح تلقائيًا هي النتيجة. لكن إن كان هناك التعليمة return فإن القاعدة سهلة:

  • إن استُدعِيَت return مع كائن، يُرجَع الكائن بدلًا من this.
  • إن استُدعِيَت return مع متغير أولي، يُتَجاهَل. بمعنىً آخر، return مع كائن يُرجَع الكائن، وفي الحالات الأخرى تُرجَع this. مثلًا، يعاد في المثال التالي الكائن المرفق بعد return ويهمل الكائن المسنَد إلى this:
function BigUser() {
this.name = "John";
return { name: "Godzilla" }; // <-- تُرجِع هذا الكائن
}
alert( new BigUser().name ); // Godzilla, حصلنا على الكائن

وهنا مثال على استعمال return فارغة (أو يمكننا وضع متغير أولي بعدها، لا فرق):

function SmallUser() {
this.name = "John";
return; // ← this تُرجِع
}
alert( new SmallUser().name ); // John

لا تحتوي الدوال البانية غالبًا على تعليمة الإعادة return. نذكر هنا هذا التصرف الخاص عند إرجاع الكائنات بغرض شمول جميع النواحي.

حذف الأقواس

بالمناسبة، يمكننا حذف أقواس new في حال غياب المعاملات مُعامِلات:

let user = new User; // <-- لا يوجد أقوس
// الغرض ذاته
let user = new User();

لا يُعد حذف الأقواس أسلوبًا جيدَا، لكن الصيغة مسموح بها من خلال المواصفات.

الدوال في الباني

استخدام الدوال البانية لإنشاء الكائنات يُعطي مرونة كبيرة. قد تحتوي الدالة البانية على مُعامِلات ترشد في بناء الكائن ووضعه، إذ يمكننا إضافة خاصيات ودوال إلى this بالطبع. مثلًا، تُنشِئ new User(name)‎ في الأسفل كائنًا بالاسم المُعطَى name والدالة sayHi:

function User(name) {
this.name = name;
this.sayHi = function() {
alert( "My name is: " + this.name );
};
}
let john = new User("John");
john.sayHi(); // My name is: John
/*
john = {
name: "John",
sayHi: function() { ... }
}
*/

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

الخلاصة

  • الدوال البانية، أو باختصار البانيات، هي دوال عادية، لكن يوجد اتفاق متعارف عليه ببدء اسمها بحرف كبير.
  • يجب استدعاء الدوال البانية باستخدام new فقط. يتضمن هذا الاستدعاء إنشاء كائن فارغ وإسناده إلى this وبدء العملية ثم إرجاع هذا الكائن في نهاية المطاف.
  • يمكننا استخدام الدوال البانية لإنشاء كائنات متعددة متشابهة.
  • تزود JavaScript دوالًا بانية للعديد من الأنواع (الكائنات) المدمجة في اللغة: مثل النوع Date للتواريخ، و Set للمجموعات وغيرها من الكائنات التي نخطط لدراستها.

عودة قريبة

غطينا الأساسيات فقط عن الكائنات وبانياتها في هذا الفصل. هذه الأساسيات مهمة تمهيدًا لتعلم المزيد عن أنواع البيانات والدوال في الفصل التالي. بعد تعلم ذلك، سنعود للكائنات ونغطيها بعمق في فصل الخصائص، والوراثة، والأصناف.

تمارين

دالتين – كائن واحد

الأهمية: 2 هل يمكن إنشاء الدالة A و B هكذا new A()==new B()‎؟

function A() { ... }
function B() { ... }
let a = new A;
let b = new B;
alert( a == b ); // true

إن كان ممكنًا، وضح ذلك بمثال برمجي. الحل نعم يمكن ذلك. إن كان هناك دالة تُرجِع كائنًا فإن new تُرجِعه بدلًا من this. لذا فمن الممكن، مثلًا، إرجاع الكائن المعرف خارجيًا obj:

let obj = {};
function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true

إنشاء حاسبة جديدة

الأهمية: 5 إنشِئ دالة بانية باسم Calculator تنشئ كائنًا بثلاث دوال:

  • read()‎ تطلب قيمتين باستخدام سطر الأوامر وتحفظها في خاصيات الكائن.
  • sum()‎ تُرجِع مجموع الخاصيتين.
  • mul()‎ تُرجِع حاصل ضرب الخاصيتين. مثلًا:
let calculator = new Calculator();
calculator.read();
alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

الحل

function Calculator() {
this.read = function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
};
this.sum = function() {
return this.a + this.b;
};
this.mul = function() {
return this.a * this.b;
};
}
let calculator = new Calculator();
calculator.read();
alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

إنشاء مجمِّع

الأهمية: 5 أنشِئ دالة بانية باسم Accumulator(startingValue)‎، إذ يجب أن يتصف هذا الكائن بأنَّه:

  • يخزن القيمة الحالية في الخاصية value. تُعَيَّن قيمة البدء عبر المعامل startingValue المعطى من الدالة البانية.
  • يجب أن تستخدم الدالة read() الدالة prompt لقراءة رقم جديد وإضافته إلى value. بمعنى آخر، الخاصية value هي مجموع القيم المدخلة بواسطة المستخدم بالإضافة إلى القيمة الأولية startingValue. هنا مثال على ما يجب أن يُنَفَّذ:
let accumulator = new Accumulator(1); // القيمة الأولية 1
accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم
accumulator.read(); // يضيف قيمة مدخلة بواسطة المستخدم
alert(accumulator.value); // يعرض مجموع القيم

الحل

function Accumulator(startingValue) {
this.value = startingValue;
this.read = function() {
this.value += +prompt('How much to add', 0);
};
}
let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

ترجمة -وبتصرف- للفصل Constructor, operator “new” من كتاب The JavaScript Language

مهمه

الأهمية: 2

Is it possible to create functions A and B such as new A()==new B()?

function A() { ... }
function B() { ... }

let a = new A;
let b = new B;

alert( a == b ); // true

If it is, then provide an example of their code.

Yes, it’s possible.

If a function returns an object then new returns it instead of this.

So they can, for instance, return the same externally defined object obj:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
الأهمية: 5

Create a constructor function Calculator that creates objects with 3 methods:

  • read() asks for two values using prompt and remembers them in object properties.
  • sum() returns the sum of these properties.
  • mul() returns the multiplication product of these properties.

For instance:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

قم بتشغيل العرض التوضيحي

افتح sandbox بالإختبارات.

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

افتح الحل الإختبارات في sandbox.

الأهمية: 5

Create a constructor function Accumulator(startingValue).

Object that it creates should:

  • Store the “current value” in the property value. The starting value is set to the argument of the constructor startingValue.
  • The read() method should use prompt to read a new number and add it to value.

In other words, the value property is the sum of all user-entered values with the initial value startingValue.

Here’s the demo of the code:

let accumulator = new Accumulator(1); // initial value 1

accumulator.read(); // adds the user-entered value
accumulator.read(); // adds the user-entered value

alert(accumulator.value); // shows the sum of these values

قم بتشغيل العرض التوضيحي

افتح sandbox بالإختبارات.

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

افتح الحل الإختبارات في sandbox.

التسلسل الاختياري (غير الإجباري) '.?'

إضافة حديثة
هذه إضافة حديثة إلى اللغه. المتصفحات القديمه ربما تتطلب polyfills.

التسلسل الاختياري .? هو طريقة لتجنب الأخطاء التي تهدف للوصول إلى خصائص أو حقول غرض (كائن) ما، حتى إذا لم تكن الخصائص الوسيطة موجودة.

المش كلة

إذا كنت قد بدأت للتو في قراءة هذه البرنامج التعليمي الخاصّ بـِ JavaScript، فربما لم تواجه هذه المشكلة من قبل ولكنها شائعة جداً.

فعلى سبيل المثال، يمتلك بعض مستخدمين موقعنا عناويناً، ولكن بعضاً من هؤلاء المستخدمين لم يقوموا بحفظها ضمن ملفاتهم الشخصية. بالتالي لا يمكننا كتابة التعبير التالي من دون حدوث خطأ user.address.street وذلك من أجل عرض اسم الشارع الخاص بالعنوان:

let user = {}; // قد لا يملك المستخدم عنواناً، كهذا الغرض على سبيل المثال

alert(user.address.street); // وبالتالي يحدث الخطأ عند محاولة الوصول للخواص أو الحقول ضمنه

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

// يحدث الخطأ إذا كانت نتيجة التابع أو الطريقة querySelector(...) هي null
let html = document.querySelector('.my-element').innerHTML;

قبل ظهور التركيب .? في اللغة، كنا نستخدم العامل && لحل هذه المشكلة.

على سبيل المثال:

let user = {}; // غرض لمستخدم لا يملك عنوان

alert( user && user.address && user.address.street ); // يظهر لنا undefined من دون حدوث خطأ

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

التسلسل الاختياري (غير الإجباري)

يؤدي التسلسل الاختياري .? إلى إيقاف تقييم الكود البرمجي وإرجاع undefined إذا كانت قيمة الجزء الموجود قبل (أيسر) التركيب .? هي null أو undfined.

وللإيجاز، سنقول ضمن هذه المقالة أن شيئاً ما “موجود” إذا لم تكن قيمته null ولم تكن undefined كذلك.

وأما الطريقة الآمنة للوصول لـِ user.address.street هي:

let user = {}; // غرض المستخدم التالي لا يملك خاصية العنوان

alert( user?.address?.street ); // سيظهر لنا بدون حدوث خطأ undefined

وبالتالي سيبقى التعبير الخاص بقراءة عنوان المستخدم user?.address يعمل حتى لو لم يكن غرض المستخدم موجوداً بالأصل:

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

يرجى ملاحظة أن التركيب أو الشقّ .? يعمل بشكل صحيح حيث يتم وضعه بالضبط، وليس بعد ذلك المكان الذي تم وضعه فيه.

في السطرين الأخيرين، سيتوقف تقييم الكود البرمجي بشكل فوري بعد الشقّ .?user ولا يستمر أبداً للخصائص التي تليه.

يقوم التسلسل الاختياري فقط باختبار القيم null/undefined، ولا يتداخل مع ميكانيكية أي من اللغات الأخرى.

ولكن إذا كان الغرض user موجوداً بالفعل، فيجب أن تكون الخصائص الوسيطة موجودة ونقصد بالخصائص الوسيطة user.address مثلاً.

لا تفرط في استخدام تركيب التسلسل الاختياري

يجب أن نستخدم التركيب .? فقط عندما يكون هناك غرض، كائن أو خاصية غير موجودة بالأصل.

فعلى سبيل المثال، ووفقاً للمنطق المتعلق بالأمثلة السابقة، يجب أن يكون غرض المستخدم user موجوداً بالأصل، ولكن الخاصية address هي اختيارية وقد لا تكون موجودة، وبالتالي التعبير user.address?.street سيكون أفضل من إضافة التركيب .? للتحقق من كل خاصية أو حقل تابع للغرض user?.address?.street.

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

المتحول الواقع قبل التركيب .? يجب أن يكون معرّفاً

إذا لم يتمّ تعريف المتحول user، سيؤدي التعبير user?.anything إلى حصول خطأ:

// ReferenceError: user is not defined
user?.address;

اختصار الطرق (Short-circuiting)

كما تمّ ذكره آنفاً، يقوم التركيب .? بإيقاف عملية تقييم الكود البرمجي (يختصر الطريق) إذا لم يكن القسم اليساري (على يسار التركيب) موجوداً.

لذلك، وإذا كان هنالك أي استدعاءات لتوابع، لا يتم استدعائها أو تنفيذها:

let user = null;
let x = 0;

user?.sayHi(x++); // لا يحدث شيء

alert(x); // لا يتم زيادة القيمة 0

حالات أخرى ().?، []. ?

لا يقتصر التسلسل الاختياري .? في عمله على المتحولات فقط فهو ليس بعامل (رياضي) كالجمع والطرح، بل هو تركيب بنيوي يعمل أيضاً على التوابع والأقواس المربعة (أقواس المصفوفات).

على سبيل المثال، يمكن استخدام التركيب ().? لاستدعاء تابع قد لا يكون معرّفاً بالأصل.

في المثال أدناه، يمتلك بعض أغراض المستخدمين الطريقة (method) أو التابع المُسمى admin وبعضهم الآخر لا يمتلك:

let user1 = {
  admin() {
    alert("I am admin");
  }
}

let user2 = {};

user1.admin?.(); // I am admin
user2.admin?.();

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

وبالتالي سيقوم التعبير ().? بفحص الجزء اليساري، فإذا كان التابع admin معرّفاً، فيعمل التعبير (من أجل user1)، وإلا (من أجل user2) فستتوقف عملية التقييم من دون حدوث أخطاء.

في حال الرغبة باستخدام الأقواس المربّعة [] بدلاً من النقطة . للوصول للخواص ضمن غرض أو كائن ما، سيفي التعبير [].? بالغرض أيضاً. وبشكل مشابه للحالات السابقة، يسمح هذا التعبير بشكل آمن قراءةَ خاصية أو حقل ضمن غرض معيّن قد لا يكون موجوداً.

let user1 = {
  firstName: "John"
};

let user2 = null; // على سبيل المثال، لم يكن بإمكاننا المصادقة على وجود مستخدم ما (القيام بعملية تسجيل الدخول له)

let key = "firstName";

alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined

alert( user1?.[key]?.something?.not?.existing); // undefined

كذلك يمكننا استخدام التركيب .? مع التعبير delete:

delete user?.name; // سيقوم بحذف اسم المستخدم في حال كان غرض المستخدم موجوداً
يمكننا استخدام التركيب .? للقراءة والحذف الآمن (بدون وقوع أخطاء)، ولكن ليس مع الكتابة (الإسناد)

لا يمكن استخدام تركيب التسلسل الاختياري .? في الطرف اليساري لعملية الإسناد:

// فإذا حاولنا إسناد قيمة معينة لاسم المستخدم إذا كان المستخدم موجوداً

user?.name = "John"; // فسيحدث خطأ، لأن هذه الطريقة لا تعمل
// لأنه سيتم تقييمها على أن
// undefined = "John"

ملخص الدرس

لتركيب التسلسل الاختياري .? ثلاثة أشكال:

  1. obj?.prop – يردّ التعبير obj.prop قيمةً صحيحة إذا كان الغرض obj موجوداً، وإلا يُعيد undefined.
  2. obj?.[prop] – يردّ التعبير obj[prop] قيمةً صحيحة إذا كان الغرض obj موجوداً، وإلا يُعيد undefined.
  3. ()obj?.method – يقوم باستدعاء ()obj.method إذا كان الغرض obj موجوداً، وإلا يُعيد undefined.

وكما نرى، جميع الطرق السابقة واضحة وسهلة الاستخدام. فالتركيب .? يتحقق من الجزء الأيسر فيما إذا لم يكن null/undefined ليسمح بعدها بإكمال عملية التقييم.

وإذا كان لدينا خصائص متداخلة فيما بينها، فيسمح تسلسل من التركيب .? بقرائتها بشكلٍ آمن.

ومع ذلك، يجب علينا استخدام التركيب .? بعناية (عدم الإفراط) وذلك فقط في حالة عدم تأكدنا من وجود الجزء اليساري للتركيب.

حتى لا يخفي أخطاء البرمجة التي نرتكبها أحياناً، وذلك في حال حدثت.

الرمز (Symbol type)

كما ذُكر فى المصدر, فإن صفات الكائنات (Object properties keys) يمكن أن تكون نصًا (string) أو رمزًا (symbol) . ليست رقما أو قيمه منطقيه (boolean) وإنما عباره عن نصوص أو رموز, فقط هذين النوعين.

لقد استخدمنا حتى الآن النص فقط. فهيا نرى الفوائد التى يمكن أن توفرها لنا الرموز.

الرموز

كلمة رمز فى الإنجليزيه تعنى معَرٌِف فريد من نوعه أى لا شئ مماثل له Unique Identifier.

يمكن إنشاء قيمه من نوع الرمز باستخدام الداله Symbol():

// id is a new symbol
let id = Symbol();

عند الإنشاء, يمكننا إعطاء الرمز وصفًا (ويمكن تسميته أيضا إسم الرمز), وهذا مفيد غالبا فى البحث عن الأخطاء وحلها.

// id is a symbol with the description "id"
let id = Symbol("id");

إن الرموز مضمون بتفرُّدها. حتى فى حالة إنشاء عدة رموز بنفس الوصف, ولكنهم مختلفين فى القيمه. فالوصف مجرد وَسْم لا يؤثر على أى شيء.

على سبيل المثال, هذان الرمزان لهما نفس الوصف – ولكنهما غير متساويين:

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

إذا كنت تعرف لغة برمجه مثل (Ruby) أو أى لغة برمجة أخرى لديها شئ قريب من الرموز فلا تحتار

الرموز لا تتحول إلى نص تلقائياً

أغلب القيم فى الجافاسكريبت يمكن تحويلها ضمنيًا إلى نص (string). على سبيل المثال, يمكننا أن نعرض أى قيمه فى دالة التنبيه (alert()), وستعمل, ولكن الرموز (Symbols) لها طابع خاص. فلا تسرى عليهم القاعده نفسها.

على سبيل المثال, هذا الكود سيؤدى إلى ظهور خطأ:

let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string

هذا لمنع الأخطاء غير المقصوده, لأن النصوص والرموز مختلفين تمام ولا يجب أن يتم تغيير واحد إلى الآخر عن طريق الخطأ.

إذا كنا نريد أن نعرض الرمز كما هو, فإننا نحتاج إلى أن نستدعى الداله .toString() مع هذا الرمز, كالمثال أدناه:

let id = Symbol("id");
alert(id.toString()); // Symbol(id), now it works

أو نستدعى الخاصيه symbol.description لعرض الوصف فقط:

let id = Symbol("id");
alert(id.description); // id

الخصائص المخفيه

باستخدام الرموز يمكننا أن ننشئ خصائص مخفيه لكائن ما (object) حيث لا يمكن لأى جزء آخر فى الكود أن يصل إليها ولا أن يعدل قيمتها عن طريق الخطأ.

على سبيل المثال, إذا كنا نعمل على كائن user والذى لا ينتمي إلى هذا الكود ولكننا نريد أن نضيف بعض الخصائص إليه.

هيا نستخدم رمزا من أجل ذلك:

let user = {
  // belongs to another code
  name: "John",
};

let id = Symbol("id");

user[id] = 1;

alert(user[id]); // we can access the data using the symbol as the key

إذا ماهى فائدة استخدام Symbol("id") بدلا من النص "id" ؟

حيث أن الكائن user ينتمي لكود خارجي وهذا الكود يعمل جيدا, إذا فلا يصح أن نضيف أى خاصيه لهذا الكائن. ولكن الرمز لا يمكن الوصول إليه عن طريق الخطأ مثل النص حيث أن الكود الخارجى لا يمكن أن يراه من الأساس, ولذلك هذه الطريقه تُعد صحيحه.

تخيل أيضا لو أن هناك برنامج (script) آخر يريد أن يضيف خاصية بداخل الكائن user لأغراضه الخاصه, هذا البرنامج الآخر يمكنه أن يكون مكتبه مبنية بالجافاسكريبت ولذلك فإن هذه البرامج لا تعرف شيئا عن بعضها البعض.

لذلك هذا البرنامج يمكنه أن ينشئ Symbol("id") الخاص به كالآتي:

// ...
let id = Symbol("id");

user[id] = "Their id value";

لن يكون هناك أى تعارض بين الخصائص وبعضها لأن الرموز دائما مختلفه ولا يمكن أن تتساوى حتى إن كان لهم نفس الإسم.

…ولكن ماذا لو استخدمنا نصًا بدلًا من رمز لنفس الغرض, فسيكون هناك تعارض:

let user = { name: "John" };

// Our script uses "id" property
user.id = "Our id value";

// ...Another script also wants "id" for its purposes...

user.id = "Their id value";
// Boom! overwritten by another script!

إستخدام الرموز بداخل الكائنات (objects)

إذا كنا نريد أن نضع رمزا بداخل كائن كخاصيه, فإننا نحتاج أن نضع حول الرمز أقواس مربعه []

كالمثال الآتى:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // not "id: 123"
};

هذا لأننا نريد قيمة المتغير id كإسم للخاصيه ولا نريد النص “id”.

الرموز يتم تخطيها فى التكرار for … in

الخصائص من نوع الرمز لا تشارك فى التكرار for .. in.

على سبيل المثال:

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name, age (no symbols)

// the direct access by the symbol works
alert( "Direct: " + user[id] );

وأيضا يتم تجاهل الخصائص من نوع الرمز عند استخدام Object.keys(user). لأن هذا جزء من المبدأ العام “إخفاء الخصائص الرمزيه” “hiding symbolic properties”. وبالمثل إذا كان هناك أى برنامج آخر يقوم أو مكتبه تقوم بالتكرار على الخصائص فى هذا الكائن فإنها لن تستطيع أن تصل إلى الخاصيه من نوع الرمز.

على النقيض تماما فإن Object.assign تقوم بنسخ خصائص الكائن كلها سواءًا النصيه أو الرمزيه.

let id = Symbol("id");
let user = {
  [id]: 123,
};

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

alert(clone[id]); // 123

هذا ليس تناقضا. هذا موجود فى تصميم اللغة نفسها. فالفكره وراء هذا هى أنه عندما نريد أن ننسخ كائنا ما فإننا نريد أن ننسخ كل الخصائص بما فيها من خصائص رمزيه.

الرموز العامه

كما قلنا سابقًا, فإن الرموز عادةً ما تكون مختلفة حتى وإن كان لها نفس الوصف. ولكن فى بعض الأوقات أن نصل إلى هذا الرمز بالتحديد. على سبيل المثال, إذا كان هناك أجزاء مختلفه من البرنامج الخاص بنا تريد أن تصل إلى الرمز "id" تحديدا.

ليتم تنفيذ ذلك, فإن هناك مايسمى مكان تسجيل الرموز العامه global symbol registry. حيث يمكننا أن ننشئ رمزًا ومن خلال هذا المكان نستطيع أن نصل إليه فى وقت لاحق, ومكان تسجيل الرموز يضمن لنا أن تكرار محاولات الوصول إلى الرمز بنفس الإسم سيقوم بإرجاع نفس هذا الرمز فى كل محاوله.

لقراءة (أو يمكن أن نقول إنشاء رمز فى حالة عدم وجوده) فى مكان التسجيل, استخدم Symbol.for(key).

هذه الداله ترى إن كان هناك رمز فى مكان التسجيل تم وصفه باستخدام key ستقوم بإرجاعها. أما غير ذلك فستقوم بإنشاء رمز جديد Symbol(key) وتخزينه فى مكان التسجيل باستخدام الوصف key.

على سبيل المثال:

// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
alert(id === idAgain); // true

الرموز بداخل مكان التسجيل تسمي رموزًا عامه. فإذا كنا نريد رمزا متاحا فى البرنامج بأكمله ويمكن الوصول إليه من أى مكان – فهذا هو سبب وجودها.

هذا يبدو مثل لغة البرمجه Ruby

فى بعض لغات البرمجه مثل Ruby فإن هناك رمزًا لكل إسم.

فى الجافاسكريبت كما نرى فإن هذا صحيح بالنسبة إلى الرموز العامه.

Symbol.keyFor

بالنسبة إلى الرموز العامه فإنه لايوجد Symbol.for(key) التى تقوم بإرجاع الرمز باستخدام الإسم فقط، ولكن يوجد أيضا العكس Symbol.keyFor(sym) الذى يقوم بإرجاع الإسم باستخدام الرمز العام.

على سبيل المثال:

// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// get name by symbol
alert(Symbol.keyFor(sym)); // name
alert(Symbol.keyFor(sym2)); // id

الداله Symbol.keyFor عندما تعمل تقوم باستخدام مكان تسجيل الرموز العام للبحث عن إسم للرمز, ولذلك فإنها لا تعمل إلا مع الرموز العامه. فإذا كان الرمز غير عام فلن تستطيع إيجاده وستقوم بإرجاع undefined.

يقال بأن كل رمز يملك الخاصيه description.

على سبيل المثال:

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert(Symbol.keyFor(globalSymbol)); // name, global symbol
alert(Symbol.keyFor(localSymbol)); // undefined, not global

alert(localSymbol.description); // name

الرموز الموجودة تلقائيا

هناك رموز كثيرة تستخدمها الجافاسكريبت داخليًا ويمكننا استخدامها لأغراض معينه فى الكائنات الخاصه بنا.

هذه الرموز موجوده فى المصدر فى جدول Well-known symbols:

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …وغيرها.

على سبيل المثال، Symbol.toPrimitive تمكننا أن نحول الكائن إلى قيمه فرديه (primitive). وسنرى هذا فى القريب العاجل.

الرموز الأخرى أيضا ستكون مألوفه عندما ندرس استخداماتها فى مواضيع أخرى.

ملخص الدرس

الرمز هو قيمه فرديه لخصائص فريده من نوعها (unique identifiers).

يتم إنشاء الرموز باستخدام الداله Symbol() ويمكن إضافه وصف بشكل اختيارى.

الرموز دائما ما تكون قيَمها مختلفه حتى وإن كانت بنفس الإسم. وإذا كنا نريد أن نصل إلى رمز بعينه فيجب علينا استخدام مكان التسجيل العام global registry: Symbol.for(key) تقوم بإرجاع (أو إنشاء فى حالة عدم وجوده) رمزًا عامًا حيث jey هو الإسم. والإستدعاء المتكرر للداله Symbol.for بنفس الإسم key ستقوم دائما بإرجاع نفس الرمز.

يتم استخدام الرموز فى حالتين:

  1. خصائص الكائن المخفيه.

إذا كنا نريد أن نضيف خاصيه إلى كائن لا ينتمى إلى هذا الكود بل إلى برنامج آخر أو مكتبه، فعندئذ يمكننا إنشاء رمز واستخدامه كخاصيه. والخاصيه من نوع الرمز لا تظهر فى التكرار for .. in, ولذلك لا يمكن الوصول إلى الخاصيه عن طريق الخطأ أو أن تتعارض مع أى خاصية أخرى وذلك لأن البرنامج الآخر لا يملك الرمز الخاص بنا. وبالتالى ستظل الخاصيه محميه من الوصول إليها أو التعديل عليها.

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

  1. هناك الكثير من الرموز الموجوده بالفعل فى الجافاسكريبت والتى يمكن الوصول إليها عن طريق Symbol.*. ويمكننا استخدامهم لتغيير بعض السلوك الموجود بالفعل فى اللغه. على سبيل المثال فإننا فى موضوع مقبل سنستخدم Symbol.iterator من أجل التكراريات iterables وغيرها.

عمليًا، فإن الرموز لا تكون مخفية بالكامل. ولكن هناك داله موجوده تسمى Object.getOwnPropertySymbols(obj) والتى تمكننا من الوصول إلى كل الرموز. ,توجد أيضًا دالة تسمي Reflect.ownKeys(obj) والتى تقوم بإرجاع كل الخصائص بداخل كائن معين بما فيها الخصائص التى من نوع الرمز. ولذلك فإن هذه الخصائص ليست مخفية بالكامل. ولكن أعلب المكتبات والدوال لا تستخدم هذه الوسائل.

تحويل الكائنات إلى قيم مفرده

ماذا يحدث فى حالة جمع كائنين obj1 + obj2، أو طرحهما obj1 - obj2 أو طباعتهما باستخدام دالة التنبيه alert(obj) ؟

فى هذه الحالة، تتحول الكائنات إلى قيم فردية تلقائيًا، ثم يتم تنفيذ هذه العملية الحسابية.

فى قسم (تحويل الأنواع) رأينا كيف يمكن تحويل النصوص (strings) والأرقام والقيَم المنطقيه (booleans) إلى قيم فردية. ولكننا تركنا مساحة فارغة من أجل الكائنات. والآن بعد أن عرفنا الكثير عن الدوال (methods) والرموز (symbols)، أصبح الآن ممكنًا أن نملأ هذه المساحه.

  1. كل الكائنات عند تحويلها إلى قيمه منطقيه (boolean) فإن قيمتها تساوى true. وبالتالى فإن التحويلات المتاحة هي التحويل إلى نص أو رقم.

  2. يحدث التحويل إلى رقم عند طرح كائنين أو استخدام دالة حسابية. على سبيل المثال، الكائنات من نوع Date (سيتم شرحها فى قسم التاريخ) يمكن طرحها، ونتيجة طرح date1 - date2 هي الفرق بين التاريخين.

  3. وبالنسبه إلى التحويل إلى نص – فإنه يحدث عادة عند طباعة الكائن باستخدام دالة التنبيه alert(obj) والدوال المشابهة.

ToPrimitive

يمكننا التحكم فى التحويل إلى نص أو رقم، باستخدام بعض دوال الكائنات.

هناك ثلاث ملاحظات مختلفه على تحويل الأنواع ويطلق عليها “hints” وتم ذكرها فى المصدر:

"النص"

يحدث التحويل إلى نص عندما نقوم بعملية معينه على كائن تتوقع نصًا لا كائنًا مثل دالة التنبيه alert:

// output
alert(obj);

// using object as a property key
anotherObj[obj] = 123;
"الرقم"

يحدث التحويل إلى رقم عندما نقوم يعملية حسابيه على سبيل المثال:

// explicit conversion
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// less/greater comparison
let greater = user1 > user2;
"التصرف الإفتراضي"

يحدث فى حالات نادرة عندما تكون العمليه الحسابيه غير متأكَّد من النوع المناسب معها.

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

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

// binary plus uses the "default" hint
let total = obj1 + obj2;

// obj == number uses the "default" hint
if (user == 1) { ... };

المقارنه باستخدام علامات الأكبر من أو الأصغر من مثل < >، يمكنها التعامل مع الأرقام والنصوص أيضا ولكنها مع ذلك تستخدم التحويل إلى رقم وليس الطريقه الافتراضيه، وهذا لأسباب متأصله historical reasons.

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

لا يوجد التحويل إلى "القيم المنطقيه"

لاحظ أن هناك ثلاث طرق (أو ملاحظات) فقط بكل بساطه.

لا توجد طريقة التحويل إلى “قيمه منطقيه” (لأن كل الكائنات قيمتها true عن تحويلها إلى قيمه منطقيه). وإذا تعاملنا مع "الطريقه الإفتراضيه" و "الرقم" بطريقة مشابهة مثل كل الطرق الموجوده فسيكون هناك طريقتين فقط.

عند القيام بالتحويل، تقوم الجافاسكريبت باستدعاء ثلاث دوال:

  1. استدعاء obj[Symbol.toPrimitive](hint) – وهو رمز موجود بالفعل (built-in)، وهذا فى حالة وجود هذه الدالة.
  2. فى حالة عدم وجودها وطانت الطريقه هى التحويل إلى نص
    • استخدام obj.toString() و obj.valueOf()، أيهم موجود.
  3. غير ذلك، إذا كانت الطريقه هي "الطريقه الإفتراضيه" أو "الرقم"
    • استخدام obj.valueOf() و obj.toString()، أيهم موجود.

Symbol.toPrimitive

لنبدأ بأول طريقه. يوجد رمز (symbol) موجود بالفعل يسمى Symbol.toPrimitive والذي يجب استخدامه لتسمية طريقة التحويل كالآتى:

obj[Symbol.toPrimitive] = function(hint) {
  // must return a primitive value
  // hint = one of "string", "number", "default"
};

على سبيل المثال, يطبق هذه الطريقه الكائن user:

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  },
};

// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

كما نرى من المثال، فإن الكائن user يتحول إلى نص معبر أو إلى كم النقود بناءًا على طريقة التحويل نفسها. فإن الطريقه user[Symbol.toPrimitive] تتعامل مع كل طرق التحويل.

toString/valueOf

الدوال toString و valueOfموجوده من قديم الأزل. إنهم ليسو رموزًا ولكنهم دوال تستعمل مع النصوص. ويقومون بتوفير طريقة قديمه للقيام بالتحويل.

إذا لم يكن هناك Symbol.toPrimitive فإن الجافاسكريبت تقوم بالبحث عنهمو استخدامهم بالترتيب الآتى:

  • toString -> valueOf فى الطريقه النصيه.
  • valueOf -> toString غير ذلك.

هذه الدوال لابد أن تقوم بإرجاع قيمه فردية. فإذا قامت هاتان الدالتان بإرجاع كائن فسيتم تجاهله.

أى كائن يمتلك افتراضيا الدالتين toString و valueOf:

  • الداله toString تقوم بإرجاع النص "[object Object]".
  • الداله valueOf تقوم بإرجاع الكائن نفسه.

كما فى المثال:

let user = { name: "John" };

alert(user); // [object Object]
alert(user.valueOf() === user); // true

لذلك إذا حاولنا أن نستخدم الكائن كنص، كما فى حالة استخدام الداله النصيه alert سنرى بشكل افتراضي [object object].

الداله valueOf تم ذكرها هنا فقط لإكمال المعلومات ولتجنب أى التباس. فكما ترى فإن هذه الداله تقوم بإرجاع الكائن نفسه وبالتالى يتم تجاهله. لا تسأل لماذا فهذا لأسباب متأصله historical reasons. ولذلك يمكننا اعتبار أنها غير موجوده.

هيا نقوم باستخدام هذه الدوال.

على سبيل المثال، فإن الكائن user هنا يقوم بنفس التصرف أعلاه عند استخدام خليط من toString و valueOf بدلًا من Symbol.toPrimitive:

let user = {
  name: "John",
  money: 1000,

  // for hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // for hint="number" or "default"
  valueOf() {
    return this.money;
  },
};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

كما نرى هنا فإن التصرف هو نفسه الموجود فى المثال السابق عند استخدام Symbol.toPrimitive.

ونحن عالبا مانحتاج إلى طريقه للتعامل مع كل حالات التحويل إلى قيم فرديه (primitive values). ففى هذه الحاله يمكننا استخدام toString فقط كالآتى:

let user = {
  name: "John",

  toString() {
    return this.name;
  },
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

فى حالة غياب Symbol.toPrimitive و valueOf فإن toString ستقوم بالتعامل مع كل حالات التحويل إلى قيم فرديه.

أنواع القيم المسترجعه

هناك شئ مهم يجب أن تعرفه وهو أن كل طرق التحويل إلى قيم مفرده لا يجب بالضروره أن تقوم بإرجاع نفس نوع القيمه المفرده المحوَّله إليه.

فلا يوجد ضمانه إذا كانت toString ستقوم بإرجاع نص بالتحديد أو حتى Symbol.toPrimitive ستقوم بإرجاع رقم فى طريقة "الرقم".

الأمر الوحيد الذى يمكن ضمانه والإلزامى هو أن هذه الدوال يجب أن تقوم بإرجاع يمة مفردة لا كائنًا.

ملاحظات متأصله

لأسباب قديمه historical reasons فإنه فى حالة أن الدوال toString or valueOf قامت بإرجاع كائن، فلا يوجد خطأ يظهر، بل يتم تجاه النتيجه فقط كأن شيئًا لم يكن. وذلك لأنه فى الماضي لم يكن هناك مفهوم جيد للخطأ فى الجافاسكريبت.

على النقيض، فإن Symbol.toPrimitive يجب أن تقوم بإرجاع قيمة مفرده، وإلا سيكون هناك خطأ.

التحويلات الإضافيه

كما نعرف بالفعل أن الكثير من العلامات والدوال تقوم بتحويل الأنواع, مثال على ذلك علامة * تقوم بتحويل العاملين إلى أرقام.

إذا استخدمنا كائنين كعملين رياضيين فسيكون هناك مرحلتين:

  1. تحويل إلى الكائن إلى قيمه مفردة.
  2. إذا كانت نتيجة التحويل ليست من النوع الصحيح فسيتم تحويلها.

على سبيل المثال:

let obj = {
  // toString handles all conversions in the absence of other methods
  toString() {
    return "2";
  },
};

alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
  1. عملية الضرب obj * 2 تقوم أولا بتحويل الكائن إلى قيمة مفرده (والذي هو النص "2").
  2. ثم بعد ذلك فإن الجمله "2" * 2 تتحول إلى 2 * 2 (يتحول النص إلى رقم).

علامة الجمع + ستقوم بإضافة النصوص فى نفس هذا الموقف لأنها تعمل مع النصوص:

let obj = {
  toString() {
    return "2";
  },
};

alert(obj + 2); // 22 ("2" + 2), conversion to primitive returned a string => concatenation

ملخص الدرس

التحويل من كائن إلى قيمة مفردة يحدث تلقائيا عن طريق الكثير من الدوال الموجوده بالفعل والعمليات التى تُجرى والتى تعمل فقط على قيم مفردة وليس كائنات.

هناك 3 أنواع من طرق التحويل:

  • "النص" (ويحدث ذلك عند استخدام دالة التنبيه alert والتى تتوقع نصًا).
  • "الرقم" (فى العمليات الحسابيه).
  • "الطريقة الإفتراضيه" (فى بعض العمليات).

يوضح المصدر أى عملية تستخدم أى طريقه. وهناك القليل من العمليات التي “لا تعلم ما نوع العامل الذي ستستقبله” وتستخدم "الطريقه الإفتراضيه". وعادةً ما يتم استخدام "الطريقة الإفتراضيه" مع الكائنات الموجوده بالفعل كما يتم التعامل مع "الأرقام", ولذلك عمليا فإن الطريقتين الأخيرتين يمكن ضمهما معًا.

تتم طريقة التحويل كالآتى:

  1. استدعاء الداله obj[Symbol.toPrimitive](hint) فى حالة وجودها,
  2. غير ذلك إذا كانت الظريقه "نصًا"
    • استخدام obj.toString() و obj.valueOf() فى حالة وجود أي منهم.
  3. غير ذلك إذا كانت الطريقة "رقمًا" أو "الطريقة الإفتراضيه"
    • استخدام obj.valueOf() أو obj.toString() فى حالة وجود أى منهم.

ويكفى عمليًا استخدام obj.toString() لكل التحويلات والتى تقوم بإرجاع قيمة يمكن قرائتها من أجل الطباعة أو البحث عن الأخطاء.