۲۵ ژوئیه ۲۰۲۲

ویژگی و متدهای ایستا

ما می‌توانیم یک متد را به تمام کلاس اختصاص دهیم. چنین متدهایی ایستا (static) نامیده می‌شوند.

در یک کلاس، آن‌ها با کلمه کلیدی static استفاده می‌شوند، مثل این:

class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

این کار دقیقا مانند این است که به طور مستقیم یک ویژگی را مقداردهی کنیم:

class User { }

User.staticMethod = function() {
  alert(this === User);
};

User.staticMethod(); // true

مقدار this درون فراخوانی User.staticMethod() برابر با کلاس سازنده یعنی خود User است (قانون «شیء قبل از نقطه»).

معمولا، متدهای ایستا برای پیاده‌سازی تابع‌هایی که به کل کلاس تعلق دارند و نه به هر شیء خاصی از آن استفاده می‌شوند.

برای مثال، ما شیءهای کلاس Article (به معنی مقاله) را داریم و به تابعی برای مقایسه آن‌ها نیاز داریم.

یک راه‌حل طبیعی اضافه کردن متد ایستای Article.compare است، مثلا اینگونه:

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

// کارایی
let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

articles.sort(Article.compare);

alert( articles[0].title ); // CSS

اینجا Article.compare در «بالای» مقاله‌ها (articles) قرار دارد، به عنوان روشی برای مقایسه آن‌ها. این متدی برای یک مقاله(article) نیست، بلکه برای کل کلاس است.

مثال دیگر متدی به نام “factory” (به معنی تولیدکننده) است.

فرض کنید، ما به چند راه برای ایجاد یک مقاله نیاز داریم:

  1. ساختن از طریق پارامترها (title، date و غیره).
  2. ساختن یک مقاله خالی با تاریخ امروز.
  3. …یا به روشی دیگر.

اولین راه می‌تواند با استفاده از تابع سازنده پیاده‌سازی شود. و برای راه دوم می‌توانیم یک متد ایستا برای کلاس بسازیم.

مانند Article.createTodays() در اینجا:

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // this = Article ،به یاد داشته باشید
    return new this("خلاصه‌ی امروز", new Date());
  }
}

let article = Article.createTodays();

alert( article.title ); // خلاصه‌ی امروز

حالا هر بار که نیاز داشته باشیم یک خلاصه از امروز بسازیم، می‌توانیم Article.createTodays() را فراخوانی کنیم. یکبار دیگر هم می‌گوییم، این متدی از مقاله (article) نیست بلکه متدی از کل کلاس است.

متدهای ایستا همچنین در کلاس‌های مربوط به پایگاه داده (database) برای جست‌وجو/ذخیره/حذف ورودی‌ها از پایگاه داده هم استفاده می‌شوند، مثلا اینگونه:

// کلاسی خاص برای مدیریت مقاله‌ها است Article با فرض اینکه
// :id متد ایستا برای حذف مقاله‌ها توسط
Article.remove({id: 12345});
متدهای ایستا برای شیءها قابل دسترس نیستند

متدهای ایستا بر روی کلاس‌های قابل فراخوانی هستند نه بر روی شیءها.

برای مثال، چنین کدی کار نخواهد کرد:

// ...
article.createTodays(); /// Error: article.createTodays is not a function

ویژگی‌های ایستا

به تازگی اضافه شده است
این قسمت به تازگی به زبان اضافه شده است. مثال‌ها درون Chrome اخیر کار می‌کنند.

می‌توانیم ویژگی‌های ایستا هم داشته باشیم، آن‌ها مانند ویژگی‌های معمولی کلاس بنظر می‌رسند اما قبل از آن‌ها static وجود دارد:

class Article {
  static publisher = "Ilya Kantor";
}

alert( Article.publisher ); // Ilya Kantor

درست مانند مقداردهی مستقیم به Article است:

Article.publisher = "Ilya Kantor";

ارث‌بری ویژگی‌ها و متدهای ایستا

ویژگی‌ها و متدهای ایستا به ارث برده می‌شوند.

برای مثال، در کد پایین Animal.compare و Animal.planet به ارث برده می‌شوند و به صورت Rabbit.compare و Rabbit.planet قابل دسترس هستند:

class Animal {
  static planet = "زمین";

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

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} با سرعت ${this.speed} می‌دود.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }

}

