JavaScriptの関数を基礎から応用まで完全マスターしよう🧑‍💻

公開日 :

  • コーディング

こんにちは、AndHAコーディング部です。

前回の「初心者が知るべきJavaScriptの演算子とその使い方✍️」の解説に続き、今回はJavaScriptにおける中核的な概念である「関数」について詳しく見ていきましょう。

関数は再利用可能なコード片をひとまとめにし、プログラム全体の構造を整理する上で欠かせない存在です。本記事では、初心者の方にもわかりやすいように、JavaScriptの関数の基礎から応用までを丁寧に解説していきます!

関数とは

関数とは、特定の処理をまとめたコードのかたまりです。
一度定義しておけば何度でも呼び出して使うことができます。これによりコードの再利用性が高まり、プログラム全体の可読性と保守性が向上します。

関数を定義する

JavaScriptで関数を定義する方法として、「関数宣言」と「関数式の2つがあります。

例)関数宣言

function sayHello() {
  console.log("Hello!");
}
sayHello(); // 出力: Hello!

例)関数式

const sayHello = function() {
  console.log("Hello!");
};
sayHello(); // 出力: Hello!

これら関数宣言と関数式の主な違いは、スコープとホイスティング(巻き上げ)の振る舞いにあります。

関数宣言はホイスティングされるため宣言よりも前に関数を呼び出すことができますが、関数式の場合はそうはいきません。また、関数宣言で定義された関数はグローバルスコープに属するのに対し、関数式で定義された関数はその変数が属するスコープに属します。

これらの違いを理解しておくと関数の使い分けがより適切にできるようになります。以下、詳しく解説します。

スコープとは 🤔

スコープとは、変数や関数が有効な範囲のことです。
JavaScriptには、グローバルスコープローカルスコープ(関数スコープ)に加え、ブロックスコープもあります。

グローバルスコープとは

スクリプト全体から参照できるグローバル変数やグローバル関数が存在する範囲です。

Global scope (グローバルスコープ) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

ローカルスコープとは

関数の中で定義された変数や関数が有効な範囲です。
ローカルスコープ内の変数は、その関数の外からはアクセスできません。ただしネストした関数の内側から、外側の関数のスコープにある変数を参照することはできます。

let globalVar = "グローバル変数"; 

function exampleFunction() {
  let localVar = "ローカル変数";
  console.log(globalVar); // 出力: グローバル変数
  console.log(localVar); // 出力: ローカル変数
}

exampleFunction();
console.log(globalVar); // 出力: グローバル変数
console.log(localVar); // ReferenceError: localVarは定義されていない

Local scope (ローカルスコープ) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

ブロックスコープとは

ブロックスコープは、{ }で囲まれた範囲(ifやforなどのブロック)の中で定義された変数に適用され、ブロックの外からはアクセスできません。

letconstで宣言された変数はブロックスコープの影響を受けますが、varで宣言された変数はブロックスコープを無視します。ブロックスコープの理解もJavaScriptのスコープ関連の知識を深めるうえで重要です。

{
  // ブロックスコープの始まり
  let blockScopeVar = 'ブロックスコープの変数';
  console.log(blockScopeVar); // 出力: ブロックスコープの変数

  if (true) {
    let insideIfVar = 'ifブロック内の変数';
    console.log(insideIfVar); // 出力: ifブロック内の変数
  }

  console.log(insideIfVar); // ReferenceError: insideIfVarは定義されていない
}
// ブロックスコープの終わり

console.log(blockScopeVar); // ReferenceError: blockScopeVarは定義されていない

for (let i = 0; i < 3; i++) {
  let forBlockVar = `forループ内の変数 ${i}`;
  console.log(forBlockVar);
}

console.log(forBlockVar); // ReferenceError: forBlockVarは定義されていない

ホイスティング(巻き上げ)とは

ホイスティングとは、変数宣言や関数宣言をコードの先頭に引き上げるJavaScriptエンジンの振る舞いのことです。実際にはコードが物理的に移動するわけではありませんが、エンジンはそのように解釈します。

関数宣言はホイスティングされるため、宣言よりも前に関数を呼び出すことができます。一方、varで宣言された変数はホイスティングされますが、宣言より前にアクセスするとundefinedになります。

これは変数の宣言部分だけがホイスティングされ、初期化は後からなされるためです。letconstで宣言された変数はホイスティングされず、宣言前にアクセスするとエラーになります。

console.log(varVariable); // 出力: undefined
var varVariable = "varで宣言された変数";
console.log(varVariable); // 出力: varで宣言された変数 

console.log(letVariable); // ReferenceError: letVariableは初期化されていない  
let letVariable = "letで宣言された変数";
console.log(letVariable); // 出力: letで宣言された変数

Hoisting (巻き上げ、ホイスティング) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

関数の呼び出し

関数を定義したら以下のようにして呼び出すことができます。

sayHello();

関数に引数を渡す

関数には、実行時に外部から値を受け取ることができます。
受け取った値は「引数」となり、関数の中で使用できます。引数を受け取る部分を「パラメータ」と呼びます。

デフォルト引数

関数の定義時にパラメータのデフォルト値を設定できます。こうすることで引数が与えられなかった場合の挙動を制御できます。

function greet(name = "Guest") {
  console.log("Hello, " + name + "!");
}
greet(); // 出力: Hello, Guest!
greet("Anna"); // 出力: Hello, Anna!

残余引数

複数の引数を配列の形で受け取りたい場合には「残余引数」を使います。

function sum(...numbers) {
  return numbers.reduce((acc, current) => acc + current, 0);
}
console.log(sum(1, 2, 3)); // 出力: 6

関数の戻り値

関数は、計算結果や処理の出力を呼び出し元に返すことができます。return文を使って値を返します。

function add(x, y) {
  return x + y;
}
console.log(add(5, 7)); // 出力: 12

匿名関数とアロー関数

名前を持たない関数を「匿名関数」と呼びます。ES6から導入された「アロー関数」は、より簡潔な構文で匿名関数を定義できる新しい記法です。

アロー関数の特性

アロー関数は、従来の関数とはthisの参照先が異なります。アロー関数のthisは外側のスコープのthisを参照します。

const add = (x, y) => x + y;
console.log(add(10, 5)); // 出力: 15

アロー関数のthisについて

通常の関数で定義されたthisは、その関数がどのように呼び出されたかによって変わります。一方、アロー関数で定義されたthisは、外側のスコープのthisを参照します。

これは、アロー関数がレキシカルスコープを持つためです。
つまり、アロー関数は自身が定義された位置のthisを参照するのです。

// 通常の関数
function normalFunction() {
  console.log(this); // window オブジェクト
}
normalFunction(); // 出力: window オブジェクト

// アロー関数
const arrowFunction = () => {
  console.log(this); // 外側のスコープの this (この場合はwindow)
};
arrowFunction(); // 出力: window オブジェクト

// メソッドとしての呼び出し
const obj = {
  prop: 'value',
  normalMethod: function() {
    console.log(this.prop); // 'value'
  },
  arrowMethod: () => {
    console.log(this.prop); // undefined (外側のスコープの thisを参照するため)
  }
};
obj.normalMethod(); // 出力: 'value'
obj.arrowMethod(); // 出力: undefined

通常の関数のthisは実行時の文脈によって変わりますが、アロー関数のthisは定義時の周囲のthisを参照します。そのため、メソッドとしてアロー関数を使う場合は注意が必要です。

高度な関数の利用

即時実行関数 (IIFE)

「即時実行関数」は、定義されるとすぐに実行される関数です。

主な用途は、グローバルスコープを汚染せずに変数のスコープを限定することにあります。また、プライベート変数の実現やモジュール化にも活用できます。

(() => {
  const message = "即時実行";
  console.log(message);
})(); // 出力: 即時実行

IIFE (即時実行関数式) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

高階関数

「高階関数」とは、他の関数を引数に取ったり、関数を返す関数のことです。

より抽象的なレベルでの操作が可能になります。コールバック関数を使ったイベントハンドリングや、MapやFilterなどの高度な配列操作に利用されます。

const higherOrderFunction = callback => {
  const data = { name: "JavaScript" };
  callback(data);
};

higherOrderFunction(data => {
  console.log("コールバック関数が呼ばれました:", data);
}); // 出力: コールバック関数が呼ばれました: { name: 'JavaScript' }

高階関数 – Wikipedia

クロージャ

「クロージャ」とは、関数が定義された際の外側の変数スコープに参照が維持される仕組みです。変数をプライベート化でき、特権メソッドの実装やカウンターなどの状態管理に役立ちます。

const counter = (() => {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
})();

console.log(counter()); // 出力: 1
console.log(counter()); // 出力: 2

クロージャの利点と活用例

クロージャを使うことで、カプセル化によりデータを隠蔽化(いんぺいか)でき、コードの保守性が高まります。意図しないデータの書き換えを防げるため、バグの発生を抑えられます。

また、クロージャはイベントリスナーやコールバック関数の実装にも利用でき、イベントが発生した時点でクロージャに格納されていた値や関数を後から参照して処理を行うことができます。

クロージャ – JavaScript | MDN

非同期処理とPromise

JavaScriptでは、非同期処理の理解が重要です。非同期処理を適切に利用すれば、ブラウザがフリーズするリスクを最小限に抑えつつ、効率的に並列処理を行えます

非同期処理の必要性

JavaScriptは単一スレッドのプログラミング言語で動作します。そのため、重い処理を行うとUIスレッドがブロックされ、ブラウザ全体がフリーズしてしまう可能性があります。

非同期処理では、そのような重い処理をメインスレッドからオフロードして、別の扱いをすることで、UIスレッドを確保できます。

コールバック関数

非同期処理の手段として、もともとはコールバック関数を使っていました。しかし、コールバックの中でさらにコールバックを使う「コールバック地獄」と呼ばれる状況に陥りやすく、コードの可読性が損なわれがちでした。

setTimeout(() => {
  console.log("1秒後に表示");
}, 1000); // 出力: 1秒後に表示

Callback function (コールバック関数) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

Promiseオブジェクト

そこでES6からPromiseオブジェクトが導入され、コールバック地獄を避けられるようになりました。Promiseは非同期処理の最終的な完了や失敗を表すオブジェクトです。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功");
  }, 1000);  
});

promise.then(result => {
  console.log(result); // 出力: 成功
}).catch(error => {
  console.error(error);  
});

Promise – JavaScript | MDN

プロミスチェーン

Promiseを.then()で連鎖させることで、複数の非同期処理を順次実行できます。
処理の流れを明示的に表現でき、可読性が高まります。

const fetchData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("データ取得完了"); 
    }, 1000);
  });
};

const processData = data => `${data} -> 処理済み`;

const displayData = processedData => console.log(processedData);

fetchData()
  .then(processData) 
  .then(displayData)
  .catch(error => console.error(error));
// 出力: データ取得完了 -> 処理済み

Promise.prototype.then() – JavaScript | MDN

非同期プログラミングの発展

JavaScriptの非同期プログラミングは、Promiseに加えてAsync/Await構文が導入されたことで、より洗練されたものになりました。今後は非同期処理をさらに深く理解し、効果的に実装する力が求められます。

Async/Awaitの利用

Promiseが登場したことで、コールバック地獄を回避できるようになりましたが、さらにES2017でAsync/Awaitが追加されたことで、非同期処理をより直感的な同期的な記述で表現できるようになりました。Promiseのthen()catch()の記述を不要にし、try-catchによるエラー処理も容易になるため、非同期コードの可読性が大幅に向上しています。これにより、JavaScriptの非同期プログラミングはさらに洗練された形になりました。

