آشنایی با شی‌ءگرایی در جاوااسکریپت

شنبه 28 بهمن 1396

این مقاله با معرفی برنامه‌نویسی شی‌ءگرا آغاز می‌شود.سپس مدل شیءگرایی جاوااسکریپت را بررسی کرده و درنهایت مفاهیم برنامه‌نویسی شی‌ءگرا در جاوااسکریپت را همراه با مثال‌های متعدد شرح می‌دهد.

آشنایی با شی‌ءگرایی در جاوااسکریپت

اگر احساس می‌کنید در مورد مفاهیم جاوااسکریپت مانند متغیرها، انواع، توابع و scope ضعیف هستید، می‌توانید قبل از خواندن این مقاله در مورد مقدمات آن مطالعاتی داشته باشید.

برنامه‌نویسی شیءگرا

برنامه‌نویسی شیءگرا (OOP) نمونه‌ای از برنامه‌نویسی است که از انتزاع برای ایجاد مدل‌های مبتنی بر دنیای واقعی استفاده می‌کند. OOP از تکنیک‌های متعددی از نمونه‌های از قبل تعیین شده، ازجمله ماژولار، پلی‌مورفیسم و کپسوله‌سازی استفاده می‌کند. امروزه بسیاری از زبان‌های برنامه‌نویسی محبوب (مانند جاوا، جاوااسکریپت، C#، C++، پایتون، PHP، Ruby و Objective-C) از OOP پشتیبانی می‌کنند.

OOP شامل مجموعه‌ای از اشیاست. هر شیء می‌تواند پیام‌ها را دریافت کرده، داده‌ها را پردازش کند و پیام‌ها را به اشیای دیگر ارسال کند. هر شیء می‌تواند به عنوان یک ماشین کوچک مستقل با یک نقش یا مسئولیت متفاوت مشاهده شود.

OOP انعطاف‌پذیری بیشتر و قابلیت نگهداری در برنامه‌نویسی را رواج می‌دهد و به طور گسترده در مهندسی نرم‌افزار با مقیاس بزرگ محبوب است. از آنجا که OOP شدیدا بر ماژولار تأکید دارد، کد شیءگرا برای توسعه ساده‌تر است و درک آن در آینده آسان‌تر می‌باشد. کد شی‌ءگرا، برنامه‌نویسی، درک شرایط و روش‌های پیچیده و تحلیل مستقیم‌تری را نسبت به روش‌های برنامه‌نویسی که کمتر ماژولار هستند ترویج می‌دهد.

اصطلاحات

فضای نام (Namespace)

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

کلاس (Class)

ویژگی‌های شیء را تعریف می‌کند. کلاس قالبی از تعاریف ویژگی‌ها و متدهای شیء است.

شیء (Object)

نمونه‌ای از کلاس می‌باشد.

ویژگی (Property)

ویژگی‌های شیء است، مثل رنگ.

متد (Method)

قابلیتی از شیء است، مثل راه رفتن. متد تابع وابسته به کلاس است.

سازنده (Constructor)

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

ارث‌بری (Inheritance)

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

کپسوله‌سازی (Encapsulation)

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

انتزاع (Abstraction)

یک پیوستگی از وراثت پیچیده شیء، متدها و ویژگی‌ها که اغلب باید مدلی را منعکس کند.

پلی‌مورفیسم (Polymorphism)

Poly به معنی "بسیاری" و morphism به معنی "اشکال" است. کلاس‌های مختلف ممکن است متدها و ویژگی‌های یکسانی را تعریف کنند.

برنامه‌نویسی مبنی بر نمونه اولیه (prototype-based)

برنامه‌نویسی prototype یک مدل OOP است که از کلاس‌ها استفاده نمی‌کند، بلکه ابتدا رفتار هر کلاس را انجام می‌دهد و سپس با استفاده از توسعه ویژگی‌های موجود اشیاء، مجددا از آن استفاده می‌کند (معادل ارث‌بری در زبان‌های مبنی بر کلاس). همچنین برنامه‌نویسی بدون کلاس، ویژگی‌گرا یا مبنی بر نمونه نامیده می‌شود.

زبان پروتوتایپ توسط David Ungar و Randall Smith ایجاد شده است. سبک برنامه‌نویسی بدون کلاس (class-less) رشد کرده و اخیرا به طور فزاینده‌ای محبوب شده است، و برای زبان‌های برنامه‌نویسی مثل JavaScript، Cecil، NewtonScript، Io، MOO، REBOL، Kevo، Squeak و زبان‌های دیگر پذیرفته شده است‌.

برنامه‌نویسی جاوااسکریپت شیءگرا

فضای نام

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

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

ایده ایجاد یک فضای نام در جاوا اسکریپت آسان است: یک شیء سراسری ایجاد کنید، و همه متغیرها، متدها و توابع به خواص آن شیء تبدیل شوند. استفاده از فضای نام همچنین فرصت ناسازگاری نام در برنامه را کاهش می‌دهد، زیرا هر یک از اشیای برنامه خصوصیتی از شیء سراسری تعریف شده در برنامه است.

بیایید یک شیء سراسری به نام MYAPP تعریف کنیم:

// global namespace
var MYAPP = MYAPP || {};

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

همچنین می‌توانیم sub-namespaces ایجاد کنیم (به یاد داشته باشید که ابتدا باید شیء سراسری را تعریف کنیم):

// sub namespace
MYAPP.event = {};

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

// Create container called MYAPP.commonMethod for common method and properties
MYAPP.commonMethod = {
  regExForName: "", // define regex for name validation
  regExForPhone: "", // define regex for phone no validation
  validateName: function(name){
    // Do something with name, you can access regExForName variable
    // using "this.regExForName"
  },
 
  validatePhoneNo: function(phoneNo){
    // do something with phone number
  }
}

// Object together with the method declarations
MYAPP.event = {
    addListener: function(el, type, fn) {
    // code stuff
    },
    removeListener: function(el, type, fn) {
    // code stuff
    },
    getEvent: function(e) {
    // code stuff
    }
  
    // Can add another method and properties
}

// Syntax for Using addListener method:
MYAPP.event.addListener("yourel", "type", callback);

اشیای استاندارد توکار

جاوااسکریپت شامل چندین شیء در هسته خود است. مثلا اشیایی مثل Math، Object، Array و String وجود دارند. مثال زیر نحوه استفاده از شیء Math برای گرفتن یک عدد تصادفی را با استفاده از متد ()random آن نشان می‌دهد:

console.log(Math.random());

نکته: این مثال و همه مثال‌های دیگر فرض می‌کنند که تابعی به نام ()console.log به صورت سراسری تعریف شده‌اند. در واقع تابع ()console.log بخشی از خود جاوااسکریپت نیست، اما بسیاری از مرورگرها آن را برای اشکال‌زدایی اجرا می‌کنند.

هر شیء در جاوااسکریپت نمونه‌ای از شیء Object است و به همین دلیل از تمام خواص و متدهای آن ارث‌بری می‌کند.

اشیای سفارشی

کلاس

جاوااسکریپت یک زبان مبتنی بر prototype است و حاوی هیچ کلاسی نیست. این مسأله گاهی اوقات باعث سردرگمی برنامه‌نویسانی می‌شود  که از زبان‌های مبنی بر کلاس استفاده می‌کنند. در عوض جاوااسکریپت از توابع به عنوان سازنده‌ کلاس استفاده می‌کند. تعریف کلاس مانند تعریف تابع آسان است. در مثال زیر یک کلاس جدید به نام Person را با یک سازنده خالی تعریف کرده‌ایم:

var Person = function () {};

شیء (نمونه کلاس)

برای ایجاد یک نمونه از شیء obj از یک obj جدید استفاده می‌کنیم و نتیجه (که از نوع obj است) به یک متغیر اختصاص داده می‌شود تا بعدا به آن دسترسی داشته باشیم.

در مثال بالا کلاسی با نام Person تعریف کردیم. در مثال زیر دو نمونه (person1 و person2) ایجاد می‌کنیم:

var person1 = new Person();
var person2 = new Person();

سازنده

سازنده در لحظه نمونه‌سازی (لحظه‌ای که نمونه‌ای از شیء ایجاد می‌شود) فراخوانی می‌شود. سازنده یکی از متدهای کلاس است. در جاوااسکریپت تابع به عنوان سازنده شیء عمل می‌کند، بنابراین نیازی به تعریف صریح متد سازنده نیست. هر عمل اعلان‌شده در کلاس در زمان نمونه‌سازی اجرا می‌شود.

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

در مثال زیر، سازنده کلاس Person، وقتی Person نمونه‌سازی می‌شود پیامی را ثبت می‌کند:

var Person = function () {
  console.log('instance created');
};

var person1 = new Person();
var person2 = new Person();

ویژگی (صفت شیء)

ویژگی‌ها متغیرهای موجود در کلاس هستند؛ هر نمونه از شیء، این ویژگی‌ها را دارد: خواصی که در سازنده (تابع) کلاس تنظیم شده‌اند، به طوری که در هر نمونه ایجاد می‌شوند.

کلمه کلیدی this، که به شیء جاری اشاره می‌کند، به شما اجازه می‌دهد داخل کلاس با ویژگی‌ها کار کنید. دسترسی (خواندن یا نوشتن) به یک ویژگی خارج از کلاس با این سینتکس انجام می‌شود: InstanceName.Property، مانند ++C، Java و چندین زبان دیگر. (داخل کلاس سینتکس this.Property برای دریافت یا تنظیم مقادیر ویژگی‌ها استفاده می‌شود).

در مثال زیر، ویژگی firstName را برای کلاس Person در نمونه اولیه تعریف می‌کنیم:

var Person = function (firstName) {
  this.firstName = firstName;
  console.log('Person instantiated');
};

var person1 = new Person('Alice');
var person2 = new Person('Bob');

// Show the firstName properties of the objects
console.log('person1 is ' + person1.firstName); // logs "person1 is Alice"
console.log('person2 is ' + person2.firstName); // logs "person2 is Bob"

متدها

متدها توابع هستند (و مانند توابع تعریف می‌شوند)، اما به طریق دیگری همان منطق ویژگی‌ها را دنبال می‌کنند. فراخوانی یک متد مشابه دسترسی به یک ویژگی است، اما () را در انتهای نام متد (احتمالا با آرگومان) اضافه می‌کنیم. برای تعریف متد، تابعی را به ویژگی نامیده‌شده از ویژگی‌ prototype کلاس اختصاص دهید. بعدا، می‌توانید متد را روی شیء با همان نامی که تابع را به آن اختصاص داده‌اید فراخوانی کنید.

در مثال زیر، متد ()sayHello را برای کلاس Person تعریف و استفاده کرده‌ایم:

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");

// call the Person sayHello method.
person1.sayHello(); // logs "Hello, I'm Alice"
person2.sayHello(); // logs "Hello, I'm Bob"

در متدهای جاوااسکریپت می‌توانید متدها را خارج از context فراخوانی کنید. کد زیر را در نظر بگیرید:

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");
var helloFunction = person1.sayHello;

// logs "Hello, I'm Alice"
person1.sayHello();

// logs "Hello, I'm Bob"
person2.sayHello();

// logs "Hello, I'm undefined" (or fails
// with a TypeError in strict mode)
helloFunction();                                    

// logs true
console.log(helloFunction === person1.sayHello);

// logs true
console.log(helloFunction === Person.prototype.sayHello);

// logs "Hello, I'm Alice"
helloFunction.call(person1);

همان‌طور که در مثال نشان داده شده است، تمام ارجاعات به تابع sayHello، یکی بر روی person1، روی Person.prototype، در متغیر helloFunction و غیره، به یک تابع مشابه اشاره دارند. مقدار this هنگام فراخوانی تابع به نحوه فراخوانی ما بستگی دارد. اغلب هنگامی که ما this را در حالتی فراخوانی می‌کنیم که تابع را از یک ویژگی شیء به دست می‌آوریم، ()person1.sayHello، this برای شیءای که تابع را از (person1) به دست آوردیم، تنظیم می‌شود. به همین دلیل ()person1.sayHello از نام "Alice" و ()person2.sayHello از نام "Bob" استفاده می‌کند. اما اگر آن را به روش دیگری فراخوانی کنیم، this به شیوه متفاوتی تنظیم می‌شود. فراخوانی this از یک متغیر، ()helloFunction، this را برای شیء سراسری تنظیم می‌کند (window یا مرورگر). از آنجا که شیء احتمالا یک ویژگی firstName ندارد، در نهایت با "Hello, I'm undefined" مواجه می‌شویم. ما می‌توانیم this را با استفاده از Function#call (یا Function#apply) تنظیم کنیم. همان طور که در انتهای مثال نشان داده شده است.

وراثت

وراثت شیوه‌ای برای ایجاد یک کلاس به عنوان یک نسخه اختصاصی از یک یا چند کلاس می‌باشد (جاوااسکریپت تنها از یک وراثت پشتیبانی می‌کند). کلاس اختصاصی معمولا فرزند نامیده می‌شود و کلاس‌های دیگر معمولا والد نامیده ‌می‌شوند. در جاوااسکریپت این کار را با اختصاص دادن نمونه‌ای از کلاس والد به کلاس فرزند انجام می‌دهید و سپس آن را اختصاصی می‌سازید. در مرورگرهای جدید همچنین می‌توانید از Object.create برای پیاده‌سازی ارث‌بری استفاده کنید.

نکته: جاوااسکریپت کلاس فرزند prototype.constructor را شناسایی نمی‌کند، بنابراین باید آن را به صورت دستی اعلان کنید.

در مثال زیر، کلاس Student را به عنوان کلاس فرزند تعریف می‌کنیم. سپس متد ()sayHello را مجددا تعریف کرده و متد ()sayGoodBye را نیز اضافه می‌کنیم:

// Define the Person constructor
var Person = function(firstName) {
  this.firstName = firstName;
};

// Add a couple of methods to Person.prototype
Person.prototype.walk = function(){
  console.log("I am walking!");
};

Person.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName);
};

// Define the Student constructor
function Student(firstName, subject) {
  // Call the parent constructor, making sure (using Function#call)
  // that "this" is set correctly during the call
  Person.call(this, firstName);

  // Initialize our Student-specific properties
  this.subject = subject;
};

// Create a Student.prototype object that inherits from Person.prototype.
// Note: A common error here is to use "new Person()" to create the
// Student.prototype. That's incorrect for several reasons, not least 
// that we don't have anything to give Person for the "firstName" 
// argument. The correct place to call Person is above, where we call 
// it from Student.
Student.prototype = Object.create(Person.prototype); // See note below

// Set the "constructor" property to refer to Student
Student.prototype.constructor = Student;

// Replace the "sayHello" method
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying "
              + this.subject + ".");
};

// Add a "sayGoodBye" method
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// Example usage:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Check that instanceof works correctly
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

با توجه به خط Student.prototype = Object.create(Person.prototype); بر روی موتورهای (engine) قدیمی جاوااسکریپت بدون Object.create، می‌توان از یک "polyfill" یا تابعی که نتیجه مشابه‌ای را انجام دهد استفاده کرد، مثل:

function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}

// Usage:
Student.prototype = createObject(Person.prototype);

اطمینان حاصل کنید که this، صرف نظر از نمونه‌سازی شیء، به مورد مناسبی اشاره می‌کند. با این حال یک اصلاح ساده برای ساخت راحت‌تر this وجود دارد.

