دليل سريع للاستعداد لمقابلات JavaScript: أهم المفاهيم التي تمنحك الأفضلية

دقائق القراءة: 11

مقدمة

إذا كنت تستعد لخوض مقابلة تقنية لوظيفة تتطلب إتقان JavaScript، فأنت بحاجة إلى مراجعة ذكية تركز على المفاهيم الأكثر تكراراً في الأسئلة، لا إلى قراءة مطولة تستهلك وقتك دون عائد عملي. هذا الدليل صُمم ليكون مرجعاً مركزاً يساعدك على تثبيت الأساسيات، وفهم المفاهيم المتوسطة والمتقدمة، واستيعاب الأفكار التي تظهر كثيراً في مقابلات المطورين.

ستجد هنا شرحاً احترافياً لأهم محاور JavaScript، بداية من المتغيرات والمصفوفات، مروراً بالنطاق Scope وميزة Closures، وصولاً إلى البرمجة غير المتزامنة Asynchronous JavaScript، والوعود Promises، وموضوعات متقدمة مثل debouncing وthrottling وasync وdefer.

دليل الاستعداد لمقابلات جافاسكريبت وأهم المفاهيم البرمجية المطلوبة

المتطلبات الأساسية قبل البدء

  • فهم أساسي لكيفية عمل الويب.
  • معرفة أولية بـ HTML وCSS.
  • إلمام جيد بأساسيات JavaScript، خصوصاً صياغة ES6+.

فهرس الموضوعات

  • أساسيات JavaScript والمتغيرات وطرق المقارنة.
  • المصفوفات وأشهر دوالها.
  • الدوال والبرمجة الوظيفية.
  • النطاق Scope وClosures وHoisting.
  • الكائنات Objects والكلمة المفتاحية this.
  • Prototypes والوراثة النموذجية.
  • البرمجة غير المتزامنة وحلقة الأحداث Event Loop.
  • المؤقتات setTimeout() وsetInterval().
  • الوعود Promises وasync/await.
  • مفاهيم متقدمة: Polyfills وasync/defer وdebouncing وthrottling.
  • التخزين عبر localStorage وsessionStorage.

أساسيات JavaScript

المتغيرات في JavaScript

المتغيرات هي اللبنات الأساسية في أي لغة برمجة. نستخدمها لتخزين القيم، سواء كانت أرقاماً أو نصوصاً أو كائنات أو غير ذلك. وتمتاز JavaScript بأنها لغة ذات أنواع مرنة loosely-typed، أي أنك لا تحتاج إلى التصريح بنوع المتغير يدوياً في معظم الحالات.

في JavaScript توجد ثلاث طرق شائعة لتعريف المتغيرات: var وlet وconst.

مقارنة بين var و let و const في جافاسكريبت

الفروقات الأساسية بينها تتعلق بإعادة التعريف، وإمكانية التعديل، ونطاق الوصول scope.

var a = 3
var a = 4
console.log(a) // 4

let b = 3
// let b = 4 // Syntax Error
b = 4

const c = 3
// const c = 4 // Syntax Error

const d // Syntax Error

يمكن إعادة تعريف المتغير المعرّف بواسطة var، بينما لا يمكن ذلك مع let وconst. كما أن const يتطلب تهيئة فورية عند التعريف.

الفرق بين == و===

هذا السؤال من أكثر الأسئلة شيوعاً في المقابلات. العامل == يقارن القيم بعد محاولة تحويل الأنواع تلقائياً، بينما العامل === يقارن القيمة والنوع معاً دون تحويل.

let a = 5
let b = '5'

console.log(a == b)  // true
console.log(a === b) // false

في المقابلات العملية، يُنصح غالباً باستخدام === لتجنب السلوكيات غير المتوقعة الناتجة عن التحويل الضمني للأنواع.

المصفوفات في JavaScript

عندما يزداد عدد القيم التي تتعامل معها، يصبح من الأفضل جمعها في بنية منظمة مثل Array. المصفوفة تتيح تخزين مجموعة من العناصر والوصول إليها وتعديل ترتيبها ومعالجتها باستخدام دوال جاهزة قوية.

let a = 4
const b = 5
var c = 'hello'
const array = [a, b, c]

const arr = [4, 5, 'hello']

أشهر دوال المصفوفات

من أكثر الدوال استخداماً في المقابلات: map() وfilter() وfind() وreduce() وforEach().

استخدام map()

الدالة map() تنشئ مصفوفة جديدة اعتماداً على المصفوفة الأصلية، دون تعديلها مباشرة. وهي مثالية عندما تريد تحويل كل عنصر إلى شكل جديد.

const a = [1, 2, 3, 4, 5]
const d = a.map(function(item) {
  return item * 2
})
console.log(d) // [2, 4, 6, 8, 10]

استخدام filter()

الدالة filter() تعيد مصفوفة جديدة تحتوي فقط على العناصر التي تحقق شرطاً معيناً.

const words = ['react', 'script', 'interview', 'style', 'javascript']
const ans = words.filter((word) => word.length > 6)
console.log(ans) // ['interview', 'javascript']

ويمكنك تنفيذ الفكرة نفسها بدون الاعتماد على الدوال الجاهزة إذا طُلب منك ذلك في المقابلة:

let newArr = []
for (let i = 0; i < words.length; i++) {
  if (words[i].length > 6) {
    newArr.push(words[i])
  }
}
console.log(newArr)

الفرق بين forEach() وmap()

رغم التشابه الظاهري بين forEach() وmap()، فإن بينهما فرقين مهمين:

  • map() تعيد مصفوفة جديدة.
  • forEach() لا تعيد قيمة مفيدة عادة.
let arr = [1, 2, 3, 4, 5, 6, 7]

function consoleEven(arr) {
  let data = arr.map((num) => (num % 2 === 0 ? num * 2 : num * 1))
  console.log(data)
}

consoleEven(arr)
function consoleEven(arr) {
  let data = arr.forEach((num) => (num % 2 === 0 ? num * 2 : num * 1))
  console.log(data) // undefined
}

consoleEven(arr)

كما أن map() تسمح بما يسمى method chaining، وهو أمر لا يتحقق بالطريقة نفسها مع forEach().

function consoleEven(arr) {
  let data = arr
    .map((num) => (num % 2 === 0 ? num * 2 : num))
    .map((item) => (item % 2 === 0 ? item / 2 : item))
  console.log(data)
}

consoleEven(arr)

ومن النقاط المهمة في المقابلات: كل من map() وforEach() لا يغيران المصفوفة الأصلية بشكل مباشر في هذا السياق.

البرمجة الوظيفية في JavaScript

تعريف الدوال

يمكن تخزين جزء من المنطق البرمجي داخل دالة لإعادة استخدامه. في JavaScript يمكنك كتابة الدوال بطريقتين شائعتين: الدوال التقليدية، ودوال الأسهم Arrow Functions.

function a() {
  console.log('I am a normal function')
}

const b = () => {
  console.log('I am an arrow function')
}

const c = (name) => {
  console.log(`My name is ${name}`)
}

كما يمكن تمرير دالة إلى دالة أخرى، وهو مبدأ أساسي في البرمجة الوظيفية.

const greet = () => {
  const prefix = 'Mr'
  return (name) => {
    console.log(`${prefix} ${name}, welcome!`)
  }
}

greet()('Jack')

النطاق Scope في JavaScript

يشير Scope إلى المكان الذي يمكن من خلاله الوصول إلى المتغيرات. وهناك ثلاثة أنواع رئيسية:

  • النطاق العام Global Scope.
  • نطاق الدالة Function Scope.
  • نطاق الكتلة Block Scope.

المتغيرات المعرفة بـ var تختلف عن let وconst من حيث النطاق، وهذه من النقاط الأساسية في أي مقابلة.

var a = 5

function adder() {
  let b = 7
  console.log(a + b)
}

console.log(adder())
// console.log(b) // Error

{
  const c = 10
  console.log(c)
}
// console.log(c) // Error

فهم Closures في JavaScript

تُعد Closures من أهم المفاهيم التي تُسأل عنها كثيراً. ببساطة، يحدث closure عندما تحتفظ دالة داخلية بإمكانية الوصول إلى متغيرات الدالة الخارجية، حتى بعد انتهاء تنفيذ الدالة الخارجية.

const greet = () => {
  const prefix = 'Mr'
  return (name) => {
    console.log(`${prefix} ${name}, welcome!`)
  }
}

greet()('Jack')

في المثال السابق، المتغير prefix بقي متاحاً للدالة الداخلية، وهذا هو جوهر closure.

مثال أوضح:

function x() {
  var a = 7
  function y() {
    console.log(a)
  }
  return y
}

var z = x()
console.log(z)
z()

عند استدعاء z()، ستتمكن الدالة y() من الوصول إلى المتغير a لأن الدالة احتفظت بسياقها المعجمي lexical environment.

فوائد Closures

  • تنفيذ مفهوم Currying.
  • إخفاء البيانات Data Hiding.
  • تغليف السلوك داخل واجهات محددة.
let add = function(x) {
  return function(y) {
    console.log(x + y)
  }
}

let addByTwo = add(2)
addByTwo(3)
function Counter() {
  var count = 0
  this.incrementCount = function() {
    count++
    console.log(count)
  }
}

var adder = new Counter()
adder.incrementCount() // 1

عيوب Closures

رغم أهميتها، قد تسبب closures استهلاكاً زائداً للذاكرة إذا أسيء استخدامها، لأن بعض المتغيرات تظل محتفظة بمرجع داخل الذاكرة لفترة أطول من اللازم.

مفهوم Hoisting

Hoisting هو سلوك افتراضي في JavaScript يتم فيه رفع التصريحات إلى أعلى النطاق أثناء مرحلة التحضير للتنفيذ.

  • المتغيرات المعرّفة بـ var تُرفع وتُهيأ بالقيمة undefined.
  • المتغيرات المعرّفة بـ let وconst تُرفع ولكن لا تُهيأ مباشرة.
  • تعريفات الدوال تُرفع أيضاً.
function consoleNum() {
  console.log(num)
  var num = 10
}

consoleNum() // undefined

يرى محرك التنفيذ هذا الكود أقرب إلى الشكل التالي:

function consoleNum() {
  var num
  console.log(num)
  num = 10
}

الكائنات في JavaScript

الكائنات Objects تُستخدم لتخزين البيانات على هيئة أزواج key-value. وهي من أكثر البنى استخداماً في التطبيقات الواقعية.

const developer = {
  name: 'Raj',
  age: 22
}

في هذا المثال، name هو المفتاح، وRaj هي القيمة.

ما معنى this في JavaScript؟

الكلمة المفتاحية this من أكثر المواضيع التي تربك المبتدئين وتظهر كثيراً في المقابلات. الفكرة الأساسية أن قيمة this تعتمد على طريقة استدعاء الدالة ومكانها.

console.log(this)

في بيئة المتصفح، ستشير غالباً إلى كائن window عند الاستدعاء العام.

function myFunc() {
  console.log(this)
}

const obj = {
  bool: true,
  myFunc: myFunc,
}

obj.myFunc()

في هذا السياق، ستشير this إلى الكائن obj لأن الاستدعاء تم من خلاله.

myFunc() // window

هذا ما يُعرف باسم Implicit Binding.

الربط الصريح Explicit Binding مع call()

عندما تريد إجبار دالة على استخدام كائن معين كسياق لها، يمكنك استخدام call().

const student_1 = {
  name: 'Randall',
  displayName_1: function displayName() {
    console.log(this.name)
  }
}

const student_2 = {
  name: 'Raj',
  displayName_2: function displayName() {
    console.log(this.name)
  }
}

student_1.displayName_1()
student_2.displayName_2()

ولتقليل التكرار:

student_1.displayName_1.call(student_2) // Raj

شرح استخدام this و call في جافاسكريبت داخل المقابلات التقنية

const myData = {
  name: 'Rajat',
  city: 'Delhi',
  displayStay: function() {
    console.log(this.name, 'stays in', this.city)
  },
}

myData.displayStay()

const yourData = {
  name: 'name',
  city: 'city'
}

myData.displayStay.call(yourData)

انتبه أيضاً إلى أن دوال الأسهم Arrow Functions لا تنشئ قيمة خاصة بها لـ this، بل ترثها من النطاق الخارجي.

النماذج الأولية والوراثة النموذجية

ما هي Prototypes؟

عند إنشاء كائن أو دالة أو مصفوفة في JavaScript، يضيف المحرك خصائص وطرقاً داخلية مرتبطة بما يسمى prototype. لهذا السبب تجد أن المصفوفات تمتلك دوالاً مثل forEach() وmap() دون تعريفها يدوياً.

let arr = ['Rajat', 'Raj']
console.log(arr.__proto__.forEach)
console.log(arr.__proto__)
console.log(arr.__proto__.__proto__)
console.log(arr.__proto__.__proto__.__proto__)

تُعرف هذه السلسلة باسم prototype chain.

الوراثة النموذجية Prototypal Inheritance

let object = {
  name: 'Rajat',
  city: 'Delhi',
  getIntro: function() {
    console.log(`${this.name}, ${this.city}`)
  },
}

let object2 = {
  name: 'Aditya',
}

object2.__proto__ = object
console.log(object2.city)

بعد ربط object2 بـ object عبر النموذج الأولي، سيتمكن من الوصول إلى الخصائص غير الموجودة داخله مباشرة. هذا هو جوهر الوراثة النموذجية.

البرمجة غير المتزامنة في JavaScript

JavaScript لغة أحادية الخيط single-threaded، أي أنها تنفذ مهمة واحدة في كل لحظة. لكن تطبيقات الويب تحتاج غالباً إلى التعامل مع عمليات تستغرق وقتاً غير معروف، مثل جلب البيانات من الخادم. هنا تظهر أهمية البرمجة غير المتزامنة.

حلقة الأحداث Event Loop

لفهم السلوك غير المتزامن جيداً، من الضروري استيعاب دور Event Loop في تنسيق تنفيذ المهام بين مكدس الاستدعاءات Call Stack وطوابير المهام.

في المقابلات، لا يكفي حفظ التعريف؛ الأهم أن تفهم لماذا لا تُنفَّذ بعض الأوامر فوراً رغم ظهورها مبكراً في الكود.

المؤقتات: setTimeout() وsetInterval() وclearInterval()

setTimeout(() => {
  console.log('Here - I am after 2 seconds')
}, 2000)

const timer = setInterval(() => {
  console.log('I will keep on coming back until you clear me')
}, 2000)
clearInterval(timer)

مثال شائع في المقابلات:

console.log('Hello')
setTimeout(() => {
  console.log('lovely')
}, 0)
console.log('reader')

// Hello
// reader
// lovely

رغم أن التأخير في setTimeout() يساوي 0، فإن التنفيذ لا يحدث مباشرة، لأن الدالة تُرسل إلى قائمة الانتظار وتنتظر فراغ مكدس التنفيذ.

مثال أصعب قليلاً:

for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, i * 1000)
}

// 6 6 6 6 6

السبب أن المتغير i معرّف بواسطة var، وبحلول وقت تنفيذ الدوال المؤجلة تكون الحلقة قد انتهت وأصبحت قيمة i تساوي 6.

والحل الأبسط:

for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, i * 1000)
}

الوعود Promises في JavaScript

تُعد Promises من أهم أسئلة المقابلات الحديثة. وهي تمثل نتيجة مستقبلية لعملية غير متزامنة، قد تنجح أو تفشل.

حالات Promise الأساسية:

  • Pending: حالة الانتظار.
  • Fulfilled: تم التنفيذ بنجاح.
  • Rejected: حدث فشل.
const promise = new Promise((resolve, reject) => {
  let value = true
  if (value) {
    resolve('hey value is true')
  } else {
    reject('there was an error, value is false')
  }
})

promise
  .then((x) => {
    console.log(x)
  })
  .catch((err) => console.log(err))

ولا يشترط أن تكون أسماء المعاملات resolve وreject ثابتة، لكنها التسمية الاصطلاحية المتعارف عليها.

استخدام async/await

توفر async/await صياغة أوضح وأسهل للقراءة مقارنة بسلاسل then() وcatch().

async function asyncCall() {
  const result = await promise
  console.log(result)
}

asyncCall()

من مزايا هذا الأسلوب أنه يجعل تدفق الكود أقرب إلى الكود المتزامن، ويقلل من التعقيد مقارنة بما كان يعرف سابقاً بـ callback hell.

مفاهيم JavaScript متقدمة مهمة في المقابلات

ما هي Polyfills؟

Polyfill هو كود يُستخدم لتوفير ميزة حديثة في متصفحات قديمة لا تدعمها بشكل أصلي.

مثال على بناء نسخة مبسطة من map():

Array.prototype.myMap = function(cb) {
  var arr = []
  for (var i = 0; i < this.length; i++) {
    arr.push(cb(this[i], i, this))
  }
  return arr
}

const arr = [1, 2, 3]
console.log(arr.myMap((a) => a * 2)) // [2, 4, 6]

الفرق بين async وdefer في وسم script

عند تحميل ملفات JavaScript الخارجية في صفحة الويب، يمكن استخدام الخاصيتين async وdefer لتحسين التحميل والأداء.

شرح تحميل السكربت العادي في المتصفح قبل استخدام async و deferتوضيح آلية عمل الخاصية async في تحميل ملفات جافاسكريبتتوضيح آلية عمل الخاصية defer أثناء تحميل السكربت في المتصفحمقارنة بصرية بين script العادي و async و defer في جافاسكريبت

  • استخدم defer عندما تكون الملفات البرمجية معتمدة على بعضها ويجب تنفيذها بالترتيب.
  • استخدم async عندما يكون الملف مستقلاً ولا يعتمد على ترتيب معين.

نقطة مهمة في المقابلات: الخاصية async لا تضمن ترتيب التنفيذ.

مفهوم Debouncing

Debouncing أسلوب يمنع تكرار استدعاء الدالة بشكل مفرط، ويؤجل التنفيذ حتى يتوقف الحدث لفترة زمنية محددة. هذا شائع جداً في حقول البحث.

<input type='text' id='text' />
const getData = (e) => {
  console.log(e.target.value)
}

const inputField = document.getElementById('text')

const debounce = function(fn, delay) {
  let timer
  return function() {
    let context = this
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(context, arguments)
    }, delay)
  }
}

inputField.addEventListener('keyup', debounce(getData, 300))

الفكرة هنا أن الدالة getData لن تعمل عند كل ضغطة فوراً، بل بعد مرور 300ms دون حدث جديد.

تحدٍ بسيط مشابه باستخدام العد التنازلي:

let count = 10
for (let i = 0; i < 10; i++) {
  function timer(i) {
    setTimeout(() => {
      console.log(count)
      count--
    }, i * 500)
  }
  timer(i)
}

مفهوم Throttling

Throttling يضمن تنفيذ الدالة مرة واحدة فقط خلال فترة زمنية محددة، مهما تكرر الحدث داخل هذه الفترة. وهو مناسب للأحداث المتكررة جداً مثل تغيير حجم النافذة أو التمرير.

const expensive = () => {
  console.log('expensive')
}

const throttle = (fn, limit) => {
  let context = this
  let flag = true
  return function() {
    if (flag) {
      fn.apply(context, arguments)
      flag = false
    }
    setTimeout(() => {
      flag = true
    }, limit)
  }
}

const func = throttle(expensive, 2000)
window.addEventListener('resize', func)

الفرق بين Debouncing وThrottling

المفهوم السلوك الاستخدام الشائع
Debouncing ينفذ بعد توقف الحدث لفترة محددة حقول البحث والإدخال
Throttling ينفذ مرة كل فترة زمنية ثابتة التمرير وتغيير حجم النافذة

إذا كنت تريد تقليل عدد الطلبات أثناء الكتابة في مربع البحث، فاستخدم debouncing. وإذا كنت تريد منع تنفيذ دالة ثقيلة عشرات المرات أثناء التمرير، فاستخدم throttling.

التخزين في JavaScript

من الموضوعات الصغيرة ولكن المهمة في المقابلات: آليات التخزين داخل المتصفح.

  • localStorage: يحتفظ بالبيانات حتى بعد إغلاق المتصفح.
  • sessionStorage: يحتفظ بالبيانات فقط حتى انتهاء الجلسة أو إغلاق التبويب.
// save
localStorage.setItem('key', 'value')

// get
let data = localStorage.getItem('key')

// remove
localStorage.removeItem('key')

والأوامر نفسها تقريباً تنطبق على sessionStorage.

نصائح عملية لاجتياز مقابلة JavaScript بنجاح

  1. لا تكتفِ بحفظ التعريفات؛ افهم سبب السلوك البرمجي.
  2. تدرّب على كتابة الأمثلة بنفسك دون نسخ مباشر.
  3. راجع الأسئلة الخاصة بـ this وclosures وpromises أكثر من مرة.
  4. كن قادراً على شرح الفروقات بين var وlet وconst بوضوح.
  5. تعلم متى تستخدم map() ومتى تستخدم forEach() أو filter().
  6. توقع أسئلة تطبيقية، لا نظرية فقط.

الخلاصة التقنية

إذا أردنا تلخيص أهم ما يميز المرشح القوي في مقابلات JavaScript، فسنجد أن التفوق لا يعتمد على كثرة المصطلحات بقدر ما يعتمد على الفهم العميق لسلوك اللغة. المفاهيم مثل scope وclosure وthis وevent loop وpromises تمثل العمود الفقري لأي تقييم تقني حقيقي. وكلما تمكنت من ربط هذه المفاهيم بأمثلة عملية واضحة، زادت قدرتك على الإجابة بثقة وإقناع. هذا الدليل ليس بديلاً عن الممارسة، لكنه خريطة مركزة لأكثر ما تحتاجه فعلاً قبل المقابلة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *