diff --git a/README.md b/README.md index 9b77a38..c7c14f2 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,59 @@ npm run lint * GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google * GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github +## ベースとなるドキュメントの作り方 + +- web版の ~~Gemini2.5Pro~~ Gemini3Pro を用いる。 +- 以下のプロンプトで章立てを考えさせる + > `n`章前後から構成される`言語名`のチュートリアルを書こうと思います。章立てを考えてください。`言語名`以外の言語でのプログラミングはある程度やったことがある人を対象にします。 + > +- nを8, 10, 12, 15 など変えて何回か出力させ、それを統合していい感じの章立てを決める +- 実際にドキュメントを書かせる + > 以下の内容で`言語名`チュートリアルの第`n`章を書いてください。他の言語でのプログラミングは経験がある人を対象にします。 + > タイトルにはレベル1の見出し(#), それ以降の見出しにはレベル2以下(##)を使用してください。 + REPLで動作可能なコード例はスクリプトではなくREPLの実行例として書いてください。 + > コード例はREPLの実行例では \`\`\``言語名`-repl 、ソースファイルの場合は \`\`\``言語名`:ファイル名`.拡張子` ではじまるコードブロックで示してください。ファイル名は被らないようにしてください。 + > また、ファイルの場合は \`\`\``言語名`-exec:ファイル名`.拡張子` のコードブロック内に実行結果例を記載してください。 + > また、最後には この章のまとめ セクションと、練習問題を2つほど書いてください。練習問題はこの章で学んだ内容を活用してコードを書かせるものにしてください。 + > + > 全体の構成 + > `1. hoge` + > `2. fuga` + > `3. piyo` + > `4. ...` + > + > `第n章: 第n章のタイトル` + > `第n章内の見出し・内容の概要…` + > +- Gemini出力の調整 + - Canvasを使われた場合はやり直す。(Canvasはファイル名付きコードブロックで壊れる) + - 箇条書きの最後に `` と出力される場合がある。消す + - 太字がなぜか `**キーワード**` の代わりに `\*\*キーワード\*\*` となっている場合がある。 `\*\*` → `**` の置き換えで対応 + - 見出しの前に `-----` (水平線)が入る場合がある。my.code();は水平線の表示に対応しているが、消す方向で統一 + - `言語名-repl` にはページ内で一意なIDを追加する (例: `言語名-repl:1`) + - REPLの出力部分に書かれたコメントは消えるので修正する + - ダメな例 + ```` + ```js-repl:1 + > console.log("Hello") + Hello // 文字列を表示する + ``` + ```` + - 以下のようにすればok + ```` + ```js-repl:1 + > console.log("Hello") // 文字列を表示する + Hello + + > // 文字列を表示する + > console.log("Hello") + Hello + ``` + ```` + - 練習問題の見出しは「この章のまとめ」の直下のレベル3見出しで、 `### 練習問題n` または `### 練習問題n: タイトル` とする + - 練習問題のファイル名は不都合がなければ `practice(章番号)_(問題番号).拡張子` で統一。空でもよいのでファイルコードブロックとexecコードブロックを置く +- 1章にはたぶん練習問題要らない。 + ## markdown仕様 ```` diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index 600e7b4..e6a70c0 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -110,7 +110,7 @@ function CodeComponent({ } else if (match[2] === "-repl") { // repl付きの言語指定 if (!match[3]) { - console.warn( + console.error( `${match[1]}-repl without terminal id! content: ${String(props.children).slice(0, 20)}...` ); } diff --git a/app/[docs_id]/styledSyntaxHighlighter.tsx b/app/[docs_id]/styledSyntaxHighlighter.tsx index 42b2e92..29b959e 100644 --- a/app/[docs_id]/styledSyntaxHighlighter.tsx +++ b/app/[docs_id]/styledSyntaxHighlighter.tsx @@ -32,6 +32,7 @@ export type MarkdownLang = | "sh" | "json" | "csv" + | "html" | "text" | "txt"; @@ -45,6 +46,7 @@ export type SyntaxHighlighterLang = | "javascript" | "typescript" | "bash" + | "html" | "json"; export function getSyntaxHighlighterLang( lang: MarkdownLang | undefined @@ -70,6 +72,8 @@ export function getSyntaxHighlighterLang( return "bash"; case "json": return "json"; + case "html": + return "html"; case "csv": // not supported case "text": case "txt": @@ -77,7 +81,7 @@ export function getSyntaxHighlighterLang( return undefined; default: lang satisfies never; - console.warn(`Language not listed in MarkdownLang: ${lang}`); + console.error(`getSyntaxHighlighterLang() does not handle language ${lang}`); return undefined; } } diff --git a/app/pagesList.ts b/app/pagesList.ts index 8140446..0a0c675 100644 --- a/app/pagesList.ts +++ b/app/pagesList.ts @@ -39,6 +39,23 @@ export const pagesList = [ { id: 12, title: "メタプログラミング入門" }, ], }, + { + id: "javascript", + lang: "JavaScript", + description: "hoge", + pages: [ + { id: 1, title: "JavaScriptへようこそ" }, + { id: 2, title: "基本構文とデータ型" }, + { id: 3, title: "制御構文" }, + { id: 4, title: "関数とクロージャ" }, + { id: 5, title: "'this'の正体" }, + { id: 6, title: "オブジェクトとプロトタイプ" }, + { id: 7, title: "クラス構文" }, + { id: 8, title: "配列とイテレーション" }, + { id: 9, title: "非同期処理①: Promise" }, + { id: 10, title: "非同期処理②: Async/Await" }, + ], + }, { id: "cpp", lang: "C++", diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index a191512..f80b0a8 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -67,12 +67,13 @@ export function getAceLang(lang: MarkdownLang | undefined): AceLang { case "bash": case "text": case "txt": + case "html": case undefined: console.warn(`Ace editor mode not implemented for language: ${lang}`); return "text"; default: lang satisfies never; - console.warn(`Language not listed in MarkdownLang: ${lang}`); + console.error(`getAceLang() does not handle language ${lang}`); return "text"; } } diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 4d2d0a0..62e49db 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -73,12 +73,13 @@ export function getRuntimeLang( case "csv": case "text": case "txt": + case "html": case undefined: // unsupported languages return undefined; default: lang satisfies never; - console.warn(`Language not listed in MarkdownLang: ${lang}`); + console.error(`getRuntimeLang() does not handle language ${lang}`); return undefined; } } @@ -152,6 +153,7 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { return { tabSize: 2, prompt: "> ", + promptMore: "... ", }; case "c_cpp": case "cpp": diff --git a/app/terminal/worker/jsEval.ts b/app/terminal/worker/jsEval.ts index 0023a9f..860c4e1 100644 --- a/app/terminal/worker/jsEval.ts +++ b/app/terminal/worker/jsEval.ts @@ -14,7 +14,7 @@ export function useJSEval() { return { ...context, splitReplExamples, - // getCommandlineStr, + getCommandlineStr, }; } @@ -24,11 +24,19 @@ function splitReplExamples(content: string): ReplCommand[] { if (line.startsWith("> ")) { // Remove the prompt from the command initCommands.push({ command: line.slice(2), output: [] }); + } else if (line.startsWith("... ")) { + if (initCommands.length > 0) { + initCommands[initCommands.length - 1].command += "\n" + line.slice(4); + } } else { // Lines without prompt are output from the previous command + // and the last output is return value if (initCommands.length > 0) { + initCommands[initCommands.length - 1].output.forEach( + (out) => (out.type = "stdout") + ); initCommands[initCommands.length - 1].output.push({ - type: "stdout", + type: "return", message: line, }); } @@ -36,3 +44,7 @@ function splitReplExamples(content: string): ReplCommand[] { } return initCommands; } + +function getCommandlineStr(filenames: string[]) { + return `node ${filenames[0]}`; +} diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 463b5ec..8f58205 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -2,28 +2,30 @@ import type { ReplOutput } from "../repl"; import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; +import inspect from "object-inspect"; +function format(...args: unknown[]): string { + // TODO: console.logの第1引数はフォーマット指定文字列を取ることができる + // https://nodejs.org/api/util.html#utilformatformat-args + return args.map((a) => (typeof a === "string" ? a : inspect(a))).join(" "); +} let jsOutput: ReplOutput[] = []; // Helper function to capture console output const originalConsole = self.console; self.console = { ...originalConsole, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - log: (...args: any[]) => { - jsOutput.push({ type: "stdout", message: args.join(" ") }); + log: (...args: unknown[]) => { + jsOutput.push({ type: "stdout", message: format(...args) }); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (...args: any[]) => { - jsOutput.push({ type: "stderr", message: args.join(" ") }); + error: (...args: unknown[]) => { + jsOutput.push({ type: "stderr", message: format(...args) }); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - warn: (...args: any[]) => { - jsOutput.push({ type: "stderr", message: args.join(" ") }); + warn: (...args: unknown[]) => { + jsOutput.push({ type: "stderr", message: format(...args) }); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - info: (...args: any[]) => { - jsOutput.push({ type: "stdout", message: args.join(" ") }); + info: (...args: unknown[]) => { + jsOutput.push({ type: "stdout", message: format(...args) }); }, }; @@ -36,18 +38,49 @@ async function init({ id }: WorkerRequest["init"]) { } async function runCode({ id, payload }: WorkerRequest["runCode"]) { - const { code } = payload; + let { code } = payload; try { - // Execute code directly with eval in the worker global scope - // This will preserve variables across calls - const result = self.eval(code); + let result: unknown; + + // eval()の中でconst,letを使って変数を作成した場合、 + // 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、 + // varに置き換えている + if (code.trim().startsWith("const ")) { + code = "var " + code.trim().slice(6); + } else if (code.trim().startsWith("let ")) { + code = "var " + code.trim().slice(4); + } + // eval()の中でclassを作成した場合も同様 + const classRegExp = /^\s*class\s+(\w+)/; + if (classRegExp.test(code)) { + code = code.replace(classRegExp, "var $1 = class $1"); + } - if (result !== undefined) { - jsOutput.push({ - type: "return", - message: String(result), - }); + if (code.trim().startsWith("{") && code.trim().endsWith("}")) { + // オブジェクトは ( ) で囲わなければならない + try { + result = self.eval(`(${code})`); + } catch (e) { + if (e instanceof SyntaxError) { + // オブジェクトではなくブロックだった場合、再度普通に実行 + result = self.eval(code); + } else { + throw e; + } + } + } else if (/^\s*await\W/.test(code)) { + // promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする + result = await self.eval(code.trim().slice(5)); + } else { + // Execute code directly with eval in the worker global scope + // This will preserve variables across calls + result = self.eval(code); } + + jsOutput.push({ + type: "return", + message: inspect(result), + }); } catch (e) { originalConsole.log(e); // TODO: stack trace? @@ -110,7 +143,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { try { // Try to create a Function to check syntax - new Function(code); + // new Function(code); // <- not working + self.eval(`() => {${code}}`); self.postMessage({ id, payload: { status: "complete" }, @@ -120,8 +154,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { if (e instanceof SyntaxError) { // Simple heuristic: check for "Unexpected end of input" if ( - e.message.includes("Unexpected end of input") || - e.message.includes("expected expression") + e.message.includes("Unexpected token '}'") || + e.message.includes("Unexpected end of input") ) { self.postMessage({ id, diff --git a/package-lock.json b/package-lock.json index 02d055f..1a4efb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "drizzle-orm": "^0.44.7", "mocha": "^11.7.4", "next": "<15.5", + "object-inspect": "^1.13.4", "pg": "^8.16.3", "prismjs": "^1.30.0", "pyodide": "^0.29.0", @@ -45,6 +46,7 @@ "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", "@types/node": "^20", + "@types/object-inspect": "^1.13.0", "@types/pg": "^8.15.5", "@types/prismjs": "^1.26.5", "@types/react": "^19", @@ -12248,6 +12250,13 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/object-inspect": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@types/object-inspect/-/object-inspect-1.13.0.tgz", + "integrity": "sha512-lwGTVESDDV+XsQ1pH4UifpJ1f7OtXzQ6QBOX2Afq2bM/T3oOt8hF6exJMjjIjtEWeAN2YAo25J7HxWh97CCz9w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/pg": { "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", diff --git a/package.json b/package.json index 1b7c336..7ca1ff4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "drizzle-orm": "^0.44.7", "mocha": "^11.7.4", "next": "<15.5", + "object-inspect": "^1.13.4", "pg": "^8.16.3", "prismjs": "^1.30.0", "pyodide": "^0.29.0", @@ -53,6 +54,7 @@ "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", "@types/node": "^20", + "@types/object-inspect": "^1.13.0", "@types/pg": "^8.15.5", "@types/prismjs": "^1.26.5", "@types/react": "^19", diff --git a/public/docs/javascript-1.md b/public/docs/javascript-1.md new file mode 100644 index 0000000..590cbea --- /dev/null +++ b/public/docs/javascript-1.md @@ -0,0 +1,108 @@ +# 第1章: JavaScriptへようこそ + +本章では、JavaScript(以下JS)がどのような思想で設計され、JavaやPython、C\#といった他の言語とどう異なるのか、その全体像を把握します。また、学習に必要な環境構築と最初のコード実行を行います。 + +## JavaScriptとは? + +JavaScriptは1995年、Netscape社のBrendan Eichによってわずか10日間でプロトタイプが作成されました。当初はWebページに軽い動きをつけるための言語でしたが、現在では**ECMAScript (ES)** として標準化され、フロントエンドからバックエンド、モバイルアプリまで幅広く利用されています。 + +経験豊富なエンジニアが押さえておくべき特徴は以下の3点です。 + +1. **動的型付け (Dynamic Typing):** + 変数は型を持ちません。値が型を持ちます。コンパイル時ではなく実行時に型が決まるため、柔軟ですが、実行時エラーのリスク管理が必要です(現代ではTypeScriptで補うのが一般的です)。 +2. **マルチパラダイム:** + 命令型の手続き記述はもちろん、**関数型プログラミング**(第一級関数、クロージャ)や、**プロトタイプベースのオブジェクト指向**をサポートします。クラスベースの言語に慣れていると、ここの概念モデルの違いに驚くかもしれません。 +3. **シングルスレッド & ノンブロッキングI/O:** + JSのランタイムは基本的にシングルスレッドです。しかし、**イベントループ**という仕組みにより、重いI/O操作(ネットワーク通信やファイル読み込み)を非同期で処理し、メインスレッドをブロックせずに高い並行性を実現します。 + +## 実行環境: ブラウザ vs Node.js + +JavaScriptはどこで動くのでしょうか? かつてはブラウザの中だけでしたが、現在は大きく分けて2つの環境があります。 + + * **Webブラウザ (クライアントサイド):** + + * DOM (Document Object Model) 操作により、HTML/CSSを動的に書き換えます。 + * `window` オブジェクトがグローバルスコープです。 + * セキュリティ上の制約(サンドボックス)があり、ローカルファイルへの直接アクセスなどは制限されています。 + + * **Node.js (サーバーサイド):** + + * ChromeのV8 JavaScriptエンジンをブラウザの外に取り出したランタイムです。 + * OSの機能(ファイルシステム、ネットワーク)にアクセス可能です。 + * DOMは存在しません。Webサーバー構築やCLIツールの作成に使われます。 + +言語仕様(コア機能)は同じですが、**「何ができるか(API)」は環境に依存する**という点を意識してください。 + +## 他言語との比較 + +あなたが既に知っている言語とJSを比較してみましょう。 + +| 特徴 | Java / C\# | Python | JavaScript | +| :--- | :--- | :--- | :--- | +| **型システム** | 静的型付け (強い型付け) | 動的型付け (強い型付け) | **動的型付け (弱い型付け)** | +| **並行処理** | マルチスレッド | マルチスレッド (GILあり) | **シングルスレッド + イベントループ** | +| **OOP** | クラスベース | クラスベース | **プロトタイプベース** (class構文はシンタックスシュガー) | +| **実行方式** | コンパイル (JVM/CLR) | インタープリタ | **JITコンパイル** (多くのエンジン) | + + * **Java/C\#ユーザーへの注記:** JSの`class`は見た目は似ていますが、裏側の仕組み(プロトタイプチェーン)は全く異なります。また、コンパイルエラーで弾かれるようなコードも、JSでは実行できてしまう(そして実行時に落ちる)ことがあります。 + * **Pythonユーザーへの注記:** Pythonの`asyncio`に似ていますが、JSは**デフォルトで非同期**を前提としています。また、インデントではなく波括弧 `{}` でブロックを定義します。 + +## "Hello, World\!" + +実際にコードを動かしてみましょう。ここでは2つの方法を紹介します。 + +### REPL (Read-Eval-Print Loop) での実行 + +ちょっとした動作確認にはREPLが便利です。Node.jsのREPLを起動するには、ターミナルで `node` と入力して起動します。 + +このウェブサイトではドキュメント内にJavaScriptの実行環境を埋め込んでおり、以下のように緑枠で囲われたコード例には自由にJavaScriptコードを書いて試すことができます。ただしNode.jsとは環境が異なり、Node.js特有の機能は使用できません。 + +```js-repl:1 +> console.log("Hello, World from REPL!"); +Hello, World from REPL! +undefined +> 1 + 2 +3 +``` + +※ `undefined` は `console.log` 関数の戻り値が表示されています。 + +### ソースファイルからの実行 + +本格的なプログラムはファイルに記述します。 + +まず、以下の内容で `hello.js` というファイルを作成してください。 + +```js:hello.js +// 変数定義 (後述しますが、現代ではconstを使います) +const greeting = "Hello, World!"; +const target = "Node.js"; + +// テンプレートリテラル (バッククォート ` を使用) +console.log(`${greeting} I am running on ${target}.`); +``` + +ターミナルでファイルのあるディレクトリに移動し、`node` コマンドで実行します。 + +```js-exec:hello.js +Hello, World! I am running on Node.js. +``` + +### ブラウザでの実行 (参考) + +ブラウザで動かす場合は、HTMLファイルが必要です。 +`index.html` を作成し、以下のように記述してブラウザで開いてみてください。 + +```html + + + + + + +``` + +ブラウザの開発者ツール(Consoleタブ)にメッセージが表示され、ポップアップウィンドウが出れば成功です。 diff --git a/public/docs/javascript-10.md b/public/docs/javascript-10.md new file mode 100644 index 0000000..11f7cc8 --- /dev/null +++ b/public/docs/javascript-10.md @@ -0,0 +1,247 @@ +# 第10章: 非同期処理(2)- Async/Await と Fetch API + +前回(第9章)では、JavaScriptの非同期処理の要である `Promise` について学びました。しかし、`.then()` チェーンが長く続くと、コードの可読性が下がる(いわゆる「コールバック地獄」に近い状態になる)ことがあります。 + +第10章では、この課題を解決するために導入された **Async/Await** 構文と、現代的なHTTP通信の標準である **Fetch API** について解説します。他の言語で同期的なコード(ブロッキング処理)に慣れ親しんだ方にとって、Async/Await は非常に直感的で扱いやすい機能です。 + +## Async/Await 構文 + +`async` と `await` は、ES2017で導入された `Promise` の**シンタックスシュガー(糖衣構文)**です。これを使うことで、非同期処理をあたかも「同期処理」のように上から下へと流れるコードとして記述できます。 + +### `async` 関数 + +関数宣言の前に `async` キーワードを付けると、その関数は自動的に **Promiseを返す** ようになります。値を `return` した場合、それは `Promise.resolve(値)` と同じ意味になります。 + +```js-repl:1 +> async function getMessage() { return "Hello, Async!"; } +undefined +> // async関数は常にPromiseを返す +> getMessage() +Promise { 'Hello, Async!' } + +> // 通常のPromiseと同じくthenで値を取り出せる +> getMessage().then(v => console.log(v)) +Promise { } +Hello, Async! +``` + +### `await` 式 + +`async` 関数の内部(またはモジュールのトップレベル)でのみ使用できるキーワードです。 +`await` は、右側の Promise が **Settled(解決または拒否)されるまで関数の実行を一時停止** します。Promiseが解決されると、その結果の値を返して実行を再開します。 + +これは、C\# の `async/await` や Python の `asyncio` に慣れている方にはおなじみの挙動でしょう。 + +```js-repl:2 +> function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +undefined +> async function run() { +... console.log("Start"); +... await delay(1000); // 1秒待機(ここで実行が一時停止) +... console.log("End"); +... } +undefined +> run() +Promise { } +// (1秒後に表示) +Start +End +``` + +## try...catch によるエラーハンドリング + +生の `Promise` では `.catch()` メソッドを使ってエラーを処理しましたが、Async/Await では、他の言語と同様に標準的な `try...catch` 構文を使用できます。これにより、同期エラーと非同期エラーを同じ構文で扱えるようになります。 + +```js:async_try_catch.js +// ランダムに成功・失敗する非同期関数 +function randomRequest() { + return new Promise((resolve, reject) => { + setTimeout(() => { + const success = Math.random() > 0.5; + if (success) { + resolve("Success: データ取得完了"); + } else { + reject(new Error("Failure: サーバーエラー")); + } + }, 500); + }); +} + +async function main() { + console.log("処理開始..."); + try { + // awaitしているPromiseがrejectされると、例外がスローされる + const result = await randomRequest(); + console.log(result); + } catch (error) { + // ここでエラーを捕捉 + console.error("エラーが発生しました:", error.message); + } finally { + console.log("処理終了"); + } +} + +main(); +``` + +```js-exec:async_try_catch.js +処理開始... +エラーが発生しました: Failure: サーバーエラー +処理終了 +``` + +*(※注: 実行結果はランダムで成功する場合もあります)* + +## Fetch API によるHTTPリクエスト + +JavaScript(特にブラウザ環境や最近のNode.js)でHTTPリクエストを行うための標準APIが `fetch` です。以前は `XMLHttpRequest` という扱いづらいAPIが使われていましたが、現在は `fetch` が主流です。 + +`fetch` 関数は `Promise` を返します。 + +基本的な流れは以下の通りです: + +1. `fetch(url)` を実行し、レスポンスヘッダーが届くのを待つ。 +2. Responseオブジェクトを受け取る。 +3. Responseオブジェクトからメソッド(`.json()`, `.text()`など)を使ってボディを読み込む(これも非同期)。 + +```js:fetch_basic.js +// 外部APIからJSONデータを取得する例 +// (Node.js 18以上ではfetchが標準で使用可能です) + +async function getUserData(userId) { + const url = `https://jsonplaceholder.typicode.com/users/${userId}`; + + try { + // 1. リクエスト送信 (ネットワークエラー以外はrejectされない) + const response = await fetch(url); + + // 2. HTTPステータスコードの確認 + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + // 3. レスポンスボディをJSONとしてパース (これもPromiseを返す) + const data = await response.json(); + + console.log(`Name: ${data.name}`); + console.log(`Email: ${data.email}`); + + } catch (error) { + console.error("Fetch failed:", error.message); + } +} + +getUserData(1); +``` + +```js-exec:fetch_basic.js +Name: Leanne Graham +Email: Sincere@april.biz +``` + +### JSONデータの送信 (POST) + +データを送信する場合は、第2引数にオプションオブジェクトを渡します。 + +```js-repl:3 +> const postData = { title: 'foo', body: 'bar', userId: 1 }; +> await fetch('https://jsonplaceholder.typicode.com/posts', { +... method: 'POST', +... headers: { 'Content-Type': 'application/json' }, +... body: JSON.stringify(postData) +... }).then(res => res.json()) +{ title: 'foo', body: 'bar', userId: 1, id: 101 } +``` + +## Promise.all() と Promise.race() + +Async/Await は便利ですが、単純に `await` を連発すると、処理が**直列(シーケンシャル)**になってしまい、パフォーマンスが落ちる場合があります。複数の独立した非同期処理を行う場合は、並列実行を検討します。 + +### 直列実行(遅いパターン) + +```javascript +// Aが終わってからBを開始する +const user = await fetchUser(); +const posts = await fetchPosts(); +``` + +### Promise.all() による並列実行 + +複数のPromiseを配列として受け取り、**全て完了するのを待って**から結果の配列を返します。一つでも失敗すると全体が失敗(reject)します。 + +```js:promise_all.js +const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms)); + +async function parallelDemo() { + console.time("Total Time"); + + // 2つの処理を同時に開始 + const p1 = wait(1000, "User Data"); + const p2 = wait(1000, "Post Data"); + + try { + // 両方の完了を待つ + const [user, post] = await Promise.all([p1, p2]); + console.log("Result:", user, "&", post); + } catch (e) { + console.error(e); + } + + // 本来なら直列だと2秒かかるが、並列なので約1秒で終わる + console.timeEnd("Total Time"); +} + +parallelDemo(); +``` + +```js-exec:promise_all.js +Result: User Data & Post Data +Total Time: 1.008s +``` + +### Promise.race() + +複数のPromiseのうち、**最も早く完了(または失敗)したもの**の結果だけを返します。タイムアウト処理の実装などによく使われます。 + +```js-repl:4 +> const fast = new Promise(r => setTimeout(() => r("Fast"), 100)); +> const slow = new Promise(r => setTimeout(() => r("Slow"), 500)); +> await Promise.race([fast, slow]) +'Fast' +``` + +## この章のまとめ + + * **Async/Await**: `Promise` をベースにした糖衣構文。非同期処理を同期処理のように記述でき、可読性が高い。 + * **Error Handling**: 同期コードと同じく `try...catch` が使用可能。 + * **Fetch API**: モダンなHTTP通信API。`response.ok` でステータスを確認し、`response.json()` でボディをパースする2段構えが必要。 + * **並列処理**: 独立した複数の非同期処理は `await` を連続させるのではなく、`Promise.all()` を使用して並列化することでパフォーマンスを向上させる。 + +## 練習問題 + +### 問題1: ユーザー情報の取得と表示 + +以下の要件を満たす関数 `displayUserSummary(userId)` を作成してください。 + +1. `https://jsonplaceholder.typicode.com/users/{userId}` からユーザー情報を取得する。 +2. `https://jsonplaceholder.typicode.com/users/{userId}/todos` からそのユーザーのTODOリストを取得する。 +3. 上記2つのリクエストは、**パフォーマンスを考慮して並列に実行**すること。 +4. 取得したデータから、「ユーザー名」と「完了済み(completed: true)のTODOの数」を出力する。 +5. 通信エラー時は適切にエラーメッセージを表示する。 + +```js:practice10_1.js +``` + +```js-exec:practice10_1.js +``` + +### 問題2: タイムアウト付きFetch + +指定したURLからデータを取得するが、一定時間内にレスポンスが返ってこない場合は「タイムアウト」としてエラーにする関数 `fetchWithTimeout(url, ms)` を作成してください。 +*ヒント: `fetch` のPromiseと、指定時間後に reject するPromiseを `Promise.race()` で競走させてください。* + +```js:practice10_2.js +``` + +```js-exec:practice10_2.js +``` diff --git a/public/docs/javascript-2.md b/public/docs/javascript-2.md new file mode 100644 index 0000000..e49db21 --- /dev/null +++ b/public/docs/javascript-2.md @@ -0,0 +1,250 @@ +# 第2章: 基本構文とデータ型 + +プログラミング経験者であるあなたにとって、変数の概念やデータ型の存在自体は目新しいものではありません。しかし、JavaScriptには「動的型付け」や「歴史的経緯による特殊なスコープ仕様」、「独特な型変換」といった、他言語経験者が特に躓きやすいポイントがあります。 + +本章では、モダンなJavaScript(ES6以降)における標準的な記述方法を中心に、レガシーな仕様との違いや、バグを生みやすい落とし穴について解説します。 + +## 変数宣言: let, const, var + +現代のJavaScript開発において、変数宣言のルールは非常にシンプルです。 +**「基本は `const`、再代入が必要な場合のみ `let` を使い、`var` は決して使わない」** これが鉄則です。 + +### const と let (ブロックスコープ) + +ES6(2015年)で導入された `const` と `let` は、C++やJava、C\#などと同様に**ブロックスコープ**を持ちます。 + + * **const**: 再代入不可能な変数を宣言します。定数だけでなく、再代入しない変数はすべてこれで宣言します。 + * **let**: 再代入可能な変数を宣言します。ループカウンタや、状態が変わる値に使用します。 + +### var の危険性 (関数スコープと巻き上げ) + +なぜ `var` を使うべきではないのでしょうか。それは `var` が**関数スコープ**であり、意図しない変数の共有や「巻き上げ(Hoisting)」によるバグを引き起こしやすいからです。 + +以下のコードで、スコープの違いを確認してみましょう。 + +```js:scope_demo.js +function checkScope() { + if (true) { + var functionScoped = "I am visible outside this block"; + let blockScoped = "I am NOT visible outside this block"; + const constantValue = "I am also block scoped"; + } + + console.log("var output:", functionScoped); // 参照可能(関数スコープのため) + + try { + console.log("let output:", blockScoped); // ReferenceError + } catch (e) { + console.error("let error:", e.message); + } +} + +checkScope(); +``` + +```js-exec:scope_demo.js +var output: I am visible outside this block +let error: blockScoped is not defined +``` + +## データ型: プリミティブ型 + +JavaScriptは動的型付け言語であり、変数は特定の型に紐付きませんが、値自体は型を持っています。JavaScriptの値は大きく分けて「プリミティブ型」と「オブジェクト(参照)型」に分類されます。 + +プリミティブ型はイミュータブル(変更不可)であり、以下の7種類が存在します。 + +1. **String**: 文字列。ES6から導入された「テンプレートリテラル(バッククォート `` ` ``)」を使うと、変数の埋め込みが容易です。 +2. **Number**: 数値。整数と浮動小数点数の区別はなく、すべて倍精度浮動小数点数(IEEE 754)として扱われます。 +3. **Boolean**: `true` または `false`。 +4. **undefined**: 「値が未定義である」ことを表す型。変数を宣言して値を代入していない状態です。 +5. **null**: 「値が存在しない」ことを意図的に示す型。 +6. **Symbol**: 一意で不変な識別子。オブジェクトのプロパティキーなどに使われます。 +7. **BigInt**: `Number`型では表現できない巨大な整数を扱います(末尾に `n` をつけます)。 + +### null と undefined の違い + +他言語経験者にとって混乱しやすいのがこの2つです。 + + * **undefined**: システム(JavaScriptエンジン)が「値がまだない」ことを示すために使うことが多い。 + * **null**: プログラマが「ここには値がない」ことを明示するために使うことが多い。 + +```js-repl:1 +> let unassigned; +undefined +> unassigned +undefined +> let empty = null; +undefined +> typeof unassigned +'undefined' +> typeof empty // JSの有名なバグ(仕様)で 'object' が返りますが、実際はプリミティブです +'object' +``` + +## データ型: オブジェクト型 + +プリミティブ以外のすべての値は**オブジェクト(参照型)**です。これらはメモリ上のアドレス(参照)として扱われます。 + +### const とオブジェクトの変更 + +重要な点として、`const` で宣言した変数は「再代入」ができませんが、中身がオブジェクトの場合、**プロパティの変更は可能**です。これは `const` が「参照先のメモリアドレス」を固定するものであり、ヒープ領域にあるデータそのものを不変にするわけではないためです。 + +```js:object_mutation.js +const user = { + name: "Alice", + id: 1 +}; + +// 再代入はエラーになる +// user = { name: "Bob" }; // TypeError: Assignment to constant variable. + +// プロパティの変更は可能 +user.name = "Bob"; +console.log(user); + +// 配列もオブジェクトの一種 +const colors = ["Red", "Green"]; +colors.push("Blue"); +console.log(colors); +``` + +```js-exec:object_mutation.js +{ name: 'Bob', id: 1 } +[ 'Red', 'Green', 'Blue' ] +``` + +主なオブジェクト型には以下があります。 + + * **Object**: キーと値のペアの集合(辞書、ハッシュマップに近い)。 + * **Array**: 順序付きリスト。 + * **Function**: JavaScriptでは関数もオブジェクトであり、変数に代入したり引数として渡すことができます(第一級関数)。 + +## 演算子と等価性 (== vs ===) + +JavaScriptにおける最大の落とし穴の一つが「等価演算子」です。 + +### 厳密等価演算子 (===) を使う + +常に `===` (および `!==`)を使用してください。これは「値」と「型」の両方が等しいかを比較します。 + +### 等価演算子 (==) の罠 + +`==` は、比較する前に**暗黙的な型変換**を行います。これにより、直感的ではない結果が生じることがあります。 + +```js-repl:2 +> 1 === "1" // 型が違うので false(推奨) +false +> 1 == "1" // 文字列が数値に変換されて比較されるため true(非推奨) +true +> 0 == false // true +true +> null == undefined // true(ここだけは例外的に許容するスタイルもあるが、基本は避ける) +true +> [] == ![] // 非常に難解な挙動(trueになる) +true +``` + +## 型変換(暗黙的な型変換の罠) + +JavaScriptは文脈に応じて勝手に型を変換しようとします。 + +### 加算演算子 (+) の挙動 + +`+` 演算子は、数値の加算だけでなく文字列の連結にも使われます。片方が文字列であれば、もう片方も文字列に変換されて連結されます。 + +```js-repl:3 +> 10 + 20 +30 +> 10 + "20" // 数値が文字列 "10" に変換され連結される +'1020' +> "10" + 20 +'1020' +> 10 - "2" // 減算は数値計算しかないので、文字列 "2" が数値に変換される +8 +``` + +### Falsyな値 + +条件式(if文など)で `false` とみなされる値を「Falsyな値」と呼びます。これ以外はすべて `true`(Truthy)として扱われます。 + +**Falsyな値のリスト:** + +1. `false` +2. `0` (数値のゼロ) +3. `-0` +4. `0n` (BigIntのゼロ) +5. `""` (空文字) +6. `null` +7. `undefined` +8. `NaN` (Not a Number) + +**注意:** 空の配列 `[]` や空のオブジェクト `{}` は **Truthy** です。 + +```js:falsy_check.js +const values = [0, "0", [], null, undefined, ""]; + +values.forEach(val => { + if (val) { + console.log(`Value: [${val}] is Truthy`); + } else { + console.log(`Value: [${val}] is Falsy`); + } +}); +``` + +```js-exec:falsy_check.js +Value: [0] is Falsy +Value: [0] is Truthy +Value: [] is Truthy +Value: [null] is Falsy +Value: [undefined] is Falsy +Value: [] is Falsy +``` + +## この章のまとめ + + * 変数は `const` をデフォルトとし、再代入が必要な場合のみ `let` を使う。`var` は使用しない。 + * プリミティブ型は値渡し、オブジェクト型(配列含む)は参照渡しである。 + * `const` で宣言したオブジェクトの中身は変更可能である。 + * 比較には必ず `===`(厳密等価演算子)を使用し、`==` による暗黙の型変換を避ける。 + * `0`, `""`, `null`, `undefined` などが「Falsyな値」として扱われることを理解する。 + +### 練習問題1: テンプレートリテラルと型変換 + +ユーザーの年齢(数値)と名前(文字列)を受け取り、自己紹介文を作成する関数を作成してください。 +ただし、年齢が `null` または `undefined` の場合は「不明」と表示するようにしてください。論理和演算子 `||` または Null合体演算子 `??` を活用してみましょう。 + +```js:practice2_1.js +// 以下の関数を完成させてください +function introduce(name, age) { + // ここにコードを記述 +} + +console.log(introduce("Tanaka", 25)); +console.log(introduce("Sato", null)); +``` + +```js-exec:practice2_1.js +My name is Tanaka and I am 25 years old. +My name is Sato and I am 不明 years old. +``` + +### 練習問題2: オブジェクトの操作と参照 + +以下のコードにはバグ(意図しない挙動)があります。 +`originalList` の内容を保持したまま、新しい要素を追加した `newList` を作成したいのですが、現状では `originalList` も変更されてしまいます。 +スプレッド構文 `...` などを使い、`originalList` を変更せずに `newList` を作成するように修正してください。 + +```js:practice2_2.js +const originalList = ["Apple", "Banana"]; + +// 参照コピーになっているため originalList も変わってしまう +const newList = originalList; +newList.push("Orange"); + +console.log("Original:", originalList); // ["Apple", "Banana"] と出力させたい +console.log("New:", newList); // ["Apple", "Banana", "Orange"] と出力させたい +``` + +```js-exec:practice2_2.js +``` diff --git a/public/docs/javascript-3.md b/public/docs/javascript-3.md new file mode 100644 index 0000000..4540435 --- /dev/null +++ b/public/docs/javascript-3.md @@ -0,0 +1,264 @@ +# 第3章: 制御構文 + +他の言語での開発経験がある方にとって、JavaScriptの制御構文の多くは馴染み深いものでしょう。しかし、JavaScript特有の「真偽値の評価(Truthy/Falsy)」や「反復処理(Iteration)の種類の多さ」は、バグを生みやすいポイントでもあります。 + +この章では、構文そのものだけでなく、JavaScriptならではの挙動やベストプラクティスに焦点を当てて解説します。 + +## 条件分岐 (if, switch) + +### if文とTruthy / Falsy + +基本的な `if` 文の構造はC言語やJavaと同様です。しかし、条件式における評価はJavaScript特有の**Truthy(真と見なされる値)**と**Falsy(偽と見なされる値)**の概念を理解する必要があります。 + +厳密な `true` / `false` だけでなく、あらゆる値が条件式の中で真偽判定されます。 + +**Falsyな値(falseとして扱われるもの):** + + * `false` + * `0`, `-0`, `0n` (BigInt) + * `""` (空文字) + * `null` + * `undefined` + * `NaN` + +**Truthyな値(trueとして扱われるもの):** + + * 上記Falsy以外すべて + * **注意:** 空の配列 `[]` や 空のオブジェクト `{}` は **Truthy** です(Pythonなどの経験者は注意が必要です)。 + * 文字列の `"0"` や `"false"` もTruthyです。 + +```js-repl:1 +> if (0) { 'True'; } else { 'False'; } +'False' + +> if ("") { 'True'; } else { 'False'; } +'False' + +> if ([]) { 'True'; } else { 'False'; } // 空配列は真! +'True' + +> const user = { name: "Alice" }; +> if (user) { `Hello ${user.name}`; } +'Hello Alice' +``` + +### switch文 + +`switch` 文も標準的ですが、比較が **厳密等価演算子 (`===`)** で行われる点に注意してください。型変換は行われません。 + +```js:switch_example.js +const status = "200"; // 文字列 + +switch (status) { + case 200: // 数値の200と比較 -> false + console.log("OK (Number)"); + break; + case "200": // 文字列の"200"と比較 -> true + console.log("OK (String)"); + break; + default: + console.log("Unknown status"); +} +``` + +```js-exec:switch_example.js +OK (String) +``` + +## 繰り返し (for, while) + +`while`, `do...while`, および古典的な `for` ループは、C/Java/C++等の構文とほぼ同じです。 + +### 古典的な for ループ + +```js-repl:2 +> for (let i = 0; i < 3; i++) { console.log(i); } +0 +1 +2 +``` + +### while ループ + +```js-repl:3 +> let count = 0; +> while (count < 3) { console.log(count++); } +0 +1 +2 +``` + +## イテレーション: for...of と for...in の違い + +現代のJavaScript開発において、最も重要なのがこの2つのループの使い分けです。これらは似ていますが、役割が明確に異なります。 + +### for...in ループ(プロパティ名の列挙) + +`for...in` はオブジェクトの **キー(プロパティ名)** を列挙するために設計されています。 +配列に対して使用すると、インデックス("0", "1", ...)が文字列として返ってくるだけでなく、プロトタイプチェーン上のプロパティまで列挙してしまうリスクがあるため、**配列への使用は推奨されません**。 + +```js:for_in_example.js +const user = { + name: "Bob", + age: 30, + role: "admin" +}; + +// オブジェクトのキーを列挙する +for (const key in user) { + console.log(`${key}: ${user[key]}`); +} + +// 配列に対する for...in(非推奨の例) +const colors = ["Red", "Green"]; +Array.prototype.badProp = "Do not do this"; // プロトタイプ汚染のシミュレーション + +console.log("--- Array via for...in ---"); +for (const index in colors) { + console.log(index); // "0", "1", "badProp" が出力される可能性がある +} +``` + +```js-exec:for_in_example.js +name: Bob +age: 30 +role: admin +--- Array via for...in --- +0 +1 +badProp +``` + +### for...of ループ(反復可能オブジェクトの走査) + +ES2015 (ES6) で導入された `for...of` は、**値(Values)** を反復します。 +配列、文字列、Map、Setなどの **Iterable(反復可能)** なオブジェクトに対して使用します。配列の中身を順番に処理したい場合は、こちらが正解です。 + +```js:for_of_example.js +const languages = ["JavaScript", "Python", "Go"]; + +// 配列の値を直接取得できる +for (const lang of languages) { + console.log(lang); +} + +// 文字列もIterable +const word = "AI"; +for (const char of word) { + console.log(char); +} +``` + +```js-exec:for_of_example.js +JavaScript +Python +Go +A +I +``` + +### 使い分けのまとめ + +| 構文 | 取得するもの | 対象 | 推奨ユースケース | +| :--- | :--- | :--- | :--- | +| **`for...in`** | **キー (Key)** | Object | オブジェクトのプロパティ調査 | +| **`for...of`** | **値 (Value)** | Array, String, Map, Set | 配列やリストデータの処理 | + +> **Tips:** オブジェクトの中身を `for...of` で回したい場合は、`Object.keys()`, `Object.values()`, `Object.entries()` を使うのがモダンな手法です。 + +```js-repl:4 +> const obj = { a: 1, b: 2 }; +> for (const [key, val] of Object.entries(obj)) { console.log(key, val); } +a 1 +b 2 +``` + +## 例外処理 (try...catch...finally) + +JavaScriptの例外処理は `try...catch...finally` 構文を使用します。 + +### 基本的なエラーハンドリング + +実行時にエラーが発生すると、処理が中断され `catch` ブロックに移行します。 + +```js:try_catch.js +function parseJson(jsonString) { + try { + const result = JSON.parse(jsonString); + console.log("パース成功:", result); + return result; + } catch (e) { + // エラーオブジェクトが e に入る + console.error("パース失敗:", e.name, e.message); + } finally { + console.log("処理終了(成功・失敗に関わらず実行)"); + } +} + +parseJson('{"valid": true}'); +console.log("---"); +parseJson('Invalid JSON'); +``` + +```js-exec:try_catch.js +パース成功: { valid: true } +処理終了(成功・失敗に関わらず実行) +--- +パース失敗: SyntaxError Unexpected token I in JSON at position 0 +処理終了(成功・失敗に関わらず実行) +``` + +### throw について + +JavaScriptでは `throw` で例外を投げることができます。`Error` オブジェクトを投げるのが一般的ですが、技術的には文字列や数値など、任意の値を投げることが可能です(ただし、スタックトレースが取れなくなるため推奨されません)。 + +```js-repl:5 +> try { throw new Error("Something went wrong"); } catch (e) { console.log(e.message); } +Something went wrong + +> // プリミティブ値を投げることも可能だが非推奨 +> try { throw "Just a string"; } catch (e) { console.log(typeof e, e); } +string Just a string +``` + +## この章のまとめ + + * **条件分岐:** `if` 文での `[]` や `{}` は Truthy であることに注意。`switch` は厳密等価 (`===`) で判定される。 + * **繰り返し:** 古典的な `for`, `while` は他のC系言語と同じ。 + * **for...in:** オブジェクトの **キー** を列挙する。配列には使わないこと。 + * **for...of:** 配列やコレクションの **値** を反復する。リスト処理の標準。 + * **例外処理:** `try...catch...finally` で行う。`throw` は任意の値を投げられるが、通常は `Error` オブジェクトを使用する。 + +### 練習問題1: 配列のフィルタリングと集計 + +以下の数値が入った配列 `numbers` があります。 +`for...of` ループを使用して、**偶数かつ10より大きい数値** だけを抽出し、その合計値を計算してコンソールに出力するプログラムを書いてください。 + +```js:practice3_1.js +const numbers = [5, 12, 8, 20, 7, 3, 14, 30]; +// ここにコードを書く +``` + +```js-exec:practice3_1.js +``` + +### 問題 2: 簡易コマンドディスパッチャ + +以下の仕様を満たす関数 `executeCommand(command)` を `switch` 文と `try...catch` を用いて作成してください。 + +1. 引数 `command` は文字列を受け取る。 +2. `"start"` の場合、"System starting..." を出力。 +3. `"stop"` の場合、"System stopping..." を出力。 +4. それ以外の文字列の場合、`Error` オブジェクトを `throw` する(メッセージは "Unknown command")。 +5. `try...catch` ブロックを用いてこの関数を呼び出し、エラーが発生した場合は "Error caught: Unknown command" のように出力する。 + +**ヒント:** `command` が `null` や `undefined` の場合もエラーとして処理されるように実装してください。 + +```js:practice3_2.js +function executeCommand(command) { + +} +``` + +```js-exec:practice3_2.js +``` diff --git a/public/docs/javascript-4.md b/public/docs/javascript-4.md new file mode 100644 index 0000000..5b37f3b --- /dev/null +++ b/public/docs/javascript-4.md @@ -0,0 +1,245 @@ +# 第4章: 関数とクロージャ + +JavaScriptにおいて関数はオブジェクトの一種です。つまり、変数に代入したり、他の関数の引数として渡したり、関数から戻り値として返したりすることができます。この柔軟性が、JavaScriptの設計パターンの核心を担っています。 + +## 関数の定義(関数宣言 vs 関数式) + +JavaScriptには関数を定義する方法が主に2つあります。「関数宣言」と「関数式」です。これらは似ていますが、**巻き上げ(Hoisting)** の挙動が異なります。 + +### 1\. 関数宣言 (Function Declaration) + +古くからある定義方法です。スクリプトの実行前に読み込まれるため、定義する前の行から呼び出すことができます。 + +```js:function_declaration.js +console.log(greet("Alice")); // 定義前でも呼び出せる + +function greet(name) { + return `Hello, ${name}!`; +} +``` + +```js-exec:function_declaration.js +Hello, Alice! +``` + +### 2\. 関数式 (Function Expression) + +変数に関数を代入するスタイルです。変数の代入は実行時に行われるため、定義する前に呼び出すとエラーになります。現代のJavaScript開発では、意図しない巻き上げを防ぐためにこちら(または後述のアロー関数)が好まれる傾向にあります。 + +```js:function_expression.js +// 定義前に呼び出すと... ReferenceError: Cannot access 'sayHi' before initialization +// console.log(sayHi("Bob")); + +const sayHi = function(name) { + return `Hi, ${name}!`; +}; + +console.log(sayHi("Bob")); +``` + +```js-exec:function_expression.js +Hi, Bob! +``` + +## アロー関数 (=\>) の構文と特徴 + +ES2015 (ES6) で導入されたアロー関数は、関数式をより短く記述するための構文です。Javaのラムダ式やPythonのlambdaに似ていますが、いくつか独自の特徴があります。 + +### 基本構文 + +`function` キーワードを省略し、`=>` (矢印) を使って定義します。 + +```js:arrow_function.js +// 従来の関数式 +const add = function(a, b) { + return a + b; +}; + +// アロー関数 +const addArrow = (a, b) => { + return a + b; +}; + +console.log(addArrow(3, 5)); +``` + +```js-exec:arrow_function.js +8 +``` + +### 省略記法 + +アロー関数には強力な省略記法があります。 + +1. **引数が1つの場合**: カッコ `()` を省略可能。 +2. **処理が1行でreturnする場合**: 中括弧 `{}` と `return` キーワードを省略可能(暗黙のreturn)。 + +```js-repl:4 +> const square = x => x * x; // 引数の()とreturnを省略 +> square(5); +25 + +> const getUser = (id, name) => ({ id: id, name: name }); // オブジェクトを返す場合は()で囲む +> getUser(1, "Gemini"); +{ id: 1, name: 'Gemini' } +``` + +> **注意:** アロー関数は単なる短縮記法ではありません。「`this` を持たない」という重要な特徴がありますが、これについては**第5章**で詳しく解説します。 + +## 引数:デフォルト引数、Restパラメータ (...) + +関数の柔軟性を高めるための引数の機能を見ていきましょう。 + +### デフォルト引数 + +引数が渡されなかった場合(または `undefined` の場合)に使用される初期値を設定できます。 + +```js:default_args.js +const connect = (host = 'localhost', port = 8080) => { + console.log(`Connecting to ${host}:${port}...`); +}; + +connect(); // 両方省略 +connect('127.0.0.1'); // portはデフォルト値 +connect('example.com', 22); // 両方指定 +``` + +```js-exec:default_args.js +Connecting to localhost:8080... +Connecting to 127.0.0.1:8080... +Connecting to example.com:22... +``` + +### Restパラメータ (残余引数) + +引数の数が不定の場合、`...` を使うことで、残りの引数を**配列として**受け取ることができます。以前は `arguments` オブジェクトを使っていましたが、Restパラメータの方が配列メソッド(`map`, `reduce`など)を直接使えるため便利です。 + +```js:rest_params.js +const sum = (...numbers) => { + // numbersは本物の配列 [1, 2, 3, 4, 5] + return numbers.reduce((acc, curr) => acc + curr, 0); +}; + +console.log(sum(1, 2, 3)); +console.log(sum(10, 20, 30, 40, 50)); +``` + +```js-exec:rest_params.js +6 +150 +``` + +## スコープチェーンとレキシカルスコープ + +JavaScriptの変数の有効範囲(スコープ)を理解するために、「レキシカルスコープ」という概念を知る必要があります。 + + * **レキシカルスコープ (Lexical Scope):** 関数が「どこで呼び出されたか」ではなく、**「どこで定義されたか」**によってスコープが決まるというルールです。 + * **スコープチェーン (Scope Chain):** 変数を探す際、現在のスコープになければ、定義時の外側のスコープへと順番に探しに行く仕組みです。 + +```js:scope.js +const globalVar = "Global"; + +function outer() { + const outerVar = "Outer"; + function inner() { + const innerVar = "Inner"; + // innerの中からouterVarとglobalVarが見える(スコープチェーン) + return `${globalVar} > ${outerVar} > ${innerVar}`; + } + return inner(); +} + +console.log(outer()); +``` + +```js-exec:scope.js +Global > Outer > Inner +``` + +## クロージャ:関数が状態を持つ仕組み + +クロージャ (Closure) は、この章の最重要トピックです。 +一言で言えば、**「外側の関数のスコープにある変数を、外側の関数の実行終了後も参照し続ける関数」**のことです。 + +通常、関数(`createCounter`)の実行が終わると、そのローカル変数(`count`)はメモリから破棄されます。しかし、その変数を参照している内部関数(`increment`)が存在し、その内部関数が外部に返された場合、変数は破棄されずに保持され続けます。 + +### クロージャの実例:カウンタ + +プライベートな変数を持つカウンタを作ってみましょう。 + +```js:closure_counter.js +const createCounter = () => { + let count = 0; // この変数は外部から直接アクセスできない(プライベート変数的な役割) + + return () => { + count++; + console.log(`Current count: ${count}`); + }; +}; + +const counterA = createCounter(); // counterA専用のスコープ(環境)が作られる +const counterB = createCounter(); // counterB専用のスコープが別に作られる + +counterA(); // 1 +counterA(); // 2 +counterA(); // 3 + +console.log("--- switching to B ---"); + +counterB(); // 1 (Aの状態とは独立している) +``` + +```js-exec:closure_counter.js +Current count: 1 +Current count: 2 +Current count: 3 +--- switching to B --- +Current count: 1 +``` + +### なぜクロージャを使うのか? + +1. **カプセル化 (Encapsulation):** 変数を隠蔽し、特定の関数経由でしか変更できないようにすることで、予期せぬバグを防ぎます。 +2. **状態の保持:** グローバル変数を使わずに、関数単位で永続的な状態を持てます。 +3. **関数ファクトリ:** 設定の異なる関数を動的に生成する場合に役立ちます。 + +## この章のまとめ + + * **関数定義:** 巻き上げが起こる「関数宣言」と、起こらない「関数式(アロー関数含む)」がある。 + * **アロー関数:** `(args) => body` の形式で記述し、`this` の挙動が従来と異なる。 + * **引数:** デフォルト引数とRestパラメータ(`...args`)で柔軟な引数処理が可能。 + * **レキシカルスコープ:** 関数は「定義された場所」のスコープを記憶する。 + * **クロージャ:** 内部関数が外部関数の変数を参照し続ける仕組み。データの隠蔽や状態保持に使われる。 + +## 練習問題1: アロー関数への書き換え + +以下の関数宣言を、アロー関数 `isEven` に書き換えてください。ただし、省略可能な記号(カッコやreturnなど)は可能な限り省略して最短で記述してください。 + +```js:practice4_1.js +function isEven(n) { + return n % 2 === 0; +} +``` + +```js-exec:practice4_1.js +``` + +### 問題2: クロージャによる掛け算生成器 + +`createMultiplier` という関数を作成してください。この関数は数値 `x` を引数に取り、呼び出すたびに「引数を `x` 倍して返す関数」を返します。 + +**使用例:** + +```js:practice4_2.js +// ここに関数を作成 + + +const double = createMultiplier(2); +console.log(double(5)); // 10 + +const triple = createMultiplier(3); +console.log(triple(5)); // 15 +``` + +```js-exec:practice4_2.js +``` diff --git a/public/docs/javascript-5.md b/public/docs/javascript-5.md new file mode 100644 index 0000000..ef3f490 --- /dev/null +++ b/public/docs/javascript-5.md @@ -0,0 +1,229 @@ +# 第5章: 'this'の正体 + +JavaScriptの学習、お疲れ様です。他の言語(Java, C\#, Pythonなど)の経験がある方にとって、JavaScriptの `this` は最も直感に反し、バグの温床となりやすい機能の一つです。 + +多くの言語において `this`(または `self`)は「そのクラスのインスタンス」を指す静的なものですが、**JavaScriptの `this` は「関数がどのように呼ばれたか」によって動的に変化します。** + +この章では、その複雑な挙動を解き明かし、自在にコントロールする方法を学びます。 + +## 1\. 'this' は呼び出し方で決まる + +JavaScriptにおける関数(アロー関数を除く)の `this` は、**「定義された場所」ではなく「呼び出された場所(Call Site)」**によって決定されます。 + +大きく分けて、以下の3つのパターンを理解すれば基本は押さえられます。 + +### パターンA: メソッド呼び出し + +オブジェクトのプロパティとして関数を呼び出した場合(`obj.method()`)、`this` は**ドットの左側のオブジェクト**(レシーバ)になります。これは他の言語のメンバ関数に近い挙動です。 + +### パターンB: 関数呼び出し + +関数を単体で呼び出した場合(`func()`)、`this` は**グローバルオブジェクト**(ブラウザでは `window`、Node.jsでは `global`)になります。 +ただし、**Strict Mode(`"use strict"`)**では、安全のため `undefined` になります。 + +### パターンC: コンストラクタ呼び出し + +`new` キーワードをつけて呼び出した場合、`this` は**新しく生成されたインスタンス**になります(これは第6章、第7章で詳しく扱います)。 + +以下のコードで、同じ関数でも呼び出し方によって `this` が変わる様子を確認しましょう。 + +```js:dynamic-this.js +"use strict"; // Strict Modeを有効化 + +function showThis() { + console.log(`this is: ${this}`); +} + +const person = { + name: "Alice", + show: showThis, + toString: function() { return this.name; } // コンソール出力用に設定 +}; + +// 1. メソッド呼び出し +console.log("--- Method Call ---"); +person.show(); + +// 2. 関数呼び出し(変数に代入してから実行) +console.log("--- Function Call ---"); +const standaloneShow = person.show; +standaloneShow(); +``` + +```js-exec:dynamic-this.js +--- Method Call --- +this is: Alice +--- Function Call --- +this is: undefined +``` + +> **ポイント:** `person.show` を `standaloneShow` に代入した時点で、オブジェクトとの結びつきは失われます。そのため、`standaloneShow()` と実行すると「関数呼び出し」扱いとなり、`this` は `undefined`(非Strict Modeならグローバルオブジェクト)になります。これが「`this` が消える」現象の正体です。 + +## 2\. 'this' を固定する: bind, call, apply + +「関数呼び出し」でも特定のオブジェクトを `this` として扱いたい場合があります。JavaScriptには、`this` を明示的に指定(束縛)するためのメソッドが3つ用意されています。 + +### call と apply + +これらは関数を**即座に実行**します。第一引数に `this` としたいオブジェクトを渡します。 + + * `call(thisArg, arg1, arg2, ...)`: 引数をカンマ区切りで渡す。 + * `apply(thisArg, [argsArray])`: 引数を配列として渡す。 + +```js-repl:1 +> function greet(greeting, punctuation) { return `${greeting}, ${this.name}${punctuation}`; } +undefined +> const user = { name: "Bob" }; +undefined +> // callの使用例 +> greet.call(user, "Hello", "!"); +'Hello, Bob!' +> // applyの使用例 +> greet.apply(user, ["Hi", "?"]); +'Hi, Bob?' +``` + +### bind + +`bind` は関数を実行せず、**`this` を固定した新しい関数**を返します。これは、イベントリスナーやコールバック関数としてメソッドを渡す際に非常に重要です。 + +```js:bind-example.js +const engine = { + type: "V8", + start: function() { + console.log(`Starting ${this.type} engine...`); + } +}; + +// そのまま渡すと this が失われる(関数呼び出しになるため) +const brokenStart = engine.start; +// brokenStart(); // エラー: Cannot read property 'type' of undefined + +// bind で this を engine に固定する +const fixedStart = engine.start.bind(engine); +fixedStart(); +``` + +```js-exec:bind-example.js +Starting V8 engine... +``` + +## 3\. アロー関数と 'this' + +ES2015 (ES6) で導入されたアロー関数は、これまで説明したルールとは全く異なる挙動をします。 + +アロー関数には**独自の `this` が存在しません**。アロー関数内部の `this` は、**その関数が定義されたスコープ(レキシカルスコープ)の `this`** をそのまま参照します。 + +これは、「コールバック関数内で `this` が変わってしまう問題」を解決するのに最適です。 + +### 従来の関数 vs アロー関数 + +`setTimeout` などのコールバック内でメソッドを使いたい場面を比較してみましょう。 + +```js:arrow-vs-function.js +class Timer { + constructor() { + this.seconds = 0; + } + + // 従来の方法: 失敗例 + startLegacy() { + setTimeout(function() { + // ここでの this はグローバルまたはundefined(setTimeoutの仕様) + // そのため this.seconds にアクセスできずNaNなどになる + try { + this.seconds++; + console.log("Legacy:", this.seconds); + } catch (e) { + console.log("Legacy: Error -", e.message); + } + }, 100); + } + + // アロー関数: 成功例 + startModern() { + setTimeout(() => { + // アロー関数は定義時のスコープ(startModern内のthis = インスタンス)を捕獲する + this.seconds++; + console.log("Modern:", this.seconds); + }, 100); + } +} + +const timer = new Timer(); +timer.startLegacy(); +timer.startModern(); +``` + +```js-exec:arrow-vs-function.js +Legacy: Error - Cannot read properties of undefined (reading 'seconds') +Modern: 1 +``` + +> **注意:** アロー関数は便利な反面、`this` を動的に変更することができません(`call` や `bind` を使っても無視されます)。そのため、動的なコンテキストが必要な場合(例:オブジェクトのメソッド定義そのものや、ライブラリ等で `this` を注入される場合)には通常の関数式を使います。 + +# この章のまとめ + +JavaScriptの `this` は、他の静的な言語とは異なり「呼び出し時」に解決されます。 + +1. **メソッド呼び出し (`obj.method()`)**: `this` は `obj`。 +2. **関数呼び出し (`func()`)**: `this` は `undefined` (Strict Mode) またはグローバルオブジェクト。 +3. **明示的な指定**: `call`, `apply` で一時的に、`bind` で永続的に `this` を指定可能。 +4. **アロー関数**: 独自の `this` を持たず、外側のスコープの `this` をそのまま使う(レキシカルスコープ)。 + +次の章では、この `this` を活用してオブジェクト指向プログラミングの核心である「オブジェクトとプロトタイプ」について学びます。 + +# 練習問題1: 失われたコンテキストの修復 + +以下のコードは、ボタンクリック時(ここではシミュレーション)にユーザー名を表示しようとしていますが、エラーになります。 + +1. `bind` を使って修正してください。 +2. `greet` メソッド自体をアロー関数に変更するアプローチではなく、呼び出し側を修正する形で解答してください。 + +```js:practice5_1.js +const user = { + name: "Tanaka", + greet: function() { + console.log(`Hello, ${this.name}`); + } +}; + +// クリックイベントのシミュレーター(変更不可) +function simulateClick(callback) { + // 内部で単なる関数呼び出しとして実行される + callback(); +} + +// --- 以下を修正してください --- +simulateClick(user.greet); +``` + +```js-exec:practice5_1.js +``` + +### 問題2: アロー関数の特性 + +以下の `calculator` オブジェクトにはバグがあります。`multiply` メソッドが正しい結果(配列の各要素を `factor` 倍する)を返すように修正してください。 +ヒント:`map` の中のコールバック関数に注目してください。 + +```js:practice5_2.js +const calculator = { + factor: 2, + numbers: [1, 2, 3], + multiply: function() { + return this.numbers.map(function(n) { + // ここで this.factor が読めない! + return n * this.factor; + }); + } +}; + +try { + console.log(calculator.multiply()); +} catch(e) { + console.log("Error:", e.message); +} +``` + +```js-exec:practice5_2.js +``` diff --git a/public/docs/javascript-6.md b/public/docs/javascript-6.md new file mode 100644 index 0000000..2ab1029 --- /dev/null +++ b/public/docs/javascript-6.md @@ -0,0 +1,250 @@ +# 第6章: オブジェクトとプロトタイプ + +他の言語(Java, C\#, Pythonなど)の経験がある方にとって、JavaScriptの「オブジェクト」と「継承」のモデルは最も混乱しやすい部分の一つです。JavaScriptはクラスベースではなく、**プロトタイプベース**のオブジェクト指向言語です。 + +本章では、ES6(ECMAScript 2015)以降の`class`構文(第7章で扱います)の裏側で実際に何が起きているのか、その仕組みの根幹である「プロトタイプチェーン」について解説します。 + +## オブジェクトリテラルとプロパティ + +JavaScriptにおけるオブジェクトは、基本的にはキー(プロパティ名)と値のコレクション(連想配列やハッシュマップに近いもの)です。最も一般的な生成方法は**オブジェクトリテラル** `{...}` を使うことです。 + +```js-repl:1 +> const book = { +... title: "JavaScript Primer", +... "page-count": 350, // ハイフンを含むキーは引用符が必要 +... author: { +... name: "John Doe", +... age: 30 +... } +... }; +undefined +> book.title +'JavaScript Primer' +> book["page-count"] // 識別子として無効な文字を含む場合はブラケット記法 +350 +> book.author.name +'John Doe' +``` + +### プロパティの追加・削除 + +動的な言語であるJavaScriptでは、オブジェクト作成後にプロパティを追加・削除できます。 + +```js-repl:2 +> const config = { env: "production" }; +undefined +> config.port = 8080; // 追加 +8080 +> delete config.env; // 削除 +true +> config +{ port: 8080 } +``` + +## メソッドと this(復習) + +オブジェクトのプロパティには関数も設定できます。これを**メソッド**と呼びます。 +第5章で学んだ通り、メソッド呼び出しにおける `this` は、「ドットの左側にあるオブジェクト(レシーバ)」を指します。 + +```js-repl:3 +> const counter = { +... count: 0, +... increment: function() { +... this.count++; +... return this.count; +... }, +... // ES6からの短縮記法(推奨) +... reset() { +... this.count = 0; +... } +... }; +undefined +> counter.increment(); +1 +> counter.increment(); +2 +> counter.reset(); +undefined +> counter.count +0 +``` + +## プロトタイプとは何か? + +ここからが本章の核心です。JavaScriptのすべてのオブジェクトは、自身の親となる別のオブジェクトへの隠されたリンクを持っています。このリンク先のオブジェクトを**プロトタイプ**と呼びます。 + +オブジェクトからプロパティを読み取ろうとしたとき、そのオブジェクト自身がプロパティを持っていなければ、JavaScriptエンジンは自動的にプロトタイプを探しに行きます。 + +### `__proto__` と `Object.getPrototypeOf` + +歴史的経緯により、多くのブラウザで `obj.__proto__` というプロパティを通じてプロトタイプにアクセスできますが、現在の標準的な方法は `Object.getPrototypeOf(obj)` です。 + +```js-repl:4 +> const arr = [1, 2, 3]; +undefined +> // 配列の実体はオブジェクトであり、Array.prototypeを継承している +> Object.getPrototypeOf(arr) === Array.prototype +true +> // Array.prototypeの親はObject.prototype +> Object.getPrototypeOf(Array.prototype) === Object.prototype +true +> // Object.prototypeの親はnull(チェーンの終端) +> Object.getPrototypeOf(Object.prototype) +null +``` + +## プロトタイプチェーンによる継承の仕組み + +あるオブジェクトのプロパティにアクセスした際、JavaScriptは以下の順序で探索を行います。 + +1. そのオブジェクト自身(Own Property)が持っているか? +2. 持っていなければ、そのオブジェクトのプロトタイプが持っているか? +3. それでもなければ、プロトタイプのプロトタイプが持っているか? +4. `null` に到達するまで繰り返し、見つからなければ `undefined` を返す。 + +この連鎖を**プロトタイプチェーン**と呼びます。クラス継承のように型定義をコピーするのではなく、**リンクを辿って委譲(Delegation)する**仕組みです。 + +以下のコードで、具体的な動作を確認してみましょう。 + +```js:prototype_chain.js +const animal = { + eats: true, + walk() { + console.log("Animal walks"); + } +}; + +const rabbit = { + jumps: true, + __proto__: animal // 注意: __proto__への代入は学習目的以外では非推奨 +}; + +const longEar = { + earLength: 10, + __proto__: rabbit +}; + +// 1. longEar自身は walk を持っていない -> rabbitを見る +// 2. rabbitも walk を持っていない -> animalを見る +// 3. animal が walk を持っている -> 実行 +longEar.walk(); + +// 自身のプロパティ +console.log(`Jumps? ${longEar.jumps}`); // rabbitから取得 +console.log(`Eats? ${longEar.eats}`); // animalから取得 + +// プロパティの追加(シャドーイング) +// longEar自身に walk を追加すると、animalの walk は隠蔽される +longEar.walk = function() { + console.log("LongEar walks simply"); +}; + +longEar.walk(); +``` + +```js-exec:prototype_chain.js +Animal walks +Jumps? true +Eats? true +LongEar walks simply +``` + +## Object.create() とコンストラクタ関数 + +`__proto__` を直接操作するのはパフォーマンスや標準化の観点から推奨されません。プロトタイプを指定してオブジェクトを生成する正しい方法は2つあります。 + +### 1\. Object.create() + +指定したオブジェクトをプロトタイプとする新しい空のオブジェクトを生成します。 + +```js-repl:5 +> const proto = { greet: function() { return "Hello"; } }; +undefined +> const obj = Object.create(proto); +undefined +> obj.greet(); +'Hello' +> Object.getPrototypeOf(obj) === proto +true +``` + +### 2\. コンストラクタ関数(new演算子) + +ES6の `class` が登場する前、JavaScriptでは関数をコンストラクタとして使用し、`new` 演算子を使ってインスタンスを生成していました。これは現在でも多くのライブラリの内部で使用されている重要なパターンです。 + + * 関数オブジェクトは `prototype` という特別なプロパティを持っています(`__proto__`とは別物です)。 + * `new Func()` すると、作られたインスタンスの `__proto__` に `Func.prototype` がセットされます。 + +```js:constructor_pattern.js +// コンストラクタ関数(慣習として大文字で始める) +function User(name) { + // this = {} (新しい空のオブジェクトが暗黙的に生成される) + this.name = name; + // return this (暗黙的にこのオブジェクトが返される) +} + +// すべてのUserインスタンスで共有したいメソッドは +// User.prototype に定義する(メモリ節約のため) +User.prototype.sayHi = function() { + console.log(`Hi, I am ${this.name}`); +}; + +const user1 = new User("Alice"); +const user2 = new User("Bob"); + +user1.sayHi(); +user2.sayHi(); + +// 仕組みの確認 +console.log(user1.__proto__ === User.prototype); // true +console.log(user1.sayHi === user2.sayHi); // true (同じ関数を共有している) +``` + +```js-exec:constructor_pattern.js +Hi, I am Alice +Hi, I am Bob +true +true +``` + +> **重要な区別:** +> +> * `obj.__proto__`: オブジェクトの実の親(リンク先)。 +> * `Func.prototype`: その関数を `new` したときに、生成されるインスタンスの `__proto__` に代入される**テンプレート**。 + +## この章のまとめ + +1. JavaScriptはクラスベースではなく、**プロトタイプベース**の継承を行う。 +2. オブジェクトは隠しプロパティ(`[[Prototype]]`)を持ち、プロパティが見つからない場合にそこを探索する(プロトタイプチェーン)。 +3. `Object.create(proto)` は、特定のプロトタイプを持つオブジェクトを直接生成する。 +4. コンストラクタ関数と `new` 演算子を使うと、`Func.prototype` を親に持つインスタンスを生成できる。これがJavaなどの「クラス」に近い振る舞いを模倣する仕組みである。 + +## 練習問題1: 基本的なプロトタイプ継承 + +`Object.create()` を使用して、以下の要件を満たすコードを書いてください。 + +1. `robot` オブジェクトを作成し、`battery: 100` というプロパティと、バッテリーを10減らして残量を表示する `work` メソッドを持たせる。 +2. `robot` をプロトタイプとする `cleaningRobot` オブジェクトを作成する。 +3. `cleaningRobot` 自身に `type: "cleaner"` というプロパティを追加する。 +4. `cleaningRobot.work()` を呼び出し、正しく動作(プロトタイプチェーンの利用)を確認する。 + +```js:practice6_1.js +``` + +```js-exec:practice6_1.js +``` + +### 練習問題2: コンストラクタ関数 + +コンストラクタ関数 `Item` を作成してください。 + +1. `Item` は引数 `name` と `price` を受け取り、プロパティとして保持する。 +2. `Item.prototype` に `getTaxIncludedPrice` メソッドを追加する。これは税率10%を加えた価格を返す。 +3. `new Item("Apple", 100)` でインスタンスを作成し、税込価格が110になることを確認する。 + +```js:practice6_2.js +``` + +```js-exec:practice6_2.js +``` + diff --git a/public/docs/javascript-7.md b/public/docs/javascript-7.md new file mode 100644 index 0000000..3bd0dbd --- /dev/null +++ b/public/docs/javascript-7.md @@ -0,0 +1,235 @@ +# 第7章: クラス構文 (ES6+) + +前章では、JavaScriptのオブジェクト指向の核心である「プロトタイプ」について学びました。他の言語(Java, C\#, Pythonなど)の経験者にとって、プロトタイプチェーンによる継承は柔軟ですが、少し直感的ではない部分もあったかもしれません。 + +ES6 (ECMAScript 2015) から導入された **`class` 構文** は、プロトタイプベースの継承メカニズムを隠蔽し、一般的なクラスベースのオブジェクト指向言語に近い記述を可能にするものです。これを「糖衣構文(Syntactic Sugar)」と呼びます。 + +この章では、現代のJavaScript開発で標準となっているクラスの定義方法、継承、そして比較的新しい機能であるプライベートフィールドについて解説します。 + +## クラスの定義とコンストラクタ + +JavaScriptのクラスは `class` キーワードを使って定義します。初期化処理は `constructor` という特別なメソッド内で行います。 + +基本的に、クラス定義の内部は自動的に **Strict Mode (`'use strict'`)** で実行されます。 + +```js-repl:1 +> class User { +... constructor(name, age) { +... this.name = name; +... this.age = age; +... } +... } +undefined +> const user1 = new User("Alice", 30); +undefined +> user1.name +'Alice' +> typeof User // クラスの実態は関数 +'function' +``` + +### クラス式 + +関数と同様に、クラスも式として変数に代入できます(あまり頻繁には使われませんが、知識として持っておくと良いでしょう)。 + +```js-repl:2 +> const Item = class { +... constructor(price) { +... this.price = price; +... } +... }; +undefined +> new Item(100).price +100 +``` + +## メソッド、ゲッター、セッター + +クラス構文の中では、プロトタイプへのメソッド定義を簡潔に書くことができます。`function` キーワードは不要です。また、プロパティへのアクセスを制御するゲッター (`get`) とセッター (`set`) も直感的に記述できます。 + +```js:rectangle.js +class Rectangle { + constructor(width, height) { + this.width = width; + this.height = height; + } + + // 通常のメソッド(プロトタイプメソッドになります) + calcArea() { + return this.width * this.height; + } + + // ゲッター: プロパティのようにアクセス可能 + get description() { + return `${this.width} x ${this.height} Rectangle`; + } + + // セッター: 値の検証などに利用 + set width(value) { + if (value <= 0) { + console.log("幅は0より大きくある必要があります"); + return; + } + this._width = value; + } + + get width() { + return this._width; + } +} + +const rect = new Rectangle(10, 20); + +console.log(rect.calcArea()); // メソッド呼び出し +console.log(rect.description); // ゲッター呼び出し(()は不要) + +rect.width = -5; // セッターによるバリデーション +rect.width = 15; +console.log(rect.calcArea()); +``` + +```js-exec:rectangle.js +200 +10 x 20 Rectangle +幅は0より大きくある必要があります +300 +``` + +> **Note:** セッター内で `this.width = value` とすると無限再帰になるため、慣習的に内部プロパティには `_`(アンダースコア)を付けることがよくありましたが、現在は後述するプライベートフィールド(`#`)を使うのがモダンな方法です。 + +## 継承 (extends と super) + +他の言語同様、`extends` キーワードを使用して既存のクラスを継承できます。親クラスのコンストラクタやメソッドには `super` を使ってアクセスします。 + +ここで重要なルールが1つあります。**子クラスの `constructor` 内では、`this` を使用する前に必ず `super()` を呼び出す必要があります。** + +```js:inheritance.js +class Animal { + constructor(name) { + this.name = name; + } + + speak() { + return `${this.name} makes a noise.`; + } +} + +class Dog extends Animal { + constructor(name, breed) { + // thisを使う前に親のコンストラクタを呼ぶ必須ルール + super(name); + this.breed = breed; + } + + // メソッドのオーバーライド + speak() { + // 親クラスのメソッド呼び出し + const parentSound = super.speak(); + return `${parentSound} But specifically, ${this.name} barks!`; + } +} + +const d = new Dog("Pochi", "Shiba"); +console.log(d.speak()); +console.log(d instanceof Dog); // true +console.log(d instanceof Animal); // true +``` + +```js-exec:inheritance.js +Pochi makes a noise. But specifically, Pochi barks! +true +true +``` + +## 静的メソッド (static) とプライベートフィールド (\#) + +### 静的メソッド (static) + +インスタンスではなく、クラス自体に紐付くメソッドです。ユーティリティ関数やファクトリーメソッドによく使われます。 + +### プライベートフィールド (\#) + +長らくJavaScriptには「真のプライベートプロパティ」が存在せず、`_variable` のような命名規則に頼っていました。しかし、ES2019以降、`#` をプレフィックスにすることで、**クラス外から完全にアクセス不可能なフィールド**を定義できるようになりました。 + +```js:private_static.js +class BankAccount { + // プライベートフィールドの宣言 + #balance; + + constructor(initialBalance) { + this.#balance = initialBalance; + } + + deposit(amount) { + if (amount > 0) { + this.#balance += amount; + console.log(`Deposited: ${amount}`); + } + } + + getBalance() { + return this.#balance; + } + + // 静的メソッド + static createZeroAccount() { + return new BankAccount(0); + } +} + +const account = BankAccount.createZeroAccount(); +account.deposit(1000); + +console.log(`Current Balance: ${account.getBalance()}`); + +// 外部からのアクセスを試みると、 Syntax Error になる +// console.log(account.#balance); + +// 従来のプロパティアクセスのように見えても... +console.log(account.balance); // undefined +``` + +```js-exec:private_static.js +Deposited: 1000 +Current Balance: 1000 +undefined +``` + +## この章のまとめ + +1. **`class` 構文** はプロトタイプ継承の糖衣構文であり、`constructor` で初期化を行います。 +2. **メソッド定義** は `function` キーワードが不要で、`get` / `set` でアクセサを定義できます。 +3. **継承** は `extends` を使い、子クラスのコンストラクタ内では必ず `this` に触れる前に `super()` を呼ぶ必要があります。 +4. **`static`** で静的メソッドを、**`#`** プレフィックスでハードプライベートフィールド(外部からアクセス不可)を定義できます。 + +クラス構文を使うことで、コードの構造がより明確になり、他の言語の経験者にとっても読みやすいコードになります。しかし、裏側ではプロトタイプチェーンが動いていることを忘れないでください。 + +### 練習問題 1: シンプルなRPGキャラクター + +以下の仕様を満たす `Character` クラスを作成してください。 + + * `name` (名前) と `hp` (体力) をコンストラクタで受け取る。 + * `attack(target)` メソッドを持つ。実行すると `target` の `hp` を 10 減らし、コンソールに攻撃メッセージを表示する。 + * `hp` はプライベートフィールド (`#hp`) として管理し、0未満にならないようにする。現在のHPを取得するゲッター `hp` を用意する。 + +```js:practice7_1.js +``` + +```js-exec:practice7_1.js +``` + + +### 練習問題 2: 図形の継承 + +以下の仕様を満たすクラスを作成してください。 + + * 親クラス `Shape`: コンストラクタで `color` を受け取る。`info()` メソッドを持ち、「色: [color]」を返す。 + * 子クラス `Circle`: `Shape` を継承。コンストラクタで `color` と `radius` (半径) を受け取る。`info()` メソッドをオーバーライドし、「[親のinfo], 半径: [radius]」を返す。 + * それぞれのインスタンスを作成し、`info()` の結果を表示する。 + +```js:practice7_2.js +``` + +```js-exec:practice7_2.js +``` + diff --git a/public/docs/javascript-8.md b/public/docs/javascript-8.md new file mode 100644 index 0000000..5cb6ef4 --- /dev/null +++ b/public/docs/javascript-8.md @@ -0,0 +1,251 @@ +# 第8章: 配列とイテレーション + +他の言語経験者にとって、JavaScriptの配列は「動的配列」や「リスト」、「ベクター」に近い存在です。サイズは可変であり、異なるデータ型を混在させることも可能です(通常は同じ型で統一しますが)。 + +本章では、基本的な操作から、モダンなJavaScript開発において必須となる「宣言的なデータ処理」(`map`, `filter`, `reduce`など)に焦点を当てます。従来の`for`ループよりもこれらのメソッドが好まれる理由と使い方を習得しましょう。 + +## 配列リテラルと基本的な操作 + +JavaScriptの配列は`Array`オブジェクトですが、通常はリテラル `[]` を使用して生成します。 +基本的な操作として、スタック操作(`push`, `pop`)やキュー操作に近いこと(`shift`, `unshift`)、そして万能な要素操作メソッド`splice`があります。 + +### 基本操作 (REPL) + +```js-repl:1 +> const fruits = ['Apple', 'Banana']; +undefined +> // 末尾に追加 (push) +> fruits.push('Orange'); +3 +> fruits +[ 'Apple', 'Banana', 'Orange' ] + +> // 末尾から削除 (pop) +> const last = fruits.pop(); +undefined +> last +'Orange' + +> // 先頭に追加 (unshift) +> fruits.unshift('Grape'); +3 +> fruits +[ 'Grape', 'Apple', 'Banana' ] + +> // インデックスによるアクセス +> fruits[1] +'Apple' +``` + +### 破壊的な操作: splice + +`splice`は要素の削除、置換、挿入をすべて行える強力なメソッドですが、**元の配列を変更(破壊)する**点に注意が必要です。 + +```js-repl:2 +> const numbers = [1, 2, 3, 4, 5]; +undefined +> // インデックス1から、2つの要素を削除し、そこに99, 100を挿入 +> numbers.splice(1, 2, 99, 100); +[ 2, 3 ] +> numbers +[ 1, 99, 100, 4, 5 ] +``` + +## スプレッド構文 (...) とデストラクチャリング(分割代入) + +モダンJavaScript(ES2015+)では、配列の操作をより簡潔に記述するための構文が導入されました。これらはReactやVueなどのフレームワークでも多用されます。 + +### スプレッド構文 (...) + +配列を展開する構文です。配列の結合や、\*\*浅いコピー(Shallow Copy)\*\*の作成によく使われます。 + +```js-repl:3 +> const part1 = [1, 2]; +undefined +> const part2 = [3, 4]; +undefined +> // 配列の結合(新しい配列を作成) +> const combined = [...part1, ...part2]; +undefined +> combined +[ 1, 2, 3, 4 ] + +> // 配列のコピー(新しい参照を作成) +> const copy = [...part1]; +undefined +> copy === part1 +false +``` + +### デストラクチャリング(分割代入) + +配列から要素を取り出して変数に代入する操作を簡潔に書くことができます。 + +```js-repl:4 +> const users = ['Alice', 'Bob', 'Charlie']; +undefined +> // 1つ目と2つ目の要素を変数に代入 +> const [first, second] = users; +undefined +> first +'Alice' +> second +'Bob' + +> // 3つ目だけを取り出す(最初の2つはスキップ) +> const [, , third] = users; +undefined +> third +'Charlie' + +> // 変数の値を入れ替える(スワップ)テクニック +> let a = 1; +> let b = 2; +> [a, b] = [b, a]; +[ 2, 1 ] +> a +2 +``` + +## 高階関数によるイテレーション + +JavaScriptでは、`for`文や`while`文を書く頻度は減っています。代わりに、配列のメソッドとして提供される**高階関数**(関数を引数に取る関数)を使用して、処理の意図(変換、抽出、集約など)を明確にします。 + +主なメソッドは以下の通りです。 + + * **`forEach`**: 単なる反復処理(戻り値なし)。副作用(ログ出力やDB保存など)を起こすために使う。 + * **`map`**: 全要素を変換し、**新しい配列**を返す。 + * **`filter`**: 条件に一致する要素のみを抽出し、**新しい配列**を返す。 + * **`reduce`**: 要素を一つずつ処理して、**単一の値**(合計、オブジェクトなど)に集約する。 + +以下は、これらのメソッドを使って商品リストを処理するスクリプトです。 + +```js:shopping_cart.js +const cart = [ + { id: 1, name: 'Laptop', price: 1000, category: 'Electronics' }, + { id: 2, name: 'Mouse', price: 25, category: 'Electronics' }, + { id: 3, name: 'Coffee', price: 5, category: 'Food' }, + { id: 4, name: 'Keyboard', price: 100, category: 'Electronics' }, +]; + +console.log('--- 1. map: 商品名のリストを作成 ---'); +const itemNames = cart.map(item => item.name); +console.log(itemNames); + +console.log('\n--- 2. filter: 電子機器(Electronics)のみ抽出 ---'); +const electronics = cart.filter(item => item.category === 'Electronics'); +console.log(electronics); + +console.log('\n--- 3. reduce: 合計金額を計算 ---'); +// 第2引数の 0 はアキュムレータ(sum)の初期値 +const totalPrice = cart.reduce((sum, item) => sum + item.price, 0); +console.log(`Total: $${totalPrice}`); + +console.log('\n--- 4. メソッドチェーン(組み合わせ) ---'); +// 電子機器の価格のみを抽出して合計する +const electronicsTotal = cart + .filter(item => item.category === 'Electronics') + .map(item => item.price) + .reduce((acc, price) => acc + price, 0); + +console.log(`Electronics Total: $${electronicsTotal}`); +``` + +実行結果: + +```js-exec:shopping_cart.js +--- 1. map: 商品名のリストを作成 --- +[ 'Laptop', 'Mouse', 'Coffee', 'Keyboard' ] + +--- 2. filter: 電子機器(Electronics)のみ抽出 --- +[ + { id: 1, name: 'Laptop', price: 1000, category: 'Electronics' }, + { id: 2, name: 'Mouse', price: 25, category: 'Electronics' }, + { id: 4, name: 'Keyboard', price: 100, category: 'Electronics' } +] + +--- 3. reduce: 合計金額を計算 --- +Total: $1130 + +--- 4. メソッドチェーン(組み合わせ) --- +Electronics Total: $1125 +``` + +> **Note:** `map`や`filter`は元の配列を変更せず(イミュータブル)、新しい配列を返します。これにより、予期せぬ副作用を防ぐことができます。 + +## その他の便利なメソッド:find, some, every + +特定の要素を探したり、条件チェックを行ったりする場合に特化したメソッドです。これらもコールバック関数を受け取ります。 + + * **`find`**: 最初に見つかった要素自体を返す(見つからなければ `undefined`)。 + * **`findIndex`**: 最初に見つかった要素のインデックスを返す(見つからなければ `-1`)。 + * **`some`**: 条件を満たす要素が**一つでもあれば** `true` を返す。 + * **`every`**: **すべての要素**が条件を満たせば `true` を返す。 + +```js-repl:5 +> const scores = [85, 92, 45, 78, 90]; +undefined + +> // 最初の合格者(80点以上)を探す +> const starStudent = scores.find(score => score >= 90); +undefined +> starStudent +92 + +> // 赤点(50点未満)があるか? (some) +> const hasFailure = scores.some(score => score < 50); +undefined +> hasFailure +true + +> // 全員が合格(40点以上)か? (every) +> const allPassed = scores.every(score => score >= 40); +undefined +> allPassed +true +``` + +## この章のまとめ + + * 配列は動的で、`push`/`pop`などのメソッドで伸縮可能です。 + * `splice`は配列を直接変更(破壊)するため、使用には注意が必要です。 + * スプレッド構文 `...` と分割代入を使うと、配列のコピー、結合、要素の抽出が宣言的に記述できます。 + * ループ処理には `for` 文よりも高階関数(`map`, `filter`, `reduce`)を使用することが推奨されます。これらは処理の意図を明確にし、メソッドチェーンによる可読性の向上に寄与します。 + * 検索や検証には `find`, `some`, `every` を活用しましょう。 + +### 練習問題 1: データの加工 + +以下の数値配列があります。 +`const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];` + +この配列に対して、以下の処理を行うコードを書いてください(メソッドチェーンを使用しても構いません)。 + +1. 偶数のみを取り出す。 +2. 取り出した偶数をそれぞれ2乗する。 +3. 結果の配列をコンソールに表示する。 + +```js:practice8_1.js +``` + +```js-exec:practice8_1.js +``` + + +### 練習問題 2: 集計処理 + +以下のユーザーリストから「アクティブ(`active: true`)なユーザーの年齢の平均値」を計算して表示してください。 +(ヒント: まずアクティブなユーザーを絞り込み、次に年齢の合計と人数を使って平均を算出します) + +```js:practice8_2.js +const users = [ + { name: 'Alice', age: 25, active: true }, + { name: 'Bob', age: 30, active: false }, + { name: 'Charlie', age: 35, active: true }, + { name: 'Dave', age: 40, active: false } +]; + +``` + +```js-exec:practice8_2.js +``` + diff --git a/public/docs/javascript-9.md b/public/docs/javascript-9.md new file mode 100644 index 0000000..3bb753f --- /dev/null +++ b/public/docs/javascript-9.md @@ -0,0 +1,216 @@ +# 第9章: 非同期処理(1)- Promise + +JavaScriptは基本的にシングルスレッドで動作します。つまり、一度に一つの処理しか実行できません。しかし、ネットワークリクエストやタイマーなどの重い処理を行っている間、ブラウザがフリーズしたりサーバーが応答しなくなったりしては困ります。 + +そこでJavaScriptは、重い処理をバックグラウンド(Web APIsやNode.jsのC++レイヤー)に任せ、完了通知を受け取ることで並行処理のような動きを実現しています。 + +本章では、JavaScriptの非同期処理の基盤となるメカニズムと、それを現代的に扱うための標準APIである **Promise** について解説します。 + +## 同期処理 vs 非同期処理 + +まず、挙動の違いを確認しましょう。 + + * **同期処理 (Synchronous):** コードが上から下へ順番に実行されます。前の処理が終わるまで次の処理は待たされます(ブロッキング)。 + * **非同期処理 (Asynchronous):** 処理の完了を待たずに、即座に次のコードへ進みます(ノンブロッキング)。処理結果は後でコールバックなどを通じて受け取ります。 + +以下のコードは、`setTimeout`(非同期API)を使用した例です。他言語の経験者であれば、「Start」→「1秒待機」→「Timer」→「End」と予想するかもしれませんが、JavaScriptでは異なります。 + +```js:async_demo.js +console.log('1. Start'); + +// 1000ミリ秒後にコールバックを実行する非同期関数 +setTimeout(() => { + console.log('2. Timer fired'); +}, 1000); + +console.log('3. End'); +``` + +```js-exec:async_demo.js +1. Start +3. End +2. Timer fired +``` + +`setTimeout` は「タイマーをセットする」という命令だけを出し、即座に制御を返します。そのため、タイマーの発火を待たずに `3. End` が出力されます。 + +## イベントループとコールバックキューの仕組み + +なぜシングルスレッドで非同期処理が可能なのか、その裏側にあるのが **イベントループ (Event Loop)** という仕組みです。 + +JavaScriptのランタイムは主に以下の要素で構成されています: + +1. **コールスタック (Call Stack):** 現在実行中の関数が積まれる場所。LIFO(後入れ先出し)。 +2. **Web APIs / Node APIs:** ブラウザやOSが提供する機能(タイマー、Fetch、DOMイベントなど)。非同期処理はここで実行されます。 +3. **コールバックキュー (Callback Queue):** 非同期処理が完了した後、実行待ちのコールバック関数が並ぶ列。 +4. **イベントループ (Event Loop):** コールスタックとキューを監視する仕組み。 + +**処理の流れ:** + +1. `setTimeout` がコールスタックで実行されると、ブラウザのタイマーAPIに処理を依頼し、スタックから消えます。 +2. 指定時間が経過すると、タイマーAPIはコールバック関数を **コールバックキュー** に入れます。 +3. **イベントループ** は、「コールスタックが空(メインの処理が完了)」かつ「キューにタスクがある」場合、キューからタスクを取り出してコールスタックへ移動させます。 + +この仕組みにより、メインスレッドをブロックすることなく非同期処理を実現しています。 + +## コールバック地獄の問題点 + +Promiseが登場する以前(ES5時代まで)は、非同期処理の順序制御を行うために、コールバック関数を入れ子にする手法が一般的でした。 + +例えば、「処理Aが終わったら処理B、その後に処理C...」というコードを書こうとすると、以下のようにネストが深くなります。 + +```js:callback_hell.js +function delay(ms, callback) { + setTimeout(callback, ms); +} + +console.log('Start'); + +delay(1000, () => { + console.log('Step 1 finished'); + + delay(1000, () => { + console.log('Step 2 finished'); + + delay(1000, () => { + console.log('Step 3 finished'); + console.log('End'); + }); + }); +}); +``` + +```js-exec:callback_hell.js +Start +Step 1 finished +Step 2 finished +Step 3 finished +End +``` + +これはいわゆる **「コールバック地獄 (Callback Hell)」** と呼ばれる状態で、可読性が低く、エラーハンドリングも困難です。これを解決するために導入されたのが **Promise** です。 + +## Promiseの概念 + +**Promise** は、非同期処理の「最終的な完了(または失敗)」とその「結果の値」を表すオブジェクトです。未来のある時点で値が返ってくる「約束手形」のようなものと考えてください。 + +Promiseオブジェクトは以下の3つの状態のいずれかを持ちます。 + +1. **Pending (待機中):** 初期状態。処理はまだ完了していない。 +2. **Fulfilled (履行):** 処理が成功し、値を持っている状態。(`resolve` された) +3. **Rejected (拒否):** 処理が失敗し、エラー理由を持っている状態。(`reject` された) + +Promiseの状態は一度 Pending から Fulfilled または Rejected に変化すると、二度と変化しません(Immutable)。 + +## Promiseの使い方 + +### Promiseの作成 + +`new Promise` コンストラクタを使用します。引数には `(resolve, reject)` を受け取る関数(Executor)を渡します。 + +```js-repl:1 +> const myPromise = new Promise((resolve, reject) => { +... // ここで非同期処理を行う +... const success = true; +... if (success) { +... resolve("OK!"); // 成功時 +... } else { +... reject(new Error("Failed")); // 失敗時 +... } +... }); +undefined +> myPromise +Promise { 'OK!' } +``` + +### .then(), .catch(), .finally() + +Promiseオブジェクトの結果を受け取るには、以下のメソッドを使用します。 + + * **`.then(onFulfilled)`**: PromiseがFulfilledになった時に実行されます。 + * **`.catch(onRejected)`**: PromiseがRejectedになった時に実行されます。 + * **`.finally(onFinally)`**: 成功・失敗に関わらず、処理終了時に実行されます。 + +先ほどのコールバック地獄の例を、Promiseを使って書き直してみましょう。 + +```js:promise_chain.js +// Promiseを返す関数を作成 +function delay(ms) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(`Waited ${ms}ms`); + }, ms); + }); +} + +console.log('Start'); + +delay(1000) + .then((message) => { + console.log('Step 1:', message); + // 次のPromiseを返すことでチェーンをつなぐ + return delay(1000); + }) + .then((message) => { + console.log('Step 2:', message); + return delay(1000); + }) + .then((message) => { + console.log('Step 3:', message); + console.log('End'); + }) + .catch((error) => { + // チェーンのどこかでエラーが起きればここに飛ぶ + console.error('Error:', error); + }); +``` + +```js-exec:promise_chain.js +Start +Step 1: Waited 1000ms +Step 2: Waited 1000ms +Step 3: Waited 1000ms +End +``` + +**重要なポイント:** + +1. `.then()` の中で新しい Promise を返すと、次の `.then()` はその新しい Promise の完了を待ちます。これにより、非同期処理を **フラットな連鎖** として記述できます。 +2. エラー処理は最後の `.catch()` に集約できます。`try-catch` ブロックに近い感覚で扱えるようになります。 + +## この章のまとめ + + * JavaScriptはシングルスレッドで動作し、**イベントループ** という仕組みを使って非同期処理を管理しています。 + * 非同期処理の完了を待つために、昔はコールバック関数が多用されていましたが、ネストが深くなる問題がありました。 + * **Promise** は非同期処理の状態(Pending, Fulfilled, Rejected)を管理するオブジェクトです。 + * `.then()` をチェーンさせることで、非同期処理を直列に、読みやすく記述できます。 + * エラーハンドリングは `.catch()` で一括して行えます。 + +次章では、このPromiseをさらに同期処理のように書ける構文糖衣 **async/await** について学びます。 + +## 練習問題 + +### 問題1: ランダムな成功/失敗 + +`Math.random()` を使い、50%の確率で成功(Resolve)、50%の確率で失敗(Reject)するPromiseを返す関数 `coinToss` を作成してください。 +それを使用し、成功時は "Win\!"、失敗時は "Lose..." とコンソールに表示するコードを書いてください。 + +```js:practice9_1.js +``` + +```js-exec:practice9_1.js +``` + +### 問題2: 擬似的なデータ取得フロー + +以下の仕様を満たすコードを作成してください。 + +1. 関数 `fetchUser(userId)`: 1秒後に `{ id: userId, name: "User" + userId }` というオブジェクトでresolveする。 +2. 関数 `fetchPosts(userName)`: 1秒後に `["Post 1 by " + userName, "Post 2 by " + userName]` という配列でresolveする。 +3. これらをPromiseチェーンで繋ぎ、ユーザーID `1` でユーザーを取得した後、その名前を使って投稿を取得し、最終的に投稿リストをコンソールに表示してください。 + +```js:practice9_2.js +``` + +```js-exec:practice9_2.js +```