JavaScript で undefined の使用が推奨されない理由
はじめに
この記事は #EveOneZenn (Everyday One Zenn) vol.05 です。
JavaScript で undefined
を値として使用することが推奨されない理由についてまとめます。
(過去に別所で公開していた記事の加筆版です)
前回:
TypeScript Deep Dive の見解
TypeScript Deep Dive という TypeScript のバイブル的リファレンスに次のようなページがあります。
このページでは undefined を値として使用するのを推奨していません。
たとえば、値が undefined
かどうかを調べるには null
との比較を推奨していたり、
// 👎 Not good...
obj.prop === undefined;
// 👍 Better
obj.prop == null;
ルートレベルの変数が定義されているかを判断するには typeof
の使用を推奨していたりします。
// 👎 Not good...
globalObj === undefined;
// 👍 Better
typeof globalObj === 'undefined';
ただ、TypeSCript Deep Dive には undefined
を使用するべきではない具体的な理由については言及されていません。
ESLintルールの見解
同様の主張をしている ESLint の no-undefined
ルールのドキュメントで具体的な理由が言及されていました。
説明の一部を抜粋します。
undefined
は代入可能なグローバルオブジェクトのプロパティです。 ECMAScript 3 ではundefined
を上書きすることができます。 ECMAScript 5 からグローバルのundefined
を上書きできなくなりましたが(代入はできる)、スコープ変数としてundefined
をシャドーウィングすることができます。
つまり、 undefined
自体は代入可能な変数のため、実装に寄っては undefined
に別の値が代入されることがあり得るということです。
次のコードは ES3 時代にグローバル変数として存在する undefined
に異なる値を代入する例です。
ES5 以降ではグローバル変数の undefined
への代入でエラーは発生しませんが、代入が反映されません。
// ES3
window.undefined = 1;
// undefined === 1
const obj = {};
obj.prop === undefined; // false
// ES5
window.undefined = 1;
// undefined === undefined
また、次のコードは現在でも再現できる、スコープ変数として undefined
を定義する例です。
const foo = () => {
const undefined = 1;
// undefined === 1
const obj = {};
obj.prop === undefined; // false
};
no-undefined
ルールでは undefined
の使用に関するガイドラインを次のように提示しています。
-
undefined
は初期化されていない変数にのみ使用する - 値が
undefined
かどうかの判定はtypeof
演算子を使用する
なお、 no-undefined
ルールは recommended
・eslint-config-airbnb
・eslint-config-standard
・eslint-config-google
のいずれのルールセットでも有効化されていません。
undefined
を保護する
代入やシャドーウィングから undefined
を保護するには、ESLint の no-global-assign
ルールと no-shadow-restricted-names
を使用します。
おわりに
余談として、 null
はプロパティ以外では代入も宣言もできない値なので、 undefined
のような問題は発生しません。
const null = 1;
// SyntaxError: Unexpected token 'null'
const foo = () => {
const null = 1;
// SyntaxError: Unexpected token 'null'
};
window.null = 1;
// null === null
// window.null === 1
Discussion
undefinedの使用を推奨しないのは値が書き換得られる可能性があるから、ということではないと思います。
nullは明示的に空を表す値なので、何かを空に設定するならnullを使うべきで
undefinedは、値がない、ということを示すので、設定するときに自分のコードで代入する
ということは、あまりないようにするべきということになります。
DeepDriveのコードですが下記の通りです。
b に undefined を入れるなら、bの値が存在しないように記載しようということです。
ライブラリを作っていると undefined を使わないと行けない場面は多々あるのですが
ライブラリ使用者がundefinedを代入して使うということは少なくなるように作るべきと思います。
また、TypeScriptチームはnullを使わないというのは賢明な判断だと思いますが、
TypeScript Deep Drive で書かれている、==nullで判断するというのは実に危ないコードになります。
False属性は、falseと判定するのがたくさんの値になってしまっているので、コードがよみにくく不具合の温床になってしまうのですが、同様に、 null と undefined の同一化してコードを書くと結局は不具合の温床になります。
1と1.001 がプログラム上、別の値と同じように、null と undefined も似てはいますが別の値なので、1文字のミスも許されないプログラミングという行為において、別の値を同一視してコードを書くのは、後々やばい問題を引き起こすことが多いです。
TypeScript Deep Drive が null と undefined を同一視を推奨しているのが、とても謎ですが、この1点で、私は上級者が参考にするような情報ではないと感じてます。
例えば、下記のコード。
a が null の場合と、undefined の場合、testFuncとconsole.logが呼び出されますが、どちらでもHelloと出力されるわけではないからです。
JS と TS では、常に「==」は悪で「===」が善なので、TypeScript Deep Drive で示している思想は不具合を起こしかねない書き方になります。
コメントありがとうございます!
undefined
は言葉の通り「定義されていない」を表すものなので、値として使用するのは避けるべき、というのはおっしゃるとおりかと思います。また、
== null
は JavaScript の複雑な Falsy を利用しているため、わかりづらいというのも理解します。ただ、
== null
については ESLint のeqeqeq
ルールにも例外的に== null
を除外するオプションがあるように、== null
で Nullable を判定するのはメジャーな方法なのかなと思っています。もちろん、厳密に
undefined
かを判定するにはtypeof foo === 'undefined'
を使うべきだとは思いますが、一般的なアプリケーションでは== null
を使用して問題が発生するケースは少ないのではないか、というのが私の意見です。ESLintは様々なニーズに対応できるように「==」の中で「==null」だけ許容するというオプションがあるのは納得できます。「==null」は「null」と「undefined」だけを見分けるのはそのとおりなのですが、上記で記載したようにnullとundefinedを区別しておかないとプログラムの挙動が変わるものがあるので、区別せざるおえないです。変数にnullかundefined、どっちかが入っているかわからない、なら、どっちに処理がいくのかわからない、なら、結局動くのか動かないのかわからない。となり、書いている時は平気なのですが、後々技術的負債につながっていきます。isNullとisUndefinedとisNullOrUndefined(==null)は明確に区別したほうがおすすめです。
区分けしないコードの中でnullかundefinedかで不具合を起こす危険性を排除できないのでコード組むのがかなり難しくなるので、初心者や中級者は安易に使ってしまうのですが上級者がそれだと品質の高いコードに遠のいてしまう、という感じです。
ありがとうございます。
「上級者であれば厳密に判定したほうがコードの質が向上する」というご意見には同意します。
null
とundefined
は値として異なるものとして存在するし、実装上でも異なるものとして扱うべきというのもごもっともです。