در جاوااسکریپت ما فقط میتوانیم از یک شیء ارثبری کنیم. فقط یک [[Prototype]]
برای هر شیء میتواند وجود داشته باشد. و یک کلاس فقط میتواند یک کلاس دیگر را تعمیم دهد.
اما گاهی اوقات این حس محدود بودن را دارد. برای مثال، ما کلاس StreetSweeper
و کلاس Bicycle
را داریم و میخواهیم ترکیب آنها را بسازیم: یک StreetSweepingBicycle
.
یا ما کلاس User
و کلاس EventEmitter
که پیادهسازی ایجاد رویداد (event) انجام میدهد را داریم و میخواهیم که عملکرد EventEmitter
را به User
اضافه کنیم تا کاربران ما بتوانند رویدادها را خارج کنند.
یک راهکار وجود دارد که اینجا به کمک میآید، به نام “mixins”.
همانطور که در ویکیپدیا تعریف شده است، یک mixin کلاسی شامل متدهایی است که میتوانند بدون نیاز به ارثبری از کلاس، توسط کلاسهای دیگر استفاده شوند.
به عبارتی دیگر، یک mixin متدهایی که یک کار مشخص انجام میدهند را فراهم میکند اما از آن به تنهایی استفاده نمیکنیم بلکه از آن برای اضافه کردن همان کار مشخص به کلاسهای دیگر استفاده میکنیم.
یک مثال mixin
سادهترین راه برای پیادهسازی یک mixin در جاوااسکریپت ایجاد شیءای شامل متدهایی مفید است تا بتوانیم به راحتی آنها را درون پروتوتایپ هر کلاسی ادغام کنیم.
برای مثال اینجا میکسین sayHiMixin
برای اضافه کردن «گفتار» به User
استفاده شده است:
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// :کاربرد
class User {
constructor(name) {
this.name = name;
}
}
// کپی کردن متدها
Object.assign(User.prototype, sayHiMixin);
// بگوید (hi) میتواند سلام User حالا
new User("Dude").sayHi(); // Hello Dude!
ارثبری در کار نیست، فقط یک کپی کردن متد ساده است. پس User
میتواند از کلاس دیگری ارثبری کند و همچنین mixin را هم شامل شود تا متدهای اضافی را «ترکیب» کند، مثل این:
class User extends Person {
// ...
}
Object.assign(User.prototype, sayHiMixin);
mixinها میتوانند درون خود از ارثبری استفاده کنند.
برای مثال، اینجا sayHiMixin
از sayMixin
ارثبری میکند:
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (برای تنظیم پروتوتایپ استفاده کنیم `Object.setPrototypeOf` یا میتوانستیم اینجا از)
sayHi() {
// فراخوانی متد والد
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
// کپی کردن متدها
Object.assign(User.prototype, sayHiMixin);
// بگوید (hi) میتواند سلام User حالا
new User("Dude").sayHi(); // Hello Dude!
لطفا توجه کنید که فراخوانی متد super.say()
از sayHiMixin
(در خطی که با (*)
برچسبگذاری شده) در پروتوتایپ mixin به دنبال متد میگردد نه کلاس.
اینجا شکل آن را داریم (قسمت راست را ببینید):
دلیلش این است که sayHi
و sayBye
از اول درون sayHiMixin
ایجاد شدهاند. پس حتی با اینکه کپی شدند، ویژگی درونی [[HomeObject]]
آنها به sayHiMixin
رجوع میکند، همانطور که در تصویر بالا نشان داده شده است.
چون super
درون [[HomeObject]].[[Prototype]]
به دنبال متدهای والد میگردد، یعنی sayHiMixin.[[Prototype]]
را جستوجو میکند.
EventMixin
حالا بیایید یک mixin برای دنیای واقعی بسازیم.
یک خاصیت مهم در تعداد زیادی از شیءهای مرورگر (برای مثال) این است که آنها میتوانند رویداد (event) ایجاد کنند. رویدادها راهی عالی برای «انتشار اطلاعات» به هر کسی که آن را بخواهد هستند. پس بیایید یک mixin بسازیم که به ما این امکان را میدهد تا به راحتی تابعهای مربوط به رویداد را به هر شیء/کلاسی اضافه کنیم.
- این mixin متد
.trigger(name, [...data])
را برای «ایجاد یک رویداد» زمانی که اتفاقی برای آن میافتد فراهم میکند. آرگومانname
اسم رویداد است که بعد از آن آرگومانهای اضافی اختیاری شامل دادۀ رویداد میآید. - همچنین متد
.on(name, handler)
را فراهم میکند که تابعhandler
را به عنوان کنترلکننده به رویدادهایی با نام داده شده اضافه میکند. این تابع زمانی که رویدادی همراه باname
داده شده راه میافتد (trigger) اجرا میشود و آرگومانها را از فراخوانی.trigger
دریافت میکند. - …و متد
.off(name, handler)
را هم فراهم میکند که کنترلکنندهhandler
را حذف میکند.
بعد از اضافه کردن mixin
، یک شیء user
خواهد توانست زمانی که بازدیدکننده وارد میشود (log in) یک رویداد "login"
ایجاد کند. و شیء دیگر، مثلا calendar
(تقویم) شاید بخواهد چنین رویدادهایی را کنترل کند تا تقویم را برای شخص وارد شده بارگیری کند.
یا یک menu
(فهرست) میتواند زمانی که چیزی از فهرست انتخاب شود رویداد "select"
(انتخاب) را ایجاد کند و شیءهای دیگر ممکن است کنترلکنندههایی را برای واکنش دادن به این رویداد داشته باشند. و مثالهایی دیگر.
اینجا کد آن را داریم:
let eventMixin = {
/**
* :مشترک شدن در یک رویداد، کاربرد
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* :لغو کردن اشتراک، استفاده
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* ایجاد یک رویداد همراه با داده و نام داده شده
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // کنترلکنندهای برای این نام رویداد وجود ندارد
}
// فراخوانی کنترلکنندهها
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
- متد
.on(eventName, handler)
– مشخص میکند که تابعhandler
هنگامی که رویدادی با این نام رخ میدهد اجرا شود. از لحاظ فنی، یک ویژگی_eventHandlers
وجود دارد که آرایهای از کنترلکنندهها را برای هر رویداد ذخیره میکند و این متد فقط کنترلکننده را به لیست اضافه میکند. - متد
.off(eventName, handler)
– تابع را از لیست کنترلکنندهها حذف میکند. - متد
.trigger(eventName, ...args)
– رویداد را ایجاد میکند: تمام کنترلکنندهها از_eventHandlers[eventName]
همراه با لیستی از آرگومانها...args
فراخوانی میشوند.
کاربرد:
// ایجاد یک کلاس
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// شامل متدهای مربوط به رویداد mixin اضافه کردن
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// :فراخوانی شود (select) اضافه کردن یک کنترلکننده، تا هنگام انتخاب
menu.on("select", value => alert(`Value selected: ${value}`));
// :رویداد را راه میاندازد => کنترلکننده بالا اجرا میشود و این را نمایش میدهد
// Value selected: 123
menu.choose("123");
حالا اگر ما بخواهیم هر کدی به انتخاب چیزی از فهرست واکنش نشان دهد، میتوانیم با menu.on(...)
آن را کنترل کنیم.
و eventMixin
اضافه کردن چنین رفتاری به هر چند کلاسی که بخواهیم را آسان میکند، بدون اینکه کاری به زنجیره ارثبری داشته باشیم.
خلاصه
Mixin – یک عبارت عام برنامهنویسی شیءگرا است: کلاسی که متدهایی را برای کلاسهای دیگر دربرمیگیرد.
بعضی از زبانهای دیگر ارثبری چندگانه را ممکن میسازند. جاوااسکریپت از ارثبری چندگانه پشتیبانی نمیکند اما با کپی کردن متدها درون پروتوتایپ mixinها میتوانند پیادهسازی شوند.
ما میتوانیم با اضافه کردن چند عملکرد، از mixinها به عنوان راهی برای قدرتمند کردن یک کلاس استفاده کنیم، مانند کنترل کردن رویداد که بالاتر آن را دیدیم.
اگر mixinها به طور تصادفی متدهای موجود در کلاس را بازنویسی کنند، ممکن است باعث ایجاد تناقض شوند. پس به طور کلی باید درباره نامگذاری متدهای یک mixin به خوبی فکر کنید تا احتمال اتفاق افتادن چنین چیزی را کم کنید.