زمانی که متدهای تابع را به عنوان 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 همیشه باید برای کار ما یکسان باشد، ما میتوانیم از آن تابع جزئی بسازیم و از این تابع استفاده کنیم.
نظرات
<code>استفاده کنید، برای چندین خط – کد را درون تگ<pre>قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)