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 من أنواع معيّنة، ويؤدي ما سبق إلى إيقاف هذا الربط. لتحقيق ما سبق بشكل صحيح:

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.

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