عند تمرير أساليب كائن كرد اتصال، على سبيل المثال إلى setTimeout
، توجد مشكلة معروفة: "losing this
".
في هذا الفصل سنرى طرق إصلاحه.
لقد رأينا بالفعل أمثلة على فقدان this
. بمجرد تمرير الطريقة في مكان ما بشكل منفصل عن الكائن، يتم فقدان this
.
وإليك كيف يمكن أن يحدث مع setTimeout
:
السماح للمستخدم = { الاسم الأول: "جون"، قل مرحبا () { تنبيه(`مرحبا، ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // مرحبًا، غير محدد!
كما نرى، لا يُظهر الناتج "John" كـ this.firstName
، بل undefined
!
ذلك لأن setTimeout
حصل على الدالة user.sayHi
، بشكل منفصل عن الكائن. يمكن إعادة كتابة السطر الأخير على النحو التالي:
Let f = user.sayHi; setTimeout(f, 1000); // سياق المستخدم المفقود
تعد الطريقة setTimeout
in-browser خاصة بعض الشيء: فهي تحدد this=window
لاستدعاء الوظيفة (بالنسبة إلى Node.js، يصبح this
كائن المؤقت، لكنه لا يهم حقًا هنا). لذا بالنسبة إلى this.firstName
فإنه يحاول الحصول على window.firstName
، وهو غير موجود. في حالات أخرى مماثلة، عادةً ما يصبح this
undefined
.
المهمة نموذجية تمامًا – نريد تمرير طريقة الكائن إلى مكان آخر (هنا – إلى المجدول) حيث سيتم استدعاؤها. كيف تتأكد من أنه سيتم استدعاؤه في السياق الصحيح؟
الحل الأبسط هو استخدام وظيفة الالتفاف:
السماح للمستخدم = { الاسم الأول: "جون"، قل مرحبا () { تنبيه(`مرحبا، ${this.firstName}!`); } }; setTimeout(وظيفة() { user.sayHi(); // مرحبا جون! }, 1000);
الآن يعمل، لأنه يستقبل user
من البيئة المعجمية الخارجية، ثم يستدعي الطريقة بشكل طبيعي.
نفس الشيء ولكن بشكل أقصر:
setTimeout(() => user.sayHi(), 1000); // مرحبا جون!
يبدو الأمر جيدًا، ولكن تظهر ثغرة أمنية طفيفة في بنية التعليمات البرمجية الخاصة بنا.
ماذا لو قام user
بتغيير القيمة قبل تشغيل setTimeout
(هناك تأخير لمدة ثانية واحدة!)؟ ثم، فجأة، سوف يستدعي الكائن الخطأ!
السماح للمستخدم = { الاسم الأول: "جون"، قل مرحبا () { تنبيه(`مرحبا، ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ... تتغير قيمة المستخدم خلال ثانية واحدة المستخدم = { sayHi() { تنبيه("مستخدم آخر في setTimeout!"); } }; // مستخدم آخر في setTimeout!
الحل التالي يضمن أن مثل هذا الشيء لن يحدث.
توفر الوظائف ربط طريقة مضمنًا يسمح بإصلاح this
.
بناء الجملة الأساسي هو:
// سيأتي بناء الجملة الأكثر تعقيدًا لاحقًا LetboundFunc = func.bind(context);
نتيجة func.bind(context)
هي "كائن غريب" يشبه الوظيفة الخاصة، ويمكن استدعاؤه كدالة وتمرير استدعاء func
بشفافية لإعداد this=context
.
بمعنى آخر، استدعاء boundFunc
يشبه func
مع إصلاح this
.
على سبيل المثال، هنا funcUser
يمرر استدعاء func
مع this=user
:
السماح للمستخدم = { الاسم الأول: "جون" }; وظيفة وظيفة () { تنبيه (هذا. الاسم الأول)؛ } دع funcUser = func.bind(user); funcUser(); // جون
هنا func.bind(user)
باعتباره "متغيرًا مرتبطًا" func
، مع إصلاح this=user
.
يتم تمرير جميع الوسائط إلى func
الأصلية "كما هي"، على سبيل المثال:
السماح للمستخدم = { الاسم الأول: "جون" }; وظيفة وظيفة (عبارة) { تنبيه (عبارة + '، ' + this.firstName)؛ } // ربط هذا للمستخدم دع funcUser = func.bind(user); funcUser("مرحبا"); // مرحبًا، جون (تم تمرير الوسيطة "Hello"، وهذا = المستخدم)
الآن دعونا نحاول باستخدام طريقة الكائن:
السماح للمستخدم = { الاسم الأول: "جون"، قل مرحبا () { تنبيه(`مرحبا، ${this.firstName}!`); } }; Let sayHi = user.sayHi.bind(user); // (*) // يمكن تشغيله بدون كائن sayHi(); // مرحبا جون! setTimeout(sayHi, 1000); // مرحبا جون! // حتى لو تغيرت قيمة المستخدم خلال ثانية واحدة يستخدم // sayHi القيمة المرتبطة مسبقًا والتي تشير إلى كائن المستخدم القديم المستخدم = { sayHi() { تنبيه("مستخدم آخر في setTimeout!"); } };
في السطر (*)
نأخذ الطريقة user.sayHi
ونربطها user
. إن sayHi
هي دالة "مرتبطة"، يمكن استدعاؤها بمفردها أو تمريرها إلى setTimeout
- لا يهم، سيكون السياق صحيحًا.
هنا يمكننا أن نرى أن الوسائط يتم تمريرها "كما هي"، ويتم إصلاح this
فقط عن طريق bind
:
السماح للمستخدم = { الاسم الأول: "جون"، قل (عبارة) { تنبيه(`${phrase}, ${this.firstName}!`); } }; دعنا نقول = user.say.bind(user); قل("مرحبا"); // مرحبا جون! ("مرحبا" يتم تمرير الوسيطة ليقول) قل("وداعا"); // وداعا جون! ("وداعا" تم تمريره ليقول)
طريقة الراحة: bindAll
إذا كان للكائن العديد من التوابع ونخطط لتمريرها بشكل فعال، فيمكننا ربطها جميعًا في حلقة:
لـ (السماح بإدخال المستخدم) { إذا (نوع المستخدم [مفتاح] == 'وظيفة') { user[key] = user[key].bind(user); } }
توفر مكتبات JavaScript أيضًا وظائف للربط الجماعي المريح، على سبيل المثال _.bindAll(object,methodNames) في lodash.
حتى الآن كنا نتحدث فقط عن ربط this
. دعونا نأخذها خطوة أخرى إلى الأمام.
يمكننا ربط ليس this
فقط، ولكن أيضا الحجج. ونادرا ما يتم ذلك، ولكن في بعض الأحيان يمكن أن يكون مفيدا.
بناء الجملة الكامل bind
:
Letbound = func.bind(context, [arg1], [arg2], ...);
يسمح بربط السياق بهذا this
وبدء وسيطات الوظيفة.
على سبيل المثال، لدينا دالة الضرب mul(a, b)
:
وظيفة مول (أ، ب) { العودة أ * ب؛ }
دعونا نستخدم bind
لإنشاء دالة double
على قاعدتها:
وظيفة مول (أ، ب) { العودة أ * ب؛ } Let double = mul.bind(null, 2); تنبيه(مزدوج(3)); // = مول (2، 3) = 6 تنبيه(مزدوج(4)); // = مول (2، 4) = 8 تنبيه(مزدوج(5)); // = مول (2، 5) = 10
يؤدي استدعاء mul.bind(null, 2)
إلى إنشاء دالة double
جديدة تمرر الاستدعاءات إلى mul
، مع تثبيت null
كسياق و 2
كوسيط أول. يتم تمرير الوسائط الإضافية "كما هي".
وهذا ما يسمى تطبيق الوظيفة الجزئية - نقوم بإنشاء وظيفة جديدة عن طريق إصلاح بعض معلمات الوظيفة الحالية.
يرجى ملاحظة أننا في الواقع لا نستخدم this
هنا. لكن bind
يتطلب ذلك، لذا يجب علينا وضع شيء مثل null
.
الوظيفة triple
في الكود أدناه تضاعف القيمة ثلاث مرات:
وظيفة مول (أ، ب) { العودة أ * ب؛ } Let Triple = mul.bind(null, 3); تنبيه(ثلاثي(3)); // = مول (3، 3) = 9 تنبيه(ثلاثي(4)); // = مول (3، 4) = 12 تنبيه(ثلاثي(5)); // = مول (3، 5) = 15
لماذا نقوم عادة بعمل دالة جزئية؟
الفائدة هي أنه يمكننا إنشاء دالة مستقلة باسم قابل للقراءة ( double
، triple
). يمكننا استخدامه وعدم تقديم الوسيطة الأولى في كل مرة حيث تم إصلاحها باستخدام bind
.
في حالات أخرى، يكون التطبيق الجزئي مفيدًا عندما تكون لدينا دالة عامة للغاية ونريد نسخة أقل عالمية منها من أجل الراحة.
على سبيل المثال، لدينا وظيفة send(from, to, text)
. بعد ذلك، داخل كائن user
قد نرغب في استخدام متغير جزئي منه: sendTo(to, text)
الذي يُرسل من المستخدم الحالي.
ماذا لو أردنا إصلاح بعض الحجج، ولكن ليس this
؟ على سبيل المثال، لطريقة الكائن.
bind
الأصلي لا يسمح بذلك. لا يمكننا حذف السياق والانتقال إلى الحجج.
ولحسن الحظ، يمكن بسهولة تنفيذ دالة partial
لربط الوسائط فقط.
مثله:
وظيفة جزئية (func، ...argsBound) { وظيفة الإرجاع (...الوسائط) {// (*) return func.call(this, ...argsBound, ...args); } } // الاستخدام: السماح للمستخدم = { الاسم الأول: "جون"، قل (الوقت، العبارة) { تنبيه(`[${time}] ${this.firstName}: ${phrase}!`); } }; // أضف طريقة جزئية بوقت محدد user.sayNow = جزئي(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("مرحبا"); // شيء مثل: // [10:00] جون: مرحبًا!
نتيجة الاستدعاء partial(func[, arg1, arg2...])
هي غلاف (*)
يستدعي func
مع:
نفس this
كما هو الحال (بالنسبة إلى user.sayNow
اتصل به user
)
ثم يعطيها ...argsBound
– وسيطات من الاستدعاء partial
( "10:00"
)
ثم يعطيها ...args
- الوسائط المعطاة للغلاف ( "Hello"
)
من السهل جدًا القيام بذلك باستخدام صيغة الانتشار، أليس كذلك؟
يوجد أيضًا تطبيق _.جزئي جاهز من مكتبة lodash.
تُرجع الطريقة func.bind(context, ...args)
"متغيرًا مرتبطًا" للدالة func
التي تعمل على إصلاح السياق this
والوسيطات الأولى إذا تم تقديمها.
عادةً ما نطبق bind
لإصلاح this
على طريقة كائن، حتى نتمكن من تمريره إلى مكان ما. على سبيل المثال، setTimeout
.
عندما نصلح بعض وسائط دالة موجودة، فإن الدالة الناتجة (الأقل عمومية) تسمى مطبقة جزئيًا أو جزئية .
تكون الأجزاء الجزئية ملائمة عندما لا نريد تكرار نفس الحجة مرارًا وتكرارًا. كما لو كان لدينا دالة send(from, to)
، ويجب أن تكون from
هي نفسها دائمًا لمهمتنا، فيمكننا الحصول على جزء جزئي والاستمرار في ذلك.
الأهمية: 5
ماذا سيكون الناتج؟
الدالة و() { تنبيه(هذا); // ؟ } السماح للمستخدم = { ز: f. ربط (خالية) }; user.g();
الجواب: null
.
الدالة و() { تنبيه(هذا); // باطل } السماح للمستخدم = { ز: f. ربط (خالية) }; user.g();
سياق الدالة المرتبطة ثابت للغاية. لا توجد طريقة لمزيد من التغيير.
لذلك، حتى أثناء تشغيلنا user.g()
، يتم استدعاء الوظيفة الأصلية باستخدام this=null
.
الأهمية: 5
هل يمكننا تغيير this
عن طريق ربط إضافي؟
ماذا سيكون الناتج؟
الدالة و() { تنبيه(هذا.اسم); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); و ()؛
الجواب : جون .
الدالة و() { تنبيه(هذا.اسم); } f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); و ()؛ // جون
يتذكر كائن الدالة المرتبطة الغريبة الذي تم إرجاعه بواسطة f.bind(...)
السياق (والوسيطات إذا تم توفيرها) فقط في وقت الإنشاء.
لا يمكن إعادة ربط الدالة.
الأهمية: 5
هناك قيمة في خاصية الوظيفة. هل سيتغير بعد bind
؟ لماذا، أو لماذا لا؟
الدالة sayHi() { تنبيه(هذا.الاسم); } sayHi.test = 5; اسمحوا ملزمة = sayHi.bind({ الاسم: "جون" }); تنبيه(bound.test); // ماذا سيكون الناتج؟ لماذا ؟
الجواب : undefined
.
نتيجة bind
هي كائن آخر. ليس لديها خاصية test
.
الأهمية: 5
يجب أن يتحقق استدعاء askPassword()
في الكود أدناه من كلمة المرور ثم يتصل بـ user.loginOk/loginFail
اعتمادًا على الإجابة.
لكنه يؤدي إلى خطأ. لماذا؟
قم بإصلاح الخط المميز حتى يبدأ كل شيء في العمل بشكل صحيح (لا يجوز تغيير الخطوط الأخرى).
وظيفة AskPassword (حسنًا، فشل) { دع كلمة المرور = موجه ("كلمة المرور؟"، '')؛ إذا (كلمة المرور == "rockstar") ok(); وإلا فشل ()؛ } السماح للمستخدم = { الاسم: "جون"، تسجيل الدخولموافق () { تنبيه(`${this.name} تم تسجيل الدخول`); }, فشل تسجيل الدخول () { تنبيه(`${this.name} فشل في تسجيل الدخول`); }, }; AskPassword(user.loginOk, user.loginFail);
يحدث الخطأ لأن askPassword
يحصل على وظائف loginOk/loginFail
بدون الكائن.
عندما يستدعيهم، فإنهم بطبيعة الحال يفترضون this=undefined
.
دعونا bind
السياق:
وظيفة AskPassword (حسنًا، فشل) { دع كلمة المرور = موجه ("كلمة المرور؟"، '')؛ إذا (كلمة المرور == "rockstar") ok(); وإلا فشل ()؛ } السماح للمستخدم = { الاسم: "جون"، تسجيل الدخولموافق () { تنبيه(`${this.name} تم تسجيل الدخول`); }, فشل تسجيل الدخول () { تنبيه(`${this.name} فشل في تسجيل الدخول`); }, }; AskPassword(user.loginOk.bind(user), user.loginFail.bind(user));
الآن يعمل.
الحل البديل يمكن أن يكون:
//... AskPassword(() => user.loginOk(), () => user.loginFail());
عادةً ما يعمل هذا أيضًا ويبدو جيدًا.
إنها أقل موثوقية بعض الشيء على الرغم من أنه في المواقف الأكثر تعقيدًا حيث قد يتغير متغير user
بعد استدعاء askPassword
، ولكن قبل أن يجيب الزائر ويتصل () => user.loginOk()
.
الأهمية: 5
المهمة عبارة عن متغير أكثر تعقيدًا قليلاً لإصلاح وظيفة تفقد "هذا".
تم تعديل كائن user
. الآن بدلاً من وظيفتين loginOk/loginFail
، أصبح لديه وظيفة واحدة user.login(true/false)
.
ما الذي يجب أن نمرر فيه askPassword
في الكود أدناه، بحيث يستدعي user.login(true)
على أنه ok
و user.login(false)
على أنه fail
؟
وظيفة AskPassword (حسنًا، فشل) { دع كلمة المرور = موجه ("كلمة المرور؟"، '')؛ إذا (كلمة المرور == "rockstar") ok(); وإلا فشل ()؛ } السماح للمستخدم = { الاسم: "جون"، تسجيل الدخول (النتيجة) { تنبيه( this.name + (النتيجة ? 'تم تسجيل الدخول' : 'فشل في تسجيل الدخول')); } }; AskPassword(?, ?); // ؟
يجب أن تعدل تغييراتك الجزء المميز فقط.
إما أن تستخدم دالة مجمعة، وسهمًا للإيجاز:
AskPassword(() => user.login(true), () => user.login(false));
الآن يحصل على user
من المتغيرات الخارجية ويقوم بتشغيله بالطريقة العادية.
أو أنشئ دالة جزئية من user.login
تستخدم user
كسياق ولها الوسيطة الأولى الصحيحة:
AskPassword(user.login.bind(user, true), user.login.bind(user, false));