JavaScriptのハマりどころ全11件!

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、私ごときが・・・というお話なのですが、実は少し前から個人でプログラムのオンライン・レッスンをやらせてもらっています。

そして、少し前ある生徒さんから「JavaScriptでこれは知っておいた方がいいよ、というものがあったらぜひ知りたい」というご要望をいただき、私もいい機会だと思い、そういった内容をまとめてみました。

その中で、ひとつしみじみと感じたことがあったのですが、

それは・・・・・

よく考えたらJavaScriptってハマリどころ多いよね・・・💦

というものです。

紛いなりにも15年以上、ほぼ毎日プログラムを書いてきたのでそれほど気にならなくなってしまいましたが、もし今日からJavaScriptを勉強するなら「いやいや、それダメでしょ・・・😂」というような内容があることに気がついたんですね。

そこで❗

今回は、「JavaScriptのハマリどころ」というテーマで内容をまとめてみました。

ぜひJavaScript学習者さんたちにお役に立てましたら嬉しいです😊✨

※ ちなみに、あくまで私の感覚ですが【ハマリ度】として1〜5の数字を各項目につけています。

「はじめて知ったときは、こうなりました(笑)👆」

実行環境: Google Chrome 84

Date() の月は数字がズレる

JavaScriptで日付の操作をするときによく使われるのがDate()です。

ただ、このDate()で月を取得する場合はクセが強いので注意が必要です。

では、今月が何月なのかを取得する例を見てみましょう。

// ⚠ これは間違った例です

const dt = new Date(); // 今月が8月だとして、
const month = dt.getMonth(); // 👈 ここは「7」になる😫

そうです。

実はgetMonth()ゼロから始まる数字が取得されるので、実際のデータは次のようになります。

1月 → 0(ゼロ)
2月 → 1
3月 → 2

11月 → 10
12月 → 11

おそらく英語では、月が January, February, March … と名前がついているので数字というよりは何個目みたいなイメージなのでしょう。

そのため、これを解決するには、次のように+1をする必要があります。

const dt = new Date();
const month = dt.getMonth() + 1; // これで今月の数字が取得できます

しかも、日付を取得するgetDate()は逆に1から始まるのでさらに混乱させられますね・・・

【ハマリ度】 😫😫😫

getDay()で日付は取得できない

もうひとつDate()でのハマリどころです。

Date()を使って日付を取得するとき直感的に次のように思い浮かべると思いますがこれは注意が必要です。

  • 時間を取得: getHours()
  • 分を取得: getMinutes()
  • 月を取得: getMonth()
  • 日付を取得: getDay() 👈 これは間違いです

なぜなら、getDay()日付を取得するものではなく、曜日(数字)を取得するものだからです。(なぜこのメソッド名にしてしまったのか・・・😂)

そのため、次のようにするとうまくいきそうですが、期待した動きにはなりません。

// ⚠ これは間違った例です

const dt = new Date();
const day = dt.getDay(); // 👈 これでは日付は取得できません

正しくは、getDate()です。

const dt = new Date();
const day = dt.getDate(); // これで日付が取得できます

また、年を取得する場合もgetYear()ではなくgetFullYear()を使わないといけないので、さらに混乱させられますね・・・

ということで、先ほどのgetMonth()のこともありますし、JavaScriptで日付を扱う場合は moment などのパッケージを使うことをおすすめします。

【ハマリ度】 😫😫😫

typeofでは、配列かどうかはチェックできない

JavaScriptには、「変数の中身がどんなタイプのものか」を取得できるtypeofというものが用意されています。

使い方は、次のようになります。

const test = 'テスト';

if(typeof test === 'string') {

    console.log('変数の中身は文字列です');

}

しかし、ここにも罠が潜んでいます。
それは、変数の中身が配列のときです。

では、間違った例を見てみましょう。

// ⚠これは間違った例です。

const colors = ['赤', '青', '黄'];

if(typeof colors === 'array') {

    console.log('変数は配列なのに、ここは通りません');

}

先ほどの文字列の例を見ると、上のコードは一見うまくいきそうですが、期待通りの動きにはなりません。

なぜなら、配列に「typeof」を使っても「object」が返ってくるからです。

つまり、typeofでは、オブジェクトと配列の違いを区別することはできないという意味になります。

では、配列かどうかをチェックするにはどうすればいいか?ですが、以下2つをご紹介します。

(1)instanceof で比較する

instanceofを使えば、その変数が「何のインスタンスなのか」がわかるので、以下のようにArrayのインスタンスかどうかをチェックすることで配列かどうかがわかります。

const colors = ['赤', '青', '黄'];

if(colors instanceof Array) {

    console.log('中身は配列です');

}

(2)isArray()を使う

また、isArray()を使っても同じく配列かどうかのチェックができます。

const colors = ['赤', '青', '黄'];

if(Array.isArray(colors)) {

    console.log('中身は配列です');

}

ちなみに、nullの場合でも、typeofobjectになります。
厳密に言うと「nullオブジェクト」ということで間違ってないんでしょうけど、なぜこの仕様でOKになったのか・・・😂

