اجرای مستقیم و غیرمستقیم توابع جاوا اسکریپتی – نحوه ی استفاده از Bind

07 اردیبهشت 1399
javascript-bind

یکی از موضوعاتی که اکثر برنامه نویسان تازه کار جاوا اسکریپت با آن مشکل دارند، بحث اجرای توابع است. در جاوا اسکریپت می توانیم توابع را به دو صورت مختلف اجرا کنیم: اجرای مستقیم یا direct execution و اجرای غیرمستقیم یا indirect execution. به همین دلیل تصمیم گرفتم که این مسئله را با هم تمرین کنیم و موضوعاتی مثل استفاده از Bind در جاوا اسکریپت برای کلیدواژه ی this را نیز متوجه شویم. در ابتدا از این لینک یک پروژه ی آماده را دانلود کنید تا با هم کدنویسی را شروع کنیم. این پروژه ی آماده، مقداری کد CSS و HTML و جاوا اسکریپت است و در حال حاضر فقط تابع init ر اجرا می کند. Init نیز goals را در صفحه چاپ می کند:

const goals = [
  {
    id: 'g1',
    text: 'Learn all about JavaScript!'
  },
  {
    id: 'g2',
    text: 'Understand JavaScript function execution.'
  }
];
ظاهر برنامه ی آماده شده - شامل یک لیست و یک عنوان
ظاهر برنامه ی آماده شده - شامل یک لیست و یک عنوان

من با کدهای فایل App.js شروع می کنم تا مواردی را به شما توضیح بدهم. به تابع init توجه کنید:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;

    goalListElement.append(goalElement);
  }
}

به کد بالا function declaration یا function definition می گوییم، یعنی «تعریف تابع» چرا که این کد تا زمانی که فراخوانی نشود اجرا نخواهد شد. در این یک یک تابع فقط تعریف شده است. از طرفی ما این تابع را در خط 32 سورس کدی که به شما داده ام، فراخوانی می کنیم:

init();

بنابراین زمانی که صفحه ی HTML ما باز شود، نیاز به فایل app.js دارد و آن را دانلود و اجرا خواهد کرد. با این کار کدهای درون app.js اجرا می شوند که فراخوانی init یکی از آن ها است. به همین دلیل است که به محض بارگذاری صفحه، لیست خود را مشاهده می کنیم (تابع init مسئول نمایش این لیست است). ما به این روش می گوییم اجرای مستقیم توابع (direct execution) چرا که ما به عنوان توسعه ی دهنده ی این برنامه مستقیما کد مربوط به اجرای این تابع را نوشته ام.

در حال حاضر (در تصویر بالا مشخص است) برای هر کدام از لیست ها یک button داریم که کاری انجام نمی دهد. من می خواهم کاری کنم که با کلیک کردن روی این دکمه، متن آن به قسمت بالای برنامه منتقل شود (همان قسمتی که می گوید No Active Goal. در سورس کدی که به شما دادم، کد این کار موجود است:

function setAsActiveHandler(goalId) {
  const selectedGoal = goals.find(g => g.id === goalId);
  activeGoalElement.textContent = selectedGoal.text;
}

کد بالا goalId را به صورت پارامتر دریافت می کند (این id از سمت کاربر می آید، زمانی که روی یکی از آیتم های لیست کلیک کند) سپس این id را در goals جست و جو می کند تا ببیند، آیتم کلیک شده با کدام یک از آیتم های موجود در Goals آیدی یکسانی دارند. سپس activeGoalElement را گرفته و متن آن را به متن موجود در goal مورد نظر تغییر می دهد.

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

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement.querySelector('button').addEventListener('click', setAsActiveHandler());
    goalListElement.append(goalElement);
  }
}

ما یک event listener را به goalElement متصل کرده ایم و تا این قسمت هیچ مشکلی نیست اما پاس دادن تابع setAsActiveHandler به شکل بالا کاملا اشتباه است! اگر کد بالا را در مرورگر اجرا کنید، خطای زیر را دریافت خواهید کرد:

Cannot read the property ‘text’ of undefined …

به نظر شما چرا با این مشکل روبرو می شویم؟ حدس اولیه ی شما این است که تابع setAsActiveHandler یک آرگومان ورودی به نام goalId دارد اما در کد بالا این آرگومان به آن پاس داده نشده است. حدستان صحیح است اما باز هم برنامه نویسان تازه کار با مشکل مواجه می شوند:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement.querySelector('button').addEventListener('click', setAsActiveHandler(goal.id));
    goalListElement.append(goalElement);
  }
}

برنامه نویس ما در کد بالا گفته است: خب ما که درون یک for هستیم و در هر گردش به شیء goal دسترسی داریم بنابراین می توانیم goal.id را به این تابع پاس بدهیم. آیا مشکل حل شده است؟ اگر کد بالا را ذخیره و در مرورگر اجرا کنید، خطایی دریافت نمی کنیم اما مشکل دیگری خواهیم داشت؛ goal دوم (آیتم دوم در لیست) به صورت پیش فرض و بدون کلیک ما به عنوان active goal در نظر گرفته می شود:

