۱۲ اکتبر ۲۰۲۲

سازنده، عملگر "new"

سینتکس معمولی {…} اجازه ساخت یک شیء را می دهد. اما غالبا ما نیاز داریم که شیء های متشابه زیادی ایجاد کنیم، مثل چند کاربر یا آیتم های منو و…

این می تواند با استفاده از تابع های سازنده و عملگر new انجام شود.

تابع سازنده

تابع های سازنده از لحاظ فنی همان تابع های معمولی هستند. با این حال دو قرارداد وجود دارد:

  1. آنها با حرف بزرگ انگلیسی نامگذاری می شوند.
  2. آنها باید فقط با عملگر new اجرا شوند.

برای مثال:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // نادرست

زمانی که یک تابع با new اجرا می شود، مراحل زیر را انجام می دهد:

  1. یک شیء خالی جدید ساخته می شود و به this اختصاص می یابد.
  2. بدنه ی تابع اجرا می شود. معمولا this را تغییر می دهد، ویژگی های جدید را به آن اضافه می کند.
  3. مقدار this برگردانده می شود.

به عبارتی دیگر، new User(...) چیزی مانند این را انجام می دهد:

function User(name) {
  // this = {};  (به صورت ضمنی)

  // ویژگی ها را به this اضافه میکند
  this.name = name;
  this.isAdmin = false;

  // return this;  (به صورت ضمنی)
}

پس let user = new User("Jack") نتیجه مشابهی مانند کد زیر را می دهد:

let user = {
  name: "Jack",
  isAdmin: false
};

حالا اگر ما بخواهیم که user های دیگری بسازیم، می توانیم new User("Ann")، new User("Alice") و … را صدا بزنیم. این کار بسیار کوتاه تر از استفاده همیشگی از literal ها است، و همچنین برای خواندن آسان است.

این هدف اصلی سازنده ها است – پیاده سازی کد قابل استفاده مجدد ساخت شیء.

بیایید دوباره به این موضوع اشاره کنیم – از لحاظ فنی، هر تابعی می تواند به عنوان سازنده استفاده شود. به این معنی که: هر تابعی می تواند با new اجرا شود، و الگوریتم بالا را اجرا کند. “حرف اول بزرگ انگلیسی” یک قرارداد عمومی است، تا این موضوع را که یک تابع باید با new اجرا شود را شفاف سازی کند.

new function() { … }

اگر ما خطوط زیادی از کد که همه آنها مربوط به ساخت یک شیء پیچیده هستند را داشته باشیم، می توانیم آنها را درون تابع سازنده بپیچیم، به این صورت:

// create a function and immediately call it with new
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ... کد های دیگر برای ساخت user
  // شاید شامل منطق و دستورالعمل پیچیده ای باشد
  // متغیرهای محلی و...
};

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

سازنده هایی با سینتکس دوگانه: new.target

مطالب پیشرفته

سینتکس این بخش به ندرت استفاده می شود، آن را از قلم بندازید مگر اینکه بخواهید همه چیز را بدانید.

درون یک تابع، ما می توانیم چک کنیم که همراه با new صدا زده شده یا بدون آن، با استفاده از ویژگی new.target.

آن(new.target) برای مواقعی که تابع به صورت معمولی صدا زده می شود undefined است و درصورتی که همراه با new صدا زده شود برابر با تابع است:

function User() {
  alert(new.target);
}

// بدون "new":
User(); // undefined

// همراه با "new":
new User(); // function User { ... }

از آن می توان استفاده کرد تا هم صدا زدن تابع با new و هم صدا زدن معمولی تابع به یک شکل کار کنند. یعنی اینکه شیء متشابه بسازند:

function User(name) {
  if (!new.target) { // اگر تو مرا بدون new اجرا کنی
    return new User(name); // ... من new را برای تو اضافه میکنم
  }

  this.name = name;
}

let john = User("John"); // فراخوانی را به new User هدایت میکند
alert(john.name); // John

این شیوه بعضی اوقات درون کتابخانه ها استفاده می شود تا سینتکس را منعطف تر کند. با این روش شاید مردم تابع را همراه با یا بدون new صدا بزنند، و آن همچنان کار میکند.

اگرچه شاید خوب نباشد همه جا استفاده شود، چون حذف کردن new مقداری از واضح بودن اینکه چه چیزی در حال رخ دادن است کم میکند. همراه با new همه ما میدانیم که شیء جدیدی در حال ساخته شدن است.

برگرداندن از سازنده ها

معمولا، سازنده ها دستور return ندارند. وظیفه آنها این است که تمام چیزهای ضروری را داخل this بریزند، و آن(this) تبدیل به نتیجه می شود.

اما اگر دستور return وجود داشته باشد، سپس قاعده ساده است:

  • اگر return همراه با شیء صدا زده شود، سپس شیء به جای this برگردانده می شود.
  • اگر return همراه با یک primitive صدا شده شود، نادیده گرفته می شود.

