155
208

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【JavaScript】読みやすいコードの書き方

Last updated at Posted at 2023-10-22

はじめに

私は他人のコードをレビューしたことも自身のコードを他人にレビューしてもらったこともない初学者として現在のプロジェクトに加わりました。そこから現在までの2年間毎月10から20ほどのプルリクエストをレビューし、またチームメンバー内で読みやすいコードについて議論することで、徐々に読みやすいコードを書くためのポイントが掴めてきました。

これらの経験を通じて、私が現在考えている読みやすいコードを書くためのポイントを本記事にまとめていきます✍️

前提

  • 言語はJavaScriptで、レガシーな環境での手続き的なJavaScriptを想定しています。
  • 「JavaScriptの文法やメソッドは理解してきたけど、より読みやすいコードの書き方がわからない」と感じている初学者向けです。
  • 筆者は一般的なコーディング規約や設計原則についての書籍や資料をほとんど参照していません(読んだことがある本と言えばリーダブルコードくらいです📖)。したがって、本記事で紹介するのは筆者の独自のアプローチです。あくまで参考程度にご覧ください🙇‍♂️

本題

読みやすいコードを書くためには非常に多くの要因が関わりますが、本記事では次の3つの観点に焦点を当てます。

  1. 処理を関数へ切り出す
  2. 処理を記述する順序
  3. コードの情報量を減らす

1. 処理を関数へ切り出す

処理をだらだらとグローバルスコープに書き連ねていくコードは非常に読みづらく、コードの可読性を高めるためにはコードの断片を関数に分解して切り出すことが重要です。
しかし、どの範囲をどの程度の粒度でどのような基準で関数に切り出せばよいのかが悩ましい点です🤔

このセクションでは、次の2つの観点でコードの切り出し方を考えていきます。

  1. 複数回実行される同じ処理はモジュールとして切り出す
  2. 処理に名前をつけるために切り出す

1.1. 複数回実行される同じ処理はモジュールとして切り出す

同じ処理が複数の箇所で実行される場合その処理は関数として切り出す とよいです。重複したコードは避けましょう(DRYの原則)。

この目的で切り出された関数をここではモジュールと呼びます。
ある処理をモジュール化することで 一度そのモジュールのコードを理解したら同じモジュールが別の場所で再び登場してもそのコードを再度理解する必要がなく読み進めることができる ため、読み手への負荷が減り可読性が向上します。

また、モジュールは通常「純粋関数」であることが望ましいです。純粋関数とは「同じ引数が渡された場合には常に同じ値を返し外部の変数や状態に依存しない関数」です1。モジュールが外部の変数を変更したり参照したりする場合、レビュワーは常にその外部変数の状態に気を配らなければならず、モジュール化の利点が半減してしまいます😵‍💫

モジュールの例1
// ローカルストレージから値を取得するモジュール(呼び出し側でtry-catchする手間を省くためのモジュール化)
function getValueFromLocalStorage(key) {
  try {
    return window.localStorage.getItem(key);
  } catch {
    return null;
  }
}
モジュールの例2
// フォーカス可能な要素をDOMから取得し返却するモジュール
function returnFocusableElms(parentElm = document) {
  const focusableElmSelector = [
    'a',
    'area',
    'input',
    'select',
    'textarea',
    'button',
    'iframe',
    'object',
    'embed',
    'video',
    'audio',
    'summary',
    '[tabindex]',
    '[contenteditable]',
  ].join(',');

  return parentElm.querySelectorAll(focusableElmSelector);
}
小さな補足

ここでのモジュールとは、ある特定の目的にしたがって自身で実装された処理のことを指します。既存の標準APIのメソッドを単に呼び出すだけの処理は、モジュールとして切り出す必要はありません。

不要なモジュール化の例
// このようなモジュールは不要。素直に`document.querySelector()`を呼び出せばいい
function getElmByClassString(className) {
  return document.querySelector(className);
}

function func() {
  const element = getElmByClassString('.someClass');
  // その他のコード...
}

1.2. 処理に名前をつけるために切り出す

コード全体がモジュールのみで構成されているならば上記の切り分けで完結するのですが、実際はそうではない場合が多いため、モジュール化とは別の考え方で処理を切り分けることも重要です。
それが 処理の塊に名前をつけるために関数に切り出す という考え方です✍️

