JavaScriptの関数(中級編)

jsコード JavaScript

本記事では、JavaScriptの関数について(中級編)と題し、関数の基礎(*1)を学習済の方を対象とした内容を解説していきます。

*1:ここでいう「関数の基礎」とは、JavaScript入門メニューの「JavaScriptの関数(基礎編)」の内容を指します。

関数の種類

JavaScriptの関数は第一級オブジェクトであり、「JavaScriptの関数(基礎編)」でもお伝えしているとおり、変数への代入や引数としての受け渡し、戻り値として返却することもできます。

関数には種類があり、書式や挙動、役割が異なります。
まずは、「関数宣言」と「関数式」に分けられ、いずれの形式でも記述できる特殊な関数として「ジェネレータ(Generator)関数」や「クロージャ(Closure)」があります。

  • 関数宣言(function宣言)
    • functionキーワードで始まり、関数名を付与する
    • コンストラクタ関数に成り得る
  • 関数式
    • 関数名を付与しなくても良い(無名関数)
    • 主に変数に代入されたり、即時関数として使用される
    • functionキーワードを使用しない書式がある(アロー関数)
  • どちらの形式でも記述可能な特殊な関数
    • クロージャ(Closure)
    • ジェネレータ関数(Generator)

関数宣言(function宣言)

関数宣言については、「JavaScriptの関数(基礎編)」で詳しく説明しています。

重要な個所を繰り返し説明するとともに、基礎編で解説していない内容を説明します。

関数宣言の書式

function 関数名([引数ひきすう1[, 引数2, 引数3…]]) {
// 処理
}

[]は、省略可能を表します。

よって、👆上記の書式は、「引数は省略可能であり、複数指定することができる」ことを示しています。

コンストラクタとしての関数

コンストラクタ関数は、オブジェクトを生成するための特別な関数です。
通常、new キーワードと共に使用され、新しいオブジェクトのプロパティやメソッドを初期化する役割を持ちます。

基本的なコンストラクタ関数のサンプル

コンストラクタ関数サンプル
function Person(name, age) {
  this.name = name; 
  this.age = age;
}

const jedi = new Person("Luke", 20);
console.log(jedi.name); // 出力: "Luke"

この例では、Person 関数がコンストラクタとして定義され、new キーワードと共に呼び出されています。this は、新しく生成されるオブジェクト(この場合は jedi)を指します。

コンストラクタ関数の注意点

コンストラクタ関数内での this の挙動にはいくつかの注意点があります。

  • 誤って new を使わずコンストラクタ関数呼び出した場合
    • 通常関数として呼び出されてしまうため、this がグローバルオブジェクトを指します。これは意図しないグローバル変数の作成やエラーが発生するため注意が必要です。
  • プロトタイプチェーン
    • コンストラクタ関数で生成されたオブジェクトは、プロトタイプチェーンを介してメソッドやプロパティを継承します。

関数の巻き上げ(ホイスティング)

関数を呼び出す際、その関数が「関数宣言」として定義されており、スコープ内に存在している前提であれば、「関数の巻き上げ(ホイスティング)」という仕様により、呼び出すコードよりも後ろに記述できます。

関数宣言のスコープは、自身が宣言された関数内(最上位で宣言された場合はプログラム全体)です。

Functionコンストラクタによる関数生成

大文字の「F」から始まるFunctionコンストラクタは、JavaScriptで新しい関数を動的に作成するために使用される特別なコンストラクタです。

Functionコンストラクタは関数を文字列として定義し、その文字列を実行時に評価するため、様々な問題を孕んでいます。

Function コンストラクターによる関数の生成は推奨されません。これは、文字列として関数本体が必要であり、JS エンジンによる最適化を妨げたり他の問題を引き起こしたりする場合があるためです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions#function_%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%BC

👆上記MDNの記述について、もう少し詳しく説明します。

「JSエンジンによる最適化を妨げる」理由とは

