هنگام یادگیری برنامه نویسی سوالی که ممکن است با آن مواجه شویم این است که آیا تا به حال به این موضوع فکر کردهایم که برنامهها چگونه کارهایی که باید انجام دهند را به خاطر میآورند و آنها را بارها و بارها تکرار میکنند؟ اینجاست که مفهوم توابع و 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محاسبات خود را انجام میدهد، پاسخ را محاسبه میکند (حاصل ضرب 3 و 5)، و آن را با استفاده از دستور 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";
در کد بالا، myNamehoist شده است، اما تلاش برای دسترسی به آن قبل از تعریف واقعی، به دلیل 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را محاسبه میکند. شرط پایه بررسی میکند که مقدار n0 یا 1 باشد. اگر اینطور باشد، تابع بلافاصله 1 را return میکند، زیرا فاکتوریل 0 یا 1 برابر با 1 است. حالت بازگشتی n را با نتیجه فراخوانی تابع factorial با n - 1ضرب میکند.
این کار یک زنجیره از callهای بازگشتی ایجاد میکند، که هر کدام مشکل را یک مرحله کاهش میدهد و زمانی که به شرایط پایه برسید متوقف میشود. همچنین مقادیر محاسبه شده به زنجیره برگردانده میشوند.
به عنوان مثال، هنگام فراخوانی factorial(5):
factorial(5)مقدار5 * factorial(4)را return میکند،
factorial(4)مقدار4 * factorial(3)را return میکند
factorial(3)مقدار3 * factorial(2)را return میکند
factorial(2)مقدار2 * factorial(1)را return میکند
factorial(1) مقدار 1 را return میکند
سپس این مقادیر با هم ضرب میشوند و نتیجه نهایی که 120 است به دست میآید.
بازگشت یک تکنیک قدرتمند است، اما برای جلوگیری از ایجاد حلقههای بینهایت، داشتن یک شرط پایه کاملاً ضروری است. هر فراخوانی بازگشتی باید به سمت حالت پایه حرکت کند و اطمینان حاصل کند که مشکل با هر تکرار، کوچکتر میشود.
با استفاده از 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 هستیم. در نتیجه به راحتی میتوانیم بر چالشها غلبه کرده و برنامههای کاربردی پویا بسازیم.