مکانیزم ارث ECMAScript تحقق

پیاده‌سازی مکانیزم ارث‌گیری

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

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

در حالی که ECMAScript به اندازه زبان‌های دیگر به طور دقیق کلاس‌های انتزاعی را تعریف نمی‌کند، اما گاهی اوقات واقعاً کلاس‌هایی ایجاد می‌کند که استفاده نشوند. معمولاً این نوع کلاس‌ها را کلاس‌های انتزاعی می‌نامیم.

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

روش‌های ارث‌گیری

مانند سایر امکانات، روش‌های پیاده‌سازی ارث در ECMAScript بیش از یک روش دارند. این به این دلیل است که مکانیزم ارث در JavaScript به صورت مشخص تعیین نشده است، بلکه به صورت نمونه‌سازی پیاده‌سازی شده است. این意味着 تمام جزئیات ارث‌گیری توسط تفسیرگر کاملاً پردازش نمی‌شود. به عنوان توسعه‌دهنده، شما حق دارید تصمیم بگیرید که کدام روش ارث‌گیری مناسب‌تر است.

در اینجا چندین روش خاص از ارث‌گیری برای شما معرفی می‌شود.

پنهان‌سازی شیء

در زمان طراحی ECMAScript، هیچ قصدی برای طراحی پنهان‌سازی شیء (object masquerading) نبود. این به تدریج توسعه یافت، زمانی که توسعه‌دهندگان شروع به درک نحوه کارکرد تابع‌ها کردند، به ویژه نحوه استفاده از کلیدواژه this در محیط تابع.

اصل آن به این صورت است: متد سازنده از کلیدواژه this برای تعیین تمام ویژگی‌ها و روش‌ها استفاده می‌کند (یعنی از روش متد سازنده در تعریف کلاس استفاده می‌کند). چون متد سازنده فقط یک تابع است، می‌توان از متد سازنده ClassA به عنوان یک متد برای ClassB استفاده کرد و آن را فراخوانی کرد. ClassB ویژگی‌ها و روش‌های تعریف شده در متد سازنده ClassA را دریافت می‌کند. به عنوان مثال، ClassA و ClassB به این صورت تعریف می‌شوند:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

یادتان می‌آید؟ کلیدواژه this به شیء فعلی که توسط متد سازنده ایجاد شده اشاره دارد. اما در این متد، این این به شیء متعلق به آن اشاره دارد. این اصل این است که ClassA را به عنوان یک متد معمولی برای ایجاد مکانیزم ارث استفاده می‌کند، نه به عنوان یک متد سازنده. به این ترتیب می‌توان از متد سازنده ClassB برای پیاده‌سازی مکانیزم ارث استفاده کرد:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

در این کد، روش newMethod به ClassA اختصاص داده شده است (لطفاً به خاطر بسپارید که نام‌های متد فقط اشاره‌گرهایی به آن هستند). سپس این متد فراخوانی می‌شود و به آن پارامترهای متد سازنده ClassB یعنی sColor انتقال داده می‌شود. در آخرین خط کد، اشاره‌گر به ClassA حذف می‌شود، بنابراین دیگر نمی‌توان از آن استفاده کرد.

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

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

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

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//خروجی "blue"
objB.sayColor();	//خروجی "red"
objB.sayName(); // خروجی "John"

پنهان‌سازی شیء می‌تواند多重 ارث را پیاده‌سازی کند

جالب است که پنهان‌سازی شیء می‌تواند از多重 ارث پشتیبانی کند. به عبارت دیگر، یک کلاس می‌تواند از چندین کلاس مادر ارث ببرد. مکانیزم多重 ارث با استفاده از UML در شکل زیر نشان داده شده است:

مکانیزم ارث UML نمونه‌ای

برای مثال، اگر دو کلاس ClassX و ClassY وجود داشته باشند، ClassZ می‌خواهد این دو کلاس را ارث ببرد، می‌توان از کد زیر استفاده کرد:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

یک عیب در اینجا وجود دارد، اگر دو کلاس ClassX و ClassY دارای ویژگی‌ها یا روش‌های مشابه باشند، ClassY دارای اولویت بالاتر است زیرا از کلاس‌های بعدی ارث‌برداری می‌کند. به جز این مشکل کوچک، استفاده از روشنایی (object spoofing) برای ایجاد مکانیزم چندین ارث‌برداری آسان است.

به دلیل محبوبیت این روش ارث‌برداری، نسخه سوم ECMAScript دو روش جدید به شیء Function اضافه کرده است، یعنی call() و apply().

روش call()

روش call() یکی از روش‌های مشابه با روشنایی (object spoofing) کلاسیک است. اولین پارامتر آن به عنوان شیء این (this) استفاده می‌شود. سایر پارامترها مستقیماً به تابع ارسال می‌شوند. به عنوان مثال:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

در این مثال، تابع sayColor() در بیرون از شیء تعریف شده است، حتی اگر به هیچ شیء متعلق نباشد، می‌توان به کلمه کلیدی this اشاره کرد. ویژگی color شیء obj برابر با blue است. هنگام فراخوانی روش call()، اولین پارامتر obj است که نشان می‌دهد باید به کلمه کلیدی this در تابع sayColor() ارزش obj را اختصاص دهیم. دومین و سومین پارامترها رشته‌ها هستند. آن‌ها با پارامترهای sPrefix و sSuffix در تابع sayColor() هماهنگ هستند و پیام نهایی "The color is blue, a very nice color indeed." نمایش داده خواهد شد.

برای استفاده از روش‌های روشنایی (object spoofing) در مکانیزم ارث‌برداری، تنها کافی است که کد‌های سه‌گانه تعریف، فراخوانی و حذف را جایگزین کنید:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

در اینجا، ما نیاز داریم که کلمه کلیدی this در ClassA برابر با شیء جدید ایجاد شده ClassB باشد، بنابراین this اولین پارامتر است. دومین پارامتر sColor برای هر دو کلاس منحصر به فرد است.

روش apply()

apply() روشی دو پارامتر دارد که به عنوان این (this) استفاده می‌شود و آرایه‌ای از پارامترهایی که باید به تابع ارسال شوند. به عنوان مثال:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

این مثال با مثال قبلی مشابه است، اما اکنون از روش apply() فراخوانی می‌شود. هنگام فراخوانی روش apply()، اولین پارامتر همچنان obj است، که به این معناست که باید مقدار کلید this در Function sayColor() به obj اختصاص داده شود. دومین پارامتر آرایه‌ای از دو رشته است که با پارامترهای sPrefix و sSuffix Function sayColor() هماهنگ هستند، و پیام نهایی "The color is blue, a very nice color indeed." تولید می‌شود که نمایش داده می‌شود.

این روش همچنین برای جایگزینی کد‌های سه خط اول تعریف، فراخوانی و حذف روش جدید استفاده می‌شود:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

همین طور، اولین پارامتر همچنان this است، و دومین پارامتر آرایه‌ای است که فقط یک مقدار color دارد. می‌توانید کل شیء arguments کلاس ClassB را به عنوان دومین پارامتر به روش apply() ارسال کنید:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

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

prototype chaining

این شکل از ارث در ECMAScript برای prototype chain استفاده می‌شود. در فصل قبلی روش تعریف prototype کلاس‌ها را معرفی کردیم. prototype chain این روش را گسترش داده و مکانیزم ارث را به یک روش جالب پیاده‌سازی کرده است.

در فصل قبلی آموختیم که prototype یک قالب است، و تمام اشیاء نمونه‌سازی شده بر اساس این قالب هستند. به طور خلاصه، هر یک از ویژگی‌ها و روش‌های prototype به تمام نمونه‌های کلاس منتقل می‌شوند. prototype chain از این ویژگی برای پیاده‌سازی مکانیزم ارث استفاده می‌کند.

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

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

یک ویژگی شگفت‌انگیز در روش نمونه‌های اولیه این است که خطوط کد نمایش داده شده به رنگ آبی. در اینجا، ویژگی prototype کلاس ClassB به یک نمونه از ClassA تنظیم شده است. این بسیار جالب است، زیرا می‌خواهیم تمامی ویژگی‌ها و روش‌های ClassA را داشته باشیم، اما نمی‌خواهیم آنها را به یک به یک به ویژگی prototype کلاس ClassB اضافه کنیم. آیا راه بهتری برای انجام این کار وجود دارد؟

توجه داشته باشید:توابع ساختاری ClassA را بدون ارسال پارامتر به آن فراخوانی می‌کنیم. این کار در زنجیره‌ی نمونه‌های اولیه یک عملکرد استاندارد است. باید اطمینان حاصل کنید که توابع ساختاری هیچ پارامتری ندارند.

مثل نمونه‌سازی با استفاده از نمونه‌های اولیه، تمامی ویژگی‌ها و روش‌های فرزند باید پس از تعیین ویژگی prototype وجود داشته باشند، زیرا تمامی روش‌هایی که قبل از تعیین ویژگی prototype تعیین شده‌اند، حذف می‌شوند. چرا؟ زیرا ویژگی prototype با یک شیء جدید جایگزین شده است و شیء اصلی که روش‌های جدید را اضافه کرده است، از بین می‌رود. بنابراین، کد اضافه کردن ویژگی name و روش sayName() به کلاس ClassB به صورت زیر است:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

می‌توان این کد را با اجرای مثال زیر تست کرد:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

علاوه بر این، در زنجیره‌ی نمونه‌های اولیه، نحوه‌ی عملکرد عملگر instanceof نیز بسیار خاص است. برای همه‌ی نمونه‌های ClassB، instanceof برای ClassA و ClassB هر دو true برمی‌گرداند. به عنوان مثال:

var objB = new ClassB();
alert(objB instanceof ClassA);	// خروجی "true"
alert(objB instanceof ClassB);	// خروجی "true"

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

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

روش ترکیبی

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

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

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

در این مثال، مکانیزم ارث‌برگی با دو خط کد برجسته آبی انجام شده است. در اولین خط کد برجسته آبی، در داخل فراخوانی‌کننده ClassB، با استفاده از یک شیء، به ارث‌برگی ویژگی sColor کلاس ClassA پرداخته شده است. در دومین خط کد برجسته آبی، با استفاده از زنجیره‌ی پیشنهادی، به ارث‌برگی روش‌های کلاس ClassA پرداخته شده است. به دلیل استفاده از این روش ترکیبی از زنجیره‌ی پیشنهادی، عملگر instanceof همچنان به درستی کار می‌کند.

در مثال زیر، این کد را تست کرده‌ایم:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//خروجی "blue"
objB.sayColor();	//خروجی "red"
objB.sayName();	//خروجی "John"

TIY