Q1const calc = cart.total; の後に calc() と呼ぶと、total の中の this は?
this の束縛 — 呼び方で変わる this と固定の仕方
メソッドを変数に取り出したり、コールバックに渡したりすると変わる this の決まり方を扱います。取り出したときの TypeError、bind で固定する書き方、アロー関数のフィールド、setTimeout に渡すときの注意を学べます。
カートの合計を出すメソッドを、別の処理に渡して後から呼ぶことがあります。cart.total() で動くメソッドも、変数に入れたり setTimeout に渡したりすると、this が変わって止まることがあります。
この記事では、呼び方で決まる this の束縛 と、bind やアロー関数で this を固定する書き方を扱います。
取り出して呼ぶと止まる — メソッドの this
カートの合計を出す cart.total() を合計欄の表示にも使おうと、const calcTotal = cart.total; と変数に入れたとします。calcTotal() と呼ぶと、カートの中身は同じなのに、合計を計算する前に止まります。
メソッドの中の this に入る値は、書いた場所ではなく呼んだ行で決まり、これを this の束縛(this に値が結び付くこと)と呼びます。() を付けずに cart.total と読むと、返るのは関数だけで cart は付いてきません。
ドットの左が無い calcTotal() では、class の中の this は undefined です。
class Cart {
constructor(items) {
this.items = items; // 商品ごとの小計
}
total() {
return this.items.reduce((sum, price) => sum + price, 0);
}
}
const cart = new Cart([1980, 3000]);
// ドットの左に cart を書いて呼ぶ
console.log(cart.total()); // 4980
// 変数に入れる / 分割代入で取り出す
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal); // function
// calcTotal(); // TypeError: Cannot read properties of undefined (reading 'items')
// total(); // TypeError(同じ理由)
中央と右の列で表示されるのは TypeError: Cannot read properties of undefined (reading 'items') です。分割代入の const { total } = cart; も変数に関数を 1 つ取り出す書き方なので、total() は calcTotal() と同じ理由で止まります。
変数に入れても this を保つ — bind メソッド
合計欄を表示する処理に、合計の計算を関数として渡したいとします。前の章のとおり cart.total を変数に入れただけでは cart が付いてこないので、渡した先で呼ばれた時点で TypeError になります。
bind(どの関数も持っているメソッドで、this を引数の値に固定した別の関数を返す)は cart.total.bind(cart) と書きます。返った関数は、ドットの左が無い形で呼んでも this が cart のままです。
固定されるのは返った関数だけなので、呼ぶときや渡すときは戻り値のほうを使います。
class Cart {
constructor(items) { this.items = items; } // 商品ごとの小計
total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);
// 取り出しただけの関数は、呼ぶと this が undefined
const unbound = cart.total;
// unbound(); // TypeError
// bind は this を cart に決めた別の関数を返す
const boundTotal = cart.total.bind(cart);
console.log(boundTotal()); // 4980
console.log(boundTotal === cart.total); // false
// 決まっているのは cart そのもの。後から足した小計も読む
cart.items.push(500);
console.log(boundTotal()); // 5480
total は引数を取りませんが、引数を受け取るメソッドを固定した場合も、戻り値に渡した引数はそのまま元のメソッドに届きます。固定されるのは this だけなので、引数は呼ぶたびに変えられます。
取り出しても動くメソッドを書く — アロー関数
記事のいいねボタンのように、押したときに呼ぶ関数として、同じメソッドを画面の何か所にも渡すとします。渡すたびに bind を書くと、1 か所書き忘れただけで、そのボタンだけが TypeError で止まります。
アロー関数のフィールド(クラスフィールドの初期値にアロー関数を書いたもの)は、new のたびにインスタンスごとに作られます。初期値の式の中の this は constructor と同じく新しいインスタンスで、自分の this を持たないアロー関数はこの this を使います。
取り出して呼ぶときも bind は要りません。
class LikeButton {
likes = 0;
label = `いいね ${this.likes}`; // 初期値の式の this は、作っている button
// 通常のメソッド: this は呼んだ行で決まる
addLikeMethod() { this.likes += 1; return this.likes; }
// アロー関数のフィールド: 初期値の式と同じ this を使う
addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label); // いいね 0
// bind を書かずに取り出して呼ぶ
const onPress = button.addLike;
console.log(onPress()); // 1
console.log(onPress()); // 2
console.log(Object.keys(button).join(", ")); // likes, label, addLike
// 通常のメソッドを取り出すと止まる
const onPressMethod = button.addLikeMethod;
// onPressMethod(); // TypeError
likes = 0—buttonに入るlabelの初期値の式のthis— 作っているbutton(constructor と同じ)
- 自分の
thisを持たない this.likes— 外の枠のbuttonの値を読むonPress()と呼んでもthisはbutton
this— 呼んだ行のドットの左button.addLikeMethod()ならbutton- 取り出して呼ぶと
undefined
Object.keys(button) に addLike が並ぶのは、アロー関数のフィールドが likes と同じくインスタンス自身に入るためで、addLikeMethod は並びません。そのため、インスタンスを 100 件作ると addLike の関数も 100 個作られます。
オブジェクトリテラルのアロー関数は this を決めない
const counter = { add: () => { this.likes += 1 } }; の counter.add() でも this は counter を指しません。オブジェクトリテラルは this を決めず、外側の this を使います。add() { ... } と書きます。
時間をおいて呼ばれる処理に渡す — setTimeout
注文を確定した 100ms 後に「◯◯様、ご注文を承りました」と表示するとします。メソッドの中で setTimeout(this.showMessage, 100) と書くと、100ms 後に文言は出ますが、お客様の名前が undefined になります。
setTimeout は受け取った関数を 100ms 後に自分で呼び、ブラウザではそのとき this に window(ページごとに 1 つある、ブラウザが用意したオブジェクト)を入れます。アロー関数で包むと、() => this.showMessage() の this は外側のメソッドの this なので、インスタンスで呼べます。
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
class OrderNotice {
constructor(customerName) { this.customerName = customerName; }
showMessage() { console.log(`${this.customerName}様、ご注文を承りました`); }
// メソッドをそのまま渡す。100ms 後に呼ぶのは setTimeout
notifyDirect() { setTimeout(this.showMessage, 100); }
// アロー関数で包む / bind で決めてから渡す
notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}
const notice = new OrderNotice("佐藤");
notice.notifyDirect(); // undefined様、ご注文を承りました
notice.notifyWithArrow(); // 佐藤様、ご注文を承りました
notice.notifyWithBind(); // 佐藤様、ご注文を承りました
await delay(300); // このページでは、表示が出そろうまで待つ
上の段が TypeError にならないのは、calcTotal() のように this が undefined のままではなく、window が入るためです。window は customerName を持たないので undefined と表示されます。下の表は、この記事で扱った呼び方ごとに this に入る値を並べたものです。
| 呼び方 | this に入る値 | この記事での結果 |
|---|---|---|
| cart.total() とドットの左に書いて呼ぶ | ドットの左の cart | 4980 が返る |
| 変数や分割代入で取り出して calcTotal() と呼ぶ | undefined | TypeError で止まる |
| cart.total.bind(cart) の戻り値を呼ぶ | bind に渡した cart | 4980 が返る |
| アロー関数のフィールドを取り出して onPress() と呼ぶ | フィールドを持つ button | いいねの数が増える |
| setTimeout(this.showMessage, 100) と渡す | ブラウザでは window | undefined様、ご注文を承りました |
| setTimeout(() => this.showMessage(), 100) と渡す | 外側のメソッドの this | 佐藤様、ご注文を承りました |
function で包んでも this は戻らない
setTimeout(function () { this.showMessage(); }, 100) と包むと中の this は window になり、TypeError で止まります。このページのコンソールではエラーも出ず、開発者ツールには Uncaught TypeError と出ます。アロー関数で包みます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2cart.total.bind(cart); だけを書いた後、cart.total を変数に入れて呼ぶと?
Q3メソッドの中から setTimeout に渡したとき、this がインスタンスのまま残らない書き方は?