順番に読み進めながら学べます

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(同じ理由)
同じ total でも this が分かれる
total() の本体this.items を読むcart.total()(ドットの左に cart)calcTotal()(変数に入れた)total()(分割代入で取り出した)this はcartthis はundefinedthis はundefined4980 が返るTypeErrorで止まるTypeErrorで止まる
3 つとも動かすのは同じ total の本体です。this に cart が入るのは、ドットの左に cart を書いた呼び出しだけです。

中央と右の列で表示されるのは TypeError: Cannot read properties of undefined (reading 'items') です。分割代入の const { total } = cart; も変数に関数を 1 つ取り出す書き方なので、total() は calcTotal() と同じ理由で止まります。

クリーニング店の受付で、仕上がり状況の 1 行を作るメソッドを使い回します。LaundryItem・shirt・coat は宣言済みです。

① shirt の状況の 1 行を、メソッドとして呼んで表示してください。

② shirt から取り出した同じメソッドを coat の line に入れ、coat から呼んで表示してください。

③ 同じメソッドを shirt から分割代入で取り出し、そのまま呼んでください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

変数に入れても 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
bind の戻り値と後から足した小計
cart.total.bind(cart)this を cart に決めた別の関数cart.total とは別物=== は falseboundTotal()は 4980cart.items.push(500)boundTotal() をもう一度呼ぶcart の items をその時点で読む5480 が返る
bind は cart.total を書き換えず、別の関数を返します。固定されるのは合計の値ではなく、cart そのものです。

total は引数を取りませんが、引数を受け取るメソッドを固定した場合も、戻り値に渡した引数はそのまま元のメソッドに届きます。固定されるのは this だけなので、引数は呼ぶたびに変えられます。

定期便の請求メソッドを取り出します。Subscription・coffee・tea は宣言済みです。

① coffee の bill を、this を coffee に固定して billCoffee に入れてください。

② billCoffee で 3 か月分を表示してください。

③ tea でも billTea を作り、6 か月分を表示してください。

④ [1, 12] の各要素を billCoffee で変換し、「 / 」でつないで表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

取り出しても動くメソッドを書く — アロー関数

記事のいいねボタンのように、押したときに呼ぶ関数として、同じメソッドを画面の何か所にも渡すとします。渡すたびに 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
addLike の this が決まる枠
class LikeButton の波括弧
フィールドに初期値を入れる処理(new のたびに動く)
  • likes = 0 — button に入る
  • label の初期値の式の this — 作っている button(constructor と同じ)
addLike = () => { ... } の中
  • 自分の this を持たない
  • this.likes — 外の枠の button の値を読む
  • onPress() と呼んでも this は button
addLikeMethod() { ... } の中
  • this — 呼んだ行のドットの左
  • button.addLikeMethod() なら button
  • 取り出して呼ぶと undefined
アロー関数の this は、初期値を入れる処理の this です。呼んだ行で this が変わるのは addLikeMethod だけです。

Object.keys(button) に addLike が並ぶのは、アロー関数のフィールドが likes と同じくインスタンス自身に入るためで、addLikeMethod は並びません。そのため、インスタンスを 100 件作ると addLike の関数も 100 個作られます。

オブジェクトリテラルのアロー関数は this を決めない

const counter = { add: () => { this.likes += 1 } }; の counter.add() でも this は counter を指しません。オブジェクトリテラルは this を決めず、外側の this を使います。add() { ... } と書きます。

数量ボタンの名前と、押すと呼ぶ関数を表にします。QuantityStepper・mug・buttons・pressed は宣言済みです。

① 数量を 1 増やして返す increase を、取り出しても動く形で定義してください。

② 1 減らす decrease も同じ形で書いてください。

③ pressed の順に buttons から呼び、数量を表示してください。

④ buttons.oldPlus() の結果と mug の数量を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

時間をおいて呼ばれる処理に渡す — 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);           // このページでは、表示が出そろうまで待つ
setTimeout が呼ぶときの this
setTimeout(this.showMessage)100ms 後にshowMessage を呼ぶthis にwindow が入るundefined様、ご注文を承りましたsetTimeout(() =>this.showMessage())100ms 後にアロー関数を呼ぶnotifyWithArrow のthis は notice佐藤様、ご注文を承りました
どちらも 100ms 後に setTimeout が呼びます。アロー関数の中では、notice をドットの左にして呼べます。

上の段が TypeError にならないのは、calcTotal() のように this が undefined のままではなく、window が入るためです。window は customerName を持たないので undefined と表示されます。下の表は、この記事で扱った呼び方ごとに this に入る値を並べたものです。

呼び方this に入る値この記事での結果
cart.total() とドットの左に書いて呼ぶドットの左の cart4980 が返る
変数や分割代入で取り出して calcTotal() と呼ぶundefinedTypeError で止まる
cart.total.bind(cart) の戻り値を呼ぶbind に渡した cart4980 が返る
アロー関数のフィールドを取り出して onPress() と呼ぶフィールドを持つ buttonいいねの数が増える
setTimeout(this.showMessage, 100) と渡すブラウザでは windowundefined様、ご注文を承りました
setTimeout(() => this.showMessage(), 100) と渡す外側のメソッドの this佐藤様、ご注文を承りました

function で包んでも this は戻らない

setTimeout(function () { this.showMessage(); }, 100) と包むと中の this は window になり、TypeError で止まります。このページのコンソールではエラーも出ず、開発者ツールには Uncaught TypeError と出ます。アロー関数で包みます。

ホテルの受付で、100ms 後に部屋の案内を出します。RoomGuide・tanaka・suzuki・delay は宣言済みです。

① tanaka の announce をそのまま setTimeout に渡してください。

② suzuki の announce は、this を固定してから渡してください。

③ tanaka の announce を、アロー関数で包んで渡してください。

④ 「受付を終えました」を表示し、①〜③ と比べてください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1const calc = cart.total; の後に calc() と呼ぶと、total の中の this は?

Q2cart.total.bind(cart); だけを書いた後、cart.total を変数に入れて呼ぶと?

Q3メソッドの中から setTimeout に渡したとき、this がインスタンスのまま残らない書き方は?