※ オブジェクト指向を考える上で脳内で想像していたことを噛み砕いて書いているので、完璧ではないと思います。
クラスで定義したものが、実際にインスタンス化されたオブジェクトの中でどのような形で保持されているかを考えるのが理解への近道かなと思います。
Javaの場合
クラス定義とオブジェクト生成
以下のようなクラスを考えます。
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public String makeSound() {
return name + " makes a sound.";
}
}
class Dog extends Animal {
private String owner;
public Dog(String name, String owner) {
super(name);
this.owner = owner;
}
public String getOwner() {
return this.owner;
}
}
Dog
クラスを使ってオブジェクトを生成します。
Dog dog = new Dog("Buddy", "Alice");
オブジェクトの中身(メモリ上のイメージ)
この場合、生成されたオブジェクトは次のような構造になります。
Dog {
name: "Buddy", // Animal クラスから継承されたプロパティ
owner: "Alice" // Dog クラスで追加されたプロパティ
}
Javaでは、インスタンス化されたオブジェクトはクラスのプロパティを**フィールド(メモリ上の変数)**として保持します。例えば、以下のようなコードを追加すればフィールド値が確認できます:
System.out.println(dog.name); // Buddy
System.out.println(dog.getOwner()); // Alice
オブジェクトの実際のメモリ構造
メモリ上では以下のような形で保存されています:
Dog オブジェクト:
- name: "Buddy" // 継承元 (Animal)
- owner: "Alice" // Dog クラス独自
メソッド(makeSound
やgetOwner
)は、実際にはオブジェクトに直接保存されず、クラスの定義に基づいて参照されます。
TypeScriptの場合
クラス定義とオブジェクト生成
TypeScript で同じクラスを定義します。
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
makeSound(): string {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
private owner: string;
constructor(name: string, owner: string) {
super(name);
this.owner = owner;
}
getOwner(): string {
return this.owner;
}
}
// オブジェクト生成
const dog = new Dog("Buddy", "Alice");
オブジェクトの中身(JavaScriptオブジェクトとしての構造)
TypeScript のクラスをインスタンス化すると、オブジェクトは以下のような形になります。
// dog オブジェクト
{
name: "Buddy", // Animal クラスから継承
owner: "Alice" // Dog クラスで追加
}
これは JavaScript のオブジェクトとして dog
を表示すれば確認できます。
console.log(dog);
// 出力例:
// Dog { name: "Buddy", owner: "Alice" }
dog
オブジェクトに格納されている内容
TypeScript では、オブジェクトの実態は JavaScript オブジェクトとして動作します。つまり、次のようにプロパティやメソッドにアクセスできます:
console.log(dog["name"]); // Buddy
console.log(dog.getOwner()); // Alice
実際のオブジェクトの中身とプロトタイプチェーン
TypeScript のオブジェクトは プロトタイプチェーン を通じてクラスのメソッドを参照します。dog
オブジェクトをデバッグツールで見ると、次のように表示されます。
Dog {
name: "Buddy",
owner: "Alice"
}
[[Prototype]]: Animal {
makeSound: f()
}
ここで:
-
name
とowner
はdog
オブジェクトに直接保持されています。 -
makeSound
メソッドはAnimal
のプロトタイプ(クラス定義)から参照されます。
比較:JavaとTypeScriptのオブジェクト構造
特徴 | Java | TypeScript |
---|---|---|
プロパティの格納 | メモリ上のフィールドとして直接格納 | JavaScriptオブジェクトのキーとして保持 |
メソッドの格納 | クラスの定義に保存、オブジェクトは参照 | プロトタイプチェーンで参照 |
プロパティの継承方法 | 継承元のフィールドとして保持 | 同じオブジェクト内に統合 |
こうしてオブジェクトの具体的な形を示すと、実際にクラスから作られたオブジェクトがどのようにデータを保持し、参照するかが理解しやすくなると思います!
ついでに同じ考え方で、TypeScriptを例にして、型推論がどのようにオブジェクトの形に基づいて行われるのかを説明します。
型推論とは?
TypeScriptの型推論は、変数や関数の型を明示的に指定しなくても、コードからその型を自動的に判断する仕組みです。これは、TypeScriptがオブジェクトの形やプロパティ、メソッドの定義を元に型を決定することで実現されています。
具体例: オブジェクトと型推論
以下のコードを見てみましょう。
1. クラスを使った型推論
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound(): string {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
owner: string;
constructor(name: string, owner: string) {
super(name);
this.owner = owner;
}
bark(): string {
return `${this.name} barks!`;
}
}
// オブジェクトを生成
const dog = new Dog("Buddy", "Alice");
この場合、TypeScriptは dog
の型を次のように推論します:
Dog {
name: string, // 継承されたプロパティ
owner: string, // Dog で追加されたプロパティ
makeSound(): string, // 継承されたメソッド
bark(): string // Dog で追加されたメソッド
}
つまり、Dog
クラスの構造に基づいて dog
の型が自動的に決定されます。
2. 型推論がオブジェクトの形に基づく例
オブジェクトリテラルを使った場合でも同じ原則が適用されます。
const cat = {
name: "Whiskers",
makeSound: () => "Meow!"
};
TypeScriptがこのオブジェクトの型を次のように推論します:
{
name: string;
makeSound: () => string;
}
つまり、オブジェクトの中身(キーと値)を見て、その型が自動的に決まります。
型推論のポイント:プロパティとメソッドの関係
クラスやオブジェクトの形が型の基礎になります。例えば:
継承の場合
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
owner: string;
constructor(name: string, owner: string) {
super(name);
this.owner = owner;
}
}
const animal: Animal = new Dog("Buddy", "Alice");
-
animal
の型はAnimal
と明示していますが、new Dog()
を渡すとDog
がAnimal
を拡張しているため問題ありません。 - 型推論は「代入先の型」や「オブジェクトの形」に基づいて決まります。
メソッド内で型が推論される例
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
return `${this.name} barks!`;
}
}
const dog = new Dog("Buddy");
// メソッドの戻り値も型推論される
const sound = dog.makeSound();
// TypeScript は sound の型を string と推論する
型推論がオブジェクトの形を使っている理由
型推論が「オブジェクトの形」に基づいている理由は、TypeScriptの型システムが構造的型システム(Structural Typing)に基づいているためです。
構造的型システムでは、次のように「型」ではなく「形」が一致していれば互換性があるとみなされます。
例: 形が一致する場合の互換性
type AnimalType = {
name: string;
makeSound: () => string;
};
const cat: AnimalType = {
name: "Whiskers",
makeSound: () => "Meow!"
};
AnimalType
という型は明示的に定義していますが、cat
オブジェクトの形がこれと一致しているため、問題なく代入できます。
型推論とオブジェクトの形のまとめ
- クラスやオブジェクトの形は、プロパティとメソッドをもとに型として定義されます。
- 型推論は、この形をもとに自動的に型を決定します。
- TypeScriptの構造的型システムにより、形が一致する限り互換性があります。
オブジェクトの形を意識すると、型推論の動作が直感的に理解できるようになります。