محمد ابراهیمی اول
محمد ابراهیمی اول
خواندن ۲۳ دقیقه·۲۴ روز پیش

صفر تا صد this در JavaScript

پیشگفتار

وقتی به توضیحات this در ویدیو های آموزش سطح مبتدی جاوااسکریپت نگاه کنید بحث خیلی ساده به نظر میرسه ولی طولی نمیکشه که یه روزی یه جایی پیش میاد که رفتار عجیب this شما رو گیج میکنه! من این رو نقص آموزشهای مقدماتی نمیدونم، به نظرم اونها کار درستی میکنن که زمینه گیج شدن رو برای فردی که تازه برای اولین بار میخواد این زبان رو یاد بگیره ایجاد نمیکنن و سعی میکنن قضیه رو با چند مثال و چند حالت پرتکرار جمع کنن و به موضوع بعدی برن اما این قرار نیست دلیلی بشه که شخص بعد از گرم شدن دستش در توسعه برنامهها و گرفتن لقب توسعه دهنده جاوااسکریپت به سراغ جزئیات ماجرا که کمی گیج کننده ممکن هست باشه، نره! به نظرم من، قانون ۲۰/۸۰ اینجا هم هست! یعنی شما با دونستن ۲۰٪ حالتها و توضیحات this میتونید مقدارش در ۸۰٪ موارد رو شناسایی کنید ولی برای فهمیدن اون ۲۰٪ موارد دیگه که اتفاقا خیلی هم خاص و کم پیش آمد هستن لازم هست تا اون ۸۰٪ دیگه داستان this که نمیدونید رو یاد بگیرید.

حتما یادتون هست که در امتحانات دوران مدرسه اغلب وقتی سوالی کوتاه بود جواب شرح مفصلی داشت و اگر سوال حجم و جثهی بزرگی داشت اغلب جوابی مختصر داشت. کلمه کلیدی this همین syntax مختصری که میبینید رو داره یعنی همین حروف کوچک this هست و تمام ولی وقتی که بپرسن مقدار this چی هست اینجاست که باید خبره باشی تا بتونی همه موارد رو درست جواب بدی. در این مطلب هدف فهمیدن همه حالتهایی هست که this میتونه داشته باشه.

this in javascript
this in javascript

مقدمه

کلمه کلیدی this در جاوا اسکریپت مثل نخودی بازی های دوران کودکی هست که همیشه تو بازی هست! شما هرجا بنویسید console.log(this) به ندرت پیش میاد که چیزی در خروجی چاپ نشه حتی اون undefinedی هم که ممکن هست در شرایط خاص مثل لود کردن یک فایل به صورت module رخ بده هم به نوعی خودش یک مقدار قابل تامل هست.

پس این که فکر کنیم this همیشه در جایی هست که یک Class یا Object باشه چنین چیزی حداقل در ظاهر معنی نداره چون خیلی وقتها اصلا هیچ ساختار شی یا کلاس مانندی هم وجود نداره (مثلا همون فراخوانی مستقیم this در global scope) ولی خب این تفکر آنچنان هم غلط نیست، چون میدونیم که در جاوااسکریپت به جز primitive valueها بقیه همه Object هستن! پس الکی نیست که اغلب جاها که صداش میزنیم هست و دلیلش این هست که کلا ما در یک سیستم برپایه شیگرایی داریم فعالیت میکنیم!

۲۰ نکته اساسی در مورد مقدار this در جاوا اسکریپت

۱) با بقیه زبانها فرق داره! مقدار this در جاوااسکریپت با سایر زبانها تفاوتهایی داره. به عبارتی با دانش زبانهای دیگه در همه موارد نمیشه در مقدار this رو پیش بینی کرد.

۲) مقدار this در strict mode و non-strict mode تفاوتهایی داره و اولین قدم همیشه فهمیدن این هست که ما در چه modeی داریم کد رو اجرا میکنیم . (در ادامه پس از توضیحاتی شرحش رو میگم)

