「”thisが〇〇だから▲▲”みたいな話が出てくるけど何を言っているのかわからない」
「JavaScriptの学習でbindが登場するけど使うタイミングがよくわからない」
「addEventListenerなどのイベント時の関数の実行が上手く動作しないことがある」
本日はそんな方に向けて関数におけるbindの使い方を解説していきます。
またbindを使う上でthisを知らないといけないので、thisについても一緒に理解できる記事になるはずです。
さらに動画もあるのでわかりづらい部分は動画でも確認しながら進めていただければと思います。
thisはオブジェクトのことで、bindはthisを切り替えるためのメソッド
まず結論から言うとJavaScriptではthisというものがあり、こちらはオブジェクトのことを表します。
そしてbindは「定義した関数をどのthisに対して実行するか?」を切り替えるためのメソッドです。
例えば以下のコードがあったとして、関数introduceはオブジェクトのnameプロパティの値をコンソールに出力するものです。
function introduce() {
console.log(`${this.name}さん`);
}
const person1 = {
name: "山田",
};
const person2 = {
name: "鈴木",
};
const hello = introduce.bind(person1);
hello();
nameプロパティを持つオブジェクトはperson1とperson2の2つあり、「関数introduceはどっちのオブジェクトでも使いたい」とします。
そうした場合にconsole.log()にあるように「this.name」と書くルールになっています。
わかりやすく言い換えると「this.name」とすることで、「person1.name」「person2.name」のどちらにも変換できるような仕様になります。
引数に似ていて動的に値を代入できるので便利ですね。
よってthisとはオブジェクトのことを指します、意外と単純な仕組みではないでしょうか?
「this.name」とした部分のthisの中身を切り替えるにはbindメソッドを使います。
bnidメソッドの引数にthisにしたいオブジェクトを指定する使い方です。
function introduce() {
// bindから渡されたオブジェクトがthisに入る
console.log(`${this.name}さん`);
}
// thisになる候補
const person1 = {
name: "山田",
};
// thisになる候補
const person2 = {
name: "鈴木",
};
// bindの引数にオブジェクトを指定する
const hello = introduce.bind(person1);
hello();
上記のコードではbindにperson1を指定しているのでコンソールには「山田さん」と出力されます。
bindの引数をperson2に変えてみましょう。
function introduce() {
console.log(`${this.name}さん`);
}
const person1 = {
name: "山田",
};
const person2 = {
name: "鈴木",
};
// ここを変更
const hello = introduce.bind(person2);
hello();
thisになるオブジェクトを切り替えたので「鈴木さん」になりましたね。
なんとなくbindとthisについてイメージができたのではないでしょうか?
またbindには関数の引数も一緒に指定することができます。
// ここを修正
function introduce(age) {
console.log(`${this.name}さんは${age}歳です`);
}
const person1 = {
name: "山田",
};
const person2 = {
name: "鈴木",
};
// ここを修正
const hello = introduce.bind(person1, 25);
hello();
関数introduceにageという引数を設定して、引数ageはperson1とperson2の中身には関係ないものとします。
そうした場合にはbindの第一引数にthisを、第二引数に関数の引数(age)を入れればOKです。
thisを切り替えた上で引数ageも使用できていますね。
bindとthisを使った実装例
それではもう少し実践的なプログラムでのbindとthisの使用例を紹介します。
HTMLとJavaScriptを以下のように作ります。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body>
<input type="text" class="result-form" />
<div class="items">
<p>選択一覧</p>
<ul class="item-area"></ul>
</div>
</body>
</html>
const resultForm = document.querySelector(".result-form");
const itemArea = document.querySelector(".item-area");
const itemList = [
{
id: 1,
title: "item1",
},
{
id: 2,
title: "item2",
},
{
id: 3,
title: "item3",
},
];
function loadEvent() {
for (let item of itemList) {
let el = document.createElement("li");
el.innerHTML = `
<span>${item.title}</span>
<button class="btn">選択</button>
`;
itemArea.append(el);
const btn = el.querySelector("button");
btn.addEventListener("click", selectItem.bind(item.id));
}
}
loadEvent();
function selectItem(objId) {
const selectedItem = itemList.find((obj) => {
return obj.id === objId;
});
resultForm.value = selectedItem.title;
}
最初の画面は上図のような形になり、選択一覧からクリックしたボタンのタイトルが入力欄に表示されるものを想定しました。
こちら結論から言うと、上記のコードでは上手く動作しません。
コンソールを確認しているとエラーになっているのもわかります。
エラー文と動作の様子から、選択一覧からどれをクリックしているのかが上手く判別できていないようです。
クリックした時の動作はbuttonタグに対してaddEventListenerでイベントを監視して、クリックされた場合にコールバック関数として関数selectItemを実行します。
関数selectItemは引数に3つの選択肢の中からidで判別することを作っていました。
const resultForm = document.querySelector(".result-form");
const itemArea = document.querySelector(".item-area");
// 選択一覧(idプロパティでどの選択肢かを判別できる)
const itemList = [
{
id: 1,
title: "item1",
},
{
id: 2,
title: "item2",
},
{
id: 3,
title: "item3",
},
];
function loadEvent() {
for (let item of itemList) {
let el = document.createElement("li");
el.innerHTML = `
<span>${item.title}</span>
<button class="btn">選択</button>
`;
itemArea.append(el);
const btn = el.querySelector("button");
// buttonタグがクリックされたらコールバックで関数selectItemを実行する
btn.addEventListener("click", selectItem.bind(item.id));
}
}
loadEvent();
// 関数selectItemは引数にIDを渡す
function selectItem(objId) {
const selectedItem = itemList.find((obj) => {
return obj.id === objId;
});
resultForm.value = selectedItem.title;
}
ここでポイントになるのが関数selectIetmがコールバックで実行される点です。
スコープを考える必要があり、上記の書き方だと「btn.addEventListener(“click”, selectItem.bind(item.id));」としても関数selectItemはスコープ外なので上手く渡せないのです。
addEventLisntenerはあくまでbuttonタグに使用しており、buttonタグには「selectItem」なんてものは無いからです。
もう少し端的に言うと「thisがスコープ外」と言うことです。
「thisってオブジェクトのことなのでは?」と思われた方がいるかもしれませんが、JavaScriptではありとあらゆるものはオブジェクトになっています。
関数もHTMLタグも全て立派なオブジェクトです。
またスコープについては本件に限らずいろんな場面で登場する重要な概念なので初めての方は以下の記事も参考にしてください。
現状はbuttonタグのaddEvnetListenerでは関数selectItemがスコープ外のため上手く呼び出せないです。
そこでスコープの範囲をプログラム全体に拡大してbuttonタグからでも関数selectItemを呼び出せるようにする必要があり、そんな場合にもbindメソッドを使うことができます。
実はbindメソッドはthisの切り替えと併せて、「thisのグローバルスコープ化」と言う機能も持っています。
公式ドキュメントでも紹介されているようにbindの引数にnull,undefinedを渡すと、自動的にthisが特定のオブジェクトを指すのではなくプログラム全体にすることができます。
プログラムを以下のように書き換えてみます。
const resultForm = document.querySelector(".result-form");
const itemArea = document.querySelector(".item-area");
const itemList = [
{
id: 1,
title: "item1",
},
{
id: 2,
title: "item2",
},
{
id: 3,
title: "item3",
},
];
function loadEvent() {
for (let item of itemList) {
let el = document.createElement("li");
el.innerHTML = `
<span>${item.title}</span>
<button class="btn">選択</button>
`;
itemArea.append(el);
const btn = el.querySelector("button");
// ここを修正
btn.addEventListener("click", selectItem.bind(null, item.id));
}
}
loadEvent();
function selectItem(objId) {
const selectedItem = itemList.find((obj) => {
return obj.id === objId;
});
resultForm.value = selectedItem.title;
}
ボタンをクリックすると選択肢がフォームに表示されるようになりましたね。
bindではthisになるオブジェクトを入れる代わりにnullを入れるとthisのスコープ問題を解決できます。
またbindにnullを指定した場合には、第二引数に関数の引数も指定できます。
「bind( null, 関数名(引数))」のような感じですね。
このようにbindはそもそもthisの切り替えのために使用するものであるのと同時に、thisのスコープ問題を解決することにも使用できます。
ここまで読んできた方のなかで「bindを使ってるコードってあまり見かけないよな」と思った方がいるかもしれません。
主な要因としては近年のJavaScriptはアロー関数で関数を書くのが主流になっていることにあります。
実は何気なく使っているアロー関数には「thisのグローバル化」の機能が備わっているんです。
そのため本記事で紹介したようなbindメソッドを使わなくとも、関数を最初からアロー関数の書き方にしておけばthisのスコープ問題は気にしなくて良くなっているわけです。
bindメソッドは少し古い文法になってきていて、ReactなどのモダンJSではあまり使わないのが現状です。
とはいえ仕組みは知っておく方が良いので勉強して損はないですよ。