面试汇总

1. js判断一个object为空对象

1)for ...in...遍历属性
for (var i in obj) { // 如果不为空,则会执行到这一步,返回true
    if(obj.hasOwnProperty(i)){
      return true;
    }
}
return false // 如果为空,返回false
2)JSON
if (JSON.stringify(obj) === '{}') {
    return false // 如果为空,返回false
}
return true // 如果不为空,则会执行到这一步,返回true
3) Object.keys()
if (Object.keys(obj).length === 0) {
    return false // 如果为空,返回false
}
return true // 如果不为空,则会执行到这一步,返回true

2. cookie,session,localStorage,sessionStorage

查看区别

js 操作cookie

//创建cookie
function setCookie(name, value, expires, path, domain, secure) {
    var cookieText = encodeURIComponent(name) + '=' + encodeURIComponent(value);
    if (expires instanceof Date) {
        cookieText += '; expires=' + expires;
    }
    if (path) {
        cookieText += '; path=' + path;
    }
    if (domain) {
        cookieText += '; domain=' + domain;
    }
    if (secure) {
        cookieText += '; secure';
    }
    document.cookie = cookieText;
}

//获取cookie
function getCookie(name) {
    var cookieName = encodeURIComponent(name) + '=';
    var cookieStart = document.cookie.indexOf(cookieName);
    var cookieValue = null;
    if (cookieStart > -1) {
        var cookieEnd = document.cookie.indexOf(';', cookieStart);
        if (cookieEnd == -1) {
            cookieEnd = document.cookie.length;
        }
        cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
    }
    return cookieValue;
}

//删除cookie
function unsetCookie(name) {
    document.cookie = name + "= ; expires=" + new Date(0);
}
localStorage sessionStorage
setItem(key, value)    //设置记录
getItem(key)            //获取记录
removeItem(key)        //删除该域名下单条记录
clear()                //删除该域名下所有记录

3. HTTP协议

HTTP请求方式

  • GET
  • POST
  • HEAD 仅获取头部信息
  • PUT 创建和更新资源
  • DELETE 删除资源
  • OPTIONS 用于url验证,验证接口服务是否正常

http协议详解

https协议详解

4. http状态码

    100  Continue  继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息

    200  OK   正常返回信息

    201  Created  请求成功并且服务器创建了新的资源

    202  Accepted  服务器已接受请求,但尚未处理

    301  Moved Permanently  请求的网页已永久移动到新位置。

    302 Found  临时性重定向。

    303 See Other  临时性重定向,且总是使用 GET 请求新的 URI。

    304  Not Modified  自从上次请求后,请求的网页未修改过。


    400 Bad Request  服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。

    401 Unauthorized  请求未授权。

    403 Forbidden  禁止访问。

    404 Not Found  找不到如何与 URI 相匹配的资源。

    500 Internal Server Error  最常见的服务器端错误。

    503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。

5. 常见的post提交数据方式

  • application/x-www-form-urlencoded
    原生表单提交
title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
  • multipart/form-data 表单上传文件
Content-Disposition: form-data; name="file"; filename="chrome.png"
  • application/json
{"title":"test","sub":[1,2,3]}
  • text/xml

6. transform,transition,animation


transform,transition,animation,keyframes区别

7.图片懒加载

实现方法

关键的三个高度

  • body的滚动高度 scrollTop
document.documentElement.scrollTop||document.body.scrollTop
  • 可见区域高度 clientHeight
document.documentElement.clientHeight
  • 图片距离顶部的高度
img.offsetTop

加载图片的时机:

img.offsetTop < scrollTop + clientHeight
使用函数节流提升scroll 性能
window.addEventListener('scroll',throttle(lazyload,500,1000));

函数防抖与节流

8. 跨域问题

同源策略

所谓同源是指"协议+域名+端口"三者相同。同源策略限制以下几种行为:

1) Cookie、LocalStorage 和 IndexDB 无法读取
2) DOM 和 Js对象无法获得
3) AJAX 请求不能发送
跨域解决方案
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
jsonp
function handleResponse(response) {
   alert(`You get the data : ${response}`)
}
const script = document.createElement('script')
script.src = 'http://somesite.com/json/?callback=handleResponse'
document.body.insertBefore(script, document.body.firstChild)
CORS