【ハマリ度】 😫😫

オブジェクトが空かどうかは、工夫が必要

例えば、JavaScriptで空のオブジェクトを定義する場合、以下のようになります。

const obj = {};

そして、オブジェクトが空かどうかをチェックするには、次のようにしてしまいがちですが、これは正しくありません。

// ⚠これは間違った例です

const obj = {};

if(obj === {}) {

    console.log('実際には、ここは通りません');

}

では、どうすればいいかというと、「オブジェクトのキー」がいくつ存在しているかでチェックします。

実際の例はこちらです。

const obj = {};

if(Object.keys(obj).length === 0) {

    console.log('ここは通ります(オブジェクトは空です)');

}

流れはこうです。

  1. Object.keys()でオブジェクトの全キーを取得
  2. キーの数を「length」で取得
  3. その数がゼロならオブジェクトは空

これは、今からでも先ほどの間違った例が使えるように仕様変更してほしいぐらいの落とし穴です(笑)

【ハマリ度】 😫😫😫😫

オブジェクトのコピーは、「全く同じもの」になる

例えば、obj1をコピーしてobj2をつくる場合、どうするでしょうか。
もしかすると、次のように直接代入すればいいんじゃ?と考えるかもしれません。

// ⚠これはすべきではない書き方です

let obj1 = { name: '太郎' };
let obj2 = obj1;

しかし、これは特に注意が必要です。
なぜなら、obj1obj2は全く同じものになるからです。

どういうことか実際に見てみましょう。

例えば、続きのコードとしてobj2nameだけを「次郎」に変更したとします。

// ⚠これはすべきではない書き方です

let obj1 = { name: '太郎' };
let obj2 = obj1;

obj2.name = '次郎'; // 👈 obj2 の name だけ変更

すると、なんと書き換えていない「obj1」の「name」も「次郎」に変更されてしまうのです。

つまり、obj1obj2は全く同じものと扱われるということになります。(参照渡しみたいなことですね)

では、解決法ですがこちらも2つご紹介します。

(1)一度JSONに変換して、元に戻す

これはオブジェクトを一度JSON(つまり、文字列)に変換して、また元に戻すことで「中身が同じだけど、新しいオブジェクト」をつくることができます。

const obj = { name: '太郎' };
const newObj = JSON.parse(JSON.stringify(obj));

流れは次のとおりです。

  1. JSON.stringify() でオブジェクトをJSON(文字列)にする
  2. そして、その値をすぐ JSON.parse() でオブジェクトに戻す
  3. 新しいオブジェクトの完成

(2)Object.assign() をつかう

ES 6として新しく追加されたObject.assign()は、オブジェクトを結合するメソッドですが、これを使ってオブジェクトのコピーをつくることができます。

const obj = { name: '太郎' };
const newObj = Object.assign({}, obj);

つまり、「空のオブジェクト + コピーしたいオブジェクト」で結合しているわけですね。

【ハマリ度】 😫😫😫😫😫

オブジェクトは順番を保証していない

例えば、次のようなオブジェクトがあったとします。

const mixObj = {
    name: '太郎',
    1: 'ONE',
    id: 5
};

そしてこのオブジェクトを以下のようにループで全て取り出す場合を考えてみましょう。

for(let key in mixObj) {

    console.log(key);

}

この場合、キーはどんな順番で取得できるでしょうか。

おそらくコードで書いたとおり「name」「1」「id」の順番になると思うかもしれませんが、実際はそうはなりません。

なんと、次のように「1」が先頭にきてしまいます。

  • 1
  • name
  • id

つまり、オブジェクトはデータの順番を保証していないので、もし順番が重要な場合はオブジェクトを使うべきではありません。

では、どのようにするかというと答えは「コレクション」(オブジェクトの配列)です。

実際の例を見てみましょう。

// コレクションで定義
const collection = [
    { key: 'name', value: '太郎' },
    { key: 1, value: 'ONE' },
    { key: 'id', value: 5 },
];

for(let i in collection) {

    const data = collection[i];
    console.log(data.key); // 👈 順番はそのまま保持されます

}

このようにデータを配列化することでループの順番を固定することができます。

【ハマリ度】 😫😫

for ~ of にオブジェクトは使えない

ES 6 の新しい書き方にfor ~ ofというのがあります。
これは、値をひとつずつループで取得できる書き方なのですが、実はこれにオブジェクトは使えません。

実際の例を見てみましょう。

// ⚠これは間違った例です

const alcohol = {
    beer: 'ビール',
    wine: 'ワイン',
    cocktail: 'カクテル'
};

for(let value of alcohol) { // 👈 エラーになる

    //

}

これは一見うまくいきそうですが、以下のエラーが出ます。

Uncaught TypeError: alcohol is not iterable

意味としては、「そのデータはループできないよ😫」というものです。

では、どうするかというと次のようにObject.entries()keyvalueに分割しながらループをします。

for(const [key, value] of Object.entries(alcohol)) {

    console.log(key, value);

}

なお、for ~ inでキーを取得しながらループする従来の方法も使えます。

for(let key in alcohol) {

    const value = alcohol[key];
    console.log(key, value);

}

【ハマリ度】 😫😫😫

replace() は基本的にひとつだけしか入れ替えてくれない

例えば、次のような文字列の中から1_one_に入れ替える場合を見てみましょう。

const text = '123123123';

JavaScriptには、replace()というメソッドをがあるのでこれを使って以下のように正規表現で入れ替えをします。

const text = '123123123';
const text2 = text.replace(/1/, '_ONE_');

しかし、ここは注意が必要です。

なぜなら、JavaScriptの replace() が影響を与えるのは、通常は最初の1ヶ所だけだからです。

つまり、text2は「_ONE_23123123」となります。

ではどうするかというと、正規表現にgをつけてあげることになります。

const text = '123123123';
const text2 = text.replace(/1/g, '_ONE_'); // 👈 g をつける

これで実行するとtext2は、「_ONE_23_ONE_23_ONE_23」となります。

【ハマリ度】 😫😫

階層が深いデータのif文は、注意が必要

例えば、オブジェクトにいくつかデータが入っていて、その中身の値をif文で比較する場合を見てみましょう。

const item = {
    name: '商品名',
    price: 1000
};

if(item.name !== '') {

    //

}

実はこの場合では、何も問題はありません。

しかし、問題となるパターンがあります。
それは、nullundefinedが入ってくる可能性がある場合です。

つまり、もし以下のようにitemに初期値でnullが入っていたとして、その後もデータが変更にならなかった場合です。

let item = null;

// そして、item のデータが見つからなかったら・・・

if(item.name !== '') { // ここはエラーになります

    //

}

この場合、「Uncaught TypeError: Cannot read property ‘name’ of null」というエラーが出ます。

つまり、「nullには、nameというプロパティはないよ・・・😫」と言われるわけです。

そのため、こういった場合では次のようにif文の条件をひとつ増やして保険をかけておくことをおすすめします。

if(item && item.name !== '') { // こっちがベター

   //

}

これは、1つ目のitemで値がnullundefinedの場合はそこで比較が終了するため、2つ目のitem.nameまで行かず、結果としてエラーは出ないということになります。

【ハマリ度】 😫😫😫

sort()は先頭の文字から並べ替えをする

例えば、いくつかの数字が入った配列があって、その数字をsort()で並べ替える場合は注意が必要です。

実際の例を見てみましょう。

// ⚠これは間違った例です

const numbers = [1, 3, 11, 8, 13];
const sortedNumbers = numbers.sort(); // 👈 数字の順にはならない

結果としてsortedNumbersは次のように並べ替えられることになります。

[1, 11, 13, 3, 8] // 👈 思ってたのと違う・・・

つまり、先頭の文字から順に並べ替えをしてしまうわけです。

では、全体をみて順番に並べ替えをするにはどうすればいいかというとコールバック関数を使って次のようにします。

const numbers = [1, 3, 11, 8, 13];
const sortedNumbers = numbers.sort((a, b) => {

    return a - b;

});

結果は、次のようになります。

[1, 3, 8, 11, 13]

【ハマリ度】 😫😫😫😫

indexOf(), inclues()のデータ比較は厳格

例えば、配列の中に特定の値が入っているのかをチェックする場合、indexOf()inludes()を使いますが、この場合は型のチェックも気をつけておかないと期待通りに動かないことになります。

では、まず間違った例をみてみましょう。

const numbers = [
    1,
    '2', // 👈 文字列
    3
];

if(numbers.indexOf(2) > -1) {

    console.log('ここは通りません');

}

numbersの中に「2」が入っていればif文の中を通るはずですが、実はそうはなりません。

なぜなら、データの比較をしている「2」は文字列と数値の比較になっているからです。

つまり、もし先ほどの例で「2」が含まれているのかをチェックする場合はindexOf()の中にいれる「2」も文字列にしないといけません。

if(numbers.indexOf('2') > -1) {

    console.log('こっちは通ります');

}

そして、これと同じことがincludes()にも起こります。

if(numbers.includes(2)) {

    console.log('ここは通りません');

}

if(numbers.includes('2')) {

    console.log('こっちは通ります');

}

特にMySQLのドライバーに「mysqlnd」ではなく、「mysqli」を使っている場合では、integerであっても自動で数値に変換してくれない場合がありますので、注意が必要です。

【ハマリ度】 😫😫😫😫😫

おわりに

ということで、今回は少し趣向を変えて「JavaScriptのハマリどころ」のまとめをご紹介しました。

プログラムの勉強をはじめてすぐの場合は、こういったイレギュラーな内容のせいで一日中Google検索することになったり、コードは何も書いてないのに時間だけが過ぎていって相当焦ることもあると思いますが、そういった「しなくていい苦労」が今回の記事で少しでもなくなれば嬉しいです。

そして、もし少しでも時間をショートカットできたら、ぜひその分趣味や楽しいことに使ってください 😊👍

ではでは〜❗

「一日中ハマってたのに、
寝て起きたら5分で解決したりしますよね(不思議!)😂」

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly