JavaScript知识要点

1. 判断对象为空的方法

① 使用JSON.stringify转换为字符串

var temp = JSON.stringify(data);
if (
    JSON.stringify(data) === "{}" ||
    JSON.stringify(data) === "[]" ||
    JSON.stringify(data) === "null"
) {
    console.log("空对象");
} else {
    console.log("非空对象");
}

*②使用for...in查找key

var judgeEmpty = function () {
      for (var key in data) {
        if (key) {
          return "非空对象";
        }
      }
      return "空对象";
    };
    console.log(judgeEmpty(data));

③使用ES6自带的,Object.keys(data)返回的是key值的数组

Object.keys(data).length === 0 

④使用Object对象的Object.getOwnPropertyNames

Object.getOwnPropertyNames(data).length === 0

2. Object.keys和Object.getOwnPropertyNames的区别

Object.key和Object.getOwnPropertyNames都是获取对象的key值,但是Object.key获取的是可枚举的,后者则是全部列出来。需要注意的是Symbol类型的key,都无法获取

var obj = {};
var id = Symbol("id")
Object.defineProperties(obj, {
    a: { enumerable: true, value: 1 },
    b: { enumerable: false, value: 2 },
    [id]: { enumerable: true, value: 3 }
});
for (var key in obj) {
    console.log(key, obj[key]);
}
// ->> a 1
console.log(Object.keys(obj));
// ->> ["a"]
console.log(Object.getOwnPropertyNames(obj));
// ->> ["a", "b"]

3. null和undefined的区别

  • 类型
typeof null; // object
typeof undefined // undefined
  • 类型转换
console.log(Number(undefined), Number(), Number(null)); // NaN 0 0

console.log(!null, !undefined); // true true
  • 类型判断
null == undefined // true
null === undefined // false
  • 出现场景

null表示为空对象,没有这个对象;

① 定义一个变量的初始值

var a = null;

② 作为函数中传参,某些参数为空

function print(p1, p2, p3) {
    console.log("p1=" + p1, "p2=" + p2, "p3=" + p3);
}
print(1, null, 3);

③ 作为对象原型链的终点

console.log(Object.getPrototypeOf(Object.prototype)); // null

undefined表示此处应该有一个值,但是未定义,表示原始值;
①定义了变量未赋值

var a;
console.log(a);

②对象中不存在的属性

console.log(console.a);

③函数中参数未赋值

function print(a) {
    console.log(a)
}
print();

参考阮一峰

4. typeof和instanceof区别

typeof可判别的类型有:
number,string, boolean, bigint, symbol, object, function

但是对于object, array, null都会被判别为object

console.log(typeof {}, typeof [], typeof null); // object object object

instanceof是判别对象实例是谁的,比较是否是同一个原型

function Person() {
    var name = "lin";
}
var a = new Person();
console.log(a instanceof Person); // true
console.log(Object.getPrototypeOf(a) === Person.prototype); // true

对于typeof无法区别的类型

console.log([] instanceof Array); // true

但是需要注意的是原始数据类型的类型判断,只有转成基本包装类型对象才能正确比较

console.log(1 instanceof Number); //false
var num = new Number(1);
console.log(num instanceof Number); // true

5. 判断一个对象为null,undefined, NaN,JSON对象,数组方法

  • 判断为null
data === null
Object.prototype.toString.call(data) == "[object Null]"
  • 判断为undefined

这个比较简单

typeof data === 'undefined'
  • 判断为NaN
isNaN(data)
  • JSON对象(有问题,如果是new Object()的对象)
typeof(data) == 'object' && Object.prototype.toString.call(data) == "[object Object]"
&& !data.length
  • 判断为数组
data instanceof Array // 如果是数组data instanceof Object也是true
Object.prototype.toString.call(data) == "[object Array]"
Array.isArray(data)

6. == 和 ===区别,效率上哪个更高

== 判断规则:

首先先判断类型,相同则直接比较大小,如果是number类型的,其中有NaN,结果为false

NaN == NaN // false

如果不相同,则按照如下规则转换

(1)如果是为null == undefined的比较,则返回 true

(2)如果是string == number两种类型数据的比较,则将string的转为number再比较

(3)如果是boolean == any,先Number(boolean),再比较规则从头再来比较

(4)如果是object == string | number | symbol,则会将object转为基本类型,转换方式

先调用valueOf(),如果结果为基本类型则返回,否则调用toString方法,结果为基本类型则返回,否则比较结果为false

(5)除此之外的情况,统统返回false

转换方法:

  • data.valuef()
  • data.toString()

举个例子:

var data = {
    valueOf() {
    return 1;
},
toString() {
    return "a";
}
console.log(data == 1, data == "a"); // true false

也可以自定义转换方法

 var data = {
     valueOf() {
        return 1;
     },
     toString() {
        return "a";
     },
     [Symbol.toPrimitive]() {
        return "b";
     },
 };
 console.log(data == 1, data == "a", data == "b"); // false false true

https://user-gold-cdn.xitu.io/2018/12/19/167c4a2627fe55f1?w=1005&h=426&f=png&s=38534&ynotemdtimestamp=1589420716562
参考链接

参考资料

比较方式流程图:


流程图

=== 判断规则:

(1)如果类型不一样,则返回false
(2)类型相同,如果同样是null或者同样是undefined就返回true

(3)如果有NaN,则返回false,即NaN === NaN 结果为false
(4)如果数值+0和-0 比较结果是true,数值相同为tru
(5)同一个Symbol,结果是true
(6)x和y同为true或false结果为true
(7)字符串完全相同为true
其他情况为false
送命题:
①[] == ![]的结果: true

分析:

![] 的结果为false,为boolen类型,--》0
[]为对象则转为基本类型
[].valueOf()结果为[]
[].toString()结果为""
"" == 0, Number("")结果为0
所以结果为true

②{} == !{}的结果: false
分析:

{}.valueOf()  // {}
{}.toString() // [object Object]

③{} == {}的结果:false
因为{}是对象,两个{}的地址不同,自然就不同,像下面这种就是一样的

var a = {};
b = a;
console.log(a == b); //true

④[] == []的结果:false
与③同理

==和===效率比较

在Array, Object地址比较则没有区别,两个都需要操作类型,判断类型,但是==必要时候还需要类型转换,这时候===效率会高一点

7. bind、call、apply的区别

这三个都是用来改变上下文的执行环境,即改变this的指向。
bind和其他两个的区别是bind是返回这个函数,而另外两个则是会直接执行。
call和apply第一个参数都是指向,后面的参数,call是用列表的形式传递,apply是用数组的形式传递
使用方式:

class C1 {
      constructor(name) {
        this.name = name;
      }
      showName = function () {
        console.log(this.name);
      };
}
 var f1 = new C1("b");
 f1.showName(18, "man"); // b 18 man
 f1.showName.call(obj1, 19, "man"); // a 19 man
 f1.showName.apply(obj1, [20, "man"]); // a 20 man
 var func = f1.showName.bind(obj1);
 func(21, "man"); // a 21 man

应用:
①将伪数组转为数组

比如操作dom时候获取div对象数组,类型为HtmlCollection,我们又想要它可以使用数组的操作方法,则

var div = document.getElementsByTagName("div");
var arr1 = Array.prototype.slice.call(div);
//或者
var arr2 = [].slice.call(div);

或者使用ES6的方法:

Array.from(div) //Array的内置方法
[...div] // 扩展运算符

数组调用slice()返回的就是数组

②将函数的arguments转为数组
这时候使用apply和call都是可以的

 function func1() {
      console.log(Array.prototype.slice.call(arguments));
      console.log(Array.prototype.slice.apply(arguments));
 }
 func1(1, 2, 3)

③将对象转为数组

 var obj = {
      0: "a",
      1: 18,
      length: 2,
    };
console.log(Array.prototype.slice.call(obj));

注意对象中一定要有索引,length
④获取对象的类型
这个是最好用的,获取类型的方式,上面也有提到

var obj = [];
Object.prototype.toString.call(obj) // [object Array]

⑤操作数组

 var arr1 = [1, 2];
 var arr2 = [4, 5];
 var res = Array.prototype.concat.apply(arr1, arr2);
 console.log(res, arr1, arr2);
 // [1, 2, 3, 4] [1, 2] [4, 5]
 Array.prototype.push.apply(arr1, arr2);
 console.log(arr1, arr2);
 // [1, 2, 3, 4] [4, 5]

如果使用concat,则结果是返回的值,在原来的数组不发生改变;
如果使用push,则没有返回值,结果添加到第一个参数中;

⑥实现继承

function Animal(name) {
    this.name = name;
    this.showName = function () {
        console.log(this.name);
    };
}

function Dog(name) {
    Animal.Dog(this, name);
}

将this对象代指Animai,并且传参name,让Dog可以使用Animai的属性和方法,同样也可以实现多继承

参考资料

8. 浅拷贝和深拷贝

拷贝是在开发过程中经常遇到的。我们知道数据类型分为基本数据类型和引用数据类型,引用数据类型是引用对象的地址,存在栈内存中, 实际的数据则是在堆内存中。
赋值和浅拷贝的区别

和原来数据是否指向同一个 改变第一层数据为基本数据类型 改变子对象(原数据不止一层)
赋值 yes 一起变 一起变
浅拷贝 no 不影响原来的 一起变
深拷贝 no 不影响原来的 不影响原来的

赋值操作:

var a = [0, 1];
var b = a;
b[0] = 2;
console.log(a, b); // a和b都是[2, 1]

a和b都是指向同一个对象引用

浅拷贝:

浅拷贝会创建一个新的对象,指向某个对象的指针,并不会复制对象本身。如果属性是基本数据类型则拷贝值,如果是引用类型,则还是拷贝引用地址,所以实际上只拷贝了一层。
① 使用Object.assign()

 let obj = {
    id: 0,
    info: { address: { province: "Fujian" }, name: "lin" },
 };
 let obj2 = Object.assign({}, obj);
 obj2.id = 1; 
 console.log(obj, obj2); //不改变原对象
 obj2.info.name = "chen";
 console.log(obj, obj2); // 一起改变

②使用数组的操作方法

let arr = [1, { index: [2, 3] }];
let res = arr.concat();
res[0] = 0; // 不改变原对象
res[1].index = [0]; // 一起改变
console.log(arr, res);

同样的slice方法也是这样的
③使用扩展运算符

[...obj]

深拷贝:

会创造有一个一样的对象,操作对象,对原来的对象没有影响,就是完全复制。
① 使用JSON.stringfy的方法

let obj = {
    id: 0,
    info: { name: "lin" },
};
let temp = JSON.parse(JSON.stringify(obj));
temp.info.name = "chen";
console.log(obj, temp); //不改变原对象

但是使用这个会有局限性

  • 会忽略undefined、Symbol、无法序列化function
let obj = {
    id: 0,
    info: { name: "lin" },
    age: undefined,
    index: Symbol("index"),
};
 let temp = JSON.parse(JSON.stringify(obj));
 console.log(temp);
 //结果: { id: 0, info: { name: "lin"} }
  • 如果有循环引用则会报错
let obj = {
    info: { name: "lin" },
};
obj.other = obj.info;
obj.info.other = obj.other;
let temp = JSON.parse(JSON.stringify(obj));
console.log(temp);

② 使用MessageChannel

这个可以拷贝含有undefined类型的,但是依然无法拷贝函数

 function messageClone(data) {
 return new Promise((resolve) => {
     let { port1, port2 } = new MessageChannel();
     port2.onmessage = (e) => resolve(e.data);
     port1.postMessage(data);
     });
 }
 messageClone(obj).then((res) => {
    console.log(res);
 });

③使用lodash函数库

var lodash = require("lodash")
var temp = lodash.cloneDeep(obj)

9. 原型

当我们打印一个对象:

var a = { name: "a" }
console.log(a)

打印结果如上,每个对象都有一个__proto__属性,通过这个可以找到原型,构造函数则可以通过prototype找到原型。

[图片上传失败...(image-3300a8-1589858753415)]

需要记住的是:

  • 对象都有__proto__属性,通过这个属性最终都可以找到对象原型:Object.prototype
  • 函数可以通过__proto__最终找到函数原型:Function.prototype
  • 函数也是对象,所以函数原型通过__proto__可以找到Object.prototype
  • Object.prototype__proto__指向null
  • __proto__将对象和原型连接起来就是原型链

10 .ES6新特性

ES6(ECMAScript 2015),ECMAScript是ECMA制定的标准化脚本语言。

新增特性:

  • 类(Class)

便于理解面向对象的编程,类似于java,也有继承

  • 模块化

采用模块化,export和import导入导出,每个模块都有单独的作用域,创造了明明空间,可以防止函数的命名冲突。

  • 箭头函数

箭头函数中没有this,如果使用this则是指向父级的this,获取的arguments也是父级的。

var foo = 2;
let a = {
    foo: 1,
    bar: () => console.log(this.foo),
};

如上,这个例子中箭头函数的this.foo其实指向的是外面的foo,值为2;

如果用普通函数的话,他的结果就是1

适用场景:

① 一些对象遍历过程中的map,reduce,filter

②用于简洁操作,比如直接返回表达式的结果

③在任何需要绑定 this 至当前上下文,而不是函数本身时

缺点:

① 不适合作为对象中的方法

② 带有动态上下文的回调函数,比如addEventListenr,不利于解绑

③ 影响代码可读性,不利于测试,不容易追溯代码

  • 可设置函数参数默认值
function test(a = 0) {
    console.log(a)
}   
  • 可以使用模板字符串
var name ="a"
console.log(`mmy name is ${name}`)
  • 解构赋值
var index = [0, 1, 2]
var [zero, one, two] = index
console.log(zero)  // 0
// 获取对象的某些属性
var obj = { age: 18, name: "a"}
var {name} = obj
  • 扩展操作
let clone = { ...obj };

浅拷贝对象

  • 对象简写
let name='a'
        
let student = {
    name
};
console.log(student);//{name: "a"}
  • promise

异步操作

  • let和const

let和const都是块级作用域

补充(ES7新特性)

  • includes

检测数组中是否有某个值

  • 可以使用指数操作:

2**3 结果为8

补充(ES8新特性)

重点是async/await

11. 数组常用操作方法

  • 数组的转换方法

    数组常见的转换方法有toString(),toLocalString(),valueOf()

    const value = [1, 2, 3];
    console.log(value.toString()); // 1, 2, 3
    console.log(value.toLocaleString()); // 1, 2, 3
    console.log(value.valueOf()); // // [1, 2, 3]
    

    toString返回的是所有元素用逗号拼接的结果;

    toLocalString表示在特定环境下要表示的字符串;

    valueOf返回的是数组本身;

    请看这个例子:

    const a = new Object([1, 2, 3]);
    console.log(a.toString()); // 1,2,3
    console.log(Array.prototype.toString.call(a)); // 1,2,3
    console.log(Object.prototype.toString.call(a)); // [Object Object]
    

    我们发现最后一个的结果并没有打印出1,2,3,那是因为Array继承了Object,重写了toString方法

  • 将对象转换为数组

    如果一个对象有索引值,且含有length属性,则可以转为数组,方法有:

    const person = {
      0: "age",
      1: "name",
      length: 2,
    };
    console.log([].slice.call(person));
    console.log(Array.from(person));
    console.log(Array.from(person, (x) => "head-" + x));
    console.log(person);
    

    对应的结果如下:

    [ 'age', 'name' ]
    [ 'age', 'name' ]
    [ 'head-age', 'head-name' ]
    { '0': 'age', '1': 'name', length: 2 }
    

    Array.from还接受2个参数,第二个可以用来对数组的处理,这些转换都不回影响原来的json对象

  • Array.isArray

判断是否是数组,其他方式:

arr instanceof Array
arr.constructor === Array
Object.prototyp.toString.call(arr) === '[object Array]'
  • arr.concat(arr2,arr3,...)

    返回合并后的数组,原数组不改变

  • arr.join(符号)

    根据某个符号拼接数组成字符串

  • map

    遍历数组,根据一定的操作对灭一个元素操作再返回

  • filter

    筛选符合条件的所有元素,并返回

  • indexOf

    查找某个元素的索引,找不到为-1

  • find

    返回第一个符合条件值

  • findIndex

    找到符合条件的第一个索引值

  • includes

    判断是否包含某个元素

  • reduce

    对数组做一定的运算,比如求和

    reduce用法实例

    const nums = [1, 3, 5, 7];
    const res = nums.reduce((pre, current, index, arr) => {
      console.log(pre, current, index, arr);
      return pre + current;
    }, 0);
    console.log(res);
    

    模拟函数的写法:

    function reduceAction(arr, func, initValue) {
      let originArr = arr.slice(); // 保留原数组
      let newArr = (typeof initValue !== "undefined" ? [initValue] : []).concat(
        originArr
      ); // 判断是否有初始值
      let index = 0;
      while (newArr.length > 1) {
        newArr.splice(0, 2, func(newArr[0], newArr[1], index++, originArr)); // 删除前两个元素,并且调用func函数,为了保留索引,设置自增的index
      }
      return newArr[0]; // 最后剩下的一个元素,则是reduce的结果
    }
    
  • some

    只要有一个元素符合条件,结果就是true

  • every

    每一个元素都符合条件,结果才是true

  • pop(原数组会影响)

    删除数组最后一个元素,并返回

  • push(原数组会影响)

    为数组添加元素

  • shift(原数组会影响)

    删除数组第一个元素,并返回这个元素

  • unshift(原数组会影响)

    在数组开头添加元素,并返回长度

  • reverse(原数组会影响)

    逆序

  • slice(begin, end)

    截取数组,如果begin和end是负值,则加上数组的长度,如果begin >= end,则返回空数组,使用slice对数组的切割是不会影响原数组的

    const color = ["red", "yellow", "blue", "green", "black"];
    console.log(color.slice(1, -1));
    console.log(color.slice(3, 4));
    console.log(color.slice(3, 1));
    console.log(color.slice(-2, -1));
    console.log(color);
    

    对应结果:

    [ 'yellow', 'blue', 'green' ]
    [ 'green' ]
    []
    [ 'green' ]
    [ 'red', 'yellow', 'blue', 'green', 'black' ]
    
  • splice(原数组会影响)

第一个参数是开始位置,第二个参数是删除的数量,第三个参数是需要在开始位置插入的元素,结果是返回删除的元素,如果只有第一个参数,则从当前位置删除到末尾位置。

var arr = [1, 2, 3, 4];
console.log(arr.splice(2)); //[3, 4]
console.log(arr); // [1, 2]

如果第一个参数是负数,则会默认加上数组的长度,如果截取的长度超过可取得长度,则截取剩余所有的元素,例如:

const color = ["red", "yellow", "blue", "green", "black"];
console.log(color.splice(-1, 2)); // [ 'black' ]
console.log(color); // [ 'red', 'yellow', 'blue', 'green' ]

如下例子是从索引1开始,删除两个元素,并在索引1的位置插入orange

const color = ["red", "yellow", "blue", "green", "black"];
console.log(color.splice(1, 2, "orange")); // [ 'yellow', 'blue' ]
console.log(color); // [ 'red', 'orange', 'green', 'black' ]
  • sort(原数组会影响)

    排序,可以传入参数定义具体的排序函数

  • includes

    用于检查数组中是或否有某个元素

12. 字符串常用操作方法

  • 对字符串的遍历let ... of
printStr() {    
    let str = 'my name is Linyq'    
    for (let a of str) {
        console.log(a)    
    } 
}
  • 查找字符串indexOf (数组操作中也有)
let str = 'yo world, world!' 
console.log(str.indexOf('wo'))  // 3 
console.log(str.indexOf('w', 4)) // 10 
console.log(str.indexOf('b'))   // -1
  • includes():(数组操作中也有)

    返回布尔值,表示是否找到了参数字符串。

  • charAt

    根据索引获取指定字符

  • concat

    连接两个字符串

  • slice(start, end)

    (1) start和end如果是负数,则加上字符串长度值

    (2) 没有指定end默认到头

    var str = "abcde"
    consol.log(str.slice(-2)) // de
    
  • substr(start, length)

    (1) start和length<=0,则默认为0
    

    (2)没有指定lenght默认到头

  • substring(start, end )

    (1) start 比 end 大,那么该方法在提取子串之前会先交换这两个参数

    (2) start和length<=0,则默认为0

  • toLowerCase

    转成小写

  • toUpperCase

    转成大写

  • startsWith():

    返回布尔值,表示是否在原字符串的头部。

  • endsWith():

    返回布尔值,表示是否在原字符串的尾部

  • repeat

'x'.repeat(3)  // xxx 
'ab'.repeat(2) // abab 
'x'.repeat(0)  // ''
  • replace替换字符

  • 字符串补充,padStart和padEnd,第一个参数是补充到的位数

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