Functionコンストラクタを使用すると、関数の定義が文字列として扱われます。

これは、JavaScriptエンジンがコードを事前に解析し、最適化するプロセス(Just-In-Timeコンパイル)を妨げる可能性があります。

文字列としての関数は、実行時に解釈されるため、エンジンが効率的に最適化を行うことが難しくなります。

「他の問題」とは具体的に何があるのか

Functionコンストラクタが「JSエンジンによる最適化を妨げる」以外の問題点としては、次のものが挙げられます。

  • セキュリティリスク: Functionコンストラクタは文字列による関数定義であるため、不正なコードの挿入(インジェクション攻撃)を許す可能性があります。
  • デバッグの難しさ: デバッグ時にコードの可読性(読みやすさ)が低下し、エラーの特定が難しくなります。
  • パフォーマンスの低下: 実行時に解釈されるため、パフォーマンスが低下する可能性があります。

関数式

続いて「関数式」について説明します。

関数は、式としても作成できます。これを関数式と呼びます。

関数式の関数には、名前を付ける必要がありません。(付けても良い)

無名関数

名前のない関数を「無名関数」と呼びます。

無名関数を呼びだしたい時、名前がないと呼べません。
そのため、主に次の3通りの使い方があります。

  1. 変数に代入しておき、変数名で呼び出す。
  2. 関数の引数として渡す。
  3. 即時関数として呼び出す。

1. 変数に代入しておき、その変数名で呼び出す

無名関数を変数に代入する使用例
const func = function(x) {
  return x * 2;
};

console.log( func(6) );
デベロッパーツールのコンソール
> 12

関数式で作成された関数は、「関数の巻き上げ」の対象外となるため注意が必要です。

2. 関数の引数として渡す

これは2通りの渡し方があります。

  • 関数を変数に代入して渡す
  • 変数に代入せず、関数をまるごと渡す

それぞれをサンプルで示します。

関数を変数に代入して引数として渡す使用例
/*
 * 関数を引数として受け取る関数
 * 第一引数の f が関数
 */
function acceptFunc(f, num) {
  return f(num);
}

// 👇この関数を引数として渡す
const func = function(x) {
  return x * 2;
};

console.log( acceptFunc(func, 6) );
変数に代入せず、関数をまるごと渡す使用例
/*
 * 関数を引数として受け取る関数
 * 第一引数が関数
 */
function acceptFunc(f, num) {
  return f(num);
}

// 無名関数をまるごと引数として渡している
console.log( acceptFunc(function(x) {
  return x * 2;
}, 6) );

3. 即時関数(即時実行関数式)として呼び出す

関数式を無名関数として定義して、その場で呼び出す「使い捨て」のような使い方があります。
正式には「即時実行関数」(IIFE:Immediately Invoked Function Expression)と言いますが、略して「即時関数」と呼ばれることが多いです。

即時関数を使用する主な目的は、スコープの汚染を防ぐためです。

関数は、末尾に丸カッコを付加することで「呼び出し」の意味を持つため、無名関数を次のように記述すると、定義したと同時に呼び出すことができます。

(function() {
  console.log("Hello");
})();

👆無名関数を()で括り、その後ろに()を付加して呼び出しています。

即時関数は引数を持たせることもできます。

(function() {
  console.log( messege );
})( "Hello" ); // 👈出力:Hello

アロー関数

アロー関数は、ES6(ECMAScript 2015)で導入された、JavaScriptの新しい関数定義方法です。
従来の関数表記を簡潔にし、特に this の扱いを改善することでコールバック関数やイベントハンドラーで非常に便利です。

アロー関数の構文

アロー関数は、function キーワードの代わりに矢印(=>)を使用します。
以下に、同じ機能を持つ関数を通常の「関数宣言」と「アロー関数」で作成して対比します。

アロー関数の構文
// 👇こちらは通常の関数
function add(a, b) {
    return a + b;
}

