1413 words
7 minutes
this in JavaScript: The Most Misunderstood Binding

لو سألت أي JavaScript Developer عن أكتر حاجة بتعمله صداع، هيقولك this من غير تفكير.

في لغات زي Java أو C#، الـ this كيوت وواضحة: هي دايماً بتشاور على الـ Instance اللي إنت جواه. بس في JavaScript؟ الموضوع مختلف تماماً.

الـ this في الـ JS مش مربوطة بمكان كتابة الكود (Author-time)، دي مربوطة بمكان التنفيذ (Runtime). يعني ممكن نفس الـ Function تديك 5 قيم مختلفة لـ this بناءً على “مين ندهلها وإزاي”.

المقالة دي مش عشان تحفظك قواعد، دي عشان تفهم الـ Engine بيفكر إزاي، وتعرف ليه الـ Keyword دي بتتصرف كأنها “مسكونة”.


1. ما وراء الستار: Execution Context & Scope#

عشان تفهم this وتتحكم فيها بجد، لازم ننزل سوا لغرفة المحركات (The Engine Room) ونفهم مصطلحين بيعملوا رعب للمبتدئين: Execution Context و Lexical Scope.

أولاً: الـ Lexical Scope (النطاق الكتابي)#

ده النظام اللي الـ JS بتمشي عليه عشان تعرف “أنا شايف مين ومين شايفني” بالنسبة للمتغيرات (Variables). كلمة Lexical جاية من Lexing step (مرحلة القراءة/التحليل)، ومعناها ببساطة:

“مكان كتابتك للكود هو اللي بيحدد الـ Scope بتاعه.”

لو كتبت دالة جوه دالة، الدالة الداخلية شايفة اللي بره، لأنها مكتوبة جواها. ده قرار اتخاذ وقت كتابة الكود (Author-time) وثابت مش بيتغير.

ثانياً: الـ Execution Context (سياق التنفيذ)#

ده بقى العالم الموازي. لما الـ JS Engine بيجي ينفذ أي دالة، بيعمل “صندوق” اسمه Execution Context. الصندوق ده بيحتوي على 3 حاجات حيوية عشان الكود يشتغل:

  1. Variable Environment: المتغيرات والدوال اللي جوه الصندوق ده.
  2. Scope Chain: حبل الود اللي بيربط الصندوق ده بالصناديق اللي فوقيه (عشان الـ Lexical Scope).
  3. this Binding: تحديد قيمة this للصندوق ده.

🛑 وهنا الصدمة: بينما المتغيرات (Variables) بتتبع الـ Lexical Scope (مكان الكتابة)، الـ this بتتبع الـ Execution Context (طريقة الاستدعاء). الـ this مش بتتحدد لما كتبت الكود، دي بتتحدد لما الـ Engine أنشأ الـ Execution Context، وده بيحصل فقط في لحظة التشغيل (Runtime).

عشان كده بنقول:

  • Variables: “أنت كتبتني فين؟”
  • this: “أنت ندهتلي إزاي؟” (Call-Site)

2. القواعد الأربعة (The 4 Rules)#

الـ Specification بتاع الـ JS حاطط 4 قواعد أساسية، الـ Engine بيطبقهم بالترتيب عشان يعرف this بتساوي إيه.

القاعدة 1: Default Binding (اليتيمة)#

لو ندهت الـ function لوحدها كده، من غير أي حاجة قبلها:

function sayHello() {
  console.log(this.name);
}

var name = "Global User";
sayHello(); // "Global User" (في المتصفح)

هنا this بتروح للـ Global Scope (الـ window في المتصفح). ⛔ خد بالك: لو شغال use strict (وده الطبيعي دلوقتي)، الـ this هتبقى undefined بدل الـ Global، عشان نمنع الكوارث.

القاعدة 2: Implicit Binding (السياق)#

دي أشهر حالة. لو الـ Function مسبوقة بـ “نقطة”:

const user = {
  name: "Ali",
  sayHello: function() {
    console.log(this.name);
  }
};

user.sayHello(); // "Ali"

هنا الـ Call-Site هو user.sayHello(). الـ Engine بيشوف الـ Object اللي قبل النقطة (user) وبيخليه هو this. سهلة؟

⚠️ الفخ (Losing Binding): ركز في دي عشان ده سبب 80% من مشاكل React القديمة:

const myFunc = user.sayHello; // بنخزن الـ function في متغير
myFunc(); // undefined (أو Error في strict mode)

ليه؟ لأننا لما نادينا myFunc()، مكنش في “نقطة” ولا Object قبلها. رجعنا للقاعدة رقم 1 (Default Binding). الـ Link بين الـ function والـ object اتقطع.

