اذهب إلى المحتوى

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


Emq Mohammed

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

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

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...