首先我們要清楚 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
類聲明繼承時的報錯信息和前面的一樣。