ویژگی‌های موجود در ES6 که باید بشناسید

ترجمه و تالیف : عرفان کاکایی
تاریخ انتشار : 13 خرداد 98
خواندن در 6 دقیقه
دسته بندی ها : جاوا اسکریپت

ES6 ویژگی‌های بیشتری را به زبان JavaScript می‌آورد. برخی سینتکس‌های جدید موجود در آن شما را قادر می‌سازند تا به طور رساتری کد بنویسید، برخی ویژگی‌ها جعبه ابزار برنامه‌نویسی تابعی را تکمیل می‌کنند و برخی هم سوال برانگیز هستند.

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

let و const

برای تعریف کردن یک متغیر دو راه (let و const) وجود دارد؛ به علاوه یک راه که منسوخ شده است (var).

let

let یک متغیر را تعریف کرده، و به طور اختیاری آن را در محدوده فعلی راه‌اندازی می‌کند. محدوده فعلی می‌تواند یک ماژول، یک تابع یا یک بلوک باشد. مقدار یک متغیر که راه‌اندازی نشده باشد، undefined است.

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

کد زیر که بر روی محدوده بلوک let تاکید دارد را در نظر داشته باشید:

let x = 1;
{
  let x = 2;
}
console.log(x); //1

به طور متضاد، تعریف var هیچ محدوده بلوکی نداشت:

var x = 1;
{
  var x = 2;
}
console.log(x); //2

بیانیه حلقه for به همراه اعلامیه let، یک متغیر محلی را در محدوده بلوک، و برای هر تکرار می‌سازد. حلقه بعدی پنج closure را بر روی پنج متغیر i متفاوت می‌سازد.

(function run(){
  for(let i=0; i<5; i++){
    setTimeout(function log(){
      console.log(i); //0 1 2 3 4
    }, 100);
  }
})();

نوشتن کد مشابه با استفاده از var، پنج closure را بر روی متغیرهای مشابه خواهد ساخت؛ پس تمام closureها آخرین مقدار i را نمایش خواهند داد.

تابع log() یک closure است.

const

const متغیری را تعریف می‌کند که نمی‌تواند مجددا اختصاص‌دهی شود. این متغیر فقط وقتی که مقدار اختصاص داده شده غیر قابل جهش باشد، تبدیل به یک constant می‌شود.

یک مقدار غیر قابل جهش، مقداری است که پس از ساخته شده نمی‌تواند تغییر یابد. مقادیر اولیه غیر قابل جهش بوده، و آبجکت‌ها قابل جهش هستند.

const متغیر را ثابت کرده، و Object.freeze() هم آبجکت را ثابت می‌کند.

راه‌اندازی متغیر const اجباری است.

ماژول‌ها

قبل از ماژول‌ها، یک متغیر که خارج از هر تابعی تعریف شده بود، یک متغیر global بود.

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

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

//module "./TodoStore.js"
export default function TodoStore(){}

//module "./UserStore.js"
export default function UserStore(){}

وارد کردن (import کردن)، یک تابع یا آبجکت از ماژول‌های دیگر را برای ماژول فعلی در دسترس قرار می‌دهد.

import TodoStore from "./TodoStore";
import UserStore from "./UserStore";

const todoStore = TodoStore();
const userStore = UserStore();

Spread / Rest

عملگر … بر حسب مکانی که در آن مورد استفاده قرار گرفته است، می‌تواند عملگر انتشار (spread) یا پارامتر rest باشد. این مثال را در نظر داشته باشید:

const numbers = [1, 2, 3];
const arr = ['a', 'b', 'c', ...numbers];

console.log(arr);
["a", "b", "c", 1, 2, 3]

این عملگر spread است. حال به مثال بعدی نگاه کنید:

function process(x,y, ...arr){
  console.log(arr)
}
process(1,2,3,4,5);
//[3, 4, 5]

function processArray(...arr){
  console.log(arr)
}
processArray(1,2,3,4,5);
//[1, 2, 3, 4, 5]

این پارامتر rest است.

آرگومان‌ها

با پارامتر rest، ما می‌توانیم شبه پارامتر arguments را جایگزین کنیم. پارامتر rest یک آرایه بوده، اما arguments به این صورت نیست.

function addNumber(total, value){
  return total + value;
}

function sum(...args){
  return args.reduce(addNumber, 0);
}

sum(1,2,3); //6

Clone کردن

عملگر spread فرایند clone کردن آبجکت‌ها را و آرایه‌ها را ساده‌تر و رساتر می‌کند.

عملگر ویژگی‌های انتشار آبجکت، به عنوان بخشی از ES2018 در دسترس خواهند بود.

const book = { title: "JavaScript: The Good Parts" };

//clone with Object.assign()
const clone = Object.assign({}, book);

//clone with spread operator
const clone = { ...book };

const arr = [1, 2 ,3];

//clone with slice
const cloneArr = arr.slice();

//clone with spread operator
const cloneArr = [ ...arr ];

الحاق

در مثال بعدی، عملگر spread برای الحاق آرایه‌ها استفاده شده است:

const part1 = [1, 2, 3];
const part2 = [4, 5, 6];

const arr = part1.concat(part2);

const arr = [...part1, ...part2];

ادغام آبجکت‌ها

عملگر spread به مانند Object.assign() می‌تواند برای کپی کردن ویژگی‌هایی از یک یا چند آبجکت به یک آبجکت خالی و ترکیب کردن ویژگی‌های آن‌ها استفاده شود.

const authorGateway = {
  getAuthors : function() {},
  editAuthor: function() {}
};

const bookGateway = {
  getBooks : function() {},
  editBook: function() {}
};

//copy with Object.assign()
const gateway = Object.assign({},
      authorGateway,
      bookGateway);
     
//copy with spread operator
const gateway = {
   ...authorGateway,
   ...bookGateway
};

اختصارات ویژگی‌ها

کد بعدی را در نظر بگیرید:

function BookGateway(){
  function getBooks() {}
  function editBook() {}
 
  return {
    getBooks: getBooks,
    editBook: editBook
  }
}

با اختصارات ویژگی‌ها، وقتی که نام ویژگی و نام متغیر مورد استفاده یکی باشند، ما می‌توانیم کلید مورد نظر را فقط یک بار بنویسیم:

function BookGateway(){
  function getBooks() {}
  function editBook() {}
 
  return {
    getBooks,
    editBook
  }
}

در اینجا یک مثال دیگر را مشاهده می‌نمایید:

const todoStore = TodoStore();
const userStore = UserStore();
   
const stores = {
  todoStore,
  userStore
};

تخریب اختصاص دهی‌ها

کد بعدی را در نظر داشته باشید:

function TodoStore(args){
  const helper = args.helper;
  const dataAccess = args.dataAccess;
  const userStore = args.userStore;
}

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

function TodoStore(args){
   const {
      helper,
      dataAccess,
      userStore } = args;
}

یا حتی بهتر، با سینتکس تخریب در لیست پارامترها:

function TodoStore({ helper, dataAccess, userStore }){}

در اینجا یک فراخوانی تابع را مشاهده می‌نمایید:

TodoStore({
  helper: {},
  dataAccess: {},
  userStore: {}
});

پارامترهای پیشفرض

توابع می‌توانند پارامترهای پیشفرض داشته باشند. به مثال بعدی نگاهی داشته باشید:

function log(message, mode = "Info"){
  console.log(mode + ": " + message);
}

log("An info");
//Info: An info

log("An error", "Error");
//Error: An error

ادبیات رشته‌های قالب

رشته‌های قالب با کاراکتر ` تعریف می‌شوند. با رشته‌های قالب، پیغام لاگ کردن قبلی می‌تواند به این صورت نوشته شود:

function log(message, mode= "Info"){
  console.log(`${mode}: ${message}`);
}

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

در اینجا یک تابع را مشاهده می‌نمایید که یک HTML را می‌سازد، و این HTML خطوط چندتایی را پوشش می‌دهد:

function createTodoItemHtml(todo){
  return `<li>
    <div>${todo.title}</div>
    <div>${todo.userName}</div>
  </li>`;
}

tail-callهای مناسب

وقتی آخرین کاری که یک تابع برگشتی انجام می‌دهد، یک فراخوانی برگشتی باشد، آن تابع tail recursive است.

توابع tail recursive بهتر از توابع غیر tail recursive عمل می‌کنند. فراخوانی tail recursive بهینه‌سازی شده، هیچ چارچوب stack جدیدی را برای هر فراخوانی تابع ایجاد نمی‌کند؛ بلکه از یک چارچوب stack تنها استفاده می‌کند.

ES6 بهینه‌سازی tail-call را در حالت محدود فراهم می‌کند.

تابع زیر از بهینه‌سازی tail-call بهره می‌برد.

function print(from, to)
{
  const n = from;
  if (n > to)  return;
 
  console.log(n);

  //آخرین بیانیه، فراخوانی برگشتی است
  print(n + 1, to);
}

print(1, 10);

نکته: بهینه‌سازی tail-call هنوز توسط مرورگرهای اصلی مورد پشتیبانی قرار نگرفته است.

Promiseها

یک promise، یک ارجاع به یک فراخوانی ناهمگام است. این promise در آینده ممکن است resolve شده، یا با شکست مواجه شود.

ترکیب کردن promiseها آسان‌تر است. همانطور که در این مثال مشاهده می‌نمایید، فراخونی یک تابع وقتی که تمام promiseها resolve شده‌اند، یا وقتی که اولین مورد resolve شده است آسان می‌باشد.

function getTodos() { return fetch("/todos"); }
function getUsers() { return fetch("/users"); }
function getAlbums(){ return fetch("/albums"); }

const getPromises = [
  getTodos(),
  getUsers(),
  getAlbums()
];

Promise.all(getPromises).then(doSomethingWhenAll);
Promise.race(getPromises).then(doSomethingWhenOne);

function doSomethingWhenAll(){}
function doSomethingWhenOne(){}

تابع fetch() که بخشی از اِی‌پی‌آی Fetch می‌باشد، یک promise را بر می‌گرداند.

Promise.all() یک promise را بر می‌گرداند که وقتی تمام promiseهای ورودی resolve شده‌اند، این promise هم resolve می‌شود. Promise.race() یک promise را بر می‌گرداند که وقتی یکی از promiseهای ورودی resolve شده یا رد (reject) می‌شود، این promise هم resolve شده یا رد می‌شود.

یک promise می‌تواند در یکی از سه حالت باشد: pending، resolved یا rejected. Promise مورد نظر تا زمانی که resolve‌ یا reject‌ شود، در حالت pending‌ خواهد بود.

Promiseها از یک سیستم زنجیره‌بندی پشتیبانی می‌کنند که شما را قادر می‌سازد تا داده‌ها را از طریق مجموعه‌ای از توابع منتقل کنید. در مثال بعدی، نتیجه getTodos() به عنوان یک ورودی به toJson() منتقل شده، سپس نتیجه آن به عنوان یک ورودی به getTopPriority() منتقل شده، و سپس نتیجه آن هم به عنوان یک ورودی به تابع renderTodos() منتقل شده است. وقتی که یک خطا بروز داده شده، یا یک promise رد می‌شود، handleError فراخوانی می‌شود.

getTodos()
  .then(toJson)
  .then(getTopPriority)
  .then(renderTodos)
  .catch(handleError);

function toJson(response){}
function getTopPriority(todos){}
function renderTodos(todos){}
function handleError(error){}

در مثال قبلی، .then سناریو موفقیت‌آمیز را مدیریت کرده، و .catch() هم سناریو خطا را مدیریت می‌کند. اگر در هر کدام از قدم‌ها خطایی وجود داشته باشد، زنجیره کنترل به نزدیک‌ترین handler مربوط به رد کردن در زنجیره مورد نظر می‌رود.

Promise.resolve() یک promise‌ رفع شده (resolve شده) را بر می‌گرداند. Promise.reject() یک promise رد شده را بر می‌گرداند.

Class

Class یک سینتکس شیرین برای ساخت آبجکت‌هایی به همراه یک ویژگی سفارشی می‌باشد. Class یک سینتکس بهتر نسبت به مورد قبلی، یعنی سازنده تابع (function constructor) دارد. این مثال را نگاه کنید:

class Service {
  doSomething(){ console.log("doSomething"); }
}

let service = new Service();
console.log(service.__proto__ === Service.prototype);

تمام متدهای تعریف شده در کلاس Service به آبجکت Service.prototype اضافه خواهند شد. نمونه‌های کلاس Service آبجکت ویژگی مشابه (Service.prototype) را خواهند داشت. تمام نمونه‌ها، نماینده فراخوانی‌های متد به آبجکت Service.prototype خواهند بود. متدها یک بار بر روی Service.prototype تعریف شده، و سپس توسط تمام نمونه‌ها به ارث برده می‌شوند.

وراثت

«کلاس‌ها می‌توانند از کلاس‌های دیگر به ارث ببرند.» در اینجا مثالی از وراثت را می‌بینید که در آن کلاس SpecialService از کلاس Service به ارث می‌برد:

class Service {
  doSomething(){ console.log("doSomething"); }
}

class SpecialService extends Service {
  doSomethingElse(){ console.log("doSomethingElse"); } 
}

let specialService = new SpecialService();
specialService.doSomething();
specialService.doSomethingElse();

تمام متدهای تعریف شده در SpecialService به آبجکت SpecialService.prototype اضافه خواهند شد. تمام نمونه‌ها، نماینده فراخوانی‌های متد به SpecialService.prototype خواهند بود. اگر متد مورد نظر در SpecialService.prototype پیدا نشود، در آبجکت Service.prototype برای آن جستجو خواهد شد. اگر باز هم پیدا نشود، در Object.prototype برای آن جستجو خواهد شد.

کلاس می‌تواند تبدیل به یک ویژگی بد شود

با این که اعضای یک کلاس شاید کپسوله‌ به نظر بیایند، اما تمام آن‌ها عمومی هستند. شما همچنان نباید مشکلات را در حالتی مدیریت کنید که this زمینه خود را از دست می‌دهد. API عمومی قابل جهش است.

اگر به سمت تابعیِ JavaScript توجهی نکنید، class می‌تواند تبدیل به یک ویژگی بد شود. وقتی که JavaScript هم یک زبان برنامه‌نویسی تابعی و هم یک زبان بر پایه ویژگی است، شاید class حس یک زبان بر پایه کلاس را به شما بدهد.

آبجکت‌های کپسوله‌سازی شده می‌توانند با استفاده از توابع سازنده ایجاد شوند. مثال بعدی را در نظر داشته باشید:

function Service() {
  function doSomething(){ console.log("doSomething"); }
 
  return Object.freeze({
     doSomething
  });
}

این بار تمام اعضا به طور پیشفرض خصوصی هستند. API عمومی غیر قابل جهش است. دیگر نیازی به مدیریت مشکلات وقتی که this زمینه خود را از دست می‌دهد وجود ندارد.

اگر class توسط چارچوب کامپوننت‌ها مورد نیاز باشد، شاید به عنوان یک exception مورد استفاده قرار گیرد. این مسئله در React به این صورت بود، اما با React Hooks دیگر اینطور نیست.

توابع پیکانی

توابع پیکانی می‌توانند توابع ناشناس را بسازند. این توابع می‌توانند برای ساخت callbackهای کوچک با استفاده از یک سینتکس کوتاه‌تر مورد استفاده قرار گیرند.

بیایید مجموعه‌ای از کارها را در نظر بگیریم. یک لیست کارها، یک id، یک title و یک ویژگی Boolean به نام completed دارد. حال کد بعدی را در نظر داشته باشید که فقط title را از مجموعه مورد نظر انتخاب می‌کند:

const titles = todos.map(todo => todo.title);

یا مثال بعدی که فقط todoهایی که تمام نشده‌اند را انتخاب می‌کند:

const filteredTodos = todos.filter(todo => !todo.completed);

this

توابع پیکانی، this و arguments مختص خود را دارند. در نتیجه، شاید شما توابع پیکانی را تحت استفاده برای رفع مشکلات ببینید، در حالیکه this زمینه خود را از دست می‌دهد. به نظر من بهترین را برای رفع این مشکل، این است که به کلی از this استفاده نکنید.

در صورت نیاز به اطلاعات بیشتر درباره this، به این مقاله مراجعه کنید:

  • راهنمایی برای this در JavaScript

توابع پیکانی می‌توانند تبدیل به یک ویژگی بد شوند

توابع پیکانی وقتی که به ضرر توابع نامگذاری شده مورد استفاده قرار گیرند، می‌توانند تبدیل به یک ویژگی بد شوند. این مسئله، برخی مشکلات خوانایی و نگهداری را ایجاد خواهد کرد. به کد بعدی که فقط با توابع پیکانی ناشناس نوشته شده است، نگاهی داشته باشید:

const newTodos = todos.filter(todo =>
       !todo.completed && todo.type === "RE")
    .map(todo => ({
       title : todo.title,
       userName : users[todo.userId].name
    }))
    .sort((todo1, todo2) => 
      todo1.userName.localeCompare(todo2.userName));

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

const newTodos = todos.filter(isTopPriority)
  .map(partial(toTodoView, users))
  .sort(ascByUserName);

function isTopPriority(todo){
  return !todo.completed && todo.type === "RE";
}
 
function toTodoView(users, todo){
  return {
    title : todo.title,
    userName : users[todo.userId].name
  }
}

function ascByUserName(todo1, todo2){
  return todo1.userName.localeCompare(todo2.userName);
}

حتی بهتر این که توابع پیکانی ناشناس در Call Stack به عنوان anonymous نمایان خواهند شد.

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

//با تابع پیکانی
const prop = key => obj => obj[key];

//با کلیدواژه تابع
function prop(key){
   return function(obj){
      return obj[key];
   }
}

به وقتی که یک آبجکت را بر می‌گردانید، توجه کنید. در مثال بعدی، getSampleTodo() مقدار undefined را بر می‌گرداند.

const getSampleTodo = () => { title : "A sample todo" };

getSampleTodo();
//undefined

مولدها

به نظر من مولد ES6 یک ویژگی غیر ضروری است که کد شما را پیچیده‌تر می‌کند.

مولد ES6 یک آبجکت را می‌سازد که متد next() را دارد. متدnext()  یک آبجکت را می‌سازد که ویژگی value را دارد. مولدهای ES6 استفاده از حلقه‌ها را ترویج می‌دهند. نگاهی به کد زیر داشته باشید:

function* sequence(){
  let count = 0;
  while(true) {
    count += 1;
    yield count;
  }
}

const generator = sequence();
generator.next().value;//1
generator.next().value;//2
generator.next().value;//3

همین مولد می‌تواند به سادگی و با استفاده از یک closure پیاده‌سازی شود.

function sequence(){
  let count = 0;
  return function(){
    count += 1;
    return count;
  }
}

const generator = sequence();
generator();//1
generator();//2
generator();//3

نتیجه گیری

let و const متغیرها را تعریف کرده، و راه‌اندازی می‌کنند.

ماژول‌ها عملکرد را کپسوله‌سازی کرده و فقط یک بخش کوچک را در معرض قرار می‌دهند.

عملگر spread، پارامتر rest و اختصار ویژگی، بیان همه چیز را آسان‌تر می‌کنند.

Promiseها و برگشت tail، جعبه ابزار برنامه‌نویسی تابعی را تکمیل می‌کنند.

منبع

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید