JavaScriptのデータ型 「null」「undefined」「シンボル」

jsコード JavaScript

本シリーズでは、プログラミング言語を問わず重要、且つ基礎的な内容である「データ型」について、連載形式で「これでもか」というぐらいに詳しく説明していきます。
説明するデータ型は、プリミティヴとして用意されている7種類を対象としています。

今回はその第四弾(最終回)となり、「null」、「undefined」、「シンボル」を解説します。

null

nullという値は、存在しないまたは無効なオブジェクトや、アドレスへのポインター参照を表します。

引用元:MDN

後述の「undefined」と大変似ていますが、undefinedは「値が代入されていないため、値がない」のに対し、nullは「代入すべき値が存在しないため、値がない」という違いがあります。

変数の値がnullになってしまうサンプルをいくつか提示します。
最後の例は、「DOM操作」についての学習が未だの方は分からなくても大丈夫です。

script.js
let val1 = null;  // 明示的にnullを代入

function getNull() {
  return null;  // 関数から明示的にnullを返却
}

document.querySelector("[存在しないセレクタ]").textContent = "お試し";

変数に明示的にnullを代入することによりundefinedと区別することができ、「単に初期化し忘れているだけ」なのか「意図的にnull」を持たせているのかの判断材料にもなります。

ここで、nullについては、一点補足しておく必要があります。

JavaScriptでは、nullはプリミティヴの一つであるとされています。しかし、場合によってnullは「プリミティヴ」ではありません。「typeof」を使用して型を検査した場合、返ってくるのは「object」です。

実際にプログラムを作成し、確認してみましょう。

script.js
console.log( typeof null );
実行結果
> object

これはバグなのですが、既存のプログラムにあまりに多大な影響を与えてしまうため、修正できないとされています。

歴史的背景

この背景にはJavaScript創生当時からの歴史が関係しています。

JavaScriptの初期の実装では、JavaScript の値は「型タグ」と呼ばれるインデックスのようなものと、値とのセットで表現されていました。

objectの「型タグ」は0で、nullの「型タグ」も0を持つよう実装されたため、nullをtypeofで検査すると「object」を返してしまいます。

undefined

「undefined」とは、宣言のみ行われた(値が代入されていない)変数や、実引数が与えられていない仮引数に対して、自動的に割り当てられるプリミティブ値のことです。

それぞれを実例で見てみましょう。

宣言のみ行われ、値が代入されていない変数

script.js
let something;    // 👈宣言のみ

console.log( something );  // 👈コンソールに表示してみると…
実行結果
> undefined

undefined とは、日本語に訳すと「未定義」の意味になります。

実引数が与えられていない仮引数

引数ひきすう」については、まだ学習前なので理解できなくても問題ありません。

script.js
function showName(name) {  // 👈この「name」が仮引数
  console.log( name );
}

showName();  // 実際の情報「実引数」を与えていない
実行結果
> undefined

シンボル

先にお断りしておきますが、シンボルは非常に難しく、分かりづらい概念です。

大文字から始まるSymbolは、JavaScriptに組み込まれているオブジェクトで、そのコンストラクタを呼び出すことでプリミティヴなsymbol値が返されます。
返されたプリミティヴ値は、一意であることが保証されており、同一文字列を引数に渡して得られたもの同士であっても、比較した結果は「false」になります。

これはMDNにも記述されている内容を踏襲していますが、コンストラクタと言っておきながら、「new」キーワード付きで呼び出すと、以下のようなエラーを出します。

Symbol is not a constructor

日本語に訳すと、「シンボル(Symbol)はコンストラクタではありません」という意味になります。

コンストラクタなのかコンストラクタじゃないのかハッキリしてほしいですが、このように大変分かりづらいものです。

ましてや、当ブログのメニューを最初から順に進められている方や、他プログラミング言語を学習されたことがない方にとっては、そもそも「関数」や「コンストラクタ」、「new」の意味も知らないということになります。
よって、そういった方は「シンボル」の項は飛ばして頂いても良いかと思います。

Symbol() の正体は関数で、その関数がシングルトン(単一で重複しない)のsymbol値を返している。

👆このようなイメージなのではないかと推察します。

そう考える根拠は以下の通りです。

  • Symbol() がコンストラクタなのであれば、返されるのはインスタンスであって、プリミティヴ値ではないはず。
  • 「new」キーワードを伴う呼び出しでエラーになる。
  • さらにそのエラーメッセージで「コンストラクタではない」と明言されている。

Symbolの使い方

分かりづらいものは、実践で使ってみるのが理解の近道、ということでサンプルプログラムを作ってみます。

script.js
let sbl = Symbol("something");

console.log( sbl );
実行結果
> Symbol(something)

続いて、同一文字列をSymbolの引数に渡して得られたもの同士であっても、比較した結果が「false」になることを確認するプログラムです。

script.js
let sbl1 = Symbol("something");
let sbl2 = Symbol("something");

console.log( sbl1 === sbl2 );
実行結果
> false

想定通りの結果となりました。

しかし、この一意性(ユニーク、単一性)には例外があります。
それは、Symbol() 関数によって返されたプリミティヴ値を、他の変数に代入したものと比較した場合です。

以下にサンプルを示します。

script.js
let sbl1 = Symbol("something");
let sbl2 = sbl1;

console.log( sbl1 === sbl2 );
実行結果
> true

シンボルの使いどころ

シンボルは、他のコードがオブジェクトに追加する可能性のあるキーと衝突しないように、また、他のコードがオブジェクトにアクセスするために通常使用するメカニズムから隠されるように、一意のプロパティキーをオブジェクトに追加するためによく使用されます。これによって弱いカプセル化(または弱い形の情報隠蔽)が実現できます。

出典:MDN(Symbol)

この文章だけで、「そうか、わかったぞ!」とは中々なりにくいかと思うので、実例を挙げていきたいと思います。

使いどころの例

上記MDNからの引用文には、よく使用される例として、次のような記述があります。

  1. 一意のプロパティキーをオブジェクトに追加する
  2. 弱いカプセル化(情報隠蔽)を実現する

一意のプロパティキーをオブジェクトに追加する

まずは 1. の例として、オリジナルのtoStringメソッドを作成してみます。
挙動を少しでも分かりやすくするため、極力シンプルにしています。

script.js
const originalUpperCase = Symbol();  // 👈シンボル値「originalUpperCase」
String.prototype[originalUpperCase] = function() {
  return "YAMADA";
}

console.log( "taro".toUpperCase() );        // 👈TAROが表示されるはず
console.log( "taro"[originalUpperCase]() ); // 👈YAMADAが表示されるはず
実行結果
> TARO
> YAMADA

こうすることで、「originalUpperCase」という値は一意であることが保証されるので、上書きされる心配はなくなります。
仮にこれが、プロトタイプのプロパティとして追加されていたらどうなるかを例示します。

script.js
String.prototype.originalUpperCase = function() {
  return "YAMADA";
}
String.prototype.originalUpperCase = function() {
  return "SATO";
}

// 👇上のメソッドは上書きされるため「SATO」が表示される
console.log( "taro".originalUpperCase() );

弱いカプセル化(情報隠蔽)を実現する

次に 2. の例として、オリジナルのコンストラクタ関数を作成してみます。
挙動を少しでも分かりやすくするため、極力シンプルにしていますが、それでもプログラムが少し長くなってしまいました。

script.js
function SymbolSample() {
  // 👇外部からのアクセス不可のプロパティを作成
  const firstName = Symbol();
  const familyName = Symbol();

  this[firstName] = "Mace";
  this[familyName] = "Windu";

  // 👇外部からアクセス可能なプロパティを作成
  this.firstName = "Han";
  this.familyName = "Solo"

  this.showFullName = function() {
      console.log( `${this[firstName]} ${this[familyName]}` );
  };
}

const f = new SymbolSample();
f.showFullName();
console.log( f.firstName );  // アクセス可能
console.log( f[firstName] ); // アクセス不可
実行結果
> Mace Windu  👈showFullNameメソッドを介してアクセス
> Han         👈アクセス可能なプロパティ
> Uncaught ReferenceError: firstName is not defined  👈シンボルのfirstName

最後に

nullはともかくとして、undefinedやシンボルは、ある程度深く理解するためには、比較演算や制御構文、そしてオブジェクト指向の前提知識が必要となるため、初学者の方は「よく分からなかった」と感じられたかもしれません。

現時点で記述のすべてを理解できる必要はありませんので、オブジェクト指向を学んだタイミングで再読頂くと、得るものがあると思います。

次に学習するのは『JavaScriptの配列』です。


本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。

最後までお読みいただき、ありがとうございました。

『詳説 JavaScript』メニューに戻る

コメント

タイトルとURLをコピーしました