1
2

【JS初学者へ送る】thisとbind()を徹底解説(アロー関数との違いも)

Last updated at Posted at 2024-08-16

JavaScriptのbind()メソッド解説

1. はじめに

下記のコードでなぜthis.nameの出力結果がundefinedになるか、あなたは説明できるでしょうか?

const person = {
    name: "Alice",
    greet: function () {
        console.log("こんにちは、" + this.name + "です。");
    }
};
const greetFunction = person.greet;
greetFunction(); // 出力: こんにちは、undefinedです。

説明できなかった方この記事を読んで損はないはずです:writing_hand:

JavaScriptのbind()メソッドは、関数の挙動を制御するためのメソッドですが、初学者が非同期処理と同じくらい理解不能に陥ってしまうのがこのbind()です。

今回はbind()の使い方と概要について解説していきます。

2. 関数の呼び出し方とthisの値

まず。大前提として理解しておかないといけないことは、JavaScriptでは関数の呼び出し方によってthisの値が変わる というころです。

まずの次の見慣れた形を見ていきましょう。

2.1 オブジェクトのメソッドとして呼び出す

const person = {
  name: "Alice",
  greet: function() {
    console.log("こんにちは、" + this.name + "です。");
  }
};

person.greet(); // 出力: こんにちは、Aliceです。

この場合、thispersonオブジェクトを指します。よく見る通常のthisですね。

2.2 単独の関数として呼び出す場合

では冒頭に出てきたコードをもう一度確認してみましょう。
以下のように呼び出すとどうなるでしょうか??

const person = {
    name: "Alice",
    greet: function () {
        console.log("こんにちは、" + this.name + "です。");
    }
};
const greetFunction = person.greet;
greetFunction(); // 出力: こんにちは、undefinedです。

ご覧の通り出力結果がundefindとなってしまいます。
これは、関数(greet)がオブジェクトから切り離されて呼び出されているためthisはglobalオブジェクトを探しに行った結果、見当たらずundefindとなっています。

初心者用にさらに詳しく解説(理解できている人はスキップ可):writing_hand:

  1. 関数の切り離し

    const greetFunction = person.greet;
    

    この行で、person.greet関数をgreetFunction変数に代入しています。この時点で、関数はpersonオブジェクトから「切り離された」状態になります。

  2. 切り離された関数の呼び出し:

    greetFunction("Hello");
    

    この呼び出しでは、関数はpersonオブジェクトのメソッドとしてではなく、独立した関数として実行されます。

  3. thisの挙動

    • JavaScriptでは、関数が単独で呼び出された場合(つまり、オブジェクトのメソッドとしてではなく)、thisデフォルトでグローバルオブジェクトを参照します。
    • ただし、strict modeではこのデフォルトの動作が変わり、thisundefinedになります。
  4. undefinedの結果:

    • 関数内でthis.nameにアクセスしようとしても、thisがグローバルオブジェクト(またはstrict modeではundefined)を指しているため、nameプロパティは存在しません。
    • そのため、this.nameundefinedとなり、結果として「Hello, I'm undefined」のような出力になります。

そして、これを解決してくれるのがbind() です。

次の章で解説しますが先に簡単に予習しておきましょう。
例えば以下のように、切り離された関数に対してbind()を使用すると...

const boundGreet = person.greet.bind(person);
boundGreet("Hello"); // 正しく "Hello, I'm Alice" と出力されます

このように、bind()によってグローバルオブジェクトを探しに行くthisに対して「おいおい、そこまで探しに行かなくていいよ!この値をみればいいよ!」という風にpersonオブジェクトに固定(bind)してくれるというわけです。

3. bind()メソッドの役割

bind()メソッドは、関数のthis値を固定する新しい関数を作成します。

3.1 基本的な使い方

const boundGreet = person.greet.bind(person);
boundGreet(); // 出力: こんにちは、Aliceです。

bind(person)は、グローバルオブジェクトを探しに行くthispersonオブジェクトに固定した新しい関数を作ります。

3.2 引数の事前設定

bind()thisの固定だけでなく、引数も予め設定できます。

function greet(greeting, name) {
  console.log(greeting + ", " + name + "!");
}

const greetAlice = greet.bind(null, "こんにちは");
greetAlice("Alice"); // 出力: こんにちは, Alice!

4. bind()が役立つ場面

  1. コールバック関数で元のオブジェクトの参照を維持したい場合
  2. 一部の引数を固定した新しい関数を作成する場合
  3. イベントリスナーで特定のオブジェクトのメソッドを使用する場合

各項目を実際のコード例で見ていきましょう

4.1 コールバック関数で元のオブジェクトの参照を維持したい場合

const user = {
  name: "Alice",
  greet: function() {
    console.log("こんにちは、" + this.name + "です。");
  }
};

// bindを使用しない場合
setTimeout(user.greet, 1000);
// 1秒後に出力: こんにちは、undefinedです。

// bindを使用する場合
setTimeout(user.greet.bind(user), 1000);
// 1秒後に出力: こんにちは、Aliceです。

この例では、setTimeoutのコールバックとしてuser.greetを渡す際にbind()を使用することで、thisuserオブジェクトを正しく参照するようになります。

4.2 一部の引数を固定した新しい関数を作成する場合

function multiply(a, b) {
  return a * b;
}

// 5を固定した新しい関数を作成
const multiplyByFive = multiply.bind(null, 5);

console.log(multiplyByFive(3)); // 出力: 15
console.log(multiplyByFive(7)); // 出力: 35

この例では、bind()を使って最初の引数を5に固定した新しい関数を作成しています。nullthisの値を変更しないことを示しています。

4.3 イベントリスナーで特定のオブジェクトのメソッドを使用する場合

const calculator = {
  value: 0,
  increment: function() {
    this.value++;
    console.log("現在の値: " + this.value);
  }
};

const button = document.getElementById("incrementButton");

// bindを使用しない場合
button.addEventListener("click", calculator.increment);
// クリック時に "現在の値: NaN" と出力される

// bindを使用する場合
button.addEventListener("click", calculator.increment.bind(calculator));
// クリック時に正しく "現在の値: 1", "現在の値: 2", ... と出力される

この例では、イベントリスナーにメソッドを渡す際にbind()を使用することで、thiscalculatorオブジェクトを正しく参照するようになります。

アロー関数と通常の関数におけるthisの挙動とbind()の使用

まずbind()の説明に入る前にthisの挙動について理解しておかないといけません。
初心者がないがしろにしがちなのですが、アロー関数は通常の関数とthisの扱いが異なります。

結論から言うと、通常の関数ではthisの値は呼び出し時に決定されますが、アロー関数では関数が定義された場所(レキシカルスコープ)のthisを引き継ぎます。

詳しく見ていきましょう。

1. 通常の関数のthis

通常の関数ではthisの値は呼び出し時に決定されます。

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

const obj1 = { name: 'Alice', method: regularFunction };
const obj2 = { name: 'Bob' };

obj1.method();           // 出力: 'Alice'
regularFunction();       // 出力: undefined (strictモードでは error)
regularFunction.call(obj2); // 出力: 'Bob'

通常の関数は、呼び出し方によってthisの値が変わります。

2. アロー関数のthis

アロー関数のthisは、関数が定義された時点で決まり、後から変更できません。

const arrowFunction = () => {
  console.log(this.name);
};

const obj1 = { name: 'Alice', method: arrowFunction };
const obj2 = { name: 'Bob' };

obj1.method();           // 出力: undefined
arrowFunction();         // 出力: undefined
arrowFunction.call(obj2); // 出力: undefined

アロー関数のthisは、定義されたスコープのthisを引き継ぎます。
つまり、上記の例ではグローバルスコープに定義されたアロー関数内のthisはグローバルオブジェクトを探しに行くので参照先(name)が見当たらずundefindとなっています。

ではどうやればうまくthisがnameを参照してくれるのでしょうか??

const obj = {
  name: 'Alice',
  arrowMethod: null,
  init: function() {
    this.arrowMethod = () => {
      console.log(this.name);
    };
  }
};

obj.init();
obj.arrowMethod(); // 出力: 'Alice'

この例ではアロー関数がobjのメソッド内で定義されているため、objのthisがnameを参照してくれています。

3. bind()の使用

3.1 通常の関数でのbind()

通常の関数では、bind()を使ってthisを固定できます。

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

const obj = { name: 'Charlie' };
const boundRegular = regularFunction.bind(obj);

boundRegular(); // 出力: 'Charlie'

3.2 アロー関数でのbind()(誤った使用)

アロー関数にbind()を使用しても、thisは変更されません。

const arrowFunction = () => {
  console.log(this.name);
};

const obj = { name: 'Charlie' };
const boundArrow = arrowFunction.bind(obj);

boundArrow(); // 出力: undefined

つまり、アロー関数ではbind()を使用すべきではないということです。
それは先に説明した通り、thisの挙動が異なりbind()を使用したところで効果を発揮しないからです。
(探したら無理やり使う方法あるかも...)

3.3 アロー関数の正しい使用例

アロー関数は、外部スコープのthisを利用したい場合に有用です:

const obj = {
  name: 'Alice',
  delayedGreeting: function() {
    setTimeout(() => {
      console.log('Hello, ' + this.name);
    }, 1000);
  }
};

obj.delayedGreeting(); // 1秒後に出力: 'Hello, Alice'

この例では、setTimeoutのコールバックとしてアロー関数を使用することで、objthisを正しく参照してくれています。

通常の関数とアロー関数とで比較する

const obj = {
  name: 'Charlie',
  arrowFunc: () => {
    console.log(this.name);
  },
  regularFunc: function() {
    console.log(this.name);
  }
};

obj.arrowFunc();    // 出力: undefined (thisはグローバルスコープ)
obj.regularFunc();  // 出力: 'Charlie'

const boundArrow = obj.arrowFunc.bind(obj);
boundArrow();       // 出力: undefined (bindは効果なし)

const boundRegular = obj.regularFunc.bind({ name: 'Dave' });
boundRegular();     // 出力: 'Dave'

4. bind()が必要な場合(通常の関数)

繰り返しになりますがbind()が必要な場合は、通常の関数を使用するという考え方を持っておくのがベターです。

const user = {
  name: 'Bob',
  greet: function() {
    console.log('Hello, ' + this.name);
  }
};

const greet = user.greet;
greet(); // 出力: 'Hello, undefined'

const boundGreet = user.greet.bind(user);
boundGreet(); // 出力: 'Hello, Bob'

thisに関連する call()apply()

※初学者はスキップ可能
call()apply()メソッドは、bind()と同様に関数の呼び出し方を制御し、thisの値を明示的に設定するために使用されます。

call()メソッド

  1. 関数内で使用される this の値を明示的に設定する
  2. 関数に引数を渡す

1. 基本的な構文

function.call(thisArg, arg1, arg2, ...)
  • thisArg: 関数内で this として使用される値
  • arg1, arg2, ...: 関数に渡す引数(複数可)

2. シンプルな例

まず、call()を使わない通常の関数呼び出しを見てみましょう:

function greet() {
  console.log('こんにちは、' + this.name + 'さん');
}

const person = { name: '太郎' };

// 通常の呼び出し
greet(); // 出力: "こんにちは、undefinedさん"

ここで、call()を使用して this を設定します:

greet.call(person); // 出力: "こんにちは、太郎さん"

3. 引数を渡す例

call()this の設定だけでなく、関数に引数を渡すこともできます:

function introduce(hobby, age) {
  console.log('私は' + this.name + 'です。' + hobby + 'が好きで、' + age + '歳です。');
}

const person = { name: '花子' };

introduce.call(person, '読書', 25);
// 出力: "私は花子です。読書が好きで、25歳です。"

4. call()の実用的な使用例

4.1 配列のようなオブジェクトを本物の配列に変換

function convertToArray() {
  return Array.prototype.slice.call(arguments);
}

const args = convertToArray(1, 2, 3);
console.log(args); // 出力: [1, 2, 3]

ここでは、Array.prototype.sliceメソッドをargumentsオブジェクト上で呼び出しています。

4.2 メソッドの借用

const numbers = { 0: 10, 1: 20, 2: 30, length: 3 };

const sum = Array.prototype.reduce.call(numbers, (acc, curr) => acc + curr, 0);
console.log(sum); // 出力: 60

この例では、配列ではないオブジェクトに対して配列のメソッド(reduce)を使用しています。

5. call()と通常の関数呼び出しの違い

  1. thisの明示的な設定:
    call()を使用すると、関数内のthisの値を明示的に設定できます。

  2. 関数の即時実行:
    call()は関数を即座に実行します。

  3. 引数の渡し方:
    call()では引数を個別に列挙して渡します。

6. call()まとめ

  • call()メソッドは関数のthis値を制御し、同時に引数を渡すための強力なツールです。
  • 主に以下の場面で使用されます:
    • オブジェクトのメソッドを他のオブジェクトで使用する
    • 配列のようなオブジェクトに配列メソッドを適用する
    • 関数の借用
  • call()を理解し適切に使用することで、より柔軟で再利用可能なコードを書くことができます。

3. apply() メソッド

3.1 基本的な使い方

apply()メソッドはcall()と似ていますが、引数を配列として渡します。

構文:function.apply(thisArg, [argsArray])

function greet(greeting) {
  console.log(greeting + ', ' + this.name);
}

const person = { name: 'Charlie' };

greet.apply(person, ['Hello']); // 出力: "Hello, Charlie"

3.2 複数の引数を使用する例

function introduce(greeting, profession) {
  console.log(greeting + ', I\'m ' + this.name + ' and I\'m a ' + profession);
}

const person = { name: 'David' };

introduce.apply(person, ['Hi', 'teacher']);
// 出力: "Hi, I'm David and I'm a teacher"

4. call()apply() の違い

主な違いは引数の渡し方です:

  • call(): 引数を個別に渡します。
  • apply(): 引数を配列として渡します。
function sum(a, b, c) {
  return a + b + c;
}

console.log(sum.call(null, 1, 2, 3));    // 出力: 6
console.log(sum.apply(null, [1, 2, 3])); // 出力: 6

5. 実践的な使用例

5.1 配列の操作

const numbers = [5, 6, 2, 3, 7];

// Math.maxは通常、数値のみを引数に取りますが、
// applyを使用することで配列の要素を引数として渡せます
const max = Math.max.apply(null, numbers);

console.log(max); // 出力: 7

5.2 メソッドの借用

const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

// Array.prototypeのsliceメソッドを借用して配列に変換
const array = Array.prototype.slice.call(arrayLike);

console.log(array); // 出力: ['a', 'b', 'c']

6. 注意点

  • アロー関数では、call()apply()を使用してもthisの値は変更されません。
  • nullundefinedthisArgとして渡した場合、グローバルオブジェクト(ブラウザではwindow)がthisとして使用されます(strictモードを除く)。

5. まとめ

  1. 通常の関数:

    • thisは呼び出し時に決定される
    • bind(), call(), apply()thisを変更できる
    • オブジェクトメソッドやイベントリスナーに適している
  2. アロー関数:

    • thisは定義時に決定され、変更できない
    • bind(), call(), apply()thisに影響しない
    • コールバックやレキシカルなthisが必要な場面に適している

適切な関数の種類を選ぶことで、thisの挙動に関する多くの問題を回避できます。状況に応じて、通常の関数とアロー関数を使い分けることが重要です。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2