٢٣ مايو ٢٠٢٣

نطاق المتغيرات، الإغلاق

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

نحن بالفعل نعلم أن الدالة يمكنها الوصول إلى المتغيرات الخارجية منها (“المتغيرات الخارجية”).

ولكن ماذا يحدث إذا تغيرت المتغيرات الخارجية بعد إنشاء الدالة؟ هل ستحصل الدالة على القيم الأحدث أم القيم القديمة؟

وماذا إذا تم تمرير دالة كمعلمة واستدعاؤها من مكان آخر في الكود؟ هل ستحصل الدالة على وصول إلى المتغيرات الخارجية في المكان الجديد؟

دعنا نوسع معرفتنا لفهم هذه السيناريوهات والسيناريوهات الأكثر تعقيدًا.

سنتحدث عن المُتغيرات let/const هنا

سنتحَدث عن المُتغيرات let/const هنا

فى جافا سكريبت, هناك ثلاث طُرق لتعريف متغير: let, const (الأحدث), و var (الأقدم)

  • فى هذا المقال سنستخدم مُتغيرات let فى الأمثلة.
  • المُتغيرات, المٌعرًفة بإستخدام const, تتصرف بنفس الطريقة لذلك هذه المقالة عن const أيضاً.
  • var القديمة لها بعض الإختلافات الملحوظة, سيتم تغطيتها في هذا المقال إفادة «var» القديمة.

كُتل الكود

إذا تم تعريف مُتغير داخل كتلة من الكود {...}, هذا المٌتغير يمكن رؤيته فقط داخل هذه الكتلة.

مثلاً:

{
  // تعريف مُتغير محلي لا يمكن أن تتم رؤيته في الخارج


  let message = "Hello"; // مرئي فقط داخل هذه الكتلة

  alert(message); // Hello
}

alert(message); // خطأ: message غير مُعرًفة

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

{
  // إظهار رسالة
  let message = 'Hello';
  alert(message);
}

{
  // إظهار رسالة أخرى
  let message = 'Goodbye';
  alert(message);
}

سيكون هناك خطاء بدون الأقواس

سيكون هناك خطاء بدون الأقواس

يرجي ملاحظة, بدون فصل الكُتل سيكون هناك خطأ عند إستخدام let مع إسم مُتغير موجود بالفعل::

// إظهار رسالة
let message = "Hello";
alert(message);

// إظهار رسالة أخرى
let message = "Goodbye"; // خطأ: مُتغير موجود بالفعل
alert(message);

لكلِ من if, for, while وهكذا كل المُتغيرات المُعرًفة بداخلها {...} يمكن رؤيتها فقط داخل الأقواس:

if (true) {
  let phrase = 'Hello!';

  alert(phrase); // Hello!
}

alert(phrase); // خطأ, مُتغير غير موجود!

هنا, بعد إنتهاء if, alert لن ترى phrase لذلك يوجد خطأ

هذا عظيم, هذا يسمح لنا بإنشاء متغير محلي للكتلة خاص فقط بفرع if.

نفس الشئ عند القيام بـ for و while:

for (let i = 0; i < 3; i++) {
  // المتغير i لا يمكن رؤيته إلا داخل الـ for
  alert(i); // 0, then 1, then 2
}

alert(i); // خطأ, مُتغير غير موجود!

لاحظ أن بصرياً let i تعتبر خارج الأقواس {...}. لكن for تعتبر حالة بناء خاصة لأن كل ما تم تعريفه بداخلها يعتبر داخل الأقواس.

الدوال المتداخلة

تسمي الدالة متداخلة عندما يتم إنشاتها داخل دالة أخري.

هذا سهل القيام به في جافا سكريبت.

يمكن إستخدام هذا في تنظيم الكود الخاص بنا, مثل هذا:

function sayHiBye(firstName, lastName) {
  // دالة متداخلة للمساعدة
  function getFullName() {
    return firstName + ' ' + lastName;
  }

  alert('Hello, ' + getFullName());
  alert('Bye, ' + getFullName());
}