۳) مقدار this بستگی به این دارد که در کدام یک از ۳ محیط function, Class و global scope بکار گرفته میشود. به عبارتی بعد از شناخت mode اجرای اسکریپتها (Strict mode/non-strict mode) باید بفهمید که در چه محیطی از this استفاده میکنید.

۴) در اکثر موارد پای یک function در میان هست! چه در functionها، چه در classها و چه در فضای عمومی کد (global scope) همیشه یا واضح و علنی (explicitly) یا به صورت ضمنی و مخفی (implicitly) همه چیز مانند اجرا یک function اتفاق میافته (برای اطلاعات بیشتر " javascript global execution context" رو گوگل کنید).پس شناخت رفتار this در functionها کمک میکنه که اکثر مقادیر ممکن برای this رو بدونید. خوب اینم یادآوری کنم که در جاوااسکریپت functionها نوع خاصی از object هستن نه یک ساختار مستقل.

۵) وابسته به اجرا نه به تعریف! مقدار this در اکثر موارد به این بستگی داره که function چطور اجرا بشه نه این که چطور و در کجا تعریف بشه. به این قاعده runtime binding گفته میشه یعنی خود زبان پس از بررسی function body در زمان اجرا مقداری رو برای this تعریف میکنه و مانند پارامترهایی که به فانکشن ارسال شدن میتونید اون رو تصور کنید با این تفاوت که برخلاف پارامترهای تابع که دستی خودتون می فرستید اینجا خود زبان به صورت مخفی واستون به داخل تابع ارسال کرده و میتونید ازش استفاده کنید.

۶) خطای assignment! مقدار this را نمیشود مانند مقدار دهی به یک متغییر به صورت مستقیم در حین اجرا تعیین کرد. اجرا قطعه کد زیر منجر به خطا میشود:

function fn() { this = {foo:'bar'} console.log(this) } fn() // SyntaxError: Invalid left-hand side in assignment

۷)تعریف مقدار دلخواه! به کمک متدهای bind, call و apply میتوان مقدار دلخواهی برای this تعیین کرد و اگر یکبار به کمک bind اینکار را انجام دادید دیگر نمیتوانید بر روی function خروجی آن از این متدها برای تعیین this استفاده کنید و مقدار this همچنان به همان مقدار تعیین شده در دفعه اول اشاره میکنه.

function fn() { return this } const step1 = fn.bind({a: &quota&quot}) console.log(step1()) //{a: 'a'} const step2 = step1.bind({b: &quotb&quot}) console.log(step2()) //{a: 'a'}

۸) دفعه اجرا مهم هست! هر بار که function اجرا میشود ممکن هست که مقدار this تغییر کند و این یعنی مقدار this علاوه بر بستگی داشتن به نحوه اجرا، به دفعه اجرای تابع هم بستگی داره.

let counter = 1 function yourAction() { console.log(this) } function fn() { counter++ const yourActionThis = counter % 2 === 0 ? {a: &quota&quot} : {b: &quotb&quot} yourAction.call(yourActionThis ) } fn() // {a: &quota&quot} fn() // {b: &quotb&quot} fn() // {a: &quota&quot} fn()// {b: &quotb&quot}

۹) دقت داشته باشیم که Arrow funtionها خاص هستن! هر چیزی که در مورد this در functionهای عادی (typical functions) میگیم به احتمال بسیار زیاد در مورد Arrow functionها درست نیست چون اصلیترین تفاوت تابعهای معمولی و arrow همین بحث this در اونهاست.

مثلا شما نمیتونید از متدهای bind, call و apply برای تعیین مقدار this داخل تابع استفاده کنید چون مقدار this اونها همواره برابر نزدیکترین مقدار this در محل تعریف تابع هست (it retains the this value of the enclosing lexical context).

یا این که بحث بستگی داشتن مقدار this در تابعهای عادی به نحوه اجرا برای arrow functionها معنایی نداره چون در اینها مقدار this بستگی به محل تعریف داره در حالی که در توابع معمولی محل تعریف اغلب مهم نیست!

