۶. this و Prototype؛ رفع خطای متدهای جداشده از شیء
یک شمارنده در صفحه درست کار میکند، اما وقتی متد آن را بهعنوان callback میدهید، مقدارش عوض نمیشود یا خطا میبینید. این مشکل معمولاً به نحوهٔ فراخوانی تابع مربوط است، نه خرابی دکمه. هدف این درس، شناخت اندازهٔ لازم از this و Prototype برای خواندن و عیبیابی کد واقعی است؛ قرار نیست همهٔ منطق برنامه را به کلاس تبدیل کنیم.
برای نتیجهٔ قابل تکرار، مثالهای جاوااسکریپت را در فایل .mjs با Node.js ۲۴ اجرا کنید. ماژولها در حالت strict اجرا میشوند. محیط کنسول مرورگر ممکن است برای کد سطح بالا رفتار متفاوتی داشته باشد، بنابراین نتیجهٔ this در سطح بالای کنسول را به همهٔ تابعها تعمیم ندهید.
فراخوانی متد با فراخوانی تابع فرق دارد
const cartCounter = {
quantity: 0,
increment() { this.quantity += 1; return this.quantity; }
};
console.log(cartCounter.increment()); // 1
const detached = cartCounter.increment;
try {
detached();
} catch (error) {
console.log(error instanceof TypeError); // true در ماژول
}
const safeIncrement = cartCounter.increment.bind(cartCounter);
console.log(safeIncrement()); // 2در cartCounter.increment()، شیء پیش از نقطه در تعیین this نقش دارد. انتقال خود تابع به متغیر دیگر، آن ارتباط را خودکار حفظ نمیکند. bind تابع تازهای میسازد که برای این کاربرد، this آن به شیء مشخص متصل است. راه دیگر، یک تابع پوشاننده مانند () => cartCounter.increment() است. انتخاب این دو باید با نیاز پاکسازی رویداد و خوانایی هماهنگ باشد.
در تابع پیکانی، this مستقل به شیوهٔ تابع معمولی ساخته نمیشود؛ مقدار را از محیط پیرامون میگیرد. بنابراین جایگزینکردن کورکورانهٔ همهٔ متدها با پیکان، درمان عمومی نیست. اگر شیء سادهای را در سطح ماژول تعریف کنید و متدش را پیکانی بنویسید، لزوماً this به آن شیء اشاره نخواهد کرد. بهجای حفظ یک شعار، محل تعریف و شیوهٔ فراخوانی را جدا بررسی کنید.
callback رویداد و مسئولیت پاکسازی
این مثال کامل را در یک فایل HTML قرار دهید و در مرورگر باز کنید. دکمهٔ «توقف» شنوندهٔ شمارنده را حذف میکند:
<!doctype html>
<html lang="fa" dir="rtl">
<meta charset="utf-8">
<button id="add" type="button">افزودن</button>
<button id="stop" type="button">توقف شمارنده</button>
<output id="count" aria-live="polite">0</output>
<script type="module">
const add = document.querySelector('#add');
const count = document.querySelector('#count');
const counter = {
quantity: 0,
increment() {
this.quantity += 1;
count.textContent = String(this.quantity);
}
};
const handleAdd = counter.increment.bind(counter);
add.addEventListener('click', handleAdd);
document.querySelector('#stop').addEventListener('click', () => {
add.removeEventListener('click', handleAdd);
add.disabled = true;
}, { once: true });
</script>
</html>مرجع handleAdd را ذخیره کردیم. اگر هنگام حذف دوباره bind را صدا بزنید، تابع دیگری ساخته میشود و شنوندهٔ قبلی حذف نمیشود. در رابطهای واقعی همین اشتباه میتواند به اجرای چندبارهٔ یک عملیات پس از باز و بستهشدن پنجره منجر شود. در این مثال فقط شمارنده عوض میشود؛ ثبت سفارش یا پرداخت نباید بر یک کلیک نمایشی و عدد مرورگر متکی باشد.
Prototype برای اشتراک رفتار، داده برای هر نمونه
class Draft {
constructor(title) { this.title = title; }
rename(next) { this.title = next; }
}
const first = new Draft('تیکت اول');
const second = new Draft('تیکت دوم');
console.log(first.rename === second.rename); // true
first.rename('عنوان تازه');
console.log(second.title); // تیکت دوم
console.log(Object.getPrototypeOf(first) === Draft.prototype); // trueمتد rename روی prototype کلاس قرار دارد و رفتار مشترک است؛ عنوان هر نمونه، دادهٔ همان نمونه است. جستوجوی ویژگی میتواند در زنجیرهٔ prototype ادامه پیدا کند. این توضیح به خواندن کد کمک میکند، ولی دلیل کافی برای ساختن سلسلهمراتب طولانی ارثبری نیست. در پروژههای این دوره، تابعهای مدل و دادههای ساده اغلب انتخاب شفافتریاند. وقتی فقط یک تبدیل داده لازم دارید، کلاس تازه ممکن است بار ذهنی اضافه ایجاد کند.
همچنین هر چیزی که در زنجیره پیدا میشود، ویژگی مستقیم شیء نیست. هنگام خواندن دادهٔ ورودی، قرارداد فیلدهای مجاز را صریح بسازید؛ پخشکردن دادهٔ ناشناخته در تنظیمات حساس یا تکیه بر هر ویژگی قابل دسترس، طراحی قابل اتکایی نیست. جزئیات امنیت ورودی در درسهای بعد دوباره با مثال سرور بررسی میشود.
برای عیبیابی، روی نخستین خط متد breakpoint بگذارید و مقدار this را هنگام کلیک ببینید. سپس همان متد را مستقیم صدا بزنید و تفاوت را ثبت کنید. اگر متد از بیرون درست کار میکند ولی از رویداد شکست میخورد، این مقایسه مسیر بررسی را کوتاه میکند. تغییر دادن چندین قسمت کد پیش از مشاهدهٔ این تفاوت، علت واقعی را پنهان میکند. در تمرین خود نام دکمه و تعداد اتصالهای listener را هم یادداشت کنید تا مشکل اتصال چندباره با مشکل this مخلوط نشود.
تمرین و پاسخ تشریحی
یک پنجرهٔ پیشنویس بسازید که دکمهٔ ذخیرهاش یک متد شیء را اجرا کند. پنجره را سه بار باز و بسته کنید و ثابت کنید هر کلیک فقط یک بار ذخیره میکند. سپس همین رفتار را با تابع پوشانندهٔ پیکانی پیاده کنید و تفاوت مرجع شنونده را توضیح دهید.
پاسخ راهنما: یک تابع مشخص برای هر بار اتصال نگه دارید و هنگام بستن، همان مرجع را به removeEventListener بدهید. مقدار this و عمر شنونده را دو مسئلهٔ مستقل ببینید. معیار تحویل شامل شمارش دفعات اجرا، عدم تغییر نمونهٔ دوم و توضیح تفاوت obj.method() با callback جداشده است. با عدد خروجی و بازبینی مرجعها نتیجه را نشان دهید؛ صرفاً ناپدیدشدن پیام خطا کافی نیست.
منابع رسمی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
تألیف و مثالهای اختصاصی: تحریریه درسآوند.