۱۵ دسامبر ۲۰۲۱

سینتکس پایه کلاس

در برنامه‌نویسی شیء‌گرا، کلاس یک الگوی-کد-برنامه قابل توسعه برای ایجاد اشیاء، ارائه مقادیر اولیه برای حالت (متغیرهای عضو) و پیاده‌سازی رفتار (توابع یا روش‌های عضو) است.

ویکی‌پدیا

در عمل، ما اغلب نیاز به ایجاد بسیاری از اشیاء از یک نوع، مانند کاربران، یا کالاها یا هر چیز دیگری داریم.

همانطور که قبلاً از فصل سازنده، عملگر "new" می‌دانیم، new function می‌تواند در این مورد کمک کند.

اما در جاوا‌اسکریپت مدرن، ساختار «کلاس» پیشرفته‌تری وجود دارد که ویژگی‌های جدید بسیار خوبی را معرفی می‌کند که برای برنامه‌نویسی شیء‌گرا مفید هستند.

سینتکس «کلاس»

سینتکس پایه اینگونه است:

class MyClass {
  // متد‌های کلاس
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

سپس از new MyClass() برای ایجاد یک شیء جدید با تمام متدهای فهرست شده استفاده کنید.

متد constructor() به طور خودکار توسط new فراخوانی می‌شود، بنابراین می‌توانیم شیء را در آنجا مقداردهی اولیه کنیم.

برای مثال:

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// Usage:
let user = new User("John");
user.sayHi();

وقتی new User("John") فراخوانی می‌شود:

  1. یک شیء جدید ایجاد می‌شود.
  2. constructor با آرگومان داده شده اجرا می‌شود و آن را به this.name اختصاص می‌دهد.

…سپس ما می‌توانیم متد‌های شیء را فراخوانی کنیم، مانند user.sayHi().

بین متدهای کلاس کاما وجود ندارد

یک مشکل رایج برای توسعه دهندگان تازه کار این است که یک کاما بین متدهای کلاس قرار می‌دهند که منجر به یک خطای سینتکس می‌شود.

نماد در اینجا نباید با شیء لفظی اشتباه شود. در کلاس، نیازی به کاما نیست.

یک کلاس چیست؟

بنابراین، class دقیقاً چیست؟ این یک موجودیت کاملاً جدید در سطح زبان نیست، همانطور که ممکن است تصور شود.

بیایید از هر جادویی رونمایی کنیم و ببینیم کلاس واقعاً چیست. این به درک بسیاری از جنبه‌های پیچیده کمک می‌کند.

در جاوااسکریپت کلاس نوعی تابع است.

در اینجا، نگاهی بیندازیم:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// اثبات: کاربر یک تابع است
alert(typeof User); // تابع

کاری که ساختار class User {...} واقعا انجام می‌دهد این است:

  1. تابعی به نام User ایجاد می‌کند که نتیجه تعریف کلاس می‌شود. کد تابع از متد سازنده گرفته شده است (اگر چنین روشی را ننویسیم خالی فرض می‌شود).
  2. متدهای کلاس، مانند sayHi را در User.prototype ذخیره می‌کند.

پس از ایجاد شیء new User، وقتی متد آن را فراخوانی می‌کنیم، از نمونه اولیه گرفته می‌شود، درست همانطور که در فصل F.prototype توضیح داده شد. بنابراین شیء به متدهای کلاس دسترسی دارد.

ما می‌توانیم نتیجه تعریف class User را به صورت زیر نشان دهیم:

این کد برای درون‌یابی آن است:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// کلاس یک تابع است
alert(typeof User); // تابع

// ... یا به طور دقیق تر، متد سازنده
alert(User === User.prototype.constructor); // true

// :هستند، به عنوان مثال User.prototype متدها در
alert(User.prototype.sayHi); // sayHi کد متد

// دقیقا دو متد در پروتوتایپ وجود دارد
alert(Object.getOwnPropertyNames(User.prototype)); // sayHi ،سازنده

فقط یک سینتکس برای زیباکردن(syntactic sugar) نیست

گاهی اوقات مردم می‌گویند که class یک «سینتکس برای زیباکردن(syntactic sugar)» است (سینتکسی که برای خواندن ساده‌تر طراحی شده است، اما چیز جدیدی معرفی نمی‌کند)، زیرا در واقع می‌توانیم همان را بدون کلمه کلیدی class تعریف کنیم:

// بازنویسی کلاس کاربر در توابع خالص

// 1. ایجاد تابع سازنده
function User(name) {
  this.name = name;
}
// یک پروتوتایپ تابع به طور پیش‌فرض دارای ویژگی "سازنده" است،
// پس ما نیاز نداریم که آن را ایجاد کنیم

// 2. اضافه کردن متد به پروتوتایپ
User.prototype.sayHi = function() {
  alert(this.name);
};

// Usage:
let user = new User("John");
user.sayHi();

نتیجه این تعریف تقریباً یکسان است. بنابراین، در واقع دلایلی وجود دارد که می‌توان class را یک سینتکس برای زیباکردن(syntactic sugar) برای تعریف سازنده همراه با روش‌های نمونه اولیه آن در نظر گرفت.

با این حال، تفاوت‌های مهمی وجود دارد.

