جافا سكريبت هي لغة موجهة نحو الدوال. تمنحنا اللغة حرية كبيرة، حيث يمكن إنشاء دالة في أي وقت، وإرسالها كوسيط لدالة أخرى ومن ثم استدعائها من مكان مختلف في الكود لاحقًا.
نحن بالفعل نعلم أن الدالة يمكنها الوصول إلى المتغيرات الخارجية منها (“المتغيرات الخارجية”).
ولكن ماذا يحدث إذا تغيرت المتغيرات الخارجية بعد إنشاء الدالة؟ هل ستحصل الدالة على القيم الأحدث أم القيم القديمة؟
وماذا إذا تم تمرير دالة كمعلمة واستدعاؤها من مكان آخر في الكود؟ هل ستحصل الدالة على وصول إلى المتغيرات الخارجية في المكان الجديد؟
دعنا نوسع معرفتنا لفهم هذه السيناريوهات والسيناريوهات الأكثر تعقيدًا.
سنتحدث عن المُتغيرات 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.
تتألّف كائنات البيئات المُعجمية هذه من قسمين:
- سجلّ مُعجمي Environment Record: وهو كائن يخزّن كافة المتغيرات المحلية على أنّها خاصيات له (كما وغيرها من معلومات مثل قيمة
this
). - إشارة إلى البيئة المُعجمية الخارجية – أي المرتبطة مع الكود الخارجي للكائن المُعجمي.
ليس «المتغير» إلا خاصية لإحدى الكائنات الداخلية الخاصة: السجل المُعجمي Environment Record
. وحين نعني «بأخذ المتغير أو تغيير قيمته» فنعني «بأخذ خاصية ذلك الكائن أو تغيير قيمتها».
إليك هذه الكود البسيط مثالًا (فيها بيئة مُعجمية واحدة فقط):
هذا ما نسمّيه البيئة المُعجمية العمومية (global) وهي مرتبطة بالسكربت كاملًَا.
نعني بالمستطيل (في الصورة أعلاه) السجل المُعجمي (أي مخزن المتغيرات)، ونعني بالسهم الإشارة الخارجية له. وطالما أنّ البيئة المُعجمية العمومية ليس لها إشارة خارجية، فذاك السهم يُشير إلى null
.
عندما يتم تنفيذ الكود, تتغير البيئة المُعجمية.
هاهو مثال أطول بقليل:
نرى في المستطيلات على اليمين كيف تتغيّر البيئة المُعجمية العمومية أثناء تنفيذ الكود:
- عندما يبدأ السكريبت بالعمل, تكون البيئة المُعجمية مجهزة بكل المُتغيرات المٌعرفة داخلها.
- في البداية يكونوا فى حالة تسمي غير مُعرف. هذه الحالة تعني أن المحرك يعرف عن المُتغيرات لكن لا يستطيع الإشارة إليهم حتي يتم تعريفهم عن طريق
let
.
- في البداية يكونوا فى حالة تسمي غير مُعرف. هذه الحالة تعني أن المحرك يعرف عن المُتغيرات لكن لا يستطيع الإشارة إليهم حتي يتم تعريفهم عن طريق
- بعدها يظهر التصريح
let phrase
، لكن لم تُسند للمتغيّر أيّ قيمة، لذا تخزّن البيئةundefined
. - تُسند للمتغير
phrase
قيمة. - وهنا تتغيّر قيمة
phrase
.
بسيط حتّى الآن، أم لا؟
- عند بدء تشغيل البرنامج، تمتلئ البيئة اللغوية (Lexical Environment) مسبقًا بجميع المتغيرات المعلنة.
- في البداية، تكون المتغيرات في الحالة “Uninitialized”. هذه حالة داخلية خاصة، وتعني أن المحرك يعرف المتغير، ولكن لا يمكن الإشارة إليه حتى يتم تعريفه بـ
let
. إنها تقريبًا نفس الشيء كما لو أن المتغير لم يكن موجودًا.
- ثم يظهر تعريف
let phrase
. لا يوجد تعيين حتى الآن، لذلك قيمتها هيundefined
. يمكننا استخدام المتغير من هذه النقطة فصاعدًا. - يتم تعييين قيمة
phrase
. - تتغير قيمة
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. ربما سيتم تغييرها في وقت ما. يمكنك دائمًا التحقق من ذلك عن طريق تشغيل الأمثلة على هذه الصفحة.