12k
All articles

JavaScript の落とし穴:何度も遭遇する5つの問題

型の強制変換、thisバインディング、巻き上げ、非同期の誤用、意図しないミューテーションなど、本番コードを壊すJavaScriptの落とし穴を認識して回避する方法を解説する。

OpenReplay Team
OpenReplay Team
JavaScript の落とし穴:何度も遭遇する5つの問題

リンティングを通過し、開発環境では正常に動作したコードを本番環境にデプロイしたのに、それでも障害が発生した経験はありませんか。振り返ってみれば明白なバグ—await の書き忘れ、配列の変更、予期しない場所を指す this。これらの JavaScript の落とし穴が持続するのは、言語の柔軟性が微妙なトラップを生み出し、最新のツールでも常に検出できるわけではないからです。

ここでは、実際のコードベースで今も頻繁に見られる5つの一般的な JS の間違いと、それらを回避するための実用的な方法を紹介します。

重要なポイント

  • 予期しない型強制の動作を避けるため、厳密等価演算子(===)を使用する
  • アロー関数は囲んでいるスコープから this を保持するが、通常の関数は動的に this をバインドする
  • const を優先し、変数をスコープの先頭で宣言して一時的デッドゾーンエラーを回避する
  • 並列非同期操作には Promise.all を使用し、部分的な結果が必要な場合は Promise.allSettled を使用する
  • toSorted() のような非破壊的な配列メソッドを使用し、ディープコピーには structuredClone() を使用する

型強制は今でも予想外の結果をもたらす

JavaScript の緩い等価演算子(==)は型強制を実行し、基礎となるアルゴリズムを理解するまでは非論理的に見える結果を生み出します。

0 == '0'           // true
0 == ''            // true
'' == '0'          // false
null == undefined  // true
[] == false        // true

修正方法は簡単です:どこでも厳密等価演算子(===)を使用します。しかし、型強制は他のコンテキストでも発生します。+ 演算子は、いずれかのオペランドが文字列の場合に連結を行います:

const quantity = '5'
const total = quantity + 3 // '53'、8ではない

最新の JavaScript のベストプラクティスでは、意図が重要な場合は Number()String()、またはテンプレートリテラルによる明示的な変換を推奨しています。Nullish 合体演算子(??)もここで役立ちます—||0'' を falsy として扱うのに対し、??null または undefined の場合のみフォールバックします。

this バインディングの問題

this の値は、関数が定義された場所ではなく、どのように呼び出されるかに依存します。これは JavaScript の最も持続的な落とし穴の1つです。

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name)
  }
}

const greet = user.greet
greet() // undefined—'this' はグローバルオブジェクトを指している

アロー関数は囲んでいるスコープから this をキャプチャするため、いくつかの問題は解決しますが、実際に動的バインディングが必要な場合には別の問題を引き起こします:

const user = {
  name: 'Alice',
  greet: () => {
    console.log(this.name) // 'this' は 'user' ではなく外側のスコープを参照
  }
}

コンテキストを保持したいコールバックにはアロー関数を使用します。オブジェクトメソッドには通常の関数を使用します。メソッドをコールバックとして渡す場合は、明示的にバインドするか、アロー関数でラップします。

巻き上げと一時的デッドゾーン

letconst で宣言された変数は巻き上げられますが初期化されないため、アクセスすると ReferenceError をスローする一時的デッドゾーン(TDZ)が作成されます:

console.log(x) // ReferenceError
let x = 5

これは、巻き上げられて undefined に初期化される var とは異なります。TDZ はブロックの開始から宣言が評価されるまで存在します。

関数宣言は完全に巻き上げられますが、関数式は巻き上げられません:

foo() // 動作する
bar() // TypeError: bar is not a function

function foo() {}
const bar = function() {}

変数をスコープの先頭で宣言し、デフォルトで const を優先します。これにより TDZ の驚きを排除し、意図を明確に示すことができます。

