در این درس میخوانید
گاهی چند شیء رفتار مشترک دارند؛ مثلاً چند یادگیرنده باید بتوانند پیشرفت خود را ثبت کنند. JavaScript رفتار مشترک شیءها را از راه prototype مدل میکند و class یک نحو روشن برای بسیاری از این الگوها فراهم میآورد. پیش از ساختن کلاس، مطمئن شوید مسئله واقعاً به داده و رفتار مرتبط نیاز دارد؛ هر تابع ساده به کلاس احتیاج ندارد. مثالهای این درس نهم مستقلاند و در مرورگر بهروز یا Node.js جدید اجرا میشوند.
this به شیوهٔ فراخوانی وابسته است
در یک تابع معمولی، this را صرفاً از محل نوشتهشدن تابع حدس نزنید؛ روش فراخوانی مهم است. وقتی تابع را به شکل object.method() صدا میزنیم، در این نمونه this همان object است. جداکردن متد و فراخوانی آن بهتنهایی، این ارتباط را حفظ نمیکند. تابع پیکانی this تازه نمیسازد و this محیط پیرامون را میگیرد؛ بنابراین برای یک متد وابسته به دریافتکننده، جایگزینی خودکار با arrow میتواند اشتباه باشد.
const progress = {
completed: 2,
next() {
this.completed += 1;
return this.completed;
}
};
console.log(progress.next()); // 3
const nextForProgress = progress.next.bind(progress);
console.log(nextForProgress()); // 4bind یک تابع با this مشخص میسازد و خود تابع اصلی را اجرا نمیکند. این روش هنگام دادن متد بهعنوان callback میتواند مفید باشد. راه دیگر، استفاده از wrapper مانند () => progress.next() است. این دو انتخاب در همهٔ جزئیات یکسان نیستند، اما هر دو در این سناریو دریافتکننده را روشن میکنند. از اتکا به رفتار this سراسری پرهیز کنید؛ strict mode، ماژول و محیط اجرا میتوانند نتیجه را تغییر دهند.
Prototype؛ رفتار مشترک از کجا میآید؟
هنگام خواندن یک ویژگی، اگر آن ویژگی روی خود شیء نباشد، موتور میتواند در زنجیرهٔ prototype جستوجو کند. این به معنای کپیکردن همهٔ متدها در هر نمونه نیست. زنجیره سرانجام به null میرسد و شیءهای ساختهشده با prototype برابر null هم ممکناند. prototype یک سازوکار زبان است، نه همان رابطهٔ والد و فرزند در DOM؛ این دو مفهوم را از هم جدا نگه دارید.
class LessonCounter {
constructor() { this.count = 0; }
increment() { this.count += 1; return this.count; }
}
const counter = new LessonCounter();
console.log(counter.increment()); // 1
console.log(Object.hasOwn(counter, "count")); // true
console.log(Object.hasOwn(counter, "increment")); // false
console.log(Object.getPrototypeOf(counter) === LessonCounter.prototype); // trueیک کلاس با وضعیت خصوصی
فیلد خصوصی با # تعریف میشود و دسترسی مستقیم بیرون کلاس به آن مجاز نیست. در مثال بعد، completed را فقط از راه completeOne تغییر میدهیم و از بیشترشدن آن از تعداد کل جلوگیری میکنیم. این پنهانسازی به منظمکردن قرارداد برنامه کمک میکند؛ جایگزین امنیت سمت سرور نیست. اگر نتیجهٔ دوره برای پرداخت یا صدور مدرک مهم باشد، محاسبهٔ مرورگر بهتنهایی مرجع اعتماد نخواهد بود.
class CourseProgress {
#completed = 0;
#total;
constructor(total) {
if (!Number.isInteger(total) || total < 1) {
throw new RangeError("total must be a positive integer");
}
this.#total = total;
}
completeOne() {
if (this.#completed < this.#total) this.#completed += 1;
return this.percent;
}
get percent() {
return Math.round(this.#completed / this.#total * 100);
}
}
const jsCourse = new CourseProgress(20);
console.log(jsCourse.completeOne()); // 5
console.log(jsCourse.percent); // 5getter شبیه یک ویژگی خوانده میشود؛ برای percent پرانتز نمیگذاریم. constructor هنگام new اجرا میشود و ورودی نامعتبر را رد میکند. متدهای class در strict mode اجرا میشوند؛ اگر completeOne را از نمونه جدا کنید و بدون دریافتکننده صدا بزنید، دسترسی به فیلد خصوصی معتبر نخواهد بود. برای callback از bind یا یک wrapper روشن استفاده کنید. فیلد خصوصی، همان ویژگی معمولی با نام رشتهایِ «#completed» نیست.
ارثبری یا ترکیب رفتار؟
extends و super برای ارثبری کلاسها وجود دارند، اما لازم نیست از اولین پروژه زنجیرهٔ بلند کلاس بسازید. اگر هدف فقط استفادهٔ دوباره از یک محاسبه است، یک تابع مستقل میتواند سادهتر باشد. اگر چند رفتار مستقل کنار هم لازماند، ترکیب شیءها یا تابعها ممکن است وابستگی کمتری ایجاد کند. انتخاب را بر اساس نیاز، آزمونپذیری و تغییرهای احتمالی انجام دهید، نه برای شبیهکردن JavaScript به زبان دیگری.
تمرین و پاسخ پیشنهادی
کلاس کوچکی بسازید که عنوان درس را نگه دارد و بتواند وضعیت خواندهشدن را تغییر دهد. وضعیت اولیه false باشد و پس از markRead برابر true شود. سپس همین متد را بهصورت callback مستقل اجرا کنید، بهطوری که اتصال به نمونه حفظ شود. پاسخ زیر از وضعیت خصوصی و wrapper استفاده میکند؛ برای تمرین دوم میتوانید wrapper را با bind جایگزین کنید.
class Lesson {
#read = false;
constructor(title) { this.title = title; }
markRead() { this.#read = true; }
get isRead() { return this.#read; }
}
const lesson = new Lesson("Prototype");
const finishLesson = () => lesson.markRead();
finishLesson();
console.log(lesson.isRead); // trueاشتباههای رایج
- حدسزدن this فقط از محل تعریف تابع معمولی.
- دادن متد جداشده به callback بدون توجه به دریافتکننده.
- فرض اینکه تمام ویژگیهای قابل خواندن، متعلق به خود نمونهاند.
- استفاده از کلاس برای هر محاسبهٔ کوچک و مستقل.
- درنظرگرفتن فیلد خصوصی مرورگر بهعنوان کنترل امنیتی سرور.
منابع انگلیسی
- MDN — Using classes
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_classes
- MDN — this
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
- MDN — Inheritance and the prototype chain
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
متن و مثالهای آموزشی برای این مجموعه تدوین شدهاند. از درس بعد، منطق زبان را به ساختن و تغییر صفحهٔ وب با DOM وصل میکنیم.
پیشنیاز و محیط اجرای مثالها
درس 9 از ۲۰؛ پیشنیاز: درسهای قبلی مجموعه. کدهای مستقل این درس در Console تازهٔ مرورگر یا فایل JavaScript اجرا میشوند؛ نمونهها را جداگانه آزمایش کنید.
پیشرفت روی همین مرورگر ذخیره میشود.