  1. ابتدا، یک تابع ایجاد شده توسط class توسط یک ویژگی داخلی ویژه [[IsClassConstructor]]: true برچسب گذاری می‌شود:. بنابراین کاملاً مشابه ایجاد آن به صورت دستی نیست.

    زبان آن ویژگی را در مکان‌های مختلف بررسی می‌کند. به عنوان مثال، بر خلاف یک تابع معمولی، باید با new فراخوانی شود:

    class User {
      constructor() {}
    }
    
    alert(typeof User); // function
    User(); // خطا: سازنده کلاس کاربر را نمی‌توان بدون 'new' فراخوانی کرد

    همچنین، نمایش رشته‌ای از سازنده کلاس در اکثر موتورهای جاوا‌اسکریپت با “کلاس…” شروع می‌شود.

    class User {
      constructor() {}
    }
    
    alert(User); // class User { ... }

    تفاوت‌های دیگری نیز وجود دارد، به زودی آنها را خواهیم دید.

  2. روش‌های کلاس غیرقابل شمارش هستند. یک تعریف کلاس، پرچم enumerable را برای همه متدهای "prototype"روی false تنظیم می‌کند.

    این خوب است، زیرا اگر for..in را روی یک شیء قرار دهیم، معمولاً متد‌های کلاس آن را نمی‌خواهیم.

  3. کلاس‌ها همیشه از use strict استفاده می‌کنند. تمام کدهای داخل ساختار کلاس به طور خودکار در حالت سخت قرار می‌گیرند.

علاوه بر این، سینتکس class بسیاری از ویژگی‌های دیگر را به همراه دارد که بعداً بررسی خواهیم کرد.

بیان کلاس

درست مانند توابع، کلاس‌ها را می‌توان در یک عبارت دیگر تعریف کرد، ارسال کرد، برگرداند، تخصیص داد و غیره.

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

let User = class {
  sayHi() {
    alert("سلام");
  }
};

شبیه به عبارت‌های تابع نام‌گذاری شده، عبارات کلاس ممکن است یک نام داشته باشند.

اگر یک عبارت کلاس دارای نام باشد، فقط در داخل کلاس قابل مشاهده است:

// «عبارت کلاس نامگذاری شده»
// (چنین اصطلاحی در مشخصات وجود ندارد، اما شبیه به عبارت تابع نامگذاری شده است)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // فقط در داخل کلاس قابل مشاهده است MyClass نام
  }
};

new User().sayHi(); // را نشان می‌دهد MyClass کار می کند، تعریف

alert(MyClass); // خارج از کلاس قابل مشاهده نیست MyClass خطا، نام

ما حتی می‌توانیم کلاس‌ها را به صورت پویا “بر اساس تقاضا” بسازیم، مانند این:

function makeClass(phrase) {
  // یک کلاس تعریف کنید و آن را برگردانید
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// Create a new class
let User = makeClass("سلام");

new User().sayHi(); // سلام

گیرنده/ تنظیم کننده

درست مانند اشیاء تحت اللفظی، کلاس‌ها ممکن است شامل گیرنده‌ها/تنظیم کننده‌ها، خصوصیات محاسبه شده و غیره باشند.

در اینجا یک مثال برای user.name است که با استفاده از get/set پیاده‌سازی شده است:

class User {

  constructor(name) {
    // تنظیم کننده را فراخوانی می‌کند
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("نام بسیار کوتاه است.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // .نام بسیار کوتاه است

از نظر فنی، چنین فراخوانی کلاس با ایجاد گیرنده و تنظیم کننده در User.prototype کار می‌کند.

نام‌های محاسبه شده […]

در اینجا یک مثال با نام متد محاسبه شده با استفاده از براکت [...] آورده شده است:

class User {

  ['say' + 'Hi']() {
    alert("سلام");
  }

}

new User().sayHi();

به خاطر سپردن چنین ویژگی‌هایی آسان است، زیرا آنها شبیه به اشیاء تحت اللفظی هستند.

فیلدهای کلاس

مرورگرهای قدیمی ممکن است به polyfill نیاز داشته باشند

فیلدهای کلاس اخیرا به زبان اضافه شده هستند.

قبلا، کلاس‌های ما فقط متد داشتند.

“فیلدهای کلاس” سینتکسی است که اجازه می‌دهد هر ویژگی را اضافه کنید.

برای مثال، اجازه دهید ویژگی name را به class User اضافه کنیم:

class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

بنابراین، ما فقط “ = ” را در فراخوانی می‌نویسیم، و تمام.

تفاوت مهم فیلدهای کلاس در این است که آنها بر روی اشیاء جداگانه تنظیم می‌شوند، نه User.prototype:

class User {
  name = "John";
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined

همچنین می‌توانیم با استفاده از عبارات و فراخوانی‌های توابع پیچیده‌تر، مقادیری را اختصاص دهیم:

class User {
  name = prompt("نام، لطفا؟", "John");
}

let user = new User();
alert(user.name); // John

ساخت متدهای محدود با فیلدهای کلاس

همانطور که در فصل نشان داده شد توابع پیوند تابع در جاوا‌اسکریپت دارای this پویا هستند. بستگی به زمینه فراخوانی دارد.

بنابراین، اگر یک متد شیء منتقل شود و در زمینه دیگری فراخوانی شود، this دیگر ارجاعی به شیء آن نخواهد بود.

به عنوان مثال، این کد undefined را نشان می‌دهد:

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("سلام");

setTimeout(button.click, 1000); // undefined

مشکل “از دست دادن this” نامیده می‌شود.

همانطور که در فصل پیوند تابع مورد بحث قرار گرفت، دو روش برای رفع آن وجود دارد:

  1. یک تابع دربرگیرنده مانند setTimeout(() => button.click(), 1000) را عبور دهید.
  2. متد را به شیء متصل کنید، به عنوان مثال. در سازنده

فیلدهای کلاس، سینتکس بسیار ظریف دیگری را ارائه می‌دهند:

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("سلام");

setTimeout(button.click, 1000); // سلام

فیلد کلاس click = () => {...} بر اساس هر شیء ایجاد می‌شود، یک تابع جداگانه برای هر شیء Button وجود دارد که this در داخل آن به آن شیء ارجاع می‌دهد. ما می‌توانیم button.click را به هر‌جایی منتقل کنیم، و مقدار this همیشه درست خواهد بود.

این به ویژه در محیط مرورگر، برای شنوندگان رویداد مفید است.

خلاصه

سینتکس کلاس پایه به شکل زیر است:

class MyClass {
  prop = value; // ویژگی

  constructor(...) { // سازنده
    // ...
  }

  method(...) {} // متد

  get something(...) {} // متد گیرنده
  set something(...) {} // متد تنظیم کننده

  [Symbol.iterator]() {} // (Symbol در اینجا) متد با نام محاسبه شده
  // ...
}

MyClass از نظر فنی یک تابع است (توابعی که ما به عنوان constructor ارائه می‌کنیم)، در حالی که متدها، دریافت کننده‌ها و تنظیم کننده‌ها در MyClass.prototype نوشته می‌شوند.

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

تمارین

اهمیت: 5

کلاس Clock (به جعبه شنی مراجعه کنید) به سبک تابع نوشته شده است. آن را در سینتکس “کلاس” بازنویسی کنید.

ضمیمه: ساعت در کنسول تیک‌‌تاک می‌کند، آن را باز کنید تا ببینید.

باز کردن یک sandbox برای تمرین.

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}


let clock = new Clock({template: 'h:m:s'});
clock.start();

باز کردن راه‌حل درون sandbox.

نقشه آموزش