[JavaScript講座] this 完全理解とクロージャ

当ページのリンクには広告が含まれています。
目次

this とは何か ― 「スコープ」との違い

まず一番大事なポイント:

this は「関数が どう呼ばれたか で決まる特別な値」。
スコープのように「どこに書いたか」で決まるものではない。

よくある誤解:

  • this は“定義されているオブジェクト”を指す」 → ❌
  • 「クラスっぽいから Java / C++ と同じ感覚でOK」 → ❌(似てるけどルールが違う)

this は 「呼び出し方(コールサイト)」 で決まる、というところを軸にします。

this が決まる 5 つのパターン

  1. 通常呼び出し:func()
  2. メソッド呼び出し:obj.method()
  3. コンストラクタ呼び出し:new Func()
  4. 明示的バインド:func.call(obj) / func.apply(obj) / func.bind(obj)
  5. アロー関数:this を持たず、外側の this を引き継ぐ(レキシカル this)

順番に見ていきます(strict モード前提)。

通常呼び出し func() の this

"use strict";

function showThis() {
  console.log(this);
}

showThis(); // undefined
  • strict モードでは、ただの関数呼び出しの thisundefined
  • 非 strict だとグローバルオブジェクト(window 等)になる

strict 前提で書くなら、

func() 単体呼び出しの this は基本 undefined

と覚えてOKです。

メソッド呼び出し obj.method() の this

"use strict";

const obj = {
  value: 42,
  show: function () {
    console.log(this.value);
  },
};

obj.show(); // 42
  • obj.show() と呼ぶと、this は「ドットの左側のオブジェクト」= obj

関数を別のオブジェクトにコピーすると?

const obj2 = {
  value: 100,
  show: obj.show,
};

obj2.show(); // 100
  • 同じ関数でも、「どのオブジェクトから呼ばれたか」で this が変わる

関数だけ取り出して呼ぶと?

const fn = obj.show;
fn(); // this は undefined(通常呼び出し扱い)

this は「所有者」ではなく「呼び出し方」で決まる というのが重要です。

コンストラクタ呼び出し new Func() の this

"use strict";

function Person(name) {
  this.name = name;
}

const p = new Person("Taro");
console.log(p.name); // "Taro"

new Person("Taro") のとき、内部的には:

  1. 新しい空オブジェクト {} を作る
  2. そのオブジェクトを this にセットした状態で Person を呼ぶ
  3. this.name = name でプロパティを追加
  4. 何も return しなければ、その this が返ってくる

なので、

Person("Taro"); // ← new を付け忘れると…

strict モードでは thisundefined なので、
this.name = ...TypeError になります。

call / apply / bind による明示的な this 指定

call

"use strict";

function showName() {
  console.log(this.name);
}

const user = { name: "Taro" };
const admin = { name: "Admin" };

showName.call(user);  // "Taro"
showName.call(admin); // "Admin"
  • 形式:func.call(thisArg, arg1, arg2, ...)
  • thisArgthis にして func を 即座に実行 する

apply

function sum(a, b) {
  console.log(this.label, a + b);
}

const ctx = { label: "result:" };

sum.apply(ctx, [2, 3]); // "result: 5"
  • apply は 引数を配列で渡すバージョンの call
  • ES2015 以降はスプレッド構文があるので、call + ... で代用しやすい:
sum.call(ctx, ...[2, 3]);

bind

function greet() {
  console.log(`Hello, ${this.name}`);
}

const greetUser = greet.bind({ name: "Taro" });

greetUser(); // Hello, Taro
greetUser(); // 何度呼んでも this は固定
  • func.bind(thisArg, arg1, ...) は、
    this と一部の引数を固定した「新しい関数」 を返す

典型パターン:

const button = {
  label: "保存",
  onClick() {
    console.log(this.label);
  },
};

const handler = button.onClick.bind(button);
// handler をイベントリスナーに渡す、など

アロー関数の this(レキシカル this)

「自分の this を持たない」

アロー関数は、

自分固有の this を持たず、「外側の this」をそのまま使う

という仕様です。

"use strict";

const obj = {
  value: 42,
  show: function () {
    const inner = () => {
      console.log(this.value);
    };
    inner();
  },
};

obj.show(); // 42
  • show の中の thisobj
  • inner(アロー関数)は「外側スコープの this」を引き継ぐ
    inner 内の thisobj

コールバックでの典型パターン

const counter = {
  value: 0,
  start() {
    setInterval(() => {
      this.value++;
      console.log(this.value);
    }, 1000);
  },
};

counter.start();
  • もしコールバックを function () { ... } で書くと、
    その中の thisundefined(strict)や window になってしまう
  • アロー関数を使えば、外側の this(counter)を素直に引き継げる

メソッド自体をアロー関数にするのは危険

"use strict";

const obj = {
  value: 42,
  show: () => {
    console.log(this.value);
  },
};

obj.show(); // undefined(外側がグローバル or モジュールの this)
  • アロー関数は「外側の this」を参照するので、
    オブジェクトリテラル内メソッドでアローを使うと、obj を指さない ことが多い