همچنین بحث mode محیط (Strict mode و non-strict mode) و محیط اجرا (function, Class و global scope) به صورت مستقیم به اینها ربطی نداره و اونها تنها چیزی که براشون مهم هست این هست که مقدار this در نزدیکترین scope محل تعریفشون چی هست یا به عبارتی این شرایطی که گفتیم، ممکن هست بر مقدار this محیط نزدیکش تاثیر بذاره و این عاملی بشه که به صورت غیرمستقیم بر مقدار this درarrow function تاثیر بذاره.

دلیل این اتفاقها این هست که خود زبان جاوااسکریپت بر روی این نوع فانکشنها یک Closure ایجاد میکنه که مقدار this محل تعریف در اونها قرار داده شده و این یعنی جاوااسکریپت برای arrow functionها یک object جدید خلق نمیکنه بله همون مقدار this محیط اطراف در this اینها assign میشه یا به عبارتی هر دو به یک object یکسان اشاره میکنن.

const envThis = this const custormThis = {a: &quota&quot} const fn = () => this const result = fn.call(custormThis) console.log(result === custormThis) // false console.log(result === envThis) // true

ادامه ۳) عملیات this substitution ! در حالت strict mode مد مقدار this هر چیزی (...,undefined, null, number, string) میتونه باشه. در حالت non-strict mode ساختاری تحت عنوان this substitution تضمین میکنه که مقدار this قطعا یک object خواهد بود. در این ساختار اگر مقدار this یک Primitive Value باشه به کمک Constructor آن value type یک خروجی object گرفته میشه و در this قرار داده میشه.

function strict() { &quotuse strict&quot return this } Number.prototype.strictThis = strict const strictResult = (1).strictThis() console.log(strictResult) // 1 console.log(typeof strictResult) // &quotnumber&quot function nonStrict() { return this } Number.prototype.nonStrictThis = nonStrict const nonStrictResult = (1).nonStrictThis() console.log(nonStrictResult) // Number (1) console.log(typeof nonStrictResult) // &quotobject&quot

برای فهم بهتره این اتفاق باید تفاوت Literal value و Constructor instance value رو بدونید.

const literalNumber = 123 const instanceNumber = new Number(123) console.log(literalNumber) // 123 console.log(instanceNumber) // Number (123) console.log(literalNumber.valueOf()) // 123 console.log(instanceNumber.valueOf()) // 123 console.log(typeof literalNumber) //number console.log(typeof instanceNumber) //object

همچنین اگر مقدار this برابر undefined یا null باشد زبان جاوا اسکریپت اتوماتیک مقدار this رو به مقدار globalThis تغییر میده. مقدار کلمه کلیدی globalThis در اغلب محلهای اجرا برنامه برابر Object سراسری هست. مثلا اگر scriptها در مرورگر اجرا شوند مقدار globalThis برای شیglobal یا همان شی window خواهد بود.

function fn() { return this } const fnThis = fn.call(undefined) console.log(fnThis === globalThis) // true

وقتی هم که functionها را به صورت عادی فراخوانی کنید مقدار this برابر undefined خواهد بود که در strict mode همین مقدار میماند و تغییر نمیکند ولی در non-strict mode ساختار this substitution باعث میشود که مقدار آن برابر globalThis شود.

function fn() { &quotuse strict&quot return this } console.log(fn() === undefined) // true function fn() { return this } console.log(fn() === globalThis) // true

۱۰) ارسال ضمنی در memeber access! هنگامی که از member access (هم dot member access و هم computed member access) استفاده میکنید به صورت ضمنی (implicitly) مقدار this بخش قبلی به بخش بعدی منتقل میشود. یعنی اگر متد fn شی obj را با نقطه call کنید آنگاه مقدار this در داخل متد fn برابر obj خواهد بود ولی اگر fn را در متغییر subFn قرار دهیم و بعد اجرا کنیم (بدون dot member access آن را call کنیم) مقدار this هیچ ربطی به obj نخواهد داشت و این کار مشابه این است که به صورت انتزاعی function مستقیما در جلوی fn تعریف شده و شرایط this طبق این حالت انتزاعی تعریف خواهد شد.

const obj = {fn: function () { return this } } // with member access console.log(obj.fn() === obj) //true console.log(obj[&quotfn&quot]() === obj) //true // with assignment const subFn = obj.fn console.log(subFn () === obj) // false