// 👇こちらがアロー関数
const add = (a, b) => a + b;

引数を囲むカッコの省略ルール

  • 引数が1つの場合
    • 丸カッコは省略可能
  • 引数がない、あるいは複数の場合
    • 丸カッコは省略不可

アロー関数と通常の関数式との違い

アロー関数と、functionキーワードを使用した通常の関数式との違いは次の通りです。

  • アロー関数自身には「this」、「arguments」、「super」へのバインドがない。
    👉 アロー関数は、自身の thisargumentssuper を持たず、外側のスコープからそれらを継承します。この特性により、アロー関数はコールバック関数やイベントリスナーでよく使用されます。
  • アロー関数はコンストラクターとして使用できない。
    👉 new を使用して呼び出すとTypeErrorが発生します。
  • アロー関数は本体内でのyieldの使用、およびGenerator関数として作成できない。

argumentsとは、関数の中でのみ利用できるオブジェクトで、関数に渡された値をすべて格納しています。

superとはJavaScriptのキーワードで、親オブジェクトのメソッドを呼び出すために使用できます。

特殊な関数 クロージャ(Closure)

クロージャ(Closure)とは

「関数内に作成された関数であればクロージャである」と認識される場合も多いですが、ある関数がクロージャであるためには、次の2つの条件を満たしている必要があります。

  1. 関数内に作られていること
    • 内側の関数が外側の関数のスコープ内に作成されている必要があります。
  2. 関数内の関数が外側のスコープにある変数を参照していること
    • 内側の関数が外側の関数のスコープにある変数にアクセスしている必要があります。
      このアクセスにより、内側の関数が外側のスコープを「覚えている」状態がクロージャとなります。

よって、クロージャとは「関数と、その関数が定義されたときのスコープ情報をセットにしたもの」と言えます。

関数が外部の変数にアクセスできるため、データのカプセル化やプライベート変数の実現に役立ちます。

いくつかのサンプルプログラムを例示します。

よく見かけるカウンターのクロージャ

function createCounter() {
    let count = 0; // カウントを保持する変数

    return function() {
        count ++; // 外側スコープのcountをインクリメント
        return count; // 現在のcountを返す
    };
}

// 👇createCounterによりクロージャが返されcounterに代入される
const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

もう少し実用的なサンプル

ウェブサービスにログインする際のフォームで、ユーザー名とパスワードの入力内容をクロージャを用いて検証するサンプルです。

フォームバリデータのサンプル
function createValidator() {
    const errors = [];

    return {
        validateUsername(username) {
            if (!username || username.length < 5) {
                errors.push("ユーザー名は5文字以上必要です。");
            }
        },
        validatePassword(password) {
            if (!password || password.length < 8) {
                errors.push("パスワードは8文字以上必要です。");
            }
        },
        getErrors() {
            return errors;
        },
        clearErrors() {
            errors.length = 0;
        }
    };
}

const validator = createValidator();
validator.validateUsername('user'); // エラー追加
validator.validatePassword('1234567'); // エラー追加

console.log(validator.getErrors()); 
// ["ユーザー名は5文字以上必要です。", "パスワードは8文字以上必要です。"]

👆上記の例では、errors配列はクロージャを通じて値が保持され、validateUsernamevalidatePasswordメソッドを使用してエラーメッセージを追加できます。

また、getErrorsメソッドでエラーメッセージを取得し、clearErrorsメソッドでエラーメッセージをクリアすることができます。

まとめ

本記事では、JavaScriptの関数の中級レベルとして、関数宣言と関数式に始まり、特殊な関数であるクロージャについて、それぞれの特性や使用方法を詳しく解説しました。
皆さまの関数に対する理解が深まれば幸いです。


本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。

それでは、最後までお読みいただきありがとうございます。

『詳説 JavaScript』メニューに戻る

コメント

タイトルとURLをコピーしました