۲۴ اکتبر ۲۰۲۱

پیوند تابع

زمانی که متدهای تابع را به عنوان 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 همیشه باید برای کار ما یکسان باشد، ما می‌توانیم از آن تابع جزئی بسازیم و از این تابع استفاده کنیم.

تمارین

اهمیت: 5

خروجی چه خواهد بود؟

function f() {
  alert( this ); // ?
}

let user = {
  g: f.bind(null)
};

user.g();

جواب: null.

function f() {
  alert( this ); // null
}

let user = {
  g: f.bind(null)
};

user.g();

زمینه‌ی تابع پیوند زده شده به طور قطعی ثابت شده. راهی برای تغییر بیشتر آن وجود ندارد…

پس حتی زمانی که ما user.g() را اجرا می‌کنیم، تابع اصلی با this=null فراخوانی می‌شود.

اهمیت: 5

آیا می‌توانیم با پیوند زدن اضافی this را تغییر دهیم؟

خروجی چه خواهد بود؟

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f();

جواب: John.

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Pete"} );

f(); // John

شیء بیگانه تابع پیوند زده شده که توسط f.bind(...) برگردانده شده، زمینه (و در صورت قرار دادن، آرگومان‌ها) را فقط در زمان ایجاد شدن به یاد می‌سپارد.

یک تابع نمی‌تواند دوباره پیوند زده شود.

اهمیت: 5

یک مقدار در ویژگی تابعی وجود دارد. آیا بعد از bind تغییر می‌کند؟ چرا یا چرا نه؟

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;

let bound = sayHi.bind({
  name: "John"
});

alert( bound.test ); // خروجی چه خواهد بود؟ چرا؟

جواب: undefined.

نتیجه bind شیء دیگری است. آن شیء ویژگی test را ندارد.

اهمیت: 5

فراخوانی askPassword() در کد پایین باید رمز یا چک کند و سپس با توجه به جواب user.loginOk/loginFail را فراخوانی کند.

اما به ارور برمی‌خورد. چرا؟

خط برجسته شده را تصحیح کند تا همه چیز به درستی کار کند (بقیه خطوط نیازی به تغییر ندارند).

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, user.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() را فرا بخواند، تغییر کند.

اهمیت: 5

این تمرین نوع پیچیده‌تر تابعی که "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(?, ?); // ?

تغییرات شما فقط باید قطعه برجسته شده را تغییر دهد.

  1. برای کوتاه بودن یا از تابع دربرگیرنده استفاده کنید یا از تابع کمانی:

    askPassword(() => user.login(true), () => user.login(false));

    حالا user را از متغیرهای بیرونی دریافت می‌کند و به صورت معمولی آن را اجرا می‌شود.

  2. یا یک تابع جزئی از user.login بسازید که از user به عنوان زمینه استفاده می‌کند و آرگومان اول درست را دارد:

    askPassword(user.login.bind(user, true), user.login.bind(user, false));
نقشه آموزش