به عبارتی دیگر، return همراه با یک شیء همان شیء را برمیگرداند، در دیگر موارد this برگردانده می شود.

برای مثال، اینجا return با برگرداندن یک شیء this را نادیده میگیرد:

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- این شیء را برمیگرداند
}

alert( new BigUser().name );  // Godzilla, آن شیء را نتیجه داد

و اینجا هم یک مثال با یک return خالی داریم (یا می توانستیم بعد از آن یک primitive بگذاریم، فرقی ندارد):

function SmallUser() {

  this.name = "John";

  return; // <-- this را برمیگرداند
}

alert( new SmallUser().name );  // John

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

پنهان کردن پرانتزها

راستی، اگر هیچ آرگومانی در کار نباشد، ما می توانیم پرانترهای بعد از new را حذف کنیم:

let user = new User; // <-- بدون پرانتز
// مشابه است با
let user = new User();

اینجا حذف کردن پرانتزها به عنوان یک “سبک خوب” فرض نمی شود، اما سینتکس طبق خصوصیات مجاز است.

متدها در سازنده

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

قطعا ما می توانیم علاوه بر ویژگی ها، متدها را هم به this اضافه کنیم.

برای مثال، new User(name) که در زیر قرار دارد یک شیء میسازد که به آن name و متد sayHi داده شده است:

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}

let john = new User("John");

john.sayHi(); // My name is: John

/*
john = {
   name: "John",
   sayHi: function() { ... }
}
*/

برای ساخت شیءهای پیچیده، یک سینتکس پیشرفته تر، کلاس ها، وجود دارد که ما بعدا آن را پوشش می دهیم.

خلاصه

  • تابع های سازنده، یا به اختصار، سازنده ها، تابع هایی معمولی هستند، اما یک توافق عمومی وجود دارد که آنها را با حرف بزرگ انگلیسی نامگذاری کنیم.
  • تابع های سازنده باید تنها با new صدا زده شوند. چنین صدا زدنی به ساخت یک this خالی در آغاز و برگرداندن پر شده ی آن در پایان اشاره می کند.

ما می توانیم از تابع های سازنده برای ساخت چند شیء متشابه استفاده کنیم.

جاوااسکریپت تابع های سازنده را برای بسیاری از شیءهایی که درون زبان ساخته شده اند فراهم میکند: مثل Date برای زمان ها، Set برای set ها و بقیه که ما مطالعه آنها را در نظر داریم.

شیءها، ما بر می گردیم!

در این فصل ما فقط اصول اولیه را برای شیءها و سازنده ها را پوشش می دهیم. آنها برای یادگیری بیشتر درباره انواع داده و تابع ها در فصل های آینده ضروری هستند.

بعد از اینکه آن را یاد گرفتیم، در فصل مقاله "object-oriented-programming" پیدا نشد (برنامه نویسی شیءگرا) به شیءها بر می گردیم و آنها را به صورت عمیق پوشش می دهیم، که شامل وراثت و کلاس ها هم می شود.

تمارین

اهمیت: 2

آیا امکان دارد که تابع های A و B را به گونه ای ساخت که new A()==new B()؟

function A() { ... }
function B() { ... }

let a = new A();
let b = new B();

alert( a == b ); // درست

اگر امکان دارد، پس یک مثال از کدهایشان تهیه کنید.

بله، امکان دارد.

اگر یک تابع یک شیء را برگرداند سپس new آن را به جای this برمی گرداند.

پس آنها می توانند، برای مثال، شیء تعریف شده خارجی مشابه را برگردانند.

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // درست
اهمیت: 5

یک تابع سازنده Calculator بسازید که شیء هایی با 3 متد ایجاد می کند.

  • read() با استفاده از prompt برای دو مقدار درخواست می کند و آنها را با نام‌های a و b به عنوان ویژگی‌ های شیء به خاطر می سپارد.
  • sum() مجموع این ویژگی ها را بر می گرداند.
  • mul() حاصل ضرب این ویژگی ها را بر می گرداند.

برای مثال:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

[دمو]

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

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

یک تابع سازنده Accumulator(startingValue) بسازید.

شیء ای که می سازد باید:

  • “مقدار حال حاضر” را در ویژگی value ذخیره کند. مقدار آغازین در آرگومان startingValue سازنده قرار می گیرد.
  • متد read() باید از prompt برای خواندن یک عدد جدید استفاده کند و آن را به value اضافه کند.

به عبارتی دیگر، ویژگی value حاصلِ جمع تمام مقدارهایی که کاربر وارد کرده با مقدار اولیه ی startingValue است.

اینجا نسخه دموی کد وجود دارد:

let accumulator = new Accumulator(1); // مقدار اولیه 1

accumulator.read(); // مقداری که کاربر وارد کرده را اضافه می کند
accumulator.read(); // مقداری که کاربر وارد کرده را اضافه می کند

alert(accumulator.value); // مجموع این مقدارها را نشان می دهد

[دمو]

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

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش