هنگام یادگیری برنامه نویسی سوالی که ممکن است با آن مواجه شویم این است که آیا تا به حال به این موضوع فکر کردهایم که برنامهها چگونه کارهایی که باید انجام دهند را به خاطر میآورند و آنها را بارها و بارها تکرار میکنند؟ اینجاست که مفهوم توابع و scope در جاوااسکریپت مطرح میشوند.
توابع این امکان را به ما میدهند تا خطوط کد را باهم گروهبندی کنیم و یک نام به آنها اختصاص بدهیم. این توابع مانند ابزارهای خاصی هستند که به ما کمک میکنند کد خود را سازماندهی کرده و هر زمان که به آن نیاز داشتیم اقدامات خاصی را انجام دهیم.
به جای نوشتن یک کد یکسان، میتوانیم از توابع برای آسانتر کردن کدنویسی استفاده کنیم. میتوانیم توابع را به عنوان برنامههای کوچکی در نظر بگیریم که برای سازماندهی و کارآمدتر کردن کد خود از آنها بصورت مجدا استفاده میکنیم.
Scope مفهوم دیگری است که بر نحوه عملکرد کد ما تأثیر میگذارد. این مفهوم مانند مجموعهای از قوانین است که تعیین میکند متغیرهای ما در کدام قسمتها مجاز هستند تا در دسترس باشند. گاهی اوقات آنها آزاد هستند تا در هر قسمتی از کد مورد استفاده قرار بگیرند، و گاهی اوقات مجاز هستند فقط در محدوده خاصی در دسترس باشند.
تعریف یک تابع مانند تعریف نام آن و هدف دادن به آن میباشد، اینجا قسمتی است که ما کدی را که میخواهیم تابع اجرا کند را قرار میدهیم. به عنوان مثال:
// This code is a function function greet(name) { console.log(`Hello, ${name}!`); } greet("Cas"); // Output: Hello, Cas!
در مثال بالا، تابعی به نام greet
یک پارامتر name
میگیرد و یک پیام تبریک را با استفاده از یک template literal ثبت میکند. سپس تابع greet
را با آرگومان Cas فراخوانی میکنیم و Hello, Cas!
را در خروجی میبینیم.
توابع را میتوانیم به عنوان ماشینهایی تصور کنیم که ورودیها (پارامترها) را میگیرند و خروجی را تولید میکنند. پارامترها مانند placeholderهایی برای این ورودیها میباشند. اما آرگومانها مقادیر واقعی هستند که به تابع میدهیم. مثلا:
function addNumbers(a, b) { //a, b are parameters return a + b; } const result = addNumbers(5, 7); //5,7 are arguments console.log(result); // Output: 12
اگر بخواهیم این مقادیر را با دانیای واقعی مقایسه کنیم، فرض کنید دوست خود را به یک جستجو میفرستیم. او بیرون رفته، کار را کامل میکند و با یک کالای ارزشمند برمیگردد. در دنیای توابع، این «آیتم» همان چیز است که ما آن را مقدار بازگشتی مینامیم. به عنوان مثال:
function multiply(a, b) { const result = a * b; return result; // The function gives back the 'result' as a gift } const product = multiply(3, 5); // The function is called, and the return value is captured console.log(product); // Output: 15
در مثال بالا، تابع multiply
محاسبات خود را انجام میدهد، پاسخ را محاسبه میکند (حاصل ضرب ۳ و ۵)، و آن را با استفاده از دستور return
تحویل میدهد.
گاهی اوقات ما به یک تابع با نام نیاز نداریم. یک تابع anonymous نامی ندارد، در عوض، مستقیماً در جایی که به آن اختصاص داده شده است، تعریف میشود. توابع anonymous اغلب به عنوان توابع callback یا توابع یکبار مصرف مورد استفاده قرار میگیرند. به عنوان مثال:
const multiply = function(x, y) { return x * y; }
این کد یک تابع anonymous اختصاص داده شده به متغیر multiply
را تعریف میکند که دو پارامتر x
و y
را میگیرد و هنگام فراخوانی تابع حاصل ضرب آنها را return میکند.
این مفهوم هنگام تخصیص توابع به متغیرها، ارسال توابع به عنوان آرگومان به توابع دیگر، یا return کردن توابع از توابع دیگر به کار میرود. به عبارت دیگر جایگزین برای روش رایج تعریف تابع میباشد. مثلا:
const add = function(a, b) { return a + b; }; const result = add(5, 3); // Call the function console.log(result); // Output: 8
در این مثال، یک عبارت تابع به نام add
تعریف شده و به متغیر add
اختصاص داده شده است. تابع دو پارامتر a
و b
را میگیرد و مجموع این دو عدد را return میکند.
این تابع در مورد کلمه کلیدی this
رفتار متفاوتی دارد. برخلاف توابع معمولی، arrow functionها context this
را برای خود ایجاد نمیکنند. در عوض، آنها مقدار this
را از کد اطراف خود ارثبری میکنند. به عنوان مثال:
function regularFunction() { console.log(this); // Refers to the caller } const arrowFunction = () => { console.log(this); // Inherits from where it's defined }; const obj = { regular: regularFunction, arrow: arrowFunction }; obj.regular(); // 'this' refers to 'obj' obj.arrow(); // 'this' still refers to 'obj', despite being in an arrow function
این کد تفاوت بین توابع معمولی و arrow functionها را در مورد استفاده از کلمه کلیدی this
نشان میدهد. arrow functionها context this
را از جایی که تعریف شدهاند ارثبری میکنند، در حالی که توابع معمولی به caller اشاره دارند.
یکی دیگر از مزایای arrow functionها این است که آنها ظرافت مختصری را به جاوااسکریپت میدهند. به این ترتیب هنگامی که با مقادیر پارامترهای پیشفرض ترکیب میشوند، کد ما را سادهتر میکنند. به عنوان مثال:
const greet = (name = "friend") => { console.log(`Hello, ${name}!`); }; greet(); // Output: Hello, friend! greet("Cas"); // Output: Hello, Cas!
در مثال بالا، پارامترname
دارای مقدار پیش فرض friend است. arrow functionها مخصوصاً زمانی مفید هستند که بخواهیم راهی سریع برای تعریف یک تابع با پارامترهای پیشفرض داشته باشیم.
Hoisting مانند آماده کردن صحنه قبل از شروع نمایش است. در جاوااسکریپت، تعریف توابع به بالای scope حاوی خود hoist میشود. این بدان معناست که ما میتوانیم یک تابع را قبل از اینکه در کد خود تعریف کنیم فراخوانی نماییم. مثلا:
// Function declaration (can be called anywhere) sayHello(); // This code works function sayHello() { console.log("Hello!"); }
قطعه کد بالا باتوجه به مفهوم hoisting کار میکند. با این حال، hoisting بر روی عبارات تابع اعمال نمیشود:
// Function expreesion (called before defined) sayHi(); // Error const sayHi = function() { console.log("Hi!"); }; // Function expression (should be defined before calling) const sayHello = function() { console.log("Hello!"); }; sayHello(); // This works
تابع sayHi
یک خطا ایجاد می کند. چون قبل از این که تعریف شود، فراخوانی شده است. این بدان معنی است که ما باید یک عبارت تابع را قبل از فراخوانی آن تعریف کنیم.
hoisting با کلمات کلیدی let
و const
رفتار کمی متفاوت دارد. آنها یک dead zone موقتی را تجربه میکنند، درست مانند بازیگرانی که در پشت صحنه منتظر نوبت خود هستند. dead zone در جاوااسکریپت به بازه زمانی بین ایجاد یک متغیر با استفاده از کلمات کلیدی let
یا const
و نقطهای که متغیر مقداردهی میشود، اشاره دارد.
در این مدت، اگر سعی کنیم به متغیر دسترسی داشته باشید، با خطای reference مواجه میشویم. این رفتار نتیجه نحوه عملکرد variable hoisting جاوااسکریپت با این تعریفهای block-scoped میباشد. در ادامه یک مثال دیگر داریم:
console.log(myName); // Throws an error - myName is not defined let myName = "Cas";
در کد بالا، myName
hoist شده است، اما تلاش برای دسترسی به آن قبل از تعریف واقعی، به دلیل dead zone با خطا مواجه میشود.
باید به این نکته توجه داشته باشیم در حالی که function hoisting میتواند مفید باشد، تمرین خوبی است که قبل از استفاده از توابع، آنها را تعریف کنیم تا کدی که داریم خواناتر شود.
ممکن است مواقعی پیش آمده باشد که بخواهیم یک تابع را بلافاصله پس از تعریف آن اجرا کنیم. اینجاست که IIFEها وارد عمل میشوند. آنها مانند express lane جاوااسکریپت عمل میکنند.
تنها کاری که باید انجام دهیم این است که تابع را تعریف کنیم، آن را داخل پرانتز قرار دهیم و سپس یک جفت پرانتز دیگر اضافه کنیم تا بلافاصله آن را فراخوانی نماییم. میتوانیم IIFE خود را با اضافه کردن یک پارامتر، شخصیسازی کنیم. به عنوان مثال:
(function(name) { console.log(`Hello, ${name}!`); })("Cas");
در این مثال، IIFE نام Cas را به عنوان پارامتر انتخاب کرده و بلافاصله آن را اجرا میکند.
در دنیای توابع جاوااسکریپت، انعطافپذیری بسیار مهم است. گاهی اوقات، میخواهیم تابعی که داریم مقادیر missing یا undefined را بدون ایجاد خطا مدیریت کند. اینجاست که مقادیر پارامترهای پیشفرض به کمک ما میآیند. به عنوان مثال:
function greet(name = "Guest") { console.log(`Hello, ${name}!`); } greet(); // Output: Hello, Guest! greet("Cas"); // Output: Hello, Cas!
در تابع greet
، پارامتر name
دارای مقدار پیشفرض Guest است. اگر تابع را بدون ارائه آرگومان برای name
فراخوانی کنیم، از مقدار پیشفرض استفاده میکند. اما اگر آرگومان ارائه کنیم، استفاده از مقدار پیشفرض را لغو میکند.
Rest Parameter و Spread Operator دو مفهوم مرتبط در جاوااسکریپت هستند که با مدیریت و دستکاری آرگومانها و آرایههای تابع سروکار دارند.
اگر بخواهیم این مفاهیم را با دنیای واقعی مقایسه کنیم، تصور کنید در حال برگزاری یک پیکنیک هستیم و میخواهیم تمام غذاهایی را که سایر دوستانمان همراه خود میآورند، جمعآوری کنیم. rest parameter مانند یک جمع کننده ظروف است که تمام اقلامی را که دوستان ما آوردهاند را میگیرد و آنها را در آرایهای قرار میدهد تا از آن استفاده کنیم. مثلا:
function partyPlanner(mainDish, ...sideDishes) { console.log(`Main dish: ${mainDish}`); console.log(`Side dishes: ${sideDishes.join(', ')}`); } partyPlanner( "Jollof rice", "Fufu", "Pizza", "Salad", "Kpomo", "Fries"); // Output: // Main dish: Jollof rice // Side dishes: Fufu, Pizza, Salad, Kpomo, Fries
در این مثال، پارامتر ...sideDishes
تمام مقادیر اضافی را جمعآوری کرده و آنها را در یک آرایه بستهبندی میکند و کار با تعداد ورودیهای مختلف را آسان میکند.
فرض کنید یک جعبه هدیه با آیتمهای مختلف دریافت کردهایم و میخواهیم آنها را از بستهبندی خارج کرده و بلافاصله آیتمهای مورد نیاز خود را انتخاب کنیم.
Destructuring به ما کمک میکند تا آیتمهایی را که از دادههای پیچیده نیاز داریم، مانند آبجکتها یا آرایهها، باز کرده و از آنها استفاده کنیم. به مثال زیر توجه کنید:
function printPersonInfo({ firstName, lastName, age }) { console.log(`First Name: ${firstName}`); console.log(`Last Name: ${lastName}`); console.log(`Age: ${age}`); } const person = { firstName: 'Cas', lastName: 'Nuel', age: 30 }; printPersonInfo(person); // Output: // First Name: Cas // Last Name: Nuel // Age: 30
در این مثال، تابع printPersonInfo
یک پارامتر آبجکت میگیرد. به جای دسترسی به ویژگیهای آبجکت با استفاده از person.firstName
، person.lastName
، person.Age
از destructuring در لیست پارامترهای تابع برای استخراج مستقیم ویژگیها استفاده میکنیم. این کار باعث میشود تا کدی که داریم تمیزتر و خواناتر شود. وقتی printPersonInfo(person)
را فراخوانی میکنیم، تابع آبجکت person
را destruct میکند و ویژگیهای آن را چاپ مینماید.
تابع بازگشتی جایی است که یک تابع خود را فراخوانی میکند تا یک مسئله را با تجزیه آن به مشکلات فرعی کوچکتر و مشابه حل کند.
بازگشت شامل دو جزء اصلی است: یک شرط پایه که تعیین میکند بازگشت چه زمانی باید متوقف شود، و یک مورد بازگشتی که در آن تابع خود را با پارامترهای تغییر یافته، فراخوانی میکند. در ادامه یک مثال کد از یک تابع بازگشتی داریم که فاکتوریل یک عدد را محاسبه میکند:
function factorial(n) { // Base condition: factorial of 0 or 1 is 1 if (n === 0 || n === 1) { return 1; } // Recursive case: call the function with a smaller sub-problem return n * factorial(n - 1); } const num = 5; const result = factorial(num); console.log(`Factorial of ${num} is ${result}`);
در این مثال، تابع factorial
فاکتوریل یک عدد n
را محاسبه میکند. شرط پایه بررسی میکند که مقدار n
0 یا ۱ باشد. اگر اینطور باشد، تابع بلافاصله ۱ را return میکند، زیرا فاکتوریل ۰ یا ۱ برابر با ۱ است. حالت بازگشتی n
را با نتیجه فراخوانی تابع factorial
با n - 1
ضرب میکند.
این کار یک زنجیره از callهای بازگشتی ایجاد میکند، که هر کدام مشکل را یک مرحله کاهش میدهد و زمانی که به شرایط پایه برسید متوقف میشود. همچنین مقادیر محاسبه شده به زنجیره برگردانده میشوند.
به عنوان مثال، هنگام فراخوانی factorial(5)
:
factorial(5)
مقدار۵ * factorial(4)
را return میکند،
factorial(4)
مقدار۴ * factorial(3)
را return میکند
factorial(3)
مقدار۳ * factorial(2)
را return میکند
factorial(2)
مقدار۲ * factorial(1)
را return میکند
factorial(1)
مقدار ۱ را return میکند
سپس این مقادیر با هم ضرب میشوند و نتیجه نهایی که ۱۲۰ است به دست میآید.
بازگشت یک تکنیک قدرتمند است، اما برای جلوگیری از ایجاد حلقههای بینهایت، داشتن یک شرط پایه کاملاً ضروری است. هر فراخوانی بازگشتی باید به سمت حالت پایه حرکت کند و اطمینان حاصل کند که مشکل با هر تکرار، کوچکتر میشود.
با استفاده از scope و closure در جاوااسکریپت میتوانیم کد خود را سازماندهی کنیم، دادههای private ایجاد کنیم و توابع قدرتمندی بسازیم.
میتوانیم scope سراسری را به عنوان محلهای در نظر بگیریم که همه همسایههای ما (متغیرها) در آن زندگی میکنند. متغیرهای تعریف شده در اینجا از هر قسمت کد قابل دسترسی میباشند. به عنوان مثال:
const globalVariable = "I'm global!"; function globalScopeExample() { console.log(globalVariable); // Accessing the global variable } globalScopeExample(); // Output: I'm global!
این کد یک متغیر سراسری globalVariable
با مقدار string
تعریف میکند. سپس، یک تابع globalScopeExample
وجود دارد که مقدار globalVariable
را ثبت میکند. تابع فراخوانی میشود و در نتیجه مقدار متغیر سراسری به دست میآید.
از سوی دیگر، scope محلی مانند اتاقهایی در خانه ما است. متغیرهای تعریف شده در داخل توابع یا بلاکهای کد، محلی هستند و فقط در آن تابع یا بلاک کد قابل دسترسی میباشند. مثلا:
function localScopeExample() { const localVariable = "I'm local!"; console.log(localVariable); // Accessing the local variable } localScopeExample(); // Output: I'm local! // console.log(localVariable); // This would result in an error
این کد یک تابع localScopeExample
تعریف میکند که یک متغیر localVariable
در داخل تابع ایجاد کرده و سپس مقدار آن را چاپ مینماید. هنگامی که تابع فراخوانی میشود، مقدار localVariable
را در خروجی نشان میدهد. تلاش برای دسترسی به localVariable
خارج از تابع منجر به ایجاد خطا میشود.
Lexical scope کمی شبیه عروسکهای تودرتو روسی است. هر عروسک میتواند به عروسکهای داخل خود دسترسی داشته باشد، اما برعکس نه. به طور مشابه، در برنامه نویسی، این مفهوم به این معنی است که یک تابع درونی میتواند به متغیرها از تابع بیرونی خود دسترسی داشته باشد، اما برعکس نه. مثلا:
function outer() { const outerVar = "I'm from outer function!"; function inner() { console.log(outerVar); // Accessing the outer variable } inner(); } outer(); // Output: I'm from outer function!
در این مثال یک تابع خارجی outer
را تعریف میکنیم که حاوی متغیر outerVar
است. داخل تابع outer
یک تابع داخلی inner
وجود دارد که مقدار outerVar
را ثبت میکند. هنگامی که outer
فراخوانی میشود، inner
را نیز فراخوانی میکند و در نتیجه خروجی I'm from outer function!
نمایش داده میشود.
Closureها مانند کپسولهای زمانی هستند که حتی پس از اتمام کارکرد متغیرها، روی متغیرها میمانند. آنها ترکیبی از یک تابع و محیطی هستند که در آن ایجاد شده است. به عنوان مثال:
function rememberMe() { const secret = "I'm a secret!"; return function() { console.log(secret); // This inner function remembers the 'secret' }; } const myClosure = rememberMe(); myClosure(); // Output: I'm a secret!
کد بالا یک تابع memoryMe()
را تعریف میکند که تابع دیگری را ایجاد و return میکند. این تابع بازگشتی که به عنوان closure شناخته میشود، به متغیر secret
از scope تابع parent خود دسترسی دارد. هنگامی که تابع myClosure
فراخوانی می شود، مقدار متغیر secret
را ثبت میکند.
Closureها برای ایجاد دادهها یا توابع private که فقط بخش خاصی از کد ما میتواند به آنها دسترسی داشته باشد عالی هستند. به عنوان مثال:
function counter() { let count = 0; return function() { return ++count; }; } const increment = counter(); console.log(increment()); // Output: 1 console.log(increment()); // Output: 2
کد مثال بالا یک تابع counter
ایجاد میکند که هر بار که فراخوانی میشود یک شمارنده افزایشی ایجاد میکند و به این ترتیب استفاده از closure را نشان میدهد.
هر بار که یک تابع فراخوانی میشود، جاوااسکریپت یک context اجرایی ایجاد میکند، که یک نوع محیط برای اجرای آن تابع است. این context متغیرها، رفرنسها و جایی که تابع از آنجا فراخوانی شده است را پیگیری میکند.
میتوانیم context را به عنوان یک منطقه پشت صحنه که در آن کد تابع اجرا میشود، در نظر بگیریم. تمام متغیرها، توابع و پارامترها در اینجا ذخیره میشوند. مثلا:
function first() { console.log("Hello from first!"); second(); // Calling another function } function second() { console.log("Hello from second!"); } first(); // Output: Hello from first! Hello from second!
در مثال بالا، تابع اول تابع دوم را فراخوانی میکند و یک context اجرایی جدید برای تابع دوم ایجاد مینماید.
Call Stack مانند لیستی از وظایف است که در انتظار اجرا هستند. هنگامی که یک تابع فراخوانی میشود، به بالای پشته اضافه میشود. وقتی تمام شد، حذف میگردد.
این پشته از contextها چیزی است که کد ما را ردیابی میکند.
هنگامی که کدنویسی میکنیم، احتمالاً با مسائل پیچیدهای مواجه میشویم که میتواند باعث شود کد ما رفتار غیرمنتظرهای داشته باشد. در ادامه برخی از اشکالات و خطاهای رایج را باهم بررسی میکنیم.
مثال زیر را در نظر بگیرید:
function oops() { myVariable = "I'm global!"; // Oops, forgot 'var', 'let', or 'const'! } oops(); console.log(myVariable); // Output: I'm global!
در این مثال، myVariable
متغیر سراسری میشود زیرا برای تعریف آن از var
،let
یا const
استفاده نکردهایم.
مثال زیر را در نظر بگیرید:
const x = 10; function shadowExample() { const x = 5; // This 'x' is different from the outer 'x' console.log(x); // Output: 5 } shadowExample(); console.log(x); // Output: 10
در این مثال، متغیر x
داخلی باعث ایجاد shadow بر روی متغیر x
خارجی میشود و این موضوع باعث میشود تا مقدار متغیر x
در داخل و خارج از تایع مقادیر متفاوتی داشته باشد.
مرورگرهای مدرن مانند کروم مجهز به ابزارهای توسعه دهنده هستند که به ما این امکان را میدهند تا breakpointها را تعیین کنیم، متغیرها را بررسی کرده و کد خود را خط به خط مرور نماییم.
تنظیم breakpointها شامل استفاده از ابزارهای توسعه دهنده مرورگر برای مکث کد در نقاط خاص (breakpointها) و بررسی مقادیر متغیرها میباشد. این کار به ما کمک میکند تا مشخص کنیم که کارها تا کدام قسمت پیش میروند.
Console logging شامل استفاده از عبارت console.log()
برای چاپ پیام خاص یا مقادیر متغیرها در کنسول است. این موضوع میتواند به ما کمک کند تا جریان کد خود را ردیابی کرده و رفتار غیر منتظره را شناسایی نماییم.
پرداختن به مسائل مربوط به خطاها نیاز به رویکردی علمی دارد. به این ترتیب که:
پیمایش مسائل مربوط به scope ممکن است مانند باز کردن یک گره به نظر برسد، اما با تمرین، میتوانیم به مهارتی دست پیدا کنیم که ما را قادر میسازد حتی سختترین مشکلات را هم برطرف کنیم.
در این مقاله ما توابع و scope در را جاوااسکریپت بررسی کردیم. این که توابع چگونه میتوانند به عنوان ابزار قدرتمند عمل کنند و به ما این اجازه را بدهند تا کدهای سازمانیافته با قابلیت استفاده مجدد ایجاد کنیم. همچنین یاد گرفتیم که scope مانند مجموعهای از قوانین است و تعیین میکند که متغیرها بتوانند آزادانه در همه قسمتهای کدی که داریم در دسترس باشند یا در محدودههای خاص بمانند.
اکنون با داشتن این بینشها و استراتژیها، به خوبی آماده ساخت کدهای جاوااسکریپت کارآمدتر و سازماندهی شده با استفاده از توابع و scope هستیم. در نتیجه به راحتی میتوانیم بر چالشها غلبه کرده و برنامههای کاربردی پویا بسازیم.