JavaScript基礎 #2 変数と型

#1 はじめてとセットアップ で環境を整えたので、ここから言語そのものに入っていきます。今回の記事は — 変数をどう宣言するか、JavaScriptがどんな型を持っていて、その間を行き来するときに何に注意すべきかについてです。

変数の宣言 — letconst #

JavaScriptで変数を作るときに使うキーワードは、letconst の2つです。

let と const
let count = 0;        // 後で変えられる値
count = count + 1;    // OK

const name = 'カーティス'; // 一度決めたら変えられない
name = '別の名前';    // ✗ TypeError

ルールはシンプルです — 基本は const、再代入が必要なときだけ let。最初は混乱するかもしれませんが、慣れてくるとコードの意図がより明確になります。「この値は変わらない」が一目で分かるようになるのです。

var はどこに行ったのですか? #

昔のJavaScriptには var というキーワードがありました。今でも動作しますが、新しいコードではほとんど使いませんvar には次のような落とし穴があります。

  • 関数単位のスコープ — ブロック(iffor)を無視する
  • ホイスティング時に undefined で初期化 — 宣言前にアクセスしてもエラーにならない
  • 同じ名前で重複宣言が可能 — 事故のリスク

let/const は、これらすべてをきれいに整理した後継です。このシリーズでは var をほとんど使いません。

8つの基本型 #

JavaScriptのすべての値は、次の8つの型のいずれかです。

分類
string'hello', "world", `tpl`プリミティブ
number42, 3.14, -1プリミティブ
booleantrue, falseプリミティブ
nullnull (意図的に空)プリミティブ
undefinedundefined (まだ値がない)プリミティブ
bigint9007199254740993nプリミティブ
symbolSymbol('id')プリミティブ
object{}, [], 関数, クラスインスタンス参照

最初の7つは プリミティブ型(primitive)、最後の object だけが 参照型(reference) です。この分類が、JavaScriptのすべての動作を説明する出発点になります。

型の確認 — typeof #

typeof の使用
typeof 'hello';       // 'string'
typeof 42;             // 'number'
typeof true;           // 'boolean'
typeof undefined;      // 'undefined'
typeof null;           // 'object'  ← JavaScript の有名なバグ
typeof {};             // 'object'
typeof [];             // 'object'  ← 配列もオブジェクト
typeof function(){};   // 'function' ← 実際は object だが特別扱い

typeof null === 'object' は、言語初期のバグがそのまま固まってしまったものです。互換性のために修正できずにいます。null を検査するときは、value === null で直接比較するほうが安全です。

nullundefined の違い #

どちらも「値がない」を表現しますが、意図が少し異なります。

  • undefined — 値がまだ決まっていない。JavaScriptが自動で埋めてくれる。
  • null — 値が意図的に空。開発者が明示的に書く。
undefined が自動で入るケース
let x;                         // 宣言のみで値なし → undefined
console.log(x);                 // undefined

const obj = { name: 'カーティス' };
console.log(obj.age);           // undefined (存在しないプロパティ)

function f(arg) {
  return arg;
}
f();                            // undefined (引数なし)
null は明示的に置く
let user = null;                // まだログインしていない
// ... 後で
user = { id: 'u1', name: 'カーティス' };

実務では、関数の戻り値やAPIレスポンスで両方に出会います。「あるかもしれないし、ないかもしれない値」 という意味では、両者を同じだと考えても差し支えありません。== で比較すれば両方を捕まえられます(次節)。

プリミティブ vs 参照 — JavaScriptの最も重要な違い #

ここが、入門者が一番よくつまずくところです。

プリミティブ型は、値が丸ごとコピーされます。

プリミティブ — 値コピー
let a = 10;
let b = a;       // a の値(10) を b にコピー
b = 20;
console.log(a);  // 10 (影響なし)
console.log(b);  // 20

参照型は、「どこを指しているか」がコピーされます。

オブジェクト — 参照コピー
const obj1 = { count: 10 };
const obj2 = obj1;          // 同じオブジェクトを指すようになる
obj2.count = 20;
console.log(obj1.count);    // 20 ← obj1 も一緒に変わる
console.log(obj2.count);    // 20

obj1obj2同じオブジェクトを指す2つの名前 です。片方からオブジェクトを変えると、もう片方から見ても変わって見えます。この違いが、JavaScriptで仕事をしていて出会う数多くの「なぜこれも一緒に変わるんだ?」問題の根本原因です。

オブジェクトを本当にコピーしたいとき #

シャローコピー — spread 演算子
const original = { name: 'カーティス', age: 30 };
const copy = { ...original };

copy.age = 31;
console.log(original.age);  // 30 (影響なし)
console.log(copy.age);      // 31

{...original} は、オブジェクトの1段階のプロパティだけをコピーする シャローコピー(shallow copy) です。ネストしたオブジェクトがあれば、それは依然として参照が共有されます。ディープコピーは次のシリーズで扱います。

型変換 — 落とし穴の多いところ #

JavaScriptは型を自動的に変換しようとする傾向があるため、意図とは違う結果が出やすいです。

意図的な変換 #

明示的変換
Number('42');      // 42
Number('hello');   // NaN (Not a Number)
String(42);        // '42'
Boolean(0);        // false
Boolean(1);        // true
Boolean('');       // false
Boolean('false');  // true ← 'false' という文字列は空ではない

Boolean 変換のときに使われる falsy値 の7つを覚えておくと便利です。

falsy 値 — すべて false に変換
Boolean(false);       // false
Boolean(0);           // false
Boolean(-0);          // false
Boolean(0n);          // false (bigint 0)
Boolean('');          // false
Boolean(null);        // false
Boolean(undefined);   // false
Boolean(NaN);         // false

この7つ以外はすべて truthy です。空のオブジェクト {} と空の配列 [] も truthy だというのが、混乱しやすいところです。

意外と truthy な値
Boolean({});         // true
Boolean([]);         // true
Boolean('0');        // true ← 文字列 '0' は空ではない
Boolean('false');    // true

自動変換の落とし穴 #

演算子が自動的に型を変換する場面があります。

自動変換の意外
'5' + 3;       // '53'  ← 文字列連結として動作
'5' - 3;       // 2     ← 数値演算
'5' * '2';     // 10    ← 両方とも数値に変換
[] + [];        // ''
[] + {};        // '[object Object]'

+ が一番ややこしいです。片方が文字列なら 連結、それ以外は 加算 になります。混乱したら Number()String() で明示的に変換しておくほうが安全です。

===== — 常に === を使ってください #

比較演算子が2種類あります。

緩い比較 vs 厳密な比較
// 緩い比較 — 自動変換後に比較
'5' == 5;          // true ← 文字列を数値に変換して比較
0 == false;        // true
null == undefined; // true

// 厳密な比較 — 型まで同じである必要がある
'5' === 5;         // false
0 === false;       // false
null === undefined;// false

ほぼ常に === を使ってください。自動変換は直感に反するケースが多く、事故のもとになります。nullundefined を一度に検査したいときだけ value == null パターンを使うくらいです(これは null || undefined と同等)。

letconst のスコープ #

最後に、let/const が作る ブロックスコープ の意味について。

ブロックスコープ
if (true) {
  const message = 'こんにちは';
  console.log(message);  // こんにちは
}
console.log(message);    // ✗ ReferenceError — ブロックの外からは見えない

{} の中で宣言された変数は、そのブロックの外からは見えません。forループ、ifの分岐、関数本体のすべてで同じです。これにより、変数の影響範囲が狭くなり、追跡が容易になります。

まとめ #

今回の記事で整理した内容:

  • 変数宣言は基本 const、再代入が必要なときだけ letvar は新しいコードでは使わない
  • 8つの型 — string/number/boolean/null/undefined/bigint/symbol/object
  • typeof null === 'object' の落とし穴 — value === null で直接比較
  • プリミティブは値コピー、オブジェクトは参照コピー。{...obj} でシャローコピー
  • falsyの7つ: false, 0, -0, 0n, ‘’, null, undefined, NaN
  • == ではなく常に === を使う
  • let/const はブロックスコープ

次の記事(#3 制御フロー)では、if/while/for/switchのような分岐と繰り返し、そしてモダンな構文(for...offor...in、switchの fallthrough の落とし穴)を扱います。

X