زمانی که متدهای تابع را به عنوان callback پاس میدهیم، برای مثال به setTimeout
، یک مشکل شناخته شده وجود دارد: «از دست دادن this
».
در این فصل ما راههایی را برای رفع آن خواهیم دید.
از دست دادن “this”
ما از قبل درباره از دست دادن this
مثالهایی را دیدهایم. زمانی که یک متد جایی به غیر از شیء خودش پاس داده شود، this
از دست میرود.
چیزی که ممکن است با setTimeout
اتفاق بیافتد اینجا آورده شده:
let user = {
firstName: "John",
sayHi() {
alert(`سلام، ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // !undefined ،سلام
همانطور که میبینیم، خروجی “John” را به عنوان this.firstName
نشان نداد بلکه undefined
را نمایش داد!
دلیلش این است که setTimeout
تابع user.sayHi
را جدای از شیء آن دریافت کرد. خط آخر میتواند اینگونه نوشته شود:
let f = user.sayHi;
setTimeout(f, 1000); // را از دست داد user زمینه
روش setTimeout
در مرورگر کمی خاص است: این تابع برای فراخوانی تابع this=window
را تنظیم میکند (در Node.js، مقدار this
شیء تایمر میشود اما اینجا خیلی مهم نیست). پس برای this.firstName
این تابع تلاش میکند که window.firstName
را دریافت کند، که وجود ندارد. در موارد مشابه دیگر، معمولا this
برابر با undefined
میشود.
کاری که انجام میشود کاملا معمولی است، ما میخواهیم یک متد شیء را جایی دیگر (اینجا، به زمانبند) که فراخوانی خواهد شد پاس دهیم. چگونه مطمئن شویم که با زمینه درست فراخوانی میشود؟
راهحل 1: دربرگیرنده
سادهترین راهحل استفاده از یک تابع دربرگیرنده است:
let user = {
firstName: "John",
sayHi() {
alert(`سلام، ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // !John ،سلام
}, 1000);
حالا کار میکند، چون user
را از محیط لغوی بیرونی دریافت میکند و سپس به طور معمولی متد را فراخوانی میکند.
این یکسان اما کوتاهتر است:
setTimeout(() => user.sayHi(), 1000); // !John ،سلام
مناسب بنظر میرسد اما یک آسیبپذیری جزئی ممکن است در ساختار کد ما نمایان شود.
اگر قبل از اینکه setTimeout
فعال شود (تاخیر یک ثانیهای وجود دارد!) user
مقدارش تغییر کند چه؟ سپس ناگهان، شیء اشتباهی را فراخوانی میکند!
let user = {
firstName: "John",
sayHi() {
alert(`سلام، ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...در حین 1 ثانیه تغییر میکند user مقدار
user = {
sayHi() { alert("!setTimeout دیگر در user یک"); }
};
// !setTimeout دیگر در user یک
راهحل بعدی تضمین میکند که چنین چیزی اتفاق نیافتد.
راهحل 2: متد bind
تابعها یک متد درونی bind دارند که امکان ثابت کردن this
را ایجاد میکند.
سینتکس پایهای آن:
// سینتکس پیچیدهتر کمی بعدتر فرا میرسد
let boundFunc = func.bind(context);
نتیجهی func.bind(context)
یک «شیء بیگانه» تابعمانند خاص است که میتواند به عنوان تابع فراخوانی شود و به طور پنهانی فراخوانی را با تنظیم this=context
به func
منتقل کند.
به عبارتی دیگر، فراخوانی boundFunc
مانند func
با this
تثبیت شده است.
برای مثال، اینجا funcUser
فراخوانی را با this=user
به func
منتقل میکند:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
اینجا func.bind(user)
به عنوان «یک نوع پیوند زده شده» از func
با this=user
شناخته میشود.
تمام آرگومانها «بدون تغییر» به تابع اصلی func
منتقل میشوند، برای مثال:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + '، ' + this.firstName);
}
// پیوند بزن user این را به
let funcUser = func.bind(user);
funcUser("سلام"); // (this=user آرگومان «سلام» پاس داده شد و) John ،سلام
حالا بیایید با یک متد شیء امتحان کنیم:
let user = {
firstName: "John",
sayHi() {
alert(`سلام، ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// میتوانیم آن را بدون شیء اجرا کنیم
sayHi(); // !John ،سلام
setTimeout(sayHi, 1000); // !John ،سلام
// در حین 1 ثانیه تغییر کند user حتی اگر مقدار
// رجوع میکند user از مقداری که از قبل پیوند زده شده استفاده میکند که به شیء قدیمی sayHi تابع
user = {
sayHi() { alert("!setTimeout دیگر در user یک"); }
};
در خط (*)
ما متد user.sayHi
را دریافت میکنیم و آن را به user
پیوند میزنیم. sayHi
یک تابع «پیوند زده شده» است که میتواند به تنهایی فراخوانی شود یا به setTimeout
فرستاده شود – مهم نیست، زمینه همیشه درست خواهد بود.
اینجا ما میتوانیم ببینیم آرگومانهایی که پاس داده شدند «بدون تغییر» ماندند و فقط this
توسط bind
ثابت شده است:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}، ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("سلام"); // (پاس داده شد say آرگومان «سلام» به) !John ،سلام
say("خداحافظ"); // (پاس داده شد say آرگومان «خداحافظ» به) !John ،خداحافظ
bindAll
اگر یک شیء تعداد زیادی متد داشته باشد و ما بخواهیم که آن را به صورت فعال پاس بدهیم، میتوانیم تمام متدها را با شیء در یک حلقه پیوند بزنیم:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
کتابخانههای جاوااسکریپت هم تابعهایی برای پیوند زدن گسترده و راحت ارائه میدهد، مانند _.bindAll(object, methodNames) در lodash.
تابعهای جزئی
تا حالا ما فقط درباره پیوند زدن this
صحبت کردیم. بیایید این موضوع را کمی جلوتر ببریم.
ما نه تنها توانایی پیوند زدن this
را داریم، بلکه آرگومانها را هم میتوانیم پیوند بزنیم. این مورد به ندرت اتفاق میافتد اما گاهی بدرد میخورد.
سینتکس کامل bind
:
let bound = func.bind(context, [arg1], [arg2], ...);
این سینتکس اجازه میدهد که زمینه را به عنوان this
و آرگومانهای ابتدایی تابع را پیوند بزنیم.
برای مثال، ما یک تابع ضرب mul(a, b)
داریم:
function mul(a, b) {
return a * b;
}
بیایید برای ایجاد تابع double
که بر پایه تابع ضرب است از bind
استفاده کنیم:
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
فراخوانی mul.bind(null, 2)
یک تابع جدید double
میسازد که با ثابت کردن null
به عنوان زمینه و 2
به عنوان آرگومان اول، فراخوانیها را به mul
پاس میدهد. آرگومانهای بعدی «بدون تغییر» پاس داده میشوند.
این عمل، کاربرد تابع جزئی شناخته میشود – ما با ثابت کردن بعضی از پارامترهای تابع موجود، تابعی جدید میسازیم.
لطفا در نظر داشته باشید که در واقع اینجا از this
استفاده نمیکنیم. اما bind
آن را نیاز دارد پس ما باید چیزی مانند null
را درون آن قرار دهیم.
تابع triple
در کد پایین، مقدار را سه برابر میکند:
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
چرا معمولا ما یک تابع جزئی (partial function) میسازیم؟
مزیت موجود این است که ما میتوانیم یک تابع مستقل با اسمی خوانا (double
(دو برابر کردن)، triple
(سه برابر کردن)) بسازیم. میتوانیم این تابع را استفاده کنیم و چون اولین آرگومان با bind
ثابت شده است، هر بار آن را وارد نکنیم.
در موارد دیگر، استفاده از تابع جزئی زمانی خوب است که ما یک تابع خیلی عمومی داریم و برای راحتی نوعی از آن را میخواهیم که کمتر جامع باشد.
برای مثال، ما تابع send(from, to, text)
را داریم. سپس، شاید بخواهیم درون شیء user
نوع جزئی آن را استفاده کنیم: sendTo(to, text)
که از کاربر کنونی پیامی رابه کسی میفرستد.
بدون زمینه جزئی شدن
اگر ما بخواهیم آرگومانهایی را ثابت کنیم اما زمینه this
را نه چکار کنیم؟ برای مثال، برای متد شیء.
متد bind
این اجازه را نمیدهد. ما نمیتوانیم زمینه را حذف کنیم و به آرگومانها بپریم.
خوشبختانه، تابع partial
برای اینکه فقط آرگومانها را ثابت کنیم میتواند به راحتی پیادهسازی شود.
مانند این:
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// :کاربرد
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// اضافه کردن یک متد جزئی با زمان ثابت
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// :چیزی مانند این
// [10:00] John: Hello!
نتیجه فراخوانی partial(func[, arg1, arg2...])
یک دربرگیرنده (*)
است که func
را همراه با اینها فرا میخواند:
- مقدار
this
یکسان با چیزی که دریافت میکند (برای فراخوانیuser.sayNow
برابر باuser
است) - سپس
...argsBound
را به آن میدهد – آرگومانهای حاصل از فراخوانیpartial
("10:00"
) - سپس
...args
را به آن میدهد – آرگومانهایی که به دربرگیرنده داده شدهاند ("Hello"
)
پس انجام دادن آن با سینتکس اسپرد راحت است نه؟
همچنین یک پیادهسازی آماده _.partial از کتابخانه lodash وجود دارد.
Summary
متد func.bind(context, ...args)
یک «نوع پیوند داده شده» از تابع func
را برمیگرداند که زمینه this
و اولین آرگومانهای داده شده را ثابت میکند.
معمولا ما bind
را برای ثابت کردن this
در یک متد شیء بر روی آن اعمال میکنیم تا بتوانیم آن را جایی پاس دهیم. برای مثال به setTimeout
.
زمانی که ما چند آرگومان یک تابع موجود را ثابت میکنیم، تابع حاصل (که کمتر جامع است) را به طور جزئی اعمالشده یا جزئی مینامند.
تابعهای جزئی زمانی که ما نمیخواهیم آرگومان یکسانی را هر بار تکرار کنیم مناسب هستند. مثلا زمانی که ما تابع send(from, to)
را داریم و from
همیشه باید برای کار ما یکسان باشد، ما میتوانیم از آن تابع جزئی بسازیم و از این تابع استفاده کنیم.