JavaScriptはゆるい言語??thisが表すのは何でしょう?

この記事では、JavaScriptの言語仕様の「ゆるさ」について説明し、同時に、thisの挙動についても説明していきます。

JavaScriptの引数チェックはゆるい!

今日の授業では、JavaScriptの「ゆるさ」について、説明していきます。
まずは、引数チェックがゆるいです!

function greeting(name) {
  console.log(`こんにちは、${name}さん!`);
}

greeting("佐藤"); // 正常:こんにちは、佐藤さん!
greeting("田中", "よろしく!"); // 余分な引数も許可:こんにちは、田中さん!
greeting(); // 引数不足でもエラーにならない:こんにちは、undefinedさん!

他の言語だと引数の数が合わないとエラーになることが多いですが、JavaScriptは「まあ、いいか」という感じです。でも、この緩さが時々バグの原因になるので注意が必要です!

JavaScriptは、同名関数は上書きされちゃう!

JavaScriptでは、同じ名前の関数を複数定義すると、後から定義したものが有効になります。

function introduce(name) {
  console.log(`私の名前は${name}です。`);
}

function introduce(name, age) {
  console.log(`私の名前は${name}で、${age}歳です。`);
}

introduce("山田"); // 出力:私の名前は山田で、undefinedです。
introduce("鈴木", 17); // 出力:私の名前は鈴木で、17歳です。

JavaScriptでは、同名関数は、最後に定義した関数だけが残るので、気をつけましょう!

JavaScriptで関数の引数をチェックする方法

引数の数や型をチェックするのは良い習慣です。ES5以前では以下のコードのように、argumentsオブジェクトを使っていました。(ES6以降ではもっと簡単な方法があります。)

function add(a, b) {
  // 引数の数をチェック
  if (arguments.length !== 2) {
    throw new Error('引数は2つ必要です!');
  }
  
  // 引数の型をチェック
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('引数は数値でなければいけません!');
  }
  
  return a + b;
}

try {
  console.log(add(5, 3)); // 出力:8
  console.log(add(5)); // エラー:引数は2つ必要です!
  console.log(add("5", 3)); // エラー:引数は数値でなければいけません!
} catch (error) {
  console.error(error.message);
}

ES6以降での引数チェックのモダンな方法

1. デフォルト引数を使う

引数の不足を防ぐために、デフォルト引数が便利です。引数が省略された場合でも、適切な初期値を設定できます。

// 引数に = ●● とつけることで、デフォルト値を設定できる
function add(a = throwIfMissing(), b = throwIfMissing()) {
  return a + b;
}

// 引数が省略された場合にエラーを投げる関数を作る
function throwIfMissing() {
  throw new Error('引数は必須です!');
}

try {
  console.log(add(2, 3)); // 出力:5
  console.log(add(2));     // エラー:引数は必須です!
} catch (error) {
  console.error(error.message);
}
ポイント
  • デフォルト引数内で関数を呼び出し、引数不足をチェックしています。
  • throwIfMissing のような関数を使うことで、コードが簡潔になります。
デフォルト引数のその他の使用例

デフォルト引数を使うと、引数が省略されたときに使用される値を指定できます。
書き方は簡単で、設定したいデフォルト値を=”文字列”などと記入するだけです。

function greet(name = "ゲスト", time = "昼") {
  console.log(`${time}の挨拶:こんにちは、${name}さん!`);
}

greet(); // 出力:昼の挨拶:こんにちは、ゲストさん!
greet("佐藤"); // 出力:昼の挨拶:こんにちは、佐藤さん!
greet("田中", "夜"); // 出力:夜の挨拶:こんにちは、田中さん!
デフォルト引数の利点:

関数が複数の引数を受け取る場合、残余引数を使うことで柔軟にチェックが可能です。

  1. コードが簡潔になる:以前は関数内で引数のチェックとデフォルト値の設定を行っていましたが、それが不要になります。
  2. 関数の呼び出しが柔軟になる:必須でない引数を省略できるようになります。
  3. 可読性が向上する:関数の定義を見るだけで、どの引数にどんなデフォルト値があるかが分かります。
デフォルト引数の注意点:
  • デフォルト値は、その引数が undefined のときだけ使用されます。null や空文字列、0 などは有効な値として扱われます。

2.残余引数(レストパラメータ)を使う

残余引数(レストパラメータ)を使うと、不特定多数の引数を配列として受け取ることができます
その名の通り、「残りの余った引数」という意味です。実際のコードを見たほうがイメージしやすいと思います。せっかくなので、デフォルト引数も使っています。

// ...membersの部分が残余引数(レストパラメータ)を表す
function createTeam(captain, viceCaptain = "未定", ...members) {
  console.log(`キャプテン: ${captain}`);
  console.log(`副キャプテン: ${viceCaptain}`);
  console.log(`メンバー: ${members.join(', ')}`);
}

createTeam("佐藤");
// 以下、出力内容
// キャプテン: 佐藤
// 副キャプテン: 未定
// メンバー: 

createTeam("田中", "鈴木", "高橋", "伊藤", "渡辺");
// 以下、出力内容
// キャプテン: 田中
// 副キャプテン: 鈴木
// メンバー: 高橋, 伊藤, 渡辺

この例では、captain は必須、viceCaptain はデフォルト値があり、残りの全ての引数は members 配列に格納されます。残余引数の使い方のイメージはこれで掴めましたか?

では、この残余引数を使った引数のチェック方法の一例をご紹介します。

function multiply(factor, ...numbers) {
  if (numbers.length < 1) {
    throw new Error('少なくとも1つの数値が必要です!');
  }
  
   return numbers.map(function(n) {
   return n * factor;
 });
}

try {
  console.log(multiply(2, 1, 2, 3)); // 出力:[2, 4, 6]
  console.log(multiply(2));           // エラー:少なくとも1つの数値が必要です!
} catch (error) {
  console.error(error.message);
}

JavaScriptのthisを理解しよう!

JavaScriptでは、thisは「関数やメソッドがどの文脈(環境)で呼び出されたかを指し示す特別なキーワード」です。呼び出し方によってthisが指す対象が変わるので、5つのパターンに分けて見ていきます。以下が、5つのパターンです。

  1. メソッド呼び出しパターン
  2. 関数呼び出しパターン
  3. コンストラクタ呼び出しパターン
  4. call/apply呼び出しパターン
  5. アロー関数のthis

1.メソッド呼び出しパターン

オブジェクト内の関数(メソッド)を呼び出すと、thisはそのオブジェクトを指します。

const student = {
  name: "佐藤",
  introduce: function() {
    console.log(`私は${this.name}です!`);
  }
};

student.introduce();  // 出力: 私は佐藤です!

解説:上記のthisは、メソッドが呼ばれたstudentオブジェクト」を指します。

2.関数呼び出しパターン

関数として呼び出すと、thisはデフォルトで「グローバルオブジェクト(window)」を指します。

function showThis() {
  console.log(this);  // 通常はwindowを指す
}

showThis();  // 出力: [object Window]

解説:単独の関数呼び出しでは、thiswindow(ブラウザの場合)を指します。
strictモードではthisundefinedになります。

3.コンストラクタ呼び出しパターン

classの中にコンストラクタ(this入り)を準備して、newを使ってインスタンスを作ると、thisは「そのインスタンス」を指します。

class Student {
  constructor(name) {
    this.name = name;
  }

  introduce() {
    console.log(`私の名前は${this.name}です!`);
  }
}

const studentA = new Student("田中");
studentA.introduce();  // 出力: 私の名前は田中です!

解説classを使うと、constructor内でのthisは新しく作られたインスタンスを指します。この場合、studentAというインスタンスが作られ、そのnameプロパティに”田中”が設定されます。

4. call / apply 呼び出しパターン

callの使い方
function 関数名(thisとして使いたいオブジェクト, ...引数);
  • 第1引数thisに渡したいオブジェクト
  • 第2引数以降:関数の本来の引数をカンマ区切りで渡します

よって、以下のように、callを使ってthis明示的に自分で設定できます。

const teacher = { name: "先生" };
const student = { name: "生徒" };

function greetStudent(studentName, subject) {
  console.log(`${this.name}:こんにちは、${studentName}さん!${subject}を頑張ってね!`);
}

// teacherオブジェクトをthisに渡し、"佐藤"と"数学"を引数に渡す
greetStudent.call(teacher, "佐藤", "数学");  
// 出力: 先生:こんにちは、佐藤さん!数学を頑張ってね!

// studentオブジェクトをthisに渡してみる
greetStudent.call(student, "田中", "英語");  
// 出力: 生徒:こんにちは、田中さん!英語を頑張ってね!
callの仕組みを分解して説明
  1. teacherオブジェクト
    • teachernameプロパティを持ち、"先生"がセットされています。
  2. greetStudent関数
    • この関数はthis.namestudentNameを使って、メッセージを表示する仕組みです。
      しかし、thisが何を指すかは関数の呼び出し方次第で変わります。
  3. call(teacher, "佐藤")とは?
    • greetStudent.call(teacher, "佐藤")は、次のことを実行しています:
      • teacherオブジェクトをthisとして関数に渡す
      • “佐藤”をstudentNameの引数として渡す
  4. なぜcallを使うのか?
    • greetStudentはteacherオブジェクトに属していない関数ですが、callを使えばあたかもteacherの一部であるかのように利用できます。
    • 柔軟性が高い:オブジェクトに関数を追加することなく、その場で特定のオブジェクトと紐づけて使えます。

callとapplyの違い

  • call:引数をカンマ区切りで渡す
  • apply:引数を配列で渡す

先ほどのコードをapplyで書くとすると以下のようになります。
違いは、カンマ区切りで書くか、配列で書くかの違いです。

greetStudent.apply(teacher, ["佐藤", "数学"]);  
// 出力: 先生:こんにちは、佐藤さん!数学を頑張ってね!

5. アロー関数のthis

アロー関数は、thisの振る舞いが通常の関数(function)と異なるため、特にオブジェクトのメソッドとして使うときに注意が必要です。詳しく説明します!

まず、違いのイメージを掴んでください。
例えを使って説明すると:

普通の関数のthisは、「その場その場で上司が変わる派遣社員」のようなものです。 派遣される場所によって、指示を受ける相手(this)が変わります。

アロー関数のthisは、「最初に決めた上司の下でずっと働く正社員」のようなものです。 どこで働いても、最初に決まった上司(this)の指示に従います。

少し長いですが、派遣社員(普通の関数のthis)正社員(アロー関数のthis)の例えをコードで具体化に比較してみましょう。

1.派遣社員(普通の関数)のパターン
// 派遣社員の例
const 会社 = {
  社長: "山田社長",
  派遣社員の仕事: function() {
    console.log("--- 派遣社員の場合 ---");
    console.log("直接呼び出し時のthis:", this);  // this = 会社
    console.log("上司は" + this.社長);  // "上司は山田社長"
  //↑上記のthisたちはメソッド呼び出しパターンのthisなので、
 //  thisはオブジェクトである「会社」を指している
    
    setTimeout(function() {
      console.log("1秒後のthis:", this);  // this = Window
      console.log("上司は" + this.社長);  // "上司はundefined"(上司を見失う😱)
    }, 1000);
  //↑このsetTimeout関数は、派遣社員の仕事関数の中に入っている関数である
  //よって、このthisたち関数呼び出しパターンのthisなので、Windowを指すことになる

  }
};

解説:このように派遣社員(普通の関数)は、特に非同期の場合(setTimeout関数など)で上司を見失いやすいので、setTimeout関数やsetInterval関数を使うときに注意が必要です。場所が変わるとthisも変わるというと特性を覚えておきましょう!

2.正社員(アロー関数)のパターン
// 正社員の例
const 会社 = {
  社長: "山田社長",
  正社員の仕事: function() {
    console.log("--- 正社員の場合 ---");
    
    // 直接の仕事のとき
    console.log("直接呼び出し時のthis:", this);  // this = 会社
    console.log("上司は" + this.社長);  // "上司は山田社長"

    // 別の場所での仕事のとき
    setTimeout(() => {
      console.log("1秒後のthis:", this);  // this = 会社(変わらない!)
      console.log("上司は" + this.社長);  // "上司は山田社長"(ちゃんと上司を覚えている😊)
    }, 1000);
  }
};

解説:このように、正社員(アロー関数)は、1度決まったthisはその後、どこで仕事をしても変わらない。よって、setTimeout関数やsetInterval関数を使ったときでも、最初の上司をちゃんと覚えている!といった感じです。

実務でこのthisの違いが特に重要になるのは、

  • イベントハンドラー
  • setTimeout/setInterval
  • Promise
  • コールバック関数   

などの非同期処理を使うときと知っておきましょう。

まとめ

JavaScriptの「this」は、シンプルに見えますが、呼び出し方によって挙動が変わるため、特に初心者にとっては混乱しやすいトピックです。本記事では、thisの5つの主要なパターンを紹介し、それぞれの違いを詳しく説明しました。また、アロー関数のthisが他の関数とは異なり、「定義時に決まったスコープを参照し続ける」という性質を持つことも学びました。

JavaScriptの柔軟な性質は、使いやすさを提供する一方で、意図しない挙動を引き起こすリスクもあります。特に、非同期処理やイベントハンドラー、コールバック関数の中でthisが変わるケースには注意が必要です。そのため、アロー関数やcall/applyを活用することで、意図したthisの参照先をしっかり制御することが重要です。

JavaScriptは「ゆるさ」も魅力の一つですが、その分しっかりと挙動を理解し、状況に応じたベストプラクティスを使い分けることが求められます。これらの知識を活かして、より安定したコードを書けるよう、引き続き学習を進めていきましょう!