قد يكون فهم قيمة this
أمرًا صعبًا في JavaScript، وإليك كيفية إجراء ذلك...
إنّ this
في JavaScript هو موضوع العديد من النكات، ويعود السبب في ذلك إلى أنّه معقّد جدًا.
مع ذلك، لاحظت أنّ المطوّرين ينفّذون إجراءات أكثر تعقيدًا خاصة بالنطاق لتجنُّب التعامل مع this
. إذا لم تكن متأكدًا من this
، نأمل أن تكون هذه المعلومات مفيدة. هذا دليل this
.
سأبدأ بالحالة الأكثر تحديدًا، وسأنتهي بالحالة الأقل تحديدًا. تتشابه هذه المقالة إلى حد ما مع if (…) … else if () … else if (…) …
كبيرة، لذا يمكنك الانتقال مباشرةً إلى القسم الأول الذي يطابق الرمز الذي تنظر إليه.
- إذا تم تعريف الدالة على أنّها دالة سهم
- بخلاف ذلك، إذا تم استدعاء الدالة/الفئة باستخدام
new
- بخلاف ذلك، إذا كانت الدالة لها قيمة
this
"مرتبطة" - بخلاف ذلك، إذا تم ضبط
this
في وقت الاتصال - بخلاف ذلك، إذا تم استدعاء الدالة من خلال عنصر رئيسي (
parent.func()
) - وإلا، إذا كانت الدالة أو النطاق الرئيسي في الوضع الصارم
- بخلاف ذلك
إذا تم تعريف الدالة على أنّها دالة سهمية:
const arrowFunction = () => {
console.log(this);
};
في هذه الحالة، تكون قيمة this
دائمًا مساوية لقيمة this
في النطاق الرئيسي:
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
إنّ دوالّ الأسهم رائعة لأنّه لا يمكن تغيير القيمة الداخلية لـ this
، بل هي دائمًا متطابقة
مع القيمة الخارجية لـ this
.
أمثلة أخرى
باستخدام دوالّ الأسهم، لا يمكن تغيير قيمة this
باستخدام bind
:
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
باستخدام الدوال السهمية، لا يمكن تغيير قيمة this
باستخدام call
أو apply
:
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
باستخدام دوالّ الأسهم، لا يمكن تغيير قيمة this
من خلال استدعاء الدالة كعضو في
عنصر آخر:
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
باستخدام دوالّ الأسهم، لا يمكن تغيير قيمة this
من خلال استدعاء الدالة كدالة
لإنشاء عنصر:
// TypeError: arrowFunction is not a constructor
new arrowFunction();
طرق المثيل "المرتبطة"
باستخدام طرق المثيلات، إذا أردت التأكّد من أنّ this
تشير دائمًا إلى مثيل الفئة، فإنّ أفضل طريقة هي استخدام الدوال السهمية وحقول الفئة:
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
يكون هذا النمط مفيدًا جدًا عند استخدام طرق المثيل كمستمعين للأحداث في المكوّنات (مثل مكوّنات React أو مكوّنات الويب).
قد يبدو لك أنّ الرمز أعلاه يخالف القاعدة "this
ستكون متطابقة مع this
في نطاق العنصر الرئيسي"، ولكنه يصبح منطقيًا إذا اعتبرت حقول الفئة سكرًا نحويًا لضبط العناصر في الدالة المُنشئة:
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
تتضمن الأنماط البديلة ربط دالة حالية في الدالة الانشائية، أو تعيين الدالة في الدالة الانشائية. إذا لم تتمكّن من استخدام حقول الفئة لسبب ما، يُعدّ تعيين الدوال في المنشئ بديلاً معقولاً:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
بخلاف ذلك، إذا تمّ استدعاء الدالة/الفئة باستخدام new
:
new Whatever();
سيؤدي الإجراء أعلاه إلى استدعاء Whatever
(أو دالة الإنشاء الخاصة بها إذا كانت فئة) مع ضبط this
على
نتيجة Object.create(Whatever.prototype)
.
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
وينطبق هذا أيضًا على دوال الإنشاء ذات النمط القديم:
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
أمثلة أخرى
عند استدعاء new
، لا يمكن تغيير قيمة this
باستخدام bind
:
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
عند الاستدعاء باستخدام new
، لا يمكن تغيير قيمة this
من خلال استدعاء الدالة كعضو في كائن آخر:
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
في الحالات الأخرى، إذا كان للدالة قيمة this
"مرتبطة":
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
عند استدعاء الدالة boundFunction
، سيتم تمرير قيمة this
إلى الكائن bind
(boundObject
).
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
أمثلة أخرى
عند استدعاء دالة مقيّدة، لا يمكن تغيير قيمة this
باستخدام call
أو
apply
:
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
عند استدعاء دالة مرتبطة، لا يمكن تغيير قيمة this
عن طريق استدعاء الدالة باعتبارها عضوًا في كائن آخر:
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
بخلاف ذلك، في حال ضبط this
في وقت المكالمة:
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
قيمة this
هي العنصر الذي تم تمريره إلى call
/apply
.
يتم ضبط this
على قيمة أخرى من خلال عناصر مثل مستمعي أحداث DOM، ويمكن أن يؤدي استخدامها
إلى إنشاء رمز برمجي يصعب فهمه:
element.addEventListener('click', function (event) { // Logs `element`, since the DOM spec sets `this` to // the element the handler is attached to. console.log(this); });
أتجنب استخدام this
في الحالات الموضّحة أعلاه، وأستخدِم بدلاً من ذلك:
element.addEventListener('click', (event) => { // Ideally, grab it from a parent scope: console.log(element); // But if you can't do that, get it from the event object: console.log(event.currentTarget); });
بخلاف ذلك، إذا تم استدعاء الدالة من خلال عنصر رئيسي (parent.func()
):
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
في هذه الحالة، يتمّ استدعاء الدالة كعضو في obj
، لذا سيكون this
هو obj
. ويحدث ذلك في وقت الاتصال، لذلك يتعطّل الرابط إذا تم استدعاء الدالة بدون الكائن الأصلي لها أو باستخدام كائن رئيسي مختلف:
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
القيمة someMethod() === obj
خاطئة لأنّ someMethod
لم يتم استدعاؤه كعضو في obj
. قد تكون
واجهت هذه المشكلة عند محاولة إجراء ما يلي:
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
ينقطع هذا لأن تنفيذ querySelector
يتعامل مع قيمة this
الخاصة به ويتوقع أن يكون عقدة DOM من نوع DOM، وأن ما سبق ذكره يقطع هذا الاتصال. لتحقيق ما سبق بشكل صحيح:
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
معلومة مفيدة: لا تستخدم بعض واجهات برمجة التطبيقات this
داخليًا. تم تغيير طرق وحدة التحكّم، مثل console.log
، لتجنّب مراجع this
، لذا لا يلزم ربط log
بـ console
.
بخلاف ذلك، إذا كانت الدالة أو النطاق الرئيسي في الوضع المتشدد:
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
في هذه الحالة، تكون قيمة this
غير محدّدة. ولا يلزم استخدام 'use strict'
في الدالة إذا كان النطاق الرئيسي في الوضع المتشدد (وكانت جميع الوحدات في الوضع المتشدد).
غير ذلك:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
في هذه الحالة، تكون قيمة this
هي نفسها قيمة globalThis
.
أخيرًا!
هذا كل ما في الأمر! هذا كل ما أعرفه عن this
. هل مِن أسئلة؟ هل هناك شيء فاتني؟ يمكنك
مراسلتي على Twitter.
نشكر ماتياس بينينز وإنغفار ستيبانيان وتوماس شتاينر على مراجعتهم.