٢٥ مارس ٢٠٢١

الدوال في الكائنات واستعمالها `this`

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

let user = {
  name: "John",
  age: 30
};

يمكن للمستخدم في العالم الحقيقي أن يقوم بعدة تصرفات: مثل اختيار شيء من سلة التسوق، تسجيل الدخول، والخروج …إلخ.

تُمَثَّل هذه التصرفات في لغة JavaScript بإسناد دالة إلى خاصية وتدعى الدالة آنذاك بالتابع (method، أي دالة تابعة لكائن).

أمثلة على الدوال

بدايةً، لنجعل المستخدم user يقول مرحبًا:

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

Here we’ve just used a Function Expression to create a function and assign it to the property user.sayHi of the object.

Then we can call it as user.sayHi(). The user can now speak!

A function that is a property of an object is called its method.

So, here we’ve got a method sayHi of the object user.

يمكننا أيضًا استخدام دالة معرفة مسبقًا بدلًا من ذلك كما يلي:

let user = {
  // ...
};

//  أولا، نعرف دالة
function sayHi() {
  alert("Hello!");
};

// أضِف الدالة للخاصية لإنشاء تابع
user.sayHi = sayHi;

user.sayHi(); // Hello!
يسمى كتابة الشيفرة البرمجية باستخدام الكائنات للتعبير عن الاشياء «بالبرمجة الشيئية/كائنية» ([object-oriented programming](https://en.wikipedia.org/wiki/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() { // same as "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(); // TypeError: Cannot read property 'name' of null

إن استخدمنا this.name بدلًا من user.name بداخل alert، فستعمل الشيفرة عملًا صحيحًا.

In JavaScript, keyword this behaves unlike most other programming languages. It can be used in any function, even if it’s not a method of an object.

الكلمة 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;

// tلدى الاستدعائين قيمة مختلفة لـ
// "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(); // غير معرَّف

في هذه الحالة ستكون قيمة this هي undefined في الوضع الصارم. فإن حاولنا الوصول إلى this.name سيكون هناك خطأ.

في الوضع غير الصارم، فإن قيمة this في هذه الحالة ستكون المتغير العام (في المتصفح window والتي سَنشرحها في فصل المتغيرات العامة). هذا السلوك زمني يستخدم إصلاحات الوضع الصارم "use strict".

يُعد هذا الاستدعاء خطأً برمجيًا غالبًا. فإن وًجِدت this بداخل دالة، فمن المتوقع استدعاؤها من خلال كائن.

الأمور المترتبة على this الغير محدودة النطاق

إن أتيت من لغة برمجية أخرى، فمن المتوقع أنك معتاد على “this المحدودة” إذ يمكن لِلدوال المعرَّفة في الكائن استخدام this التي ترجع للكائن.

تستخدم this بحرية في JavaScript، وتُقَيَّم قيمتها أثناء التنفيذ ولا تعتمد على المكان حيث عُرِّفت فيه، بل على الكائن الذي قبل النقطة التي استدعت الدالة.

يوجد ايجابيات وسلبيات لمبدأ تقييم this أثناء وقت التشغيل. فمن ناحية، يمكن إعادة استخدام الدالة مع عدة كائنات، ومن الناحية الأخرى، المرونة الأكثر تعطي فرصًا أكثر للخطأ. لسنا بصدد الحكم على تصميم اللغة ونعته بالجيد أم سيء، بل نحاول فهم طريقة عملها وكيفية الاستفادة من ميزاتها وتجنب الأخطاء.

لدوال السهمية لا تحوي "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 مستقلة، ونريد أخذها من السياق الخارجي بدلًا من ذلك. سَنتعمق في موضوع الدوال السهمية لاحقًا في فصل «إعادة النظر في الدوال السهمية».

الخلاصة

  • الدوال المخزنة في الكائنات تسمى «توابع» (methods).
  • تسمح هذه الكائنات باستدعائها بالشكل object.doSomething()‎.
  • يمكن للدوال الوصول إلى الكائن المعرفة فيه (أو النسخة التي استدعته المشتقة منه) باستخدام الكلمة المفتاحيةthis.
  • تُعَرَّف قيمة this أثناء التنفيذ.
  • قد نستخدم this عند تعريف دالة، لكنها لا تملك أي قيمة حتى استدعاء الدالة.
  • يمكن نسخ دالة بين الكائنات.
  • عند استدعاء دالة بالصيغة object.method()‎، فإن قيمة this أثناء الاستدعاء هي object.

لاحظ أن الدوال السهمية مختلفة تتعامل تعاملًا مختلفًا مع this إذ لا تملك قيمة لها. عند الوصول إلى this بداخل دالة سهمية فإن قيمتها تؤخذ من النطاق الموجودة فيه.

مهمه

تُرجِع الدالة makeUser كائنًا هنا. ما النتيجة من الدخول إلى ref الخاص بها؟ ولماذا؟

What is the result of accessing its ref? Why?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // ما النتيجة؟

Aالإجابة: ظهور خطأ.

جربها:

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 this; // this time there's no object literal
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

كما ترى فإن نتيجة alert( makeUser().name ) هي نفسها نتيجة alert( user.ref.name ) من المثال السابق هنا حالة معاكسة تمامًا:

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // John

أصبحت تعمل هنا لأن user.ref()‎ هي دالة، وقيمة this تعَيَّن للكائن الذي قبل النقطة '.'

أنشئ كائنًا باسم calculator يحوي الدوال الثلاث التالية:

  • read()‎ تطلب قيمتين وتحفظها كخصائص الكائن.
  • sum()‎ تُرجِع مجموع القيم المحفوظة.
  • mul()‎ تضرب القيم المحفوظة وتُرجِع النتيجة.
let calculator = {
  // ... your code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

قم بتشغيل العرض التوضيحي

افتح sandbox بالإختبارات.

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() );

افتح الحل الإختبارات في sandbox.

لدينا الكائن ladder (سُلَّم) الذي يتيح الصعود والنزول:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // shows the current step
    alert( this.step );
  }
};

الآن، إن أردنا القيام بعدة استدعاءات متتالية، يمكننا القيام بما يلي:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

عَدِّل الشيفرة الخاصة بالدوال up، و down، و showStep لجعل الاستدعاءات متسلسلة كما يلي:

ladder.up().up().down().showStep(); // 1

يُستخدم هذا النمط بنطاق واسع في مكتبات JavaScript

افتح sandbox بالإختبارات.

الحل هو إرجاع الكائن نفسه من كل استدعاء.

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

افتح الحل الإختبارات في sandbox.

خريطة الدورة التعليمية