Q1let couponCode; と書いて値を入れずに読み取ったとき、変数の値は何ですか?
null と undefined — ?. と ?? で安全に読む
値が入っていない状態を表す null と undefined の違いを整理し、入れ子の途中が無くても止まらずに読む書き方と、既定値を安全に用意する書き方まで扱います。
サーバーから受け取った注文データを画面に出すとき、住所が未入力だったり、支払い方法がまだ選ばれていなかったりします。JavaScript にはこの「値が入っていない」状態を表す値が undefined と null の 2 つあり、読み取りに失敗すると TypeError で処理が止まります。
この記事では 2 つの違いと、途中が無くても止めずに読む書き方、無いときに既定値を当てる書き方を扱います。
値が入っていない 2 つの値 — undefined と null
undefined(値がまだ入っていないことを JavaScript 側が自動で入れる値)は、書き手が代入しなくても勝手に現れます。配列の範囲外やオブジェクトの無いキーを読んだときに undefined が返るのはすでに見たとおりで、これに変数の宣言だけをした場合が加わります。
let couponCode;のように、宣言だけして値を入れていない変数order.customerのように、オブジェクトに登録されていないキー(既出)tags[5]のように、要素が 2 つしかない配列の範囲外の index(既出)
一方の null(空であることを示すために書き手が代入する値)は、自動では現れません。「配送先はまだ選ばれていない」「クーポンは使われていない」のように、値が無いこと自体をデータとして持たせたいときに、自分で null を代入します。
API から返ってくる JSON でも、項目が空であることを示すために null が入っていることがあります。
2 つは別の値なので、=== で比べると false です。ただし == は両者を同じものとして扱うため true を返します。
判定を書くときは、どちらの値が来るのかを意識して === null のように片方だけを見るか、次の章以降で扱う ?. と ?? にまとめて任せます。
let couponCode; // 宣言だけして値を入れていない
console.log(couponCode); // undefined
const order = { orderId: "ORD-1105", total: 12800 };
console.log(order.customer); // undefined(登録されていないキー)
const tags = ["限定", "予約受付"];
console.log(tags[5]); // undefined(範囲外の index)
// null は書き手が代入する
let shippingAddress = null; // 配送先はまだ選ばれていない
console.log(shippingAddress); // null
console.log(shippingAddress === undefined); // false(別の値)
console.log(shippingAddress == undefined); // true(== は同じものとして扱う)
入れ子の途中が無くても止めない — オプショナルチェーン ?.
API から返ってくるデータは、項目がいつも全部そろっているとは限りません。order.customer.address.city のように深いところを読むコードは、途中の address が無い注文が 1 件混ざっただけで止まります。
読み取り自体を失敗させずに、無いときは無いまま先へ進めたい場面です。
order.customer.address が undefined のとき、その先の .city を読もうとすると TypeError: Cannot read properties of undefined (reading 'city') が発生します。1 段階目の読み取りは通っても、2 段階目で止まります。
そこで使うのが オプショナルチェーン(左側が null か undefined なら、その先を読まずに undefined を返す ?. 演算子)です。address?.city と書くと、address に値があるときだけ .city を読み、無いときは読み取りをやめて undefined を返します。
キーを [] で渡すときは ?.[field] の形で書きます。
const order = {
orderId: "ORD-1105",
customer: { name: "中村 亮" },
};
// address が無いので、その先の city を読んだ時点で止まる
// console.log(order.customer.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')
// ?. を挟むと、読み取りをやめて undefined が返る
console.log(order.customer.address?.city); // undefined
// 値があるところはそのまま先へ進む
console.log(order.customer.name); // 中村 亮
// キーを変数で渡すときは ?.[] と書く
const field = "city";
console.log(order.customer.address?.[field]); // undefined
無くてもよい項目にだけ付ける
?. を全部の区切りに付けると、キー名の打ち間違いもエラーにならずに undefined として素通りします。必ず入っているはずの項目には付けず、入っていない可能性がある項目の手前だけ に付けると、直したいエラーは止めたまま読めます。
既定値を用意する — ?? と || の違い
項目が無いときは undefined のまま画面に出すのではなく、「ゲスト」「20 件」のような既定値を出したい場面がほとんどです。
ここで || を使うと、値が入っているのに既定値へ置き換わってしまうことがあります。在庫数の 0 や、備考の空文字列がその例です。
null と undefined の 2 つをまとめて nullish と呼びます。Nullish 合体演算子(左側が nullish のときだけ右側を返す ?? 演算子)を使うと、0 や空文字列や false はそのまま残ります。
|| は左側が falsy なら右側を返すので、値が無い場合と 0 の場合を区別できません。既定値を当てる目的なら ?? を選ぶと、意図しない置き換えを防げます。
0 のときだけ結果が分かれます。?? は null と undefined だけを置き換える ので、0 は値として残ります。| 左側の値 | || が返すもの | ?? が返すもの |
|---|---|---|
| null | 既定値 | 既定値 |
| undefined | 既定値 | 既定値 |
| 0 | 既定値 | 0 のまま |
| 空文字列 | 既定値 | 空文字列のまま |
| false | 既定値 | false のまま |
const settings = { displayName: null, itemsPerPage: 0 };
// ?? は左側が null か undefined のときだけ右側を返す
console.log(settings.displayName ?? "ゲスト"); // ゲスト
console.log(settings.itemsPerPage ?? 20); // 0(そのまま残る)
// || は左側が falsy なら右側を返すので、0 も置き換わる
console.log(settings.itemsPerPage || 20); // 20
// 登録されていないキーは、どちらでも既定値になる
console.log(settings.language ?? "ja"); // ja
console.log(settings.language || "ja"); // ja
なお displayName ?? "ゲスト" || "匿名" のように ?? と || を括弧なしで並べると、どちらを先に判定するかが決まらないため文法エラーです。両方を使いたいときは (displayName ?? "ゲスト") || "匿名" のように括弧で区切ります。
0 や空文字列を既定値で潰さない
在庫数 0・割引率 0・備考の空文字列は、どれも「値が入っている」状態です。|| で既定値を当てると、これらが falsy なので既定値に置き換わり、在庫切れの商品が在庫ありとして表示されるような食い違いが起きます。値の有無だけを見たいときは ?? を使います。
登録されていない処理を呼ばない — ?.() の呼び出し
注文処理では、「成功したときの処理」「失敗したときの処理」をオブジェクトの中に入れておき、必要になった時点で呼び出すことがあります。中身が用意されていない項目まで呼び出すと TypeError: handlers.onError is not a function で止まるため、あるときだけ呼ぶ書き方が必要です。
第 3 回で扱った関数は、プロパティの値としてオブジェクトの中に入れておくこともできます。次の例の () => ... がその場で関数を作る記法です。この書き方そのものは文法カテゴリで扱うので、ここでは「プロパティに関数が入っているかどうかで結果が変わる」ところだけを見てください。
関数が入っているプロパティを handlers.onError?.() と書いて呼ぶと、onError が null か undefined のときは呼び出しを飛ばして undefined を返します。
null なら呼ばずに undefined を返します。?. を付けずに呼ぶと TypeError で止まります。const handlers = {
onSuccess: () => console.log("注文を確定しました"),
onError: null,
};
// 関数が入っているので呼び出される
handlers.onSuccess?.(); // 注文を確定しました
// null なので呼び出しを飛ばし、undefined が返る
console.log(handlers.onError?.()); // undefined
// ?. を付けずに呼ぶと止まる
// handlers.onError();
// TypeError: handlers.onError is not a function
理解度チェック
まずは1問ずつ答えてみましょう。
Q2?. の左側が null か undefined だったとき、その先はどうなりますか?
Q3const discountRate = 0; のとき、discountRate || 5 と discountRate ?? 5 はそれぞれ何を返しますか?