JavaScript実務で注意すべき仕様とその背景

JavaScriptは軽量で柔軟性に富んだ言語ですが、その設計上の都合や歴史的背景から、初学者はもちろん実務者にとっても思わぬバグの原因となる仕様が多数存在します。以下では、開発現場で頻出する注意点とその背景、正しい使い方や代替手段を紹介します。

目次

月の値が1ずれる理由

JavaScriptのDate.getMonth()は、1月を0、12月を11として返します。これはC言語のstruct tm構造体に由来しており、Cの影響を強く受けた初期JavaScriptの設計によるものです。

正しい使い方:

const date = new Date();
const month = date.getMonth() + 1; // 月を人間向けに表示する際は+1

ゆるい比較演算子(==)の予期しない挙動

JavaScriptの==は異なる型でも比較できるように暗黙的な型変換(型の強制)を行いますが、その結果、直感に反する結果を返すことがあります。

:

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

推奨される代替手段:

value === otherValue // 厳密等価演算子を使う

typeof null"object"を返す理由

typeof null === "object"という結果は、JavaScript初期のバグがそのまま仕様として残されたものです。これは長年仕様変更されずに互換性を保っているためです。

正しい判定方法:

if (value === null) {
  // nullの場合の処理
}

NaNが自分自身と等しくない

NaN(Not a Number)は、計算が無効であることを示す特殊な値です。その仕様として、NaN === NaNは常にfalseとなります。これはIEEE 754に準拠しているためです。

代替手段:

if (Number.isNaN(value)) {
  // NaNのときの処理
}

parseInt()で基数を省略すると誤動作する可能性

先頭に0が付いた文字列をparseIntで変換する場合、実装によっては8進数と解釈されることがありました。現在の実装では改善されていますが、明示的な基数指定が安全です。

正しい使い方:

parseInt('010', 10); // 明示的に10進数で解釈

thisの文脈依存

通常の関数定義では、呼び出し元によってthisの参照先が異なります。非同期処理やコールバック関数内で意図通りのthisを保つためには、アロー関数(=>)の利用が効果的です。

例(setTimeout内):

setTimeout(() => {
  console.log(this.value); // 外側のthisが保持される
}, 1000);

配列かどうかの正しい判定

JavaScriptではtypeof []"object"を返します。そのため、配列を正確に判定するには専用のメソッドが必要です。

正しい方法:

Array.isArray(data); // true なら配列

小数の誤差に注意

JavaScriptでは二進数で小数を表現するため、0.1 + 0.2 !== 0.3となる場合があります。これはIEEE 754浮動小数点演算の仕様によるものです。

誤差を考慮した比較:

Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON

argumentsは配列ではない

関数内部のargumentsは配列のように見えても、mapreduceなどの配列メソッドを持たないArray-like Objectです。ES6以降はレストパラメータ(...args)を使うことが推奨されます。

推奨される構文:

function sum(...args) {
  return args.reduce((a, b) => a + b);
}

変数の巻き上げ(Hoisting)

varで宣言された変数は、スコープの先頭に巻き上げられるため、意図しないundefinedやバグの原因になります。ES6以降のletconstを使うことでこの問題を避けられます。

:

console.log(x); // undefined(varの場合)
let x = 5;       // ReferenceError(letの場合、より安全)

Promiseチェーンとreturnの抜け漏れ

非同期処理でPromiseチェーンを使用する際、returnを省略すると意図した非同期制御ができなくなります。特にミドルウェアやAPI処理で多発する問題です。

悪い例:

function doAsync() {
  return fetch(url)
    .then(res => {
      doSomething(res); // returnしていないので非同期が終了しない
    })
    .then(() => console.log('done'));
}

正しい書き方:

function doAsync() {
  return fetch(url)
    .then(res => {
      return doSomething(res);
    })
    .then(() => console.log('done'));
}

オブジェクトをハッシュとして使う際の注意

JavaScriptのオブジェクトはObject.prototypeを継承しており、hasOwnPropertyなどのプロパティと衝突する恐れがあります。

リスク例:

const map = {};
map['hasOwnProperty'] = true;
console.log(map.hasOwnProperty('key')); // TypeError

代替手段:

const map = Object.create(null);
map['key'] = 42;
console.log(map['key']); // 42

for…in と for…of の使い分け

for...inは列挙可能なすべてのプロパティを走査するのに対し、for...ofは反復可能な値(iterable)を対象とします。

違いの例:

const arr = [10, 20, 30];

for (const i in arr) {
  console.log(i); // インデックス(0,1,2)
}

for (const val of arr) {
  console.log(val); // 値(10,20,30)
}

クロージャとループ内の変数

varを使ったループでは、クロージャ内での変数が意図せずすべて同じ参照になってしまう問題があります。

問題のあるコード:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
} // 3, 3, 3 が出力される

代替方法:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
} // 0, 1, 2 が出力される

JSON.stringify()の制約と注意点

JSON.stringify()は循環参照があるとエラーになります。また、undefinedや関数、Symbolなどの値は無視されます。

循環参照エラーの例:

const obj = {};
obj.self = obj;
JSON.stringify(obj); // TypeError: Converting circular structure to JSON

対策:

  • 循環を持たない構造を使う
  • カスタムreplacer関数を使う
  • ライブラリ(例: flattedcircular-json)を使う

結論

JavaScriptには歴史的経緯や設計上の制約から、注意深く扱わなければならない仕様が多数存在します。実務においては、これらの挙動を理解したうえで、意図しないバグを防ぐための書き方や設計の習慣を身につけることが重要です。特にES6以降の構文や標準メソッドを積極的に活用することで、安全で可読性の高いコードが実現できます。

Comment

コメントする

目次