Q1const addFee = (price, fee = 500) => price + fee; で addFee(1000, undefined) は何を返しますか?
デフォルト引数と可変長引数 — 既定値と rest の受け取り方
JavaScript の関数で、省いた引数に使われるデフォルト引数と、個数の決まらない引数を配列でまとめて受け取る rest 引数、設定をオブジェクトで渡す options パターンを扱います。
配送料や税率のように、呼び出しのたびに同じ値を渡している引数があります。省いて呼び出してもエラーは出ず、表示された文字列に undefined が混ざって初めて渡し忘れに気づきます。
この記事では、省いた引数に使われる値を決めるデフォルト引数と、個数の決まらない引数をまとめて受け取る可変長引数、設定を 1 つのオブジェクトで渡す options パターンを扱います。
引数を省いても呼べるようにする — デフォルト引数
会議室の予約を組み立てる関数で、利用時間はほとんどが 1 時間、レイアウトもほとんどがスクール形式だとします。それでも呼び出すたびに 3 つの引数をすべて書くと、変えたい値がどれなのか呼び出し側で埋もれます。
デフォルト引数(引数が渡されなかったときに使う値を、あらかじめ決めておく書き方)は、引数の並びに hours = 1 のように = を添えて書きます。既定値を付けた引数は、呼び出し側で省けます。
// 既定値は引数の並びに「= 値」を添えて書く
const reserveRoom = (roomName, hours = 1, layout = "スクール") => {
return `${roomName} / ${hours} 時間 / ${layout}`;
};
// 3 番目を省くと、既定値の "スクール" が使われる
console.log(reserveRoom("A 会議室", 2)); // A 会議室 / 2 時間 / スクール
// 渡した値がある位置では、既定値は使われない
console.log(reserveRoom("A 会議室", 2, "対面")); // A 会議室 / 2 時間 / 対面
// 2 番目も省くと、既定値が 2 つとも使われる
console.log(reserveRoom("A 会議室")); // A 会議室 / 1 時間 / スクール
reserveRoom("A 会議室", 2) を、引数の位置ごとに追ったものです。既定値が付いていても、値が渡っていれば動きません。省けるのは右端の引数からなので、既定値を付ける引数は後ろに並べます。3 番目だけを変えたいときも、2 番目の位置には値を書く必要があります。
渡さなかった引数に入るもの — undefined と既定値
既定値を付けていない引数を省いても、その引数を使った計算や文字列の組み立てはそのまま進みます。止まらないぶん、どの呼び出しで渡し忘れたのかを後から探すことになります。
渡されなかった引数には undefined が入ります。既定値が使われるのは引数が undefined のときだけで、null や 0 や空文字列を渡した場合は、渡したその値が使われます。
const notifyStatus = (orderId, status = "受付済み") => `${orderId}: ${status}`;
// 省いたときと undefined を渡したときは、どちらも既定値になる
console.log(notifyStatus("A-1021")); // A-1021: 受付済み
console.log(notifyStatus("A-1021", undefined)); // A-1021: 受付済み
// null は値が渡された扱いなので、既定値は使われない
console.log(notifyStatus("A-1021", null)); // A-1021: null
// 既定値を書いていない引数を省くと undefined が入る
const plainStatus = (orderId, status) => `${orderId}: ${status}`;
console.log(plainStatus("A-1021")); // A-1021: undefined
status には undefined が入り、既定値の式が動きます。null は渡された値として扱われ、既定値は動きません。下の表は、notifyStatus の 2 番目に渡す値ごとに、status に入る値と表示を並べたものです。undefined を書いた行は省いた行と同じ結果になり、null の行は文字列を渡した行と同じく、渡した値がそのまま表示されます。
| 呼び出し | status に入る値 | 表示 |
|---|---|---|
| notifyStatus("A-1021") | 既定値の 受付済み | A-1021: 受付済み |
| notifyStatus("A-1021", "発送済み") | 発送済み | A-1021: 発送済み |
| notifyStatus("A-1021", undefined) | 既定値の 受付済み | A-1021: 受付済み |
| notifyStatus("A-1021", null) | null | A-1021: null |
null を渡すと既定値は使われません
null は「意図的に空である」ことを表す値で、引数に既定値を書いても置き換わりません。API の応答が null のときも既定値を使いたい場合は、引数側に既定値を置かず、関数の中で const shown = status ?? "受付済み"; のように ?? を使います。
個数が決まらない引数をまとめる — rest 引数
商品に付けるタグや経費の明細のように、渡される値の個数が呼び出しごとに変わる処理があります。引数を 5 つ用意しても足りない呼び出しが出てきますし、使われなかった引数には毎回 undefined が入ります。
rest 引数(引数の並びの残りを 1 つの配列にまとめて受け取る書き方。可変長引数とも呼びます)は、引数の最後に ...labels と書きます。分割代入で残りをまとめた ... と同じ記号で、集める対象が引数の並びになったものです。置けるのは最後の 1 つだけで、後ろに引数を続けると SyntaxError になります。
// ...labels と書くと、受け取った引数がまとまって 1 つの配列になる
const buildTags = (...labels) => `${labels.length} 件: ${labels.join(" / ")}`;
console.log(buildTags("送料無料", "在庫あり")); // 2 件: 送料無料 / 在庫あり
console.log(buildTags("新着")); // 1 件: 新着
// 逆に、呼び出しの括弧の中に ... を書くと、配列を引数の並びに戻せる
console.log(buildTags(...["新着", "限定"])); // 2 件: 新着 / 限定
// 名前を付けて受け取る引数があるときは、rest を最後に置く
const buildNotice = (orderId, ...notes) => `${orderId}(${notes.join("・")})`;
console.log(buildNotice("A-1021", "冷蔵", "時間指定")); // A-1021(冷蔵・時間指定)
console.log(buildNotice("A-1022")); // A-1022()
orderId に、残りは順に notes の要素に入ります。rest は、名前を付けた引数の後ろの残りを受け取ります。rest 引数には既定値を付けられませんが、1 つも渡されなかったときの notes は undefined ではなく空の配列です。逆に、手元に配列がある状態で 1 件ずつの引数として渡したいときは、呼び出しの括弧の中に ... を書きます。
名前を付けて渡す — options と分割代入
メールの送信のように設定項目が増えると、sendMail(to, subject, false, true) のような呼び出しになります。最後の false と true が何を指すのかは、関数の定義を開かないと読み取れません。
options パターン(複数の設定を 1 つのオブジェクトにまとめて渡す書き方)では、受け取る側で分割代入し、キーごとに既定値を付けます。設定ごと渡さない呼び出しに備えて、分割代入の後ろに = {} を添え、オブジェクトが来なかったときは空のオブジェクトを置きます。
// 引数が並ぶほど、呼び出し側では何を渡しているのか読み取れない
const sendMailOld = (to, subject, isUrgent, withPdf) =>
`${to} / ${subject} / ${isUrgent} / ${withPdf}`;
console.log(sendMailOld("aya@example.com", "発送のお知らせ", false, true));
// aya@example.com / 発送のお知らせ / false / true
// options パターン: 1 つのオブジェクトにまとめ、受け取る側で分割代入する
const sendMail = (to, { subject = "お知らせ", isUrgent = false } = {}) =>
`${to} / ${subject} / ${isUrgent}`;
console.log(sendMail("aya@example.com", { isUrgent: true })); // aya@example.com / お知らせ / true
console.log(sendMail("aya@example.com")); // aya@example.com / お知らせ / false
引数の順番を覚える代わりにキー名で意味を示せるので、設定が増えても呼び出し側は読めます。キーは名前で対応するため、{ isUrgent: true, subject: "再送" } のように書く順番を入れ替えても、受け取る値は同じです。
= {} を省くと TypeError になります
{ subject = "お知らせ" } のように分割代入だけを書くと、2 番目を省いた呼び出しで TypeError: Cannot read properties of undefined (reading 'subject') が出ます。省かれた引数の undefined から、キーを取り出そうとするためです。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2const countNotes = (id, ...notes) => notes.length; を countNotes("A-1021") と呼ぶと何が返りますか?
Q3const send = (to, { urgent = false } = {}) => urgent; を send("aya@example.com") と呼ぶと何が返りますか?