JavaScriptのコーディングに慣れてくると、意外と癖が多く、落とし穴だらけの言語であるということが分かってきます。今回は各データ型の取扱いの理解を深め、種々の問題を避ける方法を身につけていきます。

💬 なぜこんなに落とし穴が多いのか?

もともとJavaScriptはWebを拡張するための言語として生まれました。ブラウザで動くことを想定した言語なので、破壊的変更をすると、古いブラウザを使っている人にも影響が出てしまいます。そのため、後方互換性を保ちながら新機能を追加していった結果、今では非常に歪な言語として進化し続けています。

プリミティブ型とオブジェクト型

JavaScriptで扱えるデータ型にはプリミティブ型とオブジェクト型の2種類があります。プリミティブ型は真偽値や数値、文字列といった複雑でない基本型のことです。対するオブジェクト型は配列やオブジェクトといった、内部的にデータを持つ複合データ型のことを指します。

プリミティブ型

プリミティブ型について説明はあまり必要ないでしょう。それぞれの型の説明を確認して理解を深めるようにしてください。

ただし、いくつか注意点があります。まず、プリミティブ型は値を保持している変数は変更できても、それ自体を変更することはできません。

let a = 'Hello, world!'
a[0] = 'X'        // 文字列自体は変更できない
a = 'Hi, there!'  // 変数が持つ値は変更できる

数値の場合はもっとわかりやすく、12 といった値自体を別の値に変更はできません。もしできたとしたら、数値計算の法則が成り立たなくなってしまいますよね?

また、プリミティブ型に対してnewキーワードを使うと、プリミティブ型と同じ動作をするオブジェクトを生成します。これは意図しない挙動を引き起こすことがあり、通常推奨されない方法なので、避けるべきでしょう。

const a = 'Hello, world!'
const b = new String('Hello, world!')
console.log(typeof a)  // "string"
console.log(typeof b)  // "object"

オブジェクト型

プリミティブ型でないものはすべてオブジェクト型(Object)です。標準でArrayやSetといったオブジェクト型がありますが、これらの値は typeof x == 'object' を満たします。

また、JavaScriptの大きな特徴として、ユーザー定義のオブジェクトを簡単に作成でき、それがこれまでたびたび登場した {} リテラルです。このリテラルで生成される空のオブジェクトは、ユーザーが自由にプロパティやメソッドを追加して自由に拡張できる便利な複合型です。

しかし、近年クラス構文が導入されて、クラスベースのオブジェクト指向機能が強化されたこともあり、ユーザー定義オブジェクトの利用は、DTO(Data Transfer Object)や辞書型の代わりとしての利用に留めておいたほうが無難でしょう。

⚠️ 注意

{} と書いたときに、制御ブロックを表す括弧と解釈されるか、オブジェクトリテラルと解釈されるかは文脈によって異なります。

⚠️ オブジェクト型(Object)は辞書型(Map)の代わりではありません

Object型のプロパティ(キー)は文字列であることを期待しています。一方でMap型のキーは文字列以外のキーも許容します(===演算子で同値性が判定されます)。

型を判定する

typeof 演算子

typeof 演算子を使うとおおまかにその値がどんな型か取得できます。プリミティブ型はその型名が、オブジェクト型は "object" が得られます。

typeof undefined  // "undefined"
typeof true       // "boolean"
typeof 42         // "number"
typeof 'abc'      // "string"
typeof []         // "object"
typeof {}         // "object"
typeof null       // "object"

nullもプリミティブ型とはされていますが、歴史的経緯によって "object" が返却されるようになっています。これは種々の問題を引き起こすので、オブジェクト型の判定が必要なときにはnullを含むかどうかの考慮が必要です。

instanceof 演算子

もうひとつ型を判定できるものがあり、それが instanceof です。これは typeof と少し挙動が異なり、指定されたクラス自体か、サブクラスのインスタンスであることを判定します。

42 instanceof Number  // true
 
class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}
 
class UserV2 extends User {
    constructor(name, age) {
        super(name, age)
    }
}
 
new User('Alice', 14) instanceof User   // true
new UserV2('Alice', 14) instanceof User // true

クラスに対しては、constructorプロパティを参照して、より直接的に型を判定する方法もあります。

const x = new User('Alice', 14)
x.constructor == User  // true

しかし、どんな値に対しても constructor プロパティが存在するわけではないので、このあとに説明するオプショナルチェーン(?.)を使ったプロパティアクセスのほうが取り回しがいいでしょう。

未定義値

未定義値にはundefinedとnullのふたつがあります。undefinedはそもそも値が存在しない「該当なし」の意味合いが強いことに対して、nullは値はあるものの、その値が「未設定」であることを示しています。

undefinedとnullの判定

undefinedとnullを判定するには == 演算子か === 演算子が使えますが、どちらを使うかによって結果が変わります。

undefined == undefined   // true
undefined === undefined  // true 
null == null             // true
null === null            // true
undefined == null        // true
null == undefined        // true
 
undefined === null       // false
null === undefined       // false

この性質から、次のようなnullを含むオブジェクトにアクセスした際に、== で比較することは計算ロジックが期待しないものになる可能性があります。

const data = { a: 20, b: 30, c: null }
if (data.c == undefined) {
    // data.c は null が存在している有効なエントリのはず!
}

Null合体演算子 

undefinedとnullに対してはNull合体演算子 (??)が利用でき、値が未定義値だったときの値を簡単に与えることができます。同じことはif文や三項演算子を使っても実現できますが、より端的に表現できます。

const value = null
const x = value != null ? value : 42  // 42
const y = value ?? 42                 // 42

オプショナルチェーン 

また、オプショナルチェーン (?.) は未定義値になりうる値に対してプロパティアクセスやメソッド呼び出しを可能とするものです。

const data = { a: { name: 'Alice' }, b: null }
console.log(data.a?.name)  // Alice
console.log(data.b?.name)  // null

変数 data はユーザーデータを含みますが、データがないこともあります。ユーザーの名前を取得するときに、いちいちユーザーの存在を確認した上で名前を取得するようなコードを書いても構いませんが、オプショナルチェーンを使うことで、途中が未定義であるかどうかに限らず取得できるようになります。

また、オプショナルチェーンは複数回連鎖することもできます。

// 最初のユーザーの年齢の文字列表現を取得する
userList[0]?.age?.toString()

真偽値

真偽値はBoolean型として定義されていて、trueとfalseのどちらかの値を持ちます。そのまま使うこともできますし、===== 演算子の結果としても得られるほか、必要な文脈ではBoolean型への暗黙の型変換も行われます。主に制御構造の条件として利用されます。

真偽値への変換

あらゆる値は真偽値に変換できます。undefined, null, 0, "" といった未定義値、ゼロ値、空文字列は偽と判定され、それ以外は真です。また、オブジェクトは常に真です。

オブジェクトが空かどうかを判断する方法はオブジェクトに依存します。Arrayであれば x.length > 0 で判定できますし、オブジェクトであれば Object.keys(x).length > 0 となります。

また、真偽値が要求される文脈では、暗黙的に真偽値に変換されることがあります。if文やwhile文では実行条件に真偽値を要求するので、上記の規則に従って変換されます。

const value = ''
if (value) {
    // 文字列が空でなければ実行
}

特定の値の真偽値が明示的に欲しいときには、否定の否定を使った !!x というテクニックが使えることを覚えておくといいでしょう。

真偽値からの変換

真偽値を別の型として扱うことはありませんが、Number型に変換するか、Number型が期待される文脈で真偽値を利用すると、true1, false0 として解釈されます。

文字列に変換すると、その文字列表現 "true" または "false" が得られます。

論理和と論理積

JavaScriptの論理和と論理積は真偽値を返すのではなく、判定に使われた値を返します。少し複雑ですが、解釈は次のようになります。

  • a || ba ? a : b と解釈され、最初に現れる真の値を返します。
  • a && ba ? b : a と解釈され、最初に現れる偽の値を返します。

厳密には異なりますが、この特性を使うと、オプション値のデフォルト値を簡単に記述できます。最近のJavaScriptでは特に理由がない限りはNull合体演算子のほうが推奨されるのであまり出番はありません。

function generatePassword({ digits } = {}) {
    // digits未指定ではundefinedになるので8を設定する
    digits = digits || 8
 
    // ...処理...
}

強制型変換

JavaScriptには型はあるものの、計算によっては自動的に別の型に変換されることがあり、これを強制型変換と呼びます。

演算によっては予期せぬ結果になることもあるので、入力される値が不特定である場合には、明示的な形変換をした上で計算しましょう。

よくある状況として文字列を整数値に変換することを考えましょう。一般的にはparseInt()を使いますが、数値演算を利用したテクニカルな変換方法もあります。

+'42'     // 単項プラスによるNumberへの変換
'42' * 1  // 1を掛けることによるNumberへの変換
'42' | 0  // ビット論理和によるNumber(32bit整数値)への変換

⚠️ こうしたテクニカルな数値変換について

それぞれの変換方法はすべて微妙に異なる挙動を示します。例えばparseInt('')はNaNを返しますが、'' * 10 になります。

しかし、+ 二項演算子は違う挙動を示します。左辺か右辺のどちらかが文字列であれば、もう片方も文字列に変換されます。

'42' + 0   // "420"

細かい変換規則はプリミティブ変換のルールに準じます。

第2回 同値性