اذهب إلى المحتوى

المنغلقات Closure في جافاسكربت


صفا الفليج

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

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

ولكن، ماذا يحدث حين يتغيّر المتغيّر الخارجي؟ هل تستلم الدالة أحدث قيمة له أو تلك التي كانت موجودة لحظة إنشاء الدالة؟

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

يختلف سلوك اللغات عن بعضها من هذه الناحية. في هذا الفصل سنتحدّث عن سلوك جافاسكربت.

أسئلة تحتاج أجوبة

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

  • تستعمل الدالة ‎sayHi‎ المتغير الخارجي ‎name‎. ما القيمة التي ستستعملها الدالة حين تعمل؟
let name = "John";

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

name = "Pete";

sayHi(); // ‫ماذا ستعرض؟ «John» أم «Pete»؟

يشيع وجود هذه الحالات في المتصفّحات كما وفي الخواديم عند التطوير. يمكن أن تعمل الدالة بعدما تُنشأ بفترة لو أراد المطوّر (مثلًا بعد أن يتفاعل المستخدم أو يستلم المتصفّح طلبًا من الشبكة).

إذًا فالسؤال هو: هل تستعمل آخر التعديلات؟

  • تصنع الدالة ‎makeWorker‎ دالةً أخرى وتُعيدها، ويمكن أن نستعدي تلك الدالة الجديدة من أيّ مكان آخر نريد. السؤال هو: هل يمكنها الوصول إلى المتغيرات الخارجية تلك التي من مكان إنشائها الأصلي، أم تلك التي في المكان الجديد، أم من المكانين معًا؟
function makeWorker() {
  let name = "Pete";

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

let name = "John";

// نصنع الدالة
let work = makeWorker();

// نستدعيها
work(); 

‫ماذا ستعرض الدالة work()‎؟ «Pete» (الاسم الذي تراه عند الإنشاء) أم «John» (الاسم الذي تراه عند الاستدعاء)؟

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

علينا أولًا أن نعرف ما هو «المتغير» هذا أصلًا لنُدرك ما يجري بالضبط.

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

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

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

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

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

lexical-environment-global.png

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

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

وهكذا تتغيّر البيئة حين تعرّف عن متغيّر وتُسند له قيمة:

lexical-environment-global-2.png

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

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

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

نلخّص الموضوع:

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

التصريح بالدوال

لم نرى حتّى اللحظة إلا المتغيرات. حان وقت التصريحات بالدوال.

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

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

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

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

lexical-environment-global-3.png

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

الآن لنتعمّق ونرى ما يحدث حين تحاول الدالة الوصول إلى متغير خارجي.

تستعمل ‎say()‎ أثناء الاستعداء المتغير الخارجي ‎phrase‎. لنرى تفاصيل ما يجري بالضبط.

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

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

let phrase = "Hello";

function say(name) {
 alert( `‎${phrase}, ${name}‎` );
}

say("John"); // Hello, John

lexical-environment-simple.png

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

  • ترتبط البيئة المُعجمية الداخلية مع عملية التنفيذ الحالية للدالة ‎say‎.

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

  • البيئة المُعجمية الخارجية وهي هنا البيئة المُعجمية العمومية.

    تملك متغير ‎phrase‎ والدالة ذاتها.

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

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

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

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

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

lexical-environment-simple-lookup.png

يمكننا أخيرًا تقديم إجابة على السؤال في أول الفصل.

تأخذ الدالة المتغيرات الخارجية من مكانها الآن، أي أنها تستعمل أحدث القيم.

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

إذًا، إجابة السؤال الأول هي ‎Pete‎:

let name = "John";

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

name = "Pete"; // (*)

sayHi(); // (*) Pete

سير تنفيذ الشيفرة أعلاه:

  1. للبيئة المُعجمية العمومية ‎name: "John"‎.
  2. في السطر ‎(*)‎ يتغيّر المتغير العمومي ويصير الآن ‎name: "Pete"‎.
  3. تأخذ الدالة ‎sayHi()‎ حين تتنفّذ قيمة ‎name‎ من الخارج (أي البيئة المُعجمية العمومية) حيث صارت الآن ‎"Pete"‎.

لكلّ استدعاء منك، بيئة مُعجمية من اللغة لاحظ بأنّ محرّك اللغة يُنشئ بيئة مُعجمية جديدة للدالة في كلّ مرة تعمل فيها الدالة.

ولو استدعيت الدالة أكثر من مرة فلكلّ استدعاء منها بيئة مُعجمية خاصة بها مستقلة المتغيرات المحلية والمُعاملات، ومخصّصة فقط لذلك الاستدعاء.

البيئات المُعجمية كائن في توصيف اللغة كائن «البيئة المُعجمية» (Lexical Environment) هو كائن في توصيف اللغة، أي أنّه موجود «نظريًا» فقط في توصيف اللغة لشرح طريقة عمل الأمور، ولا يمكننا أخذ هذا الكائن في الشيفرة ولا التعديل عليه مباشرةً. كما يمكن أن تُحسّن محرّكات جافاسكربت هذا الكائن أو تُهمل المتغيرات غير المستخدمة فتحفظ الذاكرة أو غيرها من خُدع داخلية، كلّ هذا بمنأًى عن السلوك الظاهر لنا فيظلّ كما هو.

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

تكون الدالة «متداخلة» متى صنعتها داخل دالة أخرى.

ويمكنك بسهولة بالغة فعل ذلك داخل جافاسكربت.

يمكننا استعمال هذه الميزة لتنظيم الشيفرة الإسباغيتية، هكذا:

function sayHiBye(firstName, lastName) {

  // دالة مساعدة متداخلة نستعملها أسفله
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

صنعنا هنا الدالة المتداخلة ‎getFullName()‎ لتسهّل حياتنا علينا، فيمكنها هي الوصول إلى المتغيرات الخارجية وإعادة اسم الشخص الكامل. كثيرًا ما نستعمل الدوال المتداخلة في جافاسكربت.

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

مثال على ذلك: أسندنا الدالة المتداخلة إلى كائن جديد باستعمال دالة مُنشئة:

// تُعيد الدالة المُنشئة كائنًا جديدًا
function User(name) {

  // نصنع تابِع الكائن على أنّه دالة متداخلة
  this.sayHi = function() {
    alert(name);
  };
}

let user = new User("John");
// ‫يمكن أن تصل شيفرة تابِع الكائن «sayHi» إلى «name» الخارجي
user.sayHi();

وهنا أنشأنا دالة «عدّ» وأعدناها، لا أكثر:

function makeCounter() {
  let count = 0;

  return function() {
    // ‫يمكنها الوصول إلى  متغير «count» الخارجي
    return count++; 
  };
}

let counter = makeCounter();

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

لنتفحّص مثال ‎makeCounter‎. تصنع الشيفرة دالة «العدّ» وتُعيد العدد التالي كلّما استدعيناها. صحيح أنّ الدالة بسيطة لكن بتعديلها قليلًا يمكن استعمالها لأمور عديدة مفيدة مثل مولّدات الأعداد شبه العشوائية وغيرها.

ولكن كيف يعمل هذا العدّاد داخليًا؟

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

lexical-search-order.png

  1. المتغيرات المحلية للدالة المتداخلة…
  2. المتغيرات المحلية للدالة الخارجية…
  3. وهكذا حتى نصل المتغيرات العمومية.

في هذا المثال وجدنا المتغير ‎count‎ في الخطوة الثانية. فلو عُدّلت قيمة المتغير الخارجي فيحدث هذا التعديل في المكان الذي وجدنا المتغير فيه. لهذا تجد ‎count++‎ المتغير الخارجي وتزيد قيمته في البيئة المُعجمية التي ينتمي المتغير إليها، تمامًا كما لو استعملنا ‎let count = 1‎.

إليك سؤالين تفكّر بهما (أيضًا):

  1. هل يمكننا بطريقة أو بأخرى تصفير العدّاد ‎count‎ من الشيفرة التي لا تنتمي إلى ‎makeCounter‎؟ مثلًا بعد استدعاءات ‎alert‎ في المثال أعلاه.
  2. حين نستعدي ‎makeCounter()‎ أكثر من مرة تُعيد لنا دوال ‎counter‎ كثيرة. فهل هي مستقلة بذاتها أم تتشارك ذات متغير ‎count‎؟

حاول حلّ السؤالين قبل مواصلة القراءة.

انتهيت؟

إذًا حان وقت الإجابات.

  1. ما من طريقة أبدًا: متغير ‎count‎ هو متغير محلي داخل إحدى الدوال ولا يمكننا الوصول إليه من الخارج.
  2. كلّ استدعاء من ‎makeCounter()‎ يصنع بيئة مُعجمية جديدة للدالة لها متغير ‎count‎ خاص بها. لذا فدوال ‎counter‎ الناتج مستقلة عن بعضها البعض.

إليك شيئًا تجربّه بنفسك:

function makeCounter() {
  let count = 0;
  return function() {
    return count++;
  };
}

let counter1 = makeCounter();
let counter2 = makeCounter();

alert( counter1() ); // 0
alert( counter1() ); // 1

alert( counter2() ); // ‫0 (مستقل)

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

البيئات بالتفصيل الممل

إليك ما يجري في مثال ‎makeCounter‎ خطوةً بخطوة. احرص على اتباعه لتحرص على فهم آلية عمل البيئات بالتفصيل.

لاحظ أنّا شرحنا الخاصية الإضافية ‎[[Environment]]‎ هنا، ولم نشرحها سابقًا للتبسيط.

  1. حين يبدأ السكربت لا يكون هناك إلى بيئة مُعجمية عمومية:

    lexenv-nested-makecounter-1.png

    في تلك اللحظة ليس هناك إلا دالة ‎makeCounter‎ إذ أنها تصريح عن دالة، ولم يبدأ تشغيلها بعد.

    تستلم كافة الدوال «لحظة إفاقتها للحياة» خاصية مخفية باسم ‎[[Environment]]‎ فيها إشارة إلى البيئة المُعجمية حيث أُنشئت.

    لم نتحدّث عن هذه قبلًا، وهي الطريقة التي تعرف الدالة فيها مكان صناعتها الأولي.

    هنا أُنشأت ‎makeCounter‎ في البيئة المُعجمية العمومية، ولهذا فتُبقي ‎[[Environment]]‎ إشارة إليها.

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

  2. تبدأ أخيرًا الشيفرة بالعمل، ويرى المحرّك متغيرا عموميًا جديدًا بالاسم ‎counter‎ صرّحنا عنه وقيمته هي ناتج استعداء ‎makeCounter()‎. إليك نظرة على اللحظة التي تكون فيها عملية التنفيذ على أول سطر داخل ‎makeCounter()‎:

    lexenv-nested-makecounter-2.png

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

    وكما الحال مع البيئات هذه فهي تخزّن أمرين:

    1. سجلّ بيئي فيه المتغيرات المحلية. في حالتنا هنا متغير ‎count‎ هو الوحيد المحلي (يظهر حين يُنفّذ سطر ‎let count‎).
    2. الإشارة إلى البيئة المُعجمية الخارجية (وتُضبط قيمة لخاصية ‎[[Environment]]‎ للدالة). تُشير هنا ‎[[Environment]]‎ للدالة ‎makeCounter‎ إلى البيئة المُعجمية العمومية.

    إذًا لدينا بيئتين مُعجميتين اثنتين: الأولى عمومية والثانية مخصّصة لاستدعاء ‎makeCounter‎ الحالي، بينما الإشارة الخارجية لها هي البيئة العمومية.

  3. تُصنع -أثناء تنفيذ ‎makeCounter()‎- دالة صغيرة متداخلة.

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

    قيمة الخاصية ‎[[Environment]]‎ للدالة المتداخلة هذه هي البيئة المُعجمية الحالية للدالة ‎makeCounter()‎ (مكان صناعة الدالة المتداخلة):

    lexenv-nested-makecounter-3.png

    لاحظ أنّ الدالة الداخلية (في هذه الخطوة) أُنشئت صحيح ولكن لم نستعدها بعد. الشيفرة في ‎return count++;‎ لا تعمل.

  4. تُواصل عملية التنفيذ العمل وينتهي استدعاء ‎makeCounter()‎ ويُسند ناتجها (وهو الدالة المتداخلة الصغيرة) إلى المتغير العمومي ‎counter‎:

    lexenv-nested-makecounter-4.png

    ليس لتلك الدالة إلا سطرًا واحدًا: ‎return count++‎ وسيُنفّذ ما إن نشغّل الدالة.

  5. وحين استدعاء ‎counter()‎ تُنشأ بيئة مُعجمية جديدة، لكنّها فارغة إذ أن ليس للدالة ‎counter‎ متغيرات محلية فيها، إلّا أنّ لخاصية الدالة ‎counter‎‎[[Environment]]‎ فائدة ففيها الإشارة «الخارجية» للدالة وهي التي تتيح لنا الوصول إلى متغيرات استدعاء ‎makeCounter()‎ السابق متى ما أنشأناه:

    lexenv-nested-makecounter-5.png

    أما الآن فحين يبحث الاستدعاء عن متغير ‎count‎ فهو يبحث أولًا في بيئته المُعجمية (الفارغة)، فلو لم يجدها بحث في البيئة المُعجمية لاستدعاء ‎makeCounter()‎ الخارجي، ويجد المتغير فيه.

    لاحظ آلية إدارة الذاكرة هنا. صحيح أنّ استدعاء ‎makeCounter()‎ انتهى قبل فترة إلا أن بيئته المُعجمية بقيت في الذاكرة لأنّ الدالة المتداخلة تحمل الخاصية ‎[[Environment]]‎ التي تُشير إلى تلك البيئة.

    يمكن القول بصفة عامة بأنّ البيئة المُعجمية لا تموت طالما يمكن لدالة من الدوال استعمالها. وحين لا توجد هكذا دالة - حينها تُمسح البيئة.

  6. لا يُعيد استدعاء ‎counter()‎ قيمة الخاصية ‎count‎ فحسب، بل أيضًا يزيدها واحدًا. لاحظ كيف أنّ التعديل حدث «في ذات مكانه» In place، فتعدّلت قيمة ‎count‎ في البيئة ذاتها التي وجدناه فيها.

    lexenv-nested-makecounter-6.png

  7. تعمل استدعاءات ‎counter()‎ التالية بنفس الطريقة.

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

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

lexenv-nested-work.png

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

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

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

المُنغِلق هو دالة تتذكّر متغيراتها الخارجية كما ويمكنها أن تصل إليها. هذا الأمر -في بعض اللغات- مستحيل، أو أنّه يلزم كتابة الدالة بطريقة معيّنة ليحدث ذلك. ولكن كما شرحنا أعلاه ففي لغة جافاسكربت، كلّ الدوال مُنغلِقات بطبيعتها (وطبعًا ثمّة استثناء واحد أوحد نشرحه في فصل «صياغة الدالة الجديدة»).

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

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

كُتل الشفرات والحلقات، تعابير الدوال الآنية

ركّزتُ في الأمثلة أعلاه على الدوال، إلا أنّ البيئة المُعجمية موجودة لكلّ كتلة شيفرات ‎{...}‎.

تُنشأ البيئة المُعجمية حين تعمل أيّ كتلة شيفرات فيها متغيرات تُعدّ محلية لهذه الكتلة. إليك بعض الأمثلة.

الجملة الشرطية If

في المثال أسفله نرى المتغير ‎user‎ موجودًا فقط داخل كتلة ‎if‎:

let phrase = "Hello";

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

    alert(`‎${phrase}, ${user}‎`); // Hello, John
}

alert(user); // ‫خطأ! لا أرى هذا المتغير!

lexenv-if.png

حين تصل عملية التنفيذ داخل كتلة ‎if‎ يُنشئ المحرك البيئة المُعجمية «فقط وفقط إذا كذا…».

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

فمثلًا بعدما تنتهي إفادة ‎if‎ لن يرى التابِع ‎alert‎ أسفلها متغير ‎user‎، وهذا سبب الخطأ.

حلقة «كرّر طالما»

لكلّ دورة في حلقة التكرار بيئة مُعجمية خاصة بها. وأيضًا لو صرّحت عن متغير في ‎for(let ...)‎ فسيكون موجودًا فيها:

for (let i = 0; i < 10; i++) {
  // لكلّ دورة بيئة مُعجمية خاصة بها
  // {i: value}
}

alert(i); // خطأ، ما من متغير كهذا

لاحظ كيف أنّ الإفادة ‎let i‎ خارج كتلة ‎{...}‎ بصريًا. مُنشئ حلقة ‎for‎ خاص نوعًا ما: لكلّ دورة من الحقة بيئة مُعجمية خاصة بها تحمل قيمة ‎i‎ الحالية فيها أيضًا.

وكما مع ‎if‎ فبعد انتهاء الحلقة لا نرى ‎i‎ خارجها.

كتل الشفرات

يمكننا أيضًا استعمال كتلة شفرات‎{…}‎ «مجرّدة» لنعزل المتغيرات في «نطاق محلي» خاص بها.

فمثلًا في متصفّح الوب تتشارك كل السكربتات (عدا التي فيها ‎type="module"‎) نفس المساحة العمومية. لذا لو أنشأنا متغيرًا عموميًا في واحد من السكربتات يمكن أن تراه البقية. هذا الأمر يتسبب بمشكلة لو استعمل سكربتان اثنان نفس اسم المتغير وبدأ كلّ منهما بتعويض الذي عند الآخر.

يمكن أن يحدث هذا لو كان اسم المتغير كلمة شائعة (مثلًا ‎name‎) ولا يدري مطورو السكربتات ما يفعله الغير.

يمكن أن نستعمل كتلة شيفرات لغول السكربت كاملًا أو جزءًا منه حتى لو أردنا تجنّب هذه المشكلة:

{
  // نُجري أمرًا على المتغيرات المحلية يُمنع على ما خارجنا رؤيته

  let message = "Hello";

  alert(message); // Hello
}

alert(message); // ‫خطأ: message غير معرّف

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

تعابير الدوال الآنية IIFE

سابقًا لم تكن هناك بيئات مُعجمية للكُتل في جافاسكربت.

وكما «الحاجة أمّ الاختراع»، فكان على المطوّرين حلّ ذلك، وهكذا صنعوا ما سمّوه «تعابير الدوال آنيّة الاستدعاء» Immediately-Invoked Function Expressions.

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

إليك شكل الدوال الآنية هذه:

(function() {

  let message = "Hello";

  alert(message); // Hello

})();

هنا أنشأنا تعبير دالة واستدعيناه مباشرةً/آنيًا. لذا فتعمل الشيفرة في لحظتها كما وفيها متغيراتها الخاصة بها.

الطريقة هي أن نُحيط تعبير الدالة بأقواس ‎(function {...})‎ إذ أنّ محرّك جافاسكربت حين يقابل ‎"function"‎ في الشيفرة الأساسية يفهمها وكأنّها تصريح عن دالة. ولكن، التصريح عن الدوال يحتاج اسمًا لها، بهذا فهذه الشيفرة ستَسبّب بخطأ:

// نحاول التصريح عن الدالة واستدعائها آنيًا
function() { // <-- Error: Unexpected token (

  let message = "Hello";

  alert(message); // Hello

}();

حتّى لو قلنا «طيب لنضيف ذلك الاسم» فلن ينفع إذ أنّ محرّك جافاسكربت لا يسمح باستدعاء التصاريح عن الدوال آنيًا:

// خطأ صياغي بسبب الأقواس أسفله
function go() {

}(); // <-- لا يمكن أن نستدعي التصريح عن الدوال آنيًا

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

ثمّة طرق أخرى دون الأقواس لنُقنع المحرّك بأنّ ما نعني هو تعبير الدالة:

// طرائق إنشاء هذه التعابير الآنية

(function() {
  alert("أقواس تحيط بالدالة");
}) ();

(function() {
  alert("أقواس تحيط بكامل الجملة");
}() );

! function() {
  alert("عملية الأعداد الثنائية NOT أوّل التعبير");
}();

+ function() {
  alert("عملية الجمع الأحادية أوّل التعبير");
}();

في كلّ الحالات أعلاه: صرّحنا عن تعبير دالة واستدعيناها آنيًا. لنوضّح ذلك ثانيةً: لم يعد هناك أيّ داع لنكتب هكذا شيفرات في وقتنا الحاضر.

كنس المهملات

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

function f() {
  let value1 = 123;
  let value2 = 456;
}

f();

هنا القيمتين (تقنيًا) خاصيتين للبيئة المُعجمية. ولكن حين تنتهي ‎f()‎ لا يمكن أن نصل إلى تلك البيئة بأيّ طريقة فتُحذف من الذاكرة.

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

function f() {
  let value = 123;

  function g() { alert(value); }

  return g; // (*)
}

let func = f(); // ‫يمكن أن تصل func الآن بإشارة إلى g
// بذلك تبقى في الذاكرة، ومعها بيئتها المُعجمية الخارجية

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

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

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

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

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

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

function f() {
  let value = 123;

  function g() { alert(value); }

  return g;
}

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

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

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

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

ولكن عمليًا تُحاول محرّكات جافاسكربت تحسين أداء ذلك. فهي تحلّل استعمال المتغيرات فلو كان واضحًا لها في الشيفرة بأنّ المتغير الخارجي لم يعد مستعملًا، تحذفه.

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

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

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

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

  function g() {
    debugger; // ‫اكتب في المِعراض: alert(value);‎ ما من متغير كهذا!
  }

  return g;
}

let g = f();
g();

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

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

let value = "Surprise!";

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

  function g() {
    debugger; // ‫اكتب في المِعراض: alert(value);‎ إليك Surprise!
  }

  return g;
}

let g = f();
g();

إلى فصل آخر! من المفيد معرفة هذه الميزة في معيار V8. متى ما بدأت التنقيح في كروم أو أوبرا، فستراها شئت أم أبيت.

هذه ليست علّة في المنقّح بل هي ميزة خاصة في معيار V8. ربما تتغير لاحقًا من يدري. يمكنك أن تتحقّق منها متى أردت بتجربة الأمثلة في هذه الصفحة.

تمارين

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

الأهمية: 5

صنعنا هنا عدّادين اثنين ‎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‎ مستقل عن الثاني.

كائن عد

الأهمية: 5

هنا صنعنا كائن عدّ بمساعدة دالة مُنشئة 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‎ خارجية.

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

الأهمية: 4

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

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

مثال:

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

الحل

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

هكذا:

function sum(a) {

  return function(b) {
    return a + b; // ‫تأخذ «a» من البيئة المُعجمية الخارجية
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4

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

الأهمية: 5

نعلم بوجود التابِع ‎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]‎ فقط.

مثال:

// .. ‫شيفرة الدالتين inBetween و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

الحل

  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
  1. المرشّح 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

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

الأهمية: 5

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

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

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

// ‫حسب الاسم (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);

// ‫حسب العمر (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);

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

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

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

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

الحل

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

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

users.sort(byField('name'));
users.forEach(user => alert(user.name)); // Ann, John, Pete

users.sort(byField('age'));
users.forEach(user => alert(user.name)); // Pete, Ann, John

جيش عرمرم من الدوال

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

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

الحل

لنُجري مسحًا شاملًا على ما يجري في ‎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]‎ من المصفوفة، وهي دالة فيستدعيها.

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

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

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

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

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

كما نرى… «تعيش» القيمة في البيئة المُعجمية المرتبطة بدورة ‎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()‎ المُعجمية بل الآن في تلك البيئة المخصّصة لدورة الحلقة الحالية. هكذا صارت تعمل كما يجب.

lexenv-makearmy.png

أعدنا كتابة الشيفرة هنا وعوّضنا ‎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‎، ولكنّها مرتبطة بالدورة الحالية في الحلقة.

ترجمة -وبتصرف- للفصل Closure من كتاب The JavaScript language


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...