46 - جاوااسکریپت: Closure

موضوع closure در js مطرح شده و فهم آن دشوار است. تمام آنچه تا اینجا گفته شد برای درک چگونگی اتفاق افتادن چیزها، درک first class function، درک execution stack و execution context برای فهم closure در js لازم است. ابتدا میخواهیم قدرت closure را نشان دهیم. اینجا به جای اینکه داخل تابع کاری کنیم، تابعی را با استفاده از function expression برمیگردانیم و میخواهیم از فواید scope chain استفاده کنیم:function greet(whattosay) { return function(name) { console.log(whattosay+' '+name); }}greet('Hi')('Tony');یک تابع greet داریم که یک تابع برمیگرداند. بنابراین زمانی که greet را فراخوانی کنیم، یک تابع به ما برگردانده میشود که میتوانیم دوباره آن را invoke کنیم. کد بالا خروجی Hi Tony را میدهد. اما یک چیز غیرمعمول در حال انجام است. برای اینکه بفهمیم چگونه غیرمعمول است، کمی متفاوت کد میزنیم و یک متغیر را برابر با نتیجه تابع قرار میدهیم:function greet(whattosay) { return function(name) { console.log(whattosay+' '+name); }}var sayHi = greet('Hi');sayHi('Tony');بنابراین متغیر sayHi برابر با تابعی است که تابع greet برمیگرداند. خروجی کد بالا Hi Tony میشود. اما چگونه تابع sayHi هنوز متغیر whattosay را میشناسد. متغیر whattosay وقتی greet('Hi') را صدا زدیم ایجاد شده و وقتی کار این تابع تمام میشود، execution context آن از استک pop میشود. اما وقتی بعد از آن sayHi('Tony') را فراخوانی میکنیم، همچنان مقدار صحیح متغیر whattosay را دارد. چگونه این اتفاق افتاده است؟ دلیل آن closure است.این مسئله غیرمعمول به نظر میرسد چون تابع greet تمام میشود و وقتی تابعی که برگردانده شده را invoke میکنیم، اینطور به نظر میرسد که تابع greet همچنان جایی هست چون متغیر whattosay هنوز وجود دارد. آنچه اتفاق می افتد این است که وقتی کد شروع میشود global execution context را داریم. وقتی به خط var sayHi = greet('Hi'); میرسیم، تابع greet مان invoke میشود و یک execution context جدید ایجاد میشود و متغیری که به آن پاس داده شده است یعنی whattosay در variable environment آن قرار میگیرد. این تابع یک تابع جدید را برمیگرداند. یعنی تابع را on the fly ایجاد میکند و آن را برمیگرداند. پس از return کردن، execution context از استک pop میشود. قبلا گفته شد که هر execution context برای متغیرها و تابع هایی که داخلش ایجاد میشوند فضای خودش را در حافظه دارد. وقتی execution context از بین میرود، برای آن فضای حافظه چه اتفاقی می افتد؟در شرایط نرمال، موتور js با فرایند garbage collection این فضا را خالی میکند. اما در لحظه ای که execution context تمام میشود، این فضای حافظه هنوز آنجا هست. execution context رفته ولی این فضا جایی در حافظه است. پس به global execution context میرویم و سپس تابع sayHi('Tony') را invoke میکنیم. این یک تابع anonymous است چون به تابع اسمی نداده ایم. execution context جدید ایجاد میشود و متغیر name به آن پاس داده میشود و داخل حافظه آن قرار میگیرد. اما وقتی به خط console.log(whattosay+' '+name); از کد میرسیم و این کد invoke میشود، موتور js متغیر whattosay را میبیند. پس در scope chain پیش میرود. یعنی به جایی که تابع در آن ایجاد شده میرود. گرچه execution context مربوط به تابع greet از بین رفته و از استک pop شده، execution context مربوط به sayHi هنوز reference به متغیرها و فضای حافظه outer environment دارد. یعنی اگرچه تابع greet در واقع تمام شده اما همه تابع های ایجاد شده داخل آن وقتی فراخوانی میشوند، هنوز reference به فضای حافظه execution context تابع greet دارند. تابع greet رفته ولی حافظه آن execution context از بین نرفته. موتور js این اطمینان را میدهد که تابع من همچنان میتواند از scope chain پایین رود حتی اگر هنوز در execution stack نباشد. در این حالت میگوییم که execution context با outer variable هایش (متغیرهایی که در حالت نرمال به آنها reference دارد) closed-in است، حتی اگر آن execution context ها از بین رفته باشند. این پدیده یعنی close-in بودن همه متغیرهایی که فرض میشود به هم دسترسی دارند closure نام دارد. این پدیده چیزی نیست که ما ایجاد یا تایپ کنیم یا ما به موتور js بگوییم انجام دهد. closure یک ویژگی زبان برنامه نویسی js است. مهم نیست کی یک تابع را فراخوانی میکنیم. نیازی نیست نگران این باشیم که آیا outer environment آن هنوز در حال اجرا هست یا نه. موتور js همیشه این اطمینان را میدهد که هر تابعی را اجرا کنیم به متغیرهایی که فرض میکند دسترسی دارد، واقعا دسترسی داشته باشد. این یک ویژگی زبان است و بسیار مهم و قدرتمند است و بسیار از آن استفاده میکنیم. به ما اجازه میدهد که از الگوهای جالب زیادی در کد استفاده کنیم. درک اینکه چه اتفاقاتی می افتد به ما در ساده تر کردن درک closure ها کمک میکند. closure ها ویژگی هستند که ما را مطمئن میکنند وقتی یک تابع را اجرا میکنیم، همان طور که باید کار میکند و به outer variable هایش دسترسی دارد. مهم نیست که اجرای تابع دیگر تمام شده یا نه. بنابراین وقتی میگوییم که یک closure ایجاد کرده ایم، در واقع js آن را ایجاد کرده است و ما فقط از آن فایده میبریم.47 - جاوااسکریپت: Understanding Closuresاگر کلمه closure را سرچ در اینترنت کنیم، به مثال زیر میرسیم:function buildFunctions() {var arr = [ ];for (var i=0; i<3; i++) {arr.push( function() {console.log(i);} );}return arr;}var fs = buildFunctions();fs[0]();fs[1]();fs[2]();داخل آرایه تابع اضافه میکنیم. آرایه میتواند شامل هر چیزی باشد. داخل آرایه سه تا تابع قرار دادیم. این آرایه را در متغیر fs ریختیم. در این قسمت از کد که arr.push(function() {console.log(i);}); را نوشتیم در حال invoke کردن تابع نیستیم و فقط آن را ایجاد میکنیم. تابع ها در قسمت fs[0](); فراخوانی میشوند. وقتی به این کد نگاه میکنیم احتمالا انتظار داریم که خروجی 0 و 1 و 2 باشد. اما خروجی 3 و 3 و 3 میشود. چرا هر بار که دنبال i در outer reference رفته 3 را پیدا کرده؟وقتی این کد اجرا میشود global execution context پایین execution stack ایجاد میشود. داخل آن buildFunctions() و fs قرار دارد. وقتی به خط var fs = buildFunctions(); میرسیم، یک execution context ایجاد میشود که داخل آن دو متغیر arr و i وجود دارد. وقتی به خط کد return arr; میرسیم مقدار این متغیرها چند است؟ وقتی حلقه for اجرا میشود، مقدار i اول صفر است و یک تابع در آرایه push میکند. اما در این مرحله کد console.log(i); اجرا نمیشود. در مورد function expression صحبت کردیم. آنچه در اینجا اتفاق می افتد این است که فقط یک شیء جدید ایجاد میشود و کد داخل آن یعنی console.log(i); در ویژگی CODE آن قرار میگیرد ولی این کد اجرا نمیشود. سپس مقدار i برابر با یک میشود و ... تا اینکه مقدار i برابر با 3 شده و از حلقه خارج میشود. بنابراین وقتی به خط کد return arr; میرسیم، در حافظه execution context مقدار i برابر با 3 است و داخل arr مان سه تابع anonymous داریم. سپس به global execution context برمیگردیم و buidFuncions execution context از استک pop میشود ولی حافظه آن در دسترس باقی میماند. پس زمانی که به خط کد fs[0](); میرسیم، کد داخل CODE که console.log(i); است اجرا میشود و execution context آن ایجاد میشود و چون هیچ متغیر i ای داخل آن نیست، در scope chain پایین میرود. این تابع داخل تابع buildFuncions ایجاد شده است. پس به حافظه ای که قبلا در buildFunctions execution context بوده اشاره میکند. مقدار i در آن برابر با 3 است. پس 3 را چاپ میکند. اجرا تمام میشود و execution context مربوط به آن pop میشود و سراغ خط کد بعدی یعنی fs[1](); میرود و execution context جدید برای آن ایجاد میشود. این تابع هم outer environment reference مشابه قبلی دارد چون در همان جای تابع قبلی ایجاد شده است. پس در این حالت هم مقدار i برابر با 3 میشود. برای خط کد fs[2](); هم به همین ترتیب است. هر سه این تابع ها در تابع buildFunction ایجاد شده اند، یعنی پدر آنها یکسان است. وقتی از سه فرزند سن پدرشان را بپرسیم، سن پدر را وقتی که به دنیا آمده اند نمیگویند، سن الان او را میگویند. اینجا هم سه تابع داریم که بعدا اجرا میشوند. مقدار الان متغیر را به ما میدهند، نه مقداری که موقع ایجاد شدن داشته. بنابراین خروجی برای هر سه تابع یکسان است. پس باید توجه داشته باشیم که خط کد console.log(i); همان جا که تایپ شده اجرا نمیشود، وقتی تابع invoke میشود اجرا میشود.دیدیم که وقتی fs[0](); را اجرا میکنیم هنوز به outer variable ها دسترسی دارد. این متغیرها free variable نام دارند یعنی متغیری که خارج از تابع است ولی به آن دسترسی داریم. حال اگر نخواهیم کدی که نوشته ایم به شکلی که توضیح داده شد کار کند چه کنیم؟ یعنی میخواهیم که خروجی 0 و 1 و 2 باشد. برای این کار چند روش وجود دارد. در نسخه جدید جاوااسکریپت یعنی es6 متغیر let را داریم:function buildFunctions2() {var arr = [ ];for (var i=0; i<3; i++) {let j = i;arr.push( function() {console.log(j);} );}return arr;}var fs2 = buildFunctions2();fs2[0]();fs2[1]();fs2[2]();متغیر let در بلاک scope مربوط به for میشود. بنابراین هر موقع که حلقه for اجرا میشود، j یک متغیر جدید در حافظه میشود و وقتی تابع function() {console.log(j);} فراخوانی میشود، هر دفعه به یک جای متفاوت داخل آن حافظه اشاره میکند. در واقع subsegment است. پس با functionality جدید js میتوانیم کد را به صورت بالا بنویسیم. اما اگر بخواهیم با es5 این کار را انجام دهیم چه کنیم؟ برای preserve کردن مقدار متغیر i برای تابع function() {console.log(j);}، به یک execution context مجزا برای هر تابعی که در آرایه push میکنیم نیاز داریم. یعنی به یک parent scope نیاز داریم که مقدار فعلی i را در حلقه for نگه دارد. تنها راه برای داشتن یک execution context این است که تابع را اجرا کنیم. پس نیاز هست تابع function() {console.log(j);} را on the fly اجرا کنیم: IIFE:function buildFunctions() {var arr = [ ];for (var i=0; i<3; i++) {arr.push( (function(j){ return function(){console.log(j);} }(i)); );}return arr;}var fs = buildFunctions();fs[0]();fs[1]();fs[2]();در کد بالا هر دفعه که حلقه for اجرا میشود تابع function(j){ return function(){console.log(j);} } اجرا میشود. اگر در حلقه مقدار i برابر با صفر باشد، صفر به تابع پاس داده میشود و هر بار که در حلقه این تابع اجرا میشود، execution context مجزا دارد و j در هر سه execution context ذخیره میشود. پس در یکی مقدار j برابر با 0 و در یکی برابر با 1 و در یکی برابر با 2 است. حتی زمانی که این execution context ها از بین میروند، به خاطر closure مقدار j در هر کدام از آنها قابل دسترس است.چرا در کد بالا به جای function(j){ return function(){console.log(j);} } فقط ننوشتیم function(j){ console.log(j); }؟ چون push میخواهد نتیجه تابع را در آرایه push کند. اجرای تابعی که نوشته ایم، یک تابع برمیگرداند. اگر فقط console.log(j); را مینوشتیم خروجی تابع برگردانده نمیشد. وقتی قسمت کد function(){console.log(j);} اجرا میشود و دنبال مقدار j میگردد نیازی نیست تا حلقه for بیرون رود، فقط به execution context مربوط به تابع function(j){ return function(){console.log(j);} } میرود. مقدار j همان موقع که در حلقه اجرا میشود ذخیره میشود. این مثالی از استفاده از فواید closure بود. این مسائل به خاطر ویژگی ها first class function و closure ها هستند.