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

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 の中の thisundefined です。

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) と書きます。返った関数は、ドットの左が無い形で呼んでも thiscart のままです。

固定されるのは返った関数だけなので、呼ぶときや渡すときは戻り値のほうを使います。

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 のたびにインスタンスごとに作られます。初期値の式の中の thisconstructor と同じく新しいインスタンスで、自分の 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 = 0button に入る
  • label の初期値の式の this — 作っている button(constructor と同じ)
addLike = () => { ... } の中
  • 自分の this を持たない
  • this.likes — 外の枠の button の値を読む
  • onPress() と呼んでも thisbutton
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() でも thiscounter を指しません。オブジェクトリテラルは 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 後に自分で呼び、ブラウザではそのとき thiswindow(ページごとに 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() のように thisundefined のままではなく、window が入るためです。windowcustomerName を持たないので 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) と包むと中の thiswindow になり、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 がインスタンスのまま残らない書き方は?