アロー関数とは?functionとの違いとthisの挙動・実務での使い分け
アロー関数は、ECMAScript 2015(ES6)で追加された => を使う関数の書き方です。ただ短く書けるだけの構文糖ではありません。this が呼び出し方ではなく定義位置で決まり、new もできず、arguments も持ちません。この違いを知らずに従来の function と入れ替えると、オブジェクトのメソッドが動かなくなります。この記事では、両者の差を仕様と実行結果で確認したうえで、ESLintでの使い分けの固定、TypeScriptの .tsx で起きるジェネリックの制約、Reactでの選び方までをまとめます。
まとめ:アロー関数とfunctionの使い分け結論
先に実務での判断基準を示します。コールバックはアロー関数、オブジェクトのメソッドとプロトタイプを使う関数は function、これが原則です。map・filter・setTimeout・イベントハンドラのような「その場限りの処理を渡す」用途では、アロー関数が外側の this をそのまま引き継ぐため、旧来の var self = this; や bind(this) が不要になります。
禁止と罠は分けて覚えてください。仕様上そもそも書けないのはコンストラクタとジェネレータの2つだけです。一方、オブジェクトリテラルのメソッドと prototype への代入は構文としては書けてしまいます。書けるのに this が意図した対象を指さないため、こちらの方が発見は遅れます。モジュールのトップレベルに置く関数は、巻き上げが効いて前方から呼べる function 宣言に寄せると、相互再帰のような前方参照でも詰まりません。以降で、この結論の根拠を順に確認します。
アロー関数の構文と省略ルール
基本形は (引数) => 式 です。本体が式ひとつなら波括弧と return を省略でき、この形を式本体と呼びます。引数の括弧を省略できるのは、単純な引数がちょうど1つのときだけです。引数がゼロ個、2個以上、デフォルト値付き、分割代入、残余引数のいずれでも括弧は必須になります。
const double = (n) => n * 2;
const double2 = n => n * 2; // 単純な引数1つなら括弧を省略できる
const greet = () => "hello"; // 引数なしは括弧が必須
const sum = (a, b) => a + b; // 複数引数も括弧が必須
const first = ([head]) => head; // 分割代入も括弧が必須
const makeUser = (name) => ({ name, active: true });
最後の行が実務でつまずく箇所です。式本体でオブジェクトリテラルを返したいとき、波括弧をそのまま書くと関数本体と解釈され、{ name: ... } の name はラベル文になります。結果として戻り値は undefined になり、エラーも出ません。オブジェクトを返すときは丸括弧で囲みます。もう1つ、引数の括弧と => の間に改行を入れると SyntaxError になります。矢印より後ろの改行は問題ありません。
thisの決まり方:アロー関数とfunctionの決定的な差
両者の最大の違いは this です。function で定義した関数の this は、定義した場所ではなく呼び出し方で決まります。obj.method() なら obj、単独で fn() なら未定義またはグローバルオブジェクト、call や apply で明示すればその値です。
アロー関数は this を自分では持ちません。定義されたスコープの this をそのまま参照します。呼び出し方をどう変えても値は動かず、call・apply・bind で上書きすることもできません。MDNも、アロー関数は定義されたスコープに基づいて this が決まるため、呼び出し方によってこの値は変わらないと説明しています。
オブジェクトのメソッドをアローで書いた場合の失敗
const counter = {
count: 0,
incArrow: () => { this.count++; }, // this はオブジェクトの外側を指す
incMethod() { this.count++; }, // this は counter
};
counter.incArrow();
console.log(counter.count); // 0 のまま
incArrow の this は counter ではなく、オブジェクトリテラルを囲んでいるスコープの this です。壊れ方は実行環境で二通りです。ブラウザのスクリプト直下や Node.js v26.5.0 の CommonJS モジュールでは、this が window や module.exports を指すため例外が出ないまま counter.count が 0 のまま残ります。ES Modules ならトップレベルの this が undefined なので、同じコードが TypeError: Cannot read properties of undefined で落ちます。黙って壊れるか、即座に落ちるかの違いだけ。どちらも意図した動作にはなりません。メソッド定義にアロー関数を使ってはいけない理由はここにあります。
アロー関数のメリット:コールバックでthisを引き継ぐ書き方
同じ性質が、コールバックでは利点に変わります。setInterval や addEventListener に function を渡すと this が呼び出し元に差し替わります。Node.js v26.5.0 で setInterval に function を渡すと this は Timeout オブジェクトになり、ブラウザでは window です。そのためES5時代は var self = this; で退避するのが定番でした。アロー関数ならその退避が要りません。
class Timer {
constructor() { this.sec = 0; }
start() {
setInterval(() => { this.sec++; }, 1000); // this は Timer インスタンス
}
}
ES5では入れ子1段ごとに var self = this; を1行ずつ書く必要がありました。アロー関数ではその退避行がすべて消えます。コールバックとPromiseの関係を整理したい場合は、非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説もあわせて確認してください。
アロー関数で書けない処理:new・prototype・argumentsの制約
this 以外にも仕様上の制約があります。以下は Node.js v26.5.0 での挙動です。
| 項目 | function | アロー関数 |
|---|---|---|
| this の決まり方 | 呼び出し方で決定 | 定義位置の外側を継承 |
| new でのインスタンス化 | 可 | TypeError |
| prototype プロパティ | あり | なし |
| arguments オブジェクト | あり | なし(外側を参照) |
| yield・ジェネレータ | 可 | SyntaxError |
| call / apply / bind で this 変更 | 可 | 不可 |
| super / new.target | 自前の束縛あり | 自前の束縛なし(外側を継承) |
| 巻き上げ | 関数宣言は可 | const・let はTDZ / var は undefined |
new の行が最も遭遇しやすい制約です。アロー関数をコンストラクタとして呼ぶと TypeError: Foo is not a constructor で即座に落ちます。'prototype' in Foo は false を返すため、プロトタイプ拡張の対象にもできません。arguments は「存在しない」だけでなく、書くと外側の関数の arguments を静かに参照します。可変長引数を受けたいときは残余引数の (...args) => を使ってください。
表の最下段2行は誤解が多い箇所です。super と new.target は「使えない」のではなく、アロー関数が自前の束縛を持たないだけで、外側から継承します。Node.js v26.5.0 では、クラスのメソッド内で定義したアロー関数から super.hello() が問題なく動きます。SyntaxError になるのは、外側にも束縛が無い場所、たとえばトップレベルのアロー関数で super を書いた場合だけです。
クラスフィールドとしてのアロー関数は例外的にメソッド用途で機能し、分割代入で取り出しても this が外れません。ただしMDNが明記しているとおり、クラスフィールドはインスタンスごとに新しい関数とクロージャを確保するため、生成数が多いクラスではメモリ使用量が増えます。イベントハンドラを外部に渡す必要がある場合に限って使う、という線引きが現実的です。
functionとの使い分け基準:巻き上げと機械的なルール化
ここまでの制約に触れない場面、つまりどちらでも書ける場面をどう決めるか。判断材料は巻き上げの有無と、チームで揺れないルール化の2点です。
関数宣言のhoistingとconstのTDZ
hoisted(); // 動く
function hoisted() { console.log("ok"); }
notHoisted(); // ReferenceError
const notHoisted = () => {};
function 宣言は巻き上げられるため、定義より前の行から呼べます。const に代入したアロー関数は初期化前の参照が禁止され、Node.js v26.5.0では ReferenceError: Cannot access 'notHoisted' before initialization になります。これは欠点とは限りません。宣言より前で使えないという制約は、読み手にとっては「定義を上から順に追えば理解できる」保証でもあります。function 宣言を選ぶ理由はこの前方参照に絞ってください。「アロー関数は無名だからスタックトレースで追えない」という説明を見かけますが、これは古い話です。const に代入したアロー関数は変数名から名前が推論され、Node.js v26.5.0 のスタックトレースにも at myArrow のように関数名が出ます。相互再帰のような前方参照が要る書き方は再帰関数とは?自分を呼ぶ関数の仕組みとスタック・末尾再帰・実務判断を解説で扱っています。
ESLintで使い分けをルール化する設定
使い分けを各自の判断に任せると、レビューのたびに好みの議論が発生します。ESLintで機械的に固定するのが早道です。
// eslint.config.mjs(package.json に "type": "module" が無い場合は .mjs にする)
export default [
{
rules: {
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"prefer-arrow-callback": "error",
"arrow-body-style": ["error", "as-needed"]
}
}
];
prefer-arrow-callback はコールバックにアロー関数を要求するルールで、上の原則をそのまま強制できます。func-style は関数宣言と変数代入のどちらかに統一するルールです。この3つはいずれも Deprecated ではありませんが、ESLint公式の各ルールページには3つとも「❄️ Frozen」と表示されています。凍結は新機能を受け付けないという意味で、動作と後方互換は維持されます。設定に残して構いません。なお func-style の allowArrowFunctions: true は既存コードの移行を優先した緩和で、トップレベルのアロー代入も許可します。前節の原則を厳格に固定したい場合はこのオプションを外してください。
一方、arrow-parens・arrow-spacing・no-confusing-arrow は Deprecated です。これらを設定に残している場合は @stylistic/eslint-plugin 側の同名ルールへ移してください。ESLint本体の整形系ルールが順次移管されているためで、古い記事の設定例をそのまま貼ると警告が出ます。ルール化の位置づけについてはコードレビューとは?目的・観点・進め方と実装現場の運用設計を解説も参考になります。
TypeScriptでのアロー関数:型注釈と.tsxのジェネリック制約
TypeScriptでは => が2つの意味で登場します。値としてのアロー関数と、関数型を表す型注釈です。次の行では前者と後者が同時に現れます。
const add: (a: number, b: number) => number = (a, b) => a + b;
type ClickHandler = (e: MouseEvent) => void;
左側の (a: number, b: number) => number は「数値2つを受けて数値を返す関数」という型で、実行される関数本体ではありません。型エイリアスとして切り出しておくと、コールバックの引数に毎回注釈を書かずに済みます。
厄介なのは .tsx でのジェネリックです。<T> がJSXタグの開始と区別できないため、通常の書き方はコンパイルできません。TypeScript 7.0.2 では、次の1行目に TS17008: JSX element 'T' has no corresponding closing tag. と TS1382 が出ます(閉じタグを探しにいく副作用で TS1005 も併せて表示されます)。
const bad = <T>(x: T): T => x; // .tsx ではエラー
const ok1 = <T,>(x: T): T => x; // 末尾カンマを付ける
const ok2 = <T extends unknown>(x: T): T => x; // 制約を付ける
function ok3<T>(x: T): T { return x; } // function 宣言なら制約なし
回避策は3つあり、後半3行はいずれもエラーなしで通ります。<T,> の末尾カンマは目的が読み取りにくいため、ジェネリックなヘルパー関数は素直に function 宣言で書くのが最も無難です。同じ関数を .ts に置くならこの制約は発生しません。TypeScript側のバージョン差についてはTypeScript 6.0とは|新機能・5から6への移行手順とGo製7.0対応まで総まとめで整理しています。
Reactでのアロー関数:コンポーネント定義とJSX内インライン関数
「どちらで書くべきか」への答えを先に示します。既定はアロー関数でよく、ジェネリックを取るコンポーネントだけ function 宣言にしてください。Reactの関数コンポーネントはどちらで定義しても挙動に差はなく、Hooksの使用可否にも影響しません。差が出るのは前節の .tsx 制約だけで、そこだけ function 宣言が明確に有利になります。
const Button = ({ label, onPress }: Props) => (
<button onClick={onPress}>{label}</button>
);
判断が分かれるのは、JSXの属性に直接書くインラインのアロー関数です。
<button onClick={() => setCount(count + 1)}>+1</button>
この書き方は、レンダーのたびに新しい関数オブジェクトを生成します。propsの参照が毎回変わるため、React.memo でメモ化した子コンポーネントは再レンダーを回避できません。ただし通常のDOM要素に渡すだけなら、この生成コストを気にして書き方を変える必要はありません。避けるべきなのは、メモ化した子や大量の要素を持つリストに渡す場合に限られます。その場合は useCallback で参照を固定します。「インラインのアロー関数は常に悪」という単純化は、可読性を落とすだけで効果がありません。
ラムダ式・無名関数との違い
「アロー関数はJavaScriptのラムダ式」と説明されることがありますが、正確ではありません。無名関数(名前を持たない関数)という括りでは同じ系統に属するものの、言語ごとに書ける内容と this の扱いが異なります。
| 言語 | 記法 | 本体に書ける内容 |
|---|---|---|
| JavaScript | (a, b) => a + b |
式・複数文とも可 |
| Java | (a, b) -> a + b |
式・複数文とも可 |
| Python | lambda a, b: a + b |
式のみ |
| C# | (a, b) => a + b |
式・複数文とも可 |
矢印の形も揃っていません。Javaは ->、JavaScriptとC#は => です。Pythonの lambda は式しか書けず、複数行の処理は通常の def に切り出します。仕組みの違いはもう1つあります。Javaのラムダ式は関数型インターフェース(抽象メソッドが1つのインターフェース)の実装として扱われ、JavaScriptのように関数そのものが値として独立しているわけではありません。Java側の設計思想はJavaラムダ式の使いどころ|Stream APIと組み合わせる場面・避けるべき場面で詳しく扱っています。
よくある質問
アロー関数とはどういう意味ですか?
ECMAScript 2015(ES6)で追加された、=> を使う関数式の書き方です。呼び名は矢印に見える記号の形に由来し、仕様上の正式名称は「アロー関数式」。ラムダ式やクロージャの一種と説明されることもありますが、JavaScriptの言語仕様に出てくる用語はアロー関数式だけです。
JSの () と => は何を表しますか?
() が引数リスト、=> が引数と関数本体の区切りを表す記号です。(a, b) => a + b は「a と b を受け取り、a + b を返す関数」と読みます。() の中が空なら引数を取りません。矢印の右に波括弧を書けば通常の関数本体になり、return の明示が要ります。
アロー関数とfunctionはどちらが速いですか?
実行速度で選ぶ場面はほとんどありません。呼び出しコストの差はJITコンパイラの最適化で吸収され、アプリケーションの体感に現れる規模にならないためです。判断軸は速度ではなく、this の扱いと制約の方だと考えてください。メモリ消費に差が出るクラスフィールドのケースだけは例外で、条件は「アロー関数で書けない処理」の章に記載しています。
アロー関数はデバッグ時に名前が表示されますか?
変数に代入していれば表示されます。const myArrow = () => { ... } のように書くと変数名から関数名が推論されるため、Node.js v26.5.0 のスタックトレースにも at myArrow と出ます。名前が出ないのは、引数に直接書いた無名のアロー関数を使った場合です。デバッグしづらい箇所が特定できているなら、その関数だけ変数に切り出すと解決します。
アロー関数はどの環境から使えますか?
MDNのブラウザ互換性情報には「2016年9月以降、すべてのブラウザーで利用可能」と示されています。現行のブラウザとNode.jsではそのまま動き、ES6対応が前提の環境ならトランスパイルも不要です。判断が要るのは Internet Explorer 11 をサポート対象に含むレガシー案件だけで、その場合はBabelでのトランスパイルかES5準拠の記法を選びます。