JavaScript における非同期の落とし穴

await の書き忘れはよくありますが、より微妙な非同期の間違いがより大きな損害を引き起こします。並列実行が可能な場合に順次 await を使用すると時間を無駄にします:

// 遅い:順次実行
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()

// 速い:並列実行
const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments()
])

もう1つの頻繁な問題:Promise.all は高速失敗します。1つの Promise が reject されると、すべての結果を失います。部分的な結果が必要な場合は Promise.allSettled を使用します:

const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
const successful = results.filter(r => r.status === 'fulfilled')

常に拒否を処理します。未処理の Promise 拒否は Node プロセスをクラッシュさせ、ブラウザでは静かな失敗を引き起こす可能性があります。

JavaScript における変更と不変性

配列やオブジェクトを変更すると、特にリアクティブな状態を持つフレームワークでは、追跡が困難なバグが発生します:

const original = [3, 1, 2]
const sorted = original.sort() // original を変更してしまう!
console.log(original) // [1, 2, 3]

最新の JavaScript は非破壊的な代替手段を提供します。配列には toSorted()toReversed()with() を使用します。オブジェクトの場合、スプレッド構文でシャローコピーを作成します:

const sorted = original.toSorted()
const updated = { ...user, name: 'Bob' }

スプレッドはシャローコピーを作成することを覚えておいてください。ネストされたオブジェクトは依然として参照を共有します:

const copy = { ...original }
copy.nested.value = 'changed' // original.nested.value も変更される

ディープクローニングには、structuredClone() を使用するか、ネストされた構造を明示的に処理します。

まとめ

これら5つの問題—型強制、this バインディング、巻き上げ、非同期の誤用、偶発的な変更—は、JavaScript のバグの不釣り合いに大きな割合を占めています。コードレビューでこれらを認識することは、実践を重ねることで自動的になります。

eqeqeqno-floating-promises のような厳格な ESLint ルールを有効にします。型安全性が重要なプロジェクトでは TypeScript を検討してください。最も重要なのは、JavaScript の暗黙的な動作に依存するのではなく、意図を明示的にするコードを書くことです。

よくある質問

なぜ JavaScript には == と === の両方の演算子があるのですか?

JavaScript には歴史的な理由と柔軟性のために両方が含まれています。緩い等価演算子(==)は比較前に型強制を実行しますが、これは便利な場合もありますが、しばしば予期しない結果を生み出します。厳密等価演算子(===)は型強制なしで値と型の両方を比較します。最新のベストプラクティスでは、比較を予測可能にしてバグを減らすため、=== を強く推奨しています。

アロー関数と通常の関数はどのような場合に使い分けるべきですか?

コールバック、配列メソッド、周囲の this コンテキストを保持したい状況ではアロー関数を使用します。オブジェクトメソッド、コンストラクタ、動的な this バインディングが必要な場合には通常の関数を使用します。重要な違いは、アロー関数が囲んでいるスコープから this を字句的にバインドするのに対し、通常の関数は呼び出され方に基づいて this を決定することです。

一時的デッドゾーンとは何ですか?どのように回避できますか?

一時的デッドゾーン(TDZ)は、スコープに入ってから let または const 変数が宣言されるポイントまでの期間です。この期間中に変数にアクセスすると ReferenceError がスローされます。変数をスコープの先頭で宣言し、デフォルトで const を優先することで TDZ の問題を回避できます。これによりコードがより予測可能で読みやすくなります。

Promise.all と Promise.allSettled はどのように使い分けますか?

操作が意味を持つためにすべての Promise が成功する必要がある場合は Promise.all を使用します—いずれかの Promise が reject されると高速失敗します。複数のオプションのソースからデータを取得する場合など、個々の失敗に関係なくすべての Promise から結果が必要な場合は Promise.allSettled を使用します。Promise.allSettled は、各結果を fulfilled または rejected として記述するオブジェクトの配列を返します。

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.