this در جاوااسکریپت را بهتر بشناسیم

کلیدواژه‌ی this در جاوااسکریپت یکی از مفهوم‌هایی است که باعث سردرگمی مبتدیان این زبان می‌شود. شاید یکی از دلایل این موضوع این باشد که کلیدواژه‌ی this در جاوااسکریپت، در مقایسه با زبان‌های برنامه‌نویسی دیگر اندکی متفاوت است. از طرفی یادگیری این کلیدواژه بسیار ضروری است. چرا که برای خواندن و نوشتن کدهای حرفه‌ای و درک مفاهیم جاوااسکریپت همواره به آن نیاز خواهیم داشت. در این مقاله به بررسی این کلیدواژه و کاربردهای مختلفی که در زبان جاوااسکریپت دارد می‌پردازیم.

چم و خم استفاده از کلیدواژه‌ی this در جاوااسکریپت

this در محیط گلوبال

اگر کلیدواژه‌ی this در محیط گلوبال (به انگلیسی: Global)، یعنی خارج از توابع به کار برود، به آبجکت گلوبال اشاره خواهد کرد. از آن‌جایی که در مرورگرها آبجکت گلوبال همان window است، کلیدواژه‌ی this معادل window خواهد بود. به مثال زیر توجه کنید:

console.log(this === window); // true

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

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

با توجه به کد بالا، this مساوی window است و تعریف کردن متغیر بدون کلیدواژه (خط ۳) یا به صورت پراپرتی (به انگلیسی: Property) برای آبجکت this یا به صورت پراپرتی برای آبجکت window به یک معنی خواهند بود. در این‌جا ذکر این نکته می‌تواند مفید باشد که کلیدواژه‌ی globalThis در هر جایی فراخوانی شود، اعم از محیط گلوبال یا داخل توابع، مستقیما به آبجکت گلوبال که در مرورگرها آبجکت window است اشاره می‌کند.

this در محیط توابع

در محیط توابع، مقدار this به این بستگی دارد که تابع چطور فراخوانی شود:

فراخوانی معمولی توابع

در فراخوانی ساده‌ی توابع اگر در Strict mode نباشیم، this آبجکت گلوبال را برمی‌گرداند که در مرورگر‌ها آبجکت window است:

function f1() {
  return this;
}

// Dar Morur-gar:
f1() === window; // true

// Dar Nodejs
f1() === global; // true

با این حال اگر در حالت Strict mode باشیم، در فراخوانی ساده، که this مقداردهی نمی‌شود، this مقدار undefined را برمی‌گرداند:

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

f2() === undefined; // true

برای مقداردهی به this هنگام فراخوانی تابع، از متدهای call() و apply() استفاده می‌کنیم:

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

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

add.call(p, 5, 7); // 16

add.apply(p, [10, 20]); // 34

در خط ۷ از متد call() استفاده کرده‌ایم. در این متد اولین پارامتر آبجکتی است که می‌خواهیم this نماینده‌ی آن باشد (در اینجا آبجکت p). بقیه‌ی پارامترها، به عنوان آرگومان‌های تابع، به آن فرستاده می‌شوند. پس در خط ۷، this را نماینده‌ی آبجکت p کردیم و اعداد ۵ و ۷ را به ترتیب، برای آرگومان‌های c و d فرستادیم. استفاده از متد apply() نیز در این‌جا مشابه متد call() است؛ با این تفاوت که آرگومان‌ها به صورت آرایه به تابع فرستاده می‌شوند.

همچنین در حالت non-strict اگر مقداری که به this اطلاق می‌کنیم از نوع آبجکت نباشد، جاوااسکریپت تلاش می‌کند آن را به آبجکت تبدیل کند. بنابراین اگر مقادیر ابتدایی (به انگلیسی: primitive) مانند عدد ۷ یا رشته‌ی 'foo' به this اطلاق شوند، این مقادیر به آبجکت تبدیل می‌شوند. عدد ۷ مانند حالتی که به شکل 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

در نسخه‌ی ۵ اکمااسکریپت، Function.prototype.bind() معرفی شد. استفاده از f.bind(sth) تابعی می‌سازد که بدنه و حوزه‌ی (به انگلیسی: scope) تابع f را دارد. اما آبجکت sth برای همیشه به this استفاده شده داخل تابع f می‌چسبد و از این تابع در هر شرایط و به هر نوعی استفاده کنیم، this آن تغییری نخواهد داشت.

function f() {
  return this.a;
}

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

var h = g.bind({a: 'yoo'}); // bind tanha 1 baar kaar mikonad!
console.log(h()); // azerty

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

this در توابع فِلِش

هنگام تعریف توابع فلش (به انگلیسی: Arrow Function)، مقدار this هرچه که باشد، همان مقدار به عنوان this تابع فلش استفاده می‌شود. مثلا اگر تابع فلش در محیط گلوبال تعریف شود، this داخل آن به آبجکت گلوبال اشاره می‌کند. مثال:

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

حال اگر هنگام صدا زدن تابع فلش از متدهای call() ،apply() یا bind() استفاده کنیم، تابع فلش از استفاده آن‌ها صرف نظر می‌کند. انتقال آرگومان با استفاده از این متدها همچنان برقرار است اما اولین آرگومان که this را مشخص می‌کرد بایستی null ست شود:

var foo = (() => this);

// ُSeda zadan be onvane methode yek object:
var obj = {func: foo};
console.log(obj.func() === globalObject); // true

// talash baraye avaz kardane this ba methode call():
console.log(foo.call(obj) === globalObject); // true

// talash baraye set kardane this ba methode bind():
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

مهم نیست چکار کنیم. this تابع foo همان چیزی خواهد بود که هنگام تعریف بوده است. در مثال بالا this هنگام تعریف 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

در کد بالا یک متد برای آبجکت obj تعریف کرده‌ایم که یک تابع فلش را برمی‌گرداند که خود آن تابع فلش مقدار this داخل خودش را برمی‌گرداند (خط ۳). چون this در داخل متد bar به obj اشاره می‌کند، طبق گفته‌های بالا انتظار داریم this تابع فلش هم به طور دائمی به obj اشاره کند. همین موضوع را در خط‌های بعدی آزمایش کرده‌ایم. متد را اجرا کرده و داخل متغیر fn ریخته‌ایم. حالا متغیر fn همان تابع فلش ما است. اگر آن را اجرا کنیم می‌بینیم مقداری که برمی‌گرداند برابر با obj است (خط ۹). در خط ۱۱ به جای اجرای متد، تابع آن را داخل یک متغیر ریخته‌ایم. (بدون وجود پرانتز). در این‌جا this اشاره شده به این تابع جدید (fn2)، تغییر پیدا می‌کند. بنابراین اگر در خط ۱۲ آن تابع و تابع فلش داخلش را اجرا کنیم می‌بینیم که مقدار this که برمی‌گرداند برابر window است.

this داخل متدهای آبجکت

زمانی که تابعی به عنوان متد یک آبجکت فراخوانی می‌شود، this آن به آبجکتی که حاوی آن متد است اشاره می‌کند. در مثال زیر، زمانی که o.f() فراخوانی می‌شود، this داخل تابع به آبجکت o اشاره دارد:

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

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

به این نکته توجه کنید که رفتار this به این مربوط نمی‌شود که تابع کجا تعریف شده است. در مثال بالا ما تابع را در داخل آبجکت و حین تعریف آبجکت تعریف کردیم. اما می‌توانیم آن را به شکل تابعی مستقل تعریف کرده و بعدا به آبجکتمان اضافه کنیم. در این حالت نیز رفتار مشابهی خواهیم داشت. کد زیر معادل کد بالا عمل می‌کند:

var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

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

این موضوع نشان می‌دهد تنها چیزی که اهمیت دارد نحوه صدا کردن تابع است که از کجا فراخوانی شود؛ نه مکان تعریف تابع.

نکته‌ی دیگر در این مورد این است که this از داخلی‌ترین محیطی که تابع از آن فراخوانی می‌شود ارث می‌گیرد. مثال زیر موضوع را روشن می‌کند:

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

