مستوى المقال: متوسط. الكلام ده موجّه لحد كتب JavaScript قبل كده، وعارف يعمل function و object، لكن كلمة this لسه بتفاجئه بنتايج غريبة. لو لسه بتبدأ من الصفر، اقرأه على مهلك؛ فيه مثال من الحياة قبل كل جزء تقني.
ليه this بيتغيّر معناه في JavaScript
لو زرار شغّال تمام عندك دلوقتي بيرمي Cannot read properties of undefined بمجرد ما تنقله جوّه setTimeout أو تبعته كـ callback، المشكلة مش في الكود اللي جوّه الدالة. كلمة this بصّت لمكان تاني. المقال ده بيديك أربع قواعد تحسم قيمة this في أي سطر من غير تخمين، وبيوضّح ليه الدالة السهمية بتحل أغلب الوجع ده.
المشكلة باختصار
أغلب المطورين بيفتكروا إن this بتشير لـ«الكائن اللي الدالة متعرّفة جوّاه». الفكرة دي غلط، وبتفشل بالظبط في اللحظة اللي بتنقل فيها الدالة من مكانها: تبعتها لـ addEventListener، تخزّنها في متغير، تمرّرها لـ map. this في JavaScript مش مربوطة بمكان كتابة الدالة. هي مربوطة بطريقة استدعائها. الفرق ده بالظبط هو سبب أغلب الـ bugs اللي ليها علاقة بـ this.
مثال من الحياة قبل الكود
قبل أي كود، خد المثال ده. كلمة «أنا» في اللغة العربية مالهاش صاحب ثابت. لمّا تقول انت «أنا جعان»، الكلمة بتشير ليك. لمّا صاحبك يقول نفس الجملة، نفس الكلمة بتشير له هو. الكلمة اتنطقت بنفس الحروف، لكن اللي بتشير له بيتحدّد ساعة النطق، مش وقت ما الكلمة اتكتبت في القاموس.
this هي بالظبط كلمة «أنا» بتاعة الدوال. لمّا تكتب this جوّه دالة، انت مش بتحدّد قيمتها. القيمة بتتحدّد كل مرة الدالة بتتنادى فيها، وبتعتمد على إزاي نادتها. نفس الدالة ممكن ترجّع this مختلفة في كل استدعاء. ده مش عيب في اللغة؛ ده تصميم مقصود اسمه الربط الديناميكي (dynamic binding).
التعريف الدقيق
بعد المثال، التعريف العلمي. كل استدعاء لأي دالة في JavaScript بيعمل سياق تنفيذ (execution context). من ضمن مكوّنات السياق ده حاجة اسمها this binding. القيمة دي بتتحطّ لحظة الاستدعاء حسب صيغة النداء، وبتفضل ثابتة طول ما الدالة شغّالة. يعني this مش متغيّر عادي ومش بتورّث من الـ scope زي باقي المتغيرات؛ هي قيمة بتُحقَن من جديد في كل استدعاء.
القواعد الأربعة بالترتيب
عشان تعرف قيمة this في أي سطر، بُصّ على صيغة الاستدعاء وطبّق القواعد دي. أول قاعدة تتحقّق هي اللي بتحسم القيمة.
- الاستدعاء الحر:
fn()من غير أي حاجة قبلها. هناthisبتساويundefinedفي الوضع الصارم strict mode، أو الكائن العام (windowفي المتصفح) من غيره. - الاستدعاء كميثود:
obj.fn().thisبتساوي الكائن اللي على يسار النقطة، يعنيobj. - الاستدعاء الصريح:
fn.call(x)أوfn.apply(x)أو نسخة معمولة بـfn.bind(x).thisبتساويxاللي انت مرّرته بإيدك. - الاستدعاء بـ new:
new Fn(). بيتعمل كائن جديد فاضي وthisبتشير له.
"use strict";
function whoAmI() {
return this;
}
// 1) استدعاء حر
whoAmI(); // undefined (في strict mode)
// 2) استدعاء كميثود
const user = { name: "سارة", whoAmI };
user.whoAmI(); // الكائن user نفسه
// 3) استدعاء صريح بـ call
whoAmI.call({ name: "ليلى" }); // { name: "ليلى" }
// 4) استدعاء بـ new
function Person(name) {
this.name = name;
}
const p = new Person("كريم"); // this كائن جديد، و p.name === "كريم"
لو اتلخبطت بين قاعدتين، فيه ترتيب أولوية واضح: new بيكسب على bind، وbind بيكسب على استدعاء الميثود، واستدعاء الميثود بيكسب على الاستدعاء الحر. والدالة السهمية حالة خاصة بتتجاهل القواعد دي كلها، وهي اللي جاية دلوقتي.
الدوال السهمية: الحل اللي بيقفل أغلب الوجع
الدالة السهمية (=>) مالهاش this خاص بيها أصلاً. لمّا تكتب this جوّاها، هي بتاخدها من المكان اللي اتعرّفت فيه، يعني من الدالة الأب أو الـ scope المحيط. ودي قيمة ثابتة: مفيش call ولا bind يقدر يغيّرها. ده اللي بيخلّيها الحل العملي لأغلب مشاكل this.
المثال الكلاسيكي: عدّاد بيزوّد رقم كل ثانية. النسخة المكسورة الأول:
const counter = {
count: 0,
start() {
// this هنا = counter (استدعاء كميثود)
setInterval(function () {
// دالة عادية اتنادت بشكل حر جوّه setInterval
// this هنا = undefined -> الكراش
this.count++;
}, 1000);
},
};
counter.start(); // TypeError: Cannot read properties of undefined
دلوقتي نفس الكود بدالة سهمية:
const counter = {
count: 0,
start() {
setInterval(() => {
// الدالة السهمية ورّثت this من start()
// يعني this = counter زي ما احنا عايزين
this.count++;
console.log(this.count);
}, 1000);
},
};
counter.start(); // 1, 2, 3, ...
الفرق سطر واحد: function () {} بقت () => {}. الدالة العادية اتنادت بشكل حر فخسرت الكائن؛ الدالة السهمية مالهاش this خاص فورّثت this بتاع start.
سيناريو واقعي: زرار دفع بيبعت الطلب مرتين
خد حالة فعلية. عندك صفحة دفع فيها class بيمثّل زرار التأكيد، وميثود handleClick بيعمل this.disabled = true عشان يمنع الضغط مرتين. الكود بيشتغل تمام لمّا تستدعيه يدوي. بس انت ربطته بالحدث كده: button.addEventListener("click", obj.handleClick).
هنا حصلت الكارثة بهدوء. addEventListener بيستدعي الدالة بشكل حر، فـ this بقت مش الكائن. السطر this.disabled = true فشل في صمت، الزرار فضل شغّال، والمستخدم اللي بيدوس مرتين بيبعت الطلب مرتين. في موقع بـ 12 ألف عملية دفع في اليوم، نسبة دوسة مزدوجة 0.8% معناها حوالي 96 طلب مكرر يوميًا لازم يترجّع يدوي. الحل سطر واحد: تبعت obj.handleClick.bind(obj)، أو تعرّف handleClick كـ class field سهمي عشان يفضل مربوط بالكائن مهما اتنقل.
الـ trade-offs اللي محدش بيقولك عليها
كل حل من دول له ثمن. متختارش واحد على عماك.
- الـ class field السهمي (
handleClick = () => {...}) بيحلthisتمامًا، لكن بيعمل نسخة من الدالة لكل instance بدل ما تتشارك على الـ prototype. لو بتعمل 10 آلاف instance، ده 10 آلاف نسخة دالة في الذاكرة. مع كائنات قليلة مفيش فرق محسوس؛ مع كائنات كتير جدًا الموضوع بيبان. .bind()بيرجّع دالة جديدة كل مرة بتناديه. لو ناديته جوّه دالة الرسم في React، انت بتعمل reference جديدة كل render، واللي بيكسرReact.memoويخلّي الكومبوننت الابن يعيد الرسم بدون لزمة. اعمل الـbindمرة واحدة بره مسار الرسم.- الدالة السهمية ثابتة الـ
this، يعني مينفعش تستخدمها لمّا تكون فعلاً محتاجthisديناميكية، زي ميثود علىprototypeمفروض يشتغل على أي كائن يناديه. وكمان مينفعش تستخدمها معnew. - الوضع الصارم strict mode بيخلّي
thisفي الاستدعاء الحرundefinedبدلwindow. ده مكسب: بيوقّعك بـ error واضح بدل ما يكتب على الكائن العام في صمت. بس لو عندك كود قديم بيعتمد على السلوك القديم، هيتكسر.
متى لا تشغّل بالك بالموضوع ده
مش كل كود محتاج تفكّر في this.
- لو بتكتب modules بدوال نقية بتاخد كل حاجة كـ arguments وبترجّع قيمة،
thisمش بتلمسك أصلاً. - React بالـ hooks شالت
thisتقريبًا من كود الكومبوننت؛ لو مشروعك function components بس، الموضوع بيخصّك أقل بكتير. - لو كل ميثوداتك بتتنادى دايمًا بصيغة
obj.method()وعمرها ما بتتنقل كـ callback، القاعدة التانية كفاية ومش محتاج تعقّد.
الافتراض هنا إنك شغّال على JavaScript حديثة (ES2015 وأحدث) بـ strict mode مفعّل، وده الوضع الافتراضي جوّه أي module أو class.
المصادر
- MDN Web Docs — صفحة
thisوصفحة Arrow function expressions. - ECMAScript Language Specification (ECMA-262) — قسم Execution Contexts و this binding.
- MDN Web Docs —
Function.prototype.bind()وFunction.prototype.call(). - Kyle Simpson — كتاب «You Don't Know JS Yet: this & Object Prototypes».
الخطوة التالية
دلوقتي افتح أقدم ملف فيه class في مشروعك. دوّر على أي ميثود بيتبعت كـ callback لـ addEventListener أو setTimeout أو .map. لكل واحدة اسأل سؤال واحد: لو الدالة دي اتنادت لوحدها، this هتبقى إيه؟ لو الإجابة «undefined وأنا محتاجها الكائن»، حوّلها لدالة سهمية أو ضيف .bind(this) في مكان التعريف. ابدأ بملف واحد النهاردة، وشغّل الاختبارات بعدها؛ لو عدّت، انت قفلت فئة كاملة من الـ bugs الصامتة.