هذا المقال يتطلب مستوى متوسط. لو انت لسه بادئ في جافاسكريبت خالص، هتفهم المثال البسيط في الأول عادي، والجزء العلمي هيبقى مرجع ترجعله بعدين.
لو دالتك بتشتغل تمام لمّا تناديها لوحدها، وبتضرب undefined لمّا تحطها جوه setTimeout أو addEventListener، المشكلة مش في الدالة. المشكلة إن قيمة this اترمت وقت النداء. المقال ده هيوريك بالظبط إمتى بتضيع وإزاي تمسكها من غير ما تكسر باقي الكود.
الـ this في جافاسكريبت: قيمة بتتحدد وقت النداء مش وقت الكتابة
المشكلة باختصار
معظم اللغات بتربط الكلمة اللي زي this بالكلاس اللي اتكتبت جواه. جافاسكريبت لأ. هنا this قيمتها بتتحدد حسب إزاي ناديت الدالة، مش حسب فين عرّفتها. نفس الدالة ممكن تديك ثلاث قيم مختلفة لـ this حسب طريقة الاستدعاء. ده اللي بيخلّي مطورين شاطرين يقضّوا ساعات في باج، سببه سطر واحد نقل الدالة من مكان لمكان.
الفكرة بمثال بسيط قبل الكلام العلمي
تخيّل كلمة "أنا" في جملة. لو قلت "أنا جعان"، مين "أنا" دي؟ حسب مين اللي بيتكلم. نفس الكلمة بالظبط، بس معناها بيتغيّر حسب المتحدث اللي نطقها دلوقتي.
الـ this زيها بالظبط. مش اسم ثابت لحاجة معيّنة، ده ضمير بيشير لـ "المتحدث" وقت النداء. و"المتحدث" في جافاسكريبت هو الكائن اللي على شمال النقطة لحظة استدعاء الدالة. غيّر مين اللي بينده، هيتغيّر معنى this.
الشرح العلمي: قيمة this بتتحدد من موقع الاستدعاء
القاعدة الدقيقة: قيمة this بتتحسب من call-site، يعني الشكل اللي اتنادت بيه الدالة فعليًا. فيه أربع طرق ربط، بالأولوية دي:
- ربط عبر new: لو ناديت الدالة بـ
new،thisبتبقى الكائن الجديد. - ربط صريح: لو استخدمت
callأوapplyأوbind، انت اللي بتحددthisبإيدك. - ربط ضمني: لو ناديت الدالة كـ method بالشكل
obj.fn()،thisبتبقىobj. - الربط الافتراضي: لو نادِتها لوحدها
fn()، في الوضع الصارمthisبتبقىundefined، وفي الوضع العادي بتبقى الكائن العام (windowفي المتصفح).
الافتراض هنا إننا بنتكلم عن الدوال العادية. الدوال السهمة (arrow) قصة تانية هنشرحها تحت.
الكود اللي بيوريك ضياع this بعينك
"use strict";
const user = {
name: "أحمد",
greet() {
return `أهلاً يا ${this.name}`;
}
};
console.log(user.greet()); // "أهلاً يا أحمد" ← ربط ضمني، this = user
const sayHi = user.greet; // نقلنا الدالة برّه الكائن
console.log(sayHi()); // TypeError: Cannot read properties of undefined
// this بقت undefined لأننا نادِناها لوحدها
لاحظ إن الدالة ماتغيرتش حرف. اللي اتغيّر هو طريقة النداء. أول مرة كانت user.greet()، تاني مرة بقت sayHi() من غير كائن على الشمال، فضاعت this.
سيناريو واقعي: الباج اللي بيظهر بعد ساعات
لو عندك واجهة فيها زرار وربطت الميثود مباشرة:
class Counter {
constructor() { this.count = 0; }
increment() { this.count++; console.log(this.count); }
}
const c = new Counter();
button.addEventListener("click", c.increment); // غلط
لمّا يتضغط الزرار، المتصفح بينده increment والـ this بتبقى عنصر الـ DOM بتاع الزرار مش الكائن c. فـ this.count بتبقى undefined، وundefined++ بتطلع NaN. في الوضع العادي (مش strict) الوضع أخطر: this بتبقى window، فبتكتب على متغيّر عام من غير ما ترمي خطأ، والباج بيفضل مستخبّي.
الأرقام هنا مش عن السرعة، دي عن التكلفة الخفية: باج زي ده في لوحة فيها 30 معالج حدث ممكن ياكل نص يوم debugging، وكله سببه إن this اترمت في سطر ربط واحد.
الحل: bind أو الدالة السهمة
الدالة السهمة ماعندهاش this خاص بيها. بترث this من النطاق المحيط بيها وقت التعريف (lexical). ده بيحلّ مشكلة الـ callbacks بأناقة:
button.addEventListener("click", () => c.increment()); // this جوه increment = c
// أو التثبيت الصريح:
button.addEventListener("click", c.increment.bind(c)); // ثبّتنا this على c
الـ trade-offs: مش كل حل ببلاش
- bind: بيثبّت
thisبشكل صريح وواضح، لكنه بيعمل دالة جديدة في كل استدعاء. لو حطيتthis.method.bind(this)جوه دالة render بتتنفّذ مع كل إعادة رسم، وعندك قائمة 1000 عنصر، انت بتخلق 1000 دالة جديدة كل re-render. ثبّت الـ bind مرة واحدة في الـ constructor بدل ما تكرره. - الدالة السهمة: بترث
thisتلقائيًا، بس ماينفعش تستخدمها كـ method على prototype ولا كـ constructor بـnew، لأنها أصلًا ماعندهاشthisخاص تربطه.
المقايضة باختصار: bind = تحكّم صريح مقابل دالة إضافية في الذاكرة. Arrow = راحة وربط تلقائي مقابل إنها مش صالحة لكل المواضع.
متى لا تشغّل بالك بـ this أصلًا
لو بتكتب دوال نقية (pure functions) بتاخد كل اللي محتاجاه من المعاملات وبترجّع نتيجة، انت مش محتاج this من الأساس. مررّ البيانات صراحةً بدل ما تعتمد على سياق مخفي. الكود اللي مافيهوش this أسهل في الاختبار وأصعب إنه يتكسر لمّا تنقل الدالة. استخدم this بس لمّا يبقى عندك كائن ليه حالة فعلية بتشتغل عليها الميثودز.
الخطوة التالية
افتح أقرب addEventListener أو setTimeout عندك، وبُصّ على الدالة اللي بتمرّرها. لو هي method من كائن ومتمرّرة بالاسم من غير .bind ولا لفّها في دالة سهمة، دي قنبلة موقوتة. صلّحها دلوقتي بواحد من الحلّين اللي فوق، وجرّب تطبع this جواها قبل وبعد التعديل عشان تشوف الفرق بعينك.
المصادر
- MDN Web Docs — this: توثيق ربط this وقواعده. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
- MDN Web Docs — Arrow function expressions: سلوك this في الدوال السهمة. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- ECMAScript Language Specification (ECMA-262): تعريف this والوضع الصارم رسميًا. tc39.es/ecma262
- Kyle Simpson — You Don't Know JS: this & Object Prototypes: شرح قواعد الربط الأربعة بالتفصيل.