ملاحظة مهمة: هذه الشرح لميزات اللغة المتقدمة، إذ تتناول هذه المقالة موضوعًا متقدمًا، لفهم بعض الحالات الهامشية بطريقة أفضل. مع العلم أنها ليست مهمة. والعديد من المطورين ذوي الخبرة الجيدة ينفذون مشاريعهم بدون معرفتها. ولكن إذا أردت معرفة كيفية عمل الأشياء تحت الطاولة فتابع القراءة.
إن استدعاء التابع المقيم ديناميكيًا يمكن أن يفقد قيمة this
.
فمثلًا:
let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // works // لنستدعي الآن user.hi أو user.bye بحسب الاسم _(user.name == "John" ? user.hi : user.bye)(); // خطأ
يوجد في السطر الأخير معامل شرطي يختار إما user.hi
أو user.bye
. في هذه الحالة تكون النتيجة user.hi
.
ثم تستدعى الدالّة ذات الأقواس ()
على الفور لكنها لن تعمل بطريقة صحيحة!
كما تلاحظ، نتج عن الاستدعاء بالطريقة السابقة خطأ، وذلك لأن قيمة this
داخل الاستدعاء تصبح غير معرفة undefined
.
نلاحظ هنا أن هذه الطريقة تعمل بصورة صحيحة (اسم الكائن ثم نقطة ثم اسم الدالّة):
user.hi();
أما هذه الطريقة فلن تعمل (طريقة تقييم الدالّة):
(user.name == "John" ? user.hi : user.bye)(); // Error!
لماذا حدث ذلك؟ إذا أردنا أن نفهم سبب حدوث ذلك، فلنتعرف أولًا على كيفية عمل الاستدعاء obj.method()
.
شرح النوع المرجعي
بالنظر عن كثب، قد نلاحظ عمليتين في العبارة obj.method()
:
-
أولًا، تسترجع النقطة
'.'
الخاصيةobj.method
. -
ثم تأتي الأقواس
()
لتنفّذها.
إذًا كيف تنتقل المعلومات الخاصة بقيمة this
من الجزء الأول إلى الجزء الثاني؟
إذا وضعنا هذه العمليات في سطور منفصلة، فسنفقدُ قيمة this
بكل تأكيد:
let user = { name: "John", hi() { alert(this.name); } } // جزّء استدعاء التوابع على سطرين let hi = user.hi; hi(); // خطأ لأنها غير معرّفة
إن التعليمة hi = user.hi
تضع الدالّة في المتغير، ثم الاستدعاء في السطر الأخير يكون مستقلًا تمامًا، وبالتالي لا يوجد قيمة لـ this
.
لجعل استدعاءات user.hi()
تعمل بصورة صحيحة، تستخدم لغة جافاسكربت خدعة - بأن لا تُرجع النقطة '.'
دالة، وإنما قيمة من نوع مرجعي.
إن القيمة من نوع مرجعي هو "نوع من المواصفات". لا يمكننا استخدامها بطريقة مباشرة، ولكن تستخدم داخليًا بواسطة اللغة.
إن القيمة من نوع مرجعي هي مجموعة من ثلاث قيم (base, name, strict)
، ويشير كلُّ منها إلى:
-
base
: وهو الكائن. -
name
وهو اسم الخاصية. -
strict
تكون قيمتهاtrue
إذا كان الوضع الصارم مفعلًا.
إن نتيجة وصول الخاصية user.hi
ليست دالة، ولكنها قيمة من نوع مرجعي. بالنسبة إلى user.hi
في الوضع الصارم، تكون هكذا:
// قيمة من نوع مرجعي (user, "hi", true)
عندما تستدعى الأقواس ()
في النوع المرجعي، فإنها تتلقى المعلومات الكاملة حول الكائن ودواله، ويمكنهم تعيين قيمة this
(ستكون user في حالتنا).
النوع المرجعي هو نوع داخلي خاص "وسيط"، بهدف تمرير المعلومات من النقطة .
إلى أقواس الاستدعاء ()
.
أي عملية أخرى مثل الإسناد hi = user.hi
تتجاهل نوع المرجع ككلّ، وتأخذ قيمة user.hi
(كدالّة) وتمررها. لذا فإن أي عملية أخرى تفقد قيمة this
.
لذلك وكنتيجة لما سبق، تُمرر قيمة this
بالطريقة الصحيحة فقط إذا استدعيت الدالّة مباشرة باستخدام نقطة obj.method()
أو الأقواس المربعة obj['method']()
(الصياغتين تؤديان الشيء نفسه). لاحقًا في هذا السلسلة التعليمية، سنتعلم طرقًا مختلفة لحل هذه المشكلة مثل الدالّة func.bind()
.
الخلاصة
النوع المرجعي هو نوع داخلي في لغة جافاسكربت. فقراءة خاصية ما، مثل النقطة .
فيobj.method()
لا تُرجع قيمة الخاصية فحسب، بل تُرجع قيمة من "نوع مرجعي" خاص والّتي تخزن فيها قيمة الخاصية والكائن المأخوذة منه. هذه طريقة لاستدعاء الدالة اللاحقة ()
للحصول على الكائن وتعيين قيمة this
إليه.
بالنسبة لجميع العمليات الأخرى، يصبح قيمة النوع المرجعي تلقائيًا قيمة الخاصية (الدالّة في حالتنا). جميع هذه الآليات مخفية عن أعيننا. ولا تهمنا إلا في الحالات الدقيقة، مثل عندما نريد الحصول على تابع ديناميكيًا من الكائن ما، باستخدام تعبير معيّن.
المهام
التحقق من الصياغة
الأهمية: 2
ما هي نتيجة هذه الشيفرة البرمجية؟
let user = { name: "John", go: function() { alert(this.name) } } (user.go)()
انتبه. هنالك فخ
الحل
سينتج عن تنفيذ الشيفرة السابقة خطأ!
جربها بنفسك:
let user = { name: "John", go: function() { alert(this.name) } } (user.go)() // error!
لا تعطينا رسالة الخطأ في معظم المتصفحات فكرة عن الخطأ الّذي حدث.
يظهر الخطأ بسبب فقدان فاصلة منقوطة بعد user = {...}
.
إذ لا تُدرج لغة جافاسكربت تلقائيًا فاصلة منقوطة قبل قوس (user.go)()
، لذلك تُقرأ الشيفرة البرمجية هكذا:
let user = { go:... }(user.go)()
ثم يمكننا أن نرى أيضًا أن مثل هذا التعبير المشترك هو من الناحية التركيبية استدعاء للكائن { go: ... }
كدالة مع الوسيط (user.go)
. ويحدث ذلك أيضًا على نفس السطر مع let user
، لذلك إن الكائن user
لم يُعرّف حتى الآن، ومن هنا ظهر الخطأ.
إذا أدخلنا الفاصلة المنقوطة، فسيكون كل شيء على ما يرام:
let user = { name: "John", go: function() { alert(this.name) } }; (user.go)() // John
يرجى ملاحظة أن الأقواس حول (user.go)
لا تفعل شيئًا مميزًا هنا. وهي عادةً تعيد ترتيب العمليات، ولكن هنا تعمل النقطة .
أولًا على أي حال، لذلك ليس هناك أي تأثير لها. الشيء الوحيد المهم هو الفاصلة المنقوطة.
اشرح قيمة this
الأهمية: 3
في الشيفرة أدناه، نعتزم استدعاء التابع obj.go()
لأربع مرات على متتالية.
ولكن لماذا الندائين (1)
و(2)
يعملان بطريقة مختلفة عن الندائين (3)
و(4)
؟
let obj, method; obj = { go: function() { alert(this); } }; obj.go(); // (1) [object Object] (obj.go)(); // (2) [object Object] (method = obj.go)(); // (3) undefined (obj.go || obj.stop)(); // (4) undefined
الحل
إليك تفسير ما حدث.
-
هذا هو الطريقة العادية لاستدعاء تابع الكائن.
-
نفس الأمر يتكرر هنا، الأقواس لا تغير ترتيب العمليات هنا، النقطة هي الأولى على أي حال.
-
هنا لدينا استدعاء أكثر تعقيدًا
(expression).method()
. يعمل الاستدعاء كما لو كانت مقسمة على سطرين:f = obj.go; // يحسب نتيجة التعبير f(); // يستدعي ما لدينا
هنا تُنفذ
f()
كدالة، بدونthis
. -
مثل ما حدث مع الحالة
(3)
على يسار النقطة.
لدينا تعبير.
لشرح سلوك (3)
و(4)
نحتاج إلى أن نتذكر أن توابع الوصول (accessors) للخاصيات (النقطة أو الأقواس المربعة) تُعيد قيمة النوع المرجعي.
أي عملية نطبقها عليها باستثناء استدعاء التابع (مثل عملية الإسناد =
أو ||
) تحولها إلى قيمة عادية، والّتي لا تحمل المعلومات التي تسمح لها بتعيين قيمة this
.
ترجمة -وبتصرف- للفصل Reference Type من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.