これまでの回、駆け足すぎて置いていかれた人もいるはずです。この回は物語ではなく辞書です。読む順番は自由、気になる項目だけ拾い読みしてもらって大丈夫です。
各項目、できる限り図解(テキストの矢印図)と日本語の疑似コードを添えています。実際のJSコードだけだと「構文自体が読めない」ところで詰まってしまうので、先に日本語で流れを掴んでから、実際のコードに戻ってくる、という順番で読めるようにしています。
構成はこうです。
- 最初に3つだけ:JSの全部はここから始まる根本 — 他の全項目が、この3つのどれかから派生しています
-
JS基本文法辞典 —
var/let/constから?./??まで - React基本文法辞典 — JSXからHooksの命名規則まで
各項目は「 これは何か → 図解 → 日本語の疑似コード → なぜこの形か(意図) → クセ・注意点 」の順で統一しています。
第0部:最初に3つだけ。JSの全部はここから始まる根本
技術的な話を始める前に、この3つだけ先に頭に入れてください。この先に出てくる文法のほとんどは、突き詰めるとこの3つのどれかの「応用」か「対策」です。
根本①:thisは「誰が呼んだか」で決まる
多くの言語では、メソッドの中のthis(やself)は「そのメソッドがどのクラスに属しているか」で機械的に決まります。JSは違います。 thisは関数がどこで定義されたかではなく、「その瞬間、どうやって呼ばれたか」で決まります。
【図解】同じ関数でも、呼び方でthisが変わる
obj1.whoAmI() ──呼び出し方: 「obj1の」──▶ this = obj1
obj2.whoAmI() ──呼び出し方: 「obj2の」──▶ this = obj2
bareFn() ──呼び出し方: 「裸で」 ──▶ this = undefined
【日本語の疑似コード】
whoAmIという関数を実行する
もし「何か.whoAmI()」のように、モノの後ろにくっつけて呼ばれたなら
→ thisには、そのくっつけた「モノ」が入る
もし whoAmI() のように、何もくっつけず裸で呼ばれたなら
→ thisには何も入らない(undefined)
実際のコードはこうです。
function whoAmI() {
console.log(this);
}
const obj1 = { name: "obj1", whoAmI };
const obj2 = { name: "obj2", whoAmI };
obj1.whoAmI(); // this は obj1
obj2.whoAmI(); // this は obj2
const bareFn = obj1.whoAmI;
bareFn(); // this は undefined(厳格モードの場合)
ルールを整理するとこうなります。
| 呼び方 | thisの中身 |
|---|---|
obj.method() |
obj(ドットの前にあるもの) |
裸のfn()
|
undefined(厳格モード) |
fn.call(obj) / fn.apply(obj)
|
明示的に指定したobj
|
new Fn() |
新しく作られたオブジェクト |
| アロー関数 |
自分のthisを持たない。定義された場所のthisをそのまま借りる |
クラスコンポーネントで.bind(this)が必要だったのも、アロー関数がなぜthisを持たないのかも、全部この表の話です。関数を 渡す (onClick={obj.method})という行為は、実質的に上の表の「裸で呼ばれる」状況を将来作ってしまう可能性がある、ということです。
根本②:JSのオブジェクトは「プロトタイプ」で動く
JavaやC++のようなクラスベースの言語では、オブジェクトは「あらかじめ決められた設計図(クラス)のコピー」として作られます。JSは元々 プロトタイプベース という別方式で設計されていて、これが未だに土台になっています。
考え方はこうです。 すべてのオブジェクトは、「自分に無いプロパティを探すとき、代わりに探しに行く相手(プロトタイプ)」を1つ持っている。
【図解】プロパティを探す旅
dog.speak() を呼ぶ
│
▼
dog自身に speak はある? ── 無い
│
▼(プロトタイプを見に行く)
animal に speak はある? ── ある! ここで実行される
│
▼(もし無かったら、さらに上へ)
Object.prototype
│
▼
null (ここで探索終了)
【日本語の疑似コード】
dog.speak() を実行しようとする
dog自身に speak はある?
ある → それを実行して終わり
無い → dogのプロトタイプ(animal)を見に行く
animalに speak はある?
ある → それを実行して終わり
無い → さらに上のプロトタイプへ…(nullに行き着くまで繰り返す)
const animal = {
speak() { console.log("なにか鳴いた"); }
};
const dog = Object.create(animal); // dogのプロトタイプはanimal
dog.speak(); // dog自身には speak が無いが、プロトタイプのanimalにあるので呼べる
ES2015で追加されたclass構文は、この仕組みを 分かりやすく書けるようにしただけの糖衣構文(syntax sugar) で、中身は今も同じプロトタイプチェーンです。
class Animal {
speak() { console.log("なにか鳴いた"); }
}
class Dog extends Animal {}
const dog2 = new Dog();
dog2.speak(); // 動いている仕組みは Object.create の例と同じ
クラスコンポーネント(class Counter extends React.Component)のextendsも、この「プロトタイプを繋ぐ」処理を読みやすく書いているだけです。
根本③:関数はただの値(第一級関数)
JSでは、関数は数値や文字列と同じただの値です。これを「第一級関数(First-class function)」と呼びます。
【図解】関数というただの値が、いろんな場所に出入りする
┌─→ 変数に代入できる const fn = double
│
関数 double ──┼─→ 引数として渡せる array.map(double)
│
├─→ 返り値として返せる function make(){ return double }
│
└─→ オブジェクトに入れられる { onClick: double }
【日本語の疑似コード】
[1, 2, 3] という配列のそれぞれの値に対して
double という関数を1個ずつ適用する
1番目の値 1 → doubleに渡す → 2 が返る
2番目の値 2 → doubleに渡す → 4 が返る
3番目の値 3 → doubleに渡す → 6 が返る
結果として [2, 4, 6] という新しい配列ができる
const double = function (x) { return x * 2; };
const nums = [1, 2, 3].map(double); // 関数を「値として」渡している
console.log(nums); // [2, 4, 6]
これが根本にあるので、次のようなことが全部「当たり前」になります。
-
useState(0)がsetCountという関数を返せる(関数を返り値にできるから) -
onClick={handleClick}のように、JSXの属性に 関数を値として渡せる (関数を引数のように渡せるから) -
todos.map(todo => h("li", ..., todo.text))のように、配列の中身を関数で変換できる
逆に言うと、 「関数を渡しているのか」「関数を呼んだ結果を渡しているのか」の区別がつかないと事故る 、というのがこの根本のクセです。React辞典の「イベントハンドラ」の項目で、具体的な事故例として扱います。
(参考)根本④:クロージャ ― 関数は生まれた場所を覚えている
第4回でみっちり扱った内容なので簡単にだけ触れます。JSの関数は、自分が 定義された場所の変数 を、実行が終わった後も覚えたまま持ち歩きます。
【図解】
createGreeter("Alice") を呼ぶ
→ nameを"Alice"として覚えた関数Aが生まれる
createGreeter("Bob") を呼ぶ
→ nameを"Bob"として覚えた、別の関数Bが生まれる
(関数Aのnameは"Alice"のまま。Bを作っても書き換わらない)
function createGreeter(name) {
return function () {
console.log(`Hello, ${name}`);
};
}
const greetAlice = createGreeter("Alice"); // nameを覚えたまま生まれる
第4回の「レンダーのたびに別人のクロージャが生まれる」話は、この性質そのものでした。詳しくは第4回を参照してください。
第1部:JS基本文法辞典
変数宣言(var / let / const)
これは何か:値に名前をつける3つの方法。
【図解】varは漏れる、letは漏れない
function example() {
if (true) {
var x = 1; ─┐
} │ varは関数全体に漏れ出す
console.log(x); ─┘ → 1 が見える
if (true) {
let y = 1; ─┐
} │ letはブロックの外から見えない
console.log(y); ─┘ → エラー(yはここに存在しない)
}
【日本語の疑似コード】
if文の中で var x を作る
if文を抜けた後でも、xはまだ関数の中で生きている(漏れる)
if文の中で let y を作る
if文を抜けた瞬間、yは消える(漏れない)
なぜこの形か:varしか無かった時代(1995年、JS誕生当初)、varは「関数単位でしかスコープが区切られない」「宣言前に使ってもエラーにならずundefinedになる(巻き上げ)」という2つの弱点を持っていました。2015年、これを塞ぐためにlet/constが追加されました。
クセ・注意点:constは「再代入できない」だけで、「中身が変わらない」わけではありません。const arr = []; arr.push(1);は合法です。実務では「再代入しないならconst、するならlet、varは基本使わない」が共通認識です。
演算子(==と===)
【図解】
1 == "1" を判定する
① まず型を揃える("1" を 数値の 1 に変換)
② 揃えてから比較する → 1 と 1 → 等しい → true
1 === "1" を判定する
① 型変換はしない
② 型ごと比較する → 数値 と 文字列 → 別物 → false
【日本語の疑似コード】
1 == "1" を判定する
まず片方の型をもう片方に合わせて変換する
変換した後で値を比較する → true
1 === "1" を判定する
型変換はしない。型も含めて比較する → false
これは何か:値が等しいかどうかを調べる2つの比較演算子。
なぜこの形か:==は比較前に暗黙で型変換を行います。「初心者でも書きやすいように」という初期のJSの思想の産物ですが、[] == falseがtrueになるなど、直感に反する結果を生みやすいという副作用がありました。===は型変換をせず、型まで含めて完全一致のときだけtrueになります。
クセ・注意点:null == undefinedはtrueですがnull == 0はfalseなど、==の変換ルールは丸暗記しないと予測できません。現在はESLintで==自体を警告するのが一般的です。
関数の書き方(function宣言 / function式 / アロー関数)
これは何か:JSには関数を作る書き方が3種類あります。
function add(a, b) { return a + b; } // function宣言
const add2 = function (a, b) { return a + b; }; // function式
const add3 = (a, b) => a + b; // アロー関数
【日本語の疑似コード】
add という名前の関数を作る
2つの数字 a と b を受け取る
a + b を答えとして返す
なぜこの形か:function宣言は、コードの読み込み時点で丸ごと使えるようになる(巻き上げの対象になる)という性質があります。function式は「関数を値として代入する」という、根本③(第一級関数)をそのまま体現した書き方です。アロー関数(2015年)は、根本①が引き起こす事故を避けるために、あえてthisを持たない関数として追加されました(詳しくは根本①)。
クセ・注意点:function宣言は定義より前で呼べますが、function式・アロー関数は宣言の位置より前から呼ぶとエラーになります。
アロー関数の省略ルール、1段ずつ積み上げる
アロー関数でつまづく人のほとんどは、「thisの話」ではなく 「省略しすぎてもとの形が分からなくなる」 ところで詰まっています。アロー関数は「省略できる場所は全部省略していい」という設計なので、同じ処理が何通りにも書けてしまいます。ここでは省略を1段ずつ、逆算できるように積み上げます。
出発点:省略ゼロのフル形
const double = (x) => { return x * 2; };
ルール1:引数がちょうど1個なら、()を省略できる
const double = x => { return x * 2; }; // (x) → x
引数が0個、または2個以上のときは()を省略できません。
const greet = () => { console.log("hi"); }; // 0個は () 必須
const add = (a, b) => { return a + b; }; // 2個以上も () 必須
ルール2:処理が「式1つ」だけなら、{}とreturnを省略できる(書いた式が自動でreturnされる)
const double = x => x * 2; // { return x * 2; } → x * 2 だけ
処理が2行以上あるときは{}とreturnを省略できません(自動returnされるのは「式1つだけ」のときに限られます)。
const double = x => {
const result = x * 2;
return result; // ← これを書き忘れるとundefinedが返る、初心者が踏みがちな地雷
};
ルール3:オブジェクトをそのまま返したいときは、逆に()で包む必要がある
const makePoint = (x, y) => { x, y };
console.log(makePoint(1, 2)); // undefined ← エラーにすらならず、静かに失敗する
const makePointFixed = (x, y) => ({ x, y });
console.log(makePointFixed(1, 2)); // { x: 1, y: 2 } ← ()で包むと正しく動く
1行目はエラーにすらなりません。{ x, y }は「オブジェクト」ではなく「xとyを評価するだけの、中身が空の処理ブロック」と解釈され、returnが無いので黙ってundefinedを返します。構文エラーで気づける失敗より、エラーなく静かにundefinedが返ってくる方がむしろ厄介です。()で包むと、JSに「これはオブジェクトリテラルです」と明示的に伝わり、正しく動きます。
【図解】省略のチェーン(→の先が省略した形)
(x) => { return x * 2; } ← フル形
│ 引数1個の () を省略
▼
x => { return x * 2; }
│ 1行の式なので {} と return を省略
▼
x => x * 2 ← 最も省略された形
見た目が違う4つの書き方が、実は全部同じ処理だった、というのがアロー関数のつまづきの正体です。逆に、省略された形を見たら「フル形に戻す」練習をすると、途端に読めるようになります。
コールバック関数とは
アロー関数と並んで初心者がつまづきやすいのがコールバックです。実はアロー関数そのものより、「関数を人に渡して、後で実行してもらう」という考え方自体に慣れていないことが原因のケースが多いので、ここで独立して丁寧に扱います。
これは何か:「今すぐ自分で実行する」のではなく、「後で、別の処理に実行してもらうために渡しておく」関数のことです。
【図解】コールバックの基本構造
あなた: 「この関数、しかるべきタイミングで実行しといて」
│
▼
受け取った側: 関数を保管しておく(まだ実行しない)
│
▼ しかるべきタイミングが来たら
受け取った側: 「今だ」→ 預かっていた関数を実行する
なぜこの形か:根本③(関数は値)があるからこそ、「関数を、実行せずに持ち運んで、あとで他人に実行してもらう」ということが可能になります。関数が値ではない言語では、この発想自体が成立しません。
具体例を、身近なものから積み上げます。
例1:setTimeout ―「時間が来たら実行して」
setTimeout(() => {
console.log("1秒経ちました");
}, 1000);
console.log("これは先に表示される");
【日本語の疑似コード】
「1秒経ったら実行して」という関数を、setTimeoutに預ける
→ setTimeoutは今すぐは実行しない。1秒後まで覚えておくだけ
→ だから、次の行(2つ目のconsole.log)が先に実行されてしまう
→ 1秒後、預けておいた関数がようやく実行される
「上から順番に実行される」という感覚のまま読むと、表示される順番が直感と逆に見えます。コールバックは「今すぐ」ではなく「しかるべきタイミングで」実行されるという前提を持っておく必要があります。
例2:配列のメソッド ―「1個ずつ、この処理をして」
[1, 2, 3].forEach(x => console.log(x));
forEachは「配列の中身を1個ずつ取り出しては、預かった関数にその値を渡して実行する」という処理を、あなたの代わりにやってくれます。あなたは「何をするか」(コールバックの中身)だけ渡し、「いつ・何回・どの値で呼ぶか」はforEach側が決めます。 ここが初心者の引っかかりどころで、コールバックの引数(x)は自分で用意した変数のように見えますが、実際は呼び出す側(forEach)が「今の要素はこれです」と渡してくれているだけの、ただの受け皿です。.map()の(item, index)のindexも同じ理屈で、mapが親切に「今の位置はここですよ」と教えてくれています。
例3:ボタンのクリック ―「クリックされたら実行して」
button.addEventListener("click", () => {
console.log("クリックされました");
});
addEventListenerにコールバックを渡しておくと、ブラウザが「クリックされた瞬間」にそれを呼び出してくれます。ReactのonClick={handleClick}は、この仕組みをJSX風に書けるようにしたものでした(イベントハンドラの項目を参照)。
クセ・注意点:
-
実行タイミングは自分で選べない。 コールバックを渡した瞬間には実行されません。「いつ実行するか」は預けた相手(
setTimeout、forEach、React本体など)が決めます。 - 渡される引数も、預けた相手が決める。 自分で名付けた変数のように見えても、値そのものは呼び出し元が用意しています。
-
アロー関数がコールバックによく使われる理由。 コールバックは「その場限りの、わざわざ名前をつけるまでもない処理」であることが多いので、簡潔に書けるアロー関数と相性がいいです。加えて、根本①の通りアロー関数は独自の
thisを持たないので、クラスのメソッドの中からコールバックを渡すときもthisを見失わずに済みます(.bind(this)が不要)。
つまり、アロー関数は「短く書けるから」コールバックに使われているのではなく、「短く書ける」ことと「thisを見失わない」ことの両方が揃っているから、コールバックの定番になっている、というのが正確な理解です。アロー関数の省略ルール(前の項目)とコールバック(この項目)は、実務では常にセットで出てきます。
テンプレートリテラル(バッククォート)
【図解】
"count: " + count + ""
└──┬──┘ └─┬─┘
文字列 変数 ← +の連続で境目が見えにくい
`count: ${count}`
└─┬─┘ └──┬──┘
文字列 ${}の中だけ変数 ← 境目がひと目で分かる
【日本語の疑似コード】
"count: " という文字列と count の値をくっつけて
1つの文字列を作る
↓ テンプレートリテラルならこう書くだけ
`count: ${count}`
これは何か:文字列の中に${}で変数や式を埋め込める書き方。
なぜこの形か:2015年以前は+で連結するしかなく、変数が増えるほど文字列と変数の境目が読みにくくなっていました。${}は「ここだけが式」と視覚的に分離できます。複数行の文字列もそのまま書けるようになったのも地味に大きい変更でした。
オブジェクトリテラル {} と配列リテラル []
【図解】配列の正体
[10, 20, 30] という配列は、実はこういうオブジェクト
{
"0": 10,
"1": 20,
"2": 30,
length: 3
}
(数字の名前がついた、特殊なオブジェクト)
これは何か:オブジェクトと配列を作る、一番基本的な書き方。
const obj = { type: "li", props: {} }; // key: value のペアの集まり
const arr = [1, 2, 3]; // 順序を持った値の並び
なぜこの形か:根本②の通り、JSのオブジェクトは「名前(プロパティ)と値のペアの集まり」であり、{}はその最も直接的な書き方です。配列は実は特殊なオブジェクトで、0, 1, 2……という数値の文字列をプロパティ名として持つオブジェクトに、lengthのような特別なプロパティと専用のメソッド(push/mapなど)が足されたものです。
クセ・注意点:typeof []が"object"を返すのはこのためです。「配列は特殊なオブジェクトである」という事実は、Array.isArray()のような専用の判定関数が別途用意されている理由でもあります。
分割代入
【図解】
arrayLike = [10, 20]
[first, second] = arrayLike
first ←── 0番目 ←── 10
second ←── 1番目 ←── 20
(順番だけが頼り。名前は自由)
objectLike = { first: 10, second: 20 }
{ first, second } = objectLike
first ←── "first"という名前 ←── 10
second ←── "second"という名前 ←── 20
(名前が頼り。名前を変えるなら {first: f} のように書く)
これは何か:配列やオブジェクトから、複数の値をまとめて取り出して変数に入れる書き方。2015年追加。
const [first, second] = arrayLike; // 配列:位置で取り出す
const { first, second } = objectLike; // オブジェクト:名前で取り出す
【日本語の疑似コード】
arrayLikeという配列の
0番目の値を first という名前の変数に
1番目の値を second という名前の変数に
それぞれ入れる
なぜこの形か:配列は「順番」に意味があるデータ(根本②の通り、実体は数値キーのオブジェクト)なので位置で取り出し、オブジェクトは「名前」に意味があるデータなので名前で取り出します。useStateが{value, setValue}のようなオブジェクトではなく配列を返すのは、まさにこの違いが理由でした。オブジェクトの分割代入は名前で対応するので、useStateを複数回使うたびにvalue/setValueという名前が衝突してしまいますが、配列なら呼び出し側が毎回好きな名前(count/setCountなど)を付けられます。
クセ・注意点:[]と{}、1文字の違いで挙動が全く変わるため、TypeScriptの型注釈と重なると視認性が落ちやすい、という弱点があります。
スプレッド構文・レスト構文(...)
【図解】
mutate(直接書き換え):
todos.push(x)
書き換え前のtodos ─┐
├─ 同じ参照(同一のオブジェクト)のまま
書き換え後のtodos ─┘
spread(新しく作る):
[...todos, x]
書き換え前のtodos ──✕別物──▶ 新しくできた配列
(参照が変わる)
これは何か:配列・オブジェクトを「展開する」(スプレッド)か、「残り全部をまとめる」(レスト)かのどちらかで使う...という記号。配列版は2015年、オブジェクト版は2018年追加。
const merged = [...arr1, ...arr2]; // スプレッド:展開して結合
function h(type, props, ...children) {} // レスト:残り引数をまとめる
【日本語の疑似コード】
todosという配列と、newItemという新しい要素から
中身は同じだが「別物」の、新しい配列を作る
(元のtodosはそのまま、中身は1つも変わっていない)
なぜこの形か:元のデータを直接書き換える(mutateする)と、constで固定した「同じ変数が指すデータの中身」は変わっても、参照そのものは変わりません。本物のReactのuseStateには「新しいstateが前回と同じ参照なら再描画をスキップする」という最適化があり、mutateすると更新が握りつぶされる事故に繋がります。スプレッドで新しいオブジェクト・配列を作れば参照が変わるので、この最適化を正しく通過できます。
クセ・注意点:スプレッドは対象の中身を毎回まるごとコピーする(浅いコピー)ため、大きく・深くネストしたデータを繰り返し更新する場面ではパフォーマンスの問題になり得ます。実務ではImmerというライブラリで、変更点だけ新しく作り、変わっていない部分は使い回す(構造共有)という解決策がよく使われます。
?.(オプショナルチェイニング)と??(Nullish coalescing)
【図解】
newChild?.props?.key ?? i を評価する
newChild は null/undefined?
YES → その場で諦めて undefined
NO → 次へ進む
newChild.props は null/undefined?
YES → その場で諦めて undefined
NO → newChild.props.key の値を取り出す
最後に ?? で受け取る
左側が null/undefined だった → 右側の i を使う
左側がちゃんと値だった(0でもOK)→ その値をそのまま使う
これは何か:?.は「途中がnullかundefinedだったら、そこで諦めてundefinedを返す」、??は「左側がnullかundefinedのときだけ右側を使う」という、2020年追加の演算子。
const key = newChild?.props?.key ?? i;
なぜこの形か:それ以前はnewChild && newChild.props && newChild.props.keyのように、1段ずつ律儀に&&で確認する必要がありました。||で代用しようとすると、keyが0のようなfalsyだが正当な値のときに誤作動します(0 || iはiになってしまう)。??は判定基準を「null/undefinedかどうか」だけに絞ることで、この事故を避けます。
クセ・注意点:便利さゆえに、本当は気づくべきバグ(存在するはずの値がundefinedになっている)まで静かに握りつぶしてしまうことがあります。
truthy / falsy
【図解】
falsyな値(この6つだけ)
┌────────────────────────┐
│ false / 0 / "" / null / undefined / NaN │
└────────────────────────┘
それ以外は全部truthy(意外と引っかかる例)
"0"(文字列の0)→ truthy
[](空配列) → truthy
{}(空オブジェクト)→ truthy
これは何か:JSでは、true/false以外の値も、ifの条件式などでは自動的に真偽値として扱われます。falsy扱いされる値は上の6つだけで、それ以外は全部truthyです。
なぜこの形か:ブール値以外でも条件分岐に使えるようにする、JS初期からの利便性重視の設計です。
クセ・注意点:0や""がfalsyであることが、||を使ったデフォルト値設定を壊す原因になります(?./??の項目を参照)。「配列やオブジェクトは、中身が空でもtruthy」という点も引っかかりやすいポイントです。
class構文
これは何か:オブジェクトの雛形を定義する、2015年追加の構文。中身は根本②で説明した通り、プロトタイプチェーンの糖衣構文です(詳しくは根本②を参照)。
class Counter {
constructor(initial) { this.count = initial; }
increment() { this.count++; }
}
クセ・注意点:classのメソッドは、根本①の「普通の関数」のルールに従うため、thisは呼ばれ方次第です。クラスの中で定義したメソッドをコールバックとして渡すとthisを見失う、というのが根本①で説明した.bind(this)問題でした。
Promiseの基本
【図解】
setState() が呼ばれる
│
▼
「後で再描画してね」という予約だけする(今すぐは何もしない)
│
▼
今実行中の同期処理が全部終わる
│
▼
予約していた再描画が、ここでようやく実行される
これは何か:「今はまだ終わっていないが、いつか終わる処理」の結果を表現するオブジェクト。第4回のバッチ処理実装でPromise.resolve().then(...)という形で使いました。
なぜこの形か:Promise登場(2015年)以前、非同期処理はコールバック関数をネストして書く必要があり(通称コールバック地獄)、コードが右にどんどん深くなっていく問題がありました。Promiseは「非同期処理が終わったら.then()の中身を実行する」という形に統一することで、この問題を整理しました。
クセ・注意点:.then(fn)のfnは「今すぐ」ではなく「今実行中の同期処理が全部終わった直後」に実行されます(マイクロタスク)。第4回のバッチ処理は、この性質を利用していました。
第2部:React基本文法辞典
JSX
【図解】
<li>牛乳を買う</li>
│ コンパイル
▼
React.createElement("li", null, "牛乳を買う")
│ 実行
▼
{ type: "li", props: { children: "牛乳を買う" } }
(ただのJSオブジェクト)
これは何か:<div>...</div>のような、HTMLに似た見た目でUIを書ける記法。
なぜこの形か:第2回で見た通り、正体はReact.createElement(type, props, ...children)という関数呼び出しです。JSXはこの関数呼び出しを、見た目だけHTML風に書けるようにするビルド時の変換(トランスパイル)にすぎません。根本③(関数は値)の応用として、createElementはただのJSオブジェクトを返す関数、という理解が土台になります。
クセ・注意点:JSXの属性がcamelCase(className, onClick)なのは、HTMLの属性ではなくDOM要素のJSプロパティにマッピングしているからでした(camelCaseの項目を参照)。
コンポーネント
【日本語の疑似コード】
Greeting という関数を実行する
{name: "太郎"} というオブジェクトを受け取る
「こんにちは、太郎」という文字列を組み立てる
h("p", {}, "こんにちは、太郎") というオブジェクトを作って返す
(特別な魔法ではなく、ただの関数呼び出し)
これは何か:UIの一部を返す、ただのJS関数。
function Greeting({ name }) {
return h("p", {}, `こんにちは、${name}`);
}
なぜこの形か:根本③(関数は値)そのものです。コンポーネントは特別な構文ではなく、「呼ぶとVirtual DOMオブジェクトを返す、ただの関数」です。
クセ・注意点:コンポーネント名は必ず大文字始まりにする必要があります。小文字だとJSXコンパイラが「これはHTMLの標準タグ(divなど)だ」と誤認してしまうためです。
props
【図解】
親: h(Greeting, { name: "太郎" })
│
│ 第2引数がそのままpropsに
▼
子: function Greeting(props) {
props.name // "太郎"
}
これは何か:親コンポーネントから子コンポーネントに渡す、読み取り専用のデータ。関数の引数として渡されている(根本③)ので、「呼び出し側から中身を自由に決められる、でも受け取った側で書き換えるべきではない」という性質を持ちます。
単一ルート要素とFragment
【図解】
親のchildren配列: [ 0番目のスロット ]
│
Itemが1個返す ──▶ ちょうど1個入る ✅
Itemが2個返そうとする
[ {...}, {...} ] を1個のスロットに押し込もうとする ❌
→ 構文エラー
これは何か:コンポーネントは1つの要素しか返せない、という制約と、それを回避する<>...</>という書き方。
なぜこの形か:理由は2つあります。(1) createElementは関数なので1回の呼び出しで1つの値しか返せない。(2) Reactが差分比較しているのは木構造(第2回参照)なので、親のchildren配列の中で「1スロットに1要素」が守られていないと比較のしようがない。2017年、React 16でこの制約を保ったまま余計なDOM要素を増やさないFragment(<>...</>)が追加されました。
key
【図解】
key無し(位置だけで対応)
前 [牛乳, 卵] → 後 [パン, 牛乳, 卵]
位置0: 牛乳→パン 「中身が変わった」扱い
位置1: 卵→牛乳 「中身が変わった」扱い
位置2: (無し)→卵 新規
key有り(IDで対応)
前 [牛乳(id-A), 卵(id-B)] → 後 [パン(id-C), 牛乳(id-A), 卵(id-B)]
id-A: 同じものと認識 → 移動しただけ
id-B: 同じものと認識 → 移動しただけ
id-C: 対応する前が無い → 新規
これは何か:リストの各要素に付ける、その要素固有の識別子。第2〜3回で詳しく扱った通り、keyはReactが新旧のリストを比較するとき「どの要素とどの要素が同じものか」を教えるための目印です。
クセ・注意点:keyはpropsとして渡っているように見えて、実際にはReact内部だけで使われる特別な扱いを受け、コンポーネント側からprops.keyで読むことはできません。
useState
【図解】
1回目の useState(0) 呼び出し → cursor=0 → states[0] に対応
2回目の useState("") 呼び出し → cursor=1 → states[1] に対応
(呼ばれた「順番」がそのまま配列の「添字」になる)
これは何か:コンポーネントに「状態(記憶)」を持たせるためのHooks。第3回で自作した通り、内部的には「呼ばれた順番(配列のインデックス)」で状態を管理しています。配列を返すのは、分割代入の項目で説明した通り、呼び出し側が自由に名前を付けられるようにするためでした。
イベントハンドラは「関数を渡す」もので「呼ぶ」ものではない
【図解】
onClick={handleClick}
└──┬──┘
関数そのもの(値) ✅ クリックされた瞬間に実行される
onClick={handleClick()}
└───┬───┘
関数を今すぐ実行した結果 ❌ 描画された瞬間に実行済み。
onClickにはundefinedが渡る
【日本語の疑似コード】
正しい場合:
「handleClickという関数」をonClickに渡しておく
→ 後でボタンが押されたときに、その関数が呼ばれる
間違った場合:
「handleClickを今すぐ実行した結果」をonClickに渡す
→ 渡す前にもう実行されてしまっている
→ ボタンを押しても何も起きない
これは何か:onClick={handleClick}とは書いても、onClick={handleClick()}とは書かない、というルール。
なぜこの形か:根本③(関数は値)そのものです。handleClick(括弧無し)は「関数という値そのもの」を指しますが、handleClick()(括弧あり)は「関数を今すぐ実行して、その返り値」を指します。ボタンがクリックされたときに実行してほしいのは前者であって、後者ではありません。
クセ・注意点:この間違いは初心者が高確率で一度は踏む地雷です。引数を渡したい場合はonClick={() => handleClick(id)}のように、その場でアロー関数を作って中で呼ぶのが定石です。
条件付きレンダリング({condition && <div>...})
【図解】通常のケース
isLoggedIn && <p>ようこそ</p>
isLoggedInがfalse → &&は右側を見ずにfalseを返す → 何も描画されない
isLoggedInがtrue → &&は右側の<p>を返す → それが描画される
【図解】事故るケース
count && <p>{count}件あります</p>
countが 0 → &&は 0 を返す(0はfalsyだが数値としては有効な値)
→ Reactは「0」という文字を律儀に描画してしまう
→ 画面に "0" だけがポツンと表示される事故
これは何か:条件を満たすときだけ要素を表示する、JSXでの定番の書き方。&&は 短絡評価 という性質を持っていて、左側がfalsyなら右側を評価せずその場で左側の値を返します。
クセ・注意点:これはtruthy/falsyの項目に直結する、実務で非常によく踏む地雷です。countが0のとき、Reactはfalse/null/undefinedは「何も描画しない」扱いにしますが、 0は数値として律儀に画面に描画してしまいます。 直すにはcount > 0 && <p>...</p>のように、明示的にboolean値になる比較を書きます。
.map()でリストをレンダリングする
【日本語の疑似コード】
todosという配列の
各todoに対して
h("li", {key: todo.id}, todo.text) という
liのオブジェクトを作る
これを全部集めて、新しい配列にする
これは何か :配列のデータからJSXのリストを作るときの定番パターン。
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
なぜこの形か :.map()は「配列の各要素を、渡した関数で変換した新しい配列を作る」というJS標準のメソッドです。根本③の応用で、「データの配列」を「JSX要素の配列」に変換する、という発想がそのままコードになっています。
クセ・注意点:ここで作られる各要素にはkeyが必要でした(keyの項目を参照)。
camelCaseの属性名
【図解】
JSXの属性 DOM要素のプロパティ
className → element.className
onClick → element.onclick(イベントリスナー登録)
tabIndex → element.tabIndex
(HTMLの属性ではなく、こちらに揃えている)
これは何か :className, onClick, tabIndexのような、複数単語をつなげるときの命名規則。
なぜこの形か :JSXの属性はHTMLの属性ではなく、DOM要素が持つJSのプロパティにマッピングされています。ブラウザのDOM APIが元々camelCaseで統一されている(addEventListenerなど)ので、JSXもそれに揃えています。「classが予約語だから」という説明は半分だけ正確で、より本質的な理由はこちらです。
useから始まるHooksの命名規則
【図解】
useSomething() ← ESLintが見る: "use"で始まる
→ Hooksのルール(条件分岐で呼ぶな等)を適用する
getSomething() ← ESLintが見る: 普通の名前の関数
→ ルールの対象外。中でHooksを呼んでいても見逃す
これは何か:useState, useEffectのように、Hooksは必ずuseで始まります。第3回で自作した通りHooksは呼ばれた順番(配列のインデックス)で状態を管理しているため、条件分岐やループの中で呼ぶと壊れます。useという接頭辞は、この壊れやすいルールをESLintが静的に検知するための、人間とツールへの目印です。JS自体にもReact本体にも強制力はありません。
まとめ
- 全部の根っこにあるのは3つだけ:①thisは呼ばれ方で決まる、②JSのオブジェクトはプロトタイプで動く、③関数はただの値
- JSの基本文法は、ほとんどがこの根本のどれかに由来するか、根本が引き起こす事故への対策として追加されてきた
- Reactの基本文法(JSX、コンポーネント、props、イベントハンドラ、条件付きレンダリングなど)も同様に、根本③「関数は値である」の応用がほとんど
-
onClick={handleClick}とonClick={handleClick()}の違い、{count && <div>}が0を表示してしまう事故は、それぞれ根本③・truthy/falsyの直接の帰結
迷ったら、まずこの3つの根本のどれに関係する話なのかを考えてみてください。大抵、そこに戻ると見通しが良くなります。
参考文献
- MDN Web Docs「this」「プロトタイプ」「第一級関数」「Promise」
次回
**Fiber ~ なぜReact 16でアーキテクチャが刷新されたのか ~**
- 差分計算、実は「一気に」やると画面がカクつく問題があった
- Reactが「作業を中断・再開できる」仕組みに作り替えた理由
- 優先度をつけてレンダリングするってどういうこと?
を書きます。