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


این مقاله برای توسعهدهندگان جاوا اسکریپت نوشته شده است که دانش قبلی در خصوص برنامه نویسی شیء گرا (OOP) ندارند. در این مطلب روی بخشهایی از OOP از جمله مفهوم کلاس در برنامه نویسی شی گرا متمرکز شدهایم که به جاوا اسکریپت مرتبط هستند و مفاهیم عمومی شیءگرایی را بررسی نمیکنیم. برای نمونه به بحث چندریختی (polymorphism) نمیپردازیم، زیرا این مبحث بیشتر در زبانهای دارای نوعهای استاتیک کاربرد دارد.
چرا باید شیءگرایی را بیاموزیم؟
اگر جاوا اسکریپت را به عنوان نخستین زبان خود انتخاب کردهاید و میخواهید یک توسعهدهنده حرفهای تبدیل شوید که روی سیستمهای بزرگ سازمانی با گستره صدها هزار خط کد یا بیشتر کار کنید، میبایست به طور کامل با مفاهیم برنامهنویسی شیءگرا آشنا باشید.
ذهنیتهای مختلف
در یک بازی فوتبال رویکردهای مختلفی را میتوان انتخاب کرد. برای مثال میتوان به صورت دفاعی بازی کرد، یا به سانتر توپهای بلند از کنارهها اقدام کرد و یا آن که میتوان طوری به حمله پرداخت که گویی هیچ چیز مهم دیگری وجود ندارد. اما همه این راهبردها یک هدف دارند و آن برنده شدن در بازی است.
همین واقعیت در مورد پارادایمهای برنامهنویسی نیز صدق میکند. روشهای مختلفی برای برخورد با یک مسئله و طراحی یک راهحل وجود دارد. برنامهنویسی شیءگرا یا OOP پارادایمی برای توسعه اپلیکیشنهای مدرن است. این پارادایم از سوی زبانهای اصلی مانند جاوا، #C یا جاوا اسکریپت پشتیبانی میشود.
پارادایم شیءگرایی
از منظر OOP یک اپلیکیشن مجموعهای از شیءها است که با همدیگر ارتباط دارند. ما این شیءها را بر مبنای چیزهایی در دنیای واقعی طراحی مانند محصولاتی در انبار یا پروندههای کارگزینی ایجاد کردهایم. شیءها حاوی داده هستند و منطقهای خاصی را برای مبنای دادههای خود اجرا میکنند. در نتیجه درک کد OOP بسیار آسان است. آن چه که آسان نیست، تصمیمگیری در مورد تجزیه یک اپلیکیشن به شیءهای کوچک است.
شاید اولین بار که با عبارت برنامهنویسی شیءگرا برخورد کردهاید هیچ گونه ذهنیتی از آن نداشتهاید، چون بسیار انتزاعی به نظر میرسد. چنین طرز فکری به هیچ وجه اشکال ندارد. نکته مهم این است که وقتی با این ایده برخورد میکنید آن را به خاطر سپرده و تلاش کنید OOP را در کد خود به کار بگیرید. در ادامه و در طی زمان تجربه کسب میکنید و کدهای بیشتری را که مینویسید با این مفاهیم نظری همسو میسازید.
نکته: OOP بر مبنای شیءهای دنیای واقعی ساخته شده و هر کس میتواند با خواندن کد، آن چه را در حال وقوع است درک کند.
شیء به عنوان مرکز توجه
یک مثال ساده کمک میکند که بدانید جاوا اسکریپت چگونه مفاهیم بنیادی OOP را پیادهسازی کرده است. یک مثال خرید را تصور کنید که در طی آن محصولهایی را که خریداری کردهاید را درون سبدی قرار میدهید و سپس قیمت کلی که باید پرداخت کنید را محاسبه میکنید. اگر از دانش جاوا اسکریپت خود استفاده کنید و این موقعیت را بدون استفاده از OOP کدنویسی بکنید به صوت زیر خواهد بود:
const bread = {name: 'Bread', price: 1}; const water = {name: 'Water', price: 0.25}; const basket = []; basket.push(bread); basket.push(bread); basket.push(water); basket.push(water); basket.push(water); const total = basket .map(product => product.price) .reduce((a, b) => a + b, 0); console.log('one has to pay in total: ' + total);
چشمانداز OOP باعث میشود که کدهای بهتر را با سهولت بیشتر بنویسید، زیرا ما شیءها را همان طوری که در دنیای واقعی با آنها برخورد داریم، تصور میکنیم. از آنجا که مثال موردی ما شامل یک سبد از محصولها است، هم اینک دو نوع شیء داریم، یک شیء سبد و یکی هم اشیای محصول.
نسخه OOP سبد خرید را میتوان به صورت زیر نیز نوشت:
const bread = new Product('bread', 1); const water = new Product('water', .25) const basket = new Basket(); basket.addProduct(2, bread); basket.addProduct(3, water); basket.printShoppingInfo();
همان طور که در خط نخست، شاهد هستید، یک شیء جدید با استفاده از کلیدواژه new و در ادامه نام آن چه یک کلاس نامیده میشود، ایجاد کردهایم. این دستور یک شیء باز میگرداند که آن را در متغیر bread ذخیره میکنیم. این فرایند را برای متغیر water تکرار میکنیم و برای متغیر basket نیز مسیر مشابهی را طی میکنیم. پس از این که این محصولها را به سبد اضافه کردیم در نهایت مقدار کل مبلغی که باید پرداخت شود را محاسبه میکنیم.
تفاوت بین دو قطعه کد بدیهی است. نسخه OOP مانند جملههای زبان معمولی است و به سادگی میتوان دریافت که چه کاری در حال اجرا است.
نکته: یک شیء بر مبنای چیزهای دنیای واقعی مدلسازی میشود که شامل دادهها و تابعها است.
کلاس به عنوان قالب
ما از کلاسها در OOP به عنوان قالبهایی برای ایجاد شیء استفاده میکنیم. یک شیء «وهلهای از یک کلاس» است و وهلهسازی (instantiation) به فرایند ایجاد یک شیء بر مبنای یک کلاس گفته میشود. کد آن در کلاس تعریف شده است؛ اما نمیتواند به جز در یک شیء زنده اجرا شود.
کلاسها را میتوان مانند یک نقشه اولیه برای یک خودرو در نظر گرفت. این نقشه خصوصیات خودرو مانند گشتاور و اسب بخار، کارکردهای درونی مانند نسبت هوا به سوخت و متدهای در دسترس عموم مانند احتراق را تعریف میکند. اما تنها زمانی که کارخانه یک وهله را از روی آن بسازد، شما میتوانید سوئیچ خودرو را چرخانده و آن را برانید.
در مورد مثال کاربردی قبلی، ما از کلاس Product برای وهلهسازی دو شیء نان و آب استفاده کردیم. البته این اشیا به کدهایی نیاز دارند که در کلاس تعریف میشود. آن کدها به صورت زیر هستند:
function Product(_name, _price) { const name = _name; const price = _price; this.getName = function() { return name; }; this.getPrice = function() { return price; }; } function Basket() { const products = []; this.addProduct = function(amount, product) { products.push(...Array(amount).fill(product)); }; this.calcTotal = function() { return products .map(product => product.getPrice()) .reduce((a, b) => a + b, 0); }; this.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal()); }; }
یک کلاس در جاوا اسکریپت مانند یک تابع به نظر میرسد؛ اما از آن به طرز متفاوتی استفاده میکنیم. نام تابع همان نام کلاس است که با حروف بزرگ نوشته شده است. از آنجا که این نام هیچ چیزی باز نمیگرداند، آن را به حالت معمول فراخوانی تابع مانند زیر:
const basket = Product('bread', 1);
مورد ارجاع قرار نمیدهیم؛ بلکه کلید واژه new مانند:
const basket = new Product('bread', 1);
مورد استفاده قرار میگیرد.
کد درون تابع همان سازنده (constructor) است. این کد هر بار که وهلهای از یک شیء ساخته میشود، اجرا خواهد شد. Product دارای پارامترهای name_ و price_ است. هر شیء جدید این مقادیر را درون خود دارد.
به علاوه، میتوانیم کارکردهایی که شیء ارائه میدهد را نیز تعریف کنیم. این کارکردها از طریق استفاده از کلیدواژه this تعریف میشوند که موجب میشود از سمت بیرون نیز قابل دسترسی باشد (به مبحث کپسولهسازی مراجعه کنید) دقت کنید که این تابع دسترسی کاملی به خصوصیات دارد.
کلاس سبد (Basket) نیازمند هیچ آرگومانی برای ایجاد یک شیء جدید نیست. وهلهسازی یک شیء Basket جدید به سادگی یک فهرست خالی از محصولها ایجاد میکند که برنامه میتواند پس از آن را پر کند.
نکته: کلاس قالبی برای تولید اشیا در زمان اجرا است.
کپسولهسازی
ممکن است با نسخه دیگری از شیوه تعریف کلاس نیز مواجه شده باشید:
function Product(name, price) { this.name = name; this.price = price; }
به انتساب مشخصات به متغیر this توجه کنید. در نگاه اول به نظر میرسد که این نسخه بهتر است؛ چون نیازمند متدهای getter یعنی getName و getPrice نیست و از این رو کوتاهتر شده است؛ اما متأسفانه در این حالت از بیرون به طور کامل به خصوصیات دسترسی وجود دارد و از این رو هر کسی میتواند آنها را تغییر دهد:
const bread = new Product('bread', 1); bread.price = -10;
این وضعت قابل قبول نیست و باعث میشود که نگهداری اپلیکیشن دشوارتر شود. اگر بخواهیم کد اعتبارسنجی خاصی مثلا برای این که قیمت نهایی کمتر از 0 نباشد اضافه کنیم چه کار باید بکنیم؟ هر کدی که به خصوصیت مبلغ دسترسی داشته باشد، میتواند این اعتبارسنجی را دور بزند. این وضعیت منجر به بروز خطاهایی میشود که ردگیری آنها نیز دشوار است. در سوی دیگر کدی که از متدهای getter برای شیء استفاده میکند، اجباراً فرایند اعتبارسنجی قیمت شیء را طی میکند.
اشیا باید کنترل کاملی روی دادههایشان داشته باشند. به بیان دیگر شیءها دادههای خود را کپسوله میکنند تا از دسترسی مستقیم اشیای دیگر به دادههایشان جلوگیری کنند. تنها روش برای دسترسی دادهها به صورت مستقیم و از طریق تابعهای نوشته شده برای اشیا است.
دادهها و پردازش (یعنی منطق) به همدیگر تعلق دارند. این مسئله به طور خاص در زمان طراحی اپلیکیشنهای بزرگتر که محدودسازی دادهها به مکانهای کاملاً تعریف شده بسیار مهم است بروز مییابد.
محصولهای OOP دارای طراحی ماژولی هستند که یک نعمت در توسعه نرمافزار محسوب میشود. بدین ترتیب از کدنویسی اسپاگتی که در آن همه چیز به صورت کاملاً تنگاتنگی با هم ارتباط دارند و با تغییر بخش کوچکی همه چیز به هم میریزد، اجتناب میشود.
در مورد مثال اولیه ما، اشیای کلاس Product پس از ایجاد شدن وهله، اجازه ایجاد تغییر در قیمت یا نام را نمیدهند. وهلههای Product فقط-خواندنی هستند.
نکته: کپسولهسازی از دسترسی به دادهها مگر از طریق تابعهای شیء جلوگیری میکند.
وراثت
وراثت امکان ایجاد یک کلاس از طریق بسط دادن کلاس موجود با خصوصیات و کارکردهای اضافی را فراهم میسازد. این کلاس جدید همه ویژگیهای کلاس والد خود را به ارث میبرد و بدین ترتیب دیگر نیاز نیست که کد جدیدی را از صفر بنویسیم. به علاوه، هر تغییری که در کلاس والد صورت بگیرد به طور خودکار از سوی کلاس فرزند در دسترس خواهد بود. این امر موجب میشود که بهروزرسانیها بسیار آسانتر شوند.
فرض کنید کلاس جدیدی به نام Book داریم که یک نام، یک قیمت و یک نویسنده دارد. با وراثت میتوان گفت که Book همان Product است؛ اما خصوصیت نویسنده به آن اضافه شده است. میگوییم که Product سوپرکلاسی از Book است و Book زیرکلاس Product است:
function Book(_name, _price, _author) { Product.call(this, _name, _price); const author = _author; this.getAuthor = function() { return author; } }
دقت کنید که Product.call اضافی به همراه this به عنوان آرگومان اول است. اطلاع داشته باشید که گرچه Book دارای متدهای getter است؛ اما همچنان دسترسی مستقیمی به نام خصوصیت و قیمت آنها ندارد. Book باید دادهها را از کلاس Product بخواند.
اکنون میتوانید بدون هیچ اشکالی یک شیء book به سبد اضافه کنید:
const faust = new Book('faust', 12.5, 'Goethe'); basket.addProduct(1, faust);
Basket انتظار یک شیء از نوع Product را دارد. از آنجا که شیء book از طریق کلاس Book از کلاس Product ارثبری میکند، پس آن نیز یک Product است.
نکته: زیرکلاس میتواند خصوصیات و کارکردهای سوپر کلاس را به ارث ببرد و برخی خصوصیات و کارکردها را نیز از خود داشته باشد.
جاوا اسکریپت و OOP
برای ساخت اپلیکیشنهای جاوا اسکریپت سه پارادایم متفاوت وجود دارد. برنامهنویسی مبتنی بر پروتوتایپ، برنامهنویسی شیءگرا و برنامهنویسی تابع گرا. دلیل این چندگانگی به تاریخچه جاوا اسکریپت مربوط است. جاوا اسکریپت در ابتدا مبتنی بر پروتوتایپ بود. جاوا اسکریپت از ابتدا به منظور استفاده در اپلیکیشنهای بزرگ طراحی نشده بود.
با این حال علیرغم طراحی بنیانگذاران جاوا اسکریپت، توسعهدهندگان از جاوا اسکریپت برای اپلیکیشنهای بزرگ نیز استفاده کردند. به همین دلیل در جاوا اسکریپت OOP بر روی تکنیک ابتدایی مبتنی بر پروتوتایپ سوار شده است. رویکرد مبتنی بر پروتوتایپ در ادامه توضیح داده شده است. این همان روش کلاسیک و پیشفرض برای ساخت کلاسها است. متأسفانه در این روش از کپسولهسازی پشتیبانی نمیشود.
حتی با این که سطح پشتیبانی جاوا اسکریپت از OOP در سطح دیگر زبانها مثل جاوا نیست؛ اما تکامل زیادی یافته است. با انتشار نسخه ES6 یک کلید واژه اختصاصی class ارائه شده است که میتوان مورد استفاده قرار داد. این کلیدواژه به صورت درونی همان هدف مشخصه پروتوتایپ را ارائه میکند؛ اما اندازه کد را کاهش داده است. با این حال کلاسهای ES6 همچنان فاقد مشخصات خصوصی هستند و به همین دلیل به روش قدیمی عمل میکنند.
به منظور این که این راهنما کاملتر باشد، در ادامه روش نوشتن Product، Basket و Book با استفاده از کلاسهای ES6 و همچنین با استفاده از رویکرد پروتوتایپ (روش پیشفرض و کلاسیک) ارائه شده است. دقت کنید که این نسخهها از کپسولهسازی پشتیبانی نمیکنند:
// ES6 version class Product { constructor(name, price) { this.name = name; this.price = price; } } class Book extends Product { constructor(name, price, author) { super(name, price); this.author = author; } } class Basket { constructor() { this.products = []; } addProduct(amount, product) { this.products.push(…Array(amount).fill(product)); } calcTotal() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0); } printShoppingInfo() { console.log('one has to pay in total: ' + this.calcTotal()); } } const bread = new Product('bread', 1); const water = new Product('water', 0.25); const faust = new Book('faust', 12.5, 'Goethe'); const basket = new Basket(); basket.addProduct(2, bread); basket.addProduct(3, water); basket.addProduct(1, faust); basket.printShoppingInfo(); //Prototype version function Product(name, price) { this.name = name; this.price = price; } function Book(name, price, author) { Product.call(this, name, price); this.author = author; } Book.prototype = Object.create(Product.prototype); Book.prototype.constructor = Book; function Basket() { this.products = []; } Basket.prototype.addProduct = function(amount, product) { this.products.push(...Array(amount).fill(product)); }; Basket.prototype.calcTotal = function() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0); }; Basket.prototype.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal()); };
نکته: OOP در مراحل بعدی توسعه به جاوا اسکریپت اضافه شده است.
جمعبندی
به عنوان یک برنامهنویس تازهکار در جاوا اسکریپت شاید مدت زمانی طول کشد که برنامهنویسی شیءگرا را به طور کامل درک کنید. نکات مهمی که در این مراحل اولیه باید بشناسید شامل مفاهیم پارادایم OOP است و مزیتهای آن است به صورت فهرست زیر جمعبندی شده است:
- شیءها بر مبنای چیزهایی در دنیای واقعی مدلسازی میشوند که کانون مرکزی هر اپلیکیشن مبتنی بر OOP است.
- کپسولهسازی دادهها را در برابر دسترسی کنترل نشده حفاظت میکند.
- شیءها کارکردهایی دارند که روی دادههای درونیشان به کار گرفته میشوند.
- کلاسها قالبهایی هستند که برای وهلهسازی از شیءها مورد استفاده قرار میگیرند.
- وراثت ابزار قدرتمندی برای اجتناب از افزونگی است.
- OOP طول کد را افزایش میدهد؛ اما خواندن آن سادهتر از پارادایمهای دیگر کدنویسی است.
- از آنجا که OOP در مراحل بعدی توسعه جاوا اسکریپت به آن اضافه شده است، ممکن است با کدهای قدیمیتری مواجه شوید که از پروتوتایپ یا تکنیکهای برنامهنویسی تابعی استفاده میکنند.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- مجموعه آموزشهای طراحی و برنامه نویسی وب
- 1۰ کتابخانه و فریمورک جاوا اسکریپت که باید آنها را بشناسید
- متدهای شیء (Object Methods) در جاوا اسکریپت — به زبان ساده
- بررسی اشیاء در جاوا اسکریپت
- آموزش شی گرایی در طراحی و پیاده سازی زبان های برنامه سازی
- مفهوم کلاس در برنامه نویسی — همراه با نمونه مثال عملی
==
با سلام
اولا ممنون از مقاله
ثانیا مطالب بالا کد انتهایی رو پوشش نمیده
ثالثا اگر مقاله ای نوشته میشه باید کافی و وافی باشه و تمامی موارد توضیح داده بشه حداقل اینکه به یک مقاله کامل و درست و درمون ارجاع داده بشه
در پایان کد انتهایی شما اشکال داره البته در جایی که چیزی رو قبلا تعریف کردید دباره بکار میگیرید
اینه کد تصحیح شده:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
class Book extends Product {
constructor(name, price, author) {
super(name, price);
this.author = author;
}
}
class Basket {
constructor() {
this.products = [];
}
addProduct(amount, product) {
this.products.push(…Array(amount).fill(product));
}
calcTotal() {
return this.products
.map(product => product.price)
.reduce((a, b) => a + b, 0);
}
printShoppingInfo() {
console.log(‘one has to pay in total: ‘ + this.calcTotal());
}
}
const bread = new Product(‘bread’, 1);
const water = new Product(‘water’, 0.25);
const faust = new Book(‘faust’, 12.5, ‘Goethe’);
const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.addProduct(1, faust);
basket.printShoppingInfo();
//Prototype version
function _Product(name, price) {
this.name = name;
this.price = price;
}
function _Book(name, price, author) {
_Product.call(this, name, price);
this.author = author;
}
Book.prototype = Object.create(_Product.prototype);
Book.prototype.constructor = _Book;
function _Basket() {
this.products = [];
}
Basket.prototype.addProduct = function(amount, _Product) {
this.products.push(…Array(amount).fill(_Product));
};
Basket.prototype.calcTotal = function() {
return this.products
.map(_Product => _Product.price)
.reduce((a, b) => a + b, 0);
};
Basket.prototype.printShoppingInfo = function() {
console.log(‘one has to pay in total: ‘ + this.calcTotal());
};
سلام و وقت بخیر دوست عزیز
اولاً ممنون هستیم از توجه و ابراز لطف شما.
دوماً متوجه منظور شما از عدم پوشش کد انتهایی نشدیم.
سوماً در خصوص کافی بودن مقاله باید عرض کنیم که تمام تلاش ما در مجله فرادرس بر این بوده و هست که مطالبی با کیفیت بالا و درخور توجه شما عزیزان تهیه کنیم، اما قطعاً تصدیق میفرمایید که پرداختن به همه مفاهیم مرتبط با حوزه وسیعی مانند شیءگرایی در یک مطلب منفرد امکانپذیر نیست. لذا پیشنهاد میکنیم از دیگر مطالب مجله فرادرس نیز در این زمینه نیز بهره بگیرید:
https://blog.faradars.org/tag/%D8%B4%DB%8C-%DA%AF%D8%B1%D8%A7%DB%8C%DB%8C/
در خصوص قطعه کد ارسالی با بررسی مختصری که صورت گرفت (+) تنها دو تفاوت با کد حاضر در مطلب مشاهده شد که یکی استفاده از بک تیک به جای گیومه تکی بود که با توجه به عدم استفاده از Template Literal و نبود متون چند خطی در این کد جای ابهام وجود دارد! تفاوت دوم در استفاده از کاراکتر (_) در ابتدای نام برخی متغیرها است که گرچه سلیقه شخصی کدنویسی محسوب میشود ولی بر اساس قراردادهای نامگذاری جاوا اسکریپت کاراکتر Underscore ابتدایی صرفا در ابتدای پارامترها و متدهای خصوصی مورد استفاده قرار میگیرد.
در نهایت از توجه شما بی نهایت سپاسگزاریم.