کنترل هوشمند خواندن و نوشتن Property: Getter و Setter

مقدمه

در فصل Object Model، هنگام بررسی Class، به‌طور مختصر با Getter و Setter آشنا شدیم. در این درس، این دو مفهوم را با جزئیات کامل بررسی می‌کنیم و رابطه‌ی آن‌ها را با Property Descriptors که در درس قبل دیدیم روشن می‌سازیم. Getter و Setter به ما اجازه می‌دهند رفتار خواندن یا نوشتن یک Property را کاملاً سفارشی کنیم؛ بدون اینکه از بیرون، تفاوتی با یک Property معمولی به‌نظر برسد.

مشکل: Property های معمولی هیچ منطقی ندارند

یک Property معمولی، فقط یک مقدار خام ذخیره می‌کند؛ بدون هیچ امکانی برای اجرای منطق اضافی هنگام خواندن یا نوشتن آن:

let circle = {
  radius: 5
};

circle.radius = -10; // هیچ اعتبارسنجی‌ای انجام نمی‌شود

Getter — تعریف یک Property محاسبه‌شده

با کلیدواژه‌ی get در یک Object Literal، می‌توان Property ای تعریف کرد که هر بار خوانده شود، مقدار آن از طریق یک تابع محاسبه می‌شود:

تعریف یک Getter در Object Literal
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

با اینکه area بدون پرانتز خوانده می‌شود (circle.area، نه circle.area())، دقیقاً مثل یک Property معمولی، اما در پس‌زمینه هر بار که خوانده شود، تابع Getter دوباره اجرا می‌شود. به همین دلیل، وقتی radius تغییر می‌کند، خواندن مجدد circle.area بدون هیچ کار اضافه‌ای، مقدار به‌روزشده را برمی‌گرداند؛ چون این مقدار هرگز ذخیره نشده، بلکه هر بار از نو محاسبه می‌شود.

Setter — کنترل نوشتن یک Property

با کلیدواژه‌ی set، می‌توان منطقی برای زمانی که چیزی به یک Property نسبت داده می‌شود، تعریف کرد:

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

تحلیل رفتار جاوااسکریپت

در این مثال، نام Property داخلی واقعی عمداً به _radius تغییر کرده (با یک آندرلاین ابتدایی، که فقط یک قرارداد نام‌گذاری رایج است، نه یک قانون رسمی زبان مثل # در فصل قبل)، تا بین «Property واقعی که مقدار را نگه می‌دارد» و «Getter/Setter به نام radius» تداخل نامی رخ ندهد. وقتی circle.radius = -10 اجرا می‌شود، به‌جای نوشتن مستقیم مقدار، تابع Setter فراخوانی می‌شود؛ این تابع بررسی می‌کند که مقدار منفی است، پیام خطا را چاپ می‌کند و return می‌شود بدون اینکه _radius واقعاً تغییر کند. به همین دلیل، تلاش اول برای تغییر مقدار، بی‌اثر می‌ماند و circle.radius همچنان 5 باقی می‌ماند؛ اما تلاش دوم با مقدار معتبر (20) با موفقیت اعمال می‌شود.

Getter و Setter در Class

همان‌طور که در فصل قبل دیدیم، Class نحوی خواناتر برای همین الگو ارائه می‌دهد:

class Circle {
  constructor(radius) {
    this._radius = radius;
  }

  get area() {
    return Math.PI * this._radius * this._radius;
  }

  set radius(value) {
    if (value > 0) {
      this._radius = value;
    }
  }

  get radius() {
    return this._radius;
  }
}

این ساختار، از نظر رفتاری، دقیقاً معادل نسخه‌ی Object Literal است؛ فقط داخل Class نوشته شده است.

رابطه‌ی Getter/Setter با Property Descriptors

در درس قبل، Property های معمولی را که value داشتند بررسی کردیم؛ به این نوع Property، اصطلاحاً Data Property می‌گویند. اما Property ای که Getter یا Setter دارد، از نوع دیگری است به نام Accessor Property؛ چنین Property ای، به‌جای value و writable، از get و set در Descriptor خودش استفاده می‌کند:

بررسی Descriptor یک Accessor Property
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

خروجی این کد به‌جای value و writable که در درس قبل برای Data Property دیدیم، شامل get (ارجاع به تابع Getter)، set (که چون Setter تعریف نشده، undefined است)، و همچنان enumerable و configurable است. این نشان می‌دهد که Accessor Property و Data Property، دو دسته‌ی کاملاً متفاوت از Property Descriptor در جاوااسکریپت هستند؛ یک Property نمی‌تواند هم‌زمان هم value و هم get/set داشته باشد.

تعریف Getter/Setter با Object.defineProperty

همان ابزاری که در درس قبل برای تنظیم writable و enumerable دیدیم، برای تعریف Accessor Property هم قابل استفاده است:

let user = { firstName: "علی", lastName: "رضایی" };

Object.defineProperty(user, "fullName", {
  get: function () {
    return this.firstName + " " + this.lastName;
  },
  enumerable: true
});

console.log(user.fullName); // علی رضایی

چه زمانی از Getter/Setter استفاده کنیم؟

رایج‌ترین کاربردهای Getter و Setter عبارت‌اند از: ساخت Property های محاسبه‌شده (مثل area که از radius محاسبه می‌شود)، اعتبارسنجی مقدار پیش از ذخیره‌ی آن (مثل بررسی مثبت بودن radius)، و مخفی کردن جزئیات پیاده‌سازی داخلی در حالی که یک واجهه‌ی ساده و شبیه Property معمولی در اختیار می‌گذاریم.

جمع‌بندی

Getter و Setter، با کلیدواژه‌های get و set، اجازه می‌دهند به جای ذخیره‌ی مستقیم یک مقدار، منطقی برای خواندن یا نوشتن یک Property تعریف شود؛ درحالی‌که از بیرون، همچنان مثل یک Property معمولی (بدون پرانتز) استفاده می‌شود. Property هایی که Getter یا Setter دارند، از نوع Accessor Property هستند و در Descriptor خودشان، به‌جای value، از get و set استفاده می‌کنند. این ابزار برای Property های محاسبه‌شده و اعتبارسنجی مقدار، بسیار پرکاربرد است.