前端常见面试题总结(持续更新)

2018-09-14  本文已影响0人  好奇的猫猫猫

js基础

谈谈This对象的理解。

this是js的一个关键字,随着函数使用场合不同,this的值会发生变化。
但是有一个总原则,那就是this指的是调用函数的那个对象。
this一般情况下:是全局对象Global。 作为方法调用,那么this就是指这个对象

闭包是什么,有什么特性,对页面有什么影响?简要介绍你理解的闭包

call和apply的区别

数组方法pop() push() unshift() shift()

* Push()尾部添加 pop()尾部删除
* Unshift()头部添加 shift()头部删除

例举3种强制类型转换和2种隐式类型转换?

* 强制(parseInt,parseFloat,number)隐式(== – ===);

ajax请求的时候get 和post方式的区别?

ajax请求时,如何解释json数据

http状态码有那些?分别代表是什么意思?

100-199 用于指定客户端应相应的某些动作。
200-299 用于表示请求成功。
300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。
400-499 用于指出客户端的错误。400 1、语义有误,当前请求无法被服务器理解。401 当前请求需要用户验证 403 服务器已经理解请求,但是拒绝执行它。
500-599 用于支持服务器错误。 503 – 服务不可用

说一下什么是javascript的同源策略?

一段脚本只能读取来自于同一来源的窗口和文档的属性,这里的同一来源指的是主机名、协议和端口号的组合

Vue基础:

angualr基础

es6

es6有哪些常用的语法,与es5的区别是什么?

性能优化

你有哪些性能优化的方法?

(1) 减少http请求次数:CSS Sprites, JS、CSS源码压缩、图片大小控制合适;网页Gzip,CDN托管,data缓存 ,图片服务器。
(2)前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
(3) 用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能。
(4) 当需要设置的样式很多时设置className而不是直接操作style。
(5) 少用全局变量、缓存DOM节点查找的结果。减少IO读取操作。
(6) 避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)。
(7) 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳。
(8) 避免在页面的主体布局中使用table,table要等其中的内容完全下载之后才会显示出来,显示div+css布局慢。对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘IO。向前端优化指的是,在不影响功能和体验的情况下,能在浏览器执行的不要在服务端执行,能在缓存服务器上直接返回的不要到应用服务器,程序能直接取得的结果不要到外部取得,本机内能取得的数据不要到远程取,内存能取到的不要到磁盘取,缓存中有的不要去数据库查询。减少数据库操作指减少更新次数、缓存结果减少查询次数、将数据库执行的操作尽可能的让你的程序完成(例如join查询),减少磁盘IO指尽量不使用文件系统作为缓存、减少读写文件次数等。程序优化永远要优化慢的部分,换语言是无法“优化”的。

你常用的开发工具是什么,为什么

项目经验

具体谈一下你做过的一个最有意义的项目,项目中有遇到什么难题,是怎么去解决的

介绍下你最常用的一款框架

jquery,angular vue等;
angular/vue的常用指令
angualr/vue ng-show与ng-if的区别;v-if与v-show的区别

对于前端自动化构建工具有了解吗?简单介绍一下

Gulp,Grunt等;

自主学习

说说最近最流行的一些东西吧?常去哪些网站?

Node.js、MVVM、React-native,Angular,Weex等
CSDN,Segmentfault,博客园,掘金,Stackoverflow,伯乐在线等

下面是自己从不同渠道总结的一些面试题,感兴趣的可以看一看,顺便分享一下看法~

Q1:JavaScript中的作用域是怎样的?

在JavaScript中,每一个函数都有各自的作用域(scope)。作用域可以理解为是一个变量的集合以及相应的如何访问它的规则。只有在函数内部的变量才可以访问到该函数域的变量。
在同一个作用域内部,变量名必须要唯一。作用域可以嵌套。在最内部的作用域中,可以访问任何外部的作用域中的变量.

Q2: 请解释JavaScript中的相等判断

JavaScript中的相等判断有严格判断和带隐式转换的判断两种:

严格判断(strict comparision): 比如===,比较的时候不会隐式转换类型;
抽象判断(abstract comparasion):比如==,比较的时候会隐式转换类型。

var a = "42";
var b = 42;

a == b;         // true
a === b;        // false

复制代码一些简单的规则:
如果两边都是布尔类型的值,使用===;
如果两边是0,"",[],使用===;
所有其它类型,使用==是安全的。而且在很多情况下会简化代码、增加可读性.

Q3: 请解释什么叫做回调函数并提供一个简单的例子

回调函数是一个函数,它被作为参数传入另一个函数,当某些操作结束后,该函数被调用。下面是一个简单的例子,当数组被修改后,调用回调函数打印一行日志。

function modifyArray(arr, callback) {
  // do something to arr here
  arr.push(100);
  // then execute the callback function that was passed
  callback();
}

var arr = [1, 2, 3, 4, 5];
modifyArray(arr, function() {
  console.log("array has been modified", arr);
});

Q4: 请解释Null和Undefined

JavaScript和TypeScript有两个最基本的类型null和undefined。它们的含义是不同的:

如果还没有被初始化,则是undefined;
如果不可用,则可以用null来表示

Q5: 请实现如下函数

var addSix = createBase(6);
addSix(10); // returns 16
addSix(21); // returns 27

addSix是一个函数,也就是说createBase函数的返回是一个函数。

function createBase(baseNumber) {
  return function(N) {
    // we are referencing baseNumber here even though it was declared
    // outside of this function. Closures allow us to do this in JavaScript
    return baseNumber + N;
  }
}

var addSix = createBase(6);
addSix(10);
addSix(21);

Q6: 请解释事件冒泡以及如何阻止它?

事件冒泡的概念是指:在最内层的元素上绑定的事件被触发后,会按照嵌套的层次由内向外逐步触发。因此,点击某个孩子节点可能会触发父节点的事件。
一个阻止事件冒泡的办法就是使用event.stopPropagation(),在IE<9的浏览器上使用event.cancelBubble。

function stopPropagation(evt) {
    if (typeof evt.stopPropagation === "function") {
        evt.stopPropagation();
    } else {
        evt.cancelBubble = true;
    }
}

Q7: 如何检查一个数字是否是整数?

一个最简单的办法是判断除以1之后的余数是否为0

function isInt(num) {
  return num % 1 === 0;
}

console.log(isInt(4)); // true
console.log(isInt(12.2)); // false
console.log(isInt(0.3)); // false

Q8: 什么叫IIFEs(Immediately Invoked Function Expressions)?

IIFE叫做立即执行表达式,顾名思义,该表达式一被创建就立即执行。

(function IIFE(){
    console.log( "Hello!" );
})();
// "Hello!"

该方法常用语避免污染全局的命名空间,因为所以在IIFE中使用的变量外部都无法访问。

Q9: 请解释undefined和not defined的区别

在JavaScript中,如果你尝试使用不存在的还未申明的变量,JavaScript会抛出错误var name is not defined。但是如果你用typeof来查看其类型,会返回undefined。
我们先来澄清一下声明和定义的区别:var x是一个声明,因为你并没有定义其具体的值,你只是声明其存在性。

var x; // declaring x
console.log(x); //output: undefined

var x = 1同时兼具声明和定义,我们也可以叫它初始化。在JavaScript中,每一个变量和函数声明都会被提升到顶部。

如果我们访问一个声明了但是未定义的变量,会返回undefined.

var x; // Declaration
if(typeof x === 'undefined') // Will return true

访问一个未声明未定义的变量,会返回not defined错误。

console.log(y);  // Output: ReferenceError: y is not defined

Q10: JavaScript中闭包是什么?请提供一个例子

闭包是一个定义在其它函数(父函数)里面的函数,它拥有对父函数里面变量的访问权。闭包拥有如下三个作用域的访问权:

1.自身的作用域
2.父作用域
3.全局作用域

var globalVar = "abc";

// Parent self invoking function
(function outerFunction (outerArg) { // begin of scope outerFunction
  // Variable declared in outerFunction function scope
  var outerFuncVar = 'x';    
  // Closure self-invoking function
  (function innerFunction (innerArg) { // begin of scope innerFunction
    // variable declared in innerFunction function scope
    var innerFuncVar = "y";
    console.log(         
      "outerArg = " + outerArg + "\n" +
      "outerFuncVar = " + outerFuncVar + "\n" +
      "innerArg = " + innerArg + "\n" +
      "innerFuncVar = " + innerFuncVar + "\n" +
      "globalVar = " + globalVar);
  // end of scope innerFunction
  })(5); // Pass 5 as parameter
// end of scope outerFunction
})(7); // Pass 7 as parameter

innerFunction是一个闭包,定义在outerFunction中,它可以访问outerFunction作用域的所有变量。当然,它还可以访问全局变量。

outerArg = 7
outerFuncVar = x
innerArg = 5
innerFuncVar = y
globalVar = abc

Q11: 在JavaScript中如何创建私有变量?

你可以通过在函数中声明变量来创建私有变量。因为在函数中,外部无法直接访问。

function func() {
  var priv = "secret code";
}

console.log(priv); // throws error

为了访问该变量,可以构造一个帮助函数来返回该值。

function func() {
  var priv = "secret code";
  return function() {
    return priv;
  }
}

var getPriv = func();
console.log(getPriv()); // => secret code

Q12: 请解释原型模式(Prototype Design Pattern)

原型模式会创建一个新的对象,但不是创建一个未初始化的对象,而是通过拷贝原型链上的值或则被拷贝对象的值来完成初始化。传统的语言很少使用原型模式,但是JavaScript作为一个基于原型的语言,使用原型模式来创建新的对象。

Q13: 判断给定的字符串是否同态(isomorphic)
首先介绍什么叫做同态:两个字符串,如果A字符串中的每一个字符都可以在B字符串中找到唯一对应,并且顺序一一对应;如果存在这样的函数,那么A和B同态。

1.paper和title同态
2.egg和sad不同态
3.dgg和add同态

isIsomorphic("egg", 'add'); // true
isIsomorphic("paper", 'title'); // true
isIsomorphic("kick", 'side'); // false

function isIsomorphic(firstString, secondString) {

  // Check if the same length. If not, they cannot be isomorphic
  if (firstString.length !== secondString.length) return false

  var letterMap = {};

  for (var i = 0; i < firstString.length; i++) {
    var letterA = firstString[i],
        letterB = secondString[i];

    // If the letter does not exist, create a map and map it to the value
    // of the second letter
    if (letterMap[letterA] === undefined) {
      letterMap[letterA] = letterB;
    } else if (letterMap[letterA] !== letterB) {
      // Eles if letterA already exists in the map, but it does not map to
      // letterB, that means that A is mapping to more than one letter.
      return false;
    }
  }
  // If after iterating through and conditions are satisfied, return true.
  // They are isomorphic
  return true;
}

Q14: this关键字如何工作?请提供一些例子

在JavaScript中,this总是指向函数的“拥有者”(也就是指向该函数的对象),或则拥有该函数的对象。

function foo() {
    console.log( this.bar );
}

var bar = "global";

var obj1 = {
    bar: "obj1",
    foo: foo
};

var obj2 = {
    bar: "obj2"
};

foo();          // "global"
obj1.foo();     // "obj1"
foo.call( obj2 );  // "obj2"
new foo();       // undefined

Q15: 如何为Array对象添加你自定义的函数,使得如下代码可以正常工作。

var arr = [1, 2, 3, 4, 5];
var avg = arr.average();
console.log(avg);

JavaScript是一个基于原型的语言。也就是说对象之间通过原型链接,并继承其函数。为了给Array对象添加函数,我们可以修改其原型定义Array prorotype。

Array.prototype.average = function() {
  // calculate sum
  var sum = this.reduce(function(prev, cur) { return prev + cur; });
  // return sum divided by number of elements
  return sum / this.length;
}

var arr = [1, 2, 3, 4, 5];
var avg = arr.average();
console.log(avg); // => 3

Q16: 如下代码会返回什么结果?

0.1 + 0.2 === 0.3

不要惊讶,其结果是false。因为浮点数在系统内的精确度问题,0.1+0.2的结果并不是0.3,而是0.30000000000000004。 要避免这个问题的方法是指定返回结果的小数位数。

Q17: 请描述一下揭示模式(Revealing Module Pattern)

Module pattern的一个变种是Revealing Module Pattern。该设计模式的目的是做到很好的代码隔离,只是将需要对外公开的变量和函数暴露出来。一个直接的实现如下所示:

  var privateVariable = 10;

  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }

  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }

  var otherMethodIWantToExpose = function() {
    privateMethod();
  }

  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined

Q18:写出下面代码的运行顺序并分析

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}

async function async2() {
    console.log("async2");
}

console.log("script start");

setTimeout(function() {
    console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
    console.log("promise1");
    resolve();
}).then(function() {
    console.log("promise2");
});

console.log("script end");

运行结果:


分析参考:8张图让你一步步看清 async/await 和 promise 的执行顺序

Q19:事件捕获与事件冒泡的区别是什么?如何阻止事件的默认行为,以及如何阻止事件冒泡?
捕获型事件和冒泡型事件是相反的,从不精确的对象到最精准的对象。这种事件也称作自顶向下事件模型,因为它是从DOM层次的顶端开始向下延伸的。需要注意的是IE浏览器不支持这种类型事件,我们只需要了解即可。如果设置了捕获型事件,前面的例子会反向进行

阻止事件冒泡
DOM中提供stopPropagation()方法,但IE不支持,使用event对象在事件函数中调用就行.

IE中提供的是,cancelBubble属性,默认为false,当它设置为true时,就是阻止事件冒泡,也是用event对象在事件函数中调用.

阻止默认行为

DOM中提供preventDefault()方法来取消事件默认行为,但是只有当cancelable属性设置为true的事件,才可以使用preventDefault()来取消事件默认行为,使用event对象在事件函数中调用就行。

IE中提供的是returnValue属性,默认为true,当它设置为false时,就是取消事件默认行为,也是用event对象在事件函数中调用。
Q20:阐述什么是AMD和CMD,并说明两者的区别
AMD 是 RequireJS 在推广过程中对模块定义提出的概念。
CMD 是 SeaJS 在推广过程中对模块定义提出的概念。

RequireJS 和 Sea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。

不同之处

两者的主要区别如下:

  1. 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。

  2. 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。

  3. CMD 推崇依赖就近,AMD 推崇依赖前置

  4. 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。

  5. 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。

  6. 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。
    Q21:当遇到JS错误” (unknown): Script error”时,以下哪些尝试可以获取到更真实详细的错误信息?

A.在请求头中加入Access-Control-Allow-Origin字段

B.在JavaScript标签上设置async="true"

C.在JavaScript标签上设置crossorigin="anonymous"

D.将JavaScript标签移到head标签内
正确答案:A C
解析:当未捕获的 JavaScript 错误(通过window.onerror处理程序引发的错误,而不是捕获在try-catch中)被浏览器的跨域策略限制时,会产生这类的脚本错误。 例如,如果您将您的 JavaScript 代码托管在 CDN 上,则任何未被捕获的错误将被报告为“脚本错误” 而不是包含有用的堆栈信息。这是一种浏览器安全措施,旨在防止跨域传递数据,否则将不允许进行通信。

要获得真正的错误消息,请执行以下操作:

1. 发送 ‘Access-Control-Allow-Origin’ 头部

将 Access-Control-Allow-Origin 标头设置为 * 表示可以从任何域正确访问资源。 如有必要,您可以将域替换为您的域:例如,Access-Control-Allow-Origin:www.example.com。 但是,处理多个域会变得棘手,如果你使用 CDN,可能由此产生更多的缓存问题会让你感觉到这种努力并不值得。 在这里看到更多。

2. 在 <script> 中设置 crossorigin="anonymous"

在您的 HTML 代码中,对于您设置了Access-Control-Allow-Origin header 的每个脚本,在 script 标签上设置crossorigin =“anonymous”。在脚本标记中添加 crossorigin 属性之前,请确保验证上述 header 正确发送。 在 Firefox 中,如果存在crossorigin属性,但Access-Control-Allow-Origin头不存在,则脚本将不会执行。
Q22:以下哪一项属于cookie的属性?
A.domain
B.expires
C.browser
D.path
正确答案:ABD
解析:
cookie的属性**

name字段为一个cookie的名称。

value字段为一个cookie的值。

domain字段为可以访问此cookie的域名。

非顶级域名,如二级域名或者三级域名,设置的cookie的domain只能为顶级域名或者二级域名或者三级域名本身,不能设置其他二级域名的cookie,否则cookie无法生成。

顶级域名只能设置domain为顶级域名,不能设置为二级域名或者三级域名,否则cookie无法生成。

二级域名能读取设置了domain为顶级域名或者自身的cookie,不能读取其他二级域名domain的cookie。所以要想cookie在多个二级域名***享,需要设置domain为顶级域名,这样就可以在所有二级域名里面或者到这个cookie的值了。
顶级域名只能获取到domain设置为顶级域名的cookie,其他domain设置为二级域名的无法获取。

path字段为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。

expires/Max-Age 字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。

Size字段 此cookie大小。

http字段 cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。

secure字段 设置是否只能通过https来传递此条cookie
Q22:以下哪种方式可以有效提升CSRF攻击的门槛?
A.过滤尖括号、script等特殊字符
B.添加图片验证码、短信验证码
C.使用https协议
D.进行referer与token校验
正确答案:BD
解析 :
CSRF工具的防御手段

1. 尽量使用POST,限制GET

GET接口太容易被拿来做CSRF攻击,看第一个示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。

当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

2. 浏览器Cookie策略

IE6、7、8、Safari会默认拦截第三方本地Cookie(Third-party Cookie)的发送。但是Firefox2、3、Opera、Chrome、Android等不会拦截,所以通过浏览器Cookie策略来防御CSRF攻击不靠谱,只能说是降低了风险。

PS:Cookie分为两种,Session Cookie(在浏览器关闭后,就会失效,保存到内存里),Third-party Cookie(即只有到了Exprie时间后才会失效的Cookie,这种Cookie会保存到本地)。

PS:另外如果网站返回HTTP头包含P3P Header,那么将允许浏览器发送第三方Cookie。

3. 加验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

4. Referer Check

Referer Check在Web最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就极可能是CSRF攻击。

但是因为服务器并不是什么时候都能取到Referer,所以也无法作为CSRF防御的主要手段。但是用Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。

5. Anti CSRF Token

现在业界对CSRF的防御,一致的做法是使用一个Token(Anti CSRF Token)。
Q23:以下哪种存储方式不能做到信息的长久保存?
A.cookie
B.localStorage
C.webSQL
D.sessionStorage
正确答案:D
解析:
A、cookie:一般由服务器生成,可设置失效时间,如果在浏览器端生成cookie,默认是关闭浏览器失效。
B、localStorage:除非被清除,否则永久保存
D、sessionStorage:仅在当前会话有效,关闭页面或浏览器后被清除
Q24:下列哪个类型不属于 input 元素的类型
A.tel 类型
B.url 类型
C.email 类型
D.int 类型
正确答案:D
解析:


Q24:https://www.juzilicai.com/这个地址的默认端口是?

A.80

B.8080

C.21

D.443
正确答案:D
解析:HTTP服务器,默认的端口号为80
HTTPS服务器,默认的端口号为443
Telnet默认端口号为23
FTP默认的端口号为21
Q25:以下哪项属于nodejs的特性?
A.事件驱动
B.多线程
C.编译型
D.阻塞IO
正确答案:A
解析:Node.js是一个事件驱动I/O服务端JavaScript环境
**Q26:下面加密算法属于对称加密算法的是? **
A.RSA
B.SHA
C.MD5
D.DES
正确答案:A
解析:
常见的对称加密算法有DES3DESAESBlowfishIDEARC5RC6
Q27:以下代码,正确的运行结果是?

 var x = 5;

 console.log(x++ + ++x);

 console.log(++x * --x);

  console.log(x)

答案:12 56 7
解析:x++是先赋值再加1, ++x是先加1再赋值
所以为5+7=12
Q28:编程题:实现函数能对数字进行千分位格式化和大写金额转化

<code class="language-js">var s = '1400398'
function translate(s) {
  var num = ['', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
  var digit = ['', '萬', '亿']
  var digitWithinThousand = ['', '拾', '佰', '仟']
  var s1 = s.replace(/(?!^)(?=(\d{3})+$)/g, ',')
  var strArr = []
  for (let i = s.length; i > 0; i = i - 4) {
    if (i >= 4) {
      strArr.unshift(s.slice(i - 4, i))
    } else {
      strArr.unshift(s.slice(0, i))
    }
  }
  function strToWordWithinThousand(str) {
    str = str.replace(/^0+/, '')
    var ret = []
    for (let i = str.length - 1; i >= 0; i--) {
      var n = parseInt(str[i], 10), s
      if (n !== 0) {
        s = num[n] + digitWithinThousand[str.length - 1 - i]
      } else {
        s = '零'
      }
      ret.push(s)
    }
    return ret.reduceRight((p, c) => p + c).replace(/零+/g, '零').replace(/零$/, '')
  }
  var wordArr = strArr.map(e => strToWordWithinThousand(e))
  var word = ''
  for (let i = 0; i < wordArr.length; i++) {
    var dig = digit.slice(0, wordArr.length)
    var s = strArr[i].startsWith('0') ?
      '零' + wordArr[i] + dig[wordArr.length - 1 - i] :
      wordArr[i] + dig[wordArr.length - 1 - i]
    word = word + s
  }
  return s1 + ' ' + word
}
console.log(translate(s))
</code>
上一篇下一篇

猜你喜欢

热点阅读