پروتوتایپ شی در جاوا اسکریپت – به زبان ساده


پروتوتایپها سازوکارهایی هستند که اشیای جاوا اسکریپت به وسیله آنها برخی ویژگیها را از همدیگر به ارث میبرند. در این مقاله، به توضیح طرز کار زنجیره پروتوتایپها میپردازیم و شیوه استفاده از مشخصه پروتوتایپ شی برای افزودن متدها به سازندههای موجود را مورد بررسی قرار میدهیم.
پیشنیازها
- درک تابعهای جاوا اسکریپت
- آشنایی با مبانی جاوا اسکریپت که مطالعه مقاله زیر توصیه میشود:
- آشنایی با شیئگرایی در جاوا اسکریپت که مطالعه مقاله زیر پیشنهاد میشود:
هدف از مطالعه این مقاله درک پروتوتایپهای اشیای جاوا اسکریپت، طرز کار زنجیرههای پروتوتایپ و شیوه افزودن متدهای جدید به مشخصه پروتوتایپ است.
آیا جاوا اسکریپت زبانی مبتنی بر پروتوتایپ است؟
جاوا اسکریپت غالباً به عنوان یک زبان مبتنی بر پروتوتایپ خوانده میشود که در آن برای ایجاد وراثت، شیئها دارای یک شیئ پروتوتایپ هستند که به عنوان شیئ قالبی عمل میکند و متدها و مشخصات از آن به ارث میرسند. یک شیئ پروتوتایپِ شیئ نیز میتواند یک شیئ پروتوتایپ داشته باشد که متدها و مشخصاتش را از آن به ارث میبرد و همین طور تا آخر. این وضعیت غالباً به نام «زنجیره پروتوتایپ» (prototype chain) نامیده میشود و توضیح میدهد که چرا متدها و مشخصات اشیای مختلف در اشیای دیگری که در اختیار آنها قرار دارد تعریف شده است.
اگر بخواهیم توصیف دقیقتری داشته باشیم، مشخصات و متدها در مشخصه prototype تابعهای سازنده شیئ تعریف میشوند و نه در خود وهلههای شیئ.
در جاوا اسکریپت یک لینک بین وهلهای از شیئ و پروتوتایپ آن برقرار میشود که مشخصه _ptoro_ نام دارد و از مشخصه ptorotype روی سازنده مشتق شده است و مشخصات و متدها از طریق پیگیری زنجیره پروتوتایپ به دست میآیند.
نکته: درک تمایز بین پروتوتایپ یک شیئ که از طریق (Object.getPrototypeOf(obj، یا از طریق روش منسوخ __proto__ در دسترس ما قرار دارد و مشخصه prototype روی تابعهای سازنده حائز اهمیت است. مورد اول یک مشخصه برای هر وهله از شیئ است و مورد دوم مشخصه سازنده است. یعنی (()Object.getPrototypeOf(new Foobar به همان شیئ Foobar.prototype اشاره میکند.
با بررسی یک مثال این وضعیت را روشنتر میسازیم.
درک شیئهای پروتوتایپ
در این بخش باید به مثالی که در بخشهای قبلی نوشتیم و سازنده ()person را تمام کردیم بازگردیم. این مثال را در مرورگر خود بارگذاری کنید. اگر این مثال را از بخشهای قبلی در سیستم خود ندارید میتوانید فایل زیر را روی سیستم خود ذخیره کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript class further exercises</title>
6 </head>
7
8 <body>
9 <p>This example requires you to enter commands in your browser's JavaScript console (see <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools">What are browser developer tools</a> for more information).</p>
10
11 </body>
12
13 <script>
14 function Person(first, last, age, gender, interests) {
15 this.name = {
16 'first': first,
17 'last' : last
18 };
19 this.age = age;
20 this.gender = gender;
21 this.interests = interests;
22 this.bio = function() {
23 // First define a string, and make it equal to the part of
24 // the bio that we know will always be the same.
25 var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
26 // define a variable that will contain the pronoun part of
27 // the second sentence
28 var pronoun;
29 // check what the value of gender is, and set pronoun
30 // to an appropriate value in each case
31 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
32 pronoun = 'He likes ';
33 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
34 pronoun = 'She likes ';
35 } else {
36 pronoun = 'They like ';
37 }
38 // add the pronoun string on to the end of the main string
39 string += pronoun;
40 // use another conditional to structure the last part of the
41 // second sentence depending on whether the number of interests
42 // is 1, 2, or 3
43 if(this.interests.length === 1) {
44 string += this.interests[0] + '.';
45 } else if(this.interests.length === 2) {
46 string += this.interests[0] + ' and ' + this.interests[1] + '.';
47 } else {
48 // if there are more than 2 interests, we loop through them
49 // all, adding each one to the main string followed by a comma,
50 // except for the last one, which needs an and & a full stop
51 for(var i = 0; i < this.interests.length; i++) {
52 if(i === this.interests.length - 1) {
53 string += 'and ' + this.interests[i] + '.';
54 } else {
55 string += this.interests[i] + ', ';
56 }
57 }
58 }
59 // finally, with the string built, we alert() it
60 alert(string);
61 };
62 this.greeting = function() {
63 alert('Hi! I\'m ' + this.name.first + '.');
64 };
65 };
66 var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
67 </script>
68</html>
در این مثال ما یک تابع سازنده تعریف کردهایم که به صورت زیر است:
1function Person(first, last, age, gender, interests) {
2
3 // property and method definitions
4 this.first = first;
5 this.last = last;
6//...
7}
سپس یک وهله از شیئ به صورت زیر ساختهایم:
1var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
اگر عبارت «person1.» را در کنسول جاوا اسکریپت وارد کنید میبینید که مرورگر تلاش میکند تا آن را با نامهای عضو که روی این شیئ وجود دارد تکمیل کند.
در این فهرست، اعضای تعریف شده در سازنده person1 را مشاهده میکنید که شامل موارد زیر است:
Person() — name, age, gender, interests, bio & greeting
با این وجود اعضای دیگری مانند watch ،valueOf و غیره را نیز مشاهده میکنید. اینها روی شیئ پروتوتایپ ()Person تعریف شدهاند که Object است.
شاید از خود بپرسید اگر یک متد روی person1 فراخوانی شود چه اتفاقی رخ میدهد و در واقع چه چیزی روی Object تعریف شده است. برای مثال به کد زیر توجه کنید:
1person1.valueOf()
این متد از سوی person1 به ارث رسیده است، زیرا سازنده آن ()Person است و پروتوتایپ ()Person نیز ()Object است. دقت کنید که ()valueOf مقدار شیئ فراخوانی شده را بازگشت میدهد. اگر این وضعیت را امتحان کنید نتایج زیر به دست میآیند:
مرورگر در ابتدا بررسی میکند که آیا شیئ person1 متد ()valueOf را چنان که در سازنده آن ()Person تعریف شده دارد یا نه. چون آن را ندارد مرورگر بررسی میکند که آیا شیئ پروتوتایپ سازنده ()Person یعنی ()Object متد ()valueOf را روی خود دارد یا نه. از آنجا که چنین متدی وجود دارد آن را فراخوانی میکند و کار به پایان میرسد.
نکته: یک بار دیگر باید تکرار کنیم که متدها و مشخصهها در زنجیره پروتوتایپ از یک شیئ به شیئ دیگر کپی نمیشوند؛ بلکه با پیمودن زنجیره به روشی که توضیح داده شد، مورد دسترسی قرار میگیرد.
نکته: به طور رسمی روشی برای دسترسی به شیئ پروتوتایپ یک شیئ به صورت مستقیم وجود ندارد، لینکهای بین آیتمها در زنجیره به صورت مشخصه داخلی تعریف شدهاند که در توضیحات زبان جاوا اسکریپت به صورت مورد اشاره قرار گرفتهاند. اغلب مرورگرهای مدرن مشخصهای به نام __proto__ دارند (در هر طرف دو کاراکتر زیرخط وجود دارد) که شامل شیئ پروتوتایپ سازنده شیئ است. برای نمونه موارد __person1.__proto و __person1.__proto__.__proto را امتحان کنید تا این پنجره را عملاً در کد خود مشاهده کنید.
از استاندارد ECMAScript 2015 به بعد دسترسی به شیئ پروتوتایپ یک شیئ به صورت غیرمستقیم و از طریق (Object.getPrototypeOf(obj نیز میسر شده است.
مشخصه پروتوتایپ: اعضای به ارث رسیده کجا تعریف شدهاند؟
به طور طبیعی سؤالی که اینک پیش میآید این است که مشخصهها و متدهای به ارث رسیده اینک کجا تعریف شدهاند؟ اگر به صفحه رفرش Object نگاه کنید میبینید که در سمت چپ تعداد زیادی از مشخصهها و متدها فهرست شدهاند. این موارد بسیار بیشتر از آن چیزهایی هستند که روی شیئ person1 مشاهده کردیم و برخی از آنها هم نیستند.
همانطور که قبلاً اشاره کردیم، متدها و مشخصههای به ارث رسیده انواعی هستند که در مشخصه prototype تعریف شدهاند. شما میتوانید آن را فضای نام فرعی بنامید. این نوعها با Object.prototype. و نه با Object. آغاز میشوند. دقت کنید که مقدار مشخصه prototype یک شیئ است که اساساً دستهای از مشخصات و متدهای قوی هستند که میخواهیم از سوی شیئهایی در لایههای پایینتر زنجیره به ارث برسند.
بنابراین ()Object.prototype.watch() ،Object.prototype.valueOf و موارد دیگر برای هر نوع شیئی که از Object.prototype ارث میرسد آماده هستند و شامل وهلههای جدید ایجاد شده از سوی سازنده ()Person نیز میشود.
()Object.is() ،Object.keys و دیگر اعضایی که درون دسته prototype تعریف نشدهاند از سوی وهلههایی از شیئ یا انواع شیئی که از Object.prototype ارث میبرند، ارثبری ندارند. اینها مشخصهها / متدهایی هستند که تنها روی خود سازنده ()Object موجود هستند.
نکته: این وضعیت عجیب به نظر میرسد، چطور میتوان متدی داشت که روی یک سازنده تعریف شده باشد و خودش یک تابع باشد؟ در واقع تابع نیز خود نوعی شیئ است. برای کسب اطلاعات بیشتر میتوانید به مرجع سازنده ()Function در این صفحه (+) مراجعه کنید.
شما میتوانید مشخصههای پروتوتایپ موجود را خودتان بررسی کنید. در مثال قبلی کد زیر را در کنسول مرورگر وارد کنید:
1Person.prototype
خروجی چیز زیادی نمایش نمیدهد، زیرا هنوز چیزی را روی پروتوتایپ سفارشی خود تعریف نکردهایم. به صورت پیشفرض یک پروتوتایپ سازنده همواره به طور خالی آغاز میشود. اینک کد زیر را امتحان کنید:
1Object.prototype
این بار متدهای زیادی را میبینید که روی مشخصه prototype مربوط به Object تعریف شده است و از این رو در اختیار اشیایی است که از Object ارثبری دارند.
شما مثالهای دیگری از وراثت زنجیره پروتوتایپ را در همه جای جاوا اسکریپت شاهد خواهید بود. کافی است به متدها و مشخصههای تعریف شده روی پروتوتایپ اشیای سراسری String ،Date ،Number و Array نگاه کنید. همگی این موارد چندین عضو تعریف شده روی پروتوتایپ خود دارند. به همین دلیل است که وقتی رشتهای را به صورت زیر ایجاد میکنیم:
1var myString = 'This is my string.';
myString بیدرنگ چندین متد مفید مانند ()split() ،indexOf() ،replace و غیره روی خودش دارد.
نکته: مشخصه prototype یکی از بخشهایی از جاوا اسکریپت است که نام آن افراد زیادی را سردرگم میکند. ممکن است تصور کنید که this به شیئ پروتوتایپ شیئ اشاره میکند؛ اما چنین نیست. این یک شیئ داخلی است که از طریق __proto__ قابل دسترسی است؛ اما prototype مشخصهای است که شامل یک شیئ است که اعضایی که میخواهید به ارث برسند را تعریف میکند.
بررسی مجدد ()create
در بخشهای قبلی این سری مقالات طرز کار ()Object.create را بررسی کرده و یک وهله جدید از شیئ ساختیم.
برای نمونه کد زیر را در کنسول مرورگر وارد کنید:
1var person2 = Object.create(person1);
کاری که ()create در عمل انجام میدهد این است که شیئ جدیدی را از شیئ پروتوتایپ تعریف شده میسازد. در اینجا person2 از سوی person1 به عنوان شیئ پروتوتایپ ساخته میشود. میتوانید این وضعیت را با وارد کردن کد زیر در کنسول مرورگر خود امتحان کنید:
1person2.__proto__
کد فوق عبارت person1 را بازگشت میدهد.
مشخصه سازنده (Constructor)
هر تابع سازندهای یک مشخصه پروتوتایپ دارد که مقدار آن یک شیئ شامل مشخصه constructor است. این مشخصه سازنده به تابع سازنده اصلی اشاره میکند. همان طور که در بخش بعدی خواهید دید مشخصات تعریف شده در مشخصه Person.prototype یا به طور کلی روی یک مشخصه پروتوتایپ یک تابع سازنده که یک شیئ باشد در اختیار همه وهلههایی از شیئ که به وسیله سازنده ()Person ایجاد شود قرار میگیرند. از این رو مشخصه سازنده هم روی شیئ object1 و هم object2 موجود است.
برای نمونه دستورهای زیر را در کنسول وارد کنید:
1person1.constructor
2person2.constructor
این موارد هر دو سازنده ()Person را بازگشت میدهد، چون شامل تعریف اصلی این وهلهها است.
یک ترفند هوشمندانه این است که میتوانید پرانتزها را در انتهای مشخصه constructor (به همراه هر پارامتر مورد نیاز) قرار دهید تا یک وهله شیئ دیگر از سازنده بسازید. در هر صورت سازنده خود تابعی است و از این رو میتواند با استفاده از پرانتزها فراخوانی شود؛ کافی است کلیدواژه new را استفاده کنید تا مشخص شود که میخواهید از تابع به عنوان سازنده استفاده کنید.
کد زیر را در مرورگر وارد کنید:
1var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
اینک تلاش کنید به ویژگیهای شیئ جدید خود دسترسی داشته باشید:
1person3.name.first
2person3.age
3person3.bio()
این کد به خوبی کار میکند. لازم نیست که از آن به طور مکرر استفاده کنید؛ اما در مواردی که میخواهید وهله جدیدی بسازید و به هر دلیلی بهسادگی ارجاعی به سازنده اصلی ندارید مفید خواهد بود.
مشخصه constructor کاربردهای دیگری نیز دارد. برای نمونه اگر وهلهای از یک شیئ را داشته باشید و بخواهید نام سازندهای که وهلهاش را در اختیار دارید به دست بیاورید میتوانید از کد زیر استفاده کنید:
1instanceName.constructor.name
برای نمونه کد زیر را امتحان کنید:
1person1.constructor.name
نکته: مقدار constructor.name میتواند به دلیل وراثت پروتوتایپی، اتصال، پیش پردازشگرها، transpiler–ها و موارد دیگر تغییر پیدا کند، بنابراین برای مشاهده مثالهای پیچیدهتر میتوانید به جای آن از عملگر instanceof استفاده کنید.
تغییر دادن پروتوتایپها
در ادامه مثالی از تغییر دادن مشخصه prototype یک تابع سازنده را بررسی میکنیم. متدهای اضافه شده به پروتوتایپ میتوانند روی همه وهلههای شیئ که با آن سازنده ساخته میشوند مورد دسترسی قرار گیرند. در این مرحله در نهایت چیزی را به پروتوتایپ سازنده ()Person خود اضافه میکنیم.
به مثال ابتدای این نوشته بازگردید و یک فایل محلی از آن روی سیستم خود ایجاد کنید. کد جاوا اسکریپت زیر را به آن اضافه کنید که یک متد جدید به مشخصه prototype سازنده اضافه میکند.
1Person.prototype.farewell = function() {
2 alert(this.name.first + ' has left the building. Bye for now!');
3};
کد را ذخیره، صفحه را در مرورگر خود بارگذاری و کد زیر را در کادر ورودی متنی وارد کنید:
1person1.farewell();
بدین ترتیب یک پیام هشدار نمایش مییابد که نام فرد را که درون سازنده تعریف شده نمایش میدهد. این وضعیت واقعاً مفید است؛ اما زمانی مفیدتر میشود که کل زنجیره وراثت به صورت دینامیک بهروزرسانی شود و به صوت خودکار این متد جدید روی همه وهلههای شیئ مشتق شده از سازنده در دسترس ما قرار گیرد.
لحظهای به این وضعیت بایندیشید. ما در کد خود سازنده را تعریف کردیم، سپس یک وهله از شیئ را از سازنده ایجاد کردیم و در ادامه یک متد جدید به پروتوتایپ سازنده اضافه نمودیم:
1function Person(first, last, age, gender, interests) {
2
3 // property and method definitions
4
5}
6
7var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
8
9Person.prototype.farewell = function() {
10 alert(this.name.first + ' has left the building. Bye for now!');
11};
اما متد ()farewell همچنان روی وهلهای از شیئ person1 موجود است و اعضای آن به صوت خودکار بهروزرسانی میشوند تا متد ()farewell جدیداً تعریف شده را شامل شوند.
ما به ندرت مشخصههایی را میبینیم که روی مشخصه prototype تعریف شده باشند، چون وقتی به این روش تعریف میشوند چندان انعطافپذیر نیستند. برای نمونه میتوانید مشخصهای به صورت زیر داشته باشید:
1Person.prototype.fullName = 'Bob Smith';
این وضعیت چندان انعطافپذیر نیست، چون فرد (person) ممکن است فراخوانی نشده باشد. بهتر است که fullName را از روی name.first و name.last بسازیم:
1Person.prototype.fullName = this.name.first + ' ' + this.name.last;
با این حال این کد کار نمیکند، زیرا this در این حالت به دامنه سراسری اشاره میکند و نه دامنه تابع. فراخوانی مشخصه میتواند قبلتر و در پروتوتایپ تعریف شود زیرا درون دامنه تابع قرار دارد که به صورت موفق به دامنه وهله شیئ انتقال مییابد. بنابراین شما باید مشخصههای ثابت یعنی آنهایی که هرگز تغییر نخواهند یافت را روی پروتوتایپ تعریف کنید؛ اما به طور کلی بهتر است که مشخصهها را درون سازنده تعریف کنید.
در واقع یک الگوی کاملاً متداول برای اغلب تعاریف شیئ این است که مشخصهها درون سازنده تعریف میشوند و متدها روی پروتوتایپ. این وضعیت باعث میشود که خوانایی کد افزایش یابد، چون سازنده تنها شامل تعاریف مشخصهها است و متدها به بلوکهای مجزایی افراز شدهاند. برای نمونه به کد زیر توجه کنید:
1// Constructor with property definitions
2
3function Test(a, b, c, d) {
4 // property definitions
5}
6
7// First method definition
8
9Test.prototype.x = function() { ... };
10
11// Second method definition
12
13Test.prototype.y = function() { ... };
14
15// etc.
سخن پایانی
در این مقاله به بررسی موضوع پروتوتایپهای شیئ در جاوا اسکریپت پرداختیم که شامل توضیح طرز کار زنجیره پروتوتایپ و ارتباط آن با وراثت ویژگیها از یک پروتوتایپ به دیگری، مشخصه پروتوتایپ و شیوه استفاده از آن برای افزودن متد به سازندهها و دیگر موضوعات مختلف بود.
در مقاله بعدی به بررسی شیوه پیادهسازی وراثت کارکردها بین دو شیئ سفارشی که خودمان ایجاد کردهایم خواهیم پرداخت. برای مطالعه آن به لینک زیر مراجعه کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکریپت
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- تابع های جاوا اسکریپت — راهنمای جامع
- 1۰ کتابخانه و فریمورک جاوا اسکریپت که باید آنها را بشناسید — قسمت اول
^^
==