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);
  };
}

هذا النمط مفيد حقًا عند استخدام طرق مثيل مثل أدوات معالجة الأحداث في المكونات (مثل مكونات التفاعل أو مكونات الويب).

قد يبدو ما سبق وكأنّه يعطّل قاعدة "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. هل مِن أسئلة؟ هل هناك شيء فاتني؟ لا تتردد في إرسال تغريدات لي.

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