耶穌說:“凱撒的物當歸給凱撒,神的物當歸給神。”——《馬太福音22:21》
抱歉我其實不是個基督徒。我用這句話的原因是因為這句話很符合面向對象思想:讓一個角色做好它該做的事。如何理解這句話呢?就好比你去拿著一堆的原材料去找一個鐵匠,他就能給你打造一把你想要的武器;如果你拿著同樣這堆原材料去找一個廚子,他會拿炒勺弄死你。
道理我們都懂?但是這個跟寫代碼有什么關系嗎?當然有關系!勇者喲,在踏上征途之前,我們先在新手村做一些準備工作吧!
創世界:準備工作
上路之前,你要準備好一些東西,比如Node.js和npm。安裝Node.js的傳送門在這里。然后還需要一個世界轉換器TypeScript:
npm install -g typescript
TypeScript現在是這個世界的規則。它跟JavaScript世界很像,嗯,甚至沒什么差別,比如:
ts-the-rpg.ts(v1
function hello(hero) {
console.log("Hello , " + hero);
}
var hero = "勇者";
hello(hero);
我們需要將ts-the-rpg.ts
變成JavaScript世界的才能玩。
tsc ts-the-rpg.ts
這個時候我們發現生成了ts-the-rpg.js
。我們打開看看,發現,怎么這兩個世界一模一樣?
沒錯,這兩個世界幾乎一樣。但是接下來不一樣的來了:
ts-the-rpg.ts(v2)
function hello(hero: string) {
console.log("Hello , " + hero);
}
var hero1 = "勇者";
var hero2 = 2;
hello(hero1);
hello(hero2);
你在轉換世界的時候發現,怎么出了一個問題?
ts-the-rpg.ts(8,7): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
看起來有一個怪物混入了勇者的隊伍之中啊。勇者都有自己的名字,你卻是一個數字?你想蒙混過關?世界轉換器就會告訴你,你很危險了勇者。
但是即使這樣,世界轉換器很公正,它還是把TypeScript世界的一切帶到了JavaScript世界。因為JavaScript世界中,勇者、怪物,傻傻分不清楚。轉換后的世界:
ts-the-rpg.js
function hello(hero) {
console.log("Hello , " + hero);
}
var hero1 = "勇者";
var hero2 = 2;
hello(hero1);
hello(hero2);
你是一個創世者,也是一個游戲玩家,世界轉換器告訴了你這個世界有問題,但是你的世界你來決定。我建議,為了這個世界不至于在最后分崩離析,還是好好處理完問題再上路吧。
數據類型,是構建這個世界的基礎。這個世界本身由這么幾個基本數據類型組成:string、number、array、enum、any……等等。我們怎么判斷它是不是這種類型的怎么做?只要把類型帶在你的聲明之中。比如function hello(hero: string)
就表示,你必須是一個string
類型的數據,才能通過這里。
新手村:歡迎你,勇者
“你好,勇者!”突然你看到了一陣光,看板娘站在光中,笑靨如畫。
你想起來了,你是一個勇者。等等,什么是勇者?你作為一個創世者,深深的陷入了思考,最后得出一個結論:
ts-the-rpg.ts(v3)
var hero = {
name: "勇者",
hp: 10
}
function hello(hero) {
console.log("Hello , " + hero.name);
}
hello(hero);
不對,這樣做不是依然什么樣的牛鬼蛇神都能以勇者的身份進入這個世界嗎?嗯,作為一個先知,我告訴你怎么做:
ts-the-rpg.ts(v4)
// 定義什么是勇者
class Hero {
name: string; // 每個勇者都有一個名字
hp: number; // 每個勇者有自己的HP值
// 召喚一個勇者的規則
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 召喚一個勇者
var hero = new Hero("勇者", 10);
// 只能由勇者通過的路
function hello(hero: Hero) {
console.log("Hello , " + hero.name);
}
hello(hero);
hello("我也是一個勇者啊!");
我們先來試試看轉換這個世界,之后再來解釋一下為什么這么做。開始轉換:
ts-the-rpg.ts(21,7): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Hero'.
最后一個假裝是勇者的字符串想要蒙混過關,被轉換器攔住了。等等,轉換器怎么知道勇者長什么樣?
沒錯,我們這時候祭出了勇者召喚的特殊形式:class
。我們通過定義一個叫Hero的數據類型來告訴世界,這個世界開始有勇者了。那么以后,我們就可以在入口處判斷你是不是一個Hero。
class
由這么幾個部分組成:它是什么(定義的數據類型名)、它由什么構成(類的成員數據)、它能做什么(定義數據類型的行為)、它需要哪些素材才能被召喚出來(構造函數)。
從ts-the-rpg.ts(v4)
上看,我們定義了它是勇者(class Hero
),它有名字(name: string;
)和血量(hp: number;
),召喚他的規則就是必須要給他起個名字并提供血量(constructor(name: string, hp: number){ this.name = name; this.hp = hp; }
),然后通過特殊儀式召喚它(var hero = new Hero("勇者", 10);
)。
那你會問,如果有一個史萊姆,偽裝的特別像一個勇者,就像下面這樣,世界轉換器會怎么做?
ts-the-rpg.ts(v5)
// 定義什么是勇者
class Hero {
name: string; // 每個勇者都有一個名字
hp: number; // 每個勇者有自己的HP值
// 召喚一個勇者的規則
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 召喚一個勇者
var hero = new Hero("勇者", 10);
// 只能由勇者通過的路
function hello(hero: Hero) {
console.log("Hello , " + hero.name);
}
hello(hero);
// 偽裝成勇者的史萊姆
hello({ name: "我不是史萊姆", hp: 1 });
我們試著轉換到普通世界后,怎么世界轉換器什么都沒做?于是你陷入了深深的恐懼。沒錯,這樣召喚一個勇者肯定一瞬間就被危險的史萊姆識破并且偽裝,這個時候我們需要把勇者不為人知的一面隱藏起來,比如勇者有hp這件事。
ts-the-rpg.ts(v6)
// 定義什么是勇者
class Hero {
name: string; // 每個勇者都有一個名字
private hp: number; // 每個勇者有自己的HP值,但是受保護
// 召喚一個勇者的規則
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 召喚一個勇者
var hero = new Hero("勇者", 10);
// 只能由勇者通過的路
function hello(hero: Hero) {
console.log("Hello , " + hero.name);
}
hello(hero);
hello({ name: "我不是史萊姆", hp: 1 });
這時候我們轉換這個世界,你看史萊姆被攔在了外面:
ts-the-rpg.ts(21,7): error TS2345: Argument of type '{ name: string; hp: number; }' is not assignable to parameter of type 'Hero'.
Property 'hp' is private in type 'Hero' but not in type '{ name: string; hp: number; }'.
沒錯,你一個堂堂史萊姆,把hp值這種對于勇者如此重要的屬性暴露在外面,這種作風肯定不是勇者所為,你出去。
這時候史萊姆又來搞事情,它想,如果這樣,我也隱藏我的hp,除了定義不一樣,其他的都一摸一樣,那樣我一定能進去。
ts-the-rpg.ts(v7)
// 定義什么是勇者
class Hero {
name: string; // 每個勇者都有一個名字
private hp: number; // 每個勇者有自己的HP值,但是受保護
// 召喚一個勇者的規則
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 定義什么是史萊姆
class Slime {
name: string; // 每個史萊姆都有一個名字
private hp: number; // 每個史萊姆有自己的HP值,但是受保護
// 召喚一個史萊姆的規則
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 召喚一個勇者
var hero = new Hero("勇者", 10);
var slime = new Slime("勇者", 10);
// 只能由勇者通過的路
function hello(hero: Hero) {
console.log("Hello , " + hero.name);
}
hello(hero);
hello(slime);
這時候世界轉換器非常聰明:
ts-the-rpg.ts(33,7): error TS2345: Argument of type 'Slime' is not assignable to parameter of type 'Hero'.
Types have separate declarations of a private property 'hp'.
你作為一只史萊姆,身上流著史萊姆的血,你的血的味道,我一聞就能聞出來不一樣。史萊姆直接被推了出去。
那你肯定這時候肯定覺得很奇怪ts-the-rpg.ts(v5)
和ts-the-rpg.ts(v7)
同樣是將史萊姆偽裝成了勇者,為什么v5成功了,v7卻失敗了?
世界轉換器在這里是這么做處理的:
v5部分因為所有的部分都是公開的,那么他只會判斷是否存在,這種原理別處稱作“鴨子模型”,就是說“呱呱叫又會游泳的鳥那就肯定是鴨子”,在這里就是“有名字有hp的肯定是勇者”,所以就放行了,這是一種弱的類型檢查機制,可以抵擋住大部分的偽裝。
在v7中,有部分是隱藏的,那么不只會判斷隱藏的部分是不是存在的,還會判斷它是否來自不同的定義。
好了,一切都安全了。你到這里應該明白了TypeScript世界的一部分,類型檢查。這個特性能夠在某種程度上保護你程序的安全,不至于讓你在每個通道內設置關卡,判斷進來的東西是勇者還是史萊姆,或者是偽裝成勇者的史萊姆。在JavaScript的世界里,你需要處處小心,勇者即使進了城也要被處處浪費時間去盤問,而在TypeScript中,勇者只需要在城門口被盤問一邊,確定你是勇者后,你在城里能得到所有你能得到的東西,而不用再一遍一遍的被盤問:“你是不是勇者?”
轉職:你依然是個勇者
你站在新手村中心,不知所措的時候,邊上有三個導師,分別在招攬著自己的學徒,分別是戰士、魔法師和弓箭手:
ts-the-rpg.ts(v8)
class Hero {
name: string;
private hp: number;
// 勇者的召喚方式
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
class Warrior extends Hero {
weapon: string;
// 戰士的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
swing() {
console.log("swing");
}
}
class Magician extends Hero {
weapon: string;
// 魔法師的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
fireball() {
console.log("fireball");
}
}
class Archer extends Hero {
weapon: string;
// 弓箭手的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
shoot() {
console.log("shoot");
}
}
沒錯,三種職業有著自己的攻擊方式。勇者你要學什么呢?
ts-the-rpg.ts(v9)
class Hero {
name: string;
private hp: number;
// 勇者的召喚方式
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 通過extends繼承了勇者之力
class Warrior extends Hero {
weapon: string;
// 戰士的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
swing() {
console.log("swing");
}
}
// 通過extends繼承了勇者之力
class Magician extends Hero {
weapon: string;
// 魔法師的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
fireball() {
console.log("fireball");
}
}
// 通過extends繼承了勇者之力
class Archer extends Hero {
weapon: string;
// 弓箭手的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
shoot() {
console.log("shoot");
}
}
function forest(hero: Hero) {
console.log("Enter Forest !!");
}
var hero1 = new Warrior("warrior", 10, "sword");
var hero2 = new Magician("magician", 10, "wand");
var hero3 = new Archer("archer", 10, "bow");
forest(hero1);
forest(hero2);
forest(hero3);
世界轉換器轉換后發現,竟然戰士、法師和弓箭手都能進入森林!
你要知道,即使你選擇了職業,但是你體內的勇者之名和勇者之血都通過extends Hero
方式繼承了下來。你的召喚方式變了,但是你的召喚規則里還通過super(name, hp);
這個方式保留著你的內心,這個就像是當初召喚你的方式,new Hero(name , hp)
。所以即使這時候你們用著不同的武器,有著不同的攻擊方式,卻依然內心都還是個勇者。
技能訓練:技能雖好,可不要偷師哦!
這時候你猶豫了,你想去三個練功房都看看,再來考慮轉職的事情:
ts-the-rpg.ts(v10)
class Hero {
name: string;
private hp: number;
// 勇者的召喚方式
constructor(name: string, hp: number) {
this.name = name;
this.hp = hp;
}
}
// 通過extends繼承了勇者之力
class Warrior extends Hero {
weapon: string;
// 戰士的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
swing() {
console.log("swing");
}
}
// 通過extends繼承了勇者之力
class Magician extends Hero {
weapon: string;
// 魔法師的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
fireball() {
console.log("fireball");
}
}
// 通過extends繼承了勇者之力
class Archer extends Hero {
weapon: string;
// 弓箭手的召喚方式
constructor(name: string, hp: number , weapon: string) {
// 你的名字和你的血液是勇者的名字和勇者的血液,這是你的內心
super(name, hp);
this.weapon = weapon;
}
shoot() {
console.log("shoot");
}
}
function trainWarrior(hero: Warrior) {}
function trainMagician(hero: Magician) {}
function trainArcher(hero: Archer) {}
var hero = new Hero("普通勇者", 10);
trainWarrior(hero);
trainMagician(hero);
trainArcher(hero);
毫不意外,你被三個練功房都踢了出來。
ts-the-rpg.ts(60,14): error TS2345: Argument of type 'Hero' is not assignable to parameter of type 'Warrior'.
Property 'weapon' is missing in type 'Hero'.
ts-the-rpg.ts(61,15): error TS2345: Argument of type 'Hero' is not assignable to parameter of type 'Magician'.
Property 'weapon' is missing in type 'Hero'.
ts-the-rpg.ts(62,13): error TS2345: Argument of type 'Hero' is not assignable to parameter of type 'Archer'.
Property 'weapon' is missing in type 'Hero'.
三個房間的導師都說,你沒有武器,學不了技能。村長這個時候走過來告訴你真相:即使你有了武器,你也學不了。對,職人是繼承了勇者的內心,職人永遠都是勇者,而勇者沒有轉職,沒有職人才有的能力,你永遠只是個勇者,不是一個職人。這就是繼承的真相。
職人是勇者,勇者不是職人。
職人是勇者,勇者不是職人。
職人是勇者,勇者不是職人。
你默默念了三遍。銘記在心。
各式各樣的武器:選一件吧少年
實際上專職沒有那么簡單,你的武器不僅僅是個名字而已,這時候你的老師讓你去挑一把武器帶過來。你去了武器鋪。武器鋪的鐵匠大叔一看到你是一個勇者,非常熱心的跟你介紹了不同的武器。
ts-the-rpg.ts(v11)
class Weapon {
name: string;
private: atk;
constructor(name: string, atk: number) {
this.name = name;
this.atk = atk;
}
}
class Sword extends Weapon {
constructor(name: string, atk: number) {
super(name, atk);
}
swing() {
console.log("swing");
}
}
class Wand extends Weapon {
constructor(name: string, atk: number) {
super(name, atk);
}
fireball() {
console.log("fireball");
}
}
class Bow extends Weapon {
constructor(name: string, atk: number) {
super(name, atk);
}
shoot() {
console.log("shoot");
}
}
你說你要轉職成戰士,你只看劍。他很熱情,問問你是不是要附魔。有火焰效果和寒冰效果,然后可以給你打造一把獨一無二的劍:
ts-the-rpg.ts(v12)
class Weapon {
name: string;
private: atk;
constructor(name: string, atk: number) {
this.name = name;
this.atk = atk;
}
}
class Sword extends Weapon {
constructor(name: string, atk: number) {
super(name, atk);
}
swing() {
console.log("swing");
}
}
interface Fire {
fire();
}
interface Ice {
ice();
}
你想了想說,都要。鐵匠一愣,笑了笑,你小子為難老夫!好,難不倒老夫,于是你的劍做好了:
ts-the-rpg.ts(v12)
class Weapon {
name: string;
private: atk;
constructor(name: string, atk: number) {
this.name = name;
this.atk = atk;
}
}
class Sword extends Weapon {
constructor(name: string, atk: number) {
super(name, atk);
}
swing() {
console.log("swing");
}
}
interface Fire {
fire();
}
interface Ice {
ice();
}
class SwordOfIceAndFire extends Sword implements Ice, Fire {
constructor(name: string, atk: number) {
super(name, atk);
}
swing() {
console.log("swing");
this.ice();
this.fire();
}
ice() {
console.log("ice");
}
fire() {
console.log("fire");
}
}
沒錯,interface在面向對象中就好像是附魔屬性,只要你愿意,可以通過implements無限的往一把劍上疊加能力。但是,即使你無限疊加了能力,它還是一把劍,而不能成為魔杖或者弓箭。
冒險才剛剛開始,而我們的教程到了尾聲。我們再來回顧一下有哪些概念:
- 類和類型檢查:一個史萊姆偽裝的再好,還是史萊姆。
- 繼承是什么:勇者轉職之后還是勇者,不管他變成了劍士、魔法師還是弓箭手。
- 子類和父類的繼承關系:一個職人是勇者,但是一個勇者不是職人。
- 接口是什么:附魔屬性。
- 通過接口擴展類:只要你愿意,可以無限的往一把劍上疊加能力,讓它成為一把新的劍。但是,即使你無限疊加了能力,它還是一把劍,而不能成為魔杖或者弓箭。
希望通過這個教程幫助你理解TypeScript中面向對象的基礎。