简单请求和非简单请求

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

// 前端设置是否带cookie
xhr.withCredentials = true;
// java服务端设置
/* 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/' */
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

跨域解决方案详解

cors详解

9. XSS CSRF SQL注入

XSS:跨站脚本(Cross-site scripting,通常简称为XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

CSRF:跨站请求伪造(英语:Cross-site request forgery), 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

SQL注入:就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

防御

XSS:

1.将重要的cookie标记为http only,这样的话Javascript 中的document.cookie语句就不能获取到cookie了
2.只允许用户输入我们期望的数据。例如:年龄的textbox中,只允许用户输入数字,而数字之外的字符都过滤掉
3.对数据进行Html Encode 处理。< 转化为 &lt;、> 转化为 &gt;、& 转化为 &amp;、' 转化为 &#039;、" 转化为 &quot;、空格 转化为 &nbsp;
4.过滤或移除特殊的Html标签。例如:<script>、<iframe>、&lt; for <、&gt; for >、&quot for
5.过滤JavaScript 事件的标签。例如 “onclick=”、”onfocus” 等等 
很多浏览器都加入了安全机制来过滤XSS

CSRF:

1.检测http referer是否是同域名,通常来讲,用户提交的请求,referer应该是来来自站内地址,所以如果发现referer中地址异常,那么很可能是遭到了CSRF攻击。
2.避免登录的session长时间存储在客户端中。
3.关键请求使用验证码或者token机制。在一些十分关键的操作,比如交易付款环节。这种请求中,加入验证码,可以防止被恶意用户攻击。token机制也有一定的防御作用。具体来说就是服务器每次返回客户端页面的时候,在页面中埋上一个token字段,例如 <input type=“hidden” name=“csrftoken” value=“abcd">
之后,客户端请求的时候带上这个token,使用这个机制后,攻击者也就很难发起CSRF攻击了

SQL注入:

永远不要信任用户的输入。
1.对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和双”-“进行转换等。
2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取
3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接
4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息
5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装

10. 常用设计模式

  • 单例模式
  • 构造函数模式
  • 建造者模式
  • 工厂模式
  • 观察者模式
// 观察者模式实现自定义事件
function EventTarget(){
    this.handers = {};
}
EventTarget.prototype = {
    constructor: EventTarget,
    // 注册给定类型事件的处理函数
    addHandler: function(type,handler){
        if(typeof this.handlers[type] == 'undefined'){
            this.handlers[type]=[];
        }
        this.handlers[type].push(handler);
    },
    // 触发事件
    // e object类型
    fire: function(e){
        if(!e.target){
            e.target = this;
        }
        if(this.handlers[e.type] instanceof Array){
            let handlers = this.handlers[e.type];
            for(let i=0,len=handlers.length;i<len;i++){
                handlers[i](e);
            }
        }
    },
    // 注销某个事件类型的事件处理函数
    removeHandler: function(type,handler){
        if(this.handlers[e.type] instanceof Array){
            let handlers = this.handlers[type];
            for(let i=0,len=handlers.length;i<len;i++){
                if(handlers[i]==handler){
                    break;
                }
            }
            handers.splice(i,1);
        }
    }
}

//创建一个新对象
var target = new EventTarget();
// 添加一个事件处理程序
target.addHandler('message',handleMessage);
// 触发事件
target.fire({type:'message',message:'hello world'});
//删除事件处理程序
target.removeHandler('message',handleMessage);

常用的设计模式详解

11. 实现元素拖拽

var moveElem = document.querySelector('.drap');//待拖拽元素      
            var dragging; //是否激活拖拽状态
            var tLeft, tTop; //鼠标按下时相对于选中元素的位移

            //监听鼠标按下事件
            document.addEventListener('mousedown', function (e) {
                if (e.target == moveElem) {
                    dragging = true; //激活拖拽状态
                    var moveElemRect = moveElem.getBoundingClientRect();
                    // tLeft = e.clientX - moveElemRect.left; //鼠标按下时和选中元素的坐标偏移:x坐标
                    // tTop = e.clientY - moveElemRect.top; //鼠标按下时和选中元素的坐标偏移:y坐标
                    tLeft = e.clientX - moveElem.offsetLeft;
                    tTop = e.clientY - moveElem.offsetTop;
                }
            });

            //监听鼠标放开事件
            document.addEventListener('mouseup', function (e) {
                dragging = false;
            });

            //监听鼠标移动事件
            document.addEventListener('mousemove', function (e) {

                if (dragging) {
                    var moveX = e.clientX - tLeft,
                        moveY = e.clientY - tTop;

                    moveElem.style.left = moveX + 'px';
                    moveElem.style.top = moveY + 'px';

                }
            });

12. 前端缓存

分为HTTP缓存和浏览器缓存

HTTP缓存:强缓存和协商缓存

整体流程:HTTP缓存都是从第二次请求开始的。
第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

强缓存:

  • Cache-Control
    • max-age:xx秒,相对时间
    • no-cache:不使用缓存
    • no-store:每次都下载最新资源
    • public/private:是否是能被单个用户保存
  • Expires
    • GMT时间

协商缓存:

  • Last-Modify/If-Modify-Since GMT时间 秒级判断
  • Etag/If-None-Match 校验值

http缓存

理解Web缓存

强缓存和协商缓存的流程

13. TCP UDP

三次握手:

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

四次挥手:

image

TCP与UDP区别:

1.TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的
2.TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。
3.TCP协议所需资源多,TCP首部需20个字节(不算可选项),UDP首部字段只需8个字节。
4.TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率
5.TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。
6.TCP面向的是字节流的服务,UDP面向的是报文的服务。

14. 外边距叠加(margin collapse)

要点:

  • 毗邻

毗邻说明了他们的位置关系,没有被 padding、border、clear 和 line box 分隔开。

  • 两个或多个
  • 垂直方向
  • 普通流(in flow)

只要不是 float、position:absolute 和 root element 时就是 in flow。

15. 块格式化上下文(BFC)

BFC作用:

  • 清除内部浮动,防止容器高度塌陷
    毕竟计算BFC高度是包含其浮动子元素的
  • 阻止外边距折叠
  • 防止元素被浮动元素覆盖

触发BFC方法:

1.浮动元素 , float除了none以外的值
2.绝对定位元素, position: absolute | fixed
3.非块级盒子的块级容器, display: flex|grid|inline-block | table-cell | table-caption
4.overflow: hidden | auto | scroll 除了visible 以外的值
5.根元素(html)

标准定义

16. 前端性能优化

1、减少http请求,合理设置 HTTP缓存
2、使用浏览器缓存
  设置http头中的cache-control和expires的属性
3、启用压缩 (GZip)
4、CSS Sprites
5、LazyLoad Images
6、CSS放在页面最上部,javascript放在页面最下面
7、减少cookie传输
8、Javascript代码优化,减少DOM操作
9、CSS选择符优化
10、CDN加速,缓存静态资源,如图片、文件、CSS、script脚本、静态网页等

17. 从输入URL到页面加载发生了什么

1、DNS解析
2.TCP连接
3.发送HTTP请求
4.服务器处理请求并返回HTTP报文
5.浏览器解析渲染页面
6.连接结束

从输入URL到页面加载发生了什么

18. 类数组对象转数组

var arr = Array.prototype.slice.call(arguments);
var arr = Array.from(arguments);
var arr = [...arguments];

19. bind的实现

Function.prototype.bind=function(context){
    var that=this;
    return function(){
       return that.apply(context,arguments);
    }
}
// 经过函数柯里化后的 bind
Function.prototype.bind=function(context){
    var args=[].slice.call(arguments,1);
    var that=this;
    return function(){
        var innerArgs=[].slice(arguments);
        var finalArgs=args.concat(innerArgs);
        return that.apply(context,finalArgs);
    }
}
// 支持构造函数
Function.prototype.bind2 = function (context) {
 var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

20. MVC MVVM

MVC
image
1.View 传送指令到 Controller
2.Controller 完成业务逻辑后,要求 Model 改变状态
3.Model 将新的数据发送到 View,用户得到反馈
MVVM
image
采用双向绑定(data-binding):View的变动,自动反映在 ViewModel

21. new创建对象

当代码 new Foo(...) 执行时,会发生以下事情:

1.一个继承自 Foo.prototype 的新对象被创建。
2.使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
3.由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
// 相当于
var obj={};
obj._proto_ = Foo.prototype;
return obj;

22. 箭头函数与普通函数的区别

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

var foo = (...args) => {
  return args[0]
}
console.log(foo(1))    //1

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
(5)箭头函数没有原型
var foo = () => {};
console.log(foo.prototype) //undefined

23. 动画

// transition过渡
transtion-property : height
transition-duration : 1s
transtion-delay : 1s
transition-timing-function : linear|ease-in|ease-out|cubic-bezier函数

transition的优点在于简单易用,但是它有几个很大的局限。

(1)transition需要事件触发,所以没法在网页加载时自动发生。
(2)transition是一次性的,不能重复发生,除非一再触发。
(3)transition只能定义开始状态和结束状态,不能定义中间状态,也就是说只有两个状态。
(4)一条transition规则,只能定义一个属性的变化,不能涉及多个属性。
// animation 动画
animation-name
animation-duration
animation-timing-function
animation-delay
animation-fill-mode : none|backwards|both 动画结束的状态
animation-direction : normal|alternate|reverse|alternate-reserve 动画循环播放方向(头尾头|头尾尾头|尾头尾|尾头头尾)
animation-iteration-count 动画播放次数
animation-play-state : running|paused 动画是否正在运行或暂停

@keyframes name{ //规定动画name
   0% { background: #c00 }
  50% { background: orange }
  100%{ background: yellowgreen }
}

24. 对象深拷贝

function deepClone(obj) {
    var temp = obj.constructor === 'Array' ? [] : {};
    for (let i in obj) {
        temp[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
    }
    return temp;
}
循环引用时
function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) {
        return obj
    }
    // 查表
    if (hash.has(obj)) return hash.get(obj);
    
    let cloneObj = Array.isArray(obj) ? [] : {}
    // 哈希表设值
    hash.set(obj, cloneObj)

    let result = Object.keys(obj).map(key => {
        return {
            [key]: deepClone(obj[key], hash)
        }
    })
    return Object.assign(cloneObj, ...result)
}

// obj有循环引用时,改写的stringify
seen = [];

JSON.stringify(obj, function(key, val) {
   if (val != null && typeof val == "object") {
        if (seen.indexOf(val) >= 0) {
            return;
        }
        seen.push(val);
    }
    return val;
});

25. 轮播图

左右滑动效果的轮播图

26. redux中reducer为什么必须是纯函数

从源码中得知,redux接受一个给定的state,然后通过循环僵state的每一部分传递给每个对应的reducer。如果有任何改变,reducer将返回一个新的对象,否则将返回旧的state。==redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同==。

这样设计可以避免对象的深层比较。

27. getBoundingClientRect() 返回元素的大小及其相对于视口的位置

返回值是DOMRect对象,包含width height left top right bottom

right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。

你真的会用getBoundingClientRect吗

28. scrollHeight, clientHeight, offsetHeight

image

Js中ScrollTop、ScrollHeight、ClientHeight、OffsetHeight

29. HTTPS的工作原理

image
(1)客户使用HTTPS的URL访问Web服务器,要求与Web服务器建立SSL连接。  
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。  
(3)客户端的浏览器与Web服务器开始协商SSL/TLS连接的安全等级,也就是信息加密的等级。  
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。  
(5)Web服务器利用自己的私钥解密出会话密钥。  
(6)Web服务器利用会话密钥加密与客户端之间的通信。

https详解

30. for...infor...of的区别

1)for of只可以循环可迭代对象(包括 Array, Map, Set, String, TypedArray,arguments 对象等等)的可迭代属性

2)for...in循环出的是key,for...of循环出的是value

3)for...of不能循环普通的对象,需要通过和Object.keys()搭配使用

31. 左右盒子高度自适应

方法一:flex

align-items:stretch

方法二:display:table-cell

父盒子display:table,子盒子display:table-cell

方法三:margin-bottom:-9999px; padding-bottom:9999px;

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

推荐阅读更多精彩内容