Async/Awaitは、Promise処理を同期的に記述できます。asyncキーワードで関数を定義し、その中でawaitを使ってPromiseの完了を待機できます。

async function fetchData() {
  try {
    const response = await fetch('<https://api.example.com/data>');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

fetchData();

async/await 入門(JavaScript) #JavaScript – Qiita

非同期処理の高度な利用

非同期プログラミングには、複数の非同期処理を効率的に組み合わせるためのパターンがあります。

並列処理

Promise.allを使えば、複数の非同期処理を同時に開始し、すべての処理が完了するのを待つことができます。

async function fetchMultipleData() {
  try {
    const [data1, data2] = await Promise.all([
      fetch('<https://api.example.com/data1>').then(res => res.json()),
      fetch('<https://api.example.com/data2>').then(res => res.json()), 
    ]);
    console.log(data1);
    console.log(data2);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

fetchMultipleData();

Promise.all() – JavaScript | MDN

レート制限

特定の時間内に送信できるリクエスト数を制限し、サーバーへの負荷を管理するテクニックです。 APIの利用制限に対処するために重要です。

// リクエストの上限を1秒間に3回までに設定
const MAX_REQUESTS_PER_SECOND = 3;

// 最後のリクエスト時間を記録する変数
let lastRequestTime = 0;

// レート制限を実装したAPIアクセス関数
async function fetchWithRateLimit(url) {
  // 1秒間に上限を超えていないかチェック
  const currentTime = Date.now();
  if (currentTime - lastRequestTime < 1000 / MAX_REQUESTS_PER_SECOND) {
    // 上限を超えている場合は一定時間待機
    const timeToWait = (1000 / MAX_REQUESTS_PER_SECOND) - (currentTime - lastRequestTime);
    await new Promise(resolve => setTimeout(resolve, timeToWait));
  }

  // APIにリクエストを送信
  lastRequestTime = Date.now();
  const response = await fetch(url);
  return response.json();
}

// 使用例
fetchWithRateLimit('<https://api.example.com/data>')
  .then(data => console.log(data))
  .catch(error => console.error('エラーが発生しました:', error));

エラーハンドリング

try...catchを使って、非同期処理中のエラーを適切に捕捉・処理することが求められます。予期せぬ例外が発生した際にアプリケーションがクラッシュしないよう、適切な対処を行うために必要です。

async function fetchData() {
  try {
    const response = await fetch('<https://api.example.com/data>');
    if (!response.ok) {
      throw new Error(`HTTP error ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('エラーが発生しました:', error);
  }
}

fetchData();

try…catch – JavaScript | MDN

ジェネレーターとAsync Iterator

「ジェネレーター関数」は、実行を途中で止めて後から再開できる関数です。
Async Iteratorは、ジェネレーターとAsync/Awaitの機能を組み合わせたものです。これらを利用すると、より洗練された非同期フローを実装できるようになります。

const fetchData = async (url) => {
  const response = await fetch(url);
  return await response.json();
};

async function* getDataAsync(urls) {
  for (const url of urls) {
    const data = await fetchData(url);
    yield data;
  }
}

(async () => {
  const urls = ['<https://api.example.com/data1>', '<https://api.example.com/data2>'];
  const asyncIter = getDataAsync(urls);
  for await (const data of asyncIter) {
    console.log(data);
  }
})();
// 出力: 
// { データ1の内容 }
// { データ2の内容 }

function* 宣言 – JavaScript | MDN

AsyncIterator – JavaScript | MDN

まとめ

JavaScriptの関数について基本的な定義方法から応用的な使い方まで幅広く解説してきました。

関数はコードの再利用性を高め、プログラムの構造を整理するための重要な概念です。JavaScriptの学習を進める中で、関数の重要性を常に意識しながら、その応用力を高めていきましょう。

それでは、また!

合わせて読みたい!