در این‌جا تابع independent به عنوان عضوی از آبجکت o.b فراخوانی می‌شود. پس باید به o.b اشاره کند. این موضوع که o.b، خود، متدی از آبجکت o است تاثیری بر موضوع نخواهد داشت و داخلی‌ترین محیط برای تابع independent همان o.b است که this خود را از آن محیط به ارث خواهد برد.

this در زنجیره‌ی پروتوتایپ آبجکت‌ها

اگر متدی جایی در داخل زنجیره‌ی پروتوتایپ (به انگلیسی: 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 نیست. بلکه این متد را از پروتوتایپ خود به ارث برده است. این‌جا وقتی f را به عنوان متدی از p صدا می‌زنیم دقیقا مثل این است که f متد خود p است. پس this به کار رفته داخل آن نیز به p اشاره خواهد داشت.

this با getter و setterها

این مورد نیز شبیه مورد قبل است. در این‌جا نیز this داخل تابعی که به یک آبجکت get یا set شده است به همان آبجکت اشاره می‌کند. مثال زیر را ببینید:

function sum() {
  return this.a + this.b + this.c;
}

var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};

Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});

console.log(o.average, o.sum); // 2, 6

this در سازنده‌ها

در مورد توابعی که به عنوان “سازنده” (به انگلیسی: constructor) استفاده می‌شوند (با کلیدواژه‌ی new)، آبجکت this آن‌ها “ساخته می‌شود”. البته به شرطی که آبجکتی را برنگردانند. اگر این توابع آبجکتی برگردانند آن آبجکت “ساخته می‌شود”. اگر جملات پیشین مبهم هستند به مثال زیر توجه کنید:

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

var o = new C();
console.log(o); //{a: 37, b: 33}


function C2() {
  this.a = 37;
  this.b = 33;
  return { a: 38, b: 400 };
}

o = new C2();
console.log(o); // {a: 38, b: 400}

در خط اول تابع C را تعریف کرده‌ایم. اگر یک آبجکت به اسم o از روی آن “بسازیم” (خط ۶)، آبجکت o آبجکتی است که با توجه به this تابع C ساخته می‌شود. یعنی دارای دو پراپرتی a و b با مقادیر ۳۷ و ۳۳ خواهد بود. از طرف دیگر در خط ۱۰، تابع C2 را تعریف کرده‌ایم که با وجود داشتن متدهای a و b برای this، چون خودش یک آبجکت را برمی‌گرداند (خط ۱۳)، اگر آبجکتی از روی آن “بسازیم” (خط ۱۶)، آن آبجکت همان آبجکتی خواهد بود که تابع برگردانده و مقادیر ۳۷ و ۳۳ با وجود این‌که پردازش می‌شوند ولی خروجی‌ای ندارند و قابل حذف هستند.

this در کنترل‌کننده‌های وقایع

زمانی که یک تابع به عنوان کنترل‌کننده‌ی وقایع (به انگلیسی: Event Handler) استفاده می‌شود، this آن به المانی که کنترل کننده‌ی واقعه برای آن تعریف شده است، اشاره می‌کند. کد زیر را ببینید:

function bluify(e) {
  // Hamishe true
  console.log(this === e.currentTarget);
  // vaghti "currentTarget = target" bashad,  true ast:
  console.log(this === e.target);
  this.style.backgroundColor = '#A5D9F3';
}

// gereftane liste hameye element-ha:
var elements = document.getElementsByTagName('*');

// ezafe kardane listener baraye amale click,
// vaghti element click shavad pas-zamineh aabi mishavad:
for (var i = 0; i < elements.length; i++) {
  elements[i].addEventListener('click', bluify, false);

برای کنترل‌کننده‌های وقایع که مستقیما در داخل خود المان نوشته می‌شوند (inline)، آبجکت this به همان المان اشاره می‌کند:

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

در کد بالا عبارت button نشان داده می‌شود. البته توجه کنید که در چنین مواقعی this فقط در کدهای بیرونی به این شکل رفتار می‌کند. مثلا در کد زیر this مربوط به تابع داخلی مقداردهی نشده است. پس مثل حالت محیط گلوبال، به آبجکت گلوبال اشاره خواهد کرد. البته فرض می‌کنیم در حالت strict نیستیم.

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

برای استفاده از منبع به زبان اصلی اینجا کلیک کنید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *