scope و closure دو مفهوم مهم و کاربردی در جاوااسکریپت است که به موضوع دسترسی به متغیرها، توابع و اشیا براساس موقعیتی که در برنامه دارند اشاره میکنند.
ممکن است هنگام کد نویسی به زبان جاوااسکریپت، به کدی مشابه کد زیر برخورد کرده باشید یا خودتان آن را نوشته باشید:
function sayWord(word) { return () => console.log(word); } const sayHello = sayWord("hello"); sayHello(); // "hello"
دو نکته جالب در این قطعه کد وجود دارد. اول این که ما میتوانیم به word، که توسط تابع sayWord بازگردانده میشود دسترسی داشته باشیم. دوم، زمانی که تابع sayHello را فراخوانی میکنیم، به مقدار word دسترسی داریم – یعنی حتی اگر به word دسترسی مستقیم نداشته باشیم، تابع sayHello را فراخوانی میکنیم. در این مقاله قصد داریم تا بطور کامل در مورد scope و closure، که باعث این رفتار میشوند صحبت میکنیم.
scope اولین بخشی است که در درک مثال قبلی به ما کمک میکند. scope متغیر محدودهای از برنامه است که در آن، متغیر در دسترس است و قابل استفاده میباشد.
متغیرهای جاوااسکریپت دارای lexically scoped هستند، به این معنی که میتوانیم scope متغیر را از جایی که در سورس کد اعلام شده است تعیین کنیم. (البته این موضوع کاملا درست نیست، متغیرهای var این چنین نیستند. در ادامه در مورد آن صحبت خواهیم کرد)
مثال زیر را در نظر بگیرید:
if (true) { const foo = "foo"; console.log(foo); // "foo" }
دستور if با استفاده از آکولاد، یک محدودهی بلاک کد(block-scope) را تعریف میکند. بر این اساس میگوییم foo برای دستور if دارای block-scope است. به این معنی که، foo فقط از داخل آن بلاک کد قابل دسترسی میباشد.
اگر سعی کنیم خارج از بلاک کد به foo دسترسی پیدا کنیم، یک ReferenceError دریافت خواهیم کرد. زیرا در این حالت foo خارج از محدوده میباشد:
if (true) { const foo = "foo"; console.log(foo); // "foo" } console.log(foo); // Uncaught ReferenceError: foo is not defined
عبارات دیگری که دارای بلاک کد هستند، مانند حلقههای for و while نیز یک محدوده برای متغیرهای محدوده بلاک کد ایجاد میکنند. به عنوان مثال، foo در محدودهی بدنه تابع زیر قرار گرفته است:
function sayFoo() { const foo = "foo"; console.log(foo); } sayFoo(); // "foo" console.log(foo); // Uncaught ReferenceError: foo is not defined
جاوااسکریپت از بلاکهای کد تودرتو و در نتیجه scopeهای تودرتو پشتیبانی میکند. scopeهای تودرتو یک درخت scope یا زنجیره scope ایجاد میکنند.
کد زیر را در نظر بگیرید که چندین دستور با بلاک کد را در خود جای داده است:
if (true) { const foo = "foo"; console.log(foo); // "foo" if (true) { const bar = "bar"; console.log(foo); // "foo" if (true) { console.log(foo, bar); // "foo bar" } } }
همچنین جاوااسکریپت به ما این امکان را میدهد که از توابع به شکل تودرتو استفاده کنیم:
function foo(bar) { function baz() { console.log(bar); } baz(); } foo("bar"); // "bar"
همانطور که انتظار میرود، دسترسی به متغیرها از scope مستقیم آنها امکانپذیر میباشد(یعنی محدودهای که متغیرها در آن تعریف میشوند). همچنین میتوانیم از scope داخلی متغیرها نیز به آنها دسترسی داشته باشیم(یعنی محدودههایی که در scope مستقیم متغیرها قرار دارند). به طور کلی نتیجه میگیریم که، ما میتوانیم از محدودهای که متغیرها در آن اعلام میشوند و از هر محدوده داخلیتر، به آنها دسترسی داشته باشیم.
قبل از این که موضوع را ادامه دهیم، باید تفاوت این رفتار را بین انواع متغیرها بررسی کنیم.
ما میتوانیم با استفاده از عبارات let، const و var متغیرها را تعریف کنیم. محدوده بلاک کد برای let و const، همانطور که در بالا توضیح داده شد کار میکند. با این حال، var رفتار متفاوتی دارد.
let و const متغیرهایی با محدوده بلاک کد ایجاد میکنند. هنگامی که متغیر در یک بلاک کد تعریف میشود، فقط در آن بلاک قابل دسترسی است. این رفتار را در مثالهای قبلی نیز نشان دادهایم:
if (true) { const foo = "foo"; console.log(foo); // "foo" } console.log(foo); // Uncaught ReferenceError: foo is not defined
متغیرهای تعریف شده با var به نزدیکترین تابع یا scope سراسری(global scope)، که در ادامه در مورد آن صحبت خواهیم کرد محدود میشوند. آنها محدوده بلاک کد ندارند:
function foo() { if (true) { var foo = "foo"; } console.log(foo); } foo(); // "foo"
البته باید به این نکته اشاره کنیم که، var میتواند موقعیتهای گیجکننده ایجاد کند و این اطلاعات برای کامل بودن مقاله ذکر شده است. در صورت امکان، بهتر است برای تعریف متغیر از let و const استفاده شود. ادامه مطالب این مقاله فقط به متغیرهای let و const مربوط میشود.
اگر به نحوه رفتار var در مثال بالا علاقهمند هستید، این مقاله را بررسی کنید.
علاوه بر محدودههای بلاک کد، محدوده متغیرها را میتوان به حالت سراسری و ماژول نیز تغییر داد.
در یک مرورگر وب، scope سراسری یک سطح بالا از اسکریپت است. این ریشه درخت scope است که قبلا توضیح دادیم، و شامل تمام scopeهای دیگر است. بنابراین تعریف یک متغیر در scope سراسری، باعث میشود تا آن در هر محدودهای قابل دسترسی باشد:
<script> const foo = "foo"; </script> <script> console.log(foo); // "foo" function bar() { if (true) { console.log(foo); } } bar(); // "foo" </script>
هر ماژول نیز محدوده خاص خود را دارد. متغیرهای اعلام شده در سطح ماژول فقط در آن ماژول در دسترس هستند – به این معنی که آنها سراسری نیستند:
<script type="module"> const foo = "foo"; </script> <script> console.log(foo); // Uncaught ReferenceError: foo is not defined </script>
اکنون که مفهوم scope را درک کردیم، به مثالی که در مقدمه دیدیم برمیگردیم:
function sayWord(word) { return () => console.log(word); } const sayHello = sayWord("hello"); sayHello(); // "hello"
اگر به یاد داشته باشید، دو نکته جالب در مورد این مثال وجود داشت:
اولین نکته را میتوان با lexical scope توضیح داد، تابع بازگشتی میتواند به word دسترسی داشته باشد زیرا، در محدوده بیرونی آن قرار دارد.
نکته دوم به دلیل closureها است، closure تابعی است که با ارجاع به متغیرهای تعریف شده خارج از آن، ترکیب شده است.
closureها مراجع متغیرها را حفظ میکنند، این کار به توابع اجازه میدهد به متغیرهای خارج از محدوده خود دسترسی پیدا کنند. در واقع آنها تابع و متغیرها را در محیط آن “محصور” میکنند.
احتمالاً شما بدون این که از این موضوع اطلاع داشته باشید اغلب با closure مواجه شدهاید و از آن استفاده کردهاید. در ادامه چند روش دیگر برای استفاده از closure را بررسی میکنیم.
معمولاً یک تابع callback به متغیری که خارج از خودش تعریف شده ارجاع میدهد. به عنوان مثال:
function getCarsByMake(make) { return cars.filter(x => x.make === make); }
make به دلیل lexical scope در callback موجود است و زمانی که یک تابع ناشناس به دلیل closure توسط فیلتر فراخوانی می شود، مقدار make باقی میماند.
ما میتوانیم از closure برای بازگرداندن آبجکتها از توابعی که حالت را ذخیره میکنند، استفاده کنیم. تابع makePerson زیر را در نظر بگیرید که آبجکتی که میتواند یک نام را ذخیره کند و تغییر دهد را برمیگرداند:
function makePerson(name) { let _name = name; return { setName: (newName) => (_name = newName), getName: () => _name, }; } const me = makePerson("Zach"); console.log(me.getName()); // "Zach" me.setName("Zach Snoek"); console.log(me.getName()); // "Zach Snoek"
این مثال نشان میدهد که closure ها فقط مقادیر متغیرها از scope بیرونی یک تابع را در طول ساخته شدن آنها حفظ نمیکنند. بلکه آنها دسترسی به مراجع را در طول عمر closureها نگه میدارند.
اگر با برنامه نویسی شیگرا آشنا هستید ممکن است متوجه این موضوع شوید که مثال قبلی شباهت زیادی به کلاسی دارد که حالت private را ذخیره میکند و متدهای setter و getter عمومی را نمایش میدهد. ما میتوانیم این شیگرایی را موازی با استفاده از closureها برای پیادهسازی روشهای private گسترش دهیم:
function makePerson(name) { let _name = name; function privateSetName(newName) { _name = newName; } return { setName: (newName) => privateSetName(newName), getName: () => _name, }; }
privateSetName مستقیماً در دسترس مصرفکنندگان نیست ولی میتواند از طریق closure، به متغیر حالت خصوصی name_ دسترسی داشته باشد.
در نهایت، closureها در React event handlerها رایج هستند. کامپوننت Counter زیر از مستندات React مورد بررسی قرار گرفته است و تغییرات روی آن اعمال شده است:
function Counter({ initialCount }) { const [count, setCount] = React.useState(initialCount); return ( <> <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount((prevCount) => prevCount - 1)}> - </button> <button onClick={() => setCount((prevCount) => prevCount + 1)}> + </button> <button onClick={() => alert(count)}>Show count</button> </> ); } function App() { return <Counter initialCount={0} />;
Closureها در این مثال امکانات زیر را فراهم میکنند:
closureها در سایر بخشهای React، مانند propها و هوکها نیز دارای اهمیت زیادی هستند.
scope به بخشی از برنامه اشاره دارد که میتوانیم در آن به یک متغیر دسترسی داشته باشیم. جاوااسکریپت به ما این اجازه را میدهد تا از scopeها به شکل تودرتو استفاده کنیم و متغیرهای تعریف شده در scopeهای بیرونی توسط همه متغیرهای داخلی قابل دسترسی باشند. متغیرها میتوانند به صورت سراسری، ماژول یا محدوده بلاک کد تعریف شوند.
closure تابعی است که بین ارجاعاتی به متغیرهای موجود در scope بیرونی آن قرار دارد. closureها به توابع اجازه میدهند تا ارتباطات خود را با متغیرهای بیرونی، حتی خارج از scope متغیرها نیز حفظ کنند.
closureها کاربردهای زیادی دارد، از ایجاد ساختارهای کلاس مانند که متدهای private را ذخیره و اجرا میکنند تا ارسال callback به event handlerها.
دیدگاهها: