JavaScript の基礎
この部の 1 / 9 章 ・ 全体で 34 / 76 章 ・ 読了目安 55 分
- 実行時に何が起きているかを説明できる
- 非同期処理を直列と並列で書き分けられる
- 破壊的な操作と参照の共有による事故を避けられる
TypeScript は、JavaScript に型を足したものです。 ビルドすると型は消え、実際に動くのは JavaScript です。
あなたが書く TypeScript
↓ ビルド(型を取り除くだけ)
実行される JavaScript
つまり、実行時の挙動はすべて JavaScript の挙動です。
型が付いていても、0.1 + 0.2 !== 0.3 は変わりませんし、
[1, 10, 2].sort() は [1, 10, 2] のままです(後述)。
次の章で型を扱う前に、土台になる JavaScript を押さえます。
値には7つの基本型がある
'hello' // string
42 // number ← 整数と小数の区別が無い。すべて64bit浮動小数点
10n // bigint ← 大きな整数用
true // boolean
undefined // 値が入っていない(宣言されたが未代入)
null // 「無い」ことを明示的に表す
Symbol('id') // symbol(ほぼ使わない)これ以外はすべてオブジェクトです。配列も関数も、実体はオブジェクトです。
JavaScript には int がありません。すべて64bit の浮動小数点です。
0.1 + 0.2 // 0.30000000000000004(ビットとバイト)
9007199254740993 // 9007199254740992 ← 精度が足りず、値が変わる安全に扱える整数は約 9007兆(2⁵³) までです。
Go の int64 の値を JSON で受け取ると壊れる可能性があるので、
大きな ID は文字列でやり取りするのが実務の定石です
(protobuf の int64 が JSON では文字列になるのは、これが理由です)。
undefined と null の使い分け
| 意味 | いつ出るか | |
|---|---|---|
undefined | まだ値が無い | 未代入の変数、無いプロパティ、戻り値の無い関数 |
null | 意図的に「無い」 | 人が明示的に入れた時だけ |
実務では「null は自分で入れる、undefined は勝手に出てくる」と考えると整理できます。 チームで片方に統一するのが一般的です。
変数は const を使う
const x = 1; // 再代入できない(基本これ)
let y = 2; // 再代入する時だけ
var z = 3; // 使わないvar を使わない理由は、スコープの単位がブロックではなく関数だからです。
if (true) { var a = 1; let b = 2; }
console.log(a); // 1 ← ブロックの外から見えてしまう
console.log(b); // エラー ← 正しい挙動const が保証するのは再代入できないことだけです。
const user = { name: 'sato' };
user.name = 'tanaka'; // これは通る
user = {}; // これはエラー中身まで固定したい場合は Object.freeze か、
TypeScript の readonly(次章)を使います。
比較は === を使う
== は型を勝手に変換してから比較します。
0 == '0' // true ← 文字列が数値に変換される
0 == '' // true
0 == false // true
null == undefined // true
'' == '0' // false ← 直感に反する規則が複雑で覚える価値がないので、常に === を使います。
唯一の例外は x == null(null と undefined の両方を判定する)で、
これは慣用句として許容されることがあります。
falsy な値
if (x) で偽と判定される値は、次の8つです。
false, 0, -0, 0n, '', null, undefined, NaN
function render(count) {
if (!count) return '件数がありません'; // count === 0 でもここに来る
return `${count} 件`;
}0 と空文字は「値が無い」ではありません。
if (count === undefined) ... // 明示的に書く
const n = count ?? 0; // ?? は null / undefined の時だけ既定値を使う
const m = count || 0; // || は 0 や '' も置き換えてしまう??(null 合体演算子)と || の違いは、実務でよく効きます。
NaN は自分と等しくない
NaN === NaN // false
Number.isNaN(NaN) // true ← 判定にはこれを使うNumber('abc') や 0/0 の結果が NaN です。
数値に変換できなかったことに気づかず計算を続けるのが、よくあるバグです。
オブジェクトと配列は「参照」
const a = { n: 1 };
const b = a;
b.n = 2;
console.log(a.n); // 2 ← 同じものを指している関数に渡した場合も同じです。中身を書き換えると呼び出し元にも影響します。
function addTax(items) {
items.forEach((i) => { i.price *= 1.1; }); // 引数を破壊している
}これは「引数を渡しただけなのに元の配列が変わっている」という 追いにくいバグを生みます。新しい配列を返すのが原則です。
const withTax = items.map((i) => ({ ...i, price: Math.floor(i.price * 1.1) }));const copy = { ...original };
copy.tags.push('x'); // original.tags も変わる(同じ配列を指している){ ...obj } は1階層だけコピーします。
入れ子まで含めて複製したい場合は structuredClone(obj) を使います
(JSON.parse(JSON.stringify(obj)) は Date や undefined が壊れるため非推奨)。
関数
function f(a, b = 10) { return a + b; } // 関数宣言(巻き上げられる)
const g = (a, b = 10) => a + b; // アロー関数(式)
const h = (...args) => args.length; // 可変長引数関数は値なので、変数に入れたり、引数として渡したりできます。
これが map や filter にコールバックを渡せる理由です。
クロージャ — 関数が外の変数を覚えている
function counter() {
let count = 0;
return () => ++count; // 外側の count を「閉じ込めて」いる
}
const next = counter();
next(); // 1
next(); // 2counter() はもう終わっているのに、count は生き続けます。
返された関数が参照しているからです(プロセスとメモリの GC の話につながります)。
実務で意識的に使うのは、設定を先に固定した関数を作る時です。
const withRetry = (times) => async (fn) => {
for (let i = 0; i < times; i++) {
try { return await fn(); } catch (e) { if (i === times - 1) throw e; }
}
};
const retry3 = withRetry(3);this は呼び出し方で決まる
JavaScript で最も混乱するところです。this は定義場所ではなく、呼び出し方で決まります。
const obj = {
name: 'sato',
hello() { return this.name; },
};
obj.hello(); // 'sato' (obj. の形で呼んだから)
const fn = obj.hello;
fn(); // undefined(呼び出し元が消えた)アロー関数は自分の this を持たず、書かれた場所の this を使います。
これが「コールバックはアロー関数で書く」という慣習の理由です。
class Timer {
constructor() { this.count = 0; }
start() {
setInterval(() => { this.count++; }, 1000); // 動く
setInterval(function () { this.count++; }, 1000); // 動かない(this が違う)
}
}実務では、メソッドをそのまま値として渡さない、 コールバックはアロー関数で書くの2つを守れば、ほぼ事故りません。
配列メソッド
for を書く前に、これらで書けないか考えます。
const users = [{ name: 'a', age: 20 }, { name: 'b', age: 30 }];
users.map((u) => u.name); // 変換 → ['a', 'b']
users.filter((u) => u.age >= 25); // 絞り込み
users.find((u) => u.name === 'a'); // 最初の1件(無ければ undefined)
users.some((u) => u.age > 25); // 1件でも該当するか → boolean
users.every((u) => u.age > 10); // 全部該当するか
users.reduce((sum, u) => sum + u.age, 0); // 畳み込み → 50
users.flatMap((u) => [u.name, u.age]); // 変換して平坦化
Object.groupBy(users, (u) => u.age >= 25 ? 'senior' : 'junior'); // 分類破壊的か、そうでないか
元の配列を書き換えるメソッドがあります。 ここは覚えてください。
| 破壊する | 破壊しない(新しい配列を返す) |
|---|---|
sort reverse splice push pop shift unshift fill | map filter slice concat toSorted toReversed |
const a = [3, 1, 2];
const b = a.sort(); // a も [1,2,3] に変わる(b と a は同じ配列)
const c = a.toSorted(); // a は変わらない(新しい配列)これは非常によく踏みます。
[1, 10, 2, 20].sort(); // [1, 10, 2, 20] ← 文字列順
[1, 10, 2, 20].sort((a, b) => a - b); // [1, 2, 10, 20]デフォルトの sort は要素を文字列に変換して比較します。
数値をソートする時は必ず比較関数を渡してください。
便利な記法
const { name, age = 0 } = user; // 分割代入(既定値つき)
const [first, ...rest] = list; // 配列の分割代入
const merged = { ...defaults, ...options }; // スプレッド(後勝ち)
user?.profile?.avatar // オプショナルチェイニング(途中が null/undefined なら undefined)
user.name ?? '名無し' // null 合体(null/undefined の時だけ既定値)
`${name} さん(${age}歳)` // テンプレートリテラル?. と ?? は、null チェックのネストを消せるので実務で多用します。
非同期処理
これが JavaScript で最も重要な部分です。
ブラウザはなぜ動くのかで見たとおり、JavaScript はシングルスレッドです。 時間のかかる処理(通信・ファイル読み込み)で待ってしまうと、 その間なにもできません。だから待たずに、終わったら教えてもらう方式を取ります。
Promise
fetch('/api/users') // Promise を返す(まだ結果は無い)
.then((res) => res.json()) // 成功したら次へ
.then((data) => console.log(data))
.catch((err) => console.error(err)) // どこかで失敗したらここ
.finally(() => console.log('完了'));Promise は「いつか結果が入る箱」で、3つの状態を持ちます。
pending(処理中) → fulfilled(成功)
→ rejected(失敗)
async / await
then の連鎖は読みにくいので、同期のコードのように書ける記法が用意されています。
async function loadUsers() {
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
logger.error({ err }, 'ユーザーの取得に失敗');
throw err;
}
}asyncを付けた関数は、必ず Promise を返しますawaitは Promise の結果が出るまで、その関数の中だけ待ちますawaitはasync関数の中か、モジュールのトップレベルでのみ使えます
const res = await fetch('/api/nope'); // 404 でも例外にならないfetch が失敗するのはネットワークに到達できなかった時だけです。
HTTP のステータスは自分で確認する必要があります(res.ok)。
これを忘れると、「エラーなのに成功として処理が進む」バグになります。
並列にする
これを知らないと、無駄に遅いコードを書きます。
// 直列: 合計 300ms(100 + 100 + 100)
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
// 並列: 合計 100ms(同時に始めて、全部揃うのを待つ)
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);互いに依存しない処理は Promise.all でまとめます。
| 使い分け | 意味 |
|---|---|
Promise.all | 全部成功で成功。1つでも失敗したら即失敗 |
Promise.allSettled | 全部の結果を成否込みで受け取る |
Promise.race | 最初に決着したものを採用(タイムアウトの実装に使う) |
// 遅い: 1件ずつ順番に待つ(1000件 × 50ms = 50秒)
for (const id of ids) {
results.push(await fetchUser(id));
}ただし、全部を一斉に投げるのも危険です。1000件同時に リクエストを送ると、相手のサービスを落とします(マイクロサービスの実務)。
同時実行数を制限しながら回すのが正解です。
// 10件ずつ処理する
for (let i = 0; i < ids.length; i += 10) {
const chunk = ids.slice(i, i + 10);
results.push(...await Promise.all(chunk.map(fetchUser)));
}items.forEach(async (i) => { await save(i); });
console.log('完了'); // save が終わる前に出力されるforEach はコールバックの Promise を待ちません。
for...of を使うか、Promise.all(items.map(...)) にします。
実行順を予測する
ブラウザはなぜ動くのかのイベントループの話です。
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 出力: 1, 4, 3, 2同期コードが全部終わってから、Promise(マイクロタスク)が先、
その後に setTimeout(マクロタスク)が実行されます。
モジュール
// export する側
export const VERSION = '1.0';
export function hello() {}
export default class Server {}
// import する側
import Server, { hello, VERSION } from './server.js';
import * as server from './server.js';これが ESM(ECMAScript Modules)で、現在の標準です。
Node.js には古い CommonJS(require / module.exports)もあり、
両者が混在しているのが、Node 界隈のややこしさの元です。
// CommonJS(古い方)
const { hello } = require('./server');
module.exports = { hello };package.json の "type": "module" で ESM 扱いになります。
新規に書くなら ESM です。
A が B を import し、B が A を import すると、 片方が初期化前の undefined を掴むことがあります。
エラーメッセージが undefined is not a function のような
分かりにくい形になるのが厄介です。
循環が起きたら、共通部分を3つ目のモジュールに切り出すのが定石です。 これは設計の基礎の依存の向きの話につながります。
`const ids = [3, 25, 100]; ids.sort();` の結果はどうなりますか。
実務の落とし穴まとめ
==を使う — 常に===if (count)— 0 と空文字を弾いてしまう。??と||を区別するsort()に比較関数を渡さない — 文字列比較になる- 破壊的メソッドに気づかない —
sortreversespliceは元を変える - スプレッドで深いコピーのつもり — 1階層だけ
fetchが 404 で例外にならない —res.okを必ず見るforEachの中でawait— 待たれない- ループの中で直列
await— 遅い。ただし一斉並列も危険 - 大きな整数を number で受ける — 2⁵³ を超えると壊れる。ID は文字列で
thisを持つメソッドを値として渡す —thisが消える
まとめ
- TypeScript は型を消せば JavaScript。実行時の挙動はすべてこの章の話
constを基本に、===で比較する- オブジェクトと配列は参照。引数を破壊せず、新しい値を返す
thisは呼び出し方で決まる。コールバックはアロー関数で書く- 配列メソッドは破壊的かどうかを意識する。
sortは要注意 - 非同期は Promise と async/await。
fetchの失敗判定と、Promise.allによる並列化が実務の勘所 - 実行順は 同期 → マイクロタスク(Promise)→ マクロタスク(setTimeout)
- モジュールは ESM を使う。循環 import は設計の警告
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| MDN JavaScript ガイド(日本語) | https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide |
| MDN JavaScript リファレンス | https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference |
| ECMAScript 仕様 | https://tc39.es/ecma262/ |
章末問題
3つの API を順に await した処理が 900ms かかっています。3つは互いに独立しています。どう直しますか。
配列を受け取って合計金額に消費税を加える関数を書きます。最も安全なのはどれですか。
次は、この JavaScript に型を足します。