var Person = function(firstName) {
  if (this instanceof Person) {
    this.firstName = firstName;
  } else {
    return new Person(firstName);
  }
}

کپسوله‌سازی

در مثال قبلی، Student نیازی به دانستن نحوه پیاده‌سازی متد ()walk کلاس Person نداشت،‌ اما همچنان می‌تواند از آن متد استفاده کند؛ کلاس Student نیازی ندارد تا به صراحت آن متد را تعریف کند مگر اینکه بخواهیم آن را تغییر دهیم. این عمل کپسوله‌سازی نامیده می‌شود که هر کلاس داده‌ها و متدها را بسته بندی می‌کند.

پنهان‌سازی اطلاعات، ویژگی رایج در زبان‌های دیگر که معمولا ویژگی ها و متدهای private و protected هستند می‌باشد. حتی اگر بتوانید چیزی شبیه به این را در جاوااسکریپت شبیه‌سازی کنید، نیازی نیست تا برنامه‌نویسی شیءگرا انجام دهید.

انتزاع (Abstraction)

انتزاع مکانیسمی است که به شما اجازه می‌دهد تا قسمت فعلی کار را با ارث‌بری (اختصاصی کردن) یا ترکیب شکل دهید.

کلاس Function جاوااسکریپت از کلاس Object ارث برده می‌شود (این اختصاصی کردن مدل را نشان می‌دهد) و Function.prototype property نمونه‌ای از Object است (این ترکیب را نشان می‌دهد).

var foo = function () {};

// logs "foo is a Function: true"
console.log('foo is a Function: ' + (foo instanceof Function));

// logs "foo.prototype is an Object: true"
console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object));

پلی‌مورفیسم

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

نتیجه‌گیری

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

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

نویسنده 3355 مقاله در برنامه نویسان

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

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید