۳۱ اوت ۲۰۲۲

Mixinها

در جاوااسکریپت ما فقط می‌توانیم از یک شیء ارث‌بری کنیم. فقط یک [[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 به خوبی فکر کنید تا احتمال اتفاق افتادن چنین چیزی را کم کنید.

نقشه آموزش