٢٥ مارس ٢٠٢١

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

Variable scope, closure

JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created at any moment, passed as an argument to another function, and then called from a totally different place of code later.

We already know that a function can access variables outside of it (“outer” variables).

But what happens if outer variables change since a function is created? Will the function get newer values or the old ones?

And what if a function is passed along as a parameter and called from another place of code, will it get access to outer variables at the new place?

Let’s expand our knowledge to understand these scenarios and more complex ones.

سنتحدث عن المُتغيرات 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]]‎ وطريقة عمل البيئات المُعجمية.

كنس المهملات

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

However, if there’s a nested function that is still reachable after the end of a function, then it has [[Environment]] property that references the lexical 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

Please note that if f() is called many times, and resulting functions are saved, then all corresponding Lexical Environment objects will also be retained in memory. In the code below, all 3 of them:

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; // ...والآن لم تعد كذلك ونكون قد نظّفنا الذاكرة

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

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

An important side effect in V8 (Chrome, Edge, Opera) is that such variable will become unavailable in debugging.

ثمّة -في محرّك 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();

This feature of V8 is good to know. If you are debugging with Chrome/Edge/Opera, sooner or later you will meet it.

That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. You can always check for it by running the examples on this page.

مهمه

الأهمية: 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.

Why do all of the shooters show the same value?

Fix the code so that they work as intended.

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

Let’s examine what exactly happens inside makeArmy, and the solution will become obvious.

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

    let shooters = [];
  2. Fills it with functions via shooters.push(function) in the loop.

    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. The array is returned from the function.

    Then, later, the call to any member, e.g. army[5]() will get the element army[5] from the array (which is a function) and calls it.

    Now why do all such functions show the same value, 10?

    That’s because there’s no local variable i inside shooter functions. When such a function is called, it takes i from its outer lexical environment.

    Then, what will be the value of i?

    If we look at the source:

    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++;
      }
      ...
    }

    We can see that all shooter functions are created in the lexical environment of makeArmy() function. But when army[5]() is called, makeArmy has already finished its job, and the final value of i is 10 (while stops at i=10).

    As the result, all shooter functions get the same value from the outer lexical environment and that is, the last value, i=10.

    As you can see above, on each iteration of a while {...} block, a new lexical environment is created. So, to fix this, we can copy the value of i into a variable within the while {...} block, like this:

    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();
    
    // Now the code works correctly
    army[0](); // 0
    army[5](); // 5

    Here let j = i declares an “iteration-local” variable j and copies i into it. Primitives are copied “by value”, so we actually get an independent copy of i, belonging to the current loop iteration.

    The shooters work correctly, because the value of i now lives a little bit closer. Not in makeArmy() Lexical Environment, but in the Lexical Environment that corresponds the current loop iteration:

    Such problem could also be avoided if we used for in the beginning, like this:

    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

    That’s essentially the same, because for on each iteration generates a new lexical environment, with its own variable i. So shooter generated in every iteration references its own i, from that very iteration.

Now, as you’ve put so much effort into reading this, and the final recipe is so simple – just use for, you may wonder – was it worth that?

Well, if you could easily answer the question, you wouldn’t read the solution. So, hopefully this task must have helped you to understand things a bit better.

Besides, there are indeed cases when one prefers while to for, and other scenarios, where such problems are real.

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

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

التعليقات

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