تنظیم خودکار دومین آیتم لیست به عنوان title صفحه!
تنظیم خودکار دومین آیتم لیست به عنوان title صفحه!

بدتر از این موضوع، آنجاست که متوجه می شویم با کلیک کردن روی button ها هیچ تغییری اتفاق نمی افتد! دوباره به کد زیر نگاه کنید:

    goalElement.querySelector('button').addEventListener('click', setAsActiveHandler(goal.id));

اینطور نیست که کد بالا بگوید هر زمان که روی button کلیک شد، تابع setAsActiveHandler را اجرا کن. با قرار دادن پرانتز برای یک تابع، آن تابع سریعا اجرا خواهد شد. در کد بالا نیز زمانی که اجرای کد به این خط برسد، بدون وقفه تابع setAsActiveHandler را اجرا می کند (دقیق مانند نحوه ی اجرای init).

با این حساب زمانی که صفحه بارگذاری می شود، وارد حلقه ی For می شویم و کد بالا نیز می گوید زمانی که به این خط رسیدی، تابع برگردانده شده توسط setAsActiveHandler را اجرا کن! تابع setAsActiveHandler نیز هیچ تابع دیگری را Return نمی کند که بخواهد به عنوان آرگومان دوم addEventListener قرار بگیرد. بدین صورت آرگومان دوم پاس داده شده به addEventListener برابر با undefined خواهد شد. از طرفی اگر بخواهیم به مرورگر بگوییم که ما به دنبال اجرای نتیجه ی تابع نیستیم بلکه می خواهیم خود تابع را اجرا کنیم بای پرانتزهایش را حذف کنیم:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement.querySelector('button').addEventListener('click', setAsActiveHandler);
    goalListElement.append(goalElement);
  }
}

با این کار فقط یک pointer به تابع setAsActiveHandler را به addEventListener داده اید. Pointer یعنی نشانگر و منظور ما در این قسمت این است که خود تابع setAsActiveHandler پاس داده نشده است بلکه یک نشانگر پاس داده شده است. یعنی به جاوا اسکریپت گفته ایم که اگر به این خط رسیدی و روی عنصر ما کلیک شد، باید تابعی را اجرا کنی که من به آن اشاره کرده ام (point کرده ام). متوجه تفاوت شدید؟ در حالت قبل که از پرانتزها استفاده کرده بودیم، pointer ای در کار نبود و ما خود تابع را در همان خط اجرا می کردیم تا ببینیم چیزی به ما برمی گرداند یا خیر اما زمانی که نام تابع را بدون پرانتز قرار می دهیم یک pointer به مکان اصلی تعریف تابع خواهیم داشت و خود تابع اجرا نخواهد شد (چرا که خود تابع را نداریم، فقط به آن اشاره یا point می کنیم).

دو راه حل برای این مشکل وجود دارد. راه حل اول می گوید که باید یک تابع anonymous را به عنوان آرگومان دوم پاس بدهیم و درون این تابع، تابع خودمان را صدا بزنیم! به کد زیر توجه کنید:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement
    .querySelector('button')
    .addEventListener('click', function () {
      setAsActiveHandler(goal.id);
    });
    goalListElement.append(goalElement);
  }
}

من کد را شکسته ام (در چند خط قرار داده ام) تا خوانا تر شود. راه حل بالا بر اساس حرفی است که قبلا به شما زدم. ما گفتیم زمانی که نام تابع setAsActiveHandler را به همراه پرانتز بنویسیم، به جاوا اسکریپت گفته ایم setAsActiveHandler تابع دیگری را return خواهد کرد که باید آن تابع را اجرا کنی و از آنجایی که تابع دیگری توسط setAsActiveHandler برگردانده نمی شد با خطای undefined روبرو می شدیم. ما می توانیم از همین مبحث استفاده کنیم تا مشکل را حل کنیم!

در کد بالا به جای اینکه تابع setAsActiveHandler را مستقیما صدا بزنم، یک تابع به آن داده ام (function declaration یا تعریف تابع) که تابع setAsActiveHandler را درون خود صدا می زند. بدین صورت تابع را درجا اجرا نکرده ایم بلکه فقط تعریف کرده ایم. اگر می خواستیم این تابع anonymous را درجا اجرا کنیم باید در انتهای آن دو پرانتز قرار می دادیم:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement
    .querySelector('button')
    .addEventListener('click', function () {
      setAsActiveHandler(goal.id);
    }());
    goalListElement.append(goalElement);
  }
}

قرار دادن دو پرانتز در انتهای تعریف این تابع باعث اجرا شدن سریع آن می شود اما شما نباید چنین کاری کنید و گرنه با همان مشکل قبلی مواجه می شویم. در ضمن به عنوان نکته ی جانبی باید بگویم که شما می توانید از arrow function ها نیز استفاده کنید و تفاوتی بین توابع ES5 و ES6 وجود ندارد. مثال:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement
    .querySelector('button')
    .addEventListener('click', () => {
      setAsActiveHandler(goal.id);
    });
    goalListElement.append(goalElement);
  }
}

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

روش اول بدون عیب است اما روش بهتر و کوتاه تری برای حل این مشکل وجود دارد. برای استفاده از روش دوم باید بدانید که تمام توابع در جاوا اسکریپت، به نوعی یک شیء هستند. همانطور که می دانید اشیاء می توانند متد های مختلفی داشته باشند. به کد زیر نگاه کنید:

function message() {
  alert("Greetings Linda!");
}
alert(typeof message);                   // => function
alert(message instanceof Object);        // => true

من در این کد یک تابع بسیار ساده به نام message را تعریف کرده ام. اگر از اپراتور typeof برای message استفاده کنیم، مقدار function را دریافت می کنیم که یعنی message یک تابع است اما اگر از اپراتور instanceof استفاده نماییم متوجه می شویم که message یک نمونه از کلاس Object (شیء) می باشد. به طور مثال اگر یادتان باشد در ES5 برای نوشتن کدهای شیء گرا باید به شکل زیر عمل می کردیم:

function Book (type, author) {
  this.type = type;
  this.author = author;
  this.getDetails = function () {
      return this.type + " written by " + this.author;
  }
}

به عبارتی، book که یک تابع است در کد بالا constructor کلاس حساب می شود و هر کلاس یا شیء ای می توانید خصوصیات (property) و متد (method) های مختلفی داشته باشد.

مسئله اینجاست که تمام توابع جاوا اسکریپت (که شیء هستند) متدی به نام bind را در خودشان دارند. Bind در جاوا اسکریپت تابعی را که شما نوشته اید، گرفته و سپس با استفاده از اطلاعات پاس داده شده یک تابع جدید را می سازد. تابعی که از قبل تنظیم شده و آماده ی اجرا شدن است. مثلا یکی از رایج ترین استفاده های bind تعیین کلیدواژه ی This است (اگر با مبحث Closure آشنا باشید می دانید که this در توابع تغییر می کند و دیگر به شیء اصلی اشاره نمی کند که می توان با bind جلوی این اتفاق را گرفت). ما در کدمان با this کاری نداریم اما می توانیم آرگومان های این تابع را با bind تنظیم کنیم.

در واقع Bind در جاوا اسکریپت دو آرگومان می گیرد: آرگومان اول می پرسد که کلیدواژه ی this درون این تابع به چه چیزی اشاره کند و از آنجایی که ما با this کار نداریم، می توانیم null را به آن پاس بدهیم. آرگومان دومی که bind می گیرد برابر با اولین پارامتری است که برای تابع تعریف شده است ( GoalId) بنابراین:

function init() {
  for (const goal of goals) {
    const goalElement = document.createElement('li');
    goalElement.innerHTML = `
      <span>${goal.text}</span>
      <button>Set as Active Goal</button>
    `;
    goalElement
    .querySelector('button')
    .addEventListener('click', setAsActiveHandler.bind(null, goal.id));
    goalListElement.append(goalElement);
  }
}

نکته: اگر تعداد آرگومان های تابع شما بیشتر بود، می توانید تعداد بیشتری را پاس بدهید:

    .addEventListener('click', setAsActiveHandler.bind(null, goal.id, 'second argument', 'third argument'));

همچنین اگر مانند کد ما addEventListener داشتید که در آن آرگومانی به صورت خودکار به تابع پاس داده می شود (در کد بالا event به صورت خودکار پاس داده خواهد شد) این آرگومان به عنوان آخرین آرگومان و به صورت خودکار به bind داده می شود و نیازی نیست شما کاری انجام دهید. برای اینکه با نحوه ی کار آن آشنا شوید تابع setAsActiveHandler را به شکل زیر تغییر می دهم:

function setAsActiveHandler(goalId, event) {
  console.log(event);
  const selectedGoal = goals.find(g => g.id === goalId);
  activeGoalElement.textContent = selectedGoal.text;
}

یعنی شیء Event را نیز گرفته و آن را log می کند تا با آن هم کار کرده باشیم. از آنجایی که bind را صدا زده ایم، Event نیز به صورت خودکار پاس داده می شود و نیازی به کار دیگری نیست. حالا می توانیم مرورگر را باز کرده و روی یکی از دکمه ها کلیک کنیم:

در شکل مشاهده می کنید که bind مشکل ما را حل کرده و برنامه بدون خطا کار می کند
در شکل مشاهده می کنید که bind مشکل ما را حل کرده و برنامه بدون خطا کار می کند

امیدوارم این مقاله به شما در درک اجرای توابع و استفاده از Bind در جاوا اسکریپت کمک کرده باشد.

نویسنده شوید
دیدگاه‌های شما (1 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

احقافففف
26 تیر 1399
ولی به هر صورت باز هم متوجه نشدیم که چه لزومی برای استفاده از bind وجود داره؟؟؟ به نظر من bind دیگه منسوخ شده و هزاران روش جدید، جایگزین اون شده.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.