وراثت در جاوا اسکریپت – راهنمای کاربردی


در بخش قبلی این سری مطالب راهنمای کاربردی به بررسی جزییات شیءگرایی در جاوا اسکریپت پرداختیم. در این مقاله شیوه ایجاد کلاسهای شیء «فرزند» را توضیح میدهیم که برخی ویژگیها را از کلاسهای «والد» خود به ارث میبرند. به علاوه برخی نکات دیگر در مورد وراثت در جاوا اسکریپت و زمان و مکان استفاده از شیءگرایی مطرح کرده و ارتباط کلاسها با ساختار ECMAScript مدرن را نیز بررسی میکنیم. برای مطالعه بخش قبلی این مجموعه آموزشی میتوانید به لینک زیر مراجعه کنید:
پیشنیازها
- سواد مقدماتی رایانه
- درکی ابتدایی از HTML و CSS
- آشنایی با مبانی جاوا اسکریپت و مبانی شیءگرایی در آن
هدف از این مقاله، معرفی روش پیادهسازی وراثت در جاوا اسکریپت است.
وراثت پروتوتایپی
در بخشهای قبلی این سری مقالات راهنمای کاربردی جاوا اسکریپت، برخی موارد وراثت را در عمل مشاهده کردیم. دیدیم که زنجیره پروتوتایپ چگونه کار میکند و چگونه اعضا در راستای این زنجیره از عناصر بالاتر ارثبری میکنند. اما این روند در اغلب موارد نیازمند تابعهای داخلی مرورگر است. چگونه میتوان یک شیء را در جاوا اسکریپت طوری ساخت که از شیء دیگر ارث ببرد؟ این مسئله را با ارائه چند مثال بنیادی در ادامه مورد بررسی قرار میدهیم.
سرآغاز
قبل از هر چیز یک کپی از کد زیر را روی سیستم خود در فایلی با نام oojs-class-inheritance-start.html ذخیره کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript inheritance</title>
6 </head>
7
8 <body>
9 <div>
10 <label for="jscode">Enter code:</label>
11 <input type="text" id="jscode">
12 <button>Submit code</button>
13 </div>
14
15 <p></p>
16 </body>
17
18 <script>
19 var input = document.querySelector('input');
20 var btn = document.querySelector('button');
21 var para = document.querySelector('p');
22 btn.onclick = function() {
23 var code = input.value;
24 para.textContent = eval(code);
25 }
26 function Person(first, last, age, gender, interests) {
27 this.name = {
28 first,
29 last
30 };
31 this.age = age;
32 this.gender = gender;
33 this.interests = interests;
34 };
35 Person.prototype.bio = function() {
36 // First define a string, and make it equal to the part of
37 // the bio that we know will always be the same.
38 var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
39 // define a variable that will contain the pronoun part of
40 // the second sentence
41 var pronoun;
42 // check what the value of gender is, and set pronoun
43 // to an appropriate value in each case
44 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
45 pronoun = 'He likes ';
46 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
47 pronoun = 'She likes ';
48 } else {
49 pronoun = 'They like ';
50 }
51 // add the pronoun string on to the end of the main string
52 string += pronoun;
53 // use another conditional to structure the last part of the
54 // second sentence depending on whether the number of interests
55 // is 1, 2, or 3
56 if(this.interests.length === 1) {
57 string += this.interests[0] + '.';
58 } else if(this.interests.length === 2) {
59 string += this.interests[0] + ' and ' + this.interests[1] + '.';
60 } else {
61 // if there are more than 2 interests, we loop through them
62 // all, adding each one to the main string followed by a comma,
63 // except for the last one, which needs an and & a full stop
64 for(var i = 0; i < this.interests.length; i++) {
65 if(i === this.interests.length - 1) {
66 string += 'and ' + this.interests[i] + '.';
67 } else {
68 string += this.interests[i] + ', ';
69 }
70 }
71 }
72 // finally, with the string built, we alert() it
73 alert(string);
74 };
75 Person.prototype.greeting = function() {
76 alert('Hi! I\'m ' + this.name.first + '.');
77 };
78 Person.prototype.farewell = function() {
79 alert(this.name.first + ' has left the building. Bye for now!');
80 }
81 var person1 = new Person('Tammi', 'Smith', 17, 'female', ['music', 'skiing', 'kickboxing']);
82 </script>
83</html>
در فایل فوق مثال سازنده ()Person را که در بخشهای قبلی بررسی کردهایم، در سراسر ماژول مشاهده میکنید که البته کمی تغییر یافته است. ما تنها مشخصهها را درون سازنده تعریف کردهایم:
1function Person(first, last, age, gender, interests) {
2 this.name = {
3 first,
4 last
5 };
6 this.age = age;
7 this.gender = gender;
8 this.interests = interests;
9};
متدها همگی روی پروتوتایپ سازنده تعریف شدهاند. به عنوان نمونه به مثال زیر توجه کنید:
1Person.prototype.greeting = function() {
2 alert('Hi! I\'m ' + this.name.first + '.');
3};
نکته: در کد منبع متدهای ()bio و ()farewell را نیز میبینید که تعریف شدهاند. در ادامه به بررسی روش ارثبری سازندههای دیگر از این موارد نیز میپردازیم.
فرض کنید میخواهید یک کلاس Teacher ایجاد کنید. این کلاس شبیه به تعریف شیءگرای اولیه ما است که همه اعضا از Person ارثبری میکنند؛ اما شامل موارد زیر نیز هست:
- یک مشخصه جدید – این مشخصه شامل موضوع تدریس معلم خواهد بود.
- یک بهروزرسانی از متد ()greeting – این متد که کمی رسمیتر از متد استاندارد ()greeting است، برای معرفی یک معلم در مدرسه به دانشآموزان مناسبتر است.
تعریف کردن تابع سازنده ()Teacher
نخستین کاری که باید انجام دهیم ایجاد تابع سازنده ()Teacher است.
به این منظور کد زیر را به کدهای موجود اضافه میکنیم:
1function Teacher(first, last, age, gender, interests, subject) {
2 Person.call(this, first, last, age, gender, interests);
3
4 this.subject = subject;
5}
این سازنده از جهات مختلف شبیه به سازنده Person به نظر میرسد؛ اما نکته عجیبی در آن هست که تا به اکنون ندیدهایم و آن تابع ()call است. این تابع اساساً امکان فراخوانی یک تابع تعریف شده در جای دیگر اما در context کنونی را میدهد. پارامتر اول مقدار this را توصیف میکند که قرار است هنگام اجرای تابع استفاده شود و دیگر پارامترها مواردی هستند که در زمان فراخوانی تابع به آن ارسال میشوند.
ما میخواهیم سازنده ()Teacher همان پارامترهای سازنده ()Person را که از آن ارثبری کرده است بگیرد و از این رو همه آن پارامترها را در فراخوانی ()call ذکر میکنیم.
خط آخر سازنده صرفاً به تعریف مشخصه subject جدید میپردازید که معلمها باید داشته باشند و افراد به طور کلی فاقد آن هستند. دقت کنید که ما میتوانیم این کار را به سادگی به صورت زیر انجام دهیم:
1function Teacher(first, last, age, gender, interests, subject) {
2 this.name = {
3 first,
4 last
5 };
6 this.age = age;
7 this.gender = gender;
8 this.interests = interests;
9 this.subject = subject;
10}
اما این کار در واقع بازتعریف مجدد مشخصهها است و نه به ارث بردن آنها از ()Person و از این رو با مقصودی که ما در ذهن خود داریم مغایرت دارد. از طرف دیگر به کدنویسی بیشتری هم نیاز دارد.
ارثبری از یک سازنده بدون هیچ پارامتری
دقت کنید که اگر سازندهای که از آن ارثبری میکنید، مقادیر مشخصههای خود را از پارامترها نگیرد، لزومی ندارد که آنها را به صورت آرگومانهای اضافی در ()call ذکر کنید. بدین ترتیب برای مثال اگر چیز واقعاً سادهای مانند زیر داشته باشید:
1function Brick() {
2 this.width = 10;
3 this.height = 20;
4}
میتوانید مشخصههای width و height آن را به صورت زیر به ارث ببرید (البته مراحل دیگر که در بخش فوق توصیف شدند نیز مورد نیاز هستند):
1function BlueGlassBrick() {
2 Brick.call(this);
3
4 this.opacity = 0.5;
5 this.color = 'blue';
6}
توجه کنید که ما تنها this را درون ()call ذکر کردهایم و پارامترهای دیگر مورد نیاز نیستند، زیرا هیچ مشخصه دیگری را از والد به ارث نبردهایم که از طریق پارامترها تعیین شده باشد.
تعیین مرجع پروتوتایپ و سازنده ()Teacher
تا به اینجا همه چیز به طرز مناسبی پیش رفته است؛ اما یک مشکل داریم. ما یک سازنده جدید تعریف کردهایم و این سازنده یک مشخصه prototype دارد که به صورت پیشفرض شامل ارجاعی به خود تابع سازنده است. این مشخصه شامل هیچ متدی از مشخصه prototype سازنده Person نیست. اگر میخواهید این واقعیت را بررسی کنید، کد زیر را در یک فیلد ورودی متنی یا در کنسول جاوا اسکریپت مرورگر وارد کنید:
Object.getOwnPropertyNames(Teacher.prototype)
سپس آن را دوباره وارد کنید و این بار به جای Teacher از Person استفاده کنید. در هیچ کدام از موارد سازنده جدید آن متدها را به ارث نمیبرد. برای مشاهده این مورد باید خروجیهای Person.prototype.greeting و Teacher.prototype.greeting را مقایسه کنید. ما باید ()Teacher را طوری طراحی کنیم که متدهای تعریف شده روی پروتوتایپ ()Person را به ارث ببرد. اما روش انجام این کار چگونه است؟
این خط کد را زیر بخشی که قبلاً افزودید، اضافه کنید:
1Teacher.prototype = Object.create(Person.prototype);
ما در این کد ()create را تعریف کردهایم که یک بار دیگر به کمک ما میآید. در این مورد، از آن برای ایجاد یک شیء جدید و مقداردهی Teacher.prototype استفاده میکنیم. این شیء جدید دارای Person.prototype است، زیرا پروتوتایپ آن نیز به ارث رسیده است و در صورت نیاز میتوانیم همه متدهای موجود روی Person.prototype را نیز در آن داشته باشیم.
ما باید پیش از ادامه یک یا چند کار دیگر را نیز انجام دهیم. پس از افزودن خط آخر، مشخصه سازنده Teacher.prototype اینک معادل ()Person شده است، زیرا ما دقیقاً تعریف کردهایم که Teacher.prototype به شیئی که از مشخصات آن از Person.prototype ارث میبرد ارجاع دهد. کد را ذخیره، صفحه را یک بار دیگر در مرورگر بارگذاری و کد زیرا در کنسول وارد کنید تا از صحت عملکرد آن مطمئن شوید:
1Teacher.prototype.constructor
این وضعیت میتوانید یک مشکل ایجاد کند و از این رو باید آن را به طرز صحیحی تنظیم کنیم. این کار از طریق مراجعه به کد منبع و افزودن خط زیر در انتهای آن ممکن است:
1Object.defineProperty(Teacher.prototype, 'constructor', {
2 value: Teacher,
3 enumerable: false, // so that it does not appear in 'for in' loop
4 writable: true });
اکنون اگر صفحه را ذخیره و رفرش کنید، با وارد کردن Teacher.prototype.constructor میبینید که ()Teacher باز میگردد که مطلوب ما است و به علاوه اینک ما از ()Person ارثبری میکنیم.
تعیین یک تابع ()greeting جدید برای ()Teacher
برای این که کد خود را تکمیل کنیم، باید یک تابع ()greeting جدید روی سازنده ()Teacher تعریف کنیم. سادهترین روش برای انجام این کار، تعریف کردن آن روی پروتوتایپ ()Teacher است. به این منظور کد زیر را در انتهای کد اضافه کنید:
1Teacher.prototype.greeting = function() {
2 var prefix;
3
4 if (this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
5 prefix = 'Mr.';
6 } else if (this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
7 prefix = 'Mrs.';
8 } else {
9 prefix = 'Mx.';
10 }
11
12 alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');
13};
این کد یک پیام معرفی معلم را نمایش میدهد که از پیشوند نام مناسبی برای جنسیت وی نیز استفاده میکند و به این منظور از گزارههای شرطی استفاده کرده است.
آزمودن مثال
اینک که همه کد را وارد کردهایم، میتوانیم با قرار دادن کد زیر در انتهای کدهای جاوا اسکریپت (یا در جای دیگر مشابهی بسته به انتخاب شما) یک وهله از شیء را از ()Teacher بسازیم:
1var teacher1 = new Teacher('Dave'، 'Griffiths'، 31، 'male'، ['football'، 'cookery']، 'mathematics');
اکنون کد را ذخیره و رفرش کنید و تلاش کنید به مشخصهها و متدهای شیء techer1 دسترسی پیدا کنید. برای نمونه:
1teacher1.name.first;
2teacher1.interests[0];
3teacher1.bio();
4teacher1.subject;
5teacher1.greeting();
6teacher1.farewell();
این کد باید به خوبی کار کند. کوئریهای موجود در خطهای 1، 2، 3 و 6 به اعضای به ارث رسیده از سازنده ()Person ژنریک دسترسی مییابند. کوئری روی خط 4 به یک عضو دسترسی پیدا میکند که تنها روی سازنده تخصصی ()Teacher وجود دارد. کوئری خط 5 نیز به یک عضو به ارث رسیده از ()Person دسترسی دارد به جز این که ()Techer عضو خاص خود را با همان نام دارد و از این رو کوئری به آن عضو دسترسی مییابد.
نکته: اگر در اجرای کد فوق مشکل دارید، کدتان را با نسخه نهایی زیر مقایسه کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript inheritance</title>
6 </head>
7
8 <body>
9 <div>
10 <label for="jscode">Enter code:</label>
11 <input type="text" id="jscode">
12 <button>Submit code</button>
13 </div>
14
15 <p></p>
16 </body>
17
18 <script>
19 var input = document.querySelector('input');
20 var btn = document.querySelector('button');
21 var para = document.querySelector('p');
22 btn.onclick = function() {
23 var code = input.value;
24 para.textContent = eval(code);
25 }
26 function Person(first, last, age, gender, interests) {
27 this.name = {
28 first,
29 last
30 };
31 this.age = age;
32 this.gender = gender;
33 this.interests = interests;
34 };
35 Person.prototype.bio = function() {
36 // First define a string, and make it equal to the part of
37 // the bio that we know will always be the same.
38 var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
39 // define a variable that will contain the pronoun part of
40 // the sencond sentence
41 var pronoun;
42 // check what the value of gender is, and set pronoun
43 // to an appropriate value in each case
44 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
45 pronoun = 'He likes ';
46 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
47 pronoun = 'She likes ';
48 } else {
49 pronoun = 'They like ';
50 }
51 // add the pronoun string on to the end of the main string
52 string += pronoun;
53 // use another conditional to structure the last part of the
54 // second sentence depending on whether the number of interests
55 // is 1, 2, or 3
56 if(this.interests.length === 1) {
57 string += this.interests[0] + '.';
58 } else if(this.interests.length === 2) {
59 string += this.interests[0] + ' and ' + this.interests[1] + '.';
60 } else {
61 // if there are more than 2 interests, we loop through them
62 // all, adding each one to the main string followed by a comma,
63 // except for the last one, which needs an and & a full stop
64 for(var i = 0; i < this.interests.length; i++) {
65 if(i === this.interests.length - 1) {
66 string += 'and ' + this.interests[i] + '.';
67 } else {
68 string += this.interests[i] + ', ';
69 }
70 }
71 }
72 // finally, with the string built, we alert() it
73 alert(string);
74 };
75 Person.prototype.greeting = function() {
76 alert('Hi! I\'m ' + this.name.first + '.');
77 };
78 Person.prototype.farewell = function() {
79 alert(this.name.first + ' has left the building. Bye for now!');
80 }
81 var person1 = new Person('Tammi', 'Smith', 17, 'female', ['music', 'skiing', 'kickboxing']);
82 function Teacher(first, last, age, gender, interests, subject) {
83 Person.call(this, first, last, age, gender, interests);
84 this.subject = subject;
85 }
86 Teacher.prototype = Object.create(Person.prototype);
87 Teacher.prototype.constructor = Teacher;
88 Teacher.prototype.greeting = function() {
89 var prefix;
90 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
91 prefix = 'Mr.';
92 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
93 prefix = 'Mrs.';
94 } else {
95 prefix = 'Mx.';
96 }
97 alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');
98 };
99 var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');
100 </script>
101</html>
تکنیکی که در اینجا مورد بررسی قرار دادیم، تنها روش ایجاد کلاسهای ارث برنده در جاوا اسکریپت نیست؛ اما کارکرد خوبی دارد و ایده مناسبی از شیوه پیادهسازی وراثت در جاوا اسکریپت به شما میدهد.
همچنین ممکن است به برسی برخی از قابلیتهای جدید ECMAScript نیز علاقه داشته باشید که امکان اجرای وراثت را به روشی تمیزتر در جاوا اسکریپت در اختیار ما قرار میدهد. ما این موارد را در این مقاله بررسی نکردیم، چون هنوز روی مرورگرهای زیادی پشتیبانی نمیشوند. همه سازندههایی دیگری که در این مجموعه مقالات مورد بررسی قرار دادیم از مرورگر نسخه IE9 به بعد (و شاید قدیمیتر) پشتیبانی میشوند و روشهایی برای پشتیبانی از آنها در مرورگرهای قدیمیتر نیز وجود دارد.
یک روش متداول برای این منظور استفاده از یک کتابخانه جاوا اسکریپت است. اغلب گزینههای رایج یک مجموعه ساده از کارکردها را دارند که برای اجرای وراثت به روشی آسانتر و سریعتر مورد استفاده قرار میگیرند برای مثال CoffeeScript موارد class ،extends و غیره را ارائه میکند.
یک تمرین دیگر
در بخش تئوری برنامهنویسی شیءگرا در ابتدای این مقاله یک کلاس Student را نیز به عنوان یک مفهوم گنجاندهایم که همه ویژگیهای Person را به ارث میرسد و ضمناً یک متد ()greeting از Person دارد که بسیار غیررسمیتر از معرفی Teacher است. نگاهی به تعریف خوشامدگویی و معرفی دانشآموز در این بخش داشته باشید و تلاش کنید تا سازنده ()Student را که همه ویژگیهای ()Person را به ارث میبرد پیادهسازی کنید و علاوه بر آن یک تابع ()greeting متفاوت را نیز پیادهسازی کنید.
نکته: اگر در اجرایی کردن این تمرین مشکل داشتید، نسخه نهایی را میتوانید در ادامه ملاحظه کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript inheritance</title>
6 </head>
7
8 <body>
9 <div>
10 <label for="jscode">Enter code:</label>
11 <input type="text" id="jscode">
12 <button>Submit code</button>
13 </div>
14
15 <p></p>
16 </body>
17
18 <script>
19 var input = document.querySelector('input');
20 var btn = document.querySelector('button');
21 var para = document.querySelector('p');
22 btn.onclick = function() {
23 var code = input.value;
24 para.textContent = eval(code);
25 }
26 function Person(first, last, age, gender, interests) {
27 this.name = {
28 first,
29 last
30 };
31 this.age = age;
32 this.gender = gender;
33 this.interests = interests;
34 };
35 Person.prototype.bio = function() {
36 // First define a string, and make it equal to the part of
37 // the bio that we know will always be the same.
38 var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
39 // define a variable that will contain the pronoun part of
40 // the sencond sentence
41 var pronoun;
42 // check what the value of gender is, and set pronoun
43 // to an appropriate value in each case
44 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
45 pronoun = 'He likes ';
46 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
47 pronoun = 'She likes ';
48 } else {
49 pronoun = 'They like ';
50 }
51 // add the pronoun string on to the end of the main string
52 string += pronoun;
53 // use another conditional to structure the last part of the
54 // second sentence depending on whether the number of interests
55 // is 1, 2, or 3
56 if(this.interests.length === 1) {
57 string += this.interests[0] + '.';
58 } else if(this.interests.length === 2) {
59 string += this.interests[0] + ' and ' + this.interests[1] + '.';
60 } else {
61 // if there are more than 2 interests, we loop through them
62 // all, adding each one to the main string followed by a comma,
63 // except for the last one, which needs an and & a full stop
64 for(var i = 0; i < this.interests.length; i++) {
65 if(i === this.interests.length - 1) {
66 string += 'and ' + this.interests[i] + '.';
67 } else {
68 string += this.interests[i] + ', ';
69 }
70 }
71 }
72 // finally, with the string built, we alert() it
73 alert(string);
74 };
75 Person.prototype.greeting = function() {
76 alert('Hi! I\'m ' + this.name.first + '.');
77 };
78 Person.prototype.farewell = function() {
79 alert(this.name.first + ' has left the building. Bye for now!');
80 }
81 var person1 = new Person('Tammi', 'Smith', 17, 'female', ['music', 'skiing', 'kickboxing']);
82 function Teacher(first, last, age, gender, interests, subject) {
83 Person.call(this, first, last, age, gender, interests);
84 this.subject = subject;
85 }
86 Teacher.prototype = Object.create(Person.prototype);
87 Teacher.prototype.constructor = Teacher;
88 Teacher.prototype.greeting = function() {
89 var prefix;
90 if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
91 prefix = 'Mr.';
92 } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
93 prefix = 'Mrs.';
94 } else {
95 prefix = 'Mx.';
96 }
97 alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');
98 };
99 var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');
100 // Student class!
101 function Student(first, last, age, gender, interests) {
102 Person.call(this, first, last, age, gender, interests);
103 }
104 Student.prototype = Object.create(Person.prototype);
105 Student.prototype.constructor = Student;
106 Student.prototype.greeting = function() {
107 alert('Yo! I\'m ' + this.name.first + '.');
108 };
109 var student1 = new Student('Liz', 'Sheppard', 17, 'female', ['ninjitsu', 'air cadets']);
110 </script>
111</html>
جمعبندی عضو شیء
برای جمعبندی باید اشاره کنیم که همواره سه نوع مشخصه/متد وجود دارند که باید مورد توجه قرار گیرند:
آنهایی که در تابع سازنده تعریف شدهاند و به وهلههای شیء داده میشوند. یافتن این موارد نسبتاً آسان است، چون در کد خاص خودتان آن اعضایی هستند که در سازنده و با استفاده از خطوطی مانند زیر تعریف میشوند:
1this.x = x
در کد داخلی مرورگر این موارد اعضایی هستند که تنها در اختیار وهلههای شیء هستند و به طور معمول با فراخوانی سازنده با استفاده از کلیدواژه new به صورت زیر ساخته میشوند:
1var myInstance = new myConstructor()
آنهایی که مستقیماً در خود سازنده تعریف میشوند و تنها در دسترس سازنده هستند. این مواد به طور معمول در اختیار اشیای داخلی مرورگر هستند و بر اساس این که به صورت مستقیم به یک سازنده متصل شدهاند و نه به یک وهله، شناسایی میشوند. برای نمونه:
1Object.keys()
آنهایی که در پروتوتایپ یک سازنده تعریف شدهاند و از سوی همه وهلهها و کلاسهای شیء ارثبرنده به ارث میرسند. این موارد شامل همه اعضای تعریف شده روی مشخصه پروتوتایپ سازنده مانند زیر هستند:
1myConstructor.prototype.x()
اگر مطمئن نیستید که کدام یک از موارد فوق، کدام هستند، لازم نیست مضطرب شوید، چون همچنان در حال یادگیری هستید و با تمرین بیشتر میتوانید بیشتر با آنها آشنا شوید.
کلاسهای ECMAScript 2015
ECMAScript 2015 مفهوم «ساختار کلاس» (class syntax) را به عنوان یک روش جدید برای نوشتن کلاسهای با قابلیت استفاده مجدد و با ساختار سادهتر و تمیزتر در جاوا اسکریپت معرفی کرد. این ساختار شباهت زیادی به کلاسها در C++ یا جاوا دارد. در این بخش مثالهای Person و Teacher را در مورد وراثت پروتوتایپی برای کلاسها مورد استفاده قرار میدهیم تا شیوه انجام کار را نمایش دهیم.
نکته: روش مدرن نوشتن کلاسها در همه مرورگرهای مدرن پشتیبانی میشود؛ اما همچنان لازم است با طرز کار درونی وراثت پروتوتایپی آشنا باشید تا در مواردی که روی پروژهای کار میکنید و به پشتیبانی مرورگرهای قدیمی که از این ساختارها پشتیبانی نمیکنند نیاز دارید، دچار مشکل نشوید.
در ادامه نسخه بازنویسی شده از مثال Person را به سبک کلاس ملاحظه میکنید:
1class Person {
2 constructor(first, last, age, gender, interests) {
3 this.name = {
4 first,
5 last
6 };
7 this.age = age;
8 this.gender = gender;
9 this.interests = interests;
10 }
11
12 greeting() {
13 console.log(`Hi! I'm ${this.name.first}`);
14 };
15
16 farewell() {
17 console.log(`${this.name.first} has left the building. Bye for now!`);
18 };
19}
گزاره class نشان میدهد که مشغول ساخت یک کلاس جدید هستیم. درون این بلوک همه ویژگیهای کلاس را تعریف کردهایم. متد ()constructor تابع سازندهای تعریف میکند که کلاس Person ما را نمایش میدهد. ()greeting و ()farewell دو متد کلاس ما هستند. هر متدی که میخواهید با کلاس مرتبط باشد درون آن و پس از سازنده تعریف میشود. در این مثال، ما از template literals به جای الحاق رشتهای استفاده کردهایم تا خوانایی کد افزایش پیدا کند.
اکنون میتوانیم وهلههای شیء را با استفاده از عملگر new وهلهسازی کنیم و این کار دقیقاً به همان روشی که قبلاً انجام دادیم، اجرا خواهد شد:
1let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
2han.greeting();
3// Hi! I'm Han
4
5let leia = new Person('Leia', 'Organa', 19, 'female', ['Government']);
6leia.farewell();
7// Leia has left the building. Bye for now
نکته: کلاسهای شما در پشت صحنه به مدلهای وراثت پروتوتایپی تبدیل میشوند و این وضعیت صرفاً یک «تغییر ظاهری» (syntactic sugar) است. البته مطمئناً موافق هستید که نوشتن این کد سادهتر است.
وراثت با ساختار کلاس
در بخش فوق ما یک کلاس ساختهایم که یک «شخص» (Person) را نمایش میدهد. این شخص یک سری خصوصیتها دارد که در میان همه افراد مشترک است. در این بخش کلاس Teacher تخصصی خودمان را میسازیم که با استفاده از ساختار مدیریت class از Person ارثبری میکند. این کار ایجاد «زیرکلاس» (subclass) یا زیرکلاس سازی (Subclassing) نامیده میشود.
برای ایجاد یک زیرکلاس باید از کلیدواژه extend استفاده کنیم تا به جاوا اسکریپت اعلام کنیم که کلاسی که ساخته میشود بر مبنای کلاس دیگری ساخته میشود:
1class Teacher extends Person {
2 constructor(subject, grade) {
3 this.subject = subject;
4 this.grade = grade;
5 }
6}
اما یک مشکل کوچک وجود دارد. برخلاف تابعهای سازنده به سبک قدیم که عملگر new کار مقداردهی اولیه this به یک شیء جدیداً تخصیصیافته را بر عهده داشت، این کار برای کلاسی که با استفاده از کلیدواژه extend تعریف میشود، یعنی زیرکلاس جدید، به صورت خودکار انجام نمییابد. از این رو اجرای کد فوق خطایی به صورت زیر ایجاد میکند:
1Uncaught ReferenceError: Must call super constructor in derived class before
2accessing 'this' or returning from derived constructor
در مورد زیرکلاسها، مقداردهی اولیه this برای یک شیء جدیداً تخصیصیافته همواره به سازنده کلاس والد یعنی تابع سازنده کلاسی که از آن بسط میدهیم، وابسته است.
در اینجا ما در حال بسط دادن کلاس Person هستیم و زیرکلاس Teacher، بسطی از کلاس Person محسوب میشود. از این رو مقداردهی اولیه this برای Teacher با استفاده از سازنده Person صورت میپذیرد.
برای فراخوانی سازنده والد باید از عملگر ()super به صورت زیر استفاده کنیم:
1class Teacher extends Person {
2 constructor(subject, grade) {
3 super(); // Now `this` is intialized by calling the parent constructor.
4 this.subject = subject;
5 this.grade = grade;
6 }
7}
در صورتی که یک زیرکلاس نتواند مشخصههای خود را از کلاس والد به ارث ببرد، ایجاد آن هیچ فایدهای نخواهد داشت. خبر خوب این است که عملگر ()super آرگومانهایی برای سازنده والد نیز قبول میکند.
اگر به سازنده ()Person نگاه مجددی داشته باشیم، میبینیم که بلوک کد زیر در متد سازنده آن وجود دارد:
1constructor(first, last, age, gender, interests) {
2 this.name = {
3 first,
4 last
5 };
6 this.age = age;
7 this.gender = gender;
8 this.interests = interests;
9}
از آنجا که عملگر ()super در واقع سازنده کلاس والد است، ارسال آرگومانهای ضروری سازنده کلاس والد به آن موجب مقداردهی اولیه مشخصههای کلاس والد در زیرکلاس نیز میشود و بدین ترتیب از آن ارثبری میکند:
1class Teacher extends Person {
2 constructor(first, last, age, gender, interests, subject, grade) {
3 super(first, last, age, gender, interests);
4
5 // subject and grade are specific to Teacher
6 this.subject = subject;
7 this.grade = grade;
8 }
9}
اکنون زمانی که وهلههای شیء Teacher را مقدر دهی اولیه بکنیم، میتوانیم متدها و مشخصههای تعریف شده روی هر دو کلاس Teacher و Person را چنان که انتظار داریم، فراخوانی بکنیم:
1let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5);
2snape.greeting(); // Hi! I'm Severus.
3snape.farewell(); // Severus has left the building. Bye for now.
4snape.age // 58
5snape.subject; // Dark arts
همانند وضعیتی که در Teacher داشتیم، میتوانیم زبرکلاسهای دیگری از Person را نیز ایجاد بکنیم تا بدون نیاز به ایجاد تغییراتی در کلاس پایه، موارد تخصصیتری را داشته باشیم.
کد کامل این مثال به صورت زیر است:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript inheritance</title>
6 </head>
7
8 <body>
9
10 </body>
11
12 <script>
13 // Basic Person class
14 class Person {
15 constructor(first, last, age, gender, interests) {
16 this.name = {
17 first,
18 last
19 };
20 this.age = age;
21 this.gender = gender;
22 this.interests = interests;
23 }
24 greeting() {
25 console.log(`Hi! I'm ${this.name.first}`);
26 };
27 farewell() {
28 console.log(`${this.name.first} has left the building. Bye for now!`);
29 };
30 }
31 // Instantiate Person
32 let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
33 han.greeting();
34 // Hi! I'm Han
35 let leia = new Person('Leia', 'Organa', 19, 'female' ['Government']);
36 leia.farewell();
37 // Leia has left the building. Bye for now
38 // Extend Person with Teacher
39 class Teacher extends Person {
40 constructor(first, last, age, gender, interests, subject, grade) {
41 super(first, last, age, gender, interests);
42 // subject and grade are specific to Teacher
43 this.subject = subject;
44 this.grade = grade;
45 }
46 }
47 // Instantiate Teacher
48 let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5);
49 snape.greeting(); // Hi! I'm Severus.
50 snape.farewell(); // Severus has left the building. Bye for now.
51 console.log(snape.age) // 58
52 console.log(snape.subject) // Dark arts
53 </script>
54</html>
Getter-ها و Setter-ها
ممکن است مواردی باشند که بخواهیم مقادیر یک خصوصیت را در کلاسهایی که ایجاد کردهایم، تغییر دهیم یا ندانیم که مقدار نهایی یک خصوصیت چه مقدار خواهد بود. با بهرهگیری از مثال Teacher فوق، شاید پیش از ایجاد شیءهای Teacher ندانیم که موضوع تدریس آن معلم چه خواهد بود و یا این که در طول سال تحصیلی موضوع تدریس معلم تغییر پیدا کند. چنین موقعیتهایی را میتوان با Getter-ها و Setter-ها مدیریت کرد.
در ادامه کلاس Teacher را با استفاده از Getter-ها و Setter-ها بهبود میبخشیم. آغاز این کلاس همانند قبل خواهد بود. Getter-ها و Setter-ها به صورت متقابل با یادگیری همکاری دارند. یک Getter مقدار کنونی یک متغیر را بازگشت میدهد و Setter مربوطه نیز مقدار متغیر را به مقدار تعریف شده تغییر میدهد.
کلاس Teacher اصلاحشده مانند زیر خواهد بود:
1class Teacher extends Person {
2 constructor(first, last, age, gender, interests, subject, grade) {
3 super(first, last, age, gender, interests);
4 // subject and grade are specific to Teacher
5 this._subject = subject;
6 this.grade = grade;
7 }
8
9 get subject() {
10 return this._subject;
11 }
12
13 set subject(newSubject) {
14 this._subject = newSubject;
15 }
16}
در کلاس فوق ما یک getter و یک setter برای مشخصه subject داریم. از _ برای ایجاد یک مقدار مجزا استفاده میکنیم که در آن نام مشخصه خود را ذخیره میکنیم. اگر از این قرارداد نامگذاری استفاده نکنیم، در هر بار فراخوانی get یا set ممکن است با خطاهایی مواجه شویم. در این زمان:
برای نمایش مقدار کنونی مشخصه subject_ برای شیء snape میتوانیم از متد getter به صورت snape.subject استفاده کنیم. برای انتساب یک مقدار جدید به مشخصه subject_ میتوانیم از متد setter به صورت "snape.subject="new value استفاده کنیم.
مثال زیر دو تابع را در عمل نشان میدهد:
1// Check the default value
2console.log(snape.subject) // Returns "Dark arts"
3
4// Change the value
5snape.subject="Balloon animals" // Sets _subject to "Balloon animals"
6
7// Check it again and see if it matches the new value
8console.log(snape.subject) // Returns "Balloon animals"
نکته: کد کامل این بخش را میتوانید در ادامه ملاحظه بکنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Object-oriented JavaScript inheritance</title>
6 </head>
7
8 <body>
9
10 </body>
11
12 <script>
13 // Basic Person class
14 class Person {
15 constructor(first, last, age, gender, interests) {
16 this.name = {
17 first,
18 last
19 };
20 this.age = age;
21 this.gender = gender;
22 this.interests = interests;
23 }
24 greeting() {
25 console.log(`Hi! I'm ${this.name.first}`);
26 };
27 farewell() {
28 console.log(`${this.name.first} has left the building. Bye for now!`);
29 };
30 }
31 // Extend Person with Teacher
32 class Teacher extends Person {
33 constructor(first, last, age, gender, interests, subject, grade) {
34 super(first, last, age, gender, interests);
35 // subject and grade are specific to Teacher
36 this._subject = subject;
37 this.grade = grade;
38 }
39 get subject() {
40 return this._subject;
41 }
42 set subject(newSubject) {
43 this._subject = newSubject;
44 }
45 }
46 // Instantiate Teacher
47 let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5);
48 // Check the default value
49 console.log(snape.subject) // Returns "Dark arts"
50 // Change the value
51 snape.subject="Balloon animals" // Sets subject to "Balloon animals"
52 // Check it again and see if it matches the new value
53 console.log(snape.subject) // Returns "Balloon animals"
54 </script>
55</html>
چه زمانی باید از وراثت در جاوا اسکریپت استفاده بکنیم؟
احتمالاً پس از مطالعه این مقاله و مقاله قبلی به این نتیجه رسیده باشید که وراثت موضوع پیچیدهای است، که البته همین طور است. پروتوتایپها و وراثت برخی از پیچیدهترین جنبههای جاوا اسکریپت را شامل میشوند؛ اما بخش بزرگی از قدرت و انعطافپذیری جاوا اسکریپت نیز از ساختار شیء و وراثت ناشی میشود و آشنایی با طرز کار آن قطعاً، ارزش زمان و تلاشی که صرف میشود را دارد.
در واقع ما همواره از وراثت استفاده میکنیم. هر زمان که از ویژگیهای مختلف یک Web API استفاده میکنید، یا متدها/مشخصههای تعریف شده روی یک شیء داخلی مرورگر را روی یک رشته فراخوانی میکنید، به طور صریحی از وراثت استفاده کردهاید.
اما اگر بخواهید از وراثت در کد خود استفاده کنید، این استفاده گسترده نخواهد بود، چون در ابتدای برنامهنویسی و به خصوص پروژههای کوچک چنین کاری لزوم چندانی ندارد. استفاده از شیءها و وراثت صرفاً برای این که از آنها استفاده کرده باشیم و ضرورتی نیز برای این کار نباشد، هدر دادن زمان محسوب میشود. اما زمانی که کد شما بزرگتر میشود؛ احتمالاً با مواردی مواجه خواهید شد که به آن نیاز پیدا میکنید. اگر دیدید که مشغول ساخت اشیای مختلفی هستید که ویژگیهای مشابهی دارند، در این صورت ایجاد یک نوع شیء ژنریک و گنجاندن همه کارکردهای مشابه در آن و ارث بردن آن ویژگیها در انواع شیءهای تخصصیتر میتواند یک راهکار مفید و آسان باشد.
نکته: به دلیل طرز کار جاوا اسکریپت با زنجیره پروتوتایپی و مواردی از این دست، اشتراک کاربردها بین شیءها در اغلب موارد به نام «وکالت» (delegation) نامیده میشود. شیءهای تخصصی کارکردها را از یک نوع شیء عمومیتر به صورت «وکالت» به دست میآورند.
هنگامی که از وراثت استفاده میکنید، باید مراقب باشید که از سطوح زیادی از وراثت استفاده نکنید و به دقت رد مکانهایی که متدها و مشخصهها را تعریف میکنید، در یاد داشته باشید. ممکن است شروع به نوشتن کدی بکنید که موقتاً پروتوتایپهای اشیای داخلی مرورگر را تغییر میدهد؛ اما به جز در مواردی که دلیل کاملاً قوی برای این کار داشته باشید، نباید آن را انجام دهید. وراثت بیش از حد میتواند منجر به سردرگمی زیادی شود و دیباگ کردن چنین کدی نیز زحمت زیادی را به همراه دارد.
در نهایت باید گفت که شیءها صرفاً یک روش دیگر برای استفاده مجدد از کد مانند تابعها و حلقهها محسوب میشوند و نقشها و مزیتهای خاص خود را دارند. اگر دیدید که مشغول ساخت یک دسته از متغیرها و تابعهای مرتبط با هم هستید و میخواهید همه آنها را با هم در یک پکیج قرار دهید، استفاده از شیء ایده مناسبی به نظر میرسد. اشیا در مواردی که بخواهید یک مجموعه از دادهها را از یک مکان به مکان دیگر ارسال بکنید نیز کاملاً مفید خواهند بود. هر دو این موارد بدون استفاده از سازندهها یا وراثت نیز قابل وصول هستند. اگر تنها به یک وهله از یک شیء نیاز دارید، در این صورت احتمالاً بهتر است که صرفاً از یک object literal استفاده کنید و قطعاً نیازی به وراثت هم نخواهید داشت.
جایگزینهایی برای بسط دادن زنجیره پروتوتایپی
چندین روش برای بسط دادن پروتوتایپ یک شیء به جز آن چه در بخشهای قبلی این مقاله مشاهده کردید، در جاوا اسکریپت وجود دارد. برای مشاهده این روشها پیشنهاد میکنیم به بخشهای آتی این مقاله مراجعه کنید.
در این مقاله باقیمانده مباحث نظریه شیءگرایی در جاوا اسکریپت و ساختار آن را بررسی کردیم. در این زمان شما با شیء جاوا اسکریپت و مبانی برنامهنویسی شیءگرا، پروتوتایپها و وراثت پروتوتایپی، روش ایجاد (سازندههای) کلاس و وهلههای شیء، افزودن ویژگی به کلاسها و ایجاد زیرکلاسهایی که از کلاسهای دیگر ارثبری میکنند، آشنا شدهاید.
در بخش بعدی این سری مقالات آموزش کاربردی جاوا اسکریپت نگاهی به شیوه کار با «نمادگذاری شیء جاوا اسکریپت» (JavaScript Object Notation) با اختصار JSON خواهیم داشت که یک روش متداول برای مبادله دادهها با اشیای جاوا اسکریپت است.
برای مطالعه قسمت بعدی این مجموعه مطلب آموزشی روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوااسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوااسکریپت)
- جاوا اسکریپت چیست؟ — به زبان ساده
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
==