به زنجیره کار نداره! نکتهای که باید به اون دقت داشت این هست که اگر زنجیرهای از objectهای تو در تو داشته باشیم آنگاه مقدار this در هر مرحله برابر object مرحله قبلش است و کاری به زنجیره اون object و خود object اصلی نداره.

const a= { name: &quota&quot, b: { name: &quotb&quot, fn: function () { return this }, c: { name: &quotc&quot, fn: function () { return this } } } } const bFnResult = a.b.fn() console.log(bFnResult ) // {name: 'b', fn: ƒ, c: {…}} console.log(bFnResult === a.b) // true console.log(a.b.c.fn()) //{name: 'c', fn: ƒ}

متد super یک استثنا هست! این قاعده ارسال ضمنی this سطح قبل به سطح بعد برای کلمه کلیدی super در حالت ()super.fn اجرا نمیشه و مقدار this در fn به super اشاره نخواهد کرد و این یک استثنا هست! متد fn در این syntax به صورت ضمنی یک arrow function در نظر گرفته میشه یعنی مقدار this در fn برابر مقدار this نزدیکترین scope به fn در زمان اجرا است.

class A { static aFn() { return this } } class B extends A { static bFn() { return super.aFn() } } console.log(B.bFn() === A) // false console.log(B.bFn() === globalThis) // false console.log(B.bFn() === B) // true

گفتن این نکته هم خالی از لطف نیست که متدهای تعریف شده به صورت static در یک کلاس دقیقا مشابه این رفتار میکنن که شما یک object مخفی با نام آن کلاس در حافظه تعریف کنید و این متدها را برای آن تعریف کنید چون این متدها اختصاصی برای خود کلاس خواهند بود و ربطی به instance object ندارند. حالا یک کلمه کلیدیای هم داریم به نام super که دقیقا مشابه کلاس نیست ولی یکی از رفتارهایی که میکنه این هست که مشابه نحوه فراخوانی با نام کلاس میشه یک متد از کلاس رو با super صدا زد با این تفاوت که وقتی با super فراخوانی بشه مقدار this برابر مقدار this در scope محل اجرا خواهد بود ولی وقتی با نام کلاس صدا زده بشه طبق قاعده member acces مقدار this به خود کلاس اشاره میکنه.

class A { static name = &quotA&quot static aFn() { return this } } class B extends A { static name = &quotB&quot static bFn() { return { withSuper: super.aFn(), withCalassName: A.aFn() } } } const result = B.bFn() console.log(result.withSuper === A) // false console.log(result.withSuper === B) // true console.log(result.withCalassName === A) // true console.log(result.withCalassName === B) // false

۱۱) توابع callback! مقدار this در callback functionها از همون قواعدی که برای fucntionها گفتیم پیروی میکنه. تابع logThis در مثال زیر به عنوان callback به متدها (توابع پایه) forEach و setTimeout ارسال شدن.

function logThis() { &quotuse strict&quot console.log(this); } [1, 2, 3].forEach(logThis); // undefined, undefined, undefined setTimeout(logThis, 1000); // undefined

شرایط خاص یک متد واسه callbackش! تنها نکتهای که باید به اون دقت کرد این هست که این callback که ما به یک تابع پایه ارسال کردیم آیا توسط خود این تابع پایه تغییری در thisش صورت میگیره یا نه. مثلا ممکن هست در بدنه این تابع پایه callback با متد call و یک object دلخواه به عنوان this صدا زده بشه که در این صورت باید مستند اون تابع پایه مطالعه بشه. به عنوان مثال اغلب متدهای iterative آرایهها پارامتری به نام thisArg میگیرند که اگر شما مقداری برای اون تعریف کرده باشی این متدها callback را با این مقدار call میکنند و مقدار this برابر مقدار thisArg میشه و اگر تعریف نکنید همون قوانین عادی functionها رو خواهد داشت.

[1, 2, 3].forEach(logThis, { name: &quotobj&quot }); // { name: 'obj' }, { name: 'obj' }, { name: 'obj' }

۱۲) در Constructor functionها همواره مقدار this به instance object خلق شده اشاره میکند.

let innerThis function fn() { this.name = &quotinstance&quot innerThis = this return this } const instanceObject = new fn() console.log(instanceObject) // fn {name: 'instance'} console.log(instanceObject === innerThis) // true

این یعنی مقدار this مستقل از زنجیره propertyهایی هست که ممکن است در هنگام استفاده از new expertion وجود داشته باشند. به عبارتی قاعده ارسال ضمنی this در بحث memeber access در اینجا عمل نمیکند.

const a = { b: { c: { fn: function () { this.name = &quotavalamoozesh&quot return this } } } } const instanceObject = new a.b.c.fn() console.log(instanceObject) // fn {name: 'avalamoozesh'} console.log(instanceObject === a.b.c) // false

اگر Constructor function یک مقدار non-primitive مثل آرایه، شی یا تابع را برگرداند مقدار شی this عملا نادیده گرفته میشود. اصطلاحا به چنین حالتی dead code گفته میشود. البته لفظ مرده خیلی هم دقیق نیست چون مقدار this تعریف و به بدنه constructor ارسال و اجرا هم میشه ولی در نهایت چون چیز دیگری return میشه عملا میشه گفت در پایان اجرا constructor پرونده this بسته میشه و میمیره یا بهتره اینطور بگیم که مثل یک متغییر خصوصی واسش رفتار میکنه که با شروع تابع شروع و با پایانش تمام میشه! برخلاف بار منفیای که داره، از این روش گاهی اوقات در تعریف Constructor function استفاده میشه و الزاما چیز بدی نیست!

const customThis = {a: &quota&quot} function fn() { this.action = function () { console.log(&quotdo action&quot) } this.action() // log &quotdo action&quot string return customThis } const instanceObject = new fn() console.log(instanceObject) // {a: 'a'} console.log(instanceObject === customThis) // true

۱۳) در Classها (Class Context) همه چیز به دو گروه Instance Context و Static Context تقسیم میشه و مقدارthis هر گروه متفاوت هست!

[1] گروه اول (Instance Context) اونهایی هستن که حاصل نمونه گیری از کلاس هستن. instance object یک کلاس که با اجرا new expression تازه خلق خواهند شد و شامل Constructor, methods و instance field initializers (public or private)ها میشن و با هر بار صدا زدن new یک object مستقل از نمونههای قبلی ایجاد میشه که مقدار this برابر خود همین instance object خواهد بود یعنی نه به کلاس و پراپرتیهای staticش ربطی داره و نه به نمونههای قبلی گرفته شده از این کلاس!

باید توجه داشته باشید که به صورت پیش فرض و مستقل از هر نوع تعریفی در Class Syntax حالت محیط روی Strict mode تنظیم شده و امکان غیرفعال کردن آن هم وجود ندارد پس باید مقدار thisها در این ساختار را در حالت Strict mode ارزیابی کنید!

متد constructor کلاس بسیار مشابه یک Constructor function هست و از این جهت قواعد مقدار this در متد سازنده کلاس هم بسیار مشابه قواعد Constructor functionها هست با چند نکته که که منحصر به syntax کلاس هست.

اول super! مثلا شما نمیتونید قبل از این که در سازنده کلاسهای زیر مجموعه یک کلاس پایه (sub class یا derived class یا extended class) متد super رو صدا بزنید به this به هر شکل و نحوی اشاره کنید چون منجر به خطا میشه!

اجرای super الزامیهست! همچنین شما نمیتونید در کلاسهای زیر مجموعه constructor تعریف کنی ولی متد super رو اجرا نکنی چون منجر به خطا میشه مگر این که مثل قاعده dead code در Constructor function یک مقدار non-primitive را برگردانید با این تفاوت که در اینجا دیگه حق ندارید اشارهای به this کنید چون منجر به خطا میشه!

فقط در constructor کلاس پایه یک object برای this خلق میشود ولی برای توابع سازنده کلاسهای زیر مجموعه هیچ this جدیدی ایجاد نمیشود و همه به همان this کلاس پایه اشاره میکنن یا به عبارتی تو در تو به سطوح پایینتر پاس داده میشود.

[2] گروه دوم (Static Context) اونهایی که مربوط به خود کلاس هستن یعنی بدون این که از کلاس یک نمونه بگیریم تا بعدش بتونیم به متد و پراپرتیها دسترسی پیدا کنیم، اینجا میشه مستقیم و انگار که با کلاس طرف نیستیم بلکه با یک literal object طرف هستیم باشون کار کنیم و ازشون انتظار داشته باشیم. این گروه رو با کلمه کلیدی static در کلاس میشه تعریف کرد و شامل static methods, static field initializers و static initialization blocks میشود. مقدار this در اینها به خود کلاس اشاره میکنه.

:: نکات مشترک Static Context و Instance Context با تفاوت جزئی:

مقدار this چه در Static methodها و چه در instance methodها مشابه قواعد مقدار this که در بخش متدهای literal objectها توضیح دادیم هست یعنی به محل تعریف ربطی نداره بلکه به نحوه فراخوانی کردن آنها بستگی داره.

این یعنی اگر ما Static methodها را با member accessی برابر خود کلاس فراخوانی کنیم مقدار this به موجودیت کلاس اشاره داره (برابر مقدار this کلاس) هست و اگر در تابعی بریزیم و بعد فراخوانی کنیم this آن هیچ ربط و اشارهای به کلاس نداره و مثل یک تابع معمولی رفتار میکنه. برای instance methodها هم دقیقا همین قاعده هست با این تفاوت که instance method به instance object اشاره میکنه و به کلاس ربطی نداره! در اینجا متد رو یا میشه در تابعی ریخت و اجراش کرد که خب مثل اجرای یک تابع معمولی و مستقل از instance object خواهد بود و یا هم اگر با member accessی از خود instance object فراخوانی کنیم طبیعتا طبق قواعد انتقال ضمنی this در member access در متد مقدار this برابر instance object خواهد بود.

اگر هم متدها به صورت Arrow function تعریف شده باشند مقدار this برای متدهای static برابر this کلاس هست یا به عبارتی به کلاس اشاره داره و برای متدهای نمونه (instance method) برابر خود instance object هست یا به عبارتی به instance object اشاره میکنه. (دقت کنید که رفتار arrow functionها به ازای این که در یک literal object تعریف شدن و یا در یک کلاس متفاوت هست!)

همچنین مقدار this اگر به صورت مستقیم به فیلدهای نمونه ( instance field یا instance props) assign بشه برابر خود instance object ایجاد شده هست و اگر به فیلدهای (static) استاتیک assing بشه به کلاس اشاره داره.

class A { instanceThis= this static staticThis= this } const a = new A() console.log(a.instanceThis=== A) // false console.log(a.instanceThis=== a) // true console.log(A.staticThis=== A) // true console.log(A.staticThis=== a) // false

۱۴) مقدار this در assignment مستقیم به یک property از literal object برابر this در نزدیکترین scope به محل تعریف آن object خواهد بود. یعنی اگر در global scope چنین اتفاقی بیفته و فایل script به صورت module باگذاری نشده باشه مقدار this برابر globalThis و اگر به شکل module باشد برابر undefined خواهد بود.

const rootThis = this const a = { b: this } console.log(a.b) // undefined in modules OR window in typical browser script console.log(a.b === rootThis) // true

۱۵) مقدار this در در getter و setterها مانند متدهای literal objectها به نحوه فراخوانی بستگی دارد نه محل تعریف. البته چون اغلب این متدها توسط object محل تعریف فراخوانی میشن، اینطور هم میشه گفت که مقدار this به object محل تعریف اشاره داره.

function sum() { return this.a + this.b + this.c } const o = { a: 1, b: 2, c: 3, get average() { return (this.a + this.b + this.c) / 3 } } Object.defineProperty(o, &quotsum&quot, { get: sum, enumerable: true, configurable: true }) console.log(o.average) // 2 console.log(o.sum) // 6

