Closure ها و متد ()apply در جاوا اسکریپت

09 فروردین 1398
Advanced-Javascript-closure-apply-methods

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

متد ()apply

در قسمت قبل در مورد ()call صحبت کردیم و به شما نشان دادیم که با استفاده از آن می توان از متدهای توابعی در توابع دیگر استفاده کرد. متد ()apply شباهت بسیار زیادی با ()call دارد و کار آن نوشتن متدهایی است که می تواند در اشیاء متفاوتی مورد استفاده قرار بگیرد. apply در لغت به معنی «به کار بردن» و «اعمال کردن» است.

به طور مثال در کد زیر متدی به نام fullName تعریف می کنیم. با اینکه این متد متعلق به شیء person است اما روی person1 هم به کار می رود:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Functions</h2>
<p>In this example the fulllName method of person is <b>applied</b> on person1:</p>

<p id="demo"></p>

<script>
var person = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
var person1 = {
  firstName:"John",
  lastName: "Doe"
}
var x = person.fullName.apply(person1); 
document.getElementById("demo").innerHTML = x; 
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

میبینید که به راحتی نام کامل John Doe را برای ما برمیگرداند.

تفاوت بین call و apply

تفاوت آن ها به زبان ساده این است که:

  • ()call آرگومان هایش را جداگانه دریافت می کند.
  • ()apply آرگومان هایش را یکجا و به شکل یک آرایه دریافت می کند.

بنابراین زمانی که یک آرایه داشته باشید استفاده از ()apply بسیار آسان تر است.

به مثال زیر توجه کنید:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Functions</h2>

<p>In this example the fulllName method of person is <b>applied</b> on person1:</p>

<p id="demo"></p>

<script>
var person = {
  fullName: function(city, country) {
    return this.firstName + " " + this.lastName + "," + city + "," + country;
  }
}
var person1 = {
  firstName:"John",
  lastName: "Doe"
}
var x = person.fullName.apply(person1, ["Oslo", "Norway"]); 
document.getElementById("demo").innerHTML = x; 
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

اگر به apply دقت کنید متوجه می شوید که آرگومان هایش به این شکل هستند: ;(["apply(person1, ["Oslo", "Norway

بنابراین نیازی به استفاده ی چند باره از این دستور نیست. البته این مثال کوچک است و اعضای آرایه هم دو عدد هستند بنابراین نوشتن آن با متد call نیز آنچنان فرقی نمی کند:

person.fullName.call(person1, "Oslo""Norway");

پیدا کردن مقادیر بزرگ آرایه ها

ممکن است آرایه هایی که به متد apply می دهید بسیار بزرگ باشند و بخواهید بزرگترین مقدار آن را پیدا کنید. اگر یادتان باشد قبلا گفته بودیم که ()Math.max یکی از راه های انجام این کار (بین لیستی از اعداد) است:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Math.max()</h2>

<p>This example returns the highest number in a list of number arguments:</p>

<p id="demo"></p>

<script>
document.getElementById("demo").innerHTML = Math.max(1,2,3); 
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

مشخص است که عدد 3 از همه بزرگتر بوده و همان هم خروجی ما خواهد شد اما آرایه ها در زبان جاوا اسکریپت متد ()max را ندارند (مثال بالا آرایه نبود، بلکه تنها لیستی از اعداد بود که به صورت آرگومان به متد max داده شده بود) بنابراین راه حل ما استفاده از ()Math.max است:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript apply()</h2>

<p>This example returns the highest number in an array of numbers:</p>

<p id="demo"></p>

<script>
document.getElementById("demo").innerHTML = Math.max.apply(null, [1,2,3]); 
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

خروجی باز هم عدد 3 خواهد بود. آرگومان اولی که به متد بالا داده شده است (یعنی NULL) مهم نیست و اهمیتی ندارد. مثال بالا را می توان به اشکال زیر نوشت و در همه ی حالات همان عدد 3 خروجی ما خواهد بود:

Math.max.apply(Math, [1,2,3]); // خروجی عدد 3 خواهد بود
Math.max.apply(" ", [1,2,3]); // خروجی عدد 3 خواهد بود
Math.max.apply(0, [1,2,3]); // خروجی عدد 3 خواهد بود

closure چیست؟

closure در لغت به معنی «خاتمه» و «بستن» است اما تعریف فنی آن از این قرار است: به رابطه ی بین یک تابع و محیطی که در آن تعریف شده است closure میگویند. در واقع یک closure یک تابع است که به تابع پدر خود دسترسی دارد، حتی پس از آنکه تابع پدر بسته شده باشد. اگر کمی گیج شده اید نگران نباشید، در ادامه ی بحث متوجه خواهید شد.

همانطور که می دانید متغیرهای جاوا اسکریپت می توانند دارای scope محلی (local) و یا سراسری (global) باشند. ما می توانیم با استفاده از closure ها متغیر های سراسری را تبدیل به متغیر های محلی کنیم.

همه ی ما از دوره ی قبلی جاوا اسکریپت یاد گرفته ایم که یک تابع به تمام متغیر هایی که درون خودش تعریف شده باشند دسترسی دارد:

<!DOCTYPE html>
<html>
<body>

<p>A function can access variables defined inside the function:</p>

<button type="button" onclick="myFunction()">Click Me!</button>

<p id="demo"></p>

<script>
function myFunction() {
  var a = 4;
  document.getElementById("demo").innerHTML = a * a;
} 

</script>

</body>
</html>

مشاهده ی خروجی در JSBin

اما خیلی اوقات فراموش می کنیم که توابع به متغیر هایی که خارج از تابع تعریف شده باشند هم دسترسی دارند. به طور مثال می توانیم کد بالا را به این شکل بنویسیم:

var a = 4;
function myFunction() {
  return a * a;
}

یعنی متغیر a را خارج از تابع تعریف کنیم. در این صورت هم خروجی ما عدد 16 می شود.

در کد بالا (که متغیر a خارج از تابع است) متغیر a یک متغیر سراسری یا global محسوب می شود و همه ی ما میدانیم که متغیر های سراسری متعلق به شیء window هستند. همچنینن تمام متغیر های سراسری می توانند از طریق تمام کد های سورس کد تغییر پیدا کنند و قابل دسترس باشند.

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

نکته: متغیر هایی که بدون کلیدواژه ی var ساخته شوند همیشه از نوع سراسری خواهند بود (حتی اگر داخل یک تابع ساخته شوند).

طول عمر متغیرها

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

حالا تصور کنید می خواهید متغیری بسازید و با آن چیزی را بشمارید (یک نوع counter یا شمارنده). از طرفی می خواهید این شمارنده در دسترس تمام توابع قرار بگیرد. راه اول و آسانی که به ذهن می آیند تعریف یک تابع به همراه یک متغیر سراسری است:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Function Closures</h2>

<p>Counting with a global variable.</p>

<p id="demo"></p>

<script>
// شمارنده ی اولیه
var counter = 0;

// تابعی که شمارنده را هر باز زیاد کند
function add() {
  counter += 1;
}

// تابع را سه بار صدا می زنیم
add();
add();
add();

// شمارنده باید روی عدد 3 قرار بگیرد
document.getElementById("demo").innerHTML = "The counter is: " + counter;
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

قبل از اینکه به خودمان آفرین بگوییم باید خوب به کدی که نوشته ایم نگاه کنیم. این کد مشکل دارد! هر کدی که در صفحه باشد می تواند بدون صدا زدن تابع ()add، مقدار شمارنده ی ما را تغییر دهد. چرا؟ به دلیل اینکه این شمارنده سراسری است و در دسترس همه است!

برای حل مشکل باید متغیر را به صورت محلی و درون تابع تعریف کنیم تا فقط از طریق تابع در دسترس باشد:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Function Closures</h2>

<p>Counting with a local variable.</p>

<p id="demo"></p>

<script>
// شمارنده ی ابتدایی
var counter = 0;

// تابع برای اضافه کردن مقدار شمارنده
function add() {
  var counter = 0; 
  counter += 1;
}

// صدا زدن تابع به تعداد 3 مرتبه
add();
add();
add();

// خروجی دیگر عدد 3 نخواهد بود چرا که متغیر محلی و سراسری را قاطی کرده ایم
document.getElementById("demo").innerHTML = "The counter is: " + counter;
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

می دانید چرا این کد کار نمی کند؟ به این خاطر که هنوز متغیر سراسری را در کد نگه داشته ایم بنابراین به جای نمایش شمارنده ی محلی، شمارنده ی سراسری به نمایش در می آید و خروجی ما 0 می شود.

برای حل این مشکل باید ابتدا متغیر یا همان شمارنده ی سراسری را حذف کنیم و برای دسترسی به متغیر محلی به تابع بگوییم که آن را return کند:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Closures</h2>

<p>Counting with a local variable.</p>

<button type="button" onclick="myFunction()">Count!</button>

<p id="demo">0</p>

<script>
// تابع برای اضافه کردن مقدار شمارنده
function add() {
  var counter = 0;
  counter += 1;
  return counter;
}
// اضافه کردن مقدار شمارنده
function myFunction(){
  document.getElementById("demo").innerHTML = add();
}
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

این کد هنوز هم کار نمی کند و شمارنده ی ما تا 1 بیشتر اضافه نمی شود. چرا؟ به این دلیل که هر بار شمارنده را ریست می کنیم! راه حل آن توابع داخلی هستند!

توابع تو در تو (nested)

میدانیم که تمامی توابع جاوا اسکریپتی به scope سراسری دسترسی دارند اما نکته ای که اکثر برنامه نویسان مبتدی از آن بی خبر هستند این است که توابع جاوا اسکریپتی به به scope بالاتر از خودشان دسترسی دارند؛ یعنی همان توابع تو در تو!

اگر به مثال زیر دقت کنید متوجه می شوید که از یک تابع درونی به نام ()plus استفاده کرده ایم که به متغیر counter به معنی «شمارنده» دسترسی دارد:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Function Closures</h2>

<p>Counting with a local variable.</p>

<p id="demo">0</p>

<script>
document.getElementById("demo").innerHTML = add();
function add() {
  var counter = 0;
  function plus() {counter += 1;}
  plus();  
  return counter; 
}
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

این کد می تواند مشکل شمارنده ی ما را حل کند. برای حل مشکل شمارنده 2 کار باقی مانده است:

  • دسترسی به تابع ()plus از بیرونِ تابع
  • اجرای counter = 0 تنها یک بار

بنابراین به یک closure نیاز داریم!

آیا توابع self-invoking را از قسمت های قبل به یاد دارید؟ با استفاده از آن ها می نویسیم:

<!DOCTYPE html>
<html>
<body>

<h2>JavaScript Closures</h2>

<p>Counting with a local variable.</p>

<button type="button" onclick="myFunction()">Count!</button>

<p id="demo">0</p>

<script>
var add = (function () {
  var counter = 0;
  return function () {counter += 1; return counter;}
})();

function myFunction(){
  document.getElementById("demo").innerHTML = add();
}
</script>

</body>
</html>

مشاهده ی خروجی در JSBin

توضیح کد:

متغیر add مقدار تابع self-invoking (خودخوان) بالا را بر میگرداند (return می کند). همانطور که می دانید توابع خودخوان تنها یک بار اجرا می شوند بنابراین شمارنده را یک بار 0 می کند و تابع درونش را بر میگرداند. حالا add تبدیل به یک تابع می شود و می تواند به scope پدر خود دسترسی داشته باشد. به این موضوع closure می گوییم که به توابع اجازه می دهد متغیر های خصوصی داشته باشند؛ شمارنده ی ما از طریق scope تابعِ anonymous (ناشناس) محافظت شده و تنها می تواند از طریق تابع add تغییر کند.

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

امیدوارم از این قسمت لذت برده باشید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری جاوا اسکریپت پیشرفته توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

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