مقدمه‌ای بر برنامه نویسی شئ گرا در JavaScript: آبجکت‌ها، نمونه‌های اولیه و کلاس‌ها

13 خرداد 1398, خواندن در 10 دقیقه

در بسیاری از زبان‌های برنامه‌نویسی، کلاس‌ها یه مفهوم به خوبی تعریف شده هستند. در JavaScript به این صورت نیست، یا حداقل می‌توان گفت که در گذشته نبود. اگر برنامه‌نویسی شئ گرا و JavaScript را جستجو کنید، با تعداد زیادی مقاله، و با دستور العمل‌های مختلفی برای نحوه پیاده‌سازی یک کلاس در JavaScript‌ مواجه خواهید شد.

آیا راه ساده‌ای برای تعریف یک کلاس در JavaScript وجود دارد؟ و اگر دارد، چرا این همه دستور العمل مختلف برای آن وجود دارند؟

قبل از پاسخ دادن به آن سوالات، بیایید بهتر درک کنیم که یک آبجکت JavaScript چیست.

آبجکت‌ها در JavaScript

بیایید با یک مثال خیلی ساده شروع کنیم:

const a = {};
a.foo = 'bar';

در قطعه کد بالا، یک آبجکت ساخته شده، و با یک ویژگی foo افزایش یافته است. قابلیت اضافه کردن چیزهای دیگر به یک آبجکت از پیش موجود، چیزی است که JavaScript را از زبان‌های کلاسیک دیگر مانند Java متمایز می‌کند.

اگر بخواهیم بیشتر به جزئیات وارد شویم، باید بگوییم این که یک آبجکت می‌‌تواند افزایش یابد، باعث می‌‌‌شود که بتوانیم یک کلاس ضمنی بسازیم، بدون این که نیاز باشد در واقع آن کلاس را اصلا بسازیم. بیایید این مفهوم را با یک مثال واضح‌تر کنیم.

function distance(p1, p2) {
  return Math.sqrt(
    (p1.x - p2.x) ** 2 +
    (p1.y - p2.y) ** 2
  );
}

distance({x:1,y:1},{x:2,y:2});

در مثال بالا، من به یک کلاس نقطه برای ساخت یک نقطه نیاز نداشتم؛ بلکه فقط یک نمونه آبجکت را با اضافه کردن ویژگی‌های x و y‌ افزایش دادم. تابع distance اهمیتی نمی‌دهد که آرگومان‌ها یک نمونه کلاس نقطه هستند یا نه. تا زمانی که شما تابع distance را با دو آبجکت که ویژگی‌های x و y‌ از نوع number‌ را دارند فراخوانی کنید، به درستی کار خواهد کرد. این مفهوم، چیزی به نام duck typing می‌باشد.

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

const point1 = {
  x: 1,
  y: 1,
  toString() {
    return `(${this.x},${this.y})`;
  }
};
const point2 = {
  x: 2,
  y: 2,
  toString() {
    return `(${this.x},${this.y})`;
  }
};

این بار، آبجکتی که  یک نقطه دو بعدی را نمایش می‌دهد یک متد toString() دارد. در مثال بالا، کد toString تکرار شده است، و این مسئله خوب نیست.

راه‌های زیادی برای جلوگیری از این تکرار وجود دارند، و در واقع در مقاله‌‌های مختلف درباره آبجکت‌ها و کلاس‌های JavaScript، راه حل‌های مختلفی خواهید یافت. آیا تا به حال چیزی درباره «الگوی آشکار کردن ماژول» شنیده‌اید؟ این عبارت شامل کلمات «الگو» و «آشکار کردن» می‌باشد، همچنین جالب به نظر می‌رسد و کلمه «ماژول» هم در آن اجباری است. پس حتما این باید راه یک صحیح برای ساخت یک آبجکت باشد، اما اینطور نیست. الگوی آشکار کردن ماژول می‌تواند در برخی موارد انتخاب صحیحی باشد، اما قطعا راه پیشفرض ساخت آبجکت‌ها نیست.

حال ما آماده‌ایم که کلاس‌ها را معرفی کنیم.

کلاس‌ها در JavaScript

یک کلاس چیست؟ طبق گفته یک لغت نامه: «یک مجموعه یا دسته از چیزهایی که یک ویژگی یا صفت مشترک دارند و از نظر نوع یا کیفیت با هم تفاوت دارند.»

ما معمولا در زبان‌های برنامه‌نویسی می‌گوییم: «یک آبجکت، یک نمونه کلاس است.» این یعنی من با استفاده از یک کلاس، می‌توانم چند آبجکت بسازم که متدها و ویژگی‌های مشترکی دارند.

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

خوشبختانه ECMAScript 6 کلمه کلیدی class را فراهم می‌کند، که باعث می‌شود ساخت یک کلاس ساده‌تر شود:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return `(${this.x},${this.y})`;
  }
}

پس به نظر من این بهترین راه برای تعریف کلاس‌ها در JavaScript است. کلاس‌ها اغلب به وراثت مرتبط هستند:

