تسعة أخطاء شائعة يرتكبها المطورون في JavaScript (وكيفية إصلاحها)
تسعة أخطاء شائعة يرتكبها المطورون في JavaScript (وكيفية إصلاحها)
تُعد لغة JavaScript حجر الزاوية في تطوير الويب الحديث، حيث تضفي الحيوية والتفاعل على صفحات الويب. بالنسبة للمبتدئين القادمين من لغات برمجة أخرى، قد تبدو JavaScript سهلة الفهم في البداية، ويمكن البدء بها بسرعة بعد بضعة دروس تعليمية. ومع ذلك، هناك مجموعة من الأخطاء الشائعة التي يقع فيها العديد من المبرمجين، سواء كانوا مبتدئين أو حتى ذوي خبرة متوسطة. في هذا المقال، سنتناول تسعة من هذه الأخطاء الشائعة (أو الممارسات السيئة) ونقدم حلولها لمساعدتك على أن تصبح مطور JavaScript أفضل وأكثر احترافية.
1. الخلط بين عوامل الإسناد (=) والمساواة (==، ===)
كما يوحي اسمه، يُستخدم عامل الإسناد (=) لتعيين قيم للمتغيرات. غالبًا ما يخلط المطورون بينه وبين عوامل المساواة. إليك مثال يوضح هذا الخطأ:
const name = "javascript";
if ((name = "nodejs")) {
console.log(name);
}
// output - nodejs
في هذا المثال، لم تتم مقارنة المتغير name بالسلسلة النصية 'nodejs'. بدلاً من ذلك، تم إسناد القيمة 'nodejs' إلى المتغير name، ثم طُبعت هذه القيمة إلى الكونسول. في JavaScript، يُطلق على علامتي المساواة المزدوجة (==) والثلاثية (===) اسم عوامل المقارنة. الطريقة الصحيحة لمقارنة القيم في الكود أعلاه هي كالتالي:
const name = "javascript";
if (name == "nodejs") {
console.log(name);
}
// no output
// OR
if (name === "nodejs") {
console.log(name);
}
// no output
الفرق الجوهري بين عوامل المقارنة هذه هو أن المساواة المزدوجة (==) تُجري مقارنة ضعيفة (loose comparison)، بينما تُجري المساواة الثلاثية (===) مقارنة صارمة (strict comparison). في المقارنة الضعيفة، تُقارن القيم فقط، بينما في المقارنة الصارمة، تُقارن القيم ونوع البيانات (datatype) معًا. يوضح الكود التالي هذا الفرق بشكل أفضل:
const number = "1";
console.log(number == 1); // true
console.log(number === 1); // false
هنا، تم إسناد قيمة نصية "1" للمتغير number. عند مقارنته بالرقم 1 باستخدام المساواة المزدوجة، تكون النتيجة true لأن القيمتين متطابقتان. ولكن عند مقارنته باستخدام المساواة الثلاثية، تكون النتيجة false لأن لكل قيمة نوع بيانات مختلف (نص ورقم).
2. توقع أن تكون دوال الاستدعاء (Callbacks) متزامنة
تُعد دوال الاستدعاء (Callbacks) إحدى الطرق التي تتعامل بها JavaScript مع العمليات غير المتزامنة (asynchronous operations). ومع ذلك، تُعد الـ Promises و async/await طرقًا مفضلة للتعامل مع العمليات غير المتزامنة، لأن استخدام دوال استدعاء متعددة يؤدي إلى ما يُعرف بـ callback hell (جحيم دوال الاستدعاء).
دوال الاستدعاء ليست متزامنة. تُستخدم كدالة ليتم استدعاؤها بعد اكتمال عملية ما أو عند انتهاء تنفيذ مؤجل. مثال على ذلك هو الدالة العامة setTimeout() التي تستقبل دالة استدعاء كأول وسيط لها، ومدة زمنية (بالمللي ثانية) كوسيط ثانٍ، كما يلي:
function callback() {
console.log("I am the first");
}
setTimeout(callback, 300);
console.log("I am the last");
// output
// I am the last
// I am the first
بعد 300 مللي ثانية، تُستدعى الدالة callback. ولكن قبل اكتمالها، يتم تنفيذ باقي الكود. هذا هو السبب في أن console.log("I am the last") تم تشغيله أولاً. الخطأ الشائع الذي يرتكبه المطورون هو تفسير دوال الاستدعاء على أنها متزامنة. على سبيل المثال، دالة استدعاء تُرجع قيمة ستُستخدم في عمليات أخرى. إليك هذا الخطأ:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function() {
secondNumber = 10;
}, 200);
console.log(firstNumber + secondNumber);
}
addTwoNumbers();
// NaN
الناتج هو NaN (ليس رقمًا) لأن المتغير secondNumber غير معرف (undefined). في وقت تنفيذ firstNumber + secondNumber، لا يزال secondNumber غير معرف لأن دالة setTimeout() ستنفذ دالة الاستدعاء بعد 200ms. أفضل طريقة لمعالجة هذا الأمر هي تنفيذ باقي الكود داخل دالة الاستدعاء نفسها:
function addTwoNumbers() {
let firstNumber = 5;
let secondNumber;
setTimeout(function() {
secondNumber = 10;
console.log(firstNumber + secondNumber);
}, 200);
}
addTwoNumbers();
// 15
3. الإشارات الخاطئة إلى this
يُعد مفهوم this من المفاهيم التي يساء فهمها بشكل شائع في JavaScript. لاستخدام this بفعالية، تحتاج حقًا إلى فهم كيفية عمله لأنه يعمل بشكل مختلف قليلاً مقارنة باللغات الأخرى. إليك مثال على خطأ شائع عند استخدام this:
const obj = {
name: "JavaScript",
printName: function() {
console.log(this.name);
},
printNameIn2Secs: function() {
setTimeout(function() {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// undefined
النتيجة الأولى هي JavaScript لأن this.name يشير بشكل صحيح إلى خاصية name للكائن. أما النتيجة الثانية فهي undefined لأن this فقد إشارته إلى خصائص الكائن (بما في ذلك name). يحدث هذا لأن قيمة this تعتمد على الكائن الذي يستدعي الدالة التي تتواجد فيها. يوجد متغير this في كل دالة، ولكن الكائن الذي يشير إليه يتحدد بواسطة الكائن الذي يستدعيه.
في obj.printName()، يشير this مباشرة إلى obj. ولكن في دالة الاستدعاء الخاصة بـ setTimeout()، لا يشير this إلى أي كائن لأن لا يوجد كائن استدعاه. لكي يكون الكائن قد استدعى setTimeout()، كان يجب أن يتم تنفيذ شيء مثل obj.setTimeout.... نظرًا لعدم وجود كائن يستدعي هذه الدالة، يتم استخدام الكائن الافتراضي (وهو window في بيئات المتصفح). وبما أن الخاصية name غير موجودة على الكائن window، تكون النتيجة undefined.
أفضل الطرق للاحتفاظ بالإشارة إلى this داخل setTimeout() هي استخدام دوال bind()، call()، apply()، أو دوال الأسهم (arrow functions) التي قُدمت في ES6. على عكس الدوال العادية، لا تُنشئ دوال الأسهم سياق this الخاص بها. لذا، سيحتفظ الكود التالي بإشارته إلى this:
const obj = {
name: "JavaScript",
printName: function() {
console.log(this.name);
},
printNameIn2Secs: function() {
setTimeout(() => {
console.log(this.name);
}, 2000);
},
};
obj.printName();
// JavaScript
obj.printNameIn2Secs();
// JavaScript
4. تجاهل قابلية تغيير الكائنات (Object Mutability)
على عكس أنواع البيانات الأولية (primitive data types) مثل السلاسل النصية (string) والأرقام (number) وما إلى ذلك، تُعد الكائنات (objects) في JavaScript أنواع بيانات مرجعية (reference data types). على سبيل المثال، في الكائنات ذات المفتاح والقيمة:
const obj1 = {
name: "JavaScript",
};
const obj2 = obj1;
obj2.name = "programming";
console.log(obj1.name);
// programming
هنا، يمتلك obj1 و obj2 نفس المرجع إلى الموقع في الذاكرة حيث يتم تخزين الكائن. وفي المصفوفات:
const arr1 = [2, 3, 4];
const arr2 = arr1;
arr2[0] = "javascript";
console.log(arr1);
// ['javascript', 3, 4]
الخطأ الشائع الذي يرتكبه المطورون هو تجاهل هذه الطبيعة في JavaScript، مما يؤدي إلى أخطاء غير متوقعة. على سبيل المثال، إذا كان هناك 5 كائنات تشير إلى نفس الكائن في الذاكرة، فقد يتداخل أحد هذه الكائنات مع خصائص الكائن الأصلي في قاعدة كود كبيرة. عندما يحدث هذا، فإن أي محاولة للوصول إلى الخصائص الأصلية قد تُرجع undefined أو قد تتسبب في خطأ.
أفضل ممارسة لذلك هي دائمًا إنشاء مراجع جديدة لكائنات جديدة عندما ترغب في تكرار كائن. لتحقيق ذلك، يُعد عامل الانتشار (spread operator) (... الذي قُدم في ES6) حلاً مثاليًا. على سبيل المثال، في الكائنات ذات المفتاح والقيمة:
const obj1 = {
name: "JavaScript",
};
const obj2 = { ...obj1 };
console.log(obj2);
// {name: 'JavaScript'}
obj2.name = "programming";
console.log(obj1.name);
// 'JavaScript'
وفي المصفوفات:
const arr1 = [2, 3, 4];
const arr2 = [...arr1];
console.log(arr2);
// [2,3,4]
arr2[0] = "javascript";
console.log(arr1);
// [2, 3, 4]
5. حفظ المصفوفات والكائنات في تخزين المتصفح بشكل خاطئ
أحيانًا، أثناء العمل مع JavaScript، قد يرغب المطورون في الاستفادة من localStorage لحفظ القيم. ولكن الخطأ الشائع هو محاولة حفظ المصفوفات والكائنات كما هي في localStorage. يقبل localStorage السلاسل النصية (strings) فقط. في محاولة لحفظ الكائنات، تقوم JavaScript بتحويل الكائن إلى سلسلة نصية. وتكون النتيجة [Object Object] للكائنات، وسلسلة نصية مفصولة بفواصل لعناصر المصفوفة. على سبيل المثال:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", obj);
console.log(window.localStorage.getItem("test-object"));
// [Object Object]
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", arr);
console.log(window.localStorage.getItem("test-array"));
// JavaScript, programming, 45
عند حفظ الكائنات بهذه الطريقة، يصبح الوصول إليها صعبًا. في مثال الكائن، سيؤدي الوصول إلى الخاصية مثل .name إلى حدوث خطأ. وذلك لأن [Object Object] هو الآن سلسلة نصية، بدون خاصية name. الطريقة الأفضل لحفظ الكائنات والمصفوفات في التخزين المحلي هي باستخدام JSON.stringify() (لتحويل الكائنات إلى سلاسل نصية) و JSON.parse() (لتحويل السلاسل النصية إلى كائنات). بهذه الطريقة، يصبح الوصول إلى الكائنات سهلاً. النسخة الصحيحة من الكود أعلاه ستكون:
const obj = { name: "JavaScript" };
window.localStorage.setItem("test-object", JSON.stringify(obj));
const objInStorage = window.localStorage.getItem("test-object");
console.log(JSON.parse(objInStorage));
// {name: 'JavaScript'}
const arr = ["JavaScript", "programming", 45];
window.localStorage.setItem("test-array", JSON.stringify(arr));
const arrInStorage = window.localStorage.getItem("test-array");
console.log(JSON.parse(arrInStorage));
// ['JavaScript', 'programming', 45]
6. عدم استخدام القيم الافتراضية (Default Values)
يُعد تعيين قيم افتراضية للمتغيرات الديناميكية ممارسة جيدة جدًا لمنع الأخطاء غير المتوقعة. إليك مثال على خطأ شائع:
function addTwoNumbers(a, b) {
console.log(a + b);
}
addTwoNumbers();
// NaN
النتيجة هي NaN لأن المتغير a غير معرف (undefined) والمتغير b غير معرف أيضًا. باستخدام القيم الافتراضية، يمكن منع أخطاء مثل هذه. على سبيل المثال:
function addTwoNumbers(a, b) {
if (!a) a = 0;
if (!b) b = 0;
console.log(a + b);
}
addTwoNumbers();
// 0
بدلاً من ذلك، يمكن استخدام ميزة القيم الافتراضية التي قُدمت في ES6 على النحو التالي:
function addTwoNumbers(a = 0, b = 0) {
console.log(a + b);
}
addTwoNumbers();
// 0
على الرغم من أن هذا المثال بسيط، إلا أنه يؤكد على أهمية القيم الافتراضية. بالإضافة إلى ذلك، يمكن للمطورين توفير رسائل خطأ أو تحذير عندما لا يتم توفير القيم المتوقعة.
7. تسمية المتغيرات بشكل غير صحيح
نعم، لا يزال المطورون يرتكبون هذا الخطأ. التسمية صعبة، ولكن المطورين ليس لديهم خيار آخر سوى إتقانها. التعليقات ممارسة جيدة في البرمجة، وكذلك تسمية المتغيرات بشكل صحيح. على سبيل المثال:
function total(discount, p) {
return p * discount;
}
المتغير discount مقبول، ولكن ماذا عن p أو total؟ مجموع ماذا؟ الممارسة الأفضل للكود أعلاه ستكون:
function totalPrice(discount, price) {
return discount * price;
}
تسمية المتغيرات بشكل صحيح أمر بالغ الأهمية لأن المطور قد لا يكون الوحيد الذي يعمل على قاعدة الكود في وقت معين أو في المستقبل. تسمية المتغيرات بشكل صحيح ستسمح للمساهمين بفهم كيفية عمل المشروع بسهولة أكبر، مما يعزز التعاون ويسهل الصيانة.
8. التحقق الخاطئ من القيم المنطقية (Boolean Values)
const isRaining = false;
if (isRaining) {
console.log('It is raining');
} else {
console.log('It is not raining');
}
// It is not raining
من الممارسات الشائعة التحقق من القيم المنطقية كما هو موضح في الكود أعلاه. بينما هذا مقبول، تحدث الأخطاء عند اختبار بعض القيم. في JavaScript، تُرجع المقارنة الضعيفة (loose comparison) بين 0 و false القيمة true، وبين 1 و true القيمة true. هذا يعني أنه إذا كانت قيمة isRaining هي 1، فسيتم اعتبار isRaining على أنها true. هذا أيضًا خطأ يُرتكب غالبًا في الكائنات. على سبيل المثال:
const obj = {
name: 'JavaScript',
number: 0
};
if (obj.number) {
console.log('number property exists');
} else {
console.log('number property does not exist');
}
// number property does not exist
على الرغم من أن الخاصية number موجودة، إلا أن obj.number تُرجع 0، وهي قيمة تُعتبر falsy (قيمة خاطئة في السياق المنطقي)، وبالتالي يتم تنفيذ الكتلة else. لذا، ما لم تكن متأكدًا من نطاق القيم التي ستُستخدم، يجب اختبار القيم المنطقية والخصائص في الكائنات بهذه الطريقة:
if (a === false)...(للتأكد من أن القيمة هيfalseبالضبط)if (object.hasOwnProperty(property))...(للتأكد من وجود الخاصية في الكائن)
9. الخلط بين الجمع (Addition) والربط (Concatenation)
علامة الجمع (+) لها وظيفتان في JavaScript: الجمع للأرقام والربط للسلاسل النصية. غالبًا ما يسيء بعض المطورين استخدام هذا العامل. على سبيل المثال:
const num1 = 30;
const num2 = "20";
const num3 = 30;
const word1 = "Java";
const word2 = "Script";
console.log(num1 + num2); // 3020
console.log(num1 + num3); // 60
console.log(word1 + word2); // JavaScript
عند جمع السلاسل النصية والأرقام، تقوم JavaScript بتحويل الأرقام إلى سلاسل نصية، ثم تربط جميع القيم. أما بالنسبة لجمع الأرقام، فتُجرى عملية رياضية حسابية. فهم هذا السلوك ضروري لتجنب الأخطاء المنطقية غير المتوقعة في تطبيقاتك.
الخلاصة التقنية
إن إتقان JavaScript يتجاوز مجرد كتابة الكود؛ إنه يتعلق بفهم الفروق الدقيقة في اللغة وتجنب المزالق الشائعة التي يمكن أن تؤدي إلى أخطاء يصعب تتبعها أو ضعف في الأداء. الأخطاء التسعة التي ناقشناها هنا، بدءًا من الخلط بين عوامل الإسناد والمساواة وصولاً إلى سوء فهم طبيعة الكائنات وقيم this، تمثل تحديات رئيسية للمطورين. من خلال تبني ممارسات برمجية أفضل مثل استخدام المقارنة الصارمة (===)، والتعامل الصحيح مع العمليات غير المتزامنة، وإنشاء نسخ عميقة للكائنات، وتوفير قيم افتراضية، وتسمية المتغيرات بوضوح، والتحقق الدقيق من القيم المنطقية، واستخدام JSON.stringify() و JSON.parse() لتخزين البيانات المعقدة، يمكن للمطورين رفع مستوى جودة كودهم بشكل كبير. البقاء على اطلاع دائم بالتطورات في اللغة، مثل ميزات ES6، أمر حيوي لبناء تطبيقات ويب قوية وموثوقة وفعالة. هذه الممارسات لا تقلل فقط من الأخطاء، بل تساهم أيضًا في إنشاء كود نظيف وسهل الصيانة والفهم، مما يعزز الإنتاجية والتعاون في فرق التطوير.