تُنشّأ الكائنات عادة لتُمَثِّل أشياء من العالم الحقيقي مثل المستخدمين، والطلبات، وغيرها:
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)
يسمى كتابة الشيفرة البرمجية باستخدام الكائنات للتعبير عن الاشياء «بالبرمجة الشيئية/كائنية» (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() { // مثل "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; // this لدى الاستدعائين قيمة مختلفة لـ // التي بداخل الدالة تعني المتغير الذي قبل النقطة "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(); // undefined - غير معرَّف
في هذه الحالة ستكون قيمة this
هي undefined
في الوضع الصارم. فإن حاولنا الوصول إلى this.name
سيكون هناك خطأ.
في الوضع غير الصارم، فإن قيمة this
في هذه الحالة ستكون المتغير العام (في المتصفح window
والتي سَنشرحها في فصل المتغيرات العامة). هذا السلوك زمني يستخدم إصلاحات الوضع الصارم "use strict"
.
يُعد هذا الاستدعاء خطأً برمجيًا غالبًا. فإن وًجِدت this
بداخل دالة، فمن المتوقع استدعاؤها من خلال كائن.
الأمور المترتبة على this
الغير محدودة النطاق
إن أتيت من لغة برمجية أخرى، فمن المتوقع أنك معتاد على "this
المحدودة" إذ يمكن لِلدوال المعرَّفة في الكائن استخدام this
التي ترجع للكائن.
تستخدم this
بحرية في JavaScript، وتُقَيَّم قيمتها أثناء التنفيذ ولا تعتمد على المكان حيث عُرِّفت فيه، بل على الكائن الذي قبل النقطة التي استدعت الدالة.
يوجد ايجابيات وسلبيات لمبدأ تقييم this
أثناء وقت التشغيل. فمن ناحية، يمكن إعادة استخدام الدالة مع عدة كائنات، ومن الناحية الأخرى، المرونة الأكثر تعطي فرصًا أكثر للخطأ. لسنا بصدد الحكم على تصميم اللغة ونعته بالجيد أم سيء، بل نحاول فهم طريقة عملها وكيفية الاستفادة من ميزاتها وتجنب الأخطاء.
ميزة داخلية: النوع المرجعي
يُغطي هذا الجزء ميزة متقدمة -من ميزات اللغة-لفهم أفضل لحالة معينة. إن كنت على عجلة من أمرك، يمكنك تخطي أو تأجيل هذا الجزء.
يمكن لاستدعاء الدالة المعقد أن يفقد this
، فمثلًا:
let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // John (يعمل الاستدعاء البسيط) // user.hi أو وفقًا للاسم user.bye الآن، لنستدعي (user.name == "John" ? user.hi : user.bye)(); // خطأ!
يوجد معامل شرطي في السطر الأخير والذي يختار إما user.hi
أو user.bye
. في هذه الحالة يتم اختيار user.hi
ثم يتم استدعاء الدالة مع الأقواس ()
. لكنها لا تعمل!
كما ترى، ينتج خطأ من الاستدعاء لأن قيمة "this"
بداخل الاستدعاء أصبحت undefined
.
ستعمل بهذه الطريقة (الكائن.الدالة):
user.hi();
هذه الصياغة لا تُعطي دالة:
(user.name == "John" ? user.hi : user.bye)(); // خطأ!
لماذا؟ إن أردنا فهم سبب حدوث ذلك، لِنكشف الغطاء عن كيفية عمل الاستدعاء obj.method()
. عند النظر عن قرب، يمكننا ملاحظة عمليتين في التعليمة obj.method()
:
1- أولا، النقطة '.'
تسترجع الخاصية obj.method
. 2- ثم الأقواس ()
تنفذ الدالة.
إذًا، كيف تُمرَّر المعلومات عن this
من الجزء الأول للثاني؟ إن وضعنا العمليتين في سطرين منفصلين، فَسنفقد this
بالتأكيد:
let user = { name: "John", hi() { alert(this.name); } } // فصل الحصول على الدالة واستدعائها في سطرين منفصلين let hi = user.hi; hi(); // غير مُعَرَّفَة this خطأ، لأن
تُسنِد التعليمة hi = user.hi
الدالة إلى المتغير، ثم، في السطر الأخير تصبح مستقلة، فلا يوجد this
هنا ضمن النطاق.
تستخدم JavaScript خدعة لجعل user.hi()
تعمل - صيغة النقطة '.'
لا تُرجِع دالة، بل قيمة من النوع المرجعي الخاص
النوع المرجعي هو "نوع للتخصيص". لا يمكننا استخدام هذا النوع بشكل واضح، بل يُسخدَم داخليًا بواسطة اللغة. تُشَكَّل قيمة النوع المرجعي من ثلاث قيم (base, name, strict)
، إذ:
-
base
هي الكائن. -
name
هو اسم الخاصية. -
strict
تساوي "true" إن كان الوضع الصارمuse strict
مُفعلًا.
النتيجة من الوصول إلى خاصية user.hi
ليست دالة، إنما قيمة من النوع المرجعي. بالنسبة لـ user.hi
في الوضع الصارم تكون:
// قيمة من النوع المرجعي (user, "hi", true)
عند استدعاء الأقواس ()
في النوع المرجعي فإنها تستقبل المعلومة كاملة عن الكائن والدلة، وتتمكن من تعيين this
بطريقة صحيحة (في هذه الحالة user
).
النوع المرجعي هو نوع "وسيط" داخلي، وغرضه هو تمرير المعلومات من الصيغة النُقطية .
إلى أقواس الاستدعاء ()
.
أي عملية أخرى مثل الإسناد hi = user.hi
تُلغي النوع المرجعي ككل، فهي تأخذ قيمة الدالة user.hi
وتُمررها. فَتفقد العمليات التالية this
. لذا، ونتيجة لذلك، تُمرَّر قيمة this
بالطريقة الصحيحة إن كانت الدالة مُستدعاه مباشرة باستخدام صيغة النقطة obj.method()
أو الأقواس المربعة obj['method']()
(يؤديان العمل ذاته). سنتعلم طرائق أخرى لحل هذه المشكلة لاحقًا، مثل استخدام func.bind().
الدوال السهمية لا تحوي "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
مستقلة، ونريد أخذها من السياق الخارجي بدلًا من ذلك. سَنتعمق في موضوع الدوال السهمية لاحقًا في مقال «نظرة تفصيلية على الدوال السهمية Arrow functions».
الخلاصة
- الدوال المخزنة في الكائنات تسمى «توابع» (methods).
-
تسمح هذه الكائنات باستدعائها بالشكل
object.doSomething()
. -
يمكن للدوال الوصول إلى الكائن المعرفة فيه (أو النسخة التي استدعته المشتقة منه) باستخدام الكلمة المفتاحية
this
. -
تُعَرَّف قيمة
this
أثناء التنفيذ. -
قد نستخدم
this
عند تعريف دالة، لكنها لا تملك أي قيمة حتى استدعاء الدالة. - يمكن نسخ دالة بين الكائنات.
-
عند استدعاء دالة بالصيغة
object.method()
، فإن قيمةthis
أثناء الاستدعاء هيobject
.
لاحظ أن الدوال السهمية مختلفة تتعامل تعاملًا مختلفًا مع this
إذ لا تملك قيمة لها. عند الوصول إلى this
بداخل دالة سهمية فإن قيمتها تؤخذ من النطاق الموجودة فيه.
تمارين
فحص الصياغة
الأهمية: 2
ما نتيجة هذه الشيفرة؟
let user = { name: "John", go: function() { alert(this.name) } } (user.go)()
ملاحظة: يوجد فخ
الحل
خطأ! جرب تشغيل الشيفرة:
let user = { name: "John", go: function() { alert(this.name) } } (user.go)() // خطأ!
لا تعطي مُعظم رسائل الخطأ في المتصفحات توضيح لسبب الخطأ.
سبب الخطأ هو فاصلة منقوطة مفقودة بعد user = {...}
.
لا تقوم JavaScript بوضع فاصلة منقوطة قبل القوس (user.go)()
. لذا فإنها تقرأ الشيفرة كالتالي:
let user = { go:... }(user.go)()
يمكننا أيضًا رؤية أن هذا التعبير المتداخل هو استدعاء للكائن { go: ... }
كدالة بالمعامل (user.go)
. ويحدث ذلك أيضًا في السطر نفسه مع let user
، لذا فإن الكائن user
لم يُعَرَّف بعد، وهكذا يظهر الخطأ.
إن وضعنا الفاصلة المنقوطة، سيصبح كل شيء صحيح:
let user = { name: "John", go: function() { alert(this.name) } }; (user.go)() // John
لاحظ أن الأقواس حول (user.go)
لا تعمل شيئًا هنا. فهي ترتب العمليات غالبًا، لكن النقطة لها الأولوية على أي حال. لذا فليس هناك أي تأثير. فقط الفاصلة المنقوطة هي الخطأ.
شرح قيمة "this"
الأهمية: 3
استدعينا الدالة user.go()
في الشيفرة التي بالأسفل 4 مرات متتالية. لكن الاستدعاءان (1)
و (2)
يعملان عملًا مختلفًا عن الاستدعائين (3)
و (4)
. لماذا؟
let obj, method; obj = { go: function() { alert(this); } }; obj.go(); // (1) [object Object] (obj.go)(); // (2) [object Object] (method = obj.go)(); // (3) غير معرف (obj.go || obj.stop)(); // (4) غير معرف
الحل
هنا التوضيح:
1- يُعد استدعاء دالة عادي.
2- مثل 1 تمامًا، لا تغير الأقواس ترتيب العمليات هنا، تعمل النقطة أولًا على أي حال.
3- هنا لدينا استدعاء أكثر تعقيدًا (expression).method()
. يعمل الاستدعاء كما لو تم فصله إلى سطرين:
f = obj.go; // حساب التعبير f(); // الاستدعاء
تُنَفَّذ f()
هنا كدالة، دون this
.
4- مشابة ل (3)
، لدينا تعبيرًا يسار النقطة .
.
لشرح سلوك الاستدعائين (3)
و (4)
، نحتاج لإعادة استدعاء معاملات الوصول لتلك الخاصية (النقطة أو الأقواس المربعة) التي ترجع قيمة من النوع المرجعي.
أي عملية عليها عدا استدعاء الدالة (مثل التعيين =
أو ||
) تُرجِعُها إلى قيمة عادية، والتي لا تحمل المعلومات التي تسمح بتعيين this
.
استخدام this
في الكائن معرَّف باختصار عبر الأقواس
الأهمية: 5
تُرجِع الدالة makeUser
كائنًا هنا. ما النتيجة من الدخول إلى ref
الخاص بها؟ ولماذا؟
function makeUser() { return { name: "John", ref: this }; }; let user = makeUser(); alert( user.ref.name ); // ما النتيجة؟
الحل
الإجابة: ظهور خطأ. جربها:
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 { name: "John", ref() { return this; } }; }; let user = makeUser(); alert( user.ref().name ); // John
أصبحت تعمل هنا لأن user.ref()
هي دالة، وقيمة this
تعَيَّن للكائن الذي قبل النقطة '.'
.
إنشاء آلة حاسِبة
الأهمية: 5
أنشئ كائنًا باسم calculator
يحوي الدوال الثلاث التالية:
-
read()
تطلب قيمتين وتحفظها كخصائص الكائن. -
sum()
تُرجِع مجموع القيم المحفوظة. -
mul()
تضرب القيم المحفوظة وتُرجِع النتيجة.
let calculator = { // ... ضع شيفرتك هنا... }; calculator.read(); alert( calculator.sum() ); alert( calculator.mul() );
الحل
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() );
التسلسل
الأهمية: 2
لدينا الكائن ladder
(سُلَّم) الذي يتيح الصعود والنزول:
let ladder = { step: 0, up() { this.step++; }, down() { this.step--; }, showStep: function() { // يعرض الخطوة الحالية alert( this.step ); } };
الآن، إن أردنا القيام بعدة استدعاءات متتالية، يمكننا القيام بما يلي:
ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1
عَدِّل الشيفرة الخاصة بالدوال up
، و down
، و showStep
لجعل الاستدعاءات متسلسلة كما يلي:
ladder.up().up().down().showStep(); // 1
يُستخدم هذا النمط بنطاق واسع في مكتبات JavaScript.
الحل
الحل هو إرجاع الكائن نفسه من كل استدعاء.
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
ترجمة -وبتصرف- للفصل Object methods, "this" من كتاب The JavaScript Language
اقرأ أيضًا
- المقال التالي: التحويل من نوع كائن إلى نوع أولي
- المقال السابق: النوع الرمزي (Symbol)
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.