TypeScript入门教程

关于TypeScript

TypeScriptJavaScript的超集,主要提供类型系统和对ES6的支持,由Microsoft开发,第一个版本发布于2012

TypeScript的优势
  • TypeScript增加了代码的可读性和可维护性
    • 大部分的函数函数看看类型的定义就已经知道该如何使用了
    • 可以在编译的阶段就发现了大量的错误,防止在运行中发现过多的错误
    • 增强了编辑器和IDE的功能。
  • TypeScript非常的包容
    • TypeScriptJavaScript的超集,.js文件可以直接将名字转换为.ts即可
    • 即使不显式的定义类型,也可以自动的做出类型的推论
    • 可以定义从简单到复杂的一切类型
    • 即使TypeScript编译报错,也可以生成JavaScript文件
TypeScript的劣势
  • 需要一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师不是很熟悉的东西,而且现阶段的中文资料也不是很多
  • 短期内可能增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期进行维护的额项目。TypeScript能够减少维护的成本
  • 集成到构建流程需要一定的工作量
  • 可能和一些库结合的并不是很完美

原始数据类型

对于前端开发来说,主要的类型可以 分成两部分:原始数据类型和对象类型。原始数据类型包括:布尔值数值字符串nullundefined以及ES6中的新类型symbol。现在我们主要看一下这些原始值在TypeScript中的应用。

布尔值

布尔值是最基本的数据类型,在TypeScript中,使用Boolean定义布尔值类型:

let isTrue:boolean = false;

// 可以通过编译
// 对于后面来说,没有特殊强调错误代码片段,就默认为编译通过了。

注意一下,使用构造函数Boolean创建的对象不是布尔值:

let createdByNewBoolean : boolean = new Boolean(1);

// index.ts(1,5): error TS2322: Type 'Boolean' is not assignable to type 'boolean'.

上面代码报错的原因是new Boolean()返回一个Boolean对象:

let createdByNewBoolean : Boolean= new Boolean(1);

直接调用Boolean也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

TypeScript中,booleanJavaScript中的基本类型,而BooleanJavaScript 中的构造函数。其他基本类型(除了 nullundefined)一样,不再赘述。

数值

使用nubmber定义数值类型:

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

上面的编译的结果是什么:

var decLiteral = 6;
var hexLiteral = 0xf00d;
var binaryLiteral = 10;
var octalLiteral= 484;
var notNumber = NaN;
var infinityNumber = Infinity;

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

字符串

使用string定义字符串类型:

// 普通字符串
let myName:string = 'kim';
let myAge:number = 25;

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

编译结果:

var myName = 'kim';
var myAge = 25;

// 模板字符串
let sentence = "Hello,my name is"+myName+".\nI'will be"+(myAge+1)+"years old next month";
空值

JavaScript没有空值(void)的概念,在TypeScript中,可以用void表示没有任何返回值的函数。

function akertName():void{
  alert('My name is kim');
}

声明一个void类型变量是没有什么用,因为我们只能将它赋值为undefinednull:

let unusable:void = undefined
Null和undefined

TypeScript中,可以使用nullundefined来定义这两个原始数据类型:

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

undefined只能赋值为undefinednull只能赋值为null。但是这二者和void还是有区别的,nullundefined是所有类型的子类型,也就是说undefined类型的变量,可以赋值给number类型的变量:

//这样写也是没有问题的
let num:number =undefined;
let u: undefined;
let num: number = u;

但是void的类型并不是这样的,不能讲void类型的变量不能赋值给number类型的变量:

let u: void;
let num: number = u;

//index.ts(2,5): error TS2322: Type 'void' is not assignable to type 'number'.

任意值

任意值(Any)用来表示允许赋值为任意类型。

什么是任意值类型

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let myFavoriteNumber:string ='seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 类型,则允许被赋值为任意类型。

let myFavoriteNumber : any ='seven';
myFavoriteNumber = 7;
任意值的属性和方法

在任何值上访问任何属性都是允许的:

let anyThing :any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

也允许调用任何方法:

let anyThing : any = 'kim';
anyThing.setName('Tom');
anyThing.setName('Tom').sayHello();
anyThing.myName.setFirstName('kim');

可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。

未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let something;
something = 'seven';
something = 7;
something.setName('kim');

上面的代码等价于:

let something:any;
something = 'seven';
something = 7;
something.setName('kim');

类型推论

如果没有明确的字段,那么TypeScript会依照类型推论的规则判断出一个类型。
下面的代码虽然没有指定类型,但是会在编译的时候报错:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

上面的代码等价于:

let myFavoriteNumber:string  = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但是为什么这和any类型的还是有区别的,二者的区别在于在声明的时候没有赋值的话,就会判断成any类型,在后续就不会再进行检查。

联合类型

联合类型表示取值可以为多种类型中的一种。
让我们先举个小栗子:

let myFavoriteNumber:string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

上面的例子说明myFavoriteNumber可以是string类型或是number类型的。但是如果我们给这个值给一个布尔值,就会报错。

let myFavoriteNumber:string | number;
myFavoriteNumber = true;

// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
//   Type 'boolean' is not assignable to type 'number'.

联合类型使用 | 分隔每个类型。
这里的string|number的意思是,允许myFavoriteNumber的类型是string或是number,但是不能是其他类型。

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

TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性和方法:

function getLength(something: string | number): number {
  return something.length;
}
 
// index.ts(2,20): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length 不是stringnumber 的共有属性,所以会报错。
访问 string 和 number 的共有属性是没问题的:

function getString(something:string | number):string{
  return something.toString();
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber:string|number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错
 
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber被推断成了 string,访问它的length属性不会报错。
而第四行的myFavoriteNumber 被推断成了 number,访问它的 length属性时就报错了。

对象的类型 -- 接口

TypeScript中,我们使用接口来定义对象的类型。

什么是接口

在面向对象的语言中,接口是一个很重要的概念,他是对行为的抽象,而具体如何行动需要类去实现。TypeScript中的接口就是一个很灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对【对象的形状】进行描述。
简单的例子:

interface Person{
  name:string;
  age:number;
}
let kim:Person={
  name:'kim',
  age:18
}

在上面的例子中我们定义了一个接口Person,接着定义可一个变量kim,它的类型是person,这样,我们就约束了kim的形状和接口Person一致。
接口一般首字母大写。
接口的变量比接口少一些或是多一些属性是不允许的:

interface Person {
  name: string;
  age: number;
}
 
let kim: Person = {
  name: 'kim',
};
 
// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.


interface Person {
  name: string;
  age: number;
}
 
let kim: Person = {
  name: 'kim,
  age: 25,
  website: 'http://kim.com',
};
 
// index.ts(9,3): error TS2322: Type '{ name: string; age: number; website: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'website' does not exist in type 'Person'.
可选属性

有的时候我们希望不要完全匹配成一个形状,那么我们可用一些可选属性:

interface Person {
  name: string;
  age?: number;
}
 
let kim: Person = {
  name: 'kim',
};

let kimi: Person = {
  name: 'kimi',
  age:28
};

可选属性的意思就是这个属性的值可以是不存在的,但是在这种情况下依旧不允许添加未定义的属性,我们在下面举个栗子:

let Tom: Person = {
  name: 'Tom',
  age:28,
  website:'http://www.baidu.com',
};
// examples/playground/index.ts(9,3): error TS2322:
// Type '{ name: string; age: number; website: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'website' does not exist in type 'Person'.
任意属性

有的时候我们希望几口允许任意的属性,可以使用如下的方式:

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

let kim :Person ={
  name:'kim',
  website:'http://www.baidu.com',
};

我们对于上面的代码进行解析,使用[propName:string]定义了任意属性取string类型的值。但是需要注意的是,一旦定义了任意属性,那么确定属性和可选属性都必须是它的子属性:

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

let kim :Person ={
  name:'kim',
  age:25,
  website:'http://www.baidu.com',
}

// index.ts(3,3): error TS2411: 
//Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): 
//error TS2322: Type '{ [x: string]: string | number; name: string; age: number; website: string; }' is not 
//assignable to type 'Person'.
// Index signatures are incompatible.
//Type 'string | number' is not assignable to type 'string'.
//Type 'number' is not assignable to type 'string'.

上例中,任意属性的值允许是 string,但是可选属性 age 的值却是numbernumber 不是string的子属性,所以报错了。

另外,在报错信息中可以看出,此时 { name: ‘Xcat Liu’, age: 25, website: ‘http://xcatliu.com’ } 的类型被推断成了 { [x: string]: string | number; name: string; age: number; website: string; },这是联合类型和接口的结合。

只读属性

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

interface Person {
  readonly id: number;
  name: string;
  age?: number;
  [propName: string]: any;
}
 
let xcatliu: Person = {
  id: 89757,
  name: 'kim',
  website: 'http://www.baidu.com',
};
 
xcatliu.id = 9527;
 
// index.ts(14,9): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上面的id属性是只读属性,id在初始化之后,又被赋值了,所以会报错。只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

interface Person {
  readonly id: number;
  name: string;
  age?: number;
  [propName: string]: any;
}
 
let xcatliu: Person = {  // 没有给id进行赋值
  name: 'Xcat Liu',
  website: 'http://xcatliu.com',
};
 
xcatliu.id = 89757; // 错误
 
// index.ts(8,5): error TS2322: Type '{ name: string; website: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; website: string; }'.
// index.ts(13,9): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

数组的类型

[类型+方括号]表示法

最简单的方法是使用[类型+方括号]来标识数组:

let arr:number[] =[1,2,3,4,5]

数组的项中不允许出现其他的类型:

let arr:number[] =[1,'2',3,4,5];

//index.ts(1,5): error TS2322: Type '(number | string)[]' is not assignable to type 'number[]'.
//Type 'number | string' is not assignable to type 'number'.
//Type 'string' is not assignable to type 'number'.

现在的[1,'2',3,4,5]的类型被推断称为(number|string)[],这个是联合类型和数组的结合。

let arr2: number[] = [1,2,3,4,5,6,7];
arr2.push('8')

//index.ts(2,16): error TS2345: Argument of type 'string' is 
//not assignable to parameter of type 'number'.
数组泛型

也可以使用数组泛型(GenericArray<elemType> 来表示数组:

let arr3: Array<number> = [1, 1, 2, 3, 5];
用接口表示数组

接口也可以用来描述数组:

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

NumberArray 表示:只要 index 的类型是 number,那么值的类型必须是 number

any 在数组中的应用

一个比较常见的做法是,用any表示数组中允许出现任意类型:

let list : any[] =['kim',25,{website:'www.baidu.com'}];
类数组

类数组不是数组类型,比如arguments

function sum(){
  let args:number[] = arguments;
}
// index.ts(2,7): error TS2322: Type 'IArguments' is not assignable to type 'number[]'.
// Property 'push' is missing in type 'IArguments'.

事实上常见的类数组都有自己的接口定义,如 Arguments,NodeList, HTMLCollection 等:

function sum() {
  let args: IArguments = arguments;
}

函数的类型

函数的声明
JavaScript中,有两种常见的定义函数的方式--函数声明(Function Declaration)和函数表达式(Function Expression):

// 函数声明(Function Declaration)
function sum (x,y){
  return x+y;
}
//函数表达式(Function Expression)
let mySum = function (x,y){
  return x + y;
}

一个函数有输入和输出,要在TypeScript中对齐进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义比较简单:

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

但是输出多余的(或少于要求)参数,是不被允许的:

function sum (x:number,y:number):number{
  return x+y;
}
sum (1,2,3);
// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

sum (1);
//index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
函数表达式

如果我们现在想要写一个函数表达式的定义,我们可能会这样写:

let muSum = function(x:number,y:number):number{
  return x+y;
}

这个是可以通过编译的,但是在事实上,上面的代码只对等号右侧的匿名行数进行了类的定义,而等号左侧的mySum,是通过赋值操作对类型进行推断出来的。如果需要我们手动给mySum添加类型,代码应该只这样写的:

let mySum:(x number,y:number) =>number = 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 kimXue= buildName('Xcat', 'Xue');
let kim= buildName('kim');

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

function buildName(firstName?: string, lastName: string) {
  if (firstName) {
    return firstName + ' ' + lastName;
  } else {
    return lastName;
  }
}
let kimXue= buildName('Xcat', 'Xue');
let kim= buildName('kim');
 
// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.

上面的代码会报错,因为在first那么这个属性的后面还有必须的属性,因此报错。

参数默认值

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

function buildName(firstName: string, lastName: string = 'Xue') {
  return firstName + ' ' + lastName;
}
let kimXue= buildName('Xcat', 'Xue');
let kim= buildName('kim');

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

function buildName(firstName: string = 'kim', lastName: string) {
  return firstName + ' ' + lastName;
}
let kimXue= buildName('Xcat', 'Xue');
let kim= buildName('kim');
剩余参数

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

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

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

事实上,items是一个数组,所以我们可以用数组的类型去定义它:

function push(array:any[],...items:any[]){
  items.forEach(function (item){
    array.push(item);
  })
}

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

和上面的代码一样,rest参数只能是最后一个参数。

重载

重载允许一个函数接受不同数量或是类型的参数,并作出不同的处理。
比如,我们需要实现一个函数reverse,输入数字123的时候,输出的就是数字321,输入字符串‘hello’的时候,输出翻转的字符串‘olleh’,利用联合类型,我们可以这么实现:

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('');
  }
}

然而这样写的话,还是有一个缺点,就是不能够精确地表达,输入为数字的时候,输出的也应该是数字,输入为字符串的时候,输出也应该为字符串。这时,我们可以用重载来实现多个这样的函数:

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('');
  }
}