هنا الدالة المتداخلة getFullName() صُنعت للإقناع. هذه الدالة يمكنها الوصول للمُتغيرات الخارجية وتُرجع الإسم بالكامل. تعتبر الدوال المتداخلة إلى حد ما شائعة الإستخدام في جافا سكريبت.

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

في الأسفل, makeCounter صَنعت “counter” و دالة أخرى تُرجع الرقم التالي مع كل نداء:

function makeCounter() {
  let count = 0;

  return function () {
    return count++;
  };
}

let counter = makeCounter();

alert(counter()); // 0
alert(counter()); // 1
alert(counter()); // 2

بالرغم من أن هذا المثال بسيط جداً إلا أنه يستخدم في الحياة العملية كمثال: موْلد رقم عشوائي لصُنع قيمة عشوائية للتجارب الاوتوماتيكية.

كيف يعمل هذا؟ هل إذا صنعنا عدادات كثير سيكونوا غير معتمدين علي بعضهم؟ ماذا يحدث مع المتغيرات هنا؟

فهم هذه الأشياء يعد عظيماً للمعلومات الشاملة لجافا سكريبت ومفيد جداً في حالة السيناريوهات المعقدة. لذلك هيا نتعمق أكثر في أمور أكثر صعوبة وتحتاج إلي تركيز.

البيئات المعجمية

هنا يجب أن تكون شديد التركيز!

كل الشرح المتعمق السابق كان شرح مع تجنب الدخول في تفاصيل ذات مستوي منخفض من اللغة لكن أي فهم دون هذه التفاصيل يعتبر ناقص, لذلك كُن مستعد للأننا سنتعمق في مستويات منخفضة من اللغة

للإيضاح, سينقسم الشرح إلي عدة خطوات

الخطوة الأولي: المُتغيرات

في لغة جافا سكريبت، تملك كلّ دالة عاملة أو كتلة شفرات ‎{...}‎ أو حتّى السكربت كلّه – تملك كائنًا داخليًا مرتبطًا بها (ولكنّه مخفي) يُدعى بالبيئة المُعجمية Lexical Environment.

تتألّف كائنات البيئات المُعجمية هذه من قسمين:

  1. سجلّ مُعجمي Environment Record: وهو كائن يخزّن كافة المتغيرات المحلية على أنّها خاصيات له (كما وغيرها من معلومات مثل قيمة ‎this‎).
  2. إشارة إلى البيئة المُعجمية الخارجية – أي المرتبطة مع الكود الخارجي للكائن المُعجمي.

ليس «المتغير» إلا خاصية لإحدى الكائنات الداخلية الخاصة: السجل المُعجمي ‎Environment Record‎. وحين نعني «بأخذ المتغير أو تغيير قيمته» فنعني «بأخذ خاصية ذلك الكائن أو تغيير قيمتها».

إليك هذه الكود البسيط مثالًا (فيها بيئة مُعجمية واحدة فقط):

هذا ما نسمّيه البيئة المُعجمية العمومية (global) وهي مرتبطة بالسكربت كاملًَا.

نعني بالمستطيل (في الصورة أعلاه) السجل المُعجمي (أي مخزن المتغيرات)، ونعني بالسهم الإشارة الخارجية له. وطالما أنّ البيئة المُعجمية العمومية ليس لها إشارة خارجية، فذاك السهم يُشير إلى ‎null‎.

عندما يتم تنفيذ الكود, تتغير البيئة المُعجمية.

هاهو مثال أطول بقليل:

نرى في المستطيلات على اليمين كيف تتغيّر البيئة المُعجمية العمومية أثناء تنفيذ الكود:

  1. عندما يبدأ السكريبت بالعمل, تكون البيئة المُعجمية مجهزة بكل المُتغيرات المٌعرفة داخلها.
    • في البداية يكونوا فى حالة تسمي غير مُعرف. هذه الحالة تعني أن المحرك يعرف عن المُتغيرات لكن لا يستطيع الإشارة إليهم حتي يتم تعريفهم عن طريق let.
  2. بعدها يظهر التصريح ‎let phrase‎، لكن لم تُسند للمتغيّر أيّ قيمة، لذا تخزّن البيئة ‎undefined‎.
  3. تُسند للمتغير ‎phrase‎ قيمة.
  4. وهنا تتغيّر قيمة ‎phrase‎.

بسيط حتّى الآن، أم لا؟

  1. عند بدء تشغيل البرنامج، تمتلئ البيئة اللغوية (Lexical Environment) مسبقًا بجميع المتغيرات المعلنة.
  • في البداية، تكون المتغيرات في الحالة “Uninitialized”. هذه حالة داخلية خاصة، وتعني أن المحرك يعرف المتغير، ولكن لا يمكن الإشارة إليه حتى يتم تعريفه بـ let. إنها تقريبًا نفس الشيء كما لو أن المتغير لم يكن موجودًا.
  1. ثم يظهر تعريف let phrase. لا يوجد تعيين حتى الآن، لذلك قيمتها هي undefined. يمكننا استخدام المتغير من هذه النقطة فصاعدًا.
  2. يتم تعييين قيمة phrase.
  3. تتغير قيمة phrase.
  • المتغير هو فعليًا خاصية لإحدى الكائنات الداخلية الخاصة، وهذا الكائن مرتبط بالكتلة أو الدالة أو السكربت الذي يجري تنفيذه حاليًا.
  • حين نعمل مع المتغيرات نكون في الواقع نعمل مع خصائص ذلك الكائن.
تعتبر البيئة المُعجمية من مواصفات الكائن

تعتبر البيئة المُعجمية من مواصفات الكائن: إنها توجد فقط بشكل نظري هنا: مواصفات اللغة لوصف كيف تعمل الأمور. لكن لا نستطيع أن نأتي بهذا الكائن في كودنا الخاص ونعدل عليه.

محركات جافا سكريبت أيضاً يمكن أن تحسن هذا, بالتغاضي عن المُتغيرات الغير مستخدمة لتوفير المساحة وعمل خدع داخلة, طالما كان التصرف المرئي كما تم وصفه.

الخطوة الثانية: التصريح بالدوال

الدالة أيضاً تعتبر قيمة, مثل المُتغير.

لكن الإختلاف أن التصريح بالدالة

لكن الدوال على عكس متغيرات ‎let‎، فليست تُهيّأ تمامًا حين تصلها عملية التنفيذ، لا، بل قبل ذلك حين تُنشأ البيئة المُعجمية.

وحين نتكلم عن أعلى الدوال مستوًى، فنعني ذلك لحظة بدء السكربت.

ولهذا السبب يمكننا استدعاء الدوال التي صرّحناها حتّى قبل أن نرى ذاك التعريف.

نرى في الكود أدناه كيف أنّ البيئة المُعجمية تحتوي شيئًا منذ بداية التنفيذ (وليست فارغة)، وما تحتويه هي ‎say‎ إذ أنّها تصريح عن دالة. وبعدها تسجّل ‎phrase‎ المُصرّح باستعمال ‎let‎:

هذا التصرف موجود فقط تصاريح الدالة Function Declarations وليس تعابير الدالة Function Expressions لأن تعابير الدالة تعامل معاملة المُتغير لأنها تخزن في متغير. مثل let say = function(name)....

الخطوة الثالثة: البيئات المُعجمية الداخلية والخارجية

عندما تبدأ الدالة بالعمل, في بداية لحظة مناداتها تُنشأ بيئة مُعجمية تلقائيًا ما إن تعمل الدالة وتخزّن المتغيرات المحلية ومُعاملات ذلك الاستدعاء

فمثلًا هكذا تبدو بيئة استدعاء ‎say("John")‎ (وصل التنفيذ السطر الذي عليه سهم):

إذًا… حين نكون داخل استدعاءً لأحد الدوال نرى لدينا بيئتين مُعجميتين: الداخلية (الخاصة باستدعاء الدالة) والخارجية (العمومية):

  • ترتبط البيئة المُعجمية الداخلية مع عملية التنفيذ الحالية للدالة ‎say‎. تملك خاصية واحدة فقط: ‎name‎ (وسيط الدالة). ونحن استدعينا ‎say("John")‎ بهذا تكون قيمة ‎name‎ هي ‎"John"‎.
  • البيئة المُعجمية الخارجية وهي هنا البيئة المُعجمية العمومية. تملك متغير ‎phrase‎ والدالة ذاتها.

للبيئة المُعجمية الداخلية إشارة إلى تلك «الخارجية».

حين يريد الكود الوصول إلى متغير من المتغيرات، يجري البحث أولًا في البيئة المُعجمية الداخلية، وبعدها الخارجية، والخارجية أكثر وأكثر وكثر حتى نصل العمومية.

لو لم يوجد المتغير في عملية البحث تلك فسترى خطأً (لو استعملت النمط الصارم Strict Mode). لو لم تستعمل ‎use strict‎ فسيُنشئ الإسناد إلى متغير غير موجود (مثل ‎user = "John"‎) متغيرًا عموميًا جديدًا باسم ‎user‎. سبب ذلك هو التوافق مع الإصدارات السابقة.

لنرى عملية البحث تلك في مثالنا:

  • حين تحاول ‎alert‎ في دالة ‎say‎ الوصول إلى المتغير ‎name‎ تجده مباشرةً في البيئة المُعجمية للدالة.
  • وحين تحاول الوصول إلى متغير ‎phrase‎ ولا تجده محليًا، تتبع الإشارة في البيئة المحلية وتصل البيئة المُعجمية خارجها، وتجد المتغير فيها.

الخطوة الرابعة: إعادة/إرجاع دالة

إليك ما يجري في مثال ‎makeCounter‎ خطوةً بخطوة

function makeCounter() {
  let count = 0;

  return function () {
    return count++;
  };
}

let counter = makeCounter();

تُنشأ بيئة مُعجمية لحظة استدعاء ‎makeCounter()‎ لتحمل متغيراتها ومُعاملاتها.

إذاً نحن نمتلك بيئتين مُعجميتين, كما هو في المثال الأعلى:

ماهذا الإختلاف!, أثناء تشغيل makeCounter() هناك دالة صغيرة تم صنعها بداخلها تحتوي فقط علي سطر واحد return count++ ولم نقم بمناداتها فقط صنعناها.

جميع الدوال تتذكر البيئة المثعجمية حيث المكان الذي صُنعوا فيه. تقنياً, لا يوجد سحر هنا: كل دالة لها خاصية مخفية تسمي [[Environment]], التي تحتفظ بالبيئة المُعجمية حيث تم صنعها:

إذا counter.[[Environment]] يشير إلي البيئة المعجمية {count: 0}. هكذا تتذكر الدالة أين تم صُنعها. [[Environment]] يتم وضع قسمته مرة واحدة فقط ولا يتم تغيرها. وهذه المرة عندما يتم صنع الدالة

فيما بعد عندما تتم مناداة counter(), تظهر بيئة مُعجمية جديدة والبيئة المعجمية الخارجية لها تؤخذ من هنا counter.[[Environment]]:

الآن عندما يبحث الكود داخل counter() عن متغير count ، يبحث أولاً في بيئته اللغوية الخاصة (التي تكون فارغة ، لأنه لا توجد متغيرات محلية هناك) ، ثم في بيئة makeCounter() الخارجية التي يتم استدعاؤها منها، حيث يجد المتغير ويقوم بتغييره. ** المتغير تم تعديله في البيئة المعجمية حيث يعيش.**

ها هي الحالة بعد التنفيذ:

إذا نادينا counter() مراتٍ عديدة, المتغير count سيزيد إلي 2, 3 وهكذا في نفس المكان

المنغلقات

هناك مصطلح عام يُستعمل في البرمجة باسم «المُنغلِق» Clousure ويُفترض أن يعلم به المطوّرون.

A المنغلقات is a function that remembers هو دالة تتذكّر متغيراتها الخارجية كما ويمكنها أن تصل إليها. هذا الأمر -في بعض اللغات- مستحيل، أو أنّه يلزم كتابة الدالة بطريقة معيّنة ليحدث ذلك. ولكن كما شرحنا أعلاه ففي لغة جافا سكريبت، كلّ الدوال مُنغلِقات بطبيعتها (وطبعًا ثمّة استثناء واحد أوحد نشرحه في فصل تركيب جملة دالة جديدة "new Function").

يعني ذلك بأنّ الدوال تتذكّر أين أُنشئت باستعمال خاصية ‎[[Environment]]‎ المخفية، كما ويمكن للدوال كافة الوصول إلى متغيراتها الخارجية.

لو كنت عزيزي مطوّر الواجهات في مقابلةً وأتاك السؤال «ما هو المُنغلِق؟» فيمكنك أن تقدّم تعريفه شرحًا، كما وتُضيف بأنّ الدوال في جافا سكريبت كلّها مُنغلِقات، وربما شيء من عندك تفاصيل تقنية مثل خاصية ‎[[Environment]]‎ وطريقة عمل البيئات المُعجمية.

كنس المهملات

عادةً ما تُمسح وتُحذف البيئة المُعجمية بعدما تعمل الدالة

ومع ذلك، إذا كان هناك دالة متداخلة يمكن الوصول إليها بعد انتهاء الدالة الأصلية، فإن لديها خاصية [[Environment]] التي تشير إلى البيئة اللغوية.

في هذه الحالة، يمكن الوصول إلى البيئة اللغوية حتى بعد اكتمال الدالة، لذلك تبقى حية.

مثال:

function f() {
  let value = 123;

  return function () {
    alert(value);
  };
}

let g = f(); // g.[[Environment]] stores a reference to the Lexical Environment
// of the corresponding f() call

يرجى ملاحظة أنه إذا تم استدعاء f() العديد من المرات، وتم حفظ الدوال الناتجة، فسيتم الإحتفاظ بجميع كائنات البيئة اللغوية المقابلة في الذاكرة. في الكود أدناه، سيتم الإحتفاظ بجميع الكائنات اللغوية الثلاثة:

function f() {
  let value = Math.random();

  return function () {
    alert(value);
  };
}

// في المصفوفة ثلاث دوال تُشير كلّ منها إلى البيئة المُعجمية
// ‫في عملية التنفيذ f()‎ المقابلة لكلّ واحدة

let arr = [f(), f(), f()];

يموت كائن البيئة المُعجمية حين لا يمكن أن يصل إليه شيء (كما الحال مع أيّ كائن آخر). بعبارة أخرى فهو موجود طالما ثمّة دالة متداخلة واحدة (على الأقل) في الكود تُشير إليه.

في الكود أسفله، بعدما تصير ‎g‎ محالة الوصول تُمسح بيئتها المُعجمية فيها (ومعها متغير ‎value‎) من الذاكرة:

function f() {
  let value = 123;

  return function () {
    alert(value);
  };
}

let g = f(); // ‫طالما يمكن أن تصل func بإشارة إلى g، ستظلّ تشغل حيّزًا في الذاكرة

g = null; // ...والآن لم تعد كذلك ونكون قد نظّفنا الذاكرة

التحسينات على أرض الواقع

كما رأينا، فنظريًا طالما الدالة «حيّة تُرزق» تبقى معها كل متغيراتها الخارجية.

ثمّة -في محرّك V8 (كروم وأوبرا)- تأثير مهمّ ألا وهو أنّ هذا المتغير لن يكون مُتاحًا أثناء التنقيح.

جرّب تشغيل المثال الآتي في «أدوات المطوّرين» داخل متصفّح كروم.

ما إن يُلبث تنفيذ الشيفرة، اكتب ‎alert(value)‎ في الطرفية.

function f() {
  let value = Math.random();

  function g() {
    debugger; // in console: type alert(value); No such variable!
  }

  return g;
}

let g = f();
g();

كما رأينا، ما من متغير كهذا! يُفترض نظريًا أن نصل إليه ولكنّ المحرّك حسّن أداء الشيفرة وحذفه.

يؤدّي ذلك أحيانًا إلى مشاكل مضحكة (هذا إن لم تجلس عليها اليوم بطوله لحلّها) أثناء التنقيح. إحدى هذه المشاكل هي أن نرى المتغير الخارجي بدل الذي توقّعنا أن نراه (يحمل كلاهما نفس الاسم):

let value = 'Surprise!';

function f() {
  let value = 'the closest value';

  function g() {
    debugger; // in console: type alert(value); Surprise!
  }

  return g;
}

let g = f();
g();

هذه الميزة في V8 جيدة للمعرفة. إذا كنت تقوم بتصحيح الأخطاء باستخدام Chrome / Edge / Opera ، في وقت ما ستواجه هذه الميزة.

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

مهمه

الأهمية: 5

الدالة sayHi تستخدم اسم مُتغير خارجي. عندما تعمل الدالة أيًـا منهم سيستخدم؟

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // what will it show: "John" or "Pete"?

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

إذا السؤال هو هل ستشعر الدالة بالتغيير؟

الناتج هنا هو ‎"Pete"‎.

الدالة تحضر المتغيرات الخارجية كما هي الآن، إنها تستخدم المُعَدَّلة مؤخراً.

القديمة لا تخزن بعد الآن لذلك الدالة تحصل على آخر تحديث للمتغير إما عن طريق البيئة المعجمية الخاصة بها أو الخارجية.

الأهمية: 5

الدالة makeWorker أدناه تصنع دالة أخرى وتعيدها. هذه الدالة المُعادة يمكن مناداتها من أي مكان.

هل ستحصل على حق الوصول إلى المتغيرات الخارجية من موقع بنائها أم من موقع مناداتها أو من الاثنين؟

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// create a function
let work = makeWorker();

// call it
work(); // what will it show?

أي قيمة سوف تظهر؟ “Pete” أم “John”؟

أفترض الآن بأنّ إجابة السؤال الثاني في أول الفصل ستكون جليّة.

دالة ‎work()‎ في الشيفرة أدناه تأخذ الاسم ‎name‎ من مكانه الأصل عبر إشارة البيئة المُعجمية الخارجية إليه:

إذًا، فالناتج هنا هو ‎"Pete"‎.

ولكن لو لم نكتب ‎let name‎ في ‎makeWorker()‎ فسينتقل البحث إلى خارج الدالة تلك ويأخذ القيمة العمومية كما نرى من السلسلة أعلاه. في تلك الحالة سيكون الناتج ‎"John"‎.

هل العدّادات مستقلة عن بعضها البعض؟

صنعنا هنا عدّادين اثنين ‎counter‎ و ‎counter2‎ باستعمال ذات الدالة ‎makeCounter‎.

هل هما مستقلان عن بعضهما البعض؟ ما الذي سيعرضه العدّاد الثاني؟ ‎0,1‎ أم ‎2,3‎ أم ماذا؟

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?

الإجابة هي: 0,1.

صنعنا الدالتين ‎counter‎ و ‎counter2‎ باستدعاءين ‎makeCounter‎ مختلفين تمامًا.

لذا فلكلّ منهما بيئات مُعجمية خارجية مستقلة عن بعضها، ولكلّ منهما متغير ‎count‎ مستقل عن الثاني.

كائن عد

هنا صنعنا كائن عدّ بمساعدة دالة مُنشئة Constructor Function.

هل ستعمل؟ ماذا سيظهر؟

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };
  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?

طبعًا، ستعمل كما يجب.

صُنعت الدالتين المتداخلتين في نفس البيئة المُعجمية الخارجية، بهذا تتشاركان نفس المتغير ‎count‎ وتصلان إليه:

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };

  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1

دالة في شرط if

طالِع الشيفرة أسفله. ما ناتج الاستدعاء في آخر سطر؟

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

الناتج هو: خطأ.

صُرّح عن الدالة ‎sayHi‎ داخل الشرط ‎if‎ وتعيش فيه فقط لا غير. ما من دالة ‎sayHi‎ خارجية.

المجموع باستعمال المُنغلِقات

اكتب الدالة ‎sum‎ لتعمل هكذا: ‎sum(a)(b) = a+b‎.

نعم هكذا تمامًا باستعمال قوسين اثنين (ليست خطأً مطبعيًا).

مثال:

sum(1)(2) = 3
sum(5)(-1) = 4

ليعمل القوسين الثانيين، يجب أن يُعيد الأوليين دالة.

هكذا:

function sum(a) {

  return function(b) {
    return a + b; // takes "a" from the outer lexical environment
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
الأهمية: 4

ماذا سيكون الناتج من هذا الكود؟

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

ملحوظة: هناك خدعة في هذه المهمة. الحل ليس واضحاً بما فيه الكفاية.

الناتج: خطأ.

جرب الكود للتأكد:

let x = 1;

function func() {
  console.log(x); // ReferenceError: لا نستطيع الوصول لـ 'x' قبل إعطائها قيمة
  let x = 2;
}

func();

In this example we can observe the peculiar difference between a “non-existing” and “uninitialized” variable.

أو بطريقة أخرى, المُتغير تقنياً موجود, لكن ا تستطيع الوصول له قبل let.

الكود فى الأعلي وضح ذلك.

function func() {
// المتغير المحلي X يعتبر معروف للمحرك من البداية, لكن **غير معرف بقيمة** تظل حتي let
  // لذلك هناك خطأ

  console.log(x); // ReferenceError: لا نستطيع الوصول لـ 'x' قبل إعطائها قيمة

  let x = 2;
}

هذه المنطقة من المتغيرات المؤقتة الغير مستخدمة من بداية الكود حتي let تسمي بالمنطقة الميتة

الترشيح عبر دالة

نعلم بوجود التابِع ‎arr.filter(f)‎ للمصفوفات. ووظيفته هي ترشيح كلّ العناصر عبر الدالة ‎f‎. لو أرجعت ‎true‎ فيُعيد التابِع العنصر في المصفوفة الناتجة.

اصنع مجموعة مرشّحات «جاهزة لنستعملها مباشرة»:

  • ‎inBetween(a, b)‎ – بين ‎a‎ و‎b‎بما فيه الطرفين (أي باحتساب ‎a‎ و‎b‎).
  • ‎inArray([...])‎ – في المصفوفة الممرّرة.

هكذا يكون استعمالها:

  • ‎arr.filter(inBetween(3,6))‎ – تحدّد القيم بين 3 و6 فقط.
  • ‎arr.filter(inArray([1,2,3]))‎ – تحدّد العناصر المتطابقة مع أحد عناصر ‎[1,2,3]‎ فقط.

مثال:

/* .. your code for inBetween and inArray */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

افتح sandbox بالإختبارات.

1. المرشّح inBetween

function inBetween(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

2. المرشّح inArray

function inArray(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

افتح الحل الإختبارات في sandbox.

الترشيح حسب حقل الاستمارة

أمامنا مصفوفة كائنات علينا ترتيبها:

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

الطريقة الطبيعية هي الآتي:

// by name (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);

// by age (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);

هل يمكن أن تكون بحروف أقل، هكذا مثلًا؟

users.sort(byField('name'));
users.sort(byField('age'));

أي، بدل أن نكتب دالة، نضع ‎byField(fieldName)‎ فقط.

اكتب الدالة ‎byField‎ لنستعملها هكذا.

افتح sandbox بالإختبارات.

function byField(fieldName){
  return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}

افتح الحل الإختبارات في sandbox.

الأهمية: 5

جيش من الدوال

تصنع الشيفرة الآتية مصفوفة من مُطلقي النار ‎shooters‎.

يفترض أن تكتب لنا كلّ دالة رقم هويّتها، ولكن ثمّة خطب ما …

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // create a shooter function,
      alert( i ); // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.

لماذا يظهر لكل من المدفعين الرقم نفسه؟

قم بإصلاح الكود حتى يعمل كما هو مفترض به.

افتح sandbox بالإختبارات.

دعنا نفحص ما يحدث بالضبط داخل makeArmy، وسيصبح الحل واضحًا.

  1. تُنشئ مصفوفة ‎shooters‎ فارغة:

    let shooters = [];
    ```
  2. يتم ملؤها بالدوال باستخدام shooters.push(function) في الحلقة.

    shooters = [
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
      function () {
        alert(i);
      },
    ];
  3. يتم إرجاع المصفوفة من الدالة.

    ثم في وقت لاحق، سيتم استدعاء أي عضو، على سبيل المثال army[5]()، وسيتم الحصول على العنصر army[5] من المصفوفة (وهو دالة) ويتم استدعاؤها.

    السؤال هو لماذا تظهر لجميع الدوال نفس القيمة، وهي الرقم 10؟

    يحدث ذلك لأنه لا يوجد متغير محلي بإسم i داخل دوال shooter. عند استدعاء مثل هذه الدالة، فإنه يتم أخذ i من البيئة اللغوية الخارجية.

    إذاً، ماهي قيمة i؟

    إذا نظرنا إلى الشفرة المصدرية:

    function makeArmy() {
      ...
      let i = 0;
      while (i < 10) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter); // add function to the array
        i++;
      }
      ...
    }

    يمكننا ملاحظة أن جميع دوال shooter يتم إنشاؤها في البيئة اللغوية الخارجية لدالة makeArmy()، لكن عندما يتم استدعاء army[5]()، فقدانتهت دالة makeArmy من عملها وأصبحت قيمة i هي الأخيرة وهي 10 (حيث يتوقف الحلقة عند i=10

    وبالتالي، جميع دوال shooter ستحصل على نفس القيمة من البيئة اللغوية الخارجية وهي القيمة الأخيرة i=10.

    كما ترون في الصورة أعلاه، في كل تكرار لكتلة while {...} يتم إنشاء بيئة لغوية جديدة. لحل هذه المشكلة، يمكننا نسخ قيمة i في متغير داخل كتلة while {...}، مثل هذا:

    function makeArmy() {
      let shooters = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let shooter = function() { // shooter function
            alert( j ); // should show its number
          };
        shooters.push(shooter);
        i++;
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    // الآن يعمل الكود بشكل صحيح
    army[0](); // 0
    army[5](); // 5

    هنا let j = i يعلن عن متغير “محلي للتكرار” j ويقوم بنسخ i فيه. القيم الأساسية تنسخ “بالقيمة”، لذلك نحصل فعليًا على نسخة مستقلة من i تنتمي إلى تكرار الحلقة الحالي.

    يعمل الدوال shooter بشكل صحيح، لأن قيمة i تعيش الآن قليلاً أقرب. ليس في بيئة اللغة الخارجية لـ makeArmy()، ولكنفي البيئة اللغوية الخارجية التي تتوافق مع التكرار الحالي:

    كما يمكن تجنب مشكلة من هذا النوع إذا استخدمنا for في البداية، مثل هذا:

    function makeArmy() {
    
      let shooters = [];
    
      for(let i = 0; i < 10; i++) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    army[0](); // 0
    army[5](); // 5

    هذا بشكل أساسي نفس الأمر، لأن for في كل تكرار ينشئ بيئة لغوية جديدة، مع متغير i الخاص به. لذلك تشير الدالة التي تم إنشاؤها في كل تكرار إلى i الخاص بها، من تلك التكرار.

الآن، بعد أن قدمنا جهدًا كبيرًا في قراءة هذا الحل، وأن الوصفة النهائية هي بسيطة – استخدم for – قد تتساءل: هل كان ذلك يستحق كل هذا العناء؟

حسنًا، إذا كان بإمكانك الإجابة على السؤال بسهولة، فلن تقرأ الحل. لذلك، نأمل أن يكون قد ساعدك هذا السؤال على فهم الأمور بشكل أفضل.

بالإضافة إلى ذلك، ه

افتح الحل الإختبارات في sandbox.

خريطة الدورة التعليمية

التعليقات

إقرأ هذا قبل أن تضع تعليقًا…
  • إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات.
  • إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو.
  • إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)