typescript学习整理

typescript现在已经慢慢变成前端工程师必学必会的技能了,它在开发大型应用的时候可以让产品更加可控,本篇就试着理出typescript一些必须要掌握的知识点。

原始类型

js有五种基本类型 string、number、boolean、null、undefined,这几种类型typescript都赋予了对应的类型限定,如下:

  1. 布尔值

      let bool: boolean = false
    

    注意利用Boolean创造的对象不是布尔值:

typescript现在已经慢慢变成前端工程师必学必会的技能了,它在开发大型应用的时候可以让产品更加可控,本篇就试着理出typescript一些必须要掌握的知识点。

原始类型

js有五种基本类型 string、number、boolean、null、undefined,这几种类型typescript都赋予了对应的类型限定,如下:

  1. 布尔值

       bool: boolean = false     
    

    注意`利用Boolean创造的对象不是布尔值:

    let boolObject: boolean = new Boolean(1) //会报错
    

    new Boolean创造的是一个对象并不是布尔值,

    直接调用Boolean倒是可以

     let bool: boolean = Boolean(1) 
    
  2. 字符串

     str: string = 'hellow'
    // 模板字符串
    let sentence: string = `Hello, my name is ${myName}.
    I'll be ${myAge + 1} years old next month.`;      
    

    编译结果:

      var myName = 'Tom';
    var myAge = 25;
    // 模板字符串
    var sentence = "Hello, my name is " + myName + ".\nI'll be " + (myAge + 1) + " years old next month.";      ```
    
  3. 数值

    let decLiteral: number = 6;
    let hexLiteral: number = 0xf00d;
    // ES6 中的二进制表示法
    let binaryLiteral: number = 0b1010;
    // ES6 中的八进制表示法
    let octalLiteral: number = 0o744;
    let notANumber: number = NaN;
    let infinityNumber: number = Infinity;     
    

    编译结果:

       var decLiteral = 6;
    var hexLiteral = 0xf00d;
    // ES6 中的二进制表示法
    var binaryLiteral = 10;
    // ES6 中的八进制表示法
    var octalLiteral = 484;
    var notANumber = NaN;
    var infinityNumber = Infinity;      
    

    其中 0b10100o744ES6 中的二进制和八进制表示法,它们会被编译为十进制数字。

  4. null和undefined

    null和undefined在typescript中分别是两个独立的类型限定:

      let u: undefined = undefined;
      let n: null = null;     
    

    undefined限定的变量只能赋值undefined,null也一样。

  5. 空值void
    void限定的变量只能赋值null或者undefined:

     let noContent: void = null;
    

    与null和undefined限定不同的是,null和undefined是所有类型的子类型:

      let num: number = undefined; //这样没有问题
    // 这样也不会报错
    let u: undefined;
    let num: number = u;
    

    而void类型的变量不能互相随意传递:

      let noContent: void;
      let num: number = noContent; // 报错
    
  6. 任意类型 (any)

    被指定为任意类型的变量可以被赋值任何类型,也能被调用任何属性方法,都不会报错:

    let myFavoriteNumber: string = 'seven';
    myFavoriteNumber = 7;
    
    let anyThing: any = 'Tom';
    anyThing.setName('Jerry');
    anyThing.setName('Jerry').sayHello();
    anyThing.myName.setFirstName('Cat');
    

    注意:没有指定类型的变量,默认就是any类型

类型推论

当变量被赋值的时候没有被指定类型限定,会根据变量类型默认设置类型限定:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// 等价于下边
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

联合类型

表示取值为多种类型中的一个

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// 只能赋值string或者number
myFavoriteNumber = true // 会报错

访问联合类型的属性和方法

联合类型的变量只能访问类型共有的属性和方法

function getLength(something: string | number): number {
   return something.length; // 会报错,字符串不含有length属性
 //想解决报错可以使用类型断言
 return <string>something.length // 这样就不会报错
}
// 联合类型当赋值为一个类型会推断赋值为那个类型
myFavoriteNumber = 'ss' // 变量被设定为字符串
myFavoriteNumber.slice(0) // ok
myFavoriteNumber = 5 // 变量被推断为number类型
myFavoriteNumber.slice(0) // 会报错

接口

接口是面向对象中的概念,表示行为的抽象,具体如何行动通过类实现,在typescript中本人觉得接口主要是起到对编码中的一种约束,可以减少很多错误,并且配合很多ide(比如webstorm)代码提示可以提高编码效率。

简单例子

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
}; // ok
let lilei: Person = {
      name: 'Tom',
    age: 25,
  sexy: 'name'
} // 错误 sexy属性并没有定义

可选属性

上边例子中定义的Person接口限定的变量,必须同时设置name和age两个属性,不然会报错:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
}; // error 缺少 age属性

如果要解决这个,就需要设置可选属性,在变量前边加上

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

let tom: Person = {
   name: 'Tom'
};//ok

任意属性

有时候我们希望一个接口准许有任意属性,可以使用如下方式:

interface Person {
   name: string;
   age?: number;
   [propName: string]: any;
}

let tom: Person = {
   name: 'Tom',
   gender: 'male'
}; // ok

使用 [propName: string] 定义了任意属性取 string 类型的值。

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface Person {
  name: string;
  age?: number;
  [propName: string]: string;
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
};

上边例子中[propName: string]: string;将整个接口设置;属性名为string类型,属性值为string类型,age值为number不符合这个类型,所以报错。

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性,只读属性有两个约束:

  1. 对象创建时候要赋值

  2. 赋值后不能再重新修改属性值了

例子:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
}; // 报错 没有给id赋值

tom.id = 89757; // id是只读,不能修改

数组类型

数组类型定义方式

类型 + 方括号

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push(true) // 报错  不是number类型

数组范型

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

接口表示

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

函数类型

函数声明

function sum(x: number, y: number): number {
    return x + y;
}

// 表达式声明方式
let mySum = function (x: number, y: number): number {
    return x + y;
};

Typescript添加了对函数变量和返回值类型的限定

接口定义函数类型

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

可选参数

函数有时候会定义一些是必须传递的参数,函数的可选参数定义和接口中对象的可选参数定义方式一样,都是使用,如下:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom')

注意:可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了

function buildName(firstName?: string, lastName: string) {
    if (firstName) {
        return firstName + ' ' + lastName;
    } else {
        return lastName;
    }
} // 报错

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此时就不受可选参数必须接在必须参数后面的限制了:

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

方式

<类型>值

值 as 类型

在 tsx 语法(React 的 jsx 语法的 ts 版)中必须用后一种。

使用场景

之前有个例子当函数变量使用联合类型,如果我们在函数体中需要特定类型的方法,就需要使用类型断言,不然编译的时候就会报错:

function getLength(something: string | number): number {
    if ((<string>something).length) {
        return (<string>something).length; //不加类型断言就会报错
    } else {
        return something.toString().length;
    }
}

注意,类型断言不是类型转换,断言一个联合类型不存在的类型会报错:

function toBoolean(something: string | number): boolean {
    return <boolean>something; // 报错, 布尔类型不能赋值给something
}

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个

type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}

handleEvent(document.getElementById('hello'), 'scroll');  // 没问题
handleEvent(document.getElementById('world'), 'dbclick'); // 报错,event 不能为 'dbclick'

元组

元组可以理解为存储不同类型值的数组

let tulpe: [string, number] = ['tom', 25];
//或者
let tulpe: [string, number];
tulpe[0] = 'tom';
tulpe[1] = 25;

tulpe[0].slice(1);
tulpe[1].toFixed(2);

//也可以只赋值其中一项
let tulpe: [string, number];
tulpe[0] = 'tom';

tulpe.push(true);// 报错 赋值类型不存在申明类型中

元组使用上需要特别注意的是:

元组初始化的时候必须赋值元组声明的所有类型变量:

let tulpe: [string, number] = ['tom']; // 报错
// 或者
let tulpe: [string,number];
tulpe = ['tome'] // 报错

枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

典型例子

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

//编译后的样子
var Days;
(function (Days) {
   Days[Days["Sun"] = 0] = "Sun";
   Days[Days["Mon"] = 1] = "Mon";
   Days[Days["Tue"] = 2] = "Tue";
   Days[Days["Wed"] = 3] = "Wed";
   Days[Days["Thu"] = 4] = "Thu";
   Days[Days["Fri"] = 5] = "Fri";
   Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

手动复制

我们也可以给枚举项手动赋值:

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

未手动赋值的枚举类型变量会自动赋值(在前边变量值逐步累加1)

值得注意的是:如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的

enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

前面我们所举的例子都是常数项,一个典型的计算所得项的例子:

enum Color {Red, Green, Blue = "blue".length};

上面的例子中,"blue".length 就是一个计算所得项。

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

enum Color {Red = "red".length, Green, Blue};

常数枚举

常数枚举是使用 const enum 定义的枚举类型:

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员

上例的编译结果是:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

declare enum Directions {
   Up,
   Down,
   Left,
   Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

之前提到过,declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

上例的编译结果是:

var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

typescript中类增加的内容

  1. 限定符

    TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

    • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public

    • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问

    • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

  2. 抽象类

     abstract class Animal { // abstract标志类是抽象类
        public name;
        public constructor(name) {
            this.name = name;
        }
        public abstract sayHi();
        }
      let a = new Animal('Jack'); // 报错 抽象类不能实例化

抽象类正确的使用方式:

       abstract class Animal {
        public name;
        public constructor(name) {
              this.name = name;
        }
        public abstract sayHi();
        }

      class Cat extends Animal {
        public sayHi() { // 实现抽象类中的方法
          console.log(`Meow, My name is ${this.name}`);
        }
    }
    let cat = new Cat('Tom');

类的类型

类也可以作为一个类型来使用

    class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    sayHi(): string {
      return `My name is ${this.name}`;
    }
    }
    let a: Animal = new Animal('Jack');
    console.log(a.sayHi()); // My name is Jack

类与接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

类实现接口

interface Alarm {
   alert();
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
   alert() {
       console.log('SecurityDoor alert');
   }
}

class Car implements Alarm {
   alert() {
       console.log('Car alert');
   }
}

接口继承接口

interface Alarm {
   alert();
}

interface LightableAlarm extends Alarm {
   lightOn();
   lightOff();
}

接口继承类

接口也可以继承类:

class Point {
   x: number;
   y: number;
}

interface Point3d extends Point {
   z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

典型的例子

首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
      result[i] = value;
  }
  return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:

多个类型参数

定义泛型的时候,可以一次定义多个类型参数:


function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:


function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

nterface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

多个类型参数之间也可以互相约束:


function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
      target[id] = (<T>source)[id]; // 如果不使用泛型约束,这里会报错
  }
  return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型接口

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
      result[i] = value;
  }
  return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

进一步,我们可以把泛型参数提前到接口名上:


interface CreateArrayFunc<T> {
  (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
      result[i] = value;
  }
  return result;
}

createArray(3, 'x'); // ['x', 'x', 'x'] 

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
      result[i] = value;
  }
  return result;
}

声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:

函数的合并

我们可以使用重载定义多个函数类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
  if (typeof x === 'number') {
      return Number(x.toString().split('').reverse().join(''));
  } else if (typeof x === 'string') {
      return x.split('').reverse().join('');
  }
}

接口的合并

接口中的属性在合并时会简单的合并到一个接口中:

interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

相当于:

interface Alarm {
  price: number;
  weight: number;
}

注意,合并的属性的类型必须是唯一的

interface Alarm {
  price: number;
}
interface Alarm {
  price: number;  // 虽然重复了,但是类型都是 `number`,所以不会报错
  weight: number;
}
interface Alarm {
  price: number;
}
interface Alarm {
  price: string;  // 类型不一致,会报错
  weight: number;
}

// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type.  Variable 'price' must be of type 'number', but here has type 'string'.

接口中方法的合并,与函数的合并一样:

interface Alarm {
    price: number;
    alert(s: string): string;
}
interface Alarm {
    weight: number;
    alert(s: string, n: number): string;
}

相当于:

interface Alarm {
  price: number;
  weight: number;
  alert(s: string): string;
  alert(s: string, n: number): string;
}

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350