معرفی سریع JavaScript تابعی

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 21 مهر 1397
دسته بندی ها : جاوا اسکریپت

برنامه‌نویسی تابعی، در حال حاضر یکی از داغ‌ترین گرایش‌ها است و بحث و جدل‌های زیادی درباره این که یک شخص چرا باید در کد خود از آن استفاده کند، وجود دارد. من نمی‌خواهم وارد جزئیات تمام مفهومات و ایده‌های برنامه‌نویسی تابعی شوم، اما تلاش خواهم کرد تا یک راهنمای نمایشی درباره نحوه استفاده از برنامه‌نویسی تابعی در موقعیت‌های روزانه که شامل JavaScript می‌باشند را فراهم کنم.

«برنامه‌نویسی تابعی، یک الگوی برنامه‌نویسی است که با محاسبات به عنوان ارزیابی توابع ریاضی رفتار می‌کند و از state در حال تغییر و داده‌های قابل جهش جلوگیری می‌کند.»

تعریف مجدد توابع

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

برای مثال در JavaScript، می‌توانید این کار را انجام دهید:

// یک تابع بساز

function f(x){

  return x * x;

}

// از تابع استفاده کن

f(5); // 25

// یک تابع ناشناس بساز و آن را به یک متغیر اختصاص بده

var g = function(x){

  return x * x;

}

// حال می‌توانید تابع را منتقل کنید

var h = g;

// And use it

h(5); // 25

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

تنها نکته در اینجا این است که کد تابعی صحیح، با عدم وجود اثرات جانبی مشخص شده است، که یعنی توابع مورد نظر باید فقط به آرگومان‌های خود به عنوان ورودی تکیه کنند و نباید به هیچ وجه محیط را تحت تاثیر قرار دهند؛ در نتیجه شما نباید از چیزی خارج از تابع استفاده کنید یا آن را تغییر دهید. گرچه این مسئله پیامدهای مهمی دارد. مثلا این که یک تابع همیشه خروجی مشابهی که توسط ورودی فراهم شده است را بر می‌گرداند، و این که اگر نتیجه یک فراخوانی تابع استفاده نشود، می‌تواند بدون بروز دادن هیچ‌گونه تغییر در کد، حذف شود.

استفاده از متدهای داخلی نمونه اولیه آرایه

وقتی که وارد برنامه‌نویسی تابعی در JavaScript می‌شوید، Array.prototype جایی است که باید از آن شروع کنید. این بخش شامل متدهای پرکاربردی برای اعمال تغییر شکل (transformation) به آرایه‌ها می‌باشد، که یک سناریو استفاده رایج در اکثر برنامه‌های مدرن است.

با توجه به این که این یک تغییر شکل ساده است، شروع کردن با Array.prototype.sort() به نظر یک ایده خوب می‌آید، که با استفاده از آن به طرز شگفت‌انگیزی می‌توانید شروع به چینش یک آرایه بنمایید. sort() فقط یک آرگومان و یک تابع که برای مقایسه دو عنصر استفاده می‌شود را می‌گیرد. این تابع اگر اولین عنصر قبل از دومین عنصر بیاید، یک مقدار زیر صفر را بر می‌گرداند، و اگر برعکس این قضیه صحیح باشد هم یک مقدار بالای صفر را بر می‌گرداند.

چینش بسیار ساده به نظر می‌آید، تا زمانی که شما به سناریو‌ای بر بخورید که باید چیزی پیچیده‌تر از آرایه اعداد معمولی را مقایسه کنید. برای این مثال، ما آرایه‌ای از آبجکت‌ها خواهیم داشت، که وزن هر کدام بر حسب پوند یا کیلوگرم هستند و ما باید آن‌ها را در یک ترتیب صعودی بر حسب وزن بچینیم. بیایید به نحوه انجام این کار نگاهی بیندازیم:

// تعریف تابع مقایسه

var sortByWeight = function(x,y){

  var xW = x.measurement == "kg" ? x.weight : x.weight * 0.453592;

  var yW = y.measurement == "kg" ? y.weight : y.weight * 0.453592;

  return xW > yW ? 1 : -1;

}

// فقط دو لیست کمی متفاوت از داده‌ها که باید بر حسب وزن چیده شوند

var firstList = [

  { name: "John", weight: 220, measurement: "lbs" },

  { name: "Kate", weight: 58, measurement: "kg" },

  { name: "Mike", weight: 137, measurement: "lbs" },

  { name: "Sophie", weight: 66, measurement: "kg" },

];

var secondList = [

  { name: "Margaret", weight: 161, measurement: "lbs", age: 51 },

  { name: "Bill", weight: 76, measurement: "kg", age: 62 },

  { name: "Jonathan", weight: 72, measurement: "kg", age: 43 },

  { name: "Richard", weight: 74, measurement: "kg", age: 29 },

];

// استفاده از تابع چینش که تعریف کردیم، برای چینش هر دو لیست.

firstList.sort(sortByWeight); // Kate, Mike, Sophie, John 

secondList.sort(sortByWeight); // Jonathan, Margaret, Richard, Bill

در مثال بالا، به وضوح می‌توانید ببینید که استفاده از یک تابع سطح بالا چگونه می‌تواند فضا و زمان شما را ذخیره کند، و خواندن و استفاده مجدد کد شما را ساده‌تر کند. اگر می‌خواستید بدون استفاده از .sort() بنویسید، مجبور بودید که دو حلقه بنویسید و منطق را برای اکثر بخش‌ها تکرار کنید، که در نتیجه با یک کد طولانی‌تر و سخت‌تر مواجه می‌شدید.

چینش تنها کاری نیست که زیاد در یک آرایه انجام می‌دهیم. در تجربه من، فیلتر کردن یک آرایه بر حسب ویژگی، یک راه رایج و بهتر برای فیلتر کردن یک آرایه با استفاده از Array.prototype.filter() است. با توجه به این که شما فقط مجبورید یک تابع را به عنوان آرگومان منتقل کنید، که برای آیتم‌هایی که نیاز به فیلتر شدن دارند مقدار false، و در غیر این صورت مقدار true را بر می‌گرداند، فیلتر کردن سخت نیست. بیایید این مسئله را در عمل ببینیم:

// آرایه‌ای از مردم

var myFriends = [

  { name: "John", gender: "male" },

  { name: "Kate", gender: "female" },

  { name: "Mike", gender: "male" },

  { name: "Sophie", gender: "female" },

  { name: "Richard", gender: "male" },

  { name: "Keith", gender: "male" }

];

// یک فیلتر ساده بر حسب جنسیت

var isMale = function(x){

  return x.gender == "male";

}

myFriends.filter(isMale); // John, Mike, Richard, Keith

در حالیکه .filter() تمام عناصر در آرایه که با یک شرط برابری دارند را بر می‌گرداند، شما می‌توانید از Arrat.prototype.find() هم استفاده کنید تا فقط اولین عنصر در آرایه که با شرط مورد نظر برابری دارد را دریافت کنید؛ یا از Array.prototype.findIndex() استفاده کنید تا فهرست اولین عنصر مطابق را دریافت کنید. به طور مشابه، شما می‌توانید از Array.prototype.some() هم استفاده کنید تا ببینید که آیا تمام عناصر در آرایه با یک شرط برابری دارند یا خیر. این موارد می‌توانند در برخی برنامه‌ها به شدت پرکاربرد باشند، پس بیایید مثالی از استفاده از این متدها را ببینیم:

// آرایه‌ای از نمره‌های بالا. دقت کنید که برخی از آن‌ها نام مشخصی ندارند.

var highScores = [

  {score: 237, name: "Jim"},

  {score: 108, name: "Kit"},

  {score: 91, name: "Rob"},

  {score: 0},

  {score: 0}

];

// یک تابع ساده با قابلیت استفاده مجدد که بررسی می‌کند تا ببیند آیا یک نمره نام دارد یا نه، و این که آیا نمره بالاتر از صفر است.

var hasName = function(x){

  return typeof x['name'] !== 'undefined';

}

var hasNotName = function(x){

  return !hasName(x);

}

var nonZeroHighScore = function(x){

  return x.score != 0;

}

// نام‌های خالی را پر کن تا دیگر نام خالی‌ای موجود نباشد

while (!highScores.every(hasName)){

  var highScore = highScores.find(hasNotName);

  highScore.name = "---";

  var highScoreIndex = highScores.findIndex(hasNotName);

  highScores[highScoreIndex] = highScore;

}

// بررسی کن که آیا نمره‌های غیر صفر موجود هستند یا نه، و آن‌ها را چاپ کن.

if (highScores.some(nonZeroHighScore))

  console.log(highScores.filter(nonZeroHighScore));

else 

  console.log("No non-zero high scores!");

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

اگر یک قدم از منطق پیچیده مثال قبلی به عقب برویم، گاهی اوقات می‌خواهیم که یک آرایه را به یک آرایه دیگر با فیلدهای بیشتر یا کمتر تغییر شکل دهیم، بدون این که داده‌ها را زیاد تغییر دهیم. Array.prototype.map() در اینجا به میان می‌آید، که ما را قادر می‌سازد تا آبجکت‌ها را به آرایه تغییر شکل دهیم. تفاوت میان .map() و متد قبلی این است که تابع سطح بالایی که به عنوان آرگومان خود استفاده می‌کند، باید یک آبجکت را برگرداند، که می‌تواند تقریبا هر چیزی که شما می‌خواهید باشد. بگذارید این مسئله را با یک مثال ساده نشان دهم:

// آرایه‌ای از آبجکت‌ها

var myFriends = [

  { name: "John", surname: "Smith", age: 52},  

  { name: "Sarah", surname: "Smith", age: 49},  

  { name: "Michael", surname: "Jones", age: 46},  

  { name: "Garry", surname: "Thomas", age: 48}

];

// یک تابع ساده برای گرفتن نام و نام خانوادگی در یک رشته

var fullName = function(x){

  return x.name + " " + x.surname;

}

myFriends.map(fullName);

// Should output

// ["John Smith", "Sarah Smith", "Michael Jones", "Garry Thomas"]

در مثال بالا، با اعمال .map() به آرایه خود، ما به راحتی یک آرایه را دریافت کردیم، که آبجکتش فقط ویژگی‌های مورد نظر را دارد. در این مورد، ما فقط نماینده رشته فیلدهای name و surname آبجکت مورد نظر را می‌خواستیم، و از این رو با استفاده از یک mapping ساده، آرایه‌ای از رشته‌ها، از آرایه‌ای از آبجکت‌ها ساختیم. Mapping از آنچه که فکر می‌کنید رایج‌تر است و می‌تواند در دست هر توسعه دهنده وبی یک ابزار قدرتمند باشد؛ پس اگر قرار باشد یک نکته را از این مقاله برداشت کنید، آن نکته نحوه استفاده از .map() است.

در آخر، بهتر است به تغییر شکل آرایه همه منظوره، که به عبارتی Array.prototype.reduce() است توجه کنید. .reduce() کمی نسبت به دیگر متدهایی که در بالا به آن‌ها اشاره شد، متفاوت است؛ زیرا از یک تابع سطح بالا به عنوان آرگومان خود استفاده می‌کند. این مسئله ممکن است در ابتدا کمی گیج‌کننده به نظر برسد. پس یک مثال می‌تواند در درک اساس پشت .reduce() به شما کمک کند:

// آرایه قیمت‌ها

var oldExpenses = [

  { company: "BigCompany Co.", value: 1200.10},

  { company: "Pineapple Inc.", value: 3107.02},

  { company: "Office Supplies Inc.", value: 266.97}

];

var newExpenses = [

  { company: "Office Supplies Inc.", value: 108.11},

  { company: "Megasoft Co.", value: 1208.99}

];

// تابع جمع زدن ساده

var sumValues = function(sum, x){

  return sum + x.value;

}

// کاهش آرایه اول به جمع مقادیر

var oldExpensesSum = oldExpenses.reduce(sumValues, 0.0);

// کاهش آرایه دوم به جمع مقادیر.

console.log(newExpenses.reduce(sumValues, oldExpensesSum)); // 5891.19

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

البته، .reduce() می‌تواند کارهای بسیار بیشتری از اضافه کردن مقادیر داخل یک آرایه انجام دهد. اکثر تغییر شکل‌های پیچیده که در هیچ کدام از این متدها جای نمی‌گیرند، به راحتی می‌توانند با استفاده از .reduce() و یک accumulator آرایه یا آبجکت پیاده‌سازی شوند. هر کدام با یک عنوان و چند تگ در یک آرایه از تگ‌ها، و تعداد مقاله‌ها و آرایه‌ای از مقاله‌های مورد نظر. بیایید ببینیم که این مورد چگونه خواهد بود:

//آرایه‌ای از مقاله‌ها به همراه تگ آن‌ها

var articles = [

  {title: "Introduction to Javascript Scope", tags: [ "Javascript", "Variables", "Scope"]},

  {title: "Javascript Closures", tags: [ "Javascript", "Variables", "Closures"]},

  {title: "A Guide to PWAs", tags: [ "Javascript", "PWA"]},

  {title: "Javascript Functional Programming Examples", tags: [ "Javascript", "Functional", "Function"]},

  {title: "Why Javascript Closures are Important", tags: [ "Javascript", "Variables", "Closures"]},

];

// تابعی که آرایه بالا را به آرایه‌ای بر حسب تگ‌ها کاهش می‌دهد

var tagView = function(accumulator, x){

  // برای هر تگ در آرایه تگ مقاله

  x.tags.forEach(function(currentTag){

    // یک تابع بسار تا بررسی کند که آیا تطابق دارد

    var findCurrentTag = function(y) { return y.tag == currentTag; };

    // بررسی کن که آیا از قبل در آرایه وجود دارد

    if (accumulator.some(findCurrentTag)){

      // آن را پیدا کن و ورودی آن را بگیر

      var existingTag = accumulator.find(findCurrentTag);

      var existingTagIndex = accumulator.findIndex(findCurrentTag);

      // تعداد و آرایه مقاله‌ها را به روز رسانی کن

      accumulator[existingTagIndex].count += 1;

      accumulator[existingTagIndex].articles.push(x.title);

    }

    // در غیر این صورت تگ را به آرایه اضافه کن

    else {

      accumulator.push({tag: currentTag, count: 1, articles: [x.title]});

    }

  });

  // آرایه را برگردان

  return accumulator;

}

// آرایه اصلی را تغییر شکل بده

articles.reduce(tagView,[]);

// خروجی:

/*

[

 {tag: "Javascript", count: 5, articles: [

    "Introduction to Javascript Scope", 

    "Javascript Closures",

    "A Guide to PWAs", 

    "Javascript Functional Programming Examples",

    "Why Javascript Closures are Important"

 ]},

 {tag: "Variables", count: 3, articles: [

    "Introduction to Javascript Scope", 

    "Javascript Closures",

    "Why Javascript Closures are Important"

 ]},

 {tag: "Scope", count: 1, articles: [ 

    "Introduction to Javascript Scope" 

 ]},

 {tag: "Closures", count: 2, articles: [

    "Javascript Closures",

    "Why Javascript Closures are Important"

 ]},

 {tag: "PWA", count: 1, articles: [

    "A Guide to PWAs"

 ]},

 {tag: "Functional", count: 1, articles: [

    "Javascript Functional Programming Examples"

 ]},

 {tag: "Function", count: 1, articles: [

    "Javascript Functional Programming Examples"

 ]}

]

*/

مثال بالا ممکن است کمی سخت‌تر از آنچه که می‌باشد به نظر برسد، پس بیایید آن را بشکنیم. اول از همه، ما می‌خواهیم یک آرایه را به عنوان آخرین نتیجه خود بگیریم؛ از این رو مقدار شروع accumulator ما «[ ]» خواهد بود. سپس، ما می‌خواهیم که هر آبجکت در آرایه، نام تگ مورد نظر، تعداد و لیست مقاله‌ها را شامل باشد. ما همچنین می‌دانیم که هر تگ باید فقط یک بار در آرایه ظاهر شود، پس باید به مانند قبل با استفاده از .some()، .find() و .findIndex() بررسی کنیم که آیا وجود دارد یا نه، تا به جای اضافه کردن یک آبجکت تگ جدید، آبجکت تگ موجود را تغییر شکل دهیم.

بخش پیچیده در اینجا این است که ما نمی‌توانیم یک تابع را تعریف کنیم تا بررسی کند که آیا یکی از تگ‌ها موجود هستند یا نه، (در غیر این صورت به ۷ تابع مختلف نیاز خواهیم داشت) پس باید تابع سطح بالای خود را داخل حلقه مربوط به تگ فعلی تعریف کنیم. به این صورت می‌توانیم از آن مجددا استفاده کرده، و از کدنویسی مجدد آن جلوگیری کنیم.

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

نتیجه گیری

برنامه‌نویسی تابعی در حال حاضر یکی از داغ‌ترین گرایش‌ها است، و این مسئله علت خوبی هم دارد. این نوع برنامه‌نویسی ما را قادر می‌سازد تا کد بهتر و مرتب‌تری را بدون این که مجبور باشیم نگران اثرات جانبی باشیم، بنویسیم. متد Array.prototype در JavaScript بسیار پرکاربرد است و در بسیاری از موقعیت‌ها ما را قادر می‌سازد تا تغییر شکل‌های ساده و پیچیده را بدون تکرار کردن، به آرایه‌ها اضافه کنیم.

منبع

مقالات پیشنهادی

با استفاده از Billboardjs.js، نمودارهای داده‌ای بر پایه JavaScript بسازید

گرافیک و ویژگی‌های بصری، نقش حیاتی‌ای در پیشرفت محتویات وب بازی می‌کنند. با فناوری وب مدرن، اضافه کردن ویژگی‌های بصری سفارشی مانند آیکون‌های SVG در صف...

برترین افزونه‌های VSCode برای JavaScript، در جهت توسعه سریع‌تر

VSCode یک ویرایشگر اوپن سورس و میان پلتفرمی است، که تبدیل به ویرایشگر مورد علاقه بسیاری از توسعه دهندگان، به خصوص در جامعه توسعه وب شده است. این ویرای...

15 کتابخانه جالب javascript و css

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...

15 کتابخانه جالب javascript و css دی ۹۵

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...