class Point extends HasXY {
  constructor(x, y) {
    super(x, y);
  }

  toString() {
    return `(${this.x},${this.y})`;
  }
}

همانطور که می‌توانید در مثال بالا ببینید، استفاده از کلمه کلیدی extends برای گسترش یک کلاس دیگر کافی است.

شما می‌توانید با استفاده از عملگر new یک آبجکت را از یک کلاس بسازید:

const p = new Point(1,1);
console.log(p instanceof Point); // مقدار «صحیح» را چاپ می‌کند

یک راه شئ گرای خوب برای تعریف کلاس‌ها، باید این موارد را فراهم کند:

  • یک سینتکس ساده برای تعریف یک کلاس.
  • یک راه ساده برای دسترسی به نمونه فعلی، یا this.
  • یک سینتکس ساده برای گسترش یک کلاس.
  • یک راه ساده برای دسترسی به نمونه ابر کلاس، یا super.
  • احتمالا یک راه ساده برای تشخیص این که یک آبجکت نمونه‌ای از یک کلاس خاص است یا نه. اگر آن آبجکت یک نمونه از آن کلاس است، Obj instanceof AClass باید true را برگرداند.

سینتکس class جدید تمام موارد بالا را فراهم می‌کند.

قبل از معرفی کلمه کلیدی class، راه تعریف یک کلاس در JavaScript چه بود؟

به علاوه، یک کلاس در JavaScript در واقع چیست؟ چرا اغلب درباره نمونه‌های اولیه صحبت می‌کنیم؟

کلاس‌ها در JavaScript 5

طبق گفته صفحه Mozilla MDN درباره کلاس‌ها:

«کلاس‌های JavaScript که در ECMAScript 2015 معرفی شدند، در درجه اول ساده‌تر از وراثت‌های بر پایه نمونه اولیه JavaScript هستند. سینتکس class یک مدل وراثت شئ گرای جدید را به JavaScript معرفی نمی‌کند.»

در اینجا مفهوم کلیدی «وراثت بر پایه نمونه اولیه» است. از آنجایی که برداشت‌های اشتباه زیادی درباره این نوع وراثت وجود دارد، من قدم به قدم پیش رفته، و از کلمه کلیدی class به function می‌روم.

class Shape {}
console.log(typeof Shape);
// تابع را چاپ می‌کند

به نظر می‌رسد که class و function با هم مرتبط هستند. آیا class فقط یک نام مستعار برای function می‌باشد؟ خیر!

Shape(2);
// Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

پس به نظر می‌رسد که افرادی که کلمه کلیدی class را معرفی کردند، می‌خواستند به ما بگویند که یک کلاس، تابعی است که باید با استفاده از عملگر new فراخوانی شود.

var Shape = function Shape() {} // Or just function Shape(){}
var aShape = new Shape();
console.log(aShape instanceof Shape);
// مقدار «صحیح» را چاپ می‌کند

مثال بالا به ما نشان می‌دهد که می‌توانیم از function برای تعریف یک کلاس استفاده کنیم. گرچه نمی‌توانیم کاربر را مجبور کنیم که با استفاده از عملگر new تابع را فراخوانی کند. اگر عملگر new برای فراخوانی تابع استفاده نشده بود، میتوان یک exception را نمایش داد.

به هر حال پیشنهاد می‌کنم که این بررسی را در هر تابعی که به عنوان یک کلاس عمل می‌کند قرار ندهید. در عوض از این قرارداد استفاده کنید: هر تابعی که نامش با یک حرف بزرگ شروع می‌شود، یک کلاس است و باید با استفاده از عملگر new فراخوانی شود.

بیایید ادامه دهیم و ببینیم که یک نمونه اولیه (prototype) چیست:

class Shape {
  getName() {
    return 'Shape';
  }
}
console.log(Shape.prototype.getName);
// prints function getName() ...

هر زمان که شما یک متد را داخل یک کلاس تعریف می‌کنید، شما در واقع آن متد را به نمونه اولیه تابع متناظر اضافه می‌کنید. معادل آن در JavaScript 5 به این صورت است:

function Shape() {}
Shape.prototype.getName = function getName() {
  return 'Shape';
};
console.log(new Shape().getName()); // prints Shape

گاهی اوقات به کلاس - تابع‌ها constructor گفته می‌شود؛ زیرا به عنوان سازنده‌هایی (constructorهایی) در یک کلاس معمولی عمل می‌کنند.

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

class Point {
  static distance(p1, p2) {
    // ...
  }
}

console.log(Point.distance); // prints function distance
console.log(Point.prototype.distance); // prints undefined

از آنجایی که متدهای استاتیک یک رابطه ۱ به ۱ با کلاس‌ها دارند، تابع استاتیک به constructor - تابع اضافه می‌شود، نه به نمونه اولیه.

بیایید تمام این مفاهیم را در یک مثال ساده خلاصه کنیم:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function toString() {
  return '(' + this.x + ',' + this.y + ')';
};

Point.distance = function distance() {
  // ...
}

console.log(new Point(1,2).toString()); // prints (1,2)
console.log(new Point(1,2) instanceof Point); // prints true

تا به اینجا، ما یک راه ساده برای انجام این کارها یافته‌ایم:

  • تعریف یک تابع که به عنوان یک کلاس عمل می‌کند.
  • دسترسی به نمونه کلاس با استفاده از کلمه کلیدی this.
  • ساخت آبجکت‌هایی که در واقع نمونه‌ای از آن کلاس هستند. (new Point(1, 2) instanceof Point مقدار true را بر می‌گرداند)

اما وراثت چه؟ دسترسی به ابر کلاس چه؟

class Hello {
  constructor(greeting) {
    this._greeting = greeting;
  }

  greeting() {
    return this._greeting;
  }
}

class World extends Hello {
  constructor() {
    super('hello');
  }

  worldGreeting() {
    return super.greeting() + ' world';
  }
}

console.log(new World().greeting()); // مقدار «سلام» را چاپ می‌کند
console.log(new World().worldGreeting()); // مقدار «سلام دنیا» را چاپ می‌کند

مثال بالا، یک مثال ساده از وراثت با استفاده از ECMAScript 6 است، و در زیر همان مثال با استفاده از «وراثت نمونه اولیه» را مشاهده می‌نمایید:

function Hello(greeting) {
  this._greeting = greeting;
}

Hello.prototype.greeting = function () {
  return this._greeting;
};


function World() {
  Hello.call(this, 'hello');
}

// ابر نمونیه اولیه را کپی می‌کند
World.prototype = Object.create(Hello.prototype);

// ویژگی‌ سازنده را مجبور می‌کند که به کلاس زیر مجموعه ارجاع کند
World.prototype.constructor = World;

World.prototype.worldGreeting = function () {
  const hello = Hello.prototype.greeting.call(this);
  return hello + ' world';
};

console.log(new World().greeting()); // مقدار «سلام» را چاپ می‌کند
console.log(new World().worldGreeting()); // مقدار «سلام دنیا» را چاپ می‌کند

این راه تعریف کلاس‌ها هم در Mozilla MDN پیشنهاد شده است.

ما با استفاده از سینتکس class، متوجه شدیم که ساخت کلاس‌ها شامل دستکاری نمونه اولیه یک تابع می‌باشد. اما چرا به این صورت است؟ برای پاسخ دادن به این سوال باید کاری که عملگر new انجام می‌دهد را درک کنیم.

عملگر new در JavaScript

عملگر new به خوبی در صفحه Mozilla MDN توضیح داده شده است. اما من می‌توانم یک مثال نسبتا ساده برای شما فراهم کننم که کار عملگر new را تقلید می‌کند:

function customNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  const result = constructor.call(obj, ...args);

  return result instanceof Object ? result : obj;
}

function Point() {}
console.log(customNew(Point) instanceof Point); // مقدار «صحیح» را چاپ می‌کند

دقت کنید که الگوریتم new واقعی پیچیده‌تر است. هدف مثال بالا فقط این است که توضیح دهد اگر از عملگر new استفاده کنید، چه اتفاقی می‌افتد.

وقتی که شما new Point(1, 2) را بنویسید، این اتفاق می‌افتد:

  • نمونه اولیه Point برای ساخت یک آبجکت استفاده می‌شود.
  • سازنده تابع فراخوانی می‌شود و آبجکت ساخته شده به عنوان زمینه (یا this) به همراه آرگومان‌های دیگر منتقل می‌شود.
  • اگر constructor یک آبجکت را برگرداند، این آبجکت نتیجه آبجکت جدید بوده، و در غیر این صورت آبجکت ساخته شده از نمونه اولیه، نتیجه نهایی است.

پس معنای «وراثت نمونه اولیه» چیست؟ یعنی این که شما می‌توانید آبجکت‌هایی بسازید که تمام ویژگی‌های نمونه اولیه تابعی که با عملگر new فراخوانی شده بود را به ارث می‌برند.

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

برنامه نویسی تابعی

گاهی اوقات مردم می‌گویند که JavaScript خیلی برای برنامه‌نویسی شئ گرا مناسب نیست، و شما باید در عوض از برنامه‌نویسی تابعی استفاده کنید.

من با این که JavaScript برای برنامه‌نویسی شئ گرا مناسب نیست موافق نیستم، اما فکر می‌کنم برنامه‌نویسی تابعی یک راه خوب برای برنامه‌نویسی است.

نتیجه گیری

وقتی که امکانش هست، از سینتکس class در ECMAScript 6 استفاده کنید:

class Point {
  toString() {
    //...
  }
}

یا این که از نمونه‌های اولیه تابع برای تعریف کلاس‌ها در ECMAScript 5 استفاده کنید:

function Point() {}
Point.prototype.toString = function toString() {
  // ...
}

امیدوارم که از خواندن این مقاله لذت برده باشید.

مقالات مرتبط:

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان کاکایی @er79ka
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو