٤ يوليو ٢٠٢٠

نطاق المُتغير

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

كما نعلم بأنّ الدوال تستطيع الوصول إلى المتغيرات خارجها. نستعمل هذه الميزة كثيرًا.

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

سنتحدث عن المُتغيرات 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. When the script starts, the Lexical Environment is pre-populated with all declared variables.
    • Initially, they are in the “Uninitialized” state. That’s a special internal state, it means that the engine knows about the variable, but it cannot be referenced until it has been declared with let. It’s almost the same as if the variable didn’t exist.
  2. Then let phrase definition appears. There’s no assignment yet, so its value is undefined. We can use the variable from this point forward.
  3. phrase is assigned a value.
  4. phrase changes the value.
  • المتغير هو فعليًا خاصية لإحدى الكائنات الداخلية الخاصة، وهذا الكائن مرتبط بالكتلة أو الدالة أو السكربت الذي يجري تنفيذه حاليًا.
  • حين نعمل مع المتغيرات نكون في الواقع نعمل مع خصائص ذلك الكائن.
تعتبر البيئة المُعجمية من مواصفات الكائن

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

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

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

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

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

لكن الدوال على عكس متغيرات ‎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]]:

الأن عندما يبدأ الكود في البحث عن المتغير count داخل الدالة counter(), يبحث أولاً في البيئة المعجمية الخاصة به وإذا كانت فارفة يبحث في البيئة المعجمية الخارجية, ثم الخارج ثم الخارج حتي يجده.

Now when the code inside counter() looks for count variable, it first searches its own Lexical Environment (empty, as there are no local variables there), then the Lexical Environment of the outer makeCounter() call, where it finds and changes it.

** المتغير تم تعديله في البيئة المعجمية حيث يعيش.**

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

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

المنغلقات

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

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

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

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

كنس المهملات

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

…ولكن لو كانت هناك دالة متداخلة يمكن أن نصل إليها بعدما تنتهي ‎f‎ (ولديها خاصية ‎[[Environment]]‎ التي تُشير إلى البيئة المُعجمية الخارجية)، لو كانت فيمكن أن نصل إليها:

In that case the Lexical Environment is still reachable even after the completion of the function, so it stays alive.

مثال:

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. متى ما بدأت التنقيح في كروم أو أوبرا، فستراها شئت أم أبيت.

هذه ليست علّة في المنقّح بل هي ميزة خاصة في معيار 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();

في هذا المثال نستشف الفرق بين غير موجود و غير معرف بقيمة

كما قرأت في هذه المقالة نطاق المُتغير, المتغير أولاً يكون في حالة غير معرف بقيمة عند اللحظة الذي يتم فيها تشغيل الكتلة كلها التي تحتوي علي المتغير. وتظل هكذا حتي جملة let.

أو بطريقة أخرى, المُتغير تقنياً موجود, لكن ا تستطيع الوصول له قبل 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() { // دالة مُطلق النار
      alert( i ); // المفترض أن ترينا رقمها
    };
    shooters.push(shooter);
    i++;
  }

  return shooters;
}

let army = makeArmy();

army[0](); // مُطلق النار بالهويّة 0 يقول أنّه 10
army[5](); // ‫مُطلق النار بالهويّة 5 يقول أنّه 10...
// ... كلّ مُطلقي النار يقولون 10 بدل هويّاتهم 0 فَـ 1 فَـ 2 فَـ 3...

لماذا هويّة كلّ مُطلق نار نفس البقية؟ أصلِح الشيفرة لتعمل كما ينبغي أن تعمل.

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

لنُجري مسحًا شاملًا على ما يجري في ‎makeArmy‎، حينها سيظهر لنا الحل جليًا.

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

    let shooters = [];

. تملأ المصفوفة في حلقة عبر ‎shooters.push(function...)‎.

كلّ عنصر هو دالة، بهذا تكون المصفوفة الناتجة هكذا:


```js no-beautify
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); }
];
```
  1. تُعيد الدالة المصفوفة.

لاحقًا، يستلم استدعاء ‎army[5]()‎ العنصر ‎army[5]‎ من المصفوفة، وهي دالة فيستدعيها.

الآن، لماذا تعرض كلّ هذه الدوال نفس الناتج؟

يعود ذلك إلى عدم وجود أيّ متغير محلي باسم ‎i‎ في دوال ‎shooter‎. فحين تُستدعى هذه الدالة تأخذ المتغير ‎i‎ من البيئة المُعجمية الخارجية.

وماذا ستكون قيمة ‎i‎؟

لو رأينا مصدر القيمة:

function makeArmy() {
  ...
  let i = 0;
  while (i < 10) {
    let shooter = function() { // shooter function
      alert( i ); // should show its number
    };
    ...
  }
  ...
}

كما نرى… «تعيش» القيمة في البيئة المُعجمية المرتبطة بدورة ‎makeArmy()‎ الحالية. ولكن متى استدعينا ‎army[5]()‎، تكون دالة ‎makeArmy‎ قد أنهت مهمّتها فعلًا وقيمة ‎i‎ هي آخر قيمة، أي ‎10‎ (قيمة نهاية حلقة ‎while‎).

وبهذا تأخذ كلّ دوال ‎shooter‎ القيمة من البيئة المُعجمية الخارجية، ذات القيمة الأخيرة ‎i=10‎.

يمكن أن نُصلح ذلك بنقل تعريف المتغير إلى داخل الحلقة:

function makeArmy() {

  let shooters = [];

  for(let i = 0; i < 10; i++) {
    let shooter = function() { // دالة مُطلق النار
      alert( i );  // المفترض أن ترينا رقمها
    };
    shooters.push(shooter);
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 0
army[5](); // 5

الآن صارت تعمل كما يجب، إذ في كلّ مرة تُنفّذ كتلة الشيفرة في ‎for (let i=0...) {...}‎، يُنشئ المحرّك بيئة مُعجمية جديدة لها فيها متغير ‎i‎ المناسب لتلك الكتلة.

إذًا لنلخّص: قيمة ‎i‎ صارت «تعيش» أقرب للدالة من السابق. لم تعد في بيئة ‎makeArmy()‎ المُعجمية بل الآن في تلك البيئة المخصّصة لدورة الحلقة الحالية. هكذا صارت تعمل كما يجب.

أعدنا كتابة الشيفرة هنا وعوّضنا ‎while‎ بحلقة ‎for‎.

يمكننا أيضًا تنفيذ حيلة أخرى. لنراها لفهم الموضوع أكثر:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let j = i;
    let shooter = function() { // دالة مُطلق النار
      alert( j ); // (*) المفترض أن ترينا رقمها
    };
    shooters.push(shooter);
    i++;
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 0
army[5](); // 5

كما حلقة ‎for‎، فحلقة ‎while‎ تصنع بيئة مُعجمية جديدة لكلّ دورة، وهكذا نتأكّد بأن تكون قيمة ‎shooter‎ صحيحة.

باختصار ننسخ القيمة ‎let j = i‎ وهذا يصنع المتغير ‎j‎ المحلي داخل الحلقة وينسخ قيمة ‎i‎ إلى نفسه. تُنسخ الأنواع الأولية «حسب قيمتها» By value، لذا بهذا نأخذ نسخة كاملة مستقلة تمامًا عن ‎i‎، ولكنّها مرتبطة بالدورة الحالية في الحلقة.

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

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

التعليقات

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