انواع خطاها در جاوا اسکریپت
در این بخش ابتدا به معرفی انواع خطاها و شرایط وقوع آنها میپردازیم. سپس با چند مثال به بررسی برخی اشتباهات مرسوم در برنامهنویسی میپردازیم. این مثالها نشان میدهند که برخی روشها در برنامهنویسی ممکن است ابتدا بدون اشکال به نظر آیند، اما دارای اشکالاتی هستند و میتوانند منجر به وقوع خطا شوند.
خطاهای دستوری (Syntax Errors)
یکی از انواع خطاهای رایج در تمام زبانهای برنامهنویسی، خطاهای دستوری هستند. این نوع خطاها زمانی رخ میدهند که یکی از دستورات، با قواعد زبان جاوا اسکریپت ناسازگار باشد. معمولاً کشف این موارد بسیار ساده است. زیرا برنامههایی که دارای چنین اشکالاتی باشند، قابل اجرا نیستند. یعنی به محض اینکه مفسر جاوا اسکریپت با چنین دستوراتی مواجه میشود، اجرای برنامه را متوقف میکند. به عنوان مثال به دستورات زیر توجه کنید. در خط دوم از این دستورات یک اشکال دستوری وجود دارد. یعنی دستور موجود در خط دوم قابل تفسیر توسط مفسر جاوا اسکریپت نیست و یک دستور نامعتبر است.
let a = 10 , b = 20;
let c = a ++ b;
alert(c);
اما همانطور که پیشتر اشاره شد، مرورگرها در هنگام وقوع خطاها هیچ پیامی را به کاربر نمایش نمیدهند. بنابراین کاربر متوجه وقوع خطا نخواهد شد. برای مشاهدهی خطاهای رخ داده در یک برنامهی جاوا اسکریپت میتوان از بخش Console از ابزار Developer Tools استفاده کرد. شکل زیر وضعیت کنسول را در مرورگر Chrome، پس از اجرای دستورات فوق نشان میدهد.
مشاهده میکنید که مرورگر پیام "Uncaught SyntaxError: Unexpected identifier" را در کنسول نمایش میدهد. این پیام نشاندهندهی وقوع یک خطا از نوع دستوری یا Syntax Error است. همچنین در مقابل این پیام، نام فایل و شمارهی خطی که خطا در آن رخ داده است نیز نشان داده میشود. با کلیک کردن بر روی نام فایل میتوانید محل دقیق وقوع خطا را مشاهده کنید. البته پیامی که مشاهده میکنید در مرورگرهای مختلف کمی متفاوت است. اما نحوهی برخورد با خطا در تمام مرورگرهای یکسان است.
یکی دیگر از حالاتی که منجر به وقوع یک خطای دستوری میشود، تعریف دو شناسهی همنام در یک حوزه است. به عنوان مثال دستورات زیر منجر به وقوع یک خطای دستوری میشوند. (پیام خطا در خط سوم نمایش داده شده است.)
let a = 10;
let a = 20;
← Uncaught SyntaxError: Identifier 'a' has already been declared
توجه کنید که شناسههای همنام میتوانند از هر نوعی باشند. به عنوان مثال اگر متغیری به نام "a" در برنامه تعریف شده باشد، تعریف یک تابع با نام "a" در همان حوزه موجب بروز یک خطای دستوری میشود. دستورات زیر این حالت را نشان میدهند.
let a = 10;
function a(){
console.log('Hello');
}
← Uncaught SyntaxError: Identifier 'a' has already been declared
خطاهای ارجاع (Reference Errors)
خطاهای ارجاع یا Reference Errors نوع دیگری از خطاها هستند. این نوع خطاها معمولاً زمانی رخ میدهند که قصد استفاده از یک شناسهی تعریف نشده را داشته باشیم. مثلاً فراخوانی تابعی که وجود ندارد و یا استفاده از متغیری که تعریف نشده است. دستور زیر با فرض تعریف نشده بودن تابع a موجب وقوع یک خطای ارجاع و توقف برنامه میشود.
a();
← Uncaught ReferenceError: a is not defined
همچنین در حالت Strict mode مقداردهی به متغیرهای تعریف نشده نیز موجب بروز یک خطای ارجاع میشود. در بخش قبل دیدیم که در حالت عادی میتوان متغیرهای تعریف نشده را نیز مقداردهی کرد. که در این صورت یک متغیر سراسری جدید تعریف میشود. اما در حالت Strict mode این امکان وجود ندارد و انجام این کار موجب بروز یک خطای ارجاع میشود. دستورات زیر رفتار جاوا اسکریپت را در این حالت نشان میدهند.
"use strict";
x = 10;
← Uncaught ReferenceError: x is not defined
خطاهای محدوده (Range Errors)
در برخی دستورات که با اعداد سر و کار دارند، فقط محدودهی مشخصی از اعداد معتبر هستند و نباید از اعداد خارج از این محدوده استفاده شود. در چنین شرایطی، در صورت استفاده از اعداد خارج از محدوده، یک خطای محدوده یا Range Error رخ میدهد. به عنوان مثال طول آرایهها حتماً باید یک عدد صحیح بین صفر تا ۴۲۹۴۹۶۷۲۹۵ باشد. در صورتی که از یک عدد اعشاری و یا عدد منفی به عنوان طول آرایه استفاده شود، یک خطای محدود رخ میدهد. دستور زیر چنین خطایی تولید میکند.
const x = Array(-10);
← Uncaught RangeError: Invalid array length
لازم به ذکر است که در صورتی که تابع Array فقط یک آرگومان ورودی داشته باشد و آرگومان ورودی از نوع عددی باشد، مقدار این آرگومان به عنوان طول آرایهی جدید در نظر گرفته میشود. به همین دلیل دستور فوق منجر به تولید خطای محدوده میشود. زیرا طول آرایه نمیتواند منفی باشد.
خطاهای نوع (Type Errors)
خطاهای نوع یا Type Errors معمولاً در دو حالت رخ میدهند. حالت اول زمانی است که نوع دادهی به کار برده شده برای یک منظور خاص نامناسب باشد. مثلاً به جای ارسال آرگومان عددی به یک تابع خاص، یک رشته را ارسال کنیم. حالت دوم نیز زمانی رخ میدهد که متدی را فراخوانی کنیم که تعریف نشده است. توجه کنید که فراخوانی یک تابع تعریف نشده، خطای ارجاع تولید میکند. اما فراخوانی یک متد تعریف نشده، خطای نوع تولید میکند. به عنوان مثال دستور زیر یک خطای نوع تولید میکند. زیرا شئ Math متدی به نام int ندارد.
let a = Math.int(10.5);
← Uncaught TypeError: Math.int is not a function
خطاهای منطقی (Logical Errors)
خطاهای منطقی یا Logical Errors با تمام انواع قبلی خطاها متفاوت هستند. زیرا این نوع خطاها موجب توقف برنامه نمیشوند و به همین دلیل کشف این نوع خطاها بر خلاف خطاهای قبلی معمولاً کار سادهای نیست. در واقع مفسر جاوا اسکریپت به هیچ وجه متوجه وقوع خطاهای منطقی نمیشود. به همین دلیل هیچ خطایی نیز تولید نشده و برنامه نیز متوقف نمیشود. این نوع خطاها معمولاً منجر به تولید خروجی نادرست میشوند.
در واقع مهمترین هنر یک برنامهنویس در اشکالزدایی برنامهها، هنر او در کشف خطاهای منطقی است. زیرا این نوع خطاها هیچ ارتباطی با قواعد زبان برنامهنویسی ندارند. بلکه به دلیل استفادهی نادرست از قابلیتهای زبان برنامهنویسی و یا پیادهسازی نادرست الگوریتمها توسط برنامهنویس رخ میدهند. برای نشان دادن مفهوم خطاهای منطقی ابتدا با یک مثال بسیار ساده شروع میکنیم.
فرض کنید قصد داریم برنامهای بنویسیم که یک عدد را از کاربر دریافت کرده و در متغیر x قرار دهد. سپس مقدار y را با رابطهی "y = 3x2 + 2x - 10" محاسبه کرده و به کاربر نمایش دهد. فرض کنید دستورات زیر را برای حل این مسئله نوشتهایم.
let x = prompt('لطفاً یک عدد را وارد کنید');
let y = 3 * x ** 2 + 2 ** x - 10
alert(y);
حال برنامه را اجرا کرده و عدد ۵ را وارد میکنیم. طبق رابطهی "y = 3x2 + 2x - 10"، نتیجه باید برابر با ۷۵ باشد. اما این برنامه مقدار ۹۷ را به عنوان خروجی نمایش میدهد. در اینجا یک خطای منطقی رخ داده است. زیرا برنامه با هیچ خطایی مواجه نشده و کار خود را به طور کامل انجام داده است. اما نتیجهی اجرای برنامه با آنچه انتظار میرود متفاوت است. آیا متوجه دلیل نادرست بودن نتیجه شدهاید؟ مشکل این برنامه در خط دوم آن است. زیر برای محاسبهی 2x، به جای عبارت "2 * x" از "2 ** x" استفاده شده است.
حال سوال این است که آیا راهی وجود دارد تا مفسر جاوا اسکریپت چنین خطایی را تشخیص دهد؟ متاسفانه پاسخ این سوال منفی است. زیرا مفسر نمیداند مقصود شما از نوشتن این برنامه چیست و این به عهدهی برنامهنویس است که الگوریتم حل مسئله را صحیح پیادهسازی کند. معمولاً برنامهنویسان برای اطمینان از صحت عملکرد برنامه و کشف خطاهای منطقی احتمالی، برنامه را با چند ورودی خاص که خروجی صحیح آن معلوم است آزمایش میکنند. مثلاً در همین مثال میدانیم که این رابطه به ازای ورودی ۵ باید خروجی ۷۵ را تولید کند. اما با دیدن مقدار ۹۷ در خروجی، متوجه وجود یک خطای منطقی در برنامه میشویم.
خطاهای منطقی انواع مختلفی دارند و معمولاً برنامهنویسها در اشکالزدایی برنامهها، بیشترین زمان را به رفع این نوع خطاها اختصاص میدهند.
انواع اشکالات مرسوم در برنامهها و نحوهی رفع آنها
در ادامه به بررسی نمونههایی از اشکالات مرسوم در برنامهنویسی که منجر به بروز انواع خطاها (معمولاً خطاهای منطقی) میشوند میپردازیم. توجه کنید که همین اشکالات به ظاهر سادهای که در این مثالها میبینید، ممکن است در برنامههای بزرگ ساعتها یک برنامهنویس را برای اشکالزدایی مشغول کنند. حتی ممکن است برنامهنویسان متوجه وجود این اشکالات نشوند و کاربران نهایی آنها را کشف کنند. پس هرچه در زمان پیادهسازی توجه بیشتری به این نکات داشته باشید، در آینده با مشکلات کمتری مواجه خواهید شد.
به عنوان اولین مثال برنامهی زیر را در نظر بگیرید. این برنامه ابتدا نام کاربری را با استفاده از متد prompt از کاربر دریافت میکند و در متغیر username ذخیره میکند. سپس مقدار این متغیر را با رشتهی "admin" مقایسه میکند و پیام مناسبی را به کاربر نمایش میدهد.
let username = prompt('نام کاربری خود را وارد کنید');
if(username = "admin"){
alert('نام کاربری صحیح است');
}else{
alert('نام کاربری اشتباه است');
}
این برنامه را میتوانید اینجا اجرا کنید. با اجرای این برنامه مشاهده خواهید کرد که با وارد کردن هر مقدار دلخواهی در دیالوگ prompt، پیام یکسانی نمایش داده میشود. یعنی قسمت else به هیچ وجه اجرا نمیشود و همیشه قسمت if اجرا میشود. آیا متوجه اشکال موجود در این برنامه شدهاید؟ اشکال این برنامه در خط دوم آن است که به جای عملگر تساوی "==" از عملگر انتساب "=" استفاده شده است. این کار باعث میشود تا همیشه مقدار "admin" در متغیر username ذخیره شود. این مقدار نیز از نظر منطقی دارای ارزش true است. در نتیجه همیشه قسمت if اجرا میشود و مقدار وارد شده توسط کاربر هیچ تاثیری در روند اجرای برنامه ندارد.
استفاده از عملگر انتساب به جای عملگر تساوی یکی از اشتباهات رایج در برنامهنویسی است. همچنین یکی دیگر از اشتباهات رایج در جاوا اسکریپت، استفاده از عملگر تساوی "==" به جای عملگر تساوی صریح "===" است. هرچند در بسیاری موارد نتیجهی این دو عملگر یکسان است. اما به دلیل تبدیل نوع ضمنی در عملگر "=="، در موارد قابل توجهی نیز نتیجهی این دو عملگر یکسان نیست. چند نمونه از مواردی که نتیجهی این دو عملگر یکسان نیست در قطعه کد زیر نشان داده شده است. پس همیشه باید در زمان استفاده از عملگر تساوی، وجود تبدیل نوع ضمنی را در نظر داشته باشید. همچنین این نکته در مورد دو عملگر "=!" و "==!" نیز صادق است.
alert(5 == "5"); //true
alert(5 === "5"); //false
alert(1 == true); //true
alert(1 === true); //false
حال به بررسی یک حالت نسبتاً پیچیدهتر میپردازیم. در برنامهی زیر تابعی به نام concat نوشته شده است. این تابع ۳ پارامتر ورودی دارد. هدف این تابع دریافت ۲ یا ۳ آرگومان ورودی و بازگرداندن رشتهی حاصل از الحاق ورودیها میباشد. توجه کنید که آرگومان سوم اختیاری بوده و مقدار پیشفرض آن رشتهی تهی است. به همین دلیل ابتدا دو آرگومان اول بدون بررسی به یکدیگر الحاق میشوند. اما در مورد آرگومان سوم ابتدا بررسی میشود که آیا مقداری برای این آرگمان ارسال شده است یا خیر؟ در صورتی که مقداری برای این آرگومان ارسال شده باشد، این آرگومان نیز به مقدار نهایی الحاق میشود.
function concat(str1 , str2 , str3 = ''){
let result = str1 + str2;
if (str3 != ''){
result += str3;
}
return result;
}
حال این تابع را با چند ورودی آزمایش میکنیم. به ازای تمام ورودیهای زیر، این تابع خروجی مناسبی را تولید میکند.
concat('Abbas' , 'Moqaddam');
← "AbbasMoqaddam"
concat('Apple' , ' and ' , 'Orange');
← "Apple and Orange"
concat('Ali is ' , 20 , ' years old');
← "Ali is 20 years old"
همانطور که مشاهده میکنید، حتی با وجود استفاده از آرگومان عددی در دستور سوم، این تابع به درستی عمل کرده و نتیجهی مورد انتظار را تولید میکند. اما همیشه وضعیت به این خوبی نیست. به عنوان مثال فرض کنید عدد صفر به عنوان آرگومان سوم به این تابع ارسال شود. در این صورت این عدد به رشتهی خروجی این تابع الحاق نخواهد شد. دستور زیر رفتار این تابع را در چنین شرایطی نشان میدهد.
concat('eleven : ' , 1 , 1);
← "eleven : 11"
concat('ten : ' , 1 , 0);
← "ten : 1"
دلیل بروز این خطای منطقی، استفاده از عملگر عدم تساوی "=!"، به جای عملگر عدم تساوی صریح "==!" است. زیرا عملگر عدم تساوی "=!"، ابتدا عملوندها را به صورت ضمنی به نوع Boolean تبدیل میکند. با توجه به اینکه عدد صفر و همچنین رشتهی تهی، از نظر منطقی دارای ارزش false هستند. در نتیجه شرط مقابل if برقرار نیست و دستور داخل بلاک if اجرا نمیشود. اما عدد یک از نظر منطقی دارای ارزش true است. به همین دلیل دستور اول به درستی اجرا میشود.
برای رفع اشکال موجود در این برنامه باید از عملگر عدم تساوی صریح "==!" استفاده شود. پس میتوان برنامه را به شکل زیر اصلاح کرد. مشاهده میکنید که در این حالت خطای منطقی قبل رخ نمیدهد.
function concat(str1 , str2 , str3 = ''){
let result = str1 + str2;
if (str3 !== ''){
result += str3;
}
return result;
}
concat('ten : ' , 1 , 0);
← "ten : 10"
البته این تابع را به شکل سادهتری نیز میتوان نوشت. در این مثال صرفاً برای تشریح اهمیت تمایز قائل شدن بین انواع مختلف عملگرهای مقایسهای و توجه به تبدیلات ضمنی در این نوع عملگرها از ساختار if استفاده شد. اما میتوان همین تابع را به شکل سادهتری به صورت زیر پیادهسازی کرد.
function concat(str1 , str2 , str3 = ''){
let result = str1 + str2 + str3;
return result;
}
concat('ten : ' , 1 , 0);
← "ten : 10"
البته این تابع هنوز دارای یک مشکل جدی است. فرض کنید هر ۳ آرگومان ورودی این تابع از نوع عددی باشند. در این صورت عملگر "+" به جای الحاق این آرگومانها به یکدیگر، آنها را با یکدیگر جمع میکند. دستورات زیر این حالت را نشان میدهند.
concat('1' , '2' , '3');
← "123"
concat('1' , 2 , 3);
← "123"
concat(1 , 2 , 3);
← 6
عدم توجه به نوع دادهها، یکی از مهمترین عوامل وقوع خطاهای منطقی در برنامههای جاوا اسکریپت است. به طور کلی در زبانهای برنامهنویسی مفسری به دلیل تبدیل ضمنی نوع دادهها به یکدیگر، این نوع خطاهای منطقی زیاد رخ میدهند. برای پیشگیری از وقوع چنین خطاهایی باید همیشه قبل از استفاده از دادهها، نوع آنها بررسی شود و در صورت نیاز تبدیل نوع به صورت صریح انجام شود. به عنوان مثال تابع concat را میتوان به این صورت بازنویسی کرد.
function concat(str1 , str2 , str3 = ''){
let result = String(str1) + str2 + str3;
return result;
}
در این نسخه از تابع concat به دلیل تبدیل صریح str1 به نوع رشتهای، حتی در صورت ارسال آرگومانهای عددی، عملگر "+" قطعاً عمل الحاق را انجام میدهد. زیرا حداقل یکی از عملوندها از نوع رشته است. البته این روش در صورتی مناسب است که تبدیل نوع مجاز باشد. در برخی شرایط ممکن است تبدیل نوع مجاز نباشد و ارسال آرگومانهای عددی یا هر نوعی غیر از نوع رشته، غیر مجاز باشد. در چنین شرایطی باید ابتدا نوع آرگومانها بررسی شود تا در صورت غیر مجاز بودن نوع آرگومانهای ورودی، با تولید یک خطای سفارشی اجرای برنامه متوقف شود. (در مورد تولید خطاهای سفارشی در بخش بعدی صحبت خواهیم کرد.)
به عنوان آخرین مثال و برای نشان دادن حالتی که در آن تبدیل نوع مجاز نیست به مثال زیر توجه کنید. لازم به ذکر است که مجاز بودن یا نبودن تبدیل نوع، توسط برنامهنویس و منطق برنامه تعیین میشود. در این مثال تابعی به نام getQueryString وجود دارد که یک آدرس URL را دریافت کرده و مقدار Query String موجود در آدرس را بازمیگرداند. در این تابع برای یافتن Query String، ابتدا موقعیت (یا اندیس) کاراکتر علامت سوال با استفاده از متد indexOf به دست میآید. سپس با استفاده از متد substring، بخشی از رشته که بعد از این موقعیت قرار دارد به عنوان نتیجه بازگردانده میشود. همچنین اگر آدرس وارد شده دارای کاراکتر علامت سوال نباشد، مقدار تهی به عنوان نتیجه بازگردانده میشود.
function getQueryString(url){
let pos = url.indexOf('?');
if (pos > -1){
return url.substring(pos + 1);
}
return '';
}
این برنامه نیز در ظاهر مشکل خاصی ندارد و برای ورودیهای معتبر مانند ورودیهای زیر، خروجیهای معتبر تولید میکند.
getQueryString('http://otedia.com/?q=javascript');
← "q=javascript"
getQueryString('http://otedia.com/');
← ""
اما ممکن است ورودی این تابع یک رشته نباشد. مثلاً اگر یک عدد به عنوان آرگومان ورودی به این تابع ارسال شود. با توجه به اینکه متغیرهای عددی متدی به نام indexOf ندارند، یک خطای نوع ایجاد شده و برنامه متوقف میشود. دستور زیر چنین حالتی را نشان میدهد.
getQueryString(22);
← Uncaught TypeError: url.indexOf is not a function
در چنین شرایطی بهتر است ابتدا نوع آرگومان ورودی بررسی شود و در صورت نامعتبر بودن نوع داده، یک خطای سفارشی تولید شود. در بخش بعد در مورد این موضوع بیشتر صحبت خواهیم کرد.