scope
اسکوپ (محدوده) چرا اینقدر مهمه؟
در ابتدا دو تا قسمت اصلی از جاوااسکریپت رو معرفی میکنم تا با چگونگی کارکرد جاوا اسکریپت اشنا بشیم.
Engine: مسئول شروع و پایان کامپایل برنامه جاوااسکریپت ما و اجرای اون هست
Compiler: کارش انجام دادن parsing و code-generation (این دوتا کلا بحثش زیاده شاید بعدا گفتم)
حالا بریم سروقت اسکوپ.
Scope:
اسکوپ میاد هرچی متغیر رو ما تعریف کردیم جمع و نگهداری میکنه،وقتی که engine مقداری رو میخواد ،جستجو میکنه و بر حسب موقعیت و مجوز و … مقدار میده به engine
به صورت ساده تر یعنی توانایی دسترسی به تابعها و متغییرها و ابجکتها در بعضی از قسمت های کدهامون، که میتونه داخل بلاک باشه یا تابعها و … و اینکه توانایی دیدن این متغیرها رو در چه ناحیههایی از کدهامون داریم.
در اصل دو نوع اسکوپ داریم یکی local و دیگری global.اگر متغیری درون global تعریف کنیم در اصل تا وقتی که کد ما run هست این متغیر هم در دسترس هست. اما local scope چون داخل تابعها و بلاکها وجود داره تا وقتی در دسترس هست که تابع یا بلاک اجرا یا در دسترس باشه.
نمونه local , global اسکوپ رو میتونید ببینید.
var globalScope = 5;
function globalFunctionScope(){
var localScope = 10;
}
lexical scope:
یکی از پیچیده ترین بحثها هست.توضیحش یه مقدار سخته.امیدوارم بتونم توضیحش رو بدم.
یکی از مدلهای چگونگی کارکرد scope در جاوااسکریپت است.(روش دیگر dynamic scope مثل(this) هست) و البته باید بگم که lexical scope در اکثر زبانهای برنامه نویسی وجود داره.
قبل از همه اینها باید تعریفی از فاز lexing در کامپایلر داشته باشیم.lexing در اصل کدهامون رو به صورت استرینگ و جدا از هم تبدیل میکنه و بعد به صورت token نگهداری میشه.
مثل کد زیر .
var a=2;
که به این صورت میشه
var
, a
,=
,2
و بعد به صورت token ذخیره میشن.
در این مدل هنگامی که شما در حال نوشتن تابعها و یا بلاکها هستید اسکوپها را نیز تعریف میکنید. در هنگام lexing در کامپایل کردن ،scope میدونه کجا و چطوری تابعها و ابجکتها و متغیرها تعریف شده اند پس میدونه جایگاهشون و دسترسیشون رو.در هنگامی که engine نیاز داره به متغیری، اسکوپ طبق دسترسی که متغیر داره به engine مقادیرشون رو میده.
بزارید با یک مثال ساده تر پیش بریم.
اینجا ما دو تا تابع داریم که به صورت تودرتو هستند.داخل هر کدوم هم یک متغیر تعریف کردیم . همیشه تابع های داخلی به متغیرهای تعریف شده تابع های خارجی دسترسی دارند . اما اما تابع های که خارجی هستند به متغیرهای تابع های داخلی خودشون دسترسی ندارن.
function one(){
var first =1;
// second قابل دسترسی در اینجا نیست
function two(){
var second =2;
//first قابل دسترسی هست
}
}
closure
کلوژر یا closure ،در واقع یک تابع باعث میشه که به متغیرها و ورودیهای تابع (argument) توابع خارجی خودش ارتباط داشته باشیم.یه جوری lexical scope رو دور میزنیم. و دسترسی پیدا میکنیم به متغیرها و مقادیر ورودی تابع های بیرونی .
مثال زیر یک نمونه کوچیک از closure هست .
function siteName(hello){
var name = "kill the js";
return function returnName(a){
console.log(hello + name+a);
}
}
var getName = siteName("hello ");
getName("!"); // hello kill the JS
توی مثال بالا میبینیم که تابع returnName به متغیرها ،ورودیهای siteName دسترسی داره.
هنگامی که تابع siteName رو فراخوانی میکنیم در اصل یک تابع رو برمیگردونه که به تمام متغیرهای والدش(siteName) و ورودیهاش و متغیرهای گلوبال دسترسی داره. و میتونیم تابع return شده رو درون متغیر جدیدی از نوع تابع ذخیره کنیم که باز بتونیم صداش بزنیم و ورودی بهش بدیم.(تمام سعیمو کردم که ساده توضیح بدم ヽ(•‿•)ノ )
بعضی وقتها نیازی نیست که حتما خروجی تابع رو ذخیره کنیم درون یک تابع دیگه و بعد فراخوانی کنیم، میتونیم به صورت مستقیم از صدا زدن تابع والد به همراه یک ()
اضافهتر استفاده کرد.
مثال زیر بهتر منظورم رو میرسونه
function x(a){
return function y(b){
return a+b;
}
}
console.log(x(2)(3)); // 5
همه این توضیحات برای این بود که بتونم این کد زیر رو توضیح بدم.
for (var i=1; i<4; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
// output is 4,4,4
اما خب چرا خروجی چیزی هست که اصلا انتظارش رو نداریم ؟وقتی داشتم برای یک قسمت از سایت انیمیشن میساختم دقیقا به این مشکل برخوردم.بعد کلی جستجو و تحقیق نتایج کار اینا شد.
دلیلش اینه که تمام callbackها یا همون تابع داخل setTimeout بعد از اینکه حلقه for تموم میشه اجرا میشن. حلقه هم وقتی تموم میشه که i=۴ باشه .پس ۴ رو میده به تابع setTimeout یا تابع بیرونی setTimeout که گلوبال هست
اینجا i=4 درون global scope قراره داره پس همه جا در دسترس هست. یک نکته در نظر بگیرید که ما برای تعریف متغیر i از var استفاده کردیم و var یک functional scope هست.یعنی درون هر تابع یک اسکوپ ساخته میشه اما حلقه for از این قضیه پیروی نمیکنه.
به این خاطر که در هر تکرار داخل حلقه یک تابع درون setTimeout داریم که ساخته میشه پس در اینجا ۳ تا تابع به صورت جداگانه ساخته میشه که درون یک global scope قرار دارند حالا این global scope شامل i=4 هست به خاطر این هست که به هر سه تا تابع i=4 داده میشه. چیزی که کم داریم یک scope closure هست.
برای اینکه این اتفاق رخ نده باید یک scope داخل حلقه بسازیم و متغیر رو داخل هر تکرار ذخیره کنیم.میتونیم برای ساختن اسکوپ از تابع خود ران یا IIFE (میتونید ایفی هم بخونید) استفاده کنیم .
for (var i=1; i<4; i++) {
(function(){ // create scope with IIFE
setTimeout( function timer(){
console.log( i );
}, i*1000 );
})();
}
اما این هنوز کافی نیست اگه ران کنید این کد رو متوجه میشید باز مشکل داره. چه چیزی کم داریم اینجا ؟ اسکوپ رو که در هر تکرار ساختیم، الان تنها چیزی که نیاز داریم اینه که در هر تکرار متغیر خودمون رو بسازیم و ذخیره کنیم متغیر حلقه رو درونش.
for (var i=1; i<4; i++) {
(function(){ // create scope with IIFE
var j=i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}
خب الان درست شد. راه حل دیگ اینکه از تابع خودران (IIFE) استفاده کنیم برای اعمال متغیر خودمون بدون ساختن متغیر جدید:
for (var i=1; i<4; i++) {
(function(j){ // create scope with IIFE
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})(i);
}
دو تا روش دیگه هست برای رفع این مشکل . بزارید یکم درباره let , const صحبت کنم .
let یک block scope هست
تفاوت let,var توی دسترسی به متغیر هست. let علاوه بر functional scope شامل block statement هم میشه . یعنی داخل هر {}
متغیر تعریف بشه داخل اون و زیر مجموعه هاش قابل دسترس هست. و در اسکوپهای بیرونی قابل دسترس نیست.
const ,let مثل هم عمل میکنن با این تفاوت که const رو نمیشه تغییر داد. البته میشه اما شرایط داره و توضیحش مفصله
خب با این توضیح میشه یک راه حل دیگه هم داد به جای اینکه از تابع IIFE استفاده کنیم و بعد داخلش متغیر خودمون رو بسازیم .از let برای تعریف متغیرمون استفاده میکنیم که همین طور باعث میشه یک scope هم ساخته بشه
for (var i=1; i<4; i++) {
let j=i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}
و یا در اخر میشه از خود حلقه استفاده کرد . و به جای تعریف متغیر شمارنده بوسیله var، از let استفاده کرد که هر تکرار یک اسکوپ با متغیر خودمون میسازیم.
for (let i=1; i<4; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
امیدوارم مفید بوده باشه.
خیلی مخلصیم.