1つのファイル内で複数の機能要件が実装されている場合、その機能要件ごとに関数に切り分けます。

機能要件ごとに名前をつけるために関数に切り出した例
// モーダルの機能要件
function initModal() { ... }

// スライダーの機能要件
function initSlider() { ... }

initModal();
initSlider();
ファイルを機能ごとに分割することと同じアプローチです。
src
├ modal.js // モーダルの機能要件
├ slider.js // スライダーの機能要件
└ main.js // `initModal`および`initSlider`の呼び出し

さらに、各処理内で機能ごとに名前をつけて詳細な関数に分割し、その内部で再び機能ごとに切り分ける、といった フラクタル構造2 のコードを構築します。

// モーダルの機能要件
function initModal() {
  // フォーカスの制御に関連した処理
  function manageFocus() {
    // フォーカストラップ処理
    function focusTrap() { ... }
    // フォーカスをモーダルに移動させる処理
    function focusToModal() { ... }

    focusTrap();
    focusToModal();
  }

  // モーダルを開く処理
  function openModal() { ... }

  // モーダルを閉じる処理
  function closeModal() { ... }

  // 開く処理・閉じる処理をそれぞれのトリガー要素のイベントリスナに登録する処理
  function addEvent() { ... }

  // すべての関数を実行
  manageFocus();
  openModal();
  closeModal();
  addEvent();
}

// スライダーの機能要件
function initSlider() {
  // その他のコード...
}

このように切り分けることにより、レビュワー目線「この処理はモーダルの機能要件なんだな → その中のフォーカス制御の処理なんだな → その中のフォーカストラップの処理なんだな」と、段階的に理解できます。このアプローチにより、コードが読みやすくなります👍

1.3. その他のポイント

1.3.1. どの範囲をどのくらいの粒度で関数として切り出すか

「どの範囲をどのくらいの粒度で関数として切り出すか」を考える際に「関数には単一の責務を持たせる」という観点が非常に重要です。とある関数で異なるいくつもの処理を行わせるのではなく、可能な限りひとつの機能に関する処理だけを記述するようにします。これによりデバッグも簡単になるうえ、関数の動作を理解しやすくなります👍

モジュールは特に単一の責務だけを持つことが重要ですし、名前付けのために切り出す際も同じ考え方で切り分けます。関数の命名に悩むようであればそれは関数が単一の処理を行なっていない可能性が高い ので、さらに処理を分割することも検討してください💭

また、「どの範囲をどのくらいの粒度で関数として切り出すか」のもうひとつの考え方として「20行以上のコードになったらその処理は関数へ切り出す」というざっくりとした指標を持っている先輩メンバーもいました。アリだと思います🙆‍♂️

1.3.2. JSDocを書く

切り出した関数には JSDocを記述する ことを強くオススメします。モジュールであれば「どのような引数を渡すとどのような値が返却される」という説明が欲しいですし、機能要件ごとに切り出した関数も「なにを行なっている関数なのか」という処理の要約が日本語で書かれていると非常に読みやすくなります。

関数の概要@description、引数の情報@param、返り値の情報@returnsの3点は少なくとも記述することを推奨します👨‍💻

2. 処理を記述する順序

ここまでで処理を目的に応じて切り分ける方法をまとめてきました✍️
コードの可読性をさらに向上させるために、切り出した関数を適切な順序で記述する ことも非常に重要なポイントです。

結論として、切り出した関数は「すべて宣言 → すべて実行」という順序で記述すると可読性が向上します。(ここでは「すべて宣言」部分を「宣言パート」、「すべて実行」部分を「実行パート」と呼称します。)

先ほど一例として載せたコードもこの順序で記述されています。モーダルに関連する機能要件をすべて宣言パートで宣言したのち、実行パートですべての関数を実行しています。

image (1).png

また、切り出されたそれぞれの関数の中でも、宣言パート → 実行パートの順序で書きます。ここでもフラクタル構造となっています。
image.png

2.1. 宣言パート

宣言パート → 実行パートという順序さえ守っていれば関数の未定義エラーになることはないため(const宣言の場合、宣言前に実行してしまうとエラーとなる)、特段注意するべき点などはありません💭

ただ、変数とモジュール、および機能要件ごとの関数の宣言はそれぞれセクションでまとめておくとコードがより整頓され可読性が向上するかと思います。

(() => {
  // グローバルな変数の宣言
  let isTest;
  let state;

  // モジュールの宣言
  function module1(arg) { return arg * 2; }
  function module2(arg) { return arg + 2; }

  // 機能要件の宣言
  function func() {
    if (isTest) { ... }
    const value = module1(10);
  }
  // 宣言パート ここまで

  // 実行パート
  func();
})();

2.2. 実行パート

実行パートでは宣言した関数を実行するのみの、なるべく簡潔な流れで記述します。ここを一本の簡潔な流れで記述されていれば、処理全体の流れが一目で理解できるため非常に読みやすいコードとなります👍

実行パートは関数に括らずに実行のみを記述する以外に、init()main()などの関数で括りすぐに実行したり、即時関数で括ったり、いくつか方法はありますが、自分が読みやすいと思う方法でまとめるとよいです。

実行パートの括り方の一例
括らずに実行する例
func1();
func2();
main関数で括る例
function main() {
  func1();
  func2();
}
main();
即時関数で括る例
(() => {
  func1();
  func2();
})();

2.3 スコープを切る

「宣言パートと実行パートの順序」に加えてもうひとつ可読性向上のための重要なポイントがあります。それは「関数のスコープを適切に切る」ということです。
より具体的には、「とある関数はその関数が呼び出されているスコープ内で宣言するべき」ということです✍️

先ほどのコードを例に見ていきましょう。
フォーカスに関連した処理focusTrapfocusToModalはそれぞれmanageFocus関数内のでのみ呼び出されているため、これら関数はmanageFocusの内部で宣言されることが適切です。実行されていないスコープであるinitModal関数内やグローバルスコープで宣言することは避けましょう。

NG
function focusTrap() { ... }
function focusToModal() { ... }

// モーダルの機能要件
function initModal() {
  // フォーカスの制御に関連した処理
  function manageFocus() {
    focusTrap();
    focusToModal();
  }

  // その他のコード...

  manageFocus();
}
OK
// モーダルの機能要件
function initModal() {
  // フォーカスの制御に関連した処理
  function manageFocus() {
    function focusTrap() { ... }
    function focusToModal() { ... }

    focusTrap();
    focusToModal();
  }

  // その他のコード...

  manageFocus();
}

これにより、コード内での関数や変数のスコープを明確にし、どのスコープでどの関数が利用可能かを示すことで可読性が向上します。

3. コードの情報量を減らす

最後のセクションでは コードの情報量を減らす というポイントをまとめていきます。

非常に多くの細かい観点があるかと思いますが、ひとまず以下4つのポイントをまとめます(また思いついたら書き足します✍️)。

  1. if文の条件分岐は「条件式は肯定文で」「例外のみを書き」「早期リターンする」
  2. 関数の引数の数は極力減らす
  3. グローバルな変数は減らす
  4. letの使用をなるべく避ける

3.1. if文の条件分岐は「条件式は肯定文で」「例外のみを書き」「早期リターンする」

3.1.1. 条件式は肯定文で書く

条件式はできる限り肯定文で記述し、否定文は避けるとコードが読みやすくなります。逆に否定文が多重に重なると読みづらくなるため避けましょう😵‍💫

NG
const hasNotClassName = !element.classList.contains('.className');
if (!hasNotClassName) { ... } // `.className`を持ってなくない場合?????
OK
const hasClassName = element.classList.contains('.className');
if (hasClassName) { ... } // `.className`を持っている場合

3.1.2. 例外のみを書く

原則として条件式は肯定文で記述したほうがよいですが、ひとつの例外が条件に一致する場合は否定文でも問題ありません。特定の例外だけを条件として記述するほうが読みやすくなります👍

NG
const arr = [1, 2, 3, 4];
arr.forEach((val) => {
  // valが 1, 2, 3 の場合...
  if (val === 1 || val === 2 || val === 3) { ... }
  // その他のコード...
})
OK
const arr = [1, 2, 3, 4];
arr.forEach((val) => {
  // valが 4 以外の場合...
  if (val !== 4) { ... }
  // その他のコード...
})

3.1.3. 早期リターンする

「とある条件の時のみ処理を続ける」という場合は、if文の条件文で例外を記述して関数を早期リターンするとコードが読みやすくなります👍(3.1.2. 例外のみを書く に従い、例外を否定文で記述しても問題ありません)。

これにより例外の処理を早期に終了でき、またコードのネストやインデントが減少し可読性が向上します。

NG
function func() {
  const elm = document.querySelector('.elm');
  if (elm) {
    elm.querySelectorAll('*').forEach(() => { ... });
  }
}
OK
function func() {
  const elm = document.querySelector('.elm');
  if (elm === null) return;

  elm.querySelectorAll('*').forEach(() => { ... });
}

3.2. 関数の引数の数は極力減らす

関数の引数は極力減らし、少ければ少ないほど読みやすくなります。また、この観点での関数の切り分け方も検討するとよいです🤔

以下の例では関連するデータをオブジェクトにまとめることで、引数の数を減らし、またどの値がどの引数に関連しているのかを明確にしています。

function displayUser(name, age, email, country, input) {
  input.value = name;
  // その他のコード...
}
function displayUser(user, input) {
  const { name, age, email, country } = user;
  input.value = name;
  // その他のコード...
}

3.3. グローバルな変数は減らす

2.3. スコープを切る では関数を適切なスコープに切ることをまとめましたが、この考え方は変数に対しても同様です。なるべくグローバルスコープで宣言する変数は減らし、使用されるスコープ内で宣言するとコードが読みやすくなります👍

NG
// モーダルトリガー(グローバル変数)
const openBtn = document.querySelector('.modal .button');

function initModal() {
  // その他のコード...

  function addEvent() {
    openBtn.addEventListener('click', () => { ... });
  }

  addEvent();
}
OK
function initModal() {
  // その他のコード...

  function addEvent() {
    // モーダルトリガー(ローカル変数)
    const openBtn = document.querySelector('.modal .button');
    openBtn.addEventListener('click', () => { ... });
  }

  addEvent();
}

3.4. letの使用をなるべく避ける

変数を宣言する際に、可能な限りletは減らしconstが使用できないかを検討しましょう。
letで宣言した値は再代入可能であり、将来的に値が変わる可能性があることを示唆します。そのため、コードをレビューする際に、いつ値が変わるかを理解する必要があり、変数が参照されるたびにその値の現在の状態を考える必要があります。これはコードを読む人にとって負担となります😢

一方constで宣言した値は再代入不可能であり、これらの負荷をできます。したがって、変数をletで宣言する前に、constで宣言できるかどうかを検討しましょう👍

NG
let str = '';
const inputElm = document.querySelector('input');

switch (inputElm.value) {
  case '01':
    str = 'hoge'
    break;
  case '02':
    str = 'fuga'
    break;
  case '03':
    str = 'piyo'
    break;
}
if (str === '') return;
// その他のコード...
OK
const inputElm = document.querySelector('input');
const str = (() => {
  switch (inputElm.value) {
    case '01':
      return 'hoge';
    case '02':
      return 'fuga';
    case '03':
      return 'piyo';
    default:
      return '';
  }
})();
if (str === '') return;
// その他のコード...

さいごに

ここまで、私が考える 読みやすいコードを書くためのポイント をまとめてきました。

私はコーディングに正解や誤りはなく3、あるのは実装者の意図だけだと考えています🤔。また、レビューイとレビュワーそれぞれが意図を持って議論してよりよいコードへと着地させるプロセスこそがコードレビューだとも考えています。逆に言えば意図を持たずにコーディングすることはあまりよくないとも思っていますが...😖

本記事の内容に関しても、正解でも誤りでもなく私が私が「より読みやすい」と考える方法をまとめたものです。したがってこの記事を読んだ上で、ご自身で考えた意図をコードに落とし込んでいただければ幸いです🙇‍♂️

  1. もしかしたら『純粋関数』の厳密な定義とは違うかもしれません...。その場合はコメントにてご指摘いただけると幸いです。

  2. 「部分と全体が同じ構造(相似)となっているもののこと」フラクタル - Wikipedia

  3. もちろん、プロジェクトごとのコーディング規約に違反していたり、一般的なベストプラクティスに反するいわゆるアンチパターンなどは「誤り」と言えますが。

155
208
5

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
155
208

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?