見出し画像

TypeScript 入門の記録(65)プロを目指す人のためのTypeScript入門(49)第5章TypeScriptのクラス(9)

こんにちは。サイボウズ株式会社 開発本部 People Experienceチーム コネクト支援チームの貴島(@jnkykn)です。北海道で淡路島バーガーが食べられるお店ができて、しかも「あわぢびーる」が飲めるという話を酒井(@sakay_y)さんにご報告したら、「あわぢびーるのヴァイツェンは無限に飲めるので、ぜひ!」とおすすめされたので、次回はヴァイツェンを飲みたいというか、全種類制覇したいと思っています(無限に飲めるのは危険と思いつつ)数日前に、TypeScriptの学習記録マガジンをフォローしてくださった方がいらっしゃって、「まずい、1年以上放置してる!!!」と思いました。心を入れ替えて、TypeScriptの学習を再開します。

this

なんと、前回のTypeScript 入門の記録は、2023年8月20日。1年以上前ですね。第5章クラスの続き、thisについて試しながら学習します。

関数の中のthis

関数の中のthisは、その関数の呼び出し方によって変わるそうです。なんだって!?
うろたえずに、順を追って確認します。

class User54 {
    name: string;
    #age: number;

    constructor(name: string, age:number){
        this.name = name;
        this.#age = age;
    }

    // 成人ならtrueを返すメソッド
    isAdult(): boolean {
        return this.#age >= 18;
    }

    // ageを設定するメソッド
    setAge(newAge: number) {
        this.#age = newAge;
    }
}

// 関数の中のthisは、呼び出し方で決まる
const uhyo541 = new User54("uhyo", 26);
const john541 = new User54("Jhon Smith", 15);
console.log(`uhyo541.isAdult === john541.isAdult = ${uhyo541.isAdult === john541.isAdult}`);
console.log(`uhyo541.isAdult() = ${uhyo541.isAdult()}`);
console.log(`jhon541.isAdult() = ${john541.isAdult()}`);
console.log(`uhyo541.isAdult() === john541.isAdult() = ${uhyo541.isAdult() === john541.isAdult()}`);

生成したインスタンスの関数は、クラスの関数を使用します。上記の例の場合、クラスの関数は、インスタンスにはコピーされず、uhyo541.isAdultとjhon.isAdultはUser54.isAdultを指すということです。エコですね。

クラスのインスタンスの関数オブジェクトは、クラスの関数オブジェクトを指している

呼ばれた関数は、インスタンスのメンバーの値を使って結果を返します。上記の実行結果は、こうなります。

$ npx tsc
$ node dist/index_5-4.js
uhyo541.isAdult === john541.isAdult = true
uhyo541.isAdult() = true
jhon541.isAdult() = false
uhyo541.isAdult() === john541.isAdult() = false

メモリ上の関数オブジェクトの場所は同じだけれど、関数の実行時にインスタンスのメンバーを使うので、結果は想定どおり26歳のuhyo541さんは成人、15歳のjhon541さんは成人ではないという結果が得られます。

ここで関数オブジェクトを変数に代入して呼び出してみます。

const isAdult = uhyo541.isAdult;
console.log(`isAdult() = ${isAdult()}`);

実行してみると、関数内でメンバー変数が使えず、エラーが発生しました。

TypeError: Cannot read private member from an object whose class did not declare it

関数オブジェクトを代入した変数は、所属するインスタンスがundefinedなため、メンバー変数を参照できないのですね。(非strictモードだと、thisはグローバル変数になるので、実行できてしまうらしいです)

アロー関数内のthis

アロー関数は自身のthisを持たないので、thisを外側の関数から受け継ぎます。

  • アロー関数内のthisは外側の関数のthisと同じ

  • 関数内にないアロー関数の場合、アロー関数内のthisはundefined

また、ややこしいですね。

確かめるために、関数を追加。

    public filterOlder(users: readonly User54[]): User54[] {
        return users.filter(u => u.#age > this.#age);
    }
}

// 関数の中のthisは、呼び出し方で決まる
const uhyo541 = new User54("uhyo", 26);
const john541 = new User54("Jhon Smith", 15);
const bob542 = new User54("Bob", 40);

const older = uhyo541.filterOlder([john541, bob542]);
console.log(older);

結果を見ると、呼び出し元のuhyo541.#ageをthis.#ageとして、パラメータで渡した配列の#ageと比較した結果を返しています。

$ node dist/index_5-4.js
[ User54 { name: 'Bob' } ]

今度は、この関数を通常の関数式で書き直してみます。(thisがundefinedになる想定)

アロー関数を単純な関数として書き直してみると、thisがanyなのでエラー
thisでエラーが発生してコンパイルできない

パラメータで、thisの型を指定してみます。

    public filterOlder(users: readonly User54[]): User54[] {
        return users.filter(function(this: User54, u) {return u.#age > this.#age}) ;
    }

実行してみると、プライベートメンバーが読めないというエラーが発生しました。渡したパラメータthisのメンバーが参照できないようです。

TypeError: Cannot read private member from an object whose class did not declare it

これは、users.filterからは、呼び出し元が見えないことが原因なので、一旦受け取ったパラメータのthisを変数に退避して、見えるようにすると解決できるようです。というわけで、やってみます。

    public filterOlder(users: readonly User54[]): User54[] {
        // this を一時退避する
        const _this = this;
        return users.filter(function(u) {return u.#age > _this.#age}) ;
    }

実行してみると、期待通りの結果になりました。

$ node dist/index_5-4.js
[ User54 { name: 'Bob' } ]

これは、わかりにくいですね。アロー関数で記述すればスッキリ期待通りの結果が得られるので、関数内でthisを引き継ぎたい場合は、アロー関数にしようと思います。

thisを操作するメソッド

関数の呼び出し方のうち、オブジェクトが持つ、applyメソッドと、callメソッドを試してみます。applyメソッドは、オブジェクト内のメソッドを呼び出すメソッドです。func.apply(obj, args)という形式で呼び出し、func内のthisを、パラメータobjと置き換えます。

const uhyo541 = new User54("uhyo", 26);
const john541 = new User54("Jhon Smith", 15);
const bob542 = new User54("Bob", 40);

console.log(`uhyo541.isAdult() = ${uhyo541.isAdult()}`);
console.log(`uhyo541.isAdult.apply(john541, []) = ${uhyo541.isAdult.apply(john541, [])}`);

実行してみると、uhyo541.isAdult()はtrueですが、uhyo541.isAdult.apply (john541, []) の結果は、john.#ageが15なので、falseです。

$ node dist/index_5-4.js
uhyo541.isAdult() = true
uhyo541.isAdult.apply(john541, []) = false

applyと同様に、callも試します。callは、呼び出すときのパラメータのargsを配列ではなく、第2引数以降に列挙するところが違うだけで使い方は同じです。

console.log(`uhyo541.isAdult() = ${uhyo541.isAdult()}`);
console.log(`uhyo541.isAdult.call(john541) = ${uhyo541.isAdult.call(john541)}`);

実行時のthisがパラメータで渡されたオブジェクトと置き換えられるので、実行結果はapplyと同様です。

$ node dist/index_5-4.js
uhyo541.isAdult() = true
uhyo541.isAdult.call(john541) = false

thisを固定する、bindも試します。

const boundIsAdult = uhyo541.isAdult.bind(uhyo541);
console.log(`boundIsAdult() = ${boundIsAdult()}`);
console.log(`boundIsAdult(john541) = ${boundIsAdult.call(john541)}`);

実行してみると、callで#age15のjohn541を渡しても、uhyo541は26歳なのでtrueが返りました。

$ node dist/index_5-4.js
boundIsAdult() = true
boundIsAdult(john541) = true

まとめ

久しぶりにTypeScriptの学習を再開しました、普段雰囲気でコーディングしているので、ちゃんと理解した上で使いたいと改めて思いました。区切りが良いので、今週はここまでにします。
来週の今頃は、P2HACKS 2024発表会に出席、サイボウズ賞の受賞チームも決まって、函館で協賛報告noteを書いている予定です。


いいなと思ったら応援しよう!