⚡️

初心者の、初心者による、初心者のためのJavaScriptTips集

2024/09/04に公開

タイトルは「人民の、人民による、人民のための政治」という、エイブラハム・リンカーン大統領の有名な言葉のオマージュです。

はじめに

こんにちは hiro です。
タイトルにある通り、私が普段 JavaScript を書く上で何回も調べたことや、よくリファクタリングした部分を戒めのためにまとめてみました。

同じように悩んでいる方に共感していただけると幸いです。

1. 変数宣言は const を使用する

変数を宣言する際は、基本的にconstを使うようにしています。
ただし、ブロック内で使う変数や、ループ処理で使う変数など、後から値が変わる可能性があるものにはletを使うようにしています。

個人的には、varは絶対に使わないようにしています。正直なところ、私自身がvarを使わざるを得ないような状況に出くわしたことがないです。

const PI = 3.14;
PI = 3; // Uncaught TypeError: Assignment to constant variable.

// 値が変わりうる変数はletでOK
let count = 0;
for (let i = 0; i < 5; i++) {
  count += i;
}
console.log(count);

// 値の更新を意図していない変数のletの使用はNG
let changeableStr = "I'm hiro!";
console.log(changeableStr); // I'm hiro!

changeableStr = "I'm hogehoge!";
console.log(changeableStr); // I'm hogehoge! <= 値が書き換えられてしまう😵

// ブロックスコープでのletの使用はOK(上記同様 値が変わりうる場合)
if (true) {
  let blockScopedVar = "ブロック内でのみ有効";
  console.log(blockScopedVar); // ブロック内でのみ有効
}
console.log(blockScopedVar); // Uncaught ReferenceError: blockScopedVar is not defined

モダン JavaScript においてなぜvarがアンチパターンなのか別記事にまとめているのでそちらも参考にしてみてください。

https://zenn.dev/eneosgrandchild/articles/99aa52128f9ac7

2. 短絡評価と論理演算子

論理演算子(&&||)を使って短絡評価を行うことができます。これを利用すると、条件分岐を簡潔に書くことができ、無駄な if 文を減らすことができます。

&& (AND 演算子)

&&演算子は、左側の式が truthy な場合に右側の式を評価します。

// 従来の書き方
if (user.isLoggedIn) {
  showDashboard();
}

// 短絡評価を使った書き方
user.isLoggedIn && showDashboard();

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_AND

|| (OR 演算子)

||演算子は、左側の式が falsy な場合に右側の式を評価します。

// 従来の書き方
function greet(name) {
  if (name === undefined) {
    name = "ゲスト";
  }
  console.log(`こんにちは、${name}さん`); // こんにちは、ゲストさん
}

// 短絡評価を使った書き方
function greet(name) {
  name = name || "ゲスト";
  console.log(`こんにちは、${name}さん`); // こんにちは、ゲストさん
}

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_OR

Null 合体演算子 (??)

Null 合体演算子は ES2020 で導入されました。
??演算子は、左側の式がnullまたはundefinedの場合に右の値を返し、それ以外の場合に左の値を返します。

function displayPrice(price) {
  // priceが0の場合も'無料'と表示されてしまう
  const displayedPrice = price || '無料';

  // priceが0の場合は0と表示される
  const betterDisplayedPrice = price ?? '無料';

  console.log(価格: ${betterDisplayedPrice});
}
displayPrice(0); // 価格: 0
displayPrice(null); // 価格: 無料

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing

3. bool 値はなるべく省略して書く

bool 値の省略記法

bool 値を評価する際に、=== true=== falseを省略して書くことができます。

const checked = true;

// 従来の書き方
if (checked === true) {
  console.log("checked is true"); // checked is true
}

if (checked === false) {
  console.log("checked is false"); // 出力されない
}

// 省略した書き方
if (checked) {
  console.log("checked is true"); // checked is true
}

if (!checked) {
  console.log("checked is false"); // 出力されない
}

二重否定(!!)を使った bool 値への変換

!!を使うことで、任意の値を bool 値に変換(キャスト)することができます。
Boolean()を使用しても同様に bool 値に変換することができます。

個人的には記述量が少ないので!!を使用するようにしています。

以下のような、入力フォームでどこかしらに未入力の欄があれば、一律バリデーションに引っ掛けるような実装が簡単にできます。

const formData1 = {
  name: "太郎",
  email: "taro@example.com",
  message: "こんにちは",
};

const formData2 = {
  name: "", // <= 未入力
  email: "taro@example.com",
  message: "こんにちは",
};

function isFormValid(formData) {
  // 各フィールドが空でないかをチェック
  const isNameValid = !!formData.name;
  const isEmailValid = !!formData.email;
  const isMessageValid = !!formData.message;

  // && を使用して、全てtrueの時にしかtrueが返らないようにする
  return isNameValid && isEmailValid && isMessageValid;
}

console.log(isFormValid(formData1)); // true
console.log(isFormValid(formData2)); // false

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_NOT

4. 早期 return の使用

条件分岐が多くなると、コードが深くネストしてしまい、可読性が低下します。
早期returnを使うことで、コードをシンプルに保つことができます。

ユーザーが認証されているかどうかをチェックして、それに応じた内容を出力するような処理で比べてみます。

NG.js
// 従来の書き方
function checkUser(user) {
  if (user) {
    if (user.isLoggedIn) {
      if (user.hasPermission) {
        console.log("ユーザーは認証され、権限があります");
      } else {
        console.log("ユーザーは認証されていますが、権限がありません");
      }
    } else {
      console.log("ユーザーはログインしていません");
    }
  } else {
    console.log("ユーザーが存在しません");
  }
}

ネストしすぎて読もうとすると頭がパンクしてしまいます 🤯

早期 return を使って無駄なネストを減らします。

OK.js
function checkUser(user) {
  if (!user) {
    console.log("ユーザーが存在しません");
    return;
  }

  if (!user.isLoggedIn) {
    console.log("ユーザーはログインしていません");
    return;
  }

  if (!user.hasPermission) {
    console.log("ユーザーは認証されていますが、権限がありません");
    return;
  }

  console.log("ユーザーは認証され、権限があります");
}

かなり可読性が上がりました。

5. デフォルト引数を設定する

関数の引数にデフォルト値を設定することができます。
これにより、引数が省略された場合でも関数が正しく動作するようになります。

|| (OR 演算子)で使用したサンプルコードもデフォルト引数を使用することでさらにシンプルになります。

// 短絡評価を使った書き方
function greet(name) {
  name = name || "ゲスト";
  console.log(`こんにちは、${name}さん`);
}

// デフォルト引数を設定した書き方
function greet(name = "ゲスト") {
  console.log(`こんにちは、${name}さん`);
}

greet(); // こんにちは、ゲストさん
greet("太郎"); // こんにちは、太郎さん

6. 分割代入

ES2015 で追加された機能で、配列やオブジェクトから値を簡潔に取り出すことができます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

配列

bad.js
const arr = [1, 2, 3, 4, 5];

const first = arr[0];
const second = arr[1];
const rest = arr.slice(2, 5); // [3, 4, 5]

console.log(first); // 1
console.log(second); // 2
console.log(rest); // 3, 4, 5
good.js
const arr = [1, 2, 3, 4, 5];

const [first, second, ...rest] = arr;

console.log(first); // 1
console.log(second); // 2
console.log(rest); // 3, 4, 5

全く同じ結果なのに可読性が全然違いますね。
ちなみに...restはレストパラメータといって、分割代入で指定した要素以外の全ての要素を新しい配列として取得します。
イメージとしては残余の要素を 圧縮(まとめる) するイメージです。

オブジェクト

bad.js
const person = {
  name: '田中太郎',
  age: 30,
  city: '東京',
  country: '日本'
};

const name = person.name;
const age = person.age;
const otherInfo = {
  city: person.city,
  country: person.country
};

console.log(name);      // '田中太郎'
console.log(age);       // 30
console.log(otherInfo); // { city: '東京', country: '日本' }
good.js
const person = {
  name: '田中太郎',
  age: 30,
  city: '東京',
  country: '日本'
};

const { name, age, ...otherInfo } = person;

console.log(name);      // '田中太郎'
console.log(age);       // 30
console.log(otherInfo); // { city: '東京', country: '日本' }

オブジェクトの場合でもレストパラメータは使用できます。

ユースケース

以下のように API からのレスポンスを分割代入を使用して処理したい場合などに活用すると便利です。

sample.js
async function getUserInfo(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    const user = await response.json();

    // オブジェクトの分割代入を使用して必要な情報を抽出
    const { name, email, address: { city, country }, ...otherInfo } = user;

    // 抽出した情報を使用
    console.log(`名前: ${name}`);
    console.log(`メール: ${email}`);
    console.log(`都市: ${city}`);
    console.log(`国: ${country}`);
    console.log('その他の情報:', otherInfo);

    // 配列の分割代入を使用(ユーザーの最近のアクティビティを仮定)
    const [latestActivity, secondLatestActivity, ...olderActivities] = user.recentActivities;

    console.log('最新のアクティビティ:', latestActivity);
    console.log('2番目に新しいアクティビティ:', secondLatestActivity);
    console.log('それ以前のアクティビティ:', olderActivities);

    return { name, email, city, country, latestActivity };
  } catch (error) {
    console.error('ユーザー情報の取得に失敗しました:', error);
    throw error;
  }
}

7. スプレッド構文

ES2015 で追加された機能(オブジェクトは ES2018 で追加)で、配列やオブジェクトの要素を展開するために使用されます。
レストパラメータが圧縮するイメージだったのに対し、スプレッド構文は展開します。
同じ記法なのに挙動が変わるので注意しましょう。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

配列

bad.js
const fruits = ['りんご', 'バナナ'];
const moreFruits = ['オレンジ'].concat(fruits).concat(['ぶどう']);

const fruitsCopy = fruits.slice();

const vegetables = ['にんじん', 'ブロッコリー'];
const allFood = fruits.concat(vegetables);

console.log(moreFruits);  // ['オレンジ', 'りんご', 'バナナ', 'ぶどう']
console.log(fruitsCopy);  // ['りんご', 'バナナ']
console.log(allFood);     // ['りんご', 'バナナ', 'にんじん', 'ブロッコリー']
good.js
const fruits = ['りんご', 'バナナ'];
const moreFruits = ['オレンジ', ...fruits, 'ぶどう'];

const fruitsCopy = [...fruits];

const vegetables = ['にんじん', 'ブロッコリー'];
const allFood = [...fruits, ...vegetables];

console.log(moreFruits);  // ['オレンジ', 'りんご', 'バナナ', 'ぶどう']
console.log(fruitsCopy);  // ['りんご', 'バナナ']
console.log(allFood);     // ['りんご', 'バナナ', 'にんじん', 'ブロッコリー']

配列を結合するサンプルコードです。
concatを使用するよりも簡潔に書くことができます。

オブジェクト

bad.js
const baseConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const devConfig = Object.assign({}, baseConfig, {
  environment: 'development',
  debug: true
});

const userInfo = { name: '山田太郎', age: 30 };
const userAddress = { city: '東京', country: '日本' };
const fullUserInfo = Object.assign({}, userInfo, userAddress);

console.log(devConfig);    // {apiUrl: 'https://api.example.com', timeout: 5000, environment: 'development', debug: true}
console.log(fullUserInfo); // {name: '山田太郎', age: 30, city: '東京', country: '日本'}
good.js
const baseConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const devConfig = {
  ...baseConfig,
  environment: 'development',
  debug: true
};

const userInfo = { name: '山田太郎', age: 30 };
const userAddress = { city: '東京', country: '日本' };
const fullUserInfo = { ...userInfo, ...userAddress };

console.log(devConfig);    // {apiUrl: 'https://api.example.com', timeout: 5000, environment: 'development', debug: true}
console.log(fullUserInfo); // {name: '山田太郎', age: 30, city: '東京', country: '日本'}

オブジェクトを結合するサンプルコードです。
Object.assign()を使用するよりも簡潔に書くことができます。

また、スプレッド構文を使用すると要素の任意の位置で展開できるという点もメリットの1つかと思います。

8. 配列操作(よく使うもの)

繰り返し処理

配列の要素に対して繰り返し処理を行うには、主にforEach()map()for...ofループを使用しています。

const numbers = [1, 2, 3, 4, 5];

// forEach(): 各要素に対して処理を実行
// indexを使用したい場合に使ってます
const doubledWithForEach = [];
numbers.forEach((num, index) => {
  doubledWithForEach[index] = num * 2;
});
console.log("forEach結果:", doubledWithForEach); // [2, 4, 6, 8, 10]

// map(): 各要素を変換して新しい配列を作成
const doubledWithMap = numbers.map((num) => num * 2);
console.log("map結果:", doubledWithMap); // [2, 4, 6, 8, 10]

// for...of: 各要素に対して処理を実行
const doubledWithForOf = [];
for (const num of numbers) {
  doubledWithForOf.push(num * 2);
}
console.log("for...of結果:", doubledWithForOf); // [2, 4, 6, 8, 10]

いずれも元の配列内の数値を 2 倍して新しい配列に格納するというものですが、map()だけはデフォルトで新しい配列を生成するという挙動の違いがあります。
より簡潔に書けるので基本的にmap()で実現できる場合は優先的に使用するようにしています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map

並び替えと絞り込み

並び替えと絞り込みは頻出なので tips としてまとめてみます。

並び替えにはsort(),絞り込みにはfilter()を使用することが多いです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

配列

const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];

// 数値の昇順でソート
const sortedNumbers = [...numbers].sort((a, b) => a - b);
console.log(sortedNumbers); // [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

// 5より大きい数値のみをフィルタリング
const filteredNumbers = numbers.filter((num) => num > 5);
console.log(filteredNumbers); // [9, 6]

オブジェクト

const people = [
  { name: "田中", age: 30 },
  { name: "佐藤", age: 25 },
  { name: "鈴木", age: 40 },
];

// 年齢でソート
const sortedPeople = [...people].sort((a, b) => a.age - b.age);
console.log(sortedPeople);

// 30歳以上のみをフィルタリング
const adultPeople = people.filter((person) => person.age >= 30);
console.log(adultPeople);

9. Set オブジェクト

ES2015 で追加されたSetオブジェクトは、中身の値に重複のないオブジェクトを返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set

const arr = ["りんご", 1, "バナナ", 2, "りんご", 3, 1, "オレンジ", 2];

const set = new Set(arr);
console.log(set); // Set(6) {'りんご', 1, 'バナナ', 2, 3, 'オレンジ'}

上記の通り文字列、数値関係なく重複している値を一意にまとめてくれます。

スプレッド構文と組み合わせることで、値の重複のない配列を簡単に作成できます。

const arr = ["りんご", 1, "バナナ", 2, "りんご", 3, 1, "オレンジ", 2];

const setArray = [...new Set(arr)];
console.log(setArray); // ['りんご', 1, 'バナナ', 2, 3, 'オレンジ']

Set オブジェクトにはadd()を使用することでオブジェクトに値を追加することができますが、自動で一意にしてくれるため、重複していてもエラーにならないし条件分岐も書く必要がありません。

const setFruit = new Set();

setFruit.add("メロン");

setFruit.add("メロン");

const fruits = setFruit; // Set(1) {'メロン'}

終わりに

ここまで読んでいただき、ありがとうございました。
今回紹介したものは私がよく使う Tips のほんの一部に過ぎませんが、少しでも皆さんの開発に役立てば幸いです。

もし、「これ便利だな」と思うものがあったら、ぜひ自分のプロジェクトに取り入れてみてください。
そして、皆さん自身の経験から得た Tips や工夫も、ぜひご教示ください。

また、間違い等があればご指摘お願いします。

Discussion