JavaScript: ما معنى ذلك؟

قد يكون فهم قيمة this أمرًا صعبًا في JavaScript، وإليك كيفية إجراء ذلك...

إنّ this في JavaScript هو موضوع العديد من النكات، ويعود السبب في ذلك إلى أنّه معقّد جدًا. مع ذلك، لاحظت أنّ المطوّرين ينفّذون إجراءات أكثر تعقيدًا خاصة بالنطاق لتجنُّب التعامل مع this. إذا لم تكن متأكدًا من this، نأمل أن تكون هذه المعلومات مفيدة. هذا دليل this.

سأبدأ بالحالة الأكثر تحديدًا، وسأنتهي بالحالة الأقل تحديدًا. تتشابه هذه المقالة إلى حد ما مع if (…) … else if () … else if (…) … كبيرة، لذا يمكنك الانتقال مباشرةً إلى القسم الأول الذي يطابق الرمز الذي تنظر إليه.

  1. إذا تم تعريف الدالة على أنّها دالة سهم
  2. بخلاف ذلك، إذا تم استدعاء الدالة/الفئة باستخدام new
  3. بخلاف ذلك، إذا كانت الدالة لها قيمة this "مرتبطة"
  4. بخلاف ذلك، إذا تم ضبط this في وقت الاتصال
  5. بخلاف ذلك، إذا تم استدعاء الدالة من خلال عنصر رئيسي (parent.func())
  6. وإلا، إذا كانت الدالة أو النطاق الرئيسي في الوضع الصارم
  7. بخلاف ذلك

إذا تم تعريف الدالة على أنّها دالة سهمية:

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.

نشكر ماتياس بينينز وإنغفار ستيبانيان وتوماس شتاينر على مراجعتهم.