JavaScript/TypeScript

this の振る舞いを整理する

5
  • JavaScript

this の振る舞いを整理する — JavaScript Tips

this は定義場所ではなく、呼び出し方(call site) で決まります。メソッド・newcall / bind・アロー(レキシカル)の違いが、コールバックやイベントで this が消える理由になります。

参考(MDN)


目次

  1. this は呼び出し方で決まる
  2. 代表的な 4 パターン
  3. アロー関数の this
  4. クラスと this
  5. bind / call / apply
  6. コールバックで this が失われる問題
  7. うまくいかないとき

this は呼び出し方で決まる

多くの言語では this は「今のオブジェクト」に固定されますが、JavaScript では 関数がどう呼ばれたかthis が決まります(アロー関数は例外)。定義した場所ではなく、実行した瞬間の文脈 が重要です。

function showThis() {  console.log(this);}showThis(); // 非 strict なスクリプトでは window(ブラウザ)や global(Node)

同じ関数でも、呼び出し方を変えると this が変わります。

const obj = {  name: "田中",  show() {    console.log(this.name);  },};obj.show(); // "田中" — メソッド呼び出しconst fn = obj.show;fn(); // undefined またはエラー — 単なる関数呼び出し

代表的な 4 パターン

1. 単なる関数呼び出し

function foo() {  "use strict";  return this;}foo(); // strict モードでは undefined

モジュール(ESM)は自動的に strict 相当なので、トップレベル関数の thisundefined になりやすいです。

2. メソッド呼び出し

const counter = {  value: 0,  increment() {    this.value += 1;    return this.value;  },};counter.increment(); // 1 — this は counter

3. new 呼び出し

function Person(name) {  this.name = name;}const p = new Person("佐藤");console.log(p.name); // "佐藤" — this は新しいインスタンス

4. call / apply / bind による明示指定

function greet(greeting) {  return `${greeting}, ${this.name}`;}const user = { name: "鈴木" };console.log(greet.call(user, "こんにちは")); // "こんにちは, 鈴木"

アロー関数の this

アロー関数は 自身の this を持ちません。外側のレキシカルスコープの this をそのまま使います(lexical this)。

const timer = {  seconds: 0,  start() {    setInterval(() => {      this.seconds += 1;      console.log(this.seconds);    }, 1000);  },};timer.start(); // this は timer を指す

通常の関数をコールバックに渡すと this が変わります。

const broken = {  seconds: 0,  start() {    setInterval(function () {      // this は broken ではない(ブラウザでは window など)      this.seconds += 1;    }, 1000);  },};

アロー関数向き — 外側の this を保ちたいコールバック

通常の関数向き — メソッド定義、new で使うコンストラクタ、動的な this が必要なとき

const obj = {  // メソッドは通常の関数が無難  method() {    return this;  },  // アローは this を外側から借りる — メソッドとしては非推奨なことが多い  arrow: () => this,};

クラスと this

クラスのメソッド内では、通常 this はインスタンスを指します。

class Stack {  constructor() {    this.items = [];  }  push(item) {    this.items.push(item);  }  pop() {    return this.items.pop();  }}const s = new Stack();s.push(1);console.log(s.pop()); // 1

メソッドを変数に取り出すと メソッド呼び出しではなくなる ため、this が失われます。

const pop = s.pop;// pop(); // TypeError — this が undefined など

bind / call / apply

bind — this を固定した新しい関数を返す

const user = {  name: "山田",  greet() {    return `こんにちは、${this.name}`;  },};const greet = user.greet.bind(user);console.log(greet()); // "こんにちは、山田"

bind即座に実行せず、あとから何度でも呼べる関数を返します。イベントリスナーや setTimeout にメソッドを渡すときに使います。

class Button {  constructor(label) {    this.label = label;    this.handleClick = this.handleClick.bind(this);  }  handleClick() {    console.log(`クリック: ${this.label}`);  }}

call — 即座に実行、引数は個別に渡す

function introduce(age, city) {  return `${this.name}、${age}歳、${city}`;}introduce.call({ name: "太郎" }, 30, "東京");

apply — 即座に実行、引数は配列で渡す

introduce.apply({ name: "花子" }, [25, "大阪"]);// 可変長引数の最大値(例)const numbers = [3, 7, 2];Math.max.apply(null, numbers); // 7 — 今はスプレッドの Math.max(...numbers) が一般的
関数 実行タイミング 引数の渡し方
bind 後で実行 後から渡す
call すぐ実行 カンマ区切り
apply すぐ実行 配列

コールバックで this が失われる問題

DOM イベントや配列メソッドにメソッドを渡すと、classic な this 問題が起きます。

class List {  constructor(items) {    this.items = items;  }  logAll() {    this.items.forEach(function (item) {      // this は List インスタンスではない      console.log(item, this);    });  }}

解決策 1 — アロー関数

logAll() {  this.items.forEach((item) => {    console.log(item, this.items); // 外側の this  });}

解決策 2bind

document.getElementById("btn").addEventListener(  "click",  this.handleClick.bind(this));

解決策 3 — クラスフィールドのアロー(インスタンスごとに 1 関数)

class App {  onReady = () => {    console.log(this);  };}

bind は明示的、アローは簡潔、クラスフィールドはコンストラクタでの bind を省略できます。プロジェクトではどれか一つに寄せると迷いにくいです。


うまくいかないとき

オブジェクトリテラルでメソッドを変数にした

const api = {  base: "/api",  fetch(endpoint) {    return `${this.base}${endpoint}`;  },};const get = api.fetch;get("/users"); // ESM では this が undefined のため TypeError

メソッドを変数へ取り出すと、api をレシーバーにした呼び出しではなくなります。ESM のような strict モードでは this 自体が undefined なので、this.base を読んだ時点で例外になります。非 strict なスクリプトでは this がグローバルオブジェクトになるため、base が無ければ文字列に undefined が入ります。

アロー関数をオブジェクトのメソッドにした

const o = {  value: 42,  getValue: () => this.value, // this は o ではなく外側から引き継ぐ};console.log(o.getValue()); // ESM では this.value の評価時に TypeError

アロー関数は呼び出し元のオブジェクトを this にしません。ESM のトップレベルでは外側の thisundefined なので、上の例は undefined を返すのではなく例外になります。ブラウザの非モジュールスクリプトなど、外側の this がオブジェクトなら、そのオブジェクトの value を読みます。

bind したあと元のメソッドも呼んで二重登録

element.addEventListener("click", this.onClick);element.addEventListener("click", this.onClick.bind(this));// 別関数として 2 回登録される

コンストラクタで一度だけ bind し、同じ参照を使い回します。

call と apply の取り違え

引数が配列のときは apply(またはスプレッド付き call)。

fn.apply(ctx, [a, b, c]);fn.call(ctx, ...[a, b, c]);

厳格モードで意図しない undefined

function alone() {  "use strict";  console.log(this); // undefined}

モジュール全体が strict なので、グローバル関数の this に依存した古いパターンは動きません。

アロー関数を new しようとしている

const F = () => {};// new F(); // TypeError — アロー関数はコンストラクタに使えない

呼び出し方と this(早見)

状況 this の向き先(目安)
obj.method() obj

シェア