النطاق والسياق في JavaScript فريدان بالنسبة للغة، ويرجع الفضل في ذلك جزئيًا إلى المرونة التي توفرها. كل وظيفة لها سياق ونطاق متغير مختلف. تشكل هذه المفاهيم أساسًا لبعض أنماط التصميم القوية في JavaScript. ومع ذلك، فإن هذا يسبب أيضًا ارتباكًا كبيرًا للمطورين. يكشف ما يلي بشكل شامل عن الاختلافات بين السياق والنطاق في JavaScript، وكيفية استخدام أنماط التصميم المختلفة لها.
السياق مقابل النطاق
أول شيء يجب توضيحه هو أن السياق والنطاق مفهومان مختلفان. على مر السنين، لاحظت أن العديد من المطورين غالبًا ما يخلطون بين هذين المصطلحين، ويصفون أحدهما بالآخر بشكل غير صحيح. لكي نكون منصفين، أصبحت هذه المصطلحات مربكة للغاية.
كل استدعاء دالة له نطاق وسياق مرتبط به. في الأساس، النطاق يعتمد على الوظيفة والسياق يعتمد على الكائن. بمعنى آخر، يرتبط النطاق بالوصول إلى المتغيرات في كل استدعاء دالة، وكل استدعاء مستقل. السياق هو دائمًا قيمة الكلمة الأساسية this، وهي إشارة إلى الكائن الذي يستدعي الكود القابل للتنفيذ الحالي.
نطاق متغير
يمكن تعريف المتغيرات في نطاقات محلية أو عالمية، مما يؤدي إلى الوصول إلى متغيرات وقت التشغيل من نطاقات مختلفة. يجب أن يتم الإعلان عن المتغيرات العامة خارج نص الوظيفة، وأن تكون موجودة طوال عملية التشغيل، ويمكن الوصول إليها وتعديلها في أي نطاق. يتم تعريف المتغيرات المحلية فقط داخل نص الوظيفة ولها نطاق مختلف لكل استدعاء دالة. هذا الموضوع هو إسناد وتقييم وتشغيل القيم فقط داخل الاستدعاء، ولا يمكن الوصول إلى القيم خارج النطاق.
حاليًا، لا تدعم JavaScript النطاق على مستوى الكتلة. يشير النطاق على مستوى الكتلة إلى تعريف المتغيرات في كتل البيانات مثل عبارات if، وعبارات التبديل، وعبارات الحلقة، وما إلى ذلك. وهذا يعني أنه لا يمكن الوصول إلى المتغيرات خارج كتلة العبارة. حاليًا، يمكن الوصول إلى أي متغيرات محددة داخل كتلة البيان خارج كتلة البيان. ومع ذلك، سيتغير هذا قريبًا، حيث تمت إضافة الكلمة الأساسية Let رسميًا إلى مواصفات ES6. استخدمها بدلاً من الكلمة الأساسية var للإعلان عن المتغيرات المحلية كنطاق على مستوى الكتلة.
السياق "هذا".
يعتمد السياق عادةً على كيفية استدعاء الوظيفة. عندما يتم استدعاء دالة كطريقة على كائن ما، يتم تعيين ذلك على الكائن الذي يتم استدعاء الطريقة عليه:
انسخ رمز الكود كما يلي:
كائن فار = {
فو: وظيفة () {
تنبيه (هذا === الكائن)؛
}
};
object.foo();
ينطبق نفس المبدأ عند استدعاء دالة لإنشاء مثيل لكائن باستخدام العامل الجديد. عند استدعائها بهذه الطريقة، سيتم تعيين قيمة this على المثيل المنشأ حديثًا:
انسخ رمز الكود كما يلي:
وظيفة فو () {
تنبيه(هذا);
}
فو () // نافذة
جديد فو () // فو
عند استدعاء دالة غير منضمة، سيتم تعيين هذا إلى السياق العام أو كائن النافذة (إذا كان في المتصفح) بشكل افتراضي. ومع ذلك، إذا تم تنفيذ الوظيفة في الوضع الصارم ("استخدام صارم")، فسيتم تعيين قيمة هذا على غير محدد افتراضيًا.
سياق التنفيذ وسلسلة النطاق
جافا سكريبت هي لغة ذات ترابط واحد، مما يعني أنها تستطيع القيام بشيء واحد فقط في كل مرة في المتصفح. عندما يقوم مترجم JavaScript بتنفيذ التعليمات البرمجية في البداية، فإنه يتم تعيينه أولاً بشكل افتراضي على السياق العام. يؤدي كل استدعاء لوظيفة إلى إنشاء سياق تنفيذ جديد.
غالبًا ما يحدث ارتباك هنا. مصطلح "سياق التنفيذ" هنا يعني النطاق، وليس السياق كما تمت مناقشته أعلاه. هذه تسمية سيئة، ولكن المصطلح محدد بواسطة مواصفات ECMAScript وليس لديه خيار سوى الالتزام بها.
في كل مرة يتم إنشاء سياق تنفيذ جديد، تتم إضافته إلى الجزء العلوي من سلسلة النطاق ويصبح التنفيذ أو مكدس الاستدعاءات. يعمل المتصفح دائمًا في سياق التنفيذ الحالي في الجزء العلوي من سلسلة النطاق. بمجرد الانتهاء، تتم إزالته (سياق التنفيذ الحالي) من أعلى المكدس ويتم إرجاع التحكم إلى سياق التنفيذ السابق. على سبيل المثال:
انسخ رمز الكود كما يلي:
الوظيفة أولاً (){
ثانية()؛
الوظيفة الثانية (){
ثالث()؛
الوظيفة الثالثة (){
الرابع ()؛
الوظيفة الرابعة (){
// افعل شيئا
}
}
}
}
أولاً()؛
سيؤدي تشغيل الكود السابق إلى تنفيذ الوظائف المتداخلة من الأعلى إلى الأسفل حتى الوظيفة الرابعة. في هذا الوقت، تكون سلسلة النطاق من الأعلى إلى الأسفل هي: الرابعة والثالثة والثانية والأولى والعالمية. يمكن للوظيفة الرابعة الوصول إلى المتغيرات العامة وأي متغيرات محددة في الوظائف الأولى والثانية والثالثة تمامًا مثل المتغيرات الخاصة بها. بمجرد اكتمال تنفيذ الوظيفة الرابعة، ستتم إزالة السياق الرابع من أعلى سلسلة النطاق وسيعود التنفيذ إلى الوظيفة الثالثة. تستمر هذه العملية حتى يتم الانتهاء من تنفيذ كافة التعليمات البرمجية.
يتم حل تعارضات التسمية المتغيرة بين سياقات التنفيذ المختلفة عن طريق تسلق سلسلة النطاق، من المحلي إلى العالمي. وهذا يعني أن المتغيرات المحلية التي تحمل نفس الاسم لها أولوية أعلى في سلسلة النطاق.
ببساطة، في كل مرة تحاول فيها الوصول إلى متغير في سياق تنفيذ الوظيفة، تبدأ عملية البحث دائمًا من الكائن المتغير الخاص. إذا لم يتم العثور على المتغير الذي تبحث عنه في كائن المتغير الخاص بك، فاستمر في البحث في سلسلة النطاق. سوف يتسلق سلسلة النطاق ويفحص كل كائن متغير سياق التنفيذ للعثور على قيمة تطابق اسم المتغير.
إنهاء
يتم تشكيل الإغلاق عند الوصول إلى وظيفة متداخلة خارج تعريفها (نطاقها) بحيث يمكن تنفيذها بعد عودة الوظيفة الخارجية. يحافظ (الإغلاق) (في الوظيفة الداخلية) على الوصول إلى المتغيرات المحلية والوسائط وإعلانات الوظائف في الوظيفة الخارجية. يسمح لنا التغليف بإخفاء سياق التنفيذ وحمايته من النطاق الخارجي، مع كشف الواجهة العامة التي يمكن من خلالها تنفيذ المزيد من العمليات. مثال بسيط يبدو كالتالي:
انسخ رمز الكود كما يلي:
وظيفة فو () {
var local = 'متغير خاص';
شريط وظيفة العودة () {
العودة المحلية.
}
}
var getLocalVariable = foo();
getLocalVariable() // متغير خاص
أحد أكثر أنواع الإغلاق شيوعًا هو نمط الوحدة المعروف. يسمح لك بالسخرية من الأعضاء العامين والخاصين والمتميزين:
انسخ رمز الكود كما يلي:
وحدة فار = (وظيفة(){
var PrivateProperty = 'foo';
وظيفة PrivateMethod(args){
// افعل شيئا
}
يعود {
الملكية العامة: ""،
الطريقة العامة: الوظيفة (الوسائط) {
// افعل شيئا
},
الطريقة المميزة: الوظيفة (الوسائط) {
PrivateMethod(args);
}
}
})();
الوحدات في الواقع تشبه إلى حد ما الوحدات المفردة، حيث تضيف زوجًا من الأقواس في النهاية وتنفذها مباشرة بعد انتهاء المترجم من تفسيرها (تنفيذ الوظيفة على الفور). الأعضاء الخارجيون الوحيدون المتوفرون في سياق تنفيذ الإغلاق هم الأساليب والخصائص العامة في الكائن الذي تم إرجاعه (مثل Module.publicMethod). ومع ذلك، فإن جميع الخصائص والأساليب الخاصة ستكون موجودة طوال دورة حياة البرنامج، لأن سياق التنفيذ محمي (الإغلاق)، والتفاعل مع المتغيرات يكون من خلال الأساليب العامة.
هناك نوع آخر من الإغلاق يسمى تعبير الوظيفة الذي يتم استدعاؤه على الفور IIFE، وهو ليس أكثر من مجرد وظيفة مجهولة يتم استدعاؤها ذاتيًا في سياق النافذة.
انسخ رمز الكود كما يلي:
وظيفة (نافذة) {
var a = 'foo', b = 'bar';
وظيفة خاصة () {
// افعل شيئا
}
نافذة.الوحدة النمطية = {
عامة: وظيفة () {
// افعل شيئا
}
};
})(هذا)؛
يعد هذا التعبير مفيدًا جدًا لحماية مساحة الاسم العامة. جميع المتغيرات المعلنة داخل نص الوظيفة هي متغيرات محلية وتستمر طوال بيئة التشغيل بأكملها من خلال عمليات الإغلاق. تحظى هذه الطريقة لتغليف كود المصدر بشعبية كبيرة لكل من البرامج وأطر العمل، وعادة ما تعرض واجهة عالمية واحدة للتفاعل مع العالم الخارجي.
اتصل وطبق
تسمح هاتان الطريقتان البسيطتان، المدمجتان في جميع الوظائف، بتنفيذ الوظائف في سياق مخصص. تتطلب وظيفة الاستدعاء قائمة معلمات بينما تسمح لك وظيفة التطبيق بتمرير المعلمات كمصفوفة:
انسخ رمز الكود كما يلي:
مستخدم الوظيفة (الأول والأخير والعمر) {
// افعل شيئا
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
نتيجة التنفيذ هي نفسها، ويتم استدعاء وظيفة المستخدم في سياق النافذة ويتم توفير نفس المعلمات الثلاثة.
قدم ECMAScript 5 (ES5) أسلوب Function.prototype.bind للتحكم في السياق، والذي يُرجع دالة جديدة مرتبطة بشكل دائم بالمعلمة الأولى لأسلوب الربط، بغض النظر عن كيفية استدعاء الوظيفة. يقوم بتصحيح سياق الوظيفة من خلال عمليات الإغلاق، وإليك الحل للمتصفحات التي لا تدعمها:
انسخ رمز الكود كما يلي:
إذا (!('ربط' في Function.prototype)){
Function.prototype.bind = function(){
var fn = this, context = الوسيطات[0], args = Array.prototype.slice.call(arguments, 1);
وظيفة الإرجاع (){
إرجاع fn.apply(context, args);
}
}
}
يتم استخدامه بشكل شائع في فقدان السياق: المعالجة الموجهة للكائنات ومعالجة الأحداث. يعد ذلك ضروريًا لأن طريقة addEventListener الخاصة بالعقدة تحافظ دائمًا على سياق تنفيذ الوظيفة باعتبارها العقدة التي يرتبط بها معالج الحدث، وهو أمر مهم. ومع ذلك، إذا كنت تستخدم تقنيات موجهة للكائنات متقدمة وتحتاج إلى الحفاظ على سياق وظيفة رد الاتصال كمثيل للأسلوب، فيجب عليك ضبط السياق يدويًا. هذه هي الراحة التي يوفرها الربط:
انسخ رمز الكود كما يلي:
وظيفة ماي كلاس () {
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);
}
MyClass.prototype.onClick = function(e){
// افعل شيئا
};
عند النظر مرة أخرى إلى التعليمات البرمجية المصدر لوظيفة الربط، قد تلاحظ السطر التالي البسيط نسبيًا من التعليمات البرمجية، الذي يستدعي أسلوبًا في Array:
انسخ رمز الكود كما يلي:
Array.prototype.slice.call(arguments, 1);
ومن المثير للاهتمام، من المهم أن نلاحظ هنا أن كائن الوسيطات ليس في الواقع مصفوفة، ومع ذلك يتم وصفه غالبًا على أنه كائن يشبه المصفوفة، يشبه إلى حد كبير قائمة العقدة (النتيجة التي يتم إرجاعها بواسطة الأسلوب document.getElementsByTagName()). تحتوي على خصائص الطول، ويمكن فهرسة القيم، لكنها لا تزال ليست مصفوفات لأنها لا تدعم أساليب المصفوفة الأصلية مثل الشريحة والدفع. ومع ذلك، نظرًا لأنها تتصرف بشكل مشابه للمصفوفات، فمن الممكن استدعاء أساليب المصفوفة واختطافها. إذا كنت تريد تنفيذ أساليب المصفوفة في سياق يشبه المصفوفة، فاتبع المثال أعلاه.
يتم تطبيق تقنية استدعاء أساليب الكائنات الأخرى أيضًا على كائنات التوجه، عند محاكاة الوراثة الكلاسيكية (وراثة الفئة) في JavaScript:
انسخ رمز الكود كما يلي:
MyClass.prototype.init = function(){
// استدعاء أسلوب init للفئة الفائقة في سياق مثيل "MyClass".
MySuperClass.prototype.init.apply(this,حجج);
}
يمكننا إعادة إنتاج نمط التصميم القوي هذا عن طريق استدعاء أساليب الفئة الفائقة (MySuperClass) في مثيلات الفئة الفرعية (MyClass).
ختاماً
من المهم جدًا فهم هذه المفاهيم قبل البدء في تعلم أنماط التصميم المتقدمة، نظرًا لأن النطاق والسياق يلعبان دورًا مهمًا وأساسيًا في JavaScript الحديثة. سواء كنا نتحدث عن عمليات الإغلاق، أو الكائنات الموجهة، أو الميراث أو التطبيقات المحلية المختلفة، فإن السياق والنطاق يلعبان دورًا مهمًا. إذا كان هدفك هو إتقان لغة JavaScript والحصول على فهم عميق لمكوناتها، فيجب أن يكون النطاق والسياق هو نقطة البداية.
ملحق المترجم
وظيفة الربط التي ينفذها المؤلف غير كاملة. لا يمكن تمرير المعلمات عند استدعاء الوظيفة التي يتم إرجاعها بواسطة الربط. يعمل التعليمة البرمجية التالية على إصلاح هذه المشكلة:
انسخ رمز الكود كما يلي:
إذا (!('ربط' في Function.prototype)){
Function.prototype.bind = function(){
var fn = this, context = الوسيطات[0], args = Array.prototype.slice.call(arguments, 1);
وظيفة الإرجاع (){
return fn.apply(context, args.concat(arguments));//fixed
}
}
}