۱۶) وقتی شما scriptها رو در چارچوب functionهای عادی یا Class که به صورت واضح تعریف شدهاند ننویسید میشه گفت که شما scriptها رو در بخش عمومی کد یا همون global context نوشتید. این محیط درست هست که هیچ ظاهر function مانند واضحی نداره ولی اگر شما فرض کنید که هر چه در global scope مینویسید مشابه این هست که در یک تابع با نام global نامرئی نوشتید که بعدا این تابع اتوماتیک با شروع ارزیابی scriptها توسط خود جاوااسکریپت اجرا خواهد شد،آنچان هم تصور غلطی نیست و اتفاق اصلی بسیار مشابه همین هست پس برای فهم بهتره میتونید از این شماتیک ذهنی استفاده کنید و بعد قواعد this در functionها رو به یاد بیارید.

در global scope مقدار کلمه کلیدی this مستقل از این که شرایط محیط Strict mode تعریف شده یا non-strict mode همواره برابر خواهد بود با globalThis که در اغلب محیطهای اجرا برابر global object خواهد بود.

بسته به موتور اجرا کننده! اگر scriptهای موجود در global scope را در مرورگر اجرا کنید global object شی window خواهد بود پس مقدار globalThis هم برابر window و مقدار this هم برابر window خواهد بود یا در NodeJS چون شی پایه window وجود ندارد مقدار globalThis با مقدار شی خاص global در NodeJS برابر خواهد بود.

در حالتmodule! دقت کنید که اگر یک script به صورت module بارگذاری شود دیگر root آن فایل global scope نخواهد بود. در این حالت مقدار this در global scope اون script همواره و مستقل از Strict mode و non-strict mode برابر undefined خواهد بود. همچنین مقدار globalThis طبق تعریف بالا اغلب برابر global object خواهد بود که در اکثر مرورگرها برابر شی window هست.

<!DOCTYPE html> <html> <head> <title>Aval Amoozesh</title> <meta charset=&quotUTF-8&quot /> </head> <body> <script src=&quotscript.js&quot type=&quotmodule&quot> </body> </html> //ــــــــــــــــــــ in script.js ــــــــــــــــــــ var title= &quotavalamoozesh&quot console.log(this) // undefined console.log(globalThis === this) // false console.log(window.title) // undefined console.log(globalThis === window) // true

۱۷) موتور اجرا کننده! باید اتفاقات خاص محیط محل اجرای برنامه رو هم دقیق بشناسید چون ممکن هست در برخی محیطها توسعهدهندههای آن شرایط خاصی برای this تعیین کرده باشن. مثلا CommonJS در NodeJS با این که در ظاهر scriptها در global scope تعریف میشوند ولی مقدار this برابر module.exports است. یا در تعریف در (DOM event handler) رویدادهای تگهای HTML در اغلب محیطهای اجرا مقدار this به element محل تعریف آن event اشاره میکنه که یک رفتار خاص تعریف شده برای اون متد هست.

<div id=&quotred&quot style=&quotpadding: 30px; background-color: red&quot =&quotconsole.log(this) //div#red&quot> </div>

همچنین مقدار this در callback رویدادها همواره برابر event.currentTarget و اگر المنت محل رخ دادن رویداد با المنت محل تعریف رویداد یکی باشد برابر e.target است.

<div id=&quotred&quot style=&quotpadding: 30px; background-color: red&quot> <div id=&quotblue&quot style=&quotpadding: 30px; background-color: blue&quot> </div> </div> function redElementClickHandler(e) { // Always true console.log(this === e.currentTarget) // true when click on div#red and false when click on div#blue console.log(this === e.target) } document.querySelector(&quot#red&quot).addEventListener(&quotclick&quot, redElementClickHandler)

۱۸) مقدار this در متد evel به دو حالت فراخوانی مستقیم (direct) و غیر مستقیم (indirect) تقسیم میشه. در فراخوانی مستقیم عملا آن قطعه کد در حال ارزیابی بخشی از کد محل اجرا متد evel به حساب میاد ولی در فراخوانی غیر مستقیم آن قطعه کد هیچ ربطی به محل اجرا متد evel نداره و به عنوان بخشی از global scope ارزیابی میشه.

globalThis.foo = 1 function direct() { let foo = 10 eval(&quotfoo++&quot) console.log(foo) // 11 console.log(globalThis.foo) // 1 } function indirect() { let foo = 10 eval?.(&quotfoo++&quot) console.log(foo) // 10 console.log(globalThis.foo) // 2 } direct() indirect()

۱۹) در IIFEها (Immediately Invoked Function Expression) چون عملا تعریف و اجرا تابع در یک محل و هم زمان هست قوانین حاکم بر مقدار this در این functionها دقیقا مشابه این هست که ابتدا تابع را تعریف کنیم و بعد با فراخوانی نام تابع آن را اجرا کنیم پس از قوانین اجرای تابع معمولی پیروی میکنه.

function main() { const innerThis = this; (function () { console.log(this == innerThis) // false console.log(this == globalThis) // true })() function fn() { console.log(this == innerThis) // false console.log(this == globalThis) // true } fn(); (() => { console.log(this == innerThis) // true console.log(this == globalThis) // false })() const arrowFn = () => { console.log(this == innerThis) // true console.log(this == globalThis) // false } arrowFn() } main.call({a: &quota&quot})

۲۰) هر چند در with statements منسوخ شده ولی لازم هست بدونیم که مقدار this در اینها چی هست، شاید برای فهم کدهای یک پروژه قدیمی روزی به کار بیاد. در این ساختار به جای تکرار object برای دسترسی به properyها، آنها را در block خود در دسترس قرار میده و این مشابه دسترسی به مقادیر object از طریق member access عمل میکنه و قوانین حاکم بر مقدار this هم مثل بحث دسترسی و فراخوانی از طریق member access هست.

const outerThis = this const obj = { bar: this, foo() { return this } } with (obj) { console.log(bar === outerThis) // true console.log(foo() === obj) // true }

جمعبندی

اگر کل این متن رو چکیده کنیم به این میرسیم که اول باید ببینم حالت strict چی هست بعد ببینم در function، class یا global scope هستیم بعد ببینیم این کد روی چه موتور و در چه محیطی (مرورگر، node js ...) داره اجرا میشه بعد به این دقت کنیم که ایا تابع با member access فراخوانی شده یا نه و اگر بله چه استثناها(مثل super) و شرایطی داره، بعد به این که این تابع یک arrow function بوده یا نه و بعد این که ایا این به صورت callback ارسال شده یا نه و اگر بله اون متدی که این رو براش فرستادیم دستکاری خاصی میکنه یا نه. در کنار اینها یکسری نکته واضح رو هم باید یاد بگیریم یا بهتره بگم حفظ کنیم، مثلا این که نمیشه مقدار this رو با assignment (=) مستقیم به در this قرار داد یا این برای eval دو حالت متفاوت اجرا داریم و ... .

به نظرم باید حداقل این متن ۳ بار در ۳ هفته جدا خونده بشه تا احتمال این که به ذهنتون سپرده شده بالا بره مگر نه با یک بار خوندن احتمال میدم ماه دیگه ازتون کسی بپرسه میگید، راستش تو مطلبی که محمد ابراهیمی اول در اول آموزش گذاشته بود خونده بودم یادم گرفته بودم و ولی حقیقتا الان دقیق یادم نیست و سوالم این هست اگر قرار باشه که اینقدر سریع و راحت فراموشش کنیم اصلا چرا باید بخونیمش! پس تلاش کنید که مسلط بشید با تکرار و تکرار و تکرار. یک راه جانبی هم که کمک کننده هست یادداشت کردن خلاصهای از نکات به قلم خودتون و مرور اون به عنوان یک cheatsheet برای this در javascript هست.

منبع: MDN Web Doc
ترجمه و گرداوری شده توسط محمد ابراهیمی اول در پاییز ۱۴۰۱

thisjavascriptجاوااسکریپت
یاد می‌گیرم، تجربه می‌کنم، اشتباه می‌کنم و این چرخه من است تا موفق بشم یا ازش درس بگیرم.
شاید از این پست‌ها خوشتان بیاید