首先我們要清楚 private 、 protected 現階段只是javascript中的保留字(Reserved words),而非關鍵字(Keywords )。因此TypeScript中的純類型聲明語句,編譯后都會被擦除。
class Person {
public name: string;
protected age: number;
private isMarried: boolean;
}
//編譯結果
class Person {
}
TypeScript是一個結構類型語言。當比較兩個不同的類型時,不管它們來自哪里,如果所有成員的類型都是兼容的,那么就說這些類型本身是兼容的。
interface Named {
name: string;
}
class Bar {
name: string;
}
class Foo {
name: string;
}
// OK, because of structural typing
let a: Named = new Person(); //✔️
let b: Foo = new Bar(); //✔️
由於 TypeScript 屬性聲明默認是 public,所以上面可以以 b.name 形式訪問,而java則默認是protected。
但是,當比較具有 private 成員或 protected 成員的類型時,會區別對待這些類型。如果其中一種類型具有private成員,那么另一種類型必須具有來源於同一處聲明的private成員。這同樣適用於protected成員。
class Bar {
private name: string;
}
class Foo {
private name: string;
}
let bar: Bar = new Foo(); // ❌
//Type 'Foo' is not assignable to type 'Bar'.
//Types have separate declarations of a private property 'name'.
上面的這些概念規則來源於 TypeScript Handbook,這里只是做個簡要的引子。
TypeScript 在判斷類型兼容時,為什么處理 private、protected 的規則要有別於 public , 這究竟有什么潛在的好處。
假設有這樣一個場景,目前電動汽車尚且處於發展的初級階段,汽車品牌特斯拉、蔚來的最大里程數 maxMileage 值一樣。
interface Car {
maxMileage: number;
}
class Tesla implements Car {
maxMileage: number = 500;
}
class Nio implements Car {
maxMileage: number = 500;
}
function drive(car :Tesla) {
console.log(car.maxMileage)
}
let tesla = new Tesla();
let nio = new Nio();
drive(tesla); // ✔️
drive(nio); // ✔️
由於TypeScript是結構式語言,因Tesla、Nio又有着相同名稱、類型的字段 maxMileage ,即使 drive 入參聲明為 Tesla 類型,也能通過校驗。目前而言,即使誤用,drive 的表現一樣,不會有問題,但隨着技術的發展,兩個品牌的 maxMileage 值將不一樣,drive 的行為也將千差萬別。這個bug將一直潛伏着,直到引起嚴重故障才會引起關注。
在上例基礎上增加1) 2) 兩處,多了 private(protected亦可) 聲明的 brand 屬性,來解決結構一樣,但又想區分類型的場景,達到類似聲明式類型系統的效果。這里就是利用了private、protected屬性必須源於同一處聲明才可判定類型兼容。
class Tesla implements Car {
private brand: string = "Tesla"; // 1)
maxMileage: number = 500;
}
class Nio implements Car {
private brand: string = "Tesla"; //2)
maxMileage: number = 500;
}
function drive(car :Tesla) {
console.log(car.maxMileage)
}
let tesla = new Tesla();
let nio = new Nio();
drive(tesla); // ✔️
drive(nio); // ❌
//Argument of type 'Nio' is not assignable to parameter of type 'Tesla'.
//Types have separate declarations of a private property 'brand'.
//編譯后
class Tesla {
constructor() {
this.brand = "Tesla";
this.maxMileage = 500;
}
}
class Nio {
constructor() {
this.brand = "Tesla";
this.maxMileage = 500;
}
}
雖然達到了我們想要的效果,但類實例會多出 brand 屬性,增加了運行時開銷,如果這不是你想要的,可以如下處理:
class Tesla implements Car {
//@ts-ignore
private brand: string;
maxMileage: number = 500;
}
class Nio implements Car {
//@ts-ignore
private brand: string ;
maxMileage: number = 500;
}
//編譯后
class Tesla {
constructor() {
this.maxMileage = 500;
}
}
class Nio {
constructor() {
this.maxMileage = 500;
}
}
可以看到編譯后的代碼很純凈了。//@ts-ignore僅在 strictPropertyInitialization: true 時需要,避免因未初始化屬性而編譯報錯。
Types have separate declarations of a private property 報錯還會出現在類extends繼承的時候。初看很奇怪,使用姿勢不同,但報錯信息且類似。
class ElectricVehicle {
private charge() {};
}
//Type 'FF91' is not assignable to type 'ElectricVehicle'.
// Types have separate declarations of a private property 'charge'
class FF91 extends ElectricVehicle { // ❌
private charge() {};
}
通過將 private 改成 protected或public 可以修復。很多文章會提到這是由於 private 語義上是私有的,對子類不可見,所以不能進行覆蓋,而protected、public 語義上就是對子類可見的,子類知道當前在進行覆蓋行為,這只是一方面。
我們假設 TypeScript 允許覆蓋 private 方法,上面的類聲明編譯通過。但當我們執行下面語句時,上面的報錯再次出現。
let parent = new ElectricVehicle();
let child = new FF91();
parent = child; // ❌
//Type 'FF91' is not assignable to type 'ElectricVehicle'.
// Types have separate declarations of a private property 'charge'
最初的示例,Foo、Bar 只是兩個結構類似的類,並無繼承關系,判定類型不兼容尚可理解。這里父子類之間類型不兼容就沒法自圓了。
所以編譯器提前在類聲明時就報錯,避免延后到使用階段。這也是為什么 FF91 類聲明繼承時的報錯信息和前面的一樣。