类型断言

类型断言可以用来绕过编译器的类型判断,手动指定一个值的类型。

语法
<类型>值
// 或
值 as 类型
// 在TSX语法 (React的JSX语法的TS版)中必须用后一种

例子:将一个联合类型的变量指定为一个更加具体的类型

TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
  return something.length;
}
 
//index.ts(2,20): error TS2339: Property 'length' does not exist on type 'string | number'.
//Property 'length' does not exist on type 'number'.

这样的时候我们就需要使用类型断言,现将something断言成string

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;
}
 
// index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
//   Type 'number' is not comparable to type 'boolean'.

声明文件

当我们使用第三方库时,我们需要引用它的声明文件:

声明语法

当我们使用第三方库,比如jQuery,我们通常这样获取一个idfoo元素:

$('#foo');
// or
jQuery('#foo');

但是在 TypeScript 中,我们并不知道$jQuery是什么东西。这时,我们需要使用 declare关键字来定义它的类型,帮助 TypeScript 判断我们传入的参数类型对不对。

declear var jQuery:(string) =>any;
jQuery('#foo');

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

声明文件

通常我们会把类型声明放在一个单独的文件中,这就是声明文件:

// jQuery.d.ts
 
declare var jQuery: (string) => any;

我们约定声明文件以.d.ts为后缀。
然后在使用到的文件的开头,用「三斜线指令」表示引用了声明文件:

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

推荐阅读更多精彩内容