الكلمة المحجوزة this في JavaScript

من موسوعة حسوب

الكلمة المحجوزة this لها سلوكٌ في JavaScript يختلف عن بقية لغات البرمجة، وهنالك اختلافاتٌ في سلوكها بين النمط strict و non-strict.

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

أضافت ES5 الدالة bind التي تضبط قيمة this داخل الدالة بغض النظر عن طريقة استدعائها، وأضافت ES2015 الدوال السهمية (arrow functions) التي تُوفِّر ربطًا خاصًا بها للكلمة المحجوزة this.

البنية العامة

this

السياق العام

تُشير الكلمة المحجوزة this في سياق التنفيذ العام (أي خارج جميع الدوال) إلى الكائن العام (global object) سواءً كان التنفيذ في النمط strict أو non-strict.

// الكائن العام في المتصفحات هو window
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "HSOUB";
console.log(window.b); // "HSOUB"
console.log(b); // "HSOUB"

سياق الدوال

تختلف قيمة الكلمة المحجوزة this داخل الدوال اعتمادًا على طريقة استدعاء الدالة.

استدعاء بسيط

المثال الآتي لن يُنفَّذ في نمط strict، ولأنَّ قيمة this ليست مضبوطةً باستخدام call، فستُشير إلى الكائن العام (global object) والذي هو الكائن window في المتصفح.

function f1() {
  return this;
}

// متصفح
f1() === window; // true 

// Node
f1() === global; // true

أما قيمة this في نمط strict فستبقى مساويةً إلى القيمة التي ضُبِطَت لها عند دخولها إلى سيبق التنفيذ، أي ستكون قيمة this في المثال الآتي مساويةً إلى undefined:

function f2() {
  'use strict'; // strict mode
  return this;
}

f2() === undefined; // true

أي لو كانت قيمة this غير مُعرَّفة في سياق التنفيذ فستبقى undefined، وذلك في النمط strict.

ملاحظة: يجب أن تكون قيمة this في المثال السابق مساويةً إلى undefined، ذلك لأنَّ الدالة f2 اُستدعيَت مباشرةً ولم تستدعَ كخاصية (property) لكائنٍ ما (مثلًا: window.f2()‎)؛ لاحظ أنَّ هذه الميزة لم تكن مُطبّقةً في بعض المتصفحات عندما بدأت بدعم النمط strict، وكنتيجة لذلك قد تجدها تعيد الكائن window خطأً.

لتمرير قيمة إلى this من سياقٍ لآخر فيمكن استخدام الدالة call أو apply، لاحظ كيف أنشأنا كائنًا باسم obj ووضعنا فيه الخاصية a بقيمة معيّنة، ثم أنشأنا الخاصية a في الكائن العام واستدعينا الدالة بثلاث طرائق مختلفة:

// الكائن الذي سيُمرَّر إلى الدالة
var obj = {a: 'Custom'};

// خاصية في الكائن العام
var a = 'Global';

function whatsThis(arg) {
  return this.a;  // هذه القيمة تختلف حسب طريقة استدعاء الدالة
}

whatsThis(); // 'Global'
whatsThis.call(obj); // 'Custom'
whatsThis.apply(obj); // 'Custom'

نجد أنَّه عندما تستخدم دالةٌ ما الكلمةَ المحجوزةَ this في جسمها، فستكون قيمتها مرتبطةً بالكائن الذي مُرِّر إليها باستخدام الدالة call أو apply والتي ترثها كل الدوال من سلسلة prototype (عبر الخاصية Function.prototype).

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// سيُمرَّ أوّل معامل إلى الدالة كقيمة this
// أما المعاملات التي تليه فستُمرَّر كوسائط إلى الدالة
add.call(o, 5, 7); // 16

// سيُمرَّ أوّل معامل إلى الدالة كقيمة this
// أما عناصر المصفوفة التي تليه فستُمرَّر كوسائط إلى الدالة
add.apply(o, [10, 20]); // 34

لاحظ أنَّه عند استخدام call و apply إذا كانت القيمة المُمرَّرة كقيمة للكلمة المحجوزة this ليست كائنًا، فستتم محاولة تحويلها إلى كائن باستخدام العملية ToObject، فلو مررنا قيمةً أوليّةً (primitive) مثل 7 أو 'foo'، فستحوَّل إلى كائن باستخدام الدالة البانية المناسبة، أي أنَّ الرقم 7 سيُحوَّل إلى كائن باستخدام new Number(7)‎ والسلسلة النصية 'foo' ستحوَّل إلى كائن باستخدام ‎new String('foo')‎:

function bar() {
  console.log(Object.prototype.toString.call(this));
}

bar.call(7); // [object Number]
bar.call('foo'); // [object String]

الدالة bind

أضافت ECMAScript 5 الدالة Function.prototype.bind، وسيؤدي تنفيذ f.bind(someObject)‎ إلى إنشاء دالة جديدة لها نفس جسم ومجال الدالة f لكن ستكون قيمة الكلمة المحجوزة this في الدالة الجديدة مرتبطةً بالكائن المُمرَّر كأوّل وسيط للدالة bind، بغض النظر عن كيفية استخدام الدالة:

function f() {
  return this.a;
}

var g = f.bind({a: 'azerty'});
console.log(g()); // azerty

var h = g.bind({a: 'yoo'}); // لا يمكن تكرار العملية أكثر من مرة
console.log(h()); // azerty

var o = {a: 37, f: f, g: g, h: h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

الدوال السهمية

في الدوال السهمية (arrow functions) ستأخذ this قيمتها من سياق التنفيذ، أي أنها ستساوي الكائن العام إذا كانت الشيفرة في المجال العام:

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true

ملاحظة: إذا مررنا كائنًا إلى الدالة call أو bind أو apply عند استدعاء دالة سهمية، فلن تُسنَد قيمته إلى this وسيُهمَل، لكن ستُمرَّر بقيمة الوسائط إلى الدالة.

var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// call
console.log(foo.call(obj) === globalObject); // true

// bind
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

بغض النظر عن طريقة استدعاء الدالة foo، فستبقى الكلمة المحجوزة this فيها تابعةً لسياق الإنشاء (وهي تساوي الكائن العام في المثال السابق)، وسينطبق المثال على الدوال السهمية المُنشَأة داخل الدوال الأخرى، إذ ستبقى الكلمة المحجوزة this تُشير إلى السياق الذي أُنشِئت الدالة فيه:

var obj = {bar: function() {
                    var x = (() => this);
                    return x;
                  }
          };

var fn = obj.bar();

console.log(fn() === obj); // true

var fn2 = obj.bar;
console.log(fn2()() == window); // true

في المثال السابق، أعادت الدالة (التي سنطلق عليها اسم الدالة المجهولة A) دالةً أخرى (سنطلق عليها اسم الدالة المجهولة B) التي هي دالةٌ سهميةٌ، والنتيجة هي ربط this في الدالة B إلى قيمة this للخاصية obj.bar (الدالة A) عند استدعائها.

استخدام this في دالة تابعة لكائن

عند استدعاء دالة تابعة لكائن، فستكون قيمة this فيها مضبوطةٌ إلى الكائن الذي استدعاها؛ ففي المثال الآتي عندما ستُستدعى الدالة o.f()‎ فستُشير الكلمة المحجوزة this فيها إلى الكائن o:

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f()); // 37

لاحظ أنَّ هذا السلوك لن يتأثر بكيفية أو مكان إنشاء الدالة، ففي المثال السابق عرِّفنا الدالة على الخاصية f عند تهيئة الكائن o، لكن يمكنك تعريف الدالة أولًا ثم ربطها إلى الخاصية o.f ولن يختلف أثرها أبدًا:

var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f()); // 37

المثال السابق يوضِّح أنَّ كل ما يهم هو أن تُستدعى الدالة من الخاصية f للكائن o. لاحظ أنَّ this سترتبط بأقرِّب كائن، ففي المثال الآتي سنستدعي الدالة g التابعة للكائن o.b، وستُشير this فيها إلى الكائن o.b، ولن يهم أنَّ الكائن o.b هو عضوٌ في الكائن o:

o.b = {g: independent, prop: 42};
console.log(o.b.g()); // 42

استخدام this في سلسلة prototype

إذا كانت الدالة موجودةً في سلسلة prototype فستشير this إلى الكائن الذي اُستدعيت الدالة عليها، كما لو كانت الدالةُ تابعةً للكائن مباشرةً:

var o = {f: function() { return this.a + this.b; }};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

الكائن المُسنَد إلى المتغير p في المثال السابق لا يملك خاصيةً تابعةً له مباشرةً باسم f، وإنما يرثها من سلسلة prototype، وعند استدعاء الدالة f مع الكائن p، فستشير الكلمة المحجوزة this إلى الكائن p؛ لاحظ أنَّ هذا السلوك مثير للاهتمام في طريقة الوراثة عبر سلسلة prototype في JavaScript.

استخدام this مع الدوال البانية

عند استخدام إحدى الدوال كدالة بانية (constructor، مع الكلمة المحجوزة new)، فسترتبط الكلمة المحجوزة this مع الكائن الذي سيُنشَأ.

وصحيحٌ أنَّ السلوك الافتراضي للدالة البانية هو إعادة الكائن المُشار إليها عبر الكلمة المحجوزة this، لكن يمكنها أن تُعيد كائنًا آخر.

function C() {
  this.a = 37;
}

var o = new C();
console.log(o.a); // 37


function C2() {
  this.a = 37;
  return {a: 38};
}

o = new C2();
console.log(o.a); // 38

لاحظ كيف أُعيد كائنٌ أثناء الإنشاء في المثال الأخير (C2)، وأُهمِلَ الكائن الذي كانت تُشير this إليه.

استخدام this للتعامل مع الأحداث في DOM

عندما تُستخدَم دالةٌ لمعالجة أحد الأحداث، فستُشير الكلمة المحجوزة this إلى الكائن الذي أدى إلى إطلاق الحدث:

function bluify(e) {
  // الشرط الآتي محقق دومًا
  console.log(this === e.currentTarget); 
  // سيُحقَّق الشرط الآتي إذا كان
  // currentTarget === target
  console.log(this === e.target);
  this.style.backgroundColor = '#A5D9F3';
}

// الحصول على قائمة بجميع عناصر المستند
var elements = document.getElementsByTagName('*');

// إضافة الدالة للاستماع إلى حدث النقر على العنصر
for (var i = 0; i < elements.length; i++) {
  elements[i].addEventListener('click', bluify, false);
}

استخدام this للتعامل مع الأحداث ضمن عناصر HTML

إذا استخدمنا الأحداث ضمن عناصر HTML، فستُضبَط قيمة this إلى عنصر DOM الذي أدى إلى إطلاق الحدث. جرِّب الضغط على عنصر <button> الآتي:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

نافذة التنبيه السابقة ستُظهِر الكلمة button. لاحظ أنَّ الشيفرة الموجودة في أعلى مستوى ستكون قيمة this فيها كما سبق:

<button onclick="alert((function() { return this; })());">
  Show inner this
</button>

في هذه الحالة، ستُشير الكلمة المحجوزة this داخل الدالة إلى الكائن العام.

دعم المتصفحات

الميزة Chrome Firefox Internet Explorer Opera Safari
الدعم الأساسي نعم نعم نعم نعم نعم

مصادر ومواصفات