第9章のテーマは、変数が読み手に与える負担をどう減らすかだ。変数が多いほど、スコープが広いほど、書き換えられる回数が多いほど、読み手は頭の中で覚えておくことが増え、コードが理解しづらくなる。
これまでのメモと感想文のまとめ↓
9.1 不要な変数を削除する
読みやすさに貢献しない変数は消してしまう。変数は多いほど分かりやすくなるわけではなく、余計な変数はそのまま記憶の負担になる。消せる変数は次の3種類に分けられる。
意味のない一時変数
一度だけ代入され、一度だけ使われ、複雑な式を分割するわけでも意味を補うわけでもない変数は、式をそのまま書いたほうが短くて済む。
悪い例(余計な一時変数 now)
const now = new Date();
user.lastViewTime = now;
改善後
user.lastViewTime = new Date();
now は新しい情報を何も足していない。
中間結果を保持する変数
中間結果を別の変数に溜めず、処理できる時点で処理して早めに return する。配列から要素を探して削除する場面が典型だ。
悪い例(中間変数 indexToRemove がある)
function removeOne(array, targetValue) {
let indexToRemove = null;
for (let i = 0; i < array.length; i++) {
if (array[i] === targetValue) {
indexToRemove = i;
break;
}
}
if (indexToRemove !== null) {
array.splice(indexToRemove, 1);
}
}
改善後(見つけた時点で削除して return する)
function removeOne(array, targetValue) {
for (let i = 0; i < array.length; i++) {
if (array[i] === targetValue) {
array.splice(i, 1);
return;
}
}
}
制御フロー用の変数
ループの終了を制御するためだけの done や found のようなフラグ変数は、多くの場合 break や return で置き換えられる。
悪い例(余計な制御変数 found)
function hasValue(array, target) {
let found = false;
for (const item of array) {
if (item === target) {
found = true;
break;
}
}
return found;
}
改善後
function hasValue(array, target) {
for (const item of array) {
if (item === target) {
return true;
}
}
return false;
}
9.2 変数のスコープを縮める
原則は、変数が見える行数をできるだけ少なくすることだ。スコープが狭いほど、読み手がその変数を気にかけ続ける範囲も狭くなる。
グローバル変数を避け、クロージャで閉じ込める
JavaScript ではグローバル変数が名前の衝突や状態の混乱を起こしやすい。ある関数だけが使う変数なら、クロージャで隠してしまう。
悪い例(グローバル変数 submitted が誤って書き換えられる恐れがある)
// グローバルスコープ
let submitted = false;
function submitForm(formName) {
if (submitted) return; // 二重送信の防止
submitted = true;
// 送信処理...
}
改善後(即時実行関数で包み、submitted を内部だけに閉じ込める)
const submitForm = (function () {
let submitted = false; // 内部の関数からだけ見える
return function (formName) {
if (submitted) return;
submitted = true;
// 送信処理...
};
})();
変数の定義を使う場所の直前に置く
変数を関数の先頭にまとめて宣言せず、最初に使う直前で宣言する。
悪い例(変数が先頭に集まり、読み手は3つの変数を同時に覚えることになる)
function filterReplies(originalId) {
const filteredReplies = [];
const rootMessage = Messages.findById(originalId);
const allReplies = Messages.select({ rootId: originalId });
rootMessage.viewCount += 1;
rootMessage.lastViewTime = new Date();
rootMessage.save();
for (const reply of allReplies) {
if (reply.spamVotes <= MAX_SPAM_VOTES) {
filteredReplies.push(reply);
}
}
return filteredReplies;
}
改善後(使う直前に定義し、同時に覚える変数を減らす)
function filterReplies(originalId) {
const rootMessage = Messages.findById(originalId);
rootMessage.viewCount += 1;
rootMessage.lastViewTime = new Date();
rootMessage.save();
const allReplies = Messages.select({ rootId: originalId });
const filteredReplies = [];
for (const reply of allReplies) {
if (reply.spamVotes <= MAX_SPAM_VOTES) {
filteredReplies.push(reply);
}
}
return filteredReplies;
}
ブロックスコープを活用する
var ではなく let と const を使い、変数を if や for のような最小のブロックに閉じ込める。次の例では elem がループの中でしか見えない。
for (let i = 0; i < list.length; i++) {
const elem = list[i]; // 反復ごとに作られ、その回でだけ有効
console.log(elem.name);
}
9.3 値を一度だけ書き込む
値がころころ変わる変数は追跡しづらい。書き換えられる回数が増えるほど、読み手は現在の値を更新し続けなければならない。
JavaScript ではできるだけ let ではなく const を使い、代入を一度に制限する。段階的な計算でも、1つの変数を書き換えるのではなく、複数の読み取り専用の変数で表せる。
悪い例(1つの変数を何度も書き換え、各段階の値を追う必要がある)
let price = basePrice;
price = price * count;
price = price - discount;
price = price * taxRate;
改善後(各変数は一度だけ代入され、意味も明確)
const subtotal = basePrice * count;
const afterDiscount = subtotal - discount;
const finalPrice = afterDiscount * taxRate;
どの変数も値が変わらないので、読むときに現在の値を逆算しなくて済む。ただし、いつでも一度だけの代入にできるわけではない。それでも、変数を書き換える箇所をできるだけ減らすという方針は変わらない。
9.4 最後の例
ここまでの技法をまとめて使う例が出てくる。ページ上に input1、input2、input3 のような id の入力欄があり、最初の空の入力欄を探して値を入れ、その DOM 要素を返す関数 setFirstEmptyInput を書く。空の欄がなければ null を返す。
最初の版は変数が多く、書き換えも頻繁だ。
var setFirstEmptyInput = function (newValue) {
var i = 1;
var found = false;
var elem = document.getElementById('input' + i);
while (elem !== null) {
if (elem.value === '') {
found = true;
break;
}
i++;
elem = document.getElementById('input' + i);
}
if (found) {
elem.value = newValue;
}
return elem;
};
問題は3つある。found は余計な制御フロー変数で、elem はループの外で定義されて中で何度も書き換えられ、i もループの外にあって寿命が長い。
まず found を消し、見つけた時点で値を設定して返す。
var setFirstEmptyInput = function (newValue) {
var i = 1;
var elem = document.getElementById('input' + i);
while (elem !== null) {
if (elem.value === '') {
elem.value = newValue;
return elem; // 見つけたらすぐ返す。フラグは要らない
}
i++;
elem = document.getElementById('input' + i);
}
return null;
};
次に for ループへ書き換えてスコープを縮める。
function setFirstEmptyInput(newValue) {
for (let i = 1; true; i++) {
const elem = document.getElementById('input' + i);
if (elem === null) {
return null; // すべての要素を調べ終えた
}
if (elem.value === '') {
elem.value = newValue;
return elem;
}
}
}
elem はループの中で一度だけ代入され、見える範囲もその回の反復に限られる。i は for 文の中に収まり、found もなくなった。ロジックが一本道になり、変数の状態を頭の中で追わなくて済む。
感想
この章でいちばん印象に残ったのは、変数の話が「読み手が同時に覚えられる量には限りがある」という前提に基づいていることだ。3つの節は、読み手が気にかけなければならないものを減らすという同じ原則を、別の角度から見ているだけだと思う。
3つの技法の中では、値を一度だけ書き込むのがいちばん役に立つと感じた。price を書き換え続ける例は、subtotal、afterDiscount、finalPrice に直すと、変数名そのものがその段階の意味を説明してくれる。これまで const は誤って書き換えないための保護だと考えていたが、本当の価値は読み手の追跡コストを減らすことにあると気づいた。
また、AI にコードを書かせることが多い今の自分にとって、この章はレビューの基準として役に立つ。要件定義と設計は自分で決め、実装は AI に任せているので、生成されたコードに不要な変数がないか、スコープが広すぎる変数がないか、何度も書き換えられている変数がないかを、この3点で素早く確認できる。