2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新・JavaScript文法(5):関数の基本とアロー関数

Last updated at Posted at 2025-06-14

前回の記事 では、JavaScriptの配列の基本とモダンな操作方法について紹介しました。今回は、JavaScriptの関数について、従来の関数宣言から最新のアロー関数までを扱います。

関数の基本

関数は、コードの再利用性と構造化を可能にします。JavaScriptでは関数は「第一級オブジェクト」として扱われるため、変数に代入したり、他の関数の引数として渡したり、関数から返したりできます。

関数宣言と関数式

JavaScriptでは主に3つの方法で関数を定義できます。

// 1. 関数宣言
function sayHello(name) {
  return `こんにちは、${name}さん!`;
}

// 2. 関数式
const greet = function(name) {
  return `こんにちは、${name}さん!`;
};

// 3. アロー関数(ES2015以降)
const welcome = (name) => {
  return `こんにちは、${name}さん!`;
};

// 使い方は同じ
console.log(sayHello("田中")); // "こんにちは、田中さん!"
console.log(greet("佐藤"));    // "こんにちは、佐藤さん!"
console.log(welcome("鈴木"));  // "こんにちは、鈴木さん!"

関数宣言と関数式の違い:ホイスティング

関数宣言と関数式の主な違いは「ホイスティング」の挙動です。ホイスティングとは、変数や関数の宣言が、スコープの先頭に「巻き上げ」られるJavaScriptの仕様です。

// 関数宣言は、定義の前に呼び出せる(ホイスティングされる)
console.log(sayHello("山田")); // "こんにちは、山田さん!"

function sayHello(name) {
  return `こんにちは、${name}さん!`;
}

// 関数式は、定義の前に呼び出すとエラーになる
// エラー: Cannot access 'greet' before initialization
// (または ReferenceError: greet is not defined)
console.log(greet("小林"));

const greet = function(name) {
  return `こんにちは、${name}さん!`;
};

関数宣言は、JavaScriptエンジンによって実行前にスコープの先頭に「巻き上げ」られ、関数全体が利用可能になります。

一方、関数式は変数の宣言部分のみがスコープの先頭にホイスティングされますが、constで宣言された変数は宣言行に到達するまで「一時的デッドゾーン(Temporal Dead Zone: TDZ)」にあり、参照できません。そのため、関数の代入が実行される前に呼び出すとエラーになります。

どちらを使うべきか

実務でコードを書く際は、以下のような基準で選択するとよいでしょう。

  • 予測可能性
    関数式を使うと、宣言と実行の順序が明確になり、コードの読みやすさが向上する
  • コードの構造化
    大きな関数を先に宣言し、ヘルパー関数を後に記述したい場合は、関数宣言を使うのが便利
  • 条件付き関数定義
    条件分岐内で関数を定義する場合は、関数式を使うのが一般的(関数宣言を条件分岐内で使うと、ブラウザによって異なる挙動となる場合がある)

アロー関数

ES2015(ES6)で導入されたアロー関数は、より簡潔な関数の記述を可能にします。

基本的な構文

// 通常の関数式
const add = function(a, b) {
  return a + b;
};

// 同じ機能をアロー関数で記述
const addArrow = (a, b) => {
  return a + b;
};

// 処理が1行の場合は、さらに簡略化可能
const addArrowConcise = (a, b) => a + b;

console.log(add(2, 3));          // 5
console.log(addArrow(2, 3));     // 5
console.log(addArrowConcise(2, 3)); // 5

引数が1つの場合は、括弧の省略もできます。

// 引数が1つの場合
const double = x => x * 2;
console.log(double(5)); // 10

// 引数がない場合は、空の括弧が必要
const sayHi = () => "こんにちは!";
console.log(sayHi()); // "こんにちは!"

アロー関数が導入された理由

アロー関数が導入された主な理由は以下のとおりです。

  1. より簡潔な構文
    とくに短い関数やコールバック関数で記述が簡単になる
  2. thisの束縛に関する問題の解決
    アロー関数内のthisは、定義した際のコンテキストを保持する

this の扱いの違い

アロー関数と通常の関数で最も重要な違いは、thisキーワードの挙動です。

通常の関数では、thisは呼び出し元によって決まります(動的バインディング)。一方、アロー関数では、thisは関数が定義された時点での値をキャプチャします(レキシカルバインディング)。

// 通常の関数では、thisは呼び出し元によって決まる
const person = {
  name: "田中",
  sayNameFunction: function() {
    console.log(this.name);
  }
};
person.sayNameFunction(); // "田中" (thisはpersonオブジェクト)

// アロー関数では、thisは定義時のコンテキストを維持する
const person2 = {
  name: "佐藤",
  sayNameArrow: () => {
    // ここでの this は、person2 オブジェクトが定義された「グローバルスコープ」の this を参照します。
    // ブラウザ環境では通常 window オブジェクト、Node.jsでは global オブジェクト(厳密モードでは undefined)です。
    // このため、this.name はグローバルスコープに name プロパティがない限り undefined となります。
    console.log(this.name);
  },
  sayNameWithinArrow: function() {
    // ここでのthisはperson2オブジェクト
    const innerArrow = () => {
      console.log(this.name); // innerArrow の this は sayNameWithinArrow の外側の this(すなわちperson2)を継承する
    };
    innerArrow();
  }
};

person2.sayNameArrow();     // undefined(thisはグローバルコンテキストを指すため、nameプロパティがない)
person2.sayNameWithinArrow(); // "佐藤"(innerArrowのthisはsayNameWithinArrowのthisを継承)

実務での選択基準

実務において「関数式 vs アロー関数」を選ぶ際の一般的な基準は、以下のとおりです。

メソッドとして定義する場合

オブジェクトのメソッドとしてthisをオブジェクト自身に参照させたい場合は、通常の関数を使用します。

const counter = {
  count: 0,
  increment: function() {
    this.count++;
  }
};

コールバック関数の場合

コールバック関数では通常アロー関数が適しています(とくに外側のスコープのthisにアクセスしたい場合)。

const timer = {
  seconds: 0,
  start: function() {
    // setIntervalのコールバックでアロー関数を使うと、thisはtimerオブジェクトを参照する
    setInterval(() => {
      this.seconds++;
      console.log(`${this.seconds}秒経過`);
    }, 1000);
  }
};

コンストラクタとして使う場合

アロー関数には prototype プロパティがなく、new 演算子で呼び出せないため、コンストラクタとしては通常の関数を使用します。

短い処理やチェーン内のコールバック

とくに配列メソッドのコールバックなど、短い処理の場合はアロー関数が読みやすくなります。

const doubled = [1, 2, 3].map(x => x * 2);

デフォルトパラメータ

ES2015からは、関数のパラメータにデフォルト値を設定できるようになりました。

// デフォルトパラメータの基本
function greet(name = "名無し", greeting = "こんにちは") {
  return `${greeting}${name}さん!`;
}

console.log(greet("田中"));         // "こんにちは、田中さん!"
console.log(greet("鈴木", "おはよう")); // "おはよう、鈴木さん!"
console.log(greet());              // "こんにちは、名無しさん!"

デフォルトパラメータは、より複雑な式や関数呼び出しも指定できます。

// デフォルト値に式や関数呼び出しを使用
function getTimestamp() {
  return new Date().toLocaleString();
}

function logActivity(activity, timestamp = getTimestamp()) {
  console.log(`[${timestamp}] ${activity}`);
}

logActivity("ログイン");
// 例: [2025/1/15 12:34:56] ログイン

デフォルトパラメータは左から右へ評価され、後続のデフォルトパラメータで前のパラメータの参照もできます。

function calculatePrice(price, tax = 0.1, total = price + (price * tax)) {
  return total;
}

console.log(calculatePrice(1000));      // 1100
console.log(calculatePrice(1000, 0.08)); // 1080

Rest Parameters(残余引数)

ES2015で導入されたRest parameters(...構文)は、関数の引数において、任意の数の引数をまとめて配列として扱うことができます。この ... 構文は「スプレッド構文」とも呼ばれ、配列やオブジェクトの展開など、引数以外でも多岐にわたる用途で利用されますが、ここでは関数引数としての「Rest parameters」について解説します。

// Rest parameters
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2));         // 3
console.log(sum(1, 2, 3, 4, 5)); // 15

Rest parametersは常に最後のパラメータである必要があり、他のパラメータと組み合わせることもできます。

function filterAndSum(threshold, ...numbers) {
  return numbers
    .filter(num => num > threshold)
    .reduce((total, num) => total + num, 0);
}

console.log(filterAndSum(3, 1, 2, 3, 4, 5)); // 9 (3より大きい4と5の合計)

argumentsオブジェクトとの違い

古いJavaScriptコードでは、arguments オブジェクトを使って可変長引数を扱っていましたが、arguments はレガシーな機能であり、モダンなJavaScriptではRest parametersを使用することが強く推奨されます。Rest parametersには以下のような利点があります。

(1)配列のメソッドが使える

arguments はarray-likeオブジェクトであり、配列のメソッドを直接使用できませんが、Rest parametersは純粋な配列になります。

// 旧式: arguments
function oldSum() {
  let total = 0;
  // argumentsオブジェクトはmap, reduceなどの配列メソッドが使えない
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

// 配列メソッドを使いたい場合は変換が必要
function oldSum2() {
  // Array.from()やスプレッド構文でargumentsを配列に変換
  const args = Array.from(arguments);
  // または const args = [...arguments];
  return args.reduce((total, num) => total + num, 0);
}

// モダン: Rest parameters
function newSum(...numbers) {
  // numbersは最初から配列なので、配列メソッドが直接使える
  return numbers.reduce((total, num) => total + num, 0);
}

(2)アロー関数内で使える

アロー関数内では arguments オブジェクトにアクセスできませんが、Rest parametersは使用できます。

// これはエラー - アロー関数にはargumentsがない
const arrowSum = () => {
  return Array.from(arguments).reduce((total, num) => total + num, 0);
};

// Rest parametersなら問題なく動作
const arrowSum2 = (...numbers) => {
  return numbers.reduce((total, num) => total + num, 0);
};

(3)より明示的

コードの意図が明確になり、可読性が向上します。

実践的なコード例

1. 関数合成(Function Composition)

複数の関数を組み合わせて新しい関数を作成する例です。

// 単一の値を変換する関数
const double = x => x * 2;
const square = x => x * x;
const addOne = x => x + 1;

// 関数合成のヘルパー関数
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// 合成関数: 値を2倍にして、2乗して、1を足す
const doubleThenSquareThenAddOne = compose(addOne, square, double);

console.log(doubleThenSquareThenAddOne(3));
// double(3) => 6
// square(6) => 36
// addOne(36) => 37
// 結果: 37

2. メモ化(再帰関数の最適化)

関数の結果をキャッシュして、同じ入力に対する計算を省略する例です。

// メモ化関数
function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log(`キャッシュから取得: ${key}`);
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

// 非効率な再帰フィボナッチ関数
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// メモ化されたフィボナッチ関数
const memoFibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return memoFibonacci(n - 1) + memoFibonacci(n - 2);
});

console.time('Regular');
console.log(fibonacci(30)); // 非常に時間がかかる
console.timeEnd('Regular');

console.time('Memoized');
console.log(memoFibonacci(30)); // 非常に高速
console.timeEnd('Memoized');

3. 部分適用とカリー化

関数の一部の引数を固定して新しい関数を作成する例です。

// 部分適用の関数
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

// 元の関数
function greet(greeting, name) {
  return `${greeting}${name}さん!`;
}

// 部分適用で「こんにちは」を固定した新しい関数
const sayHelloTo = partial(greet, "こんにちは");
console.log(sayHelloTo("田中")); // "こんにちは、田中さん!"

// カリー化の関数
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return function(...moreArgs) {
      return curried(...args, ...moreArgs);
    };
  };
}

// greet関数をカリー化
const curriedGreet = curry(greet);
const sayHello = curriedGreet("こんにちは");
console.log(sayHello("鈴木")); // "こんにちは、鈴木さん!"

復習

基本問題

1. 以下のコードを実行すると、何が表示されるか

function greet() {
  console.log("こんにちは");
}

const greet2 = function() {
  console.log("おはよう");
};

greet();
greet2();

2. 以下のアロー関数を通常の関数式に書き直す

const multiply = (a, b) => a * b;

3. 以下のコードを実行すると、何が表示されるか

function calculateTotal(price, tax = 0.1) {
  return price + (price * tax);
}

console.log(calculateTotal(1000));
console.log(calculateTotal(1000, 0.08));

実践問題

4. 以下のオブジェクトと関数で、コンソールに何が出力されるか

const user = {
  name: "田中",
  sayHello: function() {
    console.log(`こんにちは、${this.name}さん!`);
  },
  sayHelloArrow: () => {
    console.log(`こんにちは、${this.name}さん!`);
  }
};

user.sayHello();
user.sayHelloArrow();

5. Rest parametersを使って、任意の数の文字列を連結する concat 関数を作成する(区切り文字として最初の引数を受け取り、残りの引数はすべて連結する)

// 使用例:
// concat('-', 'a', 'b', 'c') => "a-b-c"
// concat('/', '2023', '01', '15') => "2023/01/15"

function concat(/* ここにコードを書く */) {
  // ここにコードを書く
}

6. 以下の再帰関数を高速化するためにメモ化を適用する

function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

解答例

問題1

// このコードを実行すると、以下が順番に表示される
function greet() {
  console.log("こんにちは");
}

const greet2 = function() {
  console.log("おはよう");
};

greet();    // "こんにちは"
greet2();   // "おはよう"

問題2

// アロー関数
const multiply = (a, b) => a * b;

// 通常の関数式に書き直す
const multiply2 = function(a, b) {
  return a * b;
};

問題3

function calculateTotal(price, tax = 0.1) {
  return price + (price * tax);
}

console.log(calculateTotal(1000));     // 1100 (1000 + 1000 * 0.1)
console.log(calculateTotal(1000, 0.08)); // 1080 (1000 + 1000 * 0.08)

問題4

const user = {
  name: "田中",
  sayHello: function() {
    console.log(`こんにちは、${this.name}さん!`);
  },
  sayHelloArrow: () => {
    console.log(`こんにちは、${this.name}さん!`);
  }
};

user.sayHello();      // "こんにちは、田中さん!"
// thisはuserオブジェクトを参照する

user.sayHelloArrow(); // "こんにちは、undefinedさん!"
// アロー関数のthisはオブジェクトのメソッドとしての呼び出し方に影響を受けず、
// 定義時のスコープ(この場合はグローバルスコープ)でのthisを参照する

問題5

function concat(separator, ...strings) {
  return strings.join(separator);
}

// 使用例
console.log(concat('-', 'a', 'b', 'c')); // "a-b-c"
console.log(concat('/', '2023', '01', '15')); // "2023/01/15"

問題6

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

const factorial = memoize(function(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
});

// テスト
console.time('Factorial');
console.log(factorial(20)); // 2432902008176640000
console.timeEnd('Factorial');

// 2回目の呼び出しは非常に速い(キャッシュから結果を取得)
console.time('Factorial cached');
console.log(factorial(20)); // 2432902008176640000
console.timeEnd('Factorial cached');

まとめ

JavaScriptの関数は非常に柔軟で強力です。従来の関数宣言や関数式に加え、ES2015で導入されたアロー関数、デフォルトパラメータ、Rest parametersなどの新機能により、より簡潔なコードが書けるようになりました。

重要なのは、アロー関数と従来の関数の違い、とくに this の挙動の違いを理解することです。状況に応じて適切な関数の形式を選ぶことで、より読みやすく保守しやすいコードを書くことができます。

また、関数は単なる処理の集まりではなく、「値」としての性質を持ちます。そのため、関数を引数として渡したり、他の関数から返したりする高階関数のパターンが可能になり、より抽象度の高いプログラミングが実現できます。

次回 は、JavaScriptのスコープとクロージャについて解説します。変数の有効範囲と、関数がどのように状態を「記憶」するのかについて詳しく見ていきます。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?