Skip to content

Commit 2d7b756

Browse files
committed
js8章まで & jsEvalの挙動を修正
1 parent a65f1b6 commit 2d7b756

File tree

7 files changed

+782
-19
lines changed

7 files changed

+782
-19
lines changed

app/pagesList.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export const pagesList = [
4949
{id: 3, title: "制御構文"},
5050
{id: 4, title: "関数とクロージャ"},
5151
{id: 5, title: "'this'の正体"},
52+
{id: 6, title: "オブジェクトとプロトタイプ"},
53+
{id: 7, title: "クラス構文"},
54+
{id: 8, title: "配列とイテレーション"},
5255
]
5356
},
5457
{

app/terminal/runtime.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants {
153153
return {
154154
tabSize: 2,
155155
prompt: "> ",
156+
promptMore: "... ",
156157
};
157158
case "c_cpp":
158159
case "cpp":

app/terminal/worker/jsEval.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ function splitReplExamples(content: string): ReplCommand[] {
2424
if (line.startsWith("> ")) {
2525
// Remove the prompt from the command
2626
initCommands.push({ command: line.slice(2), output: [] });
27+
} else if (line.startsWith("... ")) {
28+
if (initCommands.length > 0) {
29+
initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
30+
}
2731
} else {
2832
// Lines without prompt are output from the previous command
2933
// and the last output is return value

app/terminal/worker/jsEval.worker.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,25 @@
22

33
import type { ReplOutput } from "../repl";
44
import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime";
5+
import { format, inspect } from "util"; // <- これなぜブラウザ側でimportできるの? :thinking:
56

67
let jsOutput: ReplOutput[] = [];
78

89
// Helper function to capture console output
910
const originalConsole = self.console;
1011
self.console = {
1112
...originalConsole,
12-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13-
log: (...args: any[]) => {
14-
jsOutput.push({ type: "stdout", message: args.join(" ") });
13+
log: (...args: unknown[]) => {
14+
jsOutput.push({ type: "stdout", message: format(...args) });
1515
},
16-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
error: (...args: any[]) => {
18-
jsOutput.push({ type: "stderr", message: args.join(" ") });
16+
error: (...args: unknown[]) => {
17+
jsOutput.push({ type: "stderr", message: format(...args) });
1918
},
20-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21-
warn: (...args: any[]) => {
22-
jsOutput.push({ type: "stderr", message: args.join(" ") });
19+
warn: (...args: unknown[]) => {
20+
jsOutput.push({ type: "stderr", message: format(...args) });
2321
},
24-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25-
info: (...args: any[]) => {
26-
jsOutput.push({ type: "stdout", message: args.join(" ") });
22+
info: (...args: unknown[]) => {
23+
jsOutput.push({ type: "stdout", message: format(...args) });
2724
},
2825
};
2926

@@ -38,6 +35,8 @@ async function init({ id }: WorkerRequest["init"]) {
3835
async function runCode({ id, payload }: WorkerRequest["runCode"]) {
3936
let { code } = payload;
4037
try {
38+
let result: unknown;
39+
4140
// eval()の中でconst,letを使って変数を作成した場合、
4241
// 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、
4342
// varに置き換えている
@@ -46,14 +45,33 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) {
4645
} else if (code.trim().startsWith("let ")) {
4746
code = "var " + code.trim().slice(4);
4847
}
48+
// eval()の中でclassを作成した場合も同様
49+
const classRegExp = /^\s*class\s+([A-Za-z0-9_]+)/;
50+
if (classRegExp.test(code)) {
51+
code = code.replace(classRegExp, "var $1 = class");
52+
}
4953

50-
// Execute code directly with eval in the worker global scope
51-
// This will preserve variables across calls
52-
const result = self.eval(code);
54+
if (code.trim().startsWith("{") && code.trim().endsWith("}")) {
55+
// オブジェクトは ( ) で囲わなければならない
56+
try {
57+
result = self.eval(`(${code})`);
58+
} catch (e) {
59+
if (e instanceof SyntaxError) {
60+
// オブジェクトではなくブロックだった場合、再度普通に実行
61+
result = self.eval(code);
62+
} else {
63+
throw e;
64+
}
65+
}
66+
} else {
67+
// Execute code directly with eval in the worker global scope
68+
// This will preserve variables across calls
69+
result = self.eval(code);
70+
}
5371

5472
jsOutput.push({
5573
type: "return",
56-
message: JSON.stringify(result),
74+
message: inspect(result),
5775
});
5876
} catch (e) {
5977
originalConsole.log(e);
@@ -117,7 +135,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {
117135

118136
try {
119137
// Try to create a Function to check syntax
120-
new Function(code);
138+
// new Function(code); // <- not working
139+
self.eval(`() => {${code}}`);
121140
self.postMessage({
122141
id,
123142
payload: { status: "complete" },
@@ -127,8 +146,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {
127146
if (e instanceof SyntaxError) {
128147
// Simple heuristic: check for "Unexpected end of input"
129148
if (
130-
e.message.includes("Unexpected end of input") ||
131-
e.message.includes("expected expression")
149+
e.message.includes("Unexpected token '}'") ||
150+
e.message.includes("Unexpected end of input")
132151
) {
133152
self.postMessage({
134153
id,

public/docs/javascript-6.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# 第6章: オブジェクトとプロトタイプ
2+
3+
他の言語(Java, C\#, Pythonなど)の経験がある方にとって、JavaScriptの「オブジェクト」と「継承」のモデルは最も混乱しやすい部分の一つです。JavaScriptはクラスベースではなく、**プロトタイプベース**のオブジェクト指向言語です。
4+
5+
本章では、ES6(ECMAScript 2015)以降の`class`構文(第7章で扱います)の裏側で実際に何が起きているのか、その仕組みの根幹である「プロトタイプチェーン」について解説します。
6+
7+
## オブジェクトリテラルとプロパティ
8+
9+
JavaScriptにおけるオブジェクトは、基本的にはキー(プロパティ名)と値のコレクション(連想配列やハッシュマップに近いもの)です。最も一般的な生成方法は**オブジェクトリテラル** `{...}` を使うことです。
10+
11+
```js-repl:1
12+
> const book = {
13+
... title: "JavaScript Primer",
14+
... "page-count": 350, // ハイフンを含むキーは引用符が必要
15+
... author: {
16+
... name: "John Doe",
17+
... age: 30
18+
... }
19+
... };
20+
undefined
21+
> book.title
22+
'JavaScript Primer'
23+
> book["page-count"] // 識別子として無効な文字を含む場合はブラケット記法
24+
350
25+
> book.author.name
26+
'John Doe'
27+
```
28+
29+
### プロパティの追加・削除
30+
31+
動的な言語であるJavaScriptでは、オブジェクト作成後にプロパティを追加・削除できます。
32+
33+
```js-repl:2
34+
> const config = { env: "production" };
35+
undefined
36+
> config.port = 8080; // 追加
37+
8080
38+
> delete config.env; // 削除
39+
true
40+
> config
41+
{ port: 8080 }
42+
```
43+
44+
## メソッドと this(復習)
45+
46+
オブジェクトのプロパティには関数も設定できます。これを**メソッド**と呼びます。
47+
第5章で学んだ通り、メソッド呼び出しにおける `this` は、「ドットの左側にあるオブジェクト(レシーバ)」を指します。
48+
49+
```js-repl:3
50+
> const counter = {
51+
... count: 0,
52+
... increment: function() {
53+
... this.count++;
54+
... return this.count;
55+
... },
56+
... // ES6からの短縮記法(推奨)
57+
... reset() {
58+
... this.count = 0;
59+
... }
60+
... };
61+
undefined
62+
> counter.increment();
63+
1
64+
> counter.increment();
65+
2
66+
> counter.reset();
67+
undefined
68+
> counter.count
69+
0
70+
```
71+
72+
## プロトタイプとは何か?
73+
74+
ここからが本章の核心です。JavaScriptのすべてのオブジェクトは、自身の親となる別のオブジェクトへの隠されたリンクを持っています。このリンク先のオブジェクトを**プロトタイプ**と呼びます。
75+
76+
オブジェクトからプロパティを読み取ろうとしたとき、そのオブジェクト自身がプロパティを持っていなければ、JavaScriptエンジンは自動的にプロトタイプを探しに行きます。
77+
78+
### `__proto__``Object.getPrototypeOf`
79+
80+
歴史的経緯により、多くのブラウザで `obj.__proto__` というプロパティを通じてプロトタイプにアクセスできますが、現在の標準的な方法は `Object.getPrototypeOf(obj)` です。
81+
82+
```js-repl:4
83+
> const arr = [1, 2, 3];
84+
undefined
85+
> // 配列の実体はオブジェクトであり、Array.prototypeを継承している
86+
> Object.getPrototypeOf(arr) === Array.prototype
87+
true
88+
> // Array.prototypeの親はObject.prototype
89+
> Object.getPrototypeOf(Array.prototype) === Object.prototype
90+
true
91+
> // Object.prototypeの親はnull(チェーンの終端)
92+
> Object.getPrototypeOf(Object.prototype)
93+
null
94+
```
95+
96+
## プロトタイプチェーンによる継承の仕組み
97+
98+
あるオブジェクトのプロパティにアクセスした際、JavaScriptは以下の順序で探索を行います。
99+
100+
1. そのオブジェクト自身(Own Property)が持っているか?
101+
2. 持っていなければ、そのオブジェクトのプロトタイプが持っているか?
102+
3. それでもなければ、プロトタイプのプロトタイプが持っているか?
103+
4. `null` に到達するまで繰り返し、見つからなければ `undefined` を返す。
104+
105+
この連鎖を**プロトタイプチェーン**と呼びます。クラス継承のように型定義をコピーするのではなく、**リンクを辿って委譲(Delegation)する**仕組みです。
106+
107+
以下のコードで、具体的な動作を確認してみましょう。
108+
109+
```js:prototype_chain.js
110+
const animal = {
111+
eats: true,
112+
walk() {
113+
console.log("Animal walks");
114+
}
115+
};
116+
117+
const rabbit = {
118+
jumps: true,
119+
__proto__: animal // 注意: __proto__への代入は学習目的以外では非推奨
120+
};
121+
122+
const longEar = {
123+
earLength: 10,
124+
__proto__: rabbit
125+
};
126+
127+
// 1. longEar自身は walk を持っていない -> rabbitを見る
128+
// 2. rabbitも walk を持っていない -> animalを見る
129+
// 3. animal が walk を持っている -> 実行
130+
longEar.walk();
131+
132+
// 自身のプロパティ
133+
console.log(`Jumps? ${longEar.jumps}`); // rabbitから取得
134+
console.log(`Eats? ${longEar.eats}`); // animalから取得
135+
136+
// プロパティの追加(シャドーイング)
137+
// longEar自身に walk を追加すると、animalの walk は隠蔽される
138+
longEar.walk = function() {
139+
console.log("LongEar walks simply");
140+
};
141+
142+
longEar.walk();
143+
```
144+
145+
```js-exec:prototype_chain.js
146+
Animal walks
147+
Jumps? true
148+
Eats? true
149+
LongEar walks simply
150+
```
151+
152+
## Object.create() とコンストラクタ関数
153+
154+
`__proto__` を直接操作するのはパフォーマンスや標準化の観点から推奨されません。プロトタイプを指定してオブジェクトを生成する正しい方法は2つあります。
155+
156+
### 1\. Object.create()
157+
158+
指定したオブジェクトをプロトタイプとする新しい空のオブジェクトを生成します。
159+
160+
```js-repl:5
161+
> const proto = { greet: function() { return "Hello"; } };
162+
undefined
163+
> const obj = Object.create(proto);
164+
undefined
165+
> obj.greet();
166+
'Hello'
167+
> Object.getPrototypeOf(obj) === proto
168+
true
169+
```
170+
171+
### 2\. コンストラクタ関数(new演算子)
172+
173+
ES6の `class` が登場する前、JavaScriptでは関数をコンストラクタとして使用し、`new` 演算子を使ってインスタンスを生成していました。これは現在でも多くのライブラリの内部で使用されている重要なパターンです。
174+
175+
* 関数オブジェクトは `prototype` という特別なプロパティを持っています(`__proto__`とは別物です)。
176+
* `new Func()` すると、作られたインスタンスの `__proto__``Func.prototype` がセットされます。
177+
178+
```js:constructor_pattern.js
179+
// コンストラクタ関数(慣習として大文字で始める)
180+
function User(name) {
181+
// this = {} (新しい空のオブジェクトが暗黙的に生成される)
182+
this.name = name;
183+
// return this (暗黙的にこのオブジェクトが返される)
184+
}
185+
186+
// すべてのUserインスタンスで共有したいメソッドは
187+
// User.prototype に定義する(メモリ節約のため)
188+
User.prototype.sayHi = function() {
189+
console.log(`Hi, I am ${this.name}`);
190+
};
191+
192+
const user1 = new User("Alice");
193+
const user2 = new User("Bob");
194+
195+
user1.sayHi();
196+
user2.sayHi();
197+
198+
// 仕組みの確認
199+
console.log(user1.__proto__ === User.prototype); // true
200+
console.log(user1.sayHi === user2.sayHi); // true (同じ関数を共有している)
201+
```
202+
203+
```js-exec:constructor_pattern.js
204+
Hi, I am Alice
205+
Hi, I am Bob
206+
true
207+
true
208+
```
209+
210+
> **重要な区別:**
211+
>
212+
> * `obj.__proto__`: オブジェクトの実の親(リンク先)。
213+
> * `Func.prototype`: その関数を `new` したときに、生成されるインスタンスの `__proto__` に代入される**テンプレート**
214+
215+
## この章のまとめ
216+
217+
1. JavaScriptはクラスベースではなく、**プロトタイプベース**の継承を行う。
218+
2. オブジェクトは隠しプロパティ(`[[Prototype]]`)を持ち、プロパティが見つからない場合にそこを探索する(プロトタイプチェーン)。
219+
3. `Object.create(proto)` は、特定のプロトタイプを持つオブジェクトを直接生成する。
220+
4. コンストラクタ関数と `new` 演算子を使うと、`Func.prototype` を親に持つインスタンスを生成できる。これがJavaなどの「クラス」に近い振る舞いを模倣する仕組みである。
221+
222+
## 練習問題1: 基本的なプロトタイプ継承
223+
224+
`Object.create()` を使用して、以下の要件を満たすコードを書いてください。
225+
226+
1. `robot` オブジェクトを作成し、`battery: 100` というプロパティと、バッテリーを10減らして残量を表示する `work` メソッドを持たせる。
227+
2. `robot` をプロトタイプとする `cleaningRobot` オブジェクトを作成する。
228+
3. `cleaningRobot` 自身に `type: "cleaner"` というプロパティを追加する。
229+
4. `cleaningRobot.work()` を呼び出し、正しく動作(プロトタイプチェーンの利用)を確認する。
230+
231+
```js:practice6_1.js
232+
```
233+
234+
```js-exec:practice6_1.js
235+
```
236+
237+
### 練習問題2: コンストラクタ関数
238+
239+
コンストラクタ関数 `Item` を作成してください。
240+
241+
1. `Item` は引数 `name``price` を受け取り、プロパティとして保持する。
242+
2. `Item.prototype``getTaxIncludedPrice` メソッドを追加する。これは税率10%を加えた価格を返す。
243+
3. `new Item("Apple", 100)` でインスタンスを作成し、税込価格が110になることを確認する。
244+
245+
```js:practice6_2.js
246+
```
247+
248+
```js-exec:practice6_2.js
249+
```
250+

0 commit comments

Comments
 (0)