虾皮、OPPO、富途等十几家公司面经总结

程序源代码

共 14151字,需浏览 29分钟

 ·

2021-11-15 12:35

最近朋友内推面试了几家公司(货拉拉虾皮有赞乐信Qtrade苹果树富途涂鸦OPPO微保微众元戎启行),也收获了满意的offer。整理了下面试遇到的问题,作为记录。

JS相关

JS原型及原型链

function Person({}
Person.prototype.name = 'Zaxlct';
Person.prototype.sayName = function({
  alert(this.name);
}
var person1 = new Person();
//JS 在创建对象的时候,都有一个__proto__ 的内置属性,用于指向创建它的构造函数的原型对象。
//每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性
// 对象 person1 有一个 __proto__属性,创建它的构造函数是 Person,构造函数的原型对象是 Person.prototype
console.log(person1.__proto__ == Person.prototype) //true
//所有函数对象的__proto__都指向Function.prototype
String.__proto__ === Function.prototype  // true
String.constructor == Function //true
prototype.jpg

JS继承的几种方式

详解

  1. 原型继承
function Parent ({
  this.name = 'Parent'
  this.sex = 'boy'
}
function Child ({
  this.name = 'child'
}
// 将子类的原型对象指向父类的实例
Child.prototype = new Parent()
//优:继承了父类的模板,又继承了父类的原型对象
//缺:1.无法实现多继承(因为已经指定了原型对象了)
//   2.创建子类时,无法向父类构造函数传参数
  1. 构造函数继承

在子类构造函数内部使用call或apply来调用父类构造函数,复制父类的实例属性给子类。

function Parent (name{
  this.name = name
}
function Child ({
  //用.call 来改变 Parent 构造函数内的指向
  Parent.call(this'child')
}
//优:解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数
//缺:构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法
  1. 组合继承

    组合继承就是将原型链继承与构造函数继承组合在一起。

    • 使用原型链继承来保证子类能继承到父类原型中的属性和方法
    • 使用构造继承来保证子类能继承到父类的实例属性和方法
  2. 寄生组合继承

  3. class继承

class 中继承主要是依靠两个东西:

  • extends

  • super

class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
class Child extends Parent {
  constructor (name) {
    super(name)
    this.sex = 'boy'
  }
}

Event Loop 事件循环

同步与异步、宏任务和微任务分别是函数两个不同维度的描述。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入任务队列task queue)的任务,只有等主线程任务执行完毕,任务队列开始通知主线程,请求执行任务,该任务才会进入主线程执行。

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务;如果没有,在执行环境栈中会读取宏任务队列中排在最前的任务;执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

同步(Promise)>异步(微任务(process.nextTick ,Promises.then, Promise.catch ,resove,reject,MutationObserver)>宏任务(setTimeout,setInterval,setImmediate))

await阻塞 后面的代码执行,因此跳出async函数执行下一个微任务

Promise 与 Async/Await  区别

async/await是基于Promise实现的,看起来更像同步代码,

  • 不需要写匿名函数处理Promise的resolve值
  • 错误处理: Async/Await 让 try/catch 可以同时处理同步和异步错误。
  • 条件语句也跟错误处理一样简洁一点
  • 中间值处理(第一个方法返回值,用作第二个方法参数) 解决嵌套问题
  • 调试方便
const makeRequest = () => {
    try {
        getJSON().then(result => {
            // JSON.parse可能会出错
            const data = JSON.parse(result)
            console.log(data)
        })
        // 取消注释,处理异步代码的错误
        // .catch((err) => {
        //   console.log(err)
        // })
    } catch (err) {
        console.log(err)
    }
}

使用aync/await的话,catch能处理JSON.parse错误:

const makeRequest = async () => {
    try {
        // this parse may fail
        const data = JSON.parse(await getJSON())
        console.log(data)
    } catch (err) {
        console.log(err)
    }
}

promise怎么实现链式调用跟返回不同的状态

实现链式调用:使用.then()或者.catch()方法之后会返回一个promise对象,可以继续用.then()方法调用,再次调用所获取的参数是上个then方法return的内容

  1. promise的三种状态是 fulfilled(已成功)/pengding(进行中)/rejected(已拒绝)

  2. 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但发生改变便不可二次修改;

  3. Promise 中使用 resolve 和 reject 两个函数来更改状态;

  4. then 方法内部做的事情就是状态判断:

  • 如果状态是成功,调用成功回调函数
  • 如果状态是失败,调用失败回调函数

函数柯里化

柯里化(Currying) 是把接收多个参数的原函数变换成接受一个单一参数(原来函数的第一个参数的函数)并返回一个新的函数,新的函数能够接受余下的参数,并返回和原函数相同的结果。

  1. 参数对复用
  2. 提高实用性
  3. 延迟执行 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。柯里化的函数可以延迟接收参数,就是比如一个函数需要接收的参数是两个,执行的时候必须接收两个参数,否则没法执行。但是柯里化后的函数,可以先接收一个参数
// 普通的add函数
function add(x, y{
    return x + y
}

// Currying后
function curryingAdd(x{
    return function (y{
        return x + y
    }
}

add(12)           // 3
curryingAdd(1)(2)   // 3

JS对象深克隆

递归遍历对象,解决循环引用问题

解决循环引用问题,我们需要一个存储容器存放当前对象和拷贝对象的对应关系(适合用key-value的数据结构进行存储,也就是map),当进行拷贝当前对象的时候,我们先查找存储容器是否已经拷贝过当前对象,如果已经拷贝过,那么直接把返回,没有的话则是继续拷贝。

function deepClone(target{
    const map = new Map()
    function clone (target{
        if (isObject(target)) {
            let cloneTarget = isArray(target) ? [] : {};
            if (map.get(target)) {
                return map.get(target)
            }
            map.set(target,cloneTarget)
            for (const key in target) {
                cloneTarget[key] = clone(target[key]);
            }
            return cloneTarget;
        } else {
            return target;
        }
    }
    return clone(target)
};

JS模块化

nodeJS里面的模块是基于commonJS规范实现的,原理是文件的读写,导出文件要使用exportsmodule.exports,引入文件用require。每个文件就是一个模块;每个文件里面的代码会用默认写在一个闭包函数里面AMD规范则是非同步加载模块,允许指定回调函数,AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。

AMD推崇依赖前置CMD推崇依赖就近。对于依赖的模块AMD是提前执行,CMD是延迟执行。

ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require

CommonJs 和 ES6 模块化的区别:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

前端模块化:CommonJS,AMD,CMD,ES6

import 和 require 导入的区别

import 的ES6 标准模块;require 是 AMD规范引入方式;

import是编译时调用,所以必须放在文件开头;是解构过程 require是运行时调用,所以require理论上可以运用在代码的任何地方;是赋值过程。其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量

异步加载JS方式

  1. 匿名函数自调动态创建script标签加载js
(function(){
    var scriptEle = document.createElement("script");
    scriptEle.type = "text/javasctipt";
    scriptEle.async = true;
    scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
    var x = document.getElementsByTagName("head")[0];
    x.insertBefore(scriptEle, x.firstChild);  
 })();
  1. async属性
// async属性规定一旦加载脚本可用,则会异步执行
等,提交后信息会存在服务器中 。

  • CSRF:跨站请求伪造 。引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。

    A站点imgsrc=B站点的请求接口,可以访问;解决:referer携带请求来源

    访问该页面后,表单自动提交, 模拟完成了一次POST操作,发送post请求

    解决:后端注入一个随机串Cookie,前端请求取出随机串添加传给后端。

  • http 劫持:电信运营商劫持

  • SQL注入

  • 点击劫持:诱使用户点击看似无害的按钮(实则点击了透明 iframe中的按钮) ,解决后端请求头加一个字段 X-Frame-Options

  • 文件上传漏洞 :服务器未校验上传的文件

  • CSS 及 HTML

    什么是BFC(块级格式化上下文)、IFC(内联格式化上下文 )、FFC(弹性盒模型)

    BFC(Block formatting context),即块级格式化上下文,它作为HTML页面上的一个独立渲染区域,只有区域内元素参与渲染,且不会影响其外部元素。简单来说,可以将 BFC 看做是一个“围城”,外面的元素进不来,里面的元素出不去(互不干扰)。

    一个决定如何渲染元素的容器 ,渲染规则 :

    • 1、内部的块级元素会在垂直方向,一个接一个地放置。

    • 2、块级元素垂直方向的距离由margin决定。属于同一个BFC的两个相邻块级元素的margin会发生重叠。

    • 3、对于从左往右的格式化,每个元素(块级元素与行内元素)的左边缘,与包含块的左边缘相接触,(对于从右往左的格式化则相反)。即使包含块中的元素存在浮动也是如此,除非其中元素再生成一个BFC。

    • 4、BFC的区域不会与浮动元素重叠。

    • 5、BFC是一个隔离的独立容器,容器里面的子元素和外面的元素互不影响。

    • 6、计算BFC容器的高度时,浮动元素也参与计算。

    形成BFC的条件:

    1、浮动元素,float 除 none 以外的值;

    2、定位元素,position(absolute,fixed);

    3、display 为以下其中之一的值 inline-block,table-cell,table-caption;

    4、overflow 除了 visible 以外的值(hidden,auto,scroll);

    BFC 一般用来解决以下几个问题

    • 边距重叠问题
    • 消除浮动问题
    • 自适应布局问题

    flex: 0 1 auto; 是什么意思?

    元素会根据自身宽高设置尺寸。它会缩短自身以适应 flex 容器,但不会伸长并吸收 flex 容器中的额外自由空间来适应 flex 容器 。水平的主轴(main axis)和垂直的交叉轴(cross axis)几个属性决定按哪个轴的排列方向

    • flex-grow0  一个无单位数(): 它会被当作的值。
    • flex-shrink1  一个有效的**宽度(width)**值: 它会被当作 的值。
    • flex-basisauto  关键字noneautoinitial.

    放大比例、缩小比例、分配多余空间之前占据的主轴空间。

    避免CSS全局污染

    1. scoped 属性
    2. css in js
    const styles = {
      bar: {
        backgroundColor'#000'
      }
    }
    const example = (props)=>{
      
    }
    1. CSS Modules
    2. 使用less,尽量少使用全局对选择器
    // 选择器上>要记得写,免得污染所有ul下面的li
    ul{
      >li{
        color:red;
      }
    }

    CSS Modules

    阮一峰 CSS Modules

    CSS Modules是一种构建步骤中的一个进程。通过构建工具来使指定class达到scope的过程。

    CSS Modules 允许使用::global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串:local(className): 做 localIdentName 规则处理,编译唯一哈希类名。

    CSS Modules使用特点:

    • 不使用选择器,只使用 class 名来定义样式
    • 不层叠多个 class,只使用一个 class 把所有样式定义好
    • 不嵌套class

    盒子模型和 box-sizing 属性

    width: 160px; padding: 20px; border: 8px solid orange;标准 box-sizing: content-box; 元素的总宽度 = 160 + 202 + 82; IE的 border-box:总宽度160

    margin/padding百分比的值时 ,基于父元素的宽度和高度的。

    css绘制三角形

    1. 通过border 处理
    // border 处理
    .class {
        width0;
        height0;
        border-left50px solid transparent;
        border-right50px solid transparent;
        border-bottom100px solid red;
    }
    // 宽高+border
    div {
        width50px;
        height50px;
        border2px solid orange;
    }
    1. clip-path裁剪获得
    div{
     clip-pathpolygon(0 100%50% 0100% 100%);
    }
    1. 渐变linear-gradient 实现
    div {
      width200px;
      height200px;
      background:linear-gradient(to bottom right, #fff 0%, #fff 49.9%, rgba(148,88,255,150%,rgba(185,88,255,1100%);
    }

    CSS实现了三角形后如何给三角形添加阴影

    ???

    CSS两列布局的N种实现

    两列布局分为两种,一种是左侧定宽、右侧自适应,另一种是两列都自适应(即左侧宽度由子元素决定,右侧补齐剩余空间)。

    1. 左侧定宽、右侧自适应如何实现
    // 两个元素都设置dislpay:inline-block
    .left {
        display: inline-block;
        width100px;
        height200px;
        background-color: red;
        vertical-align: top;
    }
    .right {
        display: inline-block;
        widthcalc(100% - 100px);
        height400px;
        background-color: blue;
        vertical-align: top;
    }
    // 两个元素设置浮动,右侧自适应元素宽度使用calc函数计算
    .left{
        float: left;
        width100px;
        height200px;
        background-color: red;
    }
    .right{
        float: left;
        widthcalc(100% - 100px);
        height400px;
        background-color: blue;
    }
    // 父元素设置displayflex,自适应元素设置flex:1
    .box{
        height600px;
        width100%;
        display: flex;
    }
    .left{
        width100px;
        height200px;
        background-color: red;
    }
    .right{
        flex1;
        height400px;
        background-color: blue;
    }
    // 父元素相对定位,左侧元素绝对定位,右侧自适应元素设置margin-left的值大于定宽元素的宽度
    .left{
        position: absolute;
        width100px;
        height200px;
        background-color: red;
    }
    .right{
        margin-left100px;
        height400px;
        background-color: blue;
    }
    1. 左右两侧元素都自适应
    // flex布局 同上
    // 父元素设置displaygridgrid-template-columns:auto 1fr;(这个属性定义列宽,auto关键字表示由浏览器自己决定长度。fr是一个相对尺寸单位,表示剩余空间做等分)grid-gap:20px(行间距)
    .parent{
        display:grid;
        grid-template-columns:auto 1fr;
        grid-gap:20px

    .left{
        background-color: red;
        height200px;
    }
    .right{
        height:300px;
        background-color: blue;
    }
    // 浮动+BFC   父元素设置overflow:hidden,左侧定宽元素浮动,右侧自适应元素设置overflow:auto创建BFC
    .box{
        height600px;
        width100%;
        overflow: hidden;
    }
    .left{
        float: left;
        width100px;
        height200px;
        background-color: red;
    }
    .right{
        overflow: auto;
        height400px;
        background-color: blue;
    }

    CSS三列布局

    1. float布局:左边左浮动,右边右浮动,中间margin:0 100px;

    2. Position布局: 左边left:0; 右边right:0; 中间left: 100px; right: 100px;

    3. table布局: 父元素 display: table; 左右 width: 100px; 三个元素display: table-cell;

    4. 弹性(flex)布局:父元素 display: flex; 左右 width: 100px;

    5. 网格(gird)布局:

    // gird提供了 gird-template-columnsgrid-template-rows属性让我们设置行和列的高、宽
    .div{
        width100%;
        display: grid;
        grid-template-rows100px;
        grid-template-columns300px auto 300px;
    }

    常见场景及问题

    app与H5 如何通讯交互的?

    // 兼容IOS和安卓
    callMobile(parameters,messageHandlerName) {
      //handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
      if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
        // alert('ios')
        window.webkit.messageHandlers[messageHandlerName].postMessage(JSON.stringify(parameters))
      } else {
        // alert('安卓')
        //安卓传输不了js json对象,只能传输string
        window.webkit[messageHandlerName](JSON.stringify(parameters))
      }
    }

    由app将原生方法注入到window上供js调用

    messageHandlerName 约定的通信方法parameters 需要传入的参数

    移动端适配方案

    rem是相对于HTML的根元素em相对于父级元素的字体大小。VW,VH 屏幕宽度高度的高分比

    //按照宽度375图算, 1rem = 100px;
    (function (win, doc{
       function changeSize({
         doc.documentElement.style.fontSize = doc.documentElement.clientWidth / 3.75 + 'px';
        console.log(100 * doc.documentElement.clientWidht / 3.75)
       }
       changeSize();
       win.addEventListener('resize', changeSize, false);

    })(windowdocument);

    代码编程相关

    实现发布订阅

    /* Pubsub */
    function Pubsub(){
      //存放事件和对应的处理方法
      this.handles = {};
    }

    Pubsub.prototype = {
      //传入事件类型type和事件处理handle
      onfunction (type, handle{
        if(!this.handles[type]){
          this.handles[type] = [];
        }
        this.handles[type].push(handle);
      },
      emitfunction ({
        //通过传入参数获取事件类型
        //将arguments转为真数组
        var type = Array.prototype.shift.call(arguments);
        if(!this.handles[type]){
          return false;
        }
        for (var i = 0; i < this.handles[type].length; i++) {
          var handle = this.handles[type][i];
          //执行事件
          handle.apply(thisarguments);
        }
      },
      offfunction (type, handle{
        handles = this.handles[type];
        if(handles){
          if(!handle){
            handles.length = 0;//清空数组
          }else{
          for (var i = 0; i < handles.length; i++) {
            var _handle = handles[i];
            if(_handle === handle){
              //从数组中删除
              handles.splice(i,1);
            }
          }
        }
      }  
    }

    promise怎么实现链式调用跟返回不同的状态

    // MyPromise.js

    // 先定义三个常量表示状态
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';

    // 新建 MyPromise 类
    class MyPromise {
      constructor(executor){
        // executor 是一个执行器,进入会立即执行
        // 并传入resolve和reject方法
        executor(this.resolve, this.reject)
      }

      // 储存状态的变量,初始值是 pending
      status = PENDING;

      // resolve和reject为什么要用箭头函数?
      // 如果直接调用的话,普通函数this指向的是window或者undefined
      // 用箭头函数就可以让this指向当前实例对象
      // 成功之后的值
      value = null;
      // 失败之后的原因
      reason = null;

      // 更改成功后的状态
      resolve = (value) => {
        // 只有状态是等待,才执行状态修改
        if (this.status === PENDING) {
          // 状态修改为成功
          this.status = FULFILLED;
          // 保存成功之后的值
          this.value = value;
        }
      }

      // 更改失败后的状态
      reject = (reason) => {
        // 只有状态是等待,才执行状态修改
        if (this.status === PENDING) {
          // 状态成功为失败
          this.status = REJECTED;
          // 保存失败后的原因
          this.reason = reason;
        }
      }

        then(onFulfilled, onRejected) {
        // 判断状态
        if (this.status === FULFILLED) {
          // 调用成功回调,并且把值返回
          onFulfilled(this.value);
        } else if (this.status === REJECTED) {
          // 调用失败回调,并且把原因返回
          onRejected(this.reason);
        }
      }

    }

    实现Promise.all

    // Promise.all
    function all(promises{
      let len = promises.length, res = []
      if (len) {
        return new Promise(function (resolve, reject{
            for(let i=0; i < len; i++){
                let promise = promises[i];
                promise.then(response => {
                    res[i] = response

                    // 当返回结果为最后一个时
                    if (res.length === len) {
                        resolve(res)
                    }

                }, error => {
                    reject(error)
                })

            }
        })
    }

    对象数组转换成tree数组

    > 将entries 按照 level 转换成 result 数据结构

    const entries = [
        {
            "province""浙江""city""杭州""name""西湖"
        }, {
            "province""四川""city""成都""name""锦里"
        }, {
            "province""四川""city""成都""name""方所"
        }, {
            "province""四川""city""阿坝""name""九寨沟"
        }
    ];
     
    const level = ["province""city""name"];

    const  result = [
     {
      value:'浙江'
      children:[
       {
        value:'杭州',
        children:[
         {
          value:'西湖'
         }
        ]
       }
      ]
     },
     {
      value:'四川'
      children:[
       {
        value:'成都',
        children:[
         {
          value:'锦里'
         },
         {
          value:'方所'
         }
        ]
       },
       {
        value:'阿坝',
        children:[
         {
          value:'九寨沟'
         }
        ]
       }
      ]
     },
    ]

    思路:涉及到树形数组,采用递归遍历的方式

    function transfrom(list, level{
      const res = [];
      list.forEach(item => {
        pushItem(res, item, 0);
      });

      function pushItem(arr, obj, i{
        const o = {
          value: obj[level[i]],
          children: [],
        };
        // 判断传入数组里是否有value等于要传入的项
        const hasItem = arr.find(el => el.value === obj[level[i]]);
        let nowArr;
        if(hasItem) {
          // 存在,则下一次遍历传入存在项的children
          nowArr = hasItem.children;
        }else{
          // 不存在 压入arr,下一次遍历传入此项的children
          arr.push(o);
          nowArr = o.children;
        }
        if(i === level.length - 1delete o.children;
        i++;
        if(i < level.length) {
          // 递归进行层级的遍历
          pushItem(nowArr, obj, i);
        }
      }
    }

    transfrom(entries, level);

    JS instanceof 方法原生实现

    简单用法

    function Fn ({}
    const fn = new Fn()
    fn instanceof Fn  // true

    实现如下:

    // left instanceof right
    function _instanceof(left, right{
      // 构造函数原型
      const prototype = right.prototype
      // 实列对象属性,指向其构造函数原型
      left = left.__proto__
      // 查实原型链
      while (true) {
        // 如果为null,说明原型链已经查找到最顶层了,真接返回false
        if (left === null) {
          return false
        }
        // 查找到原型
        if (prototype === left){
          return true
        }
        // 继续向上查找
        left = left.__proto__
      }
    }

    const str = "abc"
    _instanceof(str, String// true

    编程题

    将有同样元素的数组进行合并

    // 例如:
    const arr = [
        ['a''b''c'],
        ['a''d'],
        ['d''e'],
        ['f''g'],
        ['h''g'],
        ['i']
    ]
    // 运行后的返回结果是:
    [
        ['a''b''c''d''e'],
        ['f''g''h'],
        ['i']
    ]
    // 思路一:
    const arr = [['a''b''c'], ['a''d'], ['d''e'], ['f''g'], ['h''g'], ['i']]
    function transform(arr){
        let res = []
        arr = arr.map(el => el.sort()).sort()
        const item = arr.reduce((pre, cur) => {
          if (cur.some(el => pre && pre.includes(el))) {
            pre = pre.concat(cur)
          } else {
            res.push(pre)
            pre = cur
          }
          return [...new Set(pre)]
        })
        res.push(item)
        return res;
    }
    transform(arr)
    // console.log(transform(arr));

    // 思路二: 

    function r (arr{
      const map = new Map()
      arr.forEach((array, index) => {
        const findAlp = array.find((v) => map.get(v))
        if (findAlp) {
          const set = map.get(findAlp)
          array.forEach((alp) => {
            set.add(alp)
            const findAlp2 = map.get(alp)
            if (findAlp2 && findAlp2 !== set) {
              for(const v of findAlp2.values()){
                set.add(v)
                map.set(v, set)
              }
            }
            map.set(alp, set)
          })
        } else {
          const set = new Set(arr[index])
          array.forEach((alp) => map.set(alp, set))
        }
      })
      const set = new Set()
      const ret = []
      for (const [key, value] of map.entries()) {
        if (set.has(value)) continue
        set.add(value)
        ret.push([...value])
      }
      return ret
    }

    源自:https://juejin.cn/post/6991724298197008421

    声明:文章著作权归作者所有,如有侵权,请联系小编删除。

    感谢 · 转发欢迎大家留言
    浏览 22
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报
    评论
    图片
    表情
    推荐
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报