در
قسمت قبل، بخشی از تازههای
ES6 را که بیشتر در برنامههای مبتنی بر React مورد استفاده قرار میگیرند، بررسی کردیم. در این قسمت نیز سایر موارد مهم باقیمانده را بررسی میکنیم.
در اینجا نیز برای بررسی ویژگیهای جاوا اسکریپت مدرن، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-03
> cd sample-03
> npm start
سپس تمام کدهای داخل index.js را نیز حذف میکنیم. اکنون تمام کدهای خالص جاوا اسکریپتی خود را داخل این فایل خواهیم نوشت.
همچنین چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود میتوانید مشاهده کنید (فشردن دکمهی F12).
متد Array.map
در برنامههای مبتنی بر React، از متد Array.map برای رندر لیستها استفاده میشود و نمونههای بیشتری از آنرا در قسمتهای بعدی مشاهده خواهید کرد.
فرض کنید آرایهای از رنگها را داریم. اکنون میخواهیم لیستی را به صورت <li>color</li> به ازای هر آیتم آن، تشکیل دهیم:
const colors = ["red", "green", "blue"];
برای این منظور میتوان از متد map بر روی این آرایه به نحو زیر استفاده کرد:
const items = colors.map(function(color) {
return "<li>" + color + "</li>";
});
console.log(items);
متد map یک callback function را دریافت میکند که با هر بار فراخوانی آن، یک عنصر از عناصر آرایه را دریافت کرده، آنرا تغییر شکل داده و بازگشت میدهد (چیزی شبیه به متد Select در LINQ).
این مثال را توسط
arrow functions نیز میتوان بازنویسی کرد:
const items2 = colors.map(color => "<li>" + color + "</li>");
console.log(items2);
ابتدا function را حذف میکنیم. سپس { return } را تبدیل به یک <= خواهیم کرد. چون تک پارامتری است، نیازی به ذکر پرانتز color وجود ندارد. همچنین نیازی به ذکر سمیکالن انتهای return هم نیست؛ چون کل بدنهی این تابع، یک سطر return بیشتر نیست.
یک مرحلهی دیگر هم میتوانیم این قطعه کد را زیباتر کنیم؛ جمع زدن رشتهها در ES6 معادل بهتری پیدا کردهاست که
template literals نام دارد:
const items3 = colors.map(color => `<li>${color}</li>`);
console.log(items3);
در اینجا بجای ' و یا " از حرف back-tick استفاده میشود. سپس قالب کلی رشتهی خود را مشخص میکنیم و جائیکه قرار است متغیری را درج کنیم، از {}$ استفاده میکنیم که بسیار شبیه به ویژگی string interpolation در #C است. فقط برخلاف آن، حرف $ در ابتدای رشته قرار نمیگیرد و باید دقیقا پیش از متغیر مدنظر تعریف شود.
Object Destructuring
فرض کنید شیء آدرس را به صورت زیر تعریف کردهایم:
const address = {
street: "street 1",
city: "city 1",
country: "country 1"
};
اکنون میخواهیم خواص آنرا به متغیرهایی نسبت دهیم. یک روش متداول آن به صورت زیر است:
const street1 = address.street;
const city1 = address.city;
const country1 = address.country;
برای کاهش این حجم کد تکراری که با .address شروع میشود، میتوان از ویژگی Object Destructuring استفاده کرد:
const { street, city, country } = address;
این تک سطر، دقیقا با سه سطر قبلی که نوشتیم، عملکرد یکسانی دارد. ابتدا متغیرهای مدنظر، داخل {} قرار میگیرند و سپس کل شیء آدرس به آنها نسبت داده خواهد شد.
در اینجا باید نام متغیرهای تعریف شده با نام خواص شیء آدرس یکی باشند. همچنین ذکر تمامی این متغیرها نیز ضرورتی ندارد و برای مثال اگر فقط نیاز به street بود، میتوان تنها آنرا ذکر کرد.
اگر خواستیم نام متغیر دیگری را بجای نام خواص شیء آدرس انتخاب کنیم، میتوان از یک نام مستعار ذکر شدهی پس از : استفاده کرد:
const { street: st } = address;
console.log(st);
Spread Operator
فرض کنید دو آرایهی زیر را داریم:
const first = [1, 2, 3];
const second = [4, 5, 6];
و میخواهیم آنها را با هم ترکیب کنیم. یک روش انجام اینکار توسط متد concat آرایهها است:
const combined = first.concat(second);
console.log(combined);
در ES6 با استفاده از عملگر ... که spread نیز نام دارد، میتوان قطعه کد فوق را به صورت زیر بازنویسی کرد:
const combined2 = [...first, ...second];
console.log(combined2);
ابتدا یک آرایهی جدید را ایجاد میکنیم. سپس تمام عناصر اولین آرایه را در آن گسترده میکنیم و بعد از آن، تمام عناصر دومین آرایه را.
شاید اینطور به نظر برسد که بین دو راه حل ارائه شده آنچنانی تفاوتی نیست. اما مزیت قطعه کد دوم، سهولت افزودن المانهای جدید، به هر قسمتی از آرایه است:
const combined2 = [...first, "a", ...second, "b"];
console.log(combined2);
کاربرد دیگر عملگر spread امکان clone سادهی یک آرایهاست:
const clone = [...first];
console.log(clone);
به علاوه امکان اعمال آن به اشیاء نیز وجود دارد:
const firstObject = { name: "User 1" };
const secondObject = { job: "Job 1" };
const combinedObject = { ...firstObject, ...secondObject, location: "Here" };
console.log(combinedObject);
در اینجا تمام خواص شیء اول و دوم با هم ترکیب و همچنین یک خاصیت اختیاری نیز ذکر شدهاست. خروجی نهایی آن چنین شیءای خواهد بود:
{name: "User 1", job: "Job 1", location: "Here"}
و امکان clone اشیاء توسط آن هم وجود دارد:
const clonedObject = { ...firstObject };
console.log(clonedObject);
کلاسها در ES 6
قطعه کد کلاسیک زیر را که کار ایجاد اشیاء را در جاوا اسکریپت انجام میدهد، در نظر بگیرید:
const person = {
name: "User 1",
walk() {
console.log("walk");
}
};
const person2 = {
name: "User 2",
walk() {
console.log("walk");
}
};
ابتدا یک شیء person را با دو عضو، ایجاد کردهایم. اکنون برای ایجاد یک شیء person دیگر، باید دقیقا همان قطعه کد را تکرار کنیم. به همین جهت برای حذف کدهای تکراری، نیاز به قالبی برای ایجاد اشیاء داریم و اینجا است که از کلاسها استفاده میشود:
class Person {
constructor(name) {
this.name = name;
}
walk() {
console.log("walk");
}
}
برای تعریف یک کلاس ES6، با واژهی کلیدی class شروع میکنیم. نام یک کلاس با حروف بزرگ شروع میشود (pascal case) و اگر برای نمونه این نام قرار است دو قسمتی باشد، به مانند CoolPerson عمل میکنیم. در مرحلهی بعد، متد walk را از تعریف شیء شخص، به کلاس شخص انتقال دادهایم. سپس متد ویژهی constructor را در اینجا تعریف کردهایم. توسط آن زمانیکه یک نمونه از این کلاس ساخته میشود، پارامتری را دریافت و به یک خاصیت جدید در آن کلاس که توسط this.name تعریف شدهاست، انتساب میدهیم.
باید دقت داشت که class Person تنها یک قالب است و const person ای که پیشتر تعریف شد، یک شیء. برای اینکه از روی قالب تعریف شدهی Person، یک شیء را ایجاد کنیم، به صورت زیر توسط واژهی کلیدی new عمل میشود:
const person3 = new Person("User 3");
console.log(person3.name);
person3.walk();
در اینجا اگر دقت کنید، عبارت Person("User 3") شبیه به فراخوانی یک متد است. این متد دقیقا همان متد ویژهی constructor ای است که تعریف کردیم. اکنون توسط شیء person3، میتوان به خاصیت name و یا متد walk آن دسترسی یافت.
یک نکته: در جاوا اسکریپت، کلاسها نیز شیء هستند! از این جهت که کلاسها در جاوا اسکریپت صرفا یک بیان نحوی زیبای تابع constructor هستند و توابع در جاوا اسکریپت نیز شیء میباشند!
ارث بری کلاسها در ES6
فرض کنید میخواهیم کلاس Teacher را به نحو زیر تعریف کنیم:
class Teacher {
teach() {
console.log("teach");
}
}
این کلاس دارای متد teach است؛ اما تمام معلمها باید بتوانند راه هم بروند. همچنین قصد نداریم متد walk کلاس Person را هم با توجه به اینکه Teacher یک Person نیز هست، در اینجا تکرار کنیم. یک روش حل این مشکل، استفاده از ارثبری کلاسها است که با افزودن extends Person به نحو زیر میسر میشود:
class Teacher extends Person {
teach() {
console.log("teach");
}
}
پس از این تعریف، اگر بخواهیم توسط واژهی کلیدی new، یک شیء را بر اساس این کلاس تهیه کنیم، در VSCode، تقاضای ثبت یک سازنده نیز میشود:
علت اینجا است که کلاس Teacher، نه فقط متد walk کلاس Person را به ارث بردهاست، بلکه سازندهی آنرا نیز به ارث میبرد:
const teacher = new Teacher("User 4");
اکنون میتوان با استفاده از شیء معلم ایجاد شده، نه فقط به متدهای کلاس Teacher دسترسی یافت، بلکه امکان دسترسی به خواص و متدهای کلاس پایهی Person نیز در اینجا وجود دارد:
console.log(teacher.name);
teacher.teach();
teacher.walk();
در ادامه فرض کنید علاوه بر ذکر نام، نیاز به ذکر مدرک معلم نیز در سازندهی کلاس وجود دارد:
class Teacher extends Person {
constructor(name, degree) {}
در این حالت اگر به کنسول توسعه دهندههای مرورگر مراجعه کنید، خطای زیر را مشاهده خواهید کرد:
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
عنوان میکند که نیاز است متد ویژهی super را در سازندهی سفارشی کلاس Teacher فراخوانی کنیم. در ES6، فراخوانی سازندهی کلاس پایه، در سازندههای سفارشی کلاسهای مشتق شدهی از آن، اجباری است:
class Teacher extends Person {
constructor(name, degree) {
super(name);
this.degree = degree;
}
teach() {
console.log("teach");
}
}
با اینکار، مقدار دهی خاصیت name کلاس پایه نیز صورت خواهد گرفت. در اینجا همچنین تعریف خاصیت جدید degree و مقدار دهی آنرا نیز مشاهده میکنید. در ادامه باید این پارامتر دوم سازنده را نیز در حین نمونه سازی از کلاس Teacher تعریف کنیم:
const teacher = new Teacher("User 4", "MSc");
در برنامههای React، هر زمانیکه یک کامپوننت جدید تعریف میشود، کلاس آن، از کلاس پایهی کامپوننت، ارث بری خواهد کرد. به این ترتیب میتوان به تمام امکانات این کلاس پایه، بدون نیاز به تکرار آنها در کلاسهای مشتق شدهی از آن، دسترسی یافت.
ماژولها در ES 6
تا اینجا اگر مثالها را دنبال کرده باشید، تمام آنها را داخل همان فایل index.js درج کردهایم. به این ترتیب کم کم دارد مدیریت این فایل از دست خارج میشود. امکان تقسیم کدهای index.js به چندین فایل، مفهوم ماژولها را در ES6 تشکیل میدهد. برای این منظور قصد داریم هر کلاس تعریف شده را به یک فایل جداگانه که ماژول نامیده میشود، منتقل کنیم. از کلاس Person شروع میکنیم و آنرا به فایل جدید person.js و کلاس Teacher را به فایل جدید teacher.js منتقل میکنیم.
البته اگر از افزونههای VSCode استفاده میکنید، اگر کرسر را بر روی نام کلاس قرار دهید، یک آیکن لامپ مانند ظاهر میشود. با کلیک بر روی آن، منویی که شامل گزینهی move to a new file هست، برای انجام سادهتر این عملیات (ایجاد یک فایل جدید js، سپس انتخاب و cut کردن کل کلاس و در آخر کپی کردن آن در این فایل جدید) پیشبینی شدهاست.
هرچند این عملیات تا به اینجا خاتمه یافته به نظر میرسد، اما نیاز به اصلاحات زیر را نیز دارد:
- هنگام کار با ماژولها، اشیاء تعریف شدهی در آن به صورت پیشفرض، خصوصی و private هستند و خارج از آنها قابل دسترسی نمیباشند. به این معنا که class Teacher ما که اکنون در یک ماژول جدید قرار گرفتهاست، توسط سایر قسمتهای برنامه قابل مشاهده و دسترسی نیست.
- برای public تعریف کردن یک کلاس تعریف شدهی در یک ماژول، نیاز است آنرا export کنیم. انجام این کار نیز سادهاست. فقط کافی است واژهی کلیدی export را به پیش از class اضافه کنیم:
export class Teacher extends Person {
- اگر افزونهی eslint را نصب کرده باشید، اکنون در فایل یا ماژول جدید teacher.js، زیر کلمهی Person خط قرمز کشیدهاست و عنوان میکند که کلاس Person را نمیشناسد:
برای رفع این مشکل، باید این وابستگی را import کرد:
import { Person } from "./Person";
export class Teacher extends Person {
در اینجا شیء Person، از فایل محلی واقع شدهی در پوشهی جاری Person.js تامین میشود. نیازی به ذکر پسوند فایل در اینجا نیست.
- مرحلهی آخر، اصلاح فایل index.js است؛ چون اکنون تعاریف Person و Teacher را نمیشناسد.
import { Person } from "./Person";
import { Teacher } from "./Teacher";
دو سطر فوق را نیز به ابتدای فایل index.js اضافه میکنیم تا بتوان new Person و new Teacher نوشته شدهی در آنرا کامپایل کرد.
Exportهای پیشفرض و نامدار در ES6
اشیاء تعریف شدهی در یک ماژول، به صورت پیشفرض private هستند؛ مگر اینکه export شوند. برای مثال export class Teacher و یا export function xyz. به اینها named exports گویند. حال اگر ماژول ما تنها یک شیء عمومی شده را داشت (کلاسها هم شیء هستند!)، میتوان از واژهی کلیدی default نیز در اینجا استفاده کرد:
export default class Teacher extends Person {
پس از این دیگر نیازی به ذکر {} در حین import چنین شیءای نخواهد بود:
import Teacher from "./Teacher";
در ادامه اگر یک export نامدار دیگر را به این ماژول اضافه کنیم (مانند تابع testTeacher):
import { Person } from "./Person";
export function testTeacher() {
console.log("Test Teacher");
}
export default class Teacher extends Person {
نحوهی import آن به صورت زیر تغییر میکند:
import Teacher, { testTeacher } from "./Teacher";
یک default export و یک named export را در اینجا داریم که اولی بدون {} و دومی با {} تعریف شدهاست. این الگویی است که در برنامههای React زیاد دیده میشود؛ مانند:
import React, { Component } from 'react';
یک نکته: اگر در VSCode داخل {}، دکمههای ctrl+space را فشار دهید، میتوانید منوی exportهای ماژول تعریف شده را مشاهده کنید.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
sample-03.zip