// Animal ارث‌بری از
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} قایم می‌شود!`);
  }
}

let rabbits = [
  new Rabbit("خرگوش سفید", 10),
  new Rabbit("خرگوش مشکی", 5)
];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // خرگوش مشکی با سرعت 5 می‌دود

alert(Rabbit.planet); // زمین

حالا زمانی که Rabbit.compare را فراخوانی می‌کنیم، Animal.compare که به ارث برده شده فراخوانی خواهد شد.

این چگونه کار می‌کند؟ دوباره، با استفاده از پروتوتایپ‌ها. همانطور که ممکن است از قبل حدس زده باشید، extends به کلاس Rabbit ویژگی [[Prototype]] می‌دهد که به Animal رجوع می‌کند.

پس Rabbit extends Animal دو رجوع [[Prototype]] می‌سازد:

  1. تابع Rabbit به صورت پروتوتایپی از تابع Animal ارث‌بری می‌کند.
  2. ویژگی Rabbit.prototype به صورت پروتوتایپی از Animal.prototype ارث‌بری می‌کند.

به عنوان یک نتیجه، ارث‌بری هم برای متدهای معمولی کار می‌کند و هم برای متدهای ایستا.

بفرمایید، بیایید این موضوع را با استفاده از کد بررسی کنیم:

class Animal {}
class Rabbit extends Animal {}

// برای ایستاها
alert(Rabbit.__proto__ === Animal); // true

// برای متدهای معمولی
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

خلاصه

متدهای ایستا برای عملکردی استفاده می‌شوند که «به صورت کامل» به کلاس تعلق دارد. این موضوع به یک نمونه موجود از کلاس مربوط نمی‌شود.

برای مثال، متدی برای مقایسه Article.compare(article1, article2) یا یک متد تولیدکننده Article.createTodays().

آن‌ها با کلمه static درون تعریف کلاس برچسب زده شده‌اند.

ویژگی‌های ایستا زمانی که ما می‌خواهیم داده‌هایی در سطح کلاس ذخیره کنیم و همچنین به یک نمونه از کلاس وابسته نباشند استفاده می‌شوند.

سینتکس آن:

class MyClass {
  static property = ...;

  static method() {
    ...
  }
}

از لحاظ فنی، تعریف کردن به صورت ایستا درست مانند مقداردهی به خود کلاس است:

MyClass.property = ...
MyClass.method = ...

ویژگی‌ها و متدهای ایستا به ارث برده می‌شوند.

برای class B extends A پروتوتایپ کلاس B خودش به A اشاره می‌کند: B.[[Prototype]] = A. پس اگر یک فیلد درون B پیدا نشد، جست‌وجو درون A ادامه می‌یابد.

تمارین

اهمیت: 3

همانطور که می‌دانیم، تمام شیءهای به صورت طبیعی از Object.prototype ارث‌بری می‌کنند و به متدهای «عموnode می» مثل hastOwnProperty و بقیه آن‌ها دسترسی دارند.

برای مثل:

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

let rabbit = new Rabbit("Rab");

// است Object.prototype از hasOwnProperty متد
alert( rabbit.hasOwnProperty('name') ); // true

اما اگر ما به طور واضح "class Rabbit extends Object" را بیان کنیم، سپس نتیجه از یک کلاس ساده "class Rabbit" متفاوت خواهد بود؟

تفاوت در چیست؟

اینجا مثالی از چنین کدی داریم (این کد کار نمی‌کند – چرا؟ آن را درست کنید):

class Rabbit extends Object {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // ارور

اول، بیایید ببینیم که چرا کد کار نمی‌کند.

اگر سعی کنیم که آن را اجرا کنیم دلیل واضح می‌شود. سازنده کلاس ارث‌بر باید super() را فراخوانی کند. در غیر این صورت "this" «تعریف شده» نخواهد بود.

حل این مشکل:

class Rabbit extends Object {
  constructor(name) {
    super(); // باید هنگام ارث‌بری سازنده والد را فراخوانی کنیم
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true

اما این هنوز تمام ماجرا نیست.

حتی بعد از رفع این مشکل، هنوز هم یک تفاوت اساسی بین "class Rabbit extends Object" و class Rabbit وجود دارد.

همانطور که می‌دانیم، سینتکس “extends” دو پروتوتایپ را تنظیم می‌کند:

  1. بین "prototype" سازنده تابع‌ها (برای متدها).
  2. بین خود سازنده تابع‌ها (برای متدهای ایستا).

در مورد class Rabbit extends Object یعنی:

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true

پس حالا Rabbit به دسترسی متدهای ایستای Object از طریق Rabbit را فراهم می‌کند، مثلا اینگونه:

class Rabbit extends Object {}

// را فراخوانی می‌کنیم Object.getOwnPropertyNames معمولا ما
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b

اما اگر ما extends Object را نداشته باشیم، سپس Rabbit.__proto__ برابر با Object نخواهد بود.

اینجا یک دمو قرار دارد:

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)
alert( Rabbit.__proto__ === Function.prototype ); // به صورت پیش‌فرض، مانند هر تابعی

// وجود ندارد Rabbit ارور، چنین تابعی درون
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // ارور

پس در این صورت Rabbit دسترسی به متدهای ایستای Object را فراهم نمی‌کند.

راستی، Function.prototype دارای متدهای «عمومی» تابع است، مثل call، bind و غیره. آن‌ها سرانجام در هر دو مورد در دسترس هستند چون سازنده درون‌ساخت Object، Object.__proto__ === Function.prototype را دارد.

اینجا تصویر آن را داریم:

پس، به طور خلاصه، دو تفاوت وجود دارد:

class Rabbit class Rabbit extends Object
باید super() را دورن سازنده فراخوانی کند
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object
نقشه آموزش