۲۵ ژوئیه ۲۰۲۲

متدهای شیء، "this"

شیءها معمولا برای نمایش چیزهایی که در دنیای واقعی هستند ساخته می‌شوند، مانند کاربرها، سفارشات و غیره:

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

و در دنیای واقعی، یک کاربر می‌تواند کاری انجام دهد برای مثال چیزی را از سبد خرید اتخاب کند، وارد سایت شود، از سایت خارج شود و غیره.

اعمال در جاوااسکریپت توسط تابع‌های درون ویژگی‌ها نمایش داده می‌شوند.

مثال‌هایی از متد

برای شروع، بیایید به user یاد بدهیم که سلام کند:

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

user.sayHi = function() {
  alert("سلام!");
};

user.sayHi(); // !سلام

اینجا ما از Function Expression برای ساخت یک تابع استفاده کردیم و آن را به ویژگی user.sayHi شیء تخصیص دادیم.

سپس می‌توانیم آن را با user.sayHi() صدا بزنیم. حالا user می‌تواند صحبت کند!

تابعی که یک ویژگی از شیءای باشد متد آن نامیده می‌شود.

پس اینجا ما یک متد sayHi از شیء user داریم.

قطعا ما می‌توانستیم از تابعی که قبلا تعریف شده است استفاده کنیم، مثل اینجا:

let user = {
  // ...
};

// اول تعریف می‌کنیم
function sayHi() {
  alert("سلام!");
};

// سپس به عنوان متد آن را اضافه می‌کنیم
user.sayHi = sayHi;

user.sayHi(); // !سلام
برنامه‌نویسی شیءگرا

زمانی که ما با استفاده از شیء برای نمایش چیزهای موجود کد می‌نویسیم، به آن برنامه‌نویسی شیءگرا می‌گویند، یا به طور خلاصه: “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); // "this" به جای "user"
  }

};

…اما چنین کدی قابل اطمینان نیست. اگر ما تصمیم بگیریم که 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 استفاده می‌کردیم، کد کار می‌کرد.

“this” محدود نیست

در جاوااسکریپت، کلمه کلیدی this متفاوت از بیشتر زبان‌های برنامه‌نویسی دیگر رفتار می‌کند. این کلمه می‌تواند در هر تابعی استفاده شود، حتی اگر آن تابع متدی از یک شیء نباشد.

در مثال پایین هیچ سینتکس اروری وجود ندارد:

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() صدا زده شود، سپس در حین صدازدن f، this برابر با obj است. پس در مثال بالا یا برابر با user است یا admin.

صدازدن بدون شیء: this == undefined

ما حتی می‌توانیم تابع را بدون هیچ شیءای صدا بزنیم:

function sayHi() {
  alert(this);
}

sayHi(); // undefined

در این مورد this در حالت سخت‌گیرانه (strict mode) برابر با undefined است. اگر ما تلاش کنیم که به this.name دسترسی پیدا کنیم، یک ارور به وجود می‌آید.

در حالت غیر سخت‌گیرانه در چنین موردی مقدار this برابر است با global object (در مرورگر window است، ما در فصل شیء گلوبال به سراغ آن می‌رویم). این یک رفتار تاریخی است که "use strict" آن را درست می‌کند.

معمولا چنین صدازدنی یک ارور برنامه‌نویسی است. اگر this درون یک تابع باشد، انتظار می‌رود که در زمینه‌ی شیء صدا زده شود.

عواقب this بدون محدودیت

اگر شما از یک زبان برنامه‌نویسی دیگری میایید، پس شما احتمالا به نظریه “this محدود” عادت کرده‌اید، که متدهای تعریف‌شده درون یک شیء همیشه دارای یک this هستند که به همان شیء رجوع می‌کند.

در جاوااسکریپت this “آزاد” است، مقدار آن هنگام صدا زدن ارزیابی می‌شود و به اینکه متد کجا تعریف شده بستگی ندارد و بلکه به اینکه شیء “قبل از نقطه” چه باشد بستگی دارد.

اینکه this هنگام اجراشدن ارزیابی می‌شود فواید و زیان‌هایی دارد. از طرفی، یک تابع می‌تواند برای شیءهای متفاوت استفاده شود. از طرفی دیگر، هر چقدر اعطاف بیشتر باشد احتمالات برای اشتباهات هم بیشتر می‌شود.

اینجا ما به دنبال این نیستیم که درباره خوب یا بد بودن طراحی این زبان قضاوت کنیم. ما چگونه کارکردن با آن، چگونه سود بردن از آن و چگونگی جلوگیری از مشکلات را یاد می‌گیریم.

تابع‌های Arrow “this” ندارند

Arrow functionها خاص هستند: آنها از “خودشان” this ندارند. اگر ما از this در چنین تابعی استفاده کنیم، مقدار آن از تابع “معمولی” بیرونی گرفته می‌شود.

برای مثال، اینجا arrow() از this متد بیرونی user.sayHi() استفاده می‌کند:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

این یک ویژگی خاص arrow functionها است، و زمانی که ما نمی‌خواهیم یک this جداگانه داشته باشیم بلکه آن را از محتوای بالاتر بگیریم، از آن استفاده می‌کنیم. بعدا در فصل سرکشی دوباره از تابع‌های کمانی ما در arrow functionها عمیق‌تر می‌شویم.

خلاصه

  • تابع‌هایی که در ویژگی‌های شیءها ذخیره می‌شوند “متد” نامیده می‌شوند.
  • متدها به شیءها اجازه می‌دهند که “کاری انجام دهند” مثل object.doSomething().
  • متدها می‌توانند با this به شیء رجوع کنند.

مقدار this هنگام اجرا تعریف می‌شود.

  • هنگامی که یک تابع تعریف می‌شود، ممکن است از this استفاده کند، اما آن this تا زمانی که تابع صدا زده نشود مقداری ندارد.
  • یک تابع می‌تواند بین شیءها کپی شود.
  • زمانی که یک تابع با سینتکس “متد” صدا زده می‌شود: object.method()، مقدار this در حین صدازدن برابر با object است.

لطفا در نظر داشته باشید که arrow functionها خاص هستند: آنها this ندارند. زمانی که به this درون یک arrow function دسترسی پیدا می‌کنیم، مقدار آن از بیرون تابع گرفته می‌شود.

تمارین

اهمیت: 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 ); // Error: Cannot read property 'name' of undefined

دلیلش این است که قواعدی که this را تشکیل می‌دهند به تعریف شیء نگاه نمی‌کنند. فقط لحظه‌ی صدازدن مهم است.

اینجا مقدار this درون makeUser() برابر با undefined است، چون به عنوان تابع صدا زده شده است نه به عنوان یک متد با سینتکس نقطه.

مقدار this برای تمام تابع یکی است و بلوک‌های کد و شیءهای لیترال روی آن تاثیری نمی‌گذارند.

بنابراین ref: this در واقع this کنونی تابع را می‌گیرد.

ما می‌توانیم تابع را بازنویسی کنیم و this یکسان را با مقدار undefined برگردانیم:

function makeUser(){
  return this; // این بار هیچ شیء لیترالی وجود ندارد
}

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 برابر با شیء قبل از نقطه . است.

اهمیت: 5

یک شیء calculator با سه متد بسازید:

  • read() برای دو مقدار prompt می‌کند و آنها را به عنوان ویژگی‌های شیء با نام‌های a و b ذخیره می‌کند.
  • sum() مجموع مقدارهای ذخیره‌شده را برمی‌گرداند.
  • mul() مقدارهای ذخیره‌شده را ضرب می‌کند و نتیجه را برمی‌گرداند.
let calculator = {
  // ... کد شما ...
};

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.

اهمیت: 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
ladder.down();
ladder.showStep(); // 0

کد up، down و showStep را تغییر دهید تا صدازدن‌ها را زنجیره‌ای کنید، مثل این:

ladder.up().up().down().showStep().down().showStep(); // اول 1 را نشان می‌دهد سپس 0 را

چنین روشی در بین کتابخانه‌های جاوااسکریپت به طور گسترده استفاده می‌شود.

باز کردن یک 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().showStep().down().showStep(); // اول 1 را نشان می‌دهد و سپس 0 را

همچنین می‌توانیم به ازای هر خط یک بار صدا بزنیم. برای زنجیره‌های طولانی این روش خوانایی بیشتری دارد:

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

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش