TypeScript高級類型


交叉類型(Intersection Types)

交叉類型是將多個類型合並為一個類型。 這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如,Person & Serializable & Loggable同時是PersonSerializableLoggable。 就是說這個類型的對象同時擁有了這三種類型的成員。

我們大多是在混入(mixins)或其它不適合典型面向對象模型的地方看到交叉類型的使用。 (在JavaScript里發生這種情況的場合很多!) 下面是如何創建混入的一個簡單例子:

function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result; } class Person { constructor(public name: string) { } } interface Loggable { log(): void; } class ConsoleLogger implements Loggable { log() { // ... } } var jim = extend(new Person("Jim"), new ConsoleLogger()); var n = jim.name; jim.log(); 

聯合類型(Union Types)

聯合類型與交叉類型很有關聯,但是使用上卻完全不同。 偶爾你會遇到這種情況,一個代碼庫希望傳入numberstring類型的參數。 例如下面的函數:

/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */ function padLeft(value: string, padding: any) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); } padLeft("Hello world", 4); // returns " Hello world" 

padLeft存在一個問題,padding參數的類型指定成了any。 這就是說我們可以傳入一個既不是number也不是string類型的參數,但是TypeScript卻不報錯。

let indentedString = padLeft("Hello world", true); // 編譯階段通過,運行時報錯 

在傳統的面向對象語言里,我們可能會將這兩種類型抽象成有層級的類型。 這么做顯然是非常清晰的,但同時也存在了過度設計。 padLeft原始版本的好處之一是允許我們傳入原始類型。 這樣做的話使用起來既簡單又方便。 如果我們就是想使用已經存在的函數的話,這種新的方式就不適用了。

代替any, 我們可以使用聯合類型做為padding的參數:

/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */ function padLeft(value: string, padding: string | number) { // ... } let indentedString = padLeft("Hello world", true); // errors during compilation 

聯合類型表示一個值可以是幾種類型之一。 我們用豎線(|)分隔每個類型,所以number | string | boolean表示一個值可以是numberstring,或boolean

如果一個值是聯合類型,我們只能訪問此聯合類型的所有類型里共有的成員。

interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors 

這里的聯合類型可能有點復雜,但是你很容易就習慣了。 如果一個值的類型是A | B,我們能夠確定的是它包含了AB中共有的成員。 這個例子里,Bird具有一個fly成員。 我們不能確定一個Bird | Fish類型的變量是否有fly方法。 如果變量在運行時是Fish類型,那么調用pet.fly()就出錯了。

類型保護與區分類型(Type Guards and Differentiating Types)

聯合類型適合於那些值可以為不同類型的情況。 但當我們想確切地了解是否為Fish時怎么辦? JavaScript里常用來區分2個可能值的方法是檢查成員是否存在。 如之前提及的,我們只能訪問聯合類型中共同擁有的成員。

let pet = getSmallPet(); // 每一個成員訪問都會報錯 if (pet.swim) { pet.swim(); } else if (pet.fly) { pet.fly(); } 

為了讓這段代碼工作,我們要使用類型斷言:

let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); } 

用戶自定義的類型保護

這里可以注意到我們不得不多次使用類型斷言。 假若我們一旦檢查過類型,就能在之后的每個分支里清楚地知道pet的類型的話就好了。

TypeScript里的類型保護機制讓它成為了現實。 類型保護就是一些表達式,它們會在運行時檢查以確保在某個作用域里的類型。 要定義一個類型保護,我們只要簡單地定義一個函數,它的返回值是一個類型謂詞

function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; } 

在這個例子里,pet is Fish就是類型謂詞。 謂詞為parameterName is Type這種形式,parameterName必須是來自於當前函數簽名里的一個參數名。

每當使用一些變量調用isFish時,TypeScript會將變量縮減為那個具體的類型,只要這個類型與變量的原始類型是兼容的。

// 'swim' 和 'fly' 調用都沒有問題了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); } 

注意TypeScript不僅知道在if分支里petFish類型; 它還清楚在else分支里,一定不是Fish類型,一定是Bird類型。

typeof類型保護

現在我們回過頭來看看怎么使用聯合類型書寫padLeft代碼。 我們可以像下面這樣利用類型斷言來寫:

function isNumber(x: any): x is number { return typeof x === "number"; } function isString(x: any): x is string { return typeof x === "string"; } function padLeft(value: string, padding: string | number) { if (isNumber(padding)) { return Array(padding + 1).join(" ") + value; } if (isString(padding)) { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); } 

然而,必須要定義一個函數來判斷類型是否是原始類型,這太痛苦了。 幸運的是,現在我們不必將typeof x === "number"抽象成一個函數,因為TypeScript可以將它識別為一個類型保護。 也就是說我們可以直接在代碼里檢查類型了。

function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); } 

這些typeof類型保護只有兩種形式能被識別:typeof v === "typename"typeof v !== "typename""typename"必須是"number""string""boolean""symbol"。 但是TypeScript並不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。

instanceof類型保護

如果你已經閱讀了typeof類型保護並且對JavaScript里的instanceof操作符熟悉的話,你可能已經猜到了這節要講的內容。

instanceof類型保護是通過構造函數來細化類型的一種方式。 比如,我們借鑒一下之前字符串填充的例子:

interface Padder { getPaddingString(): string } class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) { } getPaddingString() { return Array(this.numSpaces + 1).join(" "); } } class StringPadder implements Padder { constructor(private value: string) { } getPaddingString() { return this.value; } } function getRandomPadder() { return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" "); } // 類型為SpaceRepeatingPadder | StringPadder let padder: Padder = getRandomPadder(); if (padder instanceof SpaceRepeatingPadder) { padder; // 類型細化為'SpaceRepeatingPadder' } if (padder instanceof StringPadder) { padder; // 類型細化為'StringPadder' } 

instanceof的右側要求是一個構造函數,TypeScript將細化為:

  1. 此構造函數的prototype屬性的類型,如果它的類型不為any的話
  2. 構造簽名所返回的類型的聯合

以此順序。

可以為null的類型

TypeScript具有兩種特殊的類型,nullundefined,它們分別具有值null和undefined. 我們在基礎類型一節里已經做過簡要說明。 默認情況下,類型檢查器認為nullundefined可以賦值給任何類型。 nullundefined是所有其它類型的一個有效值。 這也意味着,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種情況也不行。 null的發明者,Tony Hoare,稱它為價值億萬美金的錯誤

--strictNullChecks標記可以解決此錯誤:當你聲明一個變量時,它不會自動地包含nullundefined。 你可以使用聯合類型明確的包含它們:

let s = "foo"; s = null; // 錯誤, 'null'不能賦值給'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能賦值給'string | null' 

注意,按照JavaScript的語義,TypeScript會把nullundefined區別對待。string | nullstring | undefinedstring | undefined | null是不同的類型。

可選參數和可選屬性

使用了--strictNullChecks,可選參數會被自動地加上| undefined:

function f(x: number, y?: number) { return x + (y || 0); } f(1, 2); f(1); f(1, undefined); f(1, null); // error, 'null' is not assignable to 'number | undefined' 

可選屬性也會有同樣的處理:

class C { a: number; b?: number; } let c = new C(); c.a = 12; c.a = undefined; // error, 'undefined' is not assignable to 'number' c.b = 13; c.b = undefined; // ok c.b = null; // error, 'null' is not assignable to 'number | undefined' 

類型保護和類型斷言

由於可以為null的類型是通過聯合類型實現,那么你需要使用類型保護來去除null。 幸運地是這與在JavaScript里寫的代碼一致:

function f(sn: string | null): string { if (sn == null) { return "default"; } else { return sn; } } 

這里很明顯地去除了null,你也可以使用短路運算符:

function f(sn: string | null): string { return sn || "default"; } 

如果編譯器不能夠去除nullundefined,你可以使用類型斷言手動去除。 語法是添加!后綴:identifier!identifier的類型里去除了nullundefined

function broken(name: string | null): string { function postfix(epithet: string) { return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null } name = name || "Bob"; return postfix("great"); } function fixed(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; // ok } name = name || "Bob"; return postfix("great"); } 

本例使用了嵌套函數,因為編譯器無法去除嵌套函數的null(除非是立即調用的函數表達式)。 因為它無法跟蹤所有對嵌套函數的調用,尤其是你將內層函數做為外層函數的返回值。 如果無法知道函數在哪里被調用,就無法知道調用時name的類型。

類型別名

類型別名會給一個類型起個新名字。 類型別名有時和接口很像,但是可以作用於原始值,聯合類型,元組以及其它任何你需要手寫的類型。

type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } } 

起別名不會新建一個類型 - 它創建了一個新名字來引用那個類型。 給原始類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。

同接口一樣,類型別名也可以是泛型 - 我們可以添加類型參數並且在別名聲明的右側傳入:

type Container<T> = { value: T }; 

我們也可以使用類型別名來在屬性里引用自己:

type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; } 

與交叉類型一起使用,我們可以創建出一些十分稀奇古怪的類型。

type LinkedList<T> = T & { next: LinkedList<T> }; interface Person { name: string; } var people: LinkedList<Person>; var s = people.name; var s = people.next.name; var s = people.next.next.name; var s = people.next.next.next.name; 

然而,類型別名不能出現在聲明右側的任何地方。

type Yikes = Array<Yikes>; // error 

接口 vs. 類型別名

像我們提到的,類型別名可以像接口一樣;然而,仍有一些細微差別。

其一,接口創建了一個新的名字,可以在其它任何地方使用。 類型別名並不創建新名字—比如,錯誤信息就不會使用別名。 在下面的示例代碼里,在編譯器中將鼠標懸停在interfaced上,顯示它返回的是Interface,但懸停在aliased上時,顯示的卻是對象字面量類型。

type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface; 

另一個重要區別是類型別名不能被extendsimplements(自己也不能extendsimplements其它類型)。 因為軟件中的對象應該對於擴展是開放的,但是對於修改是封閉的,你應該盡量去使用接口代替類型別名。

另一方面,如果你無法通過接口來描述一個類型並且需要使用聯合類型或元組類型,這時通常會使用類型別名。

字符串字面量類型

字符串字面量類型允許你指定字符串必須的固定值。 在實際應用中,字符串字面量類型可以與聯合類型,類型保護和類型別名很好的配合。 通過結合使用這些特性,你可以實現類似枚舉類型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out"; class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } } } let button = new UIElement(); button.animate(0, 0, "ease-in"); button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here 

你只能從三種允許的字符中選擇其一來做為參數傳遞,傳入其它值則會產生錯誤。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面量類型還可以用於區分函數重載:

function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // ... more overloads ... function createElement(tagName: string): Element { // ... code goes here ... } 

數字字面量類型

TypeScript還具有數字字面量類型。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... } 

我們很少直接這樣使用,但它們可以用在縮小范圍調試bug的時候:

function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // Operator '!==' cannot be applied to types '1' and '2'. } } 

換句話說,當x2進行比較的時候,它的值必須為1,這就意味着上面的比較檢查是非法的。

枚舉成員類型

如我們在枚舉一節里提到的,當每個枚舉成員都是用字面量初始化的時候枚舉成員是具有類型的。

在我們談及“單例類型”的時候,多數是指枚舉成員類型和數字/字符串字面量類型,盡管大多數用戶會互換使用“單例類型”和“字面量類型”。

可辨識聯合(Discriminated Unions)

你可以合並單例類型,聯合類型,類型保護和類型別名來創建一個叫做可辨識聯合的高級模式,它也稱做標簽聯合代數數據類型。 可辨識聯合在函數式編程很有用處。 一些語言會自動地為你辨識聯合;而TypeScript則基於已有的JavaScript模式。 它具有3個要素:

  1. 具有普通的單例類型屬性—可辨識的特征
  2. 一個類型別名包含了那些類型的聯合—聯合
  3. 此屬性上的類型保護。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; } 

首先我們聲明了將要聯合的接口。 每個接口都有kind屬性但有不同的字符串字面量類型。 kind屬性稱做可辨識的特征標簽。 其它的屬性則特定於各個接口。 注意,目前各個接口間是沒有聯系的。 下面我們把它們聯合到一起:

type Shape = Square | Rectangle | Circle; 

現在我們使用可辨識聯合:

function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } } 

完整性檢查

當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了TriangleShape,我們同時還需要更新area:

type Shape = Square | Rectangle | Circle | Triangle; function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle" } 

有兩種方式可以實現。 首先是啟用--strictNullChecks並且指定一個返回值類型:

function area(s: Shape): number { // error: returns number | undefined switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } } 

因為switch沒有包涵所有情況,所以TypeScript認為這個函數有時候會返回undefined。 如果你明確地指定了返回值類型為number,那么你會看到一個錯誤,因為實際上返回值的類型為number | undefined。 然而,這種方法存在些微妙之處且--strictNullChecks對舊代碼支持不好。

第二種方法使用never類型,編譯器用它來進行完整性檢查:

function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error here if there are missing cases } } 

這里,assertNever檢查s是否為never類型—即為除去所有可能情況后剩下的類型。 如果你忘記了某個case,那么s將具有一個真實的類型並且你會得到一個錯誤。 這種方式需要你定義一個額外的函數,但是在你忘記某個case的時候也更加明顯。

多態的this類型

多態的this類型表示的是某個包含類或接口的子類型。 這被稱做F-bounded多態性。 它能很容易的表現連貫接口間的繼承,比如。 在計算器的例子里,在每個操作之后都返回this類型:

class BasicCalculator { public constructor(protected value: number = 0) { } public currentValue(): number { return this.value; } public add(operand: number): this { this.value += operand; return this; } public multiply(operand: number): this { this.value *= operand; return this; } // ... other operations go here ... } let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue(); 

由於這個類使用了this類型,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。

class ScientificCalculator extends BasicCalculator { public constructor(value = 0) { super(value); } public sin() { this.value = Math.sin(this.value); return this; } // ... other operations go here ... } let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue(); 

如果沒有this類型,ScientificCalculator就不能夠在繼承BasicCalculator的同時還保持接口的連貫性。 multiply將會返回BasicCalculator,它並沒有sin方法。 然而,使用this類型,multiply會返回this,在這里就是ScientificCalculator

索引類型(Index types)

使用索引類型,編譯器就能夠檢查使用了動態屬性名的代碼。 例如,一個常見的JavaScript模式是從對象中選取屬性的子集。

function pluck(o, names) { return names.map(n => o[n]); } 

下面是如何在TypeScript里使用此函數,通過索引類型查詢索引訪問操作符:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person = { name: 'Jarid', age: 35 }; let strings: string[] = pluck(person, ['name']); // ok, string[] 

編譯器會檢查name是否真的是Person的一個屬性。 本例還引入了幾個新的類型操作符。 首先是keyof T索引類型查詢操作符。 對於任何類型Tkeyof T的結果為T上已知的公共屬性名的聯合。 例如:

let personProps: keyof Person; // 'name' | 'age' 

keyof Person是完全可以與'name' | 'age'互相替換的。 不同的是如果你添加了其它的屬性到Person,例如address: string,那么keyof Person會自動變為'name' | 'age' | 'address'。 你可以在像pluck函數這類上下文里使用keyof,因為在使用之前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給pluck

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age' 

第二個操作符是T[K]索引訪問操作符。 在這里,類型語法反映了表達式語法。 這意味着person['name']具有類型Person['name'] — 在我們的例子里則為string類型。 然而,就像索引類型查詢一樣,你可以在普通的上下文里使用T[K],這正是它的強大所在。 你只要確保類型變量K extends keyof T就可以了。 例如下面getProperty函數的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] } 

getProperty里的o: Tname: K,意味着o[name]: T[K]。 當你返回T[K]的結果,編譯器會實例化鍵的真實類型,因此getProperty的返回值類型會隨着你需要的屬性改變。

let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age' 

索引類型和字符串索引簽名

keyofT[K]與字符串索引簽名進行交互。 如果你有一個帶有字符串索引簽名的類型,那么keyof T會是string。 並且T[string]為索引簽名的類型:

interface Map<T> { [key: string]: T; } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number 

映射類型

一個常見的任務是將一個已知的類型每個屬性都變為可選的:

interface PersonPartial { name?: string; age?: number; } 

或者我們想要一個只讀版本:

interface PersonReadonly { readonly name: string; readonly age: number; } 

這在JavaScript里經常出現,TypeScript提供了從舊類型中創建新類型的一種方式 — 映射類型。 在映射類型里,新類型以相同的形式去轉換舊類型里每個屬性。 例如,你可以令每個屬性成為readonly類型或可選的。 下面是一些例子:

type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Partial<T> = { [P in keyof T]?: T[P]; } 

像下面這樣使用:

type PersonPartial = Partial<Person>; type ReadonlyPerson = Readonly<Person>; 

下面來看看最簡單的映射類型和它的組成部分:

type Keys = 'option1' | 'option2'; type Flags = { [K in Keys]: boolean }; 

它的語法與索引簽名的語法類型,內部使用了for .. in。 具有三個部分:

  1. 類型變量K,它會依次綁定到每個屬性。
  2. 字符串字面量聯合的Keys,它包含了要迭代的屬性名的集合。
  3. 屬性的結果類型。

在個簡單的例子里,Keys是硬編碼的的屬性名列表並且屬性類型永遠是boolean,因此這個映射類型等同於:

type Flags = { option1: boolean; option2: boolean; } 

在真正的應用里,可能不同於上面的ReadonlyPartial。 它們會基於一些已存在的類型,且按照一定的方式轉換字段。 這就是keyof和索引訪問類型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null } type PartialPerson = { [P in keyof Person]?: Person[P] } 

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null } type Partial<T> = { [P in keyof T]?: T[P] } 

在這些例子里,屬性列表是keyof T且結果類型是T[P]的變體。 這是使用通用映射類型的一個好模版。 因為這類轉換是同態的,映射只作用於T的屬性而沒有其它的。 編譯器知道在添加任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設Person.name是只讀的,那么Partial<Person>.name也將是只讀的且為可選的。

下面是另一個例子,T[P]被包裝在Proxy<T>類里:

type Proxy<T> = { get(): T; set(value: T): void; } type Proxify<T> = { [P in keyof T]: Proxy<T[P]>; } function proxify<T>(o: T): Proxify<T> { // ... wrap proxies ... } let proxyProps = proxify(props); 

注意Readonly<T>Partial<T>用處不小,因此它們與PickRecord一同被包含進了TypeScript的標准庫里:

type Pick<T, K extends keyof T> = { [P in K]: T[P]; } type Record<K extends string, T> = { [P in K]: T; } 

ReadonlyPartialPick是同態的,但Record不是。 因為Record並不需要輸入類型來拷貝屬性,所以它不屬於同態:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string> 

非同態類型本質上會創建新的屬性,因此它們不會從它處拷貝屬性修飾符。

由映射類型進行推斷

現在你了解了如何包裝一個類型的屬性,那么接下來就是如何拆包。 其實這也非常容易:

function unproxify<T>(t: Proxify<T>): T { let result = {} as T; for (const k in t) { result[k] = t[k].get(); } return result; } let originalProps = unproxify(proxyProps); 

注意這個拆包推斷只適用於同態的映射類型。 如果映射類型不是同態的,那么需要給拆包函數一個明確的類型參數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM