50 – جاوااسکریپت: call, apply, bind

در execution context یک variable environment و یک outer environment reference و یک متغیر this داریم. همان طور که قبلا گفته شد متغیر this در شرایطی به global object اشاره میکند و در شرایطی به شیئی که تابع را دربر دارد (اگر تابع متدی از آن شیء باشد) اشاره میکند. خیلی خوب است اگر بتوانیم موقع ایجاد execution context روی this کنترل داشته باشیم. js این امکان را به ما میدهد و به همین دلیل call و apply و bind رایج هستند. میتوانستیم در این مورد قبلا صحبت کنیم، همان موقع که در مورد متغیر this توضیحاتی داشتیم. اما برای فهم عمیقتر آن نیاز بود در مورد first class function بدانیم. همان طور که میدانیم تابع ها نوع خاصی از شیء هستند. تابع یک ویژگی پنهان اختیاری NAME دارد. یک ویژگی CODE دارد که invocable است. همه تابع ها در js همچنین به یک سری ویژگی و یک سری متد دسترسی دارند. چون تابع یک شیء است و میتواند ویژگی ها و متدهایی داشته باشد. همه تابع ها به متدهای call() و apply() و bind() دسترسی دارند. این سه متد با متغیر this کار میکنند.var person = {firstname: ‘John’,lastname: ‘Doe’,getFullName: function() {var fullName=this.firstname+’ ‘+this.lastname; return fullName;}};در متد از this استفاده کرده ایم. چون این یک متد در شیء است، وقتی این تابع اجرا میشود this به کل شیء نگهدارنده متد اشاره میکند. حال فرض کنید تابعی داریم که خارج از این شیء است:var logName = function(lang1, lang2) { console.log(‘Logged: ‘+this.getFullName()); };logName();میدانیم که در کد بالا this.getFullName() خطا میدهد: undefined is not a function. چون this در این قسمت به global object اشاره میکند. این تابع متد شیء person نیست. در global object متد getFullName را نداریم و undefined است و این خطا را به ما میدهد. میخواهیم روی مقدار this کنترل داشته باشیم. پس قبل از فراخوانی logName(); یک تابع دیگر تعریف میکنیم:var logPersonName = logName.bind(person);همه تابع ها در js به سه متد گفته شده (از جمله bind) دسترسی دارند. توجه داشته باشید که در کد بالا تابع logName را با پرانتز فراخوانی نکرده ایم چون تابع مقداری را برمیگرداند. ولی ما اینجا میخواهیم به تابع به عنوان یک شیء نگاه کنیم و متد bind را برای آن شیء فراخوانی کنیم. (logName یک شیء تابع است و همه تابع ها این متد را دارند) میتوانیم به تابع bind هر شیئی که میخواهیم را پاس دهیم تا وقتی تابع اجرا میشود متغیر this به همان شیء پاس داده شده اشاره کند. تابع bind یک تابع جدید را برمیگرداند. در واقع یک کپی از logName در یک تابع شیء جدید ایجاد میکند. بنابراین هر موقع که اجرا میشود و execution context آن ایجاد میشود، موتور js میبیند که این شیء با bind ایجاد شده، پس مقدار متغیر this را برابر با person قرار میدهد. در واقع با این کار روی تصمیم موتور js تاثیر میگذاریم. بنابراین اگر به جای فراخوانی logName();، تابع logPersonName(); را فراخوانی کنیم خروجی میشود:Logged: John Doeمیتوانیم این کار را به صورت زیر و on the fly هم انجام دهیم:var logName = function(lang1, lang2) { console.log(‘Logged: ‘+this.getFullName()); }.bind(person);logName();در این حالت مقدار logName به یک کپی از تابع اشاره میکند که on the fly ایجاد شده که به person م bind شده است. بنابراین bind یک کپی از تابع ایجاد میکند که this به هر شیئی که به آن by reference پاس دهیم اشاره میکند.var logName = function(lang1, lang2) {console.log(‘Logged: ‘+this.getFullName());console.log(‘Arguments: ‘+lang1+’ ‘+lang2);};var logPersonName = logName.bind(person);logPersonName(‘en’);اگر کد با به صورت بالا کمی تغییر دهیم، باز هم یک کپی از تابع داریم. پس یک آرگومان گرفته و مقدار this در کد بالا همان person خواهد بود. خروجی میشود:Logged: John DoeArguments: en undefinedاگر در مثال بالا از عبارت logName.call استفاده کنیم، تابع فراخوانی میشود. این کار را میتوانیم با پرانتز خالی هم انجام دهیم. اما استفاده از متد call به ما این امکان را میدهد تا مقدار متغیر this را تعیین کنیم. اولین پارامتری که به call پاس میدهیم برای تعیین مقدار this است. سپس میتوانیم پارامترها را به آن پاس دهیم:logName.call(person, ‘en’, ‘es’);پس متد bind یک کپی از تابع ایجاد میکند ولی متد call آن را اجرا میکند. خروجی کد بالا:Logged: John DoeArguments: en esمتد bind خودش تابع را اجرا نمیکند.متد apply هم به غیر از یک مورد دقیقا مثل call عمل میکند. اگر بخواهیم متغیرها را به صورتlogName.apply(person, ‘en’, ‘es’);به تابع apply پاس دهیم، به ما خطا میدهد: Function.prototype.apply: Arguments list has wrong typeچون تابع apply آرایه ای از پارامترها را میخواهد:logName.apply(person, [‘en’, ‘es’]);خروجی این کد:Logged: John DoeArguments: en esآرایه میتواند مفیدتر باشد، بخصوص در مواردی که از ریاضیات استفاده میکنیم. مثلا موقعی که میخواهیم چندین عدد را با هم جمع کنیم، تعداد اعداد میتواند هر عددی باشد. بنابراین دو آپشن call و apply را داریم و بسته به چگونگی کد زدنمان از یکی استفاده میکنیم.اینجا هم میتوانیم از این متدها on the fly استفاده کنیم. میتوانیم از تریک گذاشتن پرانتز قبل از اسم تابع استفاده کنیم تا به syntax parser بگوییم این خط کد یک function statement نیست و یک function expression است. و به جای اینکه با () آن را فورا فراخوانی کنیم، میتوانیم با apply یا call آن را فراخوانی کنیم:(function(lang1, lang2) {console.log(‘Logged: ‘+this.getFullName());console.log(‘Arguments: ‘+lang1+’ ‘+lang2);}).apply(person, [‘en’, ‘es’]);پس یک تابع on the fly ایجاد کردیم و با apply آن را فراخوانی کردیم. خروجی این کد:Logged: John DoeArguments: en esسوالی که ممکن است پیش بیاید این است که آیا واقعا از چنین چیزهایی استفاده میشود؟ به مثالهای زیر توجه کنید. مثلا یک شیء دیگر به اسم person2 داریم که خیلی شبیه به شیء اول person است و فقط متد را ندارد:// function borrowingvar person2 = {firstname: ‘Jane’,lastname: ‘Doe’};میخواهیم با استفاده از call یا apply یک function borrowing داشته باشیم.در شیء اول متد getFullName را داریم. چون این متد یک تابع است پس متد apply را میتوانیم برای آن صدا کنیم. ولی مقدار this را برابر با person2 میگذاریم:person.getFullName.apply(person2);بنابراین متد getFullName داخل شیء اول person را فراخوانی کردیم ولی داخل آن this به شیء دوم person2 اشاره میکند. بنابراین اگر کد بالا را در کنسول اجرا بگیریم، خروجی:Jane Doeآنچه انجام دادیم، قرض کردن یک تابع است. میخواهیم از متدی در یک شیء برای شیء دیگری استفاده کنیم و با استفاده از apply آن را قرض گرفتیم. میتوانیم این کار را با call هم انجام دهیم. این دو شیء اسم ویژگی هایشان یکی بود تا تابع بتواند در شیء دوم هم اجرا شود.یک مثال دیگر استفاده از این سه متد به صورت زیر است. اینجا از متد bind استفاده میکنیم. این متد یک کپی از تابع ایجاد میکند و اگر به آن پارامترهایی را پاس دهیم میتواند جالبتر هم باشد. موقع استفاده از call و apply با پاس دادن پارامتر فقط آنها را پاس داده ایم، اما وقتی که bind یک کپی از تابع ایجاد کرد، اگر به آن پارامترهایی پاس دهیم چه اتفاقی می افتد؟// function curryingfunction multiply(a, b) { return a*b; }var multipleByTwo = multiply.bind(this, 2);multipleByTwo(3);یک تابع جدید که یک کپی از تابع multiply است ایجاد کردیم. اینجا برایمان مهم نیست که this به کجا اشاره میکند، فقط به bind یک مقدار پارامتر میدهیم. همان طور که گفته شد، متد bind تابع را اجرا نمیکند، پس دادن پارامتر 2 به چه معناست؟ دادن پارامتر به bind باعث میشود که وقتی کپی از تابع ایجاد شد مقدار پارامتر اول آن برابر با 2 باشد. مثلا این است که کد زیر را داشته باشیم:function multipleByTwo(b) { var a=2; return a*b; }موقع فراخوانی multipleByTwo فقط پارامتر دوم را به آن پاس داده ایم. اگر آن را در کنسول اجرا بگیریم خروجی 6 را میدهد. اگر موقع ایجاد تابع multipleByTwo هر دو پارامتر را به آن پاس دهیم، داریم به a و b به صورت همیشگی مقدار ثابت میدهیم.var multipleByTwo = multiply.bind(this, 2, 2);multipleByTwo(3);با اینکه موقع فراخوانی تابع multipleByTwo به آن پارامتر 3 را پاس داده ایم، ولی مقدار 4 خروجی آن خواهد بود، چون موقع ساخت تابع multipleByTwo هر دو پارامتر را برایش انتخاب کرده ایم. اگر موقع ایجاد این تابع هیچ پارامتری به آن پس ندهیم:var multipleByTwo = multiply.bind(this);multipleByTwo(5, 2);موقع اجرا نیاز است که هر دو پارامتر به آن داده شود. در این مثال خروجی 10 میشود.بنابراین یک تابع داریم. سپس یک تابع جدید با برخی مقادیر پیش فرض از آن ایجاد میکنیم. به این کار currying گفته میشود.تعریف function currying: ایجاد یک کپی از یک تابع با چند پارامتر ست شده. در مواردی که از ریاضیات استفاده میشود خیلی کاربرد دارد. پس اگر بخواهیم یک کتابخانه درست کنیم که محاسبات ریاضی زیادی را شامل میشود، میتوانیم از تابع های fundamental استفاده کنیم و سپس با bind کردن آنها تابع هایی را با مقادیر پیش فرض ایجاد کنیم.