JavaScriptの落とし穴

新人がJavaScriptでバグを発生させやすい箇所を研修用の記事としてまとめておきます。

# 目次
- [実行環境](#実行環境)
- [変数宣言時のlet/const/var](#変数宣言時のletconstvar)
- [月が0からカウントされる酷い仕様](#月が0からカウントされる酷い仕様)
- [配列とオブジェクト型の順序](#配列とオブジェクト型の順序)
- [forEachと非同期処理](#foreachと非同期処理)
- - [forEachメソッド内で非同期関数を行う際の注意点](#foreachメソッド内で非同期関数を行う際の注意点)
- - [解決方法](#解決方法)
# 実行環境
Node.js v22.12.0

# 変数宣言時のlet/const/var

**■ let の特徴**

* ブロックスコープのローカル変数を宣言
* 再宣言不可、再代入可能
* 同じスコープ内での再宣言はエラーになる

```sample.js
let y = 1;
if (true) {
  let y = 2; // 新しいスコープで別のyを宣言
  console.log(y); // 2
}
// let y = 100; 同じスコープ内での再宣言はエラー

console.log(y); // 1
y = 100;
console.log(y); // 再代入は可能　100
```
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let

**■ const の特徴**

* ブロックスコープのローカル変数（定数）を宣言
* 再宣言不可、再代入不可
* オブジェクトや配列の場合、内容の変更は可能である

```sample.js
const PI = 3.14;
// PI = 3.14159; // エラー: 再代入不可
console.log(PI);

const obj = { key: "value" };
obj.key = "new value"; // オブジェクトの中身は変更可能
console.log(obj); // { key: "new value" }
// obj = { key: "new value" };// エラー obj変数に新しいオブジェクトを再代入しようとしているため、constの制約に違反
```

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const

**■ var の特徴**

* 関数スコープまたはグローバルスコープを持つ
* 再宣言と再代入が可能
* ホイスティング（変数宣言がそのスコープの先頭に「持ち上げられる」現象）が発生する

```sample.js
var x = 1;
if (true) {
  var x = 2; // 同じ変数xを上書き
  console.log(x); // 2
}
console.log(x); // 2
var x = 100; // 同じスコープ内での再宣言可能
console.log(x); // 100

console.log(y); //　ホイスティングが発生するため、宣言前にyを使用できるが出力は「undefined」
var y = 200;
console.log(y); //200

```

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/var

**■ 使い分け**
基本的にはconstを使用し、値の再代入が必要な場合などにletを使用。
ループのカウンタ変数など、値が変化する変数にletを使用。
varの使用は極力避ける。

```sample.js
const MAX_SIZE = 100;
let count = 0;

for (let i = 0; i < MAX_SIZE; i++) {
  count += i;
}

console.log(count);
```


# 月が0からカウントされる酷い仕様
月を数値で取得するのにDateオブジェクトのgetMonth()メソッドをよく利用しますが、0からカウントされてしまう酷い仕様のため注意が必要です。

**■ getMonthメソッドの注意点**
```getMonth() メソッド```は、地方時に基づき、指定された日付の「月」を表す**0**を基点とした値を返します。
```sample.js
const date = new Date();
console.log(date); // 例：2024-12-11T07:00:44.208Z

const month = date.getMonth();
console.log(`${month}月`);// 11月
```

sample.jsでは、本来の日付が12月であるのに、```getMonthメソッド```の戻り値は**11**となってしまいます。つまり、1月が0、2月が1 ...,12月が11となり、getMonthメソッドの戻り値は、ひと月ずれてしまいます。

なぜこのような仕様なのか?　[こちらの記事](https://www.hackfront.dev/posts/20211014_getmonth)が、参考になりました。

>**「欧米の言語では、月は数値ではなく単語で表現するため」**

```sample.js
// 英語の月名配列
const eng = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];

const date = new Date();
console.log(date); // 例：2024-12-11T07:00:44.208Z

console.log(eng[date.getMonth()]); // December
```
```getMonth() メソッド```の戻り値を、そのまま数値で使用したい場合は、```getMonth()+1```する必要があります。

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

# 配列とオブジェクト型の順序
**■ 配列の順序**
配列は要素の挿入順を保持します。これは配列の基本的な特性であり、インデックスを使用して要素にアクセスすることができます。
```sample.js
const users = [
    { id: 1, name: 'Alice', age: 30 },
    { id: 2, name: 'Bob', age: 25 },
    { id: 3, name: 'John', age: 35 }
  ];
  
  console.log(users[0].name); // Alice
  console.log(users[1].name); // Bob
  console.log(users[2].name); // John

  // インデックス0～順に表示　Alice->Bob->John
  users.forEach((user, index) => {
    console.log(`${index}: ${user.name}, ${user.age}`);
  });
```

**■ オブジェクトの順序**
ブジェクトのプロパティの順序は特定の条件下でのみ保証されます。
ES2015（ES6）以降、オブジェクトのプロパティは以下のような順序で保持されます。

1. 整数プロパティ（数値キー）は昇順
1. その他の文字列プロパティは作成された順序
1. シンボルプロパティも作成された順序

```sample.js
const userObj = {
  admin: { name: 'Admin', age: 40 },
  3: { name: 'John', age: 35 },
  1: { name: 'Alice', age: 30 },
  2: { name: 'Bob', age: 25 },
  guest: { name: 'Guest', age: 20 }
};
  
console.log(Object.keys(userObj)); // ['1', '2', '3', 'admin', 'guest']

// Alice->Bob->John->admin->Guest
for (const key in userObj) {
  console.log(`${key}: ${userObj[key].name}, ${userObj[key].age}`);
}
```


# forEachと非同期処理

#### JavaScriptのforEachメソッド
JavaScriptの```forEachメソッド```は、与えられた関数を、配列の各要素に対して一度ずつ実行します。例外を発生する以外の方法でループを止めたり脱出したりする方法はなく、そのような動作を行いたい場合は```for```、```for...of```、```for...in```を使用することが推奨されます。

```sample.js
const items = [1, 2, 3];
//配列の要素を10倍にして表示
items.forEach((item) => console.log(item * 10)); //10,20,30
```

#### forEachメソッド内で非同期関数を行う際の注意点
forEachメソッド内で、非同期関数を行う際は注意が必要です。非同期関数を扱う場合に期待する順序で処理が完了しない場合があります。

```sample1.js
// 指定されたミリ秒後に解決するPromiseを返す関数
const sleep = (s) => new Promise(resolve => setTimeout(resolve, s));

// 非同期関数
const asyncFunction = async (item) => {
    // ランダムな遅延後、配列の要素を出力
    await sleep(Math.random() * 1000);
    console.log(item);
  };
  
  // 処理対象の配列
  const items = [1, 2, 3, 4, 5];
  
  // forEachで各要素に対して非同期処理を実行
  // 各処理の完了を待たずに次の処理を開始してしまう
  items.forEach(async (item) => {
    // awaitは各反復内でのみ機能し、forEachのコールバック全体を待機させない
    await asyncFunction(item);
  });
  
  // 非同期処理の完了を待たずに実行される
  // 実際の非同期処理よりも先に出力される
  console.log("forEach処理の終了");
```
**出力結果**
![非同期forEach (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/4c35c10a-597a-4bea-4156-47c906aab18f.png)

出力順序は、非同期処理の完了順序に依存し、今回の場合は```sleep(Math.random() * 1000)```となっている為、実行するたびに出力結果が変化します。

#### 解決方法
この問題を解決するためには、for文（for...of）を使用する方法があります
```sample2.js
// 指定されたミリ秒後に解決するPromiseを返す関数
const sleep = (s) => new Promise(resolve => setTimeout(resolve, s));

// 非同期関数
const asyncFunction = async (item) => {
    // ランダムな遅延後、配列の要素を出力
    await sleep(Math.random() * 1000);
    console.log(item);
  };
  
  // 処理対象の配列
  const items = [1, 2, 3, 4, 5];

  // ここまでsample1.jsと同じ

   for (let i = 0; i < items.length; i++) {
   //各処理の完了を待つ
    await asyncFunction(items[i]);
  }
  console.log("for文処理の終了");
```

**出力結果**
![非同期for文.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/ab2e2997-02a9-0a9e-f844-f5e926d13985.png)

この方法では、for文内の各要素の処理が完了するまで待ってから次の要素の処理を開始するため、要素が順番に処理され、出力結果が保証されます。


https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%97%E3%82%87%E3%81%86