実務指針:

  • メソッド本体は function / メソッド記法 show() {} を使う
  • メソッドの中で使うコールバックにはアロー関数を使う

クロージャとは何か(実行コンテキストとの関係)

クロージャの定義

クロージャは:

「関数が、自分の外側スコープの変数を“記憶”している状態」

= 「関数 + その関数がキャプチャしている環境」です。

もう少し踏み込むと:

「ある関数が、実行終了後も外側スコープの変数への参照を保持し続けるとき、
その関数はクロージャになっている」

一番典型的な例:カウンタ

function createCounter() {
  let count = 0; // createCounter のローカル変数

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();

counter(); // 1
counter(); // 2
counter(); // 3

何が起きているか:

  1. createCounter() 呼び出し → createCounter 実行コンテキストが作られる
  2. その中に let count = 0; がある
  3. return function () { ... } で 内部関数 を返す
  4. createCounter の実行自体は終了するが、
    戻り値の関数(counter)が count への参照を保持している
  5. そのため count を含む環境が GC されずに延命される

→ 「内部関数+延命された外側の環境」= クロージャ

createCounter を2回呼ぶと別インスタンスになる

const c1 = createCounter();
const c2 = createCounter();

c1(); // 1
c1(); // 2

c2(); // 1
c2(); // 2
  • createCounter() を呼ぶたびに、新しい count を持った環境が生まれる
  • 各関数は、自分専用の count をキャプチャしている

クロージャの実用パターン

ID発行器

function createIdGenerator(start = 1) {
  let current = start;

  return function () {
    const id = current;
    current++;
    return id;
  };
}

const gen = createIdGenerator(100);

console.log(gen()); // 100
console.log(gen()); // 101
console.log(gen()); // 102
  • current は外部から直接触れない「プライベート状態」
  • 返された関数を通してだけ更新できる

プライベート変数を持つオブジェクト

function createUser(name) {
  let _name = name; // 外から見えない「プライベート」

  return {
    getName() {
      return _name;
    },
    setName(newName) {
      _name = newName;
    },
  };
}

const user = createUser("Taro");

console.log(user.getName()); // "Taro"
user.setName("Hanako");
console.log(user.getName()); // "Hanako"

console.log(user._name); // undefined(直接はアクセスできない)
  • createUser のスコープ内に _name を閉じ込める
  • 外部からは getter / setter 経由でしか触れない

class + private フィールド(#name)でも同様のことができるけど、
関数 + クロージャだけでも同じ発想が実現できる、という例です。

ループとクロージャの罠(var と let)

var を使ったときの典型的な失敗

const funcs = [];

for (var i = 0; i < 3; i++) {
  funcs.push(function () {
    console.log(i);
  });
}

funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3

理由:

  • var i は「関数スコープ」なので、ループ全体で 1 つだけ 存在
  • ループ終了時点で i は 3
  • 3 つの無名関数は、みんな 同じ i を参照している
    → どれを呼んでも 3

let なら意図どおり

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs.push(function () {
    console.log(i);
  });
}

funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
  • let i はブロックスコープ
  • さらに ES2015 の仕様で、「for ループの let は各周回ごとに別の束縛を作る」 と決まっている
  • 各関数は「その周回での i」をクロージャとして捕まえている

実務指針:

  • ループ変数に var は使わない
  • 基本 for (let i = ... にしておけば、この罠は避けられる

this とクロージャの使い分け

状態を持つロジックは、

  • this(インスタンスプロパティ)で持つ
  • クロージャの中に閉じ込める

どちらでも書けます。

this で持つ(クラス的な設計)

class Counter {
  constructor(initial = 0) {
    this.value = initial;
  }

  increment() {
    this.value++;
  }
}

const c = new Counter(10);
c.increment();
console.log(c.value); // 11

特徴:

  • インスタンスごとに状態を持つ
  • 継承(extends)やポリモーフィズムと相性が良い
  • 状態は原則「public」なので、c.value = 999; もできてしまう(隠蔽したい場合は #value

クロージャで持つ(カプセル化重視)

function createCounter(initial = 0) {
  let value = initial;

  return {
    increment() {
      value++;
    },
    getValue() {
      return value;
    },
  };
}

const c = createCounter(10);
c.increment();
console.log(c.getValue()); // 11

// c.value; // undefined(外から直接触れない)

特徴:

  • value は外部から完全に隠せる(API 経由のみ)
  • 小さなモジュール・ヘルパー関数には書きやすい
  • プロトタイプ継承などは使わないシンプル構造

ざっくりした指針

  • ライブラリ / クラス設計 / 継承を使うようなオブジェクト指向設計 → class / this
  • 小さなユーティリティ・プライベート状態を持つモジュール → クロージャ

どちらも「状態を持つ」という意味では似ていますが、
設計の方向性(オブジェクト指向 vs 関数+カプセル化) が違うイメージです。


<<前へ(スコープと実行コンテキスト)

>>次へ(オブジェクトとプロトタイプ)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次