القاعدة 3: Explicit Binding (التحكم اليدوي)#

لو عايز تمسك الدريكسيون وتقول للـ Engine: “اربُط this بالـ Object ده، وملكش دعوة أنت”، يبقى جه وقت الـ Explicit Binding. عندنا 3 فرسان للمهمة دي: call, apply, bind. والفرق بينهم هو سؤال المقابلات الأبدي.

تعال نحفظهم بطريقة متتنسيش:

1. call (المباشر)#

ليه اتعملت؟ عشان الاستخدام المباشر. معاك الـ arguments بتاعتك واحد واحد؟ استخدم call.

  • المشكلة اللي بتحلها: بتخليك تقدر “تستلف” Method من Object وتستخدمها على Object تاني خالص (Method Borrowing).
  • 🔑 سر الحفظ: حرف الـ C في Call يفكرك بـ Comma (Arguments مفصولين بفاصلة).

2. apply (بتاعة الـ Arrays)#

ليه اتعملت؟ عشان زمان (قبل ES6 Spread Operator ...) مكنش في طريقة تبعت بيها Array كـ arguments لدالة بتستقبلهم مفصولين غير بـ apply.

  • المشكلة اللي بتحلها: تحويل “مصفوفة” لـ “قائمة مدخلات”.
  • 🔑 سر الحفظ: حرف الـ A في Apply يفكرك بـ Array (المدخلات في مصفوفة).
  • مثال عملي:
    const numbers = [5, 10, 15];
    // زمان (Classic Way)
    Math.max.apply(null, numbers); 
    // دلوقتي (Modern Way with Spread)
    Math.max(...numbers);
    

3. bind (الحارس الشخصي)#

ليه اتعملت؟ عشان تحل أكتر مشكلة مزعجة في الـ JS وهي “فقدان الـ this” في الـ Callbacks.

  • المشكلة اللي بتحلها: لما بتبعت دالة كـ Callback، الـ JS بتنفذها في وقت تاني، فالـ this بتضيع. bind بتثبت الـ this للأبد.
  • ميزة إضافية (Partial Application): تقدر تستخدم bind عشان تثبت arguments كمان مش بس this.
    function multiply(a, b) { return a * b; }
    const double = multiply.bind(null, 2); // قفلنا a = 2
    double(5); // 10
    
  • 🔑 الاستخدام: ممتازة للـ Callbacks والـ Event Handlers.

مثال يلم الليلة دي كلها:

function introduce(lang, level) {
  console.log(`I am ${this.name}, coding ${lang} as ${level}.`);
}

const dev = { name: "Ali" };

// 1. call: Arguments ورا بعض (Comma)
introduce.call(dev, "JS", "Senior"); 

// 2. apply: Arguments في لسته (Array)
introduce.apply(dev, ["Python", "Junior"]); 

// 3. bind: بتجهز دالة للمستقبل
const futureFunc = introduce.bind(dev, "Rust", "Expert");
// ... ممكن تناديها في أي وقت ومكان
futureFunc(); 

القاعدة 4: New Binding (الخلاط)#

لما تستخدم new قبل الـ Function (Construction Call):

function User(name) {
  this.name = name;
}

const u = new User("Mona");

كلمة new بتعمل سحر (4 خطوات بالتحديد):

  1. بتعمل Object جديد فاضي.
  2. بتربط الـ Prototype بتاعه.

ترتيب القوة (Binding Precedence)#

لما القواعد دي تدخل في خناقة مع بعض، مين يكسب؟ الترتيب من الأقوى للأضعف:

  1. new Binding: (هي الكبيرة، بتكسب أي حد).
  2. Explicit Binding: (call, apply, bind).
  3. Implicit Binding: (Context Object).
  4. Default Binding: (الغلابة).

مثال ينهي الجدل:

function User(name) { this.name = name; }

// بنحاول نربطها بـ hard object
const boundUser = User.bind({ name: "Ignored" });

// بس استخدمنا new
const instance = new boundUser("Ali");

console.log(instance.name); // "Ali" (الـ new كسبت وتجاهلت الـ bind)

3. Arrow Functions: المتمردة#

الـ ES6 لما نزلت Arrow Functions () => {}، مكنش هدفها بس إننا نكتب كود أقصر. هدفها الرئيسي كان حل أزمة الـ Dynamic Binding.

الـ Arrow Function معندهاش this خاصة بيها في الـ Execution Context بتاعها. وبالتالي، هي بتطبق قاعدة الـ Lexical Scope اللي شرحناها فوق:

