はじめに
「クロージャって、人に説明できますか?」「イベントループの仕組みを図なしで話せますか?」
普段からJavaScriptを書いていても、いざ基礎を体系的に説明しようとすると言葉に詰まる、という経験はないでしょうか。フレームワークの使い方には詳しくなっても、その土台となる言語仕様の理解が曖昧なまま何年も過ごしてしまうケースは意外と多いものです。
33-js-concepts は、そんな「なんとなく分かっているつもり」を解消するために作られたリソースです。JavaScriptを理解する上で欠かせない33個の概念をリストアップし、それぞれに解説記事や動画へのリンクをまとめてくれています。今回はこのプロジェクトの中身と活用方法を紹介します。
とはいえ、説明を読むより実際に動かして体感した方が早いと思います。クロージャによる状態の隠蔽をブラウザ上でそのまま試せるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
33-js-conceptsとは
33-js-concepts は、Leonardo Maldonado氏が作成したGitHubリポジトリで、JavaScriptエンジニアが押さえておくべき33個のコア概念を一覧化したものです。2018年にGitHubの「注目すべきオープンソースプロジェクト」の一つに選ばれ、現在では6万を超えるスターと40以上の言語への翻訳を集める、学習リソースとして定番の存在になっています。
専用サイト(33jsconcepts.com)では各概念ごとに解説とコード例、参考リンクがまとめられており、リポジトリ自体はその入り口として機能しています。
主な特徴
- 体系的なチェックリスト - 基礎から応用まで33個の概念がカテゴリ別に整理されており、抜け漏れなく学習を進められる
- 実行可能なコード例 - 概念ごとに動くサンプルコードが用意されており、手を動かしながら理解を深められる
- 多言語対応の解説記事 - コミュニティによって40以上の言語に翻訳されており、母語で学べる
インストール
33-js-concepts はライブラリではなく学習用リポジトリのため、npmでインストールするものではありません。リポジトリをクローンして、ローカルでサンプルコードを動かしながら読み進めるのが基本的な使い方です。
# リポジトリをクローン
git clone https://github.com/leonardomso/33-js-concepts.git
# ディレクトリに移動
cd 33-js-concepts
READMEから各概念の解説記事へアクセスできるので、ブラウザで直接閲覧するだけでも十分に活用できます。
33-js-conceptsのサンプルを動かす
33-js-concepts はimportして使うライブラリではありませんが、そこで扱われている概念自体は素のJavaScriptとしてブラウザ上でそのまま動かせます。まずは33項目の中でもつまずきやすい「クロージャ(closure)」を、+1 / -1 / リセットのボタンで実際に触りながら確認してみましょう。
クロージャとは、関数が定義時のスコープを覚えたまま、その外側からは直接触れない状態(変数)を保持し続ける仕組みのことです。次のように createCounter の中でしか count にアクセスできない関数を作ることで、状態を安全にカプセル化できます。
function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
reset: () => (count = initial),
value: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
// count 変数自体には createCounter の外から直接アクセスできない
下のサンプルはこの createCounter をボタン操作に接続したものです。ボタンを押すたびに increment / decrement / reset が呼ばれ、クロージャが保持している count の値が画面に反映されます。コンソールでは、外側のスコープから count に直接アクセスしようとすると undefined になる(=クロージャの外へは漏れていない)ことも確認できます。
increment や decrement はクロージャによって同じ count を参照し続けているため、ボタンを何度押しても値は正しく積み上がっていきます。一方でコンソールに出る typeof count は 'undefined' になり、count という変数が createCounter の外側には存在しないことが分かります。initial の値を書き換えてみると、リセット時の戻り先も一緒に変わります。
基本的な使い方
リポジトリで紹介されている33個の概念は、次のようなカテゴリに分かれています。
1. 基礎編
- プリミティブ型とオブジェクトの違い
- 型強制(Type Coercion)
- スコープとクロージャ
2. 実行の仕組み
- コールスタック
- イベントループ
- IIFE・モジュール・名前空間
3. オブジェクト指向
- ファクトリ関数とクラス
- this, call, apply, bind
- プロトタイプによる継承
4. 非同期処理
- コールバック
- Promise
- async/await
- ジェネレーターとイテレーター
5. 関数型プログラミング
- 高階関数
- 純粋関数
- map, reduce, filter
- 再帰、カリー化と関数合成
例えば「クロージャ」の概念を実際のコードで確認すると、次のようになります。
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
increment 関数が外側のスコープにある count を参照し続けている、これがクロージャです。33-js-concepts では、こうした概念一つひとつに対して解説記事とサンプルコードがセットで用意されているため、動作を確認しながら理解を固めていけます。
実践的なユースケース
1. チーム内の学習ロードマップとして使う
新しくJavaScriptを学ぶメンバーがいるチームでは、READMEの33項目をそのままチェックリストとして配布し、週に数個ずつ潰していくという使い方ができます。カテゴリごとに整理されているため、進捗も把握しやすいのが利点です。
2. 面接・技術試験の準備に使う
フロントエンドエンジニアの技術面接では「イベントループを説明してください」「プロトタイプ継承とは何ですか」といった基礎的な質問が定番です。33-js-concepts の各項目を自分の言葉で説明できるかを確認するだけで、実践的な面接対策になります。
代表例として頻出なのが「同期処理・setTimeout・Promise を混在させたとき、コンソールにはどの順番で出力されるか」という質問です。イベントループの仕組みでは、同期処理(コールスタック)がすべて終わったあとにマイクロタスク(Promise.then)が処理され、最後にマクロタスク(setTimeout)が実行されます。
console.log('1: 同期処理');
setTimeout(() => console.log('4: マクロタスク(setTimeout)'), 0);
Promise.resolve().then(() => console.log('3: マイクロタスク(Promise.then)'));
console.log('2: 同期処理');
// 出力順序: 1 -> 2 -> 3 -> 4
実際にボタンを押して、この実行順序をその場で確認できるサンプルが次のとおりです。
初回表示は自動でボタンを押した状態にしてあるので、開いた瞬間に 1 → 2 → 3 → 4 の順番がリストに並びます。setTimeout の待ち時間を 0 から 100 に変えても出力順序自体は変わりません。これは、マクロタスクである setTimeout がマイクロタスクである Promise.then より必ず後に実行される、というイベントループの優先順位を表しているからです。
3. コードレビューの視点を養う
例えば「高階関数」の理解が曖昧なままだと、次のようなコードのリファクタリングの意図を見逃してしまいます。
// 高階関数を使わない書き方
function double(numbers) {
const result = [];
for (const n of numbers) {
result.push(n * 2);
}
return result;
}
// 高階関数を使った書き方
const double = (numbers) => numbers.map((n) => n * 2);
33-js-concepts の「関数型プログラミング」カテゴリを一通り押さえておくと、こうした書き換えの意図を自信を持ってレビューコメントに書けるようになります。
高階関数の真価は、filter → map → reduce のようにメソッドチェーンでつなぎ、処理をパイプライン化できる点にあります。次の例では「下限価格でフィルタ」「税込価格に変換」「合計を集計」という3つの高階関数を組み合わせています。
const orders = [120, 45, 300, 80, 15];
const total = orders
.filter((price) => price >= 50) // 50円未満は除外
.map((price) => price * 1.1) // 消費税10%を加算
.reduce((sum, price) => sum + price, 0);
console.log(total); // フィルタ→変換→集計を高階関数だけで表現
金額リストと下限価格、税率を書き換えながら、filter / map / reduce の結果がどう変わるかを確認できるサンプルです。
入力のたびに filter・map・reduce が再計算され、結果がその場で更新されます。「下限価格」を 0 にすると全件が対象になり、「税率」を 0 にすると金額がそのまま合計されるので、高階関数がそれぞれ独立した処理を担っていることが体感できるはずです。
4. プロトタイプ継承の理解を確認する
「プロトタイプによる継承」も面接や設計レビューで頻出のテーマです。JavaScriptのオブジェクトは prototype チェーンを通じてメソッドを共有しており、Object.create() や Object.setPrototypeOf() を使うことで、あるコンストラクタのメソッドを別のコンストラクタに引き継がせることができます。
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} は鳴きます`;
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.speak = function () {
return `${this.name} はワンと鳴きます`;
};
const dog = new Dog('ポチ');
console.log(dog.speak());
console.log(dog instanceof Animal); // true
名前を入力してボタンを押すと、Dog.prototype が Animal.prototype を継承していること、instanceof の判定結果を確認できます。
Dog.prototype = Object.create(Animal.prototype) の行をコメントアウトすると、dog instanceof Animal が false に変わります。この1行こそが「Dog は Animal のプロトタイプチェーンに連なっている」という継承関係を成立させている部分だと分かります。
まとめ
33-js-concepts は、日々のコーディングでは意識しにくいJavaScriptの基礎概念を、体系立てて振り返るための優れたロードマップです。フレームワークの知識が先行しがちな現場だからこそ、たまに立ち止まってこの33項目をおさらいしてみると、自分の理解の穴に気づけるはずです。
まずはリポジトリのREADMEを開いて、自信を持って説明できない項目がいくつあるか、チェックしてみてはいかがでしょうか。
