ما قصد داریم این پروژهٔ متنباز را در دسترس همهٔ مردم در سرتاسر دنیا قرار دهیم.
به ترجمهٔ محتوای این آموزش به زبان خودتان کمک کنید/a>.
پیوند تابع
زمانی که متدهای تابع را به عنوان callback پاس میدهیم، برای مثال به setTimeout، یک مشکل شناخته شده وجود دارد: «از دست دادن this».
در این فصل ما راههایی را برای رفع آن خواهیم دید.
از دست دادن “this”
ما از قبل درباره از دست دادن this مثالهایی را دیدهایم. زمانی که یک متد جایی به غیر از شیء خودش پاس داده شود، this از دست میرود.
چیزی که ممکن است با setTimeout اتفاق بیافتد اینجا آورده شده:
همانطور که میبینیم، خروجی “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: دربرگیرنده
سادهترین راهحل استفاده از یک تابع دربرگیرنده است:
حالا کار میکند، چون user را از محیط لغوی بیرونی دریافت میکند و سپس به طور معمولی متد را فراخوانی میکند.
این یکسان اما کوتاهتر است:
setTimeout(() => user.sayHi(), 1000); // !John ،سلام
مناسب بنظر میرسد اما یک آسیبپذیری جزئی ممکن است در ساختار کد ما نمایان شود.
اگر قبل از اینکه setTimeout فعال شود (تاخیر یک ثانیهای وجود دارد!) user مقدارش تغییر کند چه؟ سپس ناگهان، شیء اشتباهی را فراخوانی میکند!
راهحل بعدی تضمین میکند که چنین چیزی اتفاق نیافتد.
راهحل 2: متد bind
تابعها یک متد درونی bind دارند که امکان ثابت کردن this را ایجاد میکند.
سینتکس پایهای آن:
// سینتکس پیچیدهتر کمی بعدتر فرا میرسد
let boundFunc = func.bind(context);
نتیجهی func.bind(context) یک «شیء بیگانه» تابعمانند خاص است که میتواند به عنوان تابع فراخوانی شود و به طور پنهانی فراخوانی را با تنظیم this=context به func منتقل کند.
به عبارتی دیگر، فراخوانی boundFunc مانند func با this تثبیت شده است.
برای مثال، اینجا funcUser فراخوانی را با this=user به func منتقل میکند:
اینجا func.bind(user) به عنوان «یک نوع پیوند زده شده» از func با this=user شناخته میشود.
تمام آرگومانها «بدون تغییر» به تابع اصلی func منتقل میشوند، برای مثال:
حالا بیایید با یک متد شیء امتحان کنیم:
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 ثابت شده است:
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 استفاده کنیم:
فراخوانی mul.bind(null, 2) یک تابع جدید double میسازد که با ثابت کردن null به عنوان زمینه و 2 به عنوان آرگومان اول، فراخوانیها را به mul پاس میدهد. آرگومانهای بعدی «بدون تغییر» پاس داده میشوند.
این عمل، کاربرد تابع جزئی شناخته میشود – ما با ثابت کردن بعضی از پارامترهای تابع موجود، تابعی جدید میسازیم.
لطفا در نظر داشته باشید که در واقع اینجا از this استفاده نمیکنیم. اما bind آن را نیاز دارد پس ما باید چیزی مانند null را درون آن قرار دهیم.
تابع triple در کد پایین، مقدار را سه برابر میکند:
چرا معمولا ما یک تابع جزئی (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 همیشه باید برای کار ما یکسان باشد، ما میتوانیم از آن تابع جزئی بسازیم و از این تابع استفاده کنیم.
تمارین
خروجی چه خواهد بود؟
function f() {
alert( this ); // ?
}
let user = {
g: f.bind(null)
};
user.g();
آیا میتوانیم با پیوند زدن اضافی this را تغییر دهیم؟
خروجی چه خواهد بود؟
function f() {
alert(this.name);
}
f = f.bind( {name: "John"} ).bind( {name: "Ann" } );
f();
جواب: John.
شیء بیگانه تابع پیوند زده شده که توسط f.bind(...) برگردانده شده، زمینه (و در صورت قرار دادن، آرگومانها) را فقط در زمان ایجاد شدن به یاد میسپارد.
یک تابع نمیتواند دوباره پیوند زده شود.
فراخوانی askPassword() در کد پایین باید رمز یا چک کند و سپس با توجه به جواب user.loginOk/loginFail را فراخوانی کند.
اما به ارور برمیخورد. چرا؟
خط برجسته شده را تصحیح کند تا همه چیز به درستی کار کند (بقیه خطوط نیازی به تغییر ندارند).
به دلیل اینکه ask تابعهای loginOk/loginFail را بدون شیء دریافت میکند ارور ایجاد میشود.
زمانی که این تابع آنها را فرا میخواند، به طور طبیعی آنها this=undefined را فرض میکنند.
بیایید زمینه را با bind پیوند بزنیم:
function askPassword(ok, fail) {
let password = prompt("رمز؟", '');
if (password == "rockstar") ok();
else fail();
}
let user = {
name: 'John',
loginOk() {
alert(`${this.name} وارد شد`);
},
loginFail() {
alert(`${this.name} نتوانست وارد شود`);
},
};
askPassword(user.loginOk.bind(user), user.loginFail.bind(user));
حالا کار میکند.
راهحل جایگزین میتواند این باشد:
//...
askPassword(() => user.loginOk(), () => user.loginFail());
معمولا این راهحل هم کار میکند و ظاهر خوبی دارد.
اگرچه این کد در موقعیتهای پیچیدهتر کمتر قابل اطمینان است، زمانی که متغیر user ممکن است بعد از اینکه askPassword فراخوانی شود و قبل از اینکه کاربر جواب بدهد و () => user.loginOk() را فرا بخواند، تغییر کند.
این تمرین نوع پیچیدهتر تابعی که "this" را از دست میدهد را تصحیح کنید است.
شیء user تغییر داده شد. حالا به جای دو تابع loginOk/loginFail، یک تابع user.login(true/false) دارد.
برای اینکه askPassword در کد پایین، تابع user.login(true) را به عنوان ok و user.login(false) را به عنوان fail فراخوانی کند باید چه کار کنیم؟
function askPassword(ok, fail) {
let password = prompt("رمز؟", '');
if (password == "rockstar") ok();
else fail();
}
let user = {
name: 'John',
login(result) {
alert( this.name + (result ? ' وارد شد' : ' نتوانست وارد شود') );
}
};
askPassword(?, ?); // ?
تغییرات شما فقط باید قطعه برجسته شده را تغییر دهد.
-
برای کوتاه بودن یا از تابع دربرگیرنده استفاده کنید یا از تابع کمانی:
askPassword(() => user.login(true), () => user.login(false));حالا
userرا از متغیرهای بیرونی دریافت میکند و به صورت معمولی آن را اجرا میشود. -
یا یک تابع جزئی از
user.loginبسازید که ازuserبه عنوان زمینه استفاده میکند و آرگومان اول درست را دارد:askPassword(user.login.bind(user, true), user.login.bind(user, false));
- © 2007—2026 Ilya Kantor
- دربارهٔ پروژه
- تماس با ما
نظرات
<code>استفاده کنید، برای چندین خط – کد را درون تگ<pre>قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)