“أنا مش لاقيه this عندي، فهبص للـ Scope اللي أبويا اتكتب فيه (Parent Scope) وآخد الـ this بتاعته.”

يعني رجعنا تاني لقاعدة “مكان الكتابة” مش “مكان الاستدعاء”.

const obj = {
  name: "Smart Object",
  regularFunc: function() {
    setTimeout(function() {
      console.log(this.name); // undefined (لأن setTimeout بتناديها Default Binding)
    }, 100);
  },
  arrowFunc: function() {
    setTimeout(() => {
      console.log(this.name); // "Smart Object" ✅
    }, 100);
  }
};

في الـ arrowFunc، مفيش this للدالة. فطلعت تدور بره فلقت نفسها جوه obj، فأخدت الـ this بتاعته.

فخ للمحترفين: إوعى تستخدم Arrow Function كـ Method مباشر للـ Object:

const badObj = {
  name: "No!",
  sayHi: () => {
    console.log(this.name); // Undefined or Global
  }
};

الـ {} بتاعة الـ Object مش بتعمل Scope، فالـ Arrow function شايفة الـ Global Scope (أو الـ Module Scope لو شغال بـ ES Modules).

  • في المتصفح: this هتكون window.
  • في Node.js أو Strict Mode: this ممكن تكون undefined أو module.exports.

4. React و الـ Event Handlers#

ليه في الـ Class Components كنا بنعمل this.handleClick = this.handleClick.bind(this)؟

class Button extends React.Component {
  handleClick() {
    console.log(this); // null or undefined
  }

  render() {
    // إحنا هنا بنباصي الـ reference بس للدالة
    return <button onClick={this.handleClick}>Click Me</button>;
  }
}

لما React بتيجي تنفذ handleClick لما الزرار يداس، هي بتناديها كـ Callback عادية (مش كـ Method). يعني Default Binding. وعشان الـ Classes دايماً use strict، النتيجة undefined.

الحل؟

  1. Constructor Binding: this.method = this.method.bind(this) (عذاب).
  2. Public Class Fields (Arrow Functions):
    handleClick = () => {
      console.log(this); // الـ Instance بتاع الـ Component
    }
    
    بما إنها Arrow Function، فهي هتاخد this من وقت تعريف الـ Class Instance.

وفي الـ Hooks؟ الـ Hooks لغت الاحتياج لـ this تماماً. useEffect و useCallback بيعتمدوا على الـ Closures (المتغيرات) بدل الـ this (الـ Object State). وده كان نقلة نوعية في التخلص من صداع الـ Binding.


5. من منظور الـ Engine#

الـ V8 Engine مش بيسحر. الموضوع ببساطة إن:
“كل Function في الـ JS بتاخد Parameter سري اسمه this، الـ Engine هو اللي بيحدد قيمته قبل ما ينادي الدالة، مش إنت.” لما بتكتب user.sayHi()، الـ Engine بيترجمها داخلياً لحاجة أشبه بـ sayHi.call(user).

ولو الـ Function مش مستخدمة this جواها، الـ Optimizing Compilers (زي TurboFan) ممكن يشيلوا حسابات الـ Context دي تماماً عشان السرعة. بس لو استخدمتها، الـ Engine لازم يتأكد في كل مرة الـ Function بتتنادى فيها، مين هو الـ Context صح.

عشان كده الـ Binding بياخد وقت (Cycles) في الـ CPU. الـ Arrow Functions أسرع شعرة (Micro-optimization) لأنها مش بتعمل Context جديد، بس الفرق لا يذكر في التطبيقات العادية.


الخلاصة#

الـ this مش بعبع. هي بس محتاجة تتفهم كـ “Dynamic Binding” مش “Static Reference”.

الخلاصة في 3 كلمات: “Who Called Me?”

  1. new Binding (الأقوى) → الـ Object الجديد.
  2. call/apply/bind (Explicit) → اللي حددته.
  3. obj.method() (Implicit) → الـ obj.
  4. func() (Default) → الـ Global (أو undefined).
  5. Arrow Function → ⛔ تتجاهل القواعد دي كلها وتاخد this من الـ Lexical Scope.

فهمك لـ this هو الفرق بين إنك تقضي ساعات بتعمل Debugging لـ undefined is not a function، وبين إنك تكتب كود React/Vue/Node.js ومتأكد هو شغال إزاي.

نصيحة أخيرة: لو لقيت نفسك بتستخدم this كتير في Logic معقد، فكر تستخدم Closures أو Functional Programming. الـ this مفيدة جداً في الـ OOP والـ Design Patterns، بس الـ Overuse بتاعها بيخلي الكود صعب التتبع (Hard to trace).