关注前端 | 前端博客
当前位置: JavaScript > JavaScript基础相关

JavaScript基础相关

2019-02-11 分类:JavaScript 作者:管理员 阅读(199)

JavaScript的数据类型有哪些?如何区分?

答:基本类型:String、Number、Boolean、Symbol、Undefined、Null ,引用类型:Object.
(Symbol 是 ES6 新增的一种原始数据类型,它的字面意思是:符号、标记。代表独一无二的值 。)
常用的区分方法是typeof(),严格区分可以用Object.prototype.toString.call()

JavaScript的继承有几种?

1.原型链继承

缺点:创建子类型的实例时,不能向父类的构造函数中传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Father(){
    this.fatherProps = "父亲的属性";
}

function Child(){
    this.childProps = "儿子的属性";
}

Child.prototype=new Father();

Child.prototype.constructor=Child;

var c=new Child();

console.log(c.fatherProps);//父亲的属性

2.借用构造函数继承/伪造对象继承/经典继承

缺点:方法都在函数中定义,无法函数复用,并且在父类的原型中定义的方法,对子类也是不可见的

1
2
3
4
5
6
7
8
9
10
function Father(){
    this.fatherProps = "父亲的属性";
}
function Child(){
    //继承了Father
    Father.call(this,arguments)
}

var c=new Child();
console.log(c.fatherProps);//父亲的属性

3.组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Super(name){
    this.name = name;
}
Super.prototype.sayName=function(){
    console.log(this.name)
}

function Sub(name,age){
    //继承属性
    Super.call(this,name);
    this.age=age;
}
//继承方法
Sub.prototype=new Super(name);
Sub.prototype.constructor=Sub;
Sub.prototype.sayAge=function(){
    console.log(this.age)
}
var c1=new Sub('Liming',30);
c1.sayName();//Liming
c1.sayAge();//30

4.原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
//浅复制
var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
//于是ECMAScript5通过新增Object.create( )方法规范了原型式继承
//object.create() 接收两个参数:
//一个用作新对象原型的对象
//(可选的)一个为新对象定义额外属性的对象
var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);

anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);

yetAnotherPerson.friends.push("Style");

alert(person.friends);//"Van,Louis,Nick,Rob,Style"
/*
object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同:
每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:
*/

var person = {
    name : "Van"
};
var anotherPerson = Object.create(person, {
    name : {
        value : "Louis"
    }
});
alert(anotherPerson.name);//"Louis"

//提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.

5.寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function createAnother(original){
    var clone = object(original);//通过调用object函数创建一个新对象
    clone.sayHi = function(){//以某种方式来增强这个对象
        alert("hi");
    };
    return clone;//返回这个对象
}

/*
这个例子中的代码基于person返回了一个新对象--anotherPerson. 新对象不仅具有 person 的所有属性和方法, 而且还被增强了, 拥有了sayH()方法.
注意: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.

*/

6.寄生组合式继承

1
2
3
4
5
6
function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

原型链?原型?

1、prototype和__proto__的区别

1
2
3
4
5
6
7
var a = {};
console.log(a.prototype); //undefined
console.log(a.__proto__);  //Object {}

var b = function(){}
console.log(b.prototype); //b {}
console.log(b.__proto__);  //function() {}

2、__proto__属性指向谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*1、字面量方式*/
var a = {};
console.log(a.constructor); //function Object() { [native code] } (即构造器Object)
console.log(a.__proto__ === a.constructor.prototype); //true

/*2、构造器方式*/
var A = function (){}; var a = new A();
console.log(a.constructor); // function(){}(即构造器function A)
console.log(a.__proto__ === a.constructor.prototype); //true

/*3、Object.create()方式*/
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.constructor); //function Object() { [native code] } (即构造器Object)
console.log(a2.__proto__ === a1);// true
console.log(a2.__proto__ === a2.constructor.prototype); //false(此处即为图1中的例外情况)

三、什么是原型链

1
2
3
4
5
var A = function(){};
var a = new A();
console.log(a.__proto__); //Object {}(即构造器function A 的原型对象)
console.log(a.__proto__.__proto__); //Object {}(即构造器function Object 的原型对象)
console.log(a.__proto__.__proto__.__proto__); //null

为什么不会去自身的prototype属性上查找,这叫“属性屏蔽”

原型

什么是面向对象编程(OOP)?

简单来说,面向对象编程就是将你的需求抽象成一个对象,然后对这个对象进行分析,为其添加对应的特征(属性)与行为(方法),我们将这个对象称之为 类。

什么是继承?

子类继承父类的属性和方法,同时也可以增加子类自己的属性喝方法,也可以改写或覆盖继承到的属性和方法,继承的优势:代码的抽象和代码的复用

谈谈闭包?

闭包的概念:闭包就是能够读取其他函数内部变量的函数。

闭包的用途:1.可以读取函数内部的变量;2.让这些变量的值始终保持在内存中。3.防止变量被污染

使用闭包的缺点:
1.由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,
否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2.闭包会在父函数外部,改变父函数内部变量的值。

事件流?

事件流的3个阶段:
1.捕获阶段
2.目标阶段
3.冒泡阶段

防抖节流

防抖 debounce

1
2
3
4
5
6
7
8
9
10
11
12
13
//防抖
function debounce(fn,delay){
  let timeout=null;
  return function(){
    clearTimeout(timeout);
    timeout=setTimeout(function(){
      fn.apply(this,arguments);
    },delay)
  }
}
//使用
function fn(){console.log(1111)}
document.querySelector('#ipt').addEventListener('input',debounce(fn,1000))

节流 throttle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//节流
function throttle(fn, threshhold) {
    //声明一个变量timeout
    var timeout
    //实时时间
    var start = new Date;
    //大于 threshhold 的值 才会执行函数
    var threshhold = threshhold || 160
    return function () {
        //存下this arguments 以及 当前时间戳 new Date() - 0
        var context = this, args = arguments, curr = new Date() - 0
        //总是干掉事件回调
        clearTimeout(timeout)
        //当前时间戳 - 上次的执行时间点   如果大于threshhold 就行执行函数
        if(curr - start >= threshhold){
            //console.log("now", curr, curr - start)//注意这里相减的结果,都差不多是160左右
            fn.apply(context, args) //只执行一部分方法,这些方法是在某个时间段内执行一次
            start = curr
        }else{
            //让方法在脱离事件后也能执行一次
            timeout = setTimeout(function(){
                fn.apply(context, args)
            }, threshhold);
        }
    }
}
//使用
function fn(){ console.log('需要被执行代码') }
document.addEventListener('mousemove',throttle(fn,300))

拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function drag(selector){
  let oDiv=document.querySelector(selector);
  let disX=0;
  let disY=0;
  oDiv.onmousedown=function(ev){
    oDiv.style.position='absolute';
    let e=ev||window.event;
    disX=e.clientX - oDiv.offsetLeft;
    disY=e.clientY - oDiv.offsetTop;
    document.onmousemove=function(ev){
      let e=ev||window.event;
      oDiv.style.left = e.clientX - disX + 'px';
      oDiv.style.top = e.clientY - disY + 'px';
    }
    document.onmouseup=function(){
      document.onmousemove=null;
      document.onmouseup=null;
    }
    return false;
  }
}

ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//GET
const xhr=new XMLHttpRequest();
let url='www.xxx.com';
let prame={name:'liming',age:29};
xhr.open('GET',url+'?'+formatePrame(prame));
xhr.send();
xhr.onreadystatechange=function(){
  if(xhr.readyState == 4 &&xhr.status == 200){
    console.log(xhr.responseText)
  }
}



//参数序列化
function formatePrame(o){
  let result=[];
  for(let k in o){
    result.push(k+'='+o[k])
  }
  return result.join('&')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//POST
const xhr=new XMLHttpRequest();
xhr.setRequestHeader('Content-type','application/www-form-urlencoded');
xhr.open('POST',url);
xhr.onreadystatechange=function(){
  if(xhr.readyState == 4 && xhr.status == 200){
    console.log(xhr.responseText)
  }
}
xhr.send(formatePrame(prame))


//参数序列化
function formatePrame(o){
  let result=[];
  for(let k in o){
    result.push(k+'='+o[k])
  }
  return result.join('&')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function ajax(prames){
  let type=prames.type.toUpperCase();
  let url=prames.url;
  let data=prames.data;
  let success=prames.success;
  let error=prames.error;
  let async=prames.async;
  let formatePrame=function(o){
    let result=[];
    for(let k in o){
      result.push(k+'='+o[k])
    }
    return result.join('&')
  };
  let xhr=new XMLHttpRequest();
  if(type=='GET'){
    xhr.open('GET',url+'?'+formatePrame(data));
    xhr.send();
  }else{
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
    xhr.open('POST',url,async);
    xhr.send(formatePrame(data))
  }
  xhr.onreadystatechange=function(){
    if(xhr.readyState == 4){
      success(xhr.responseText)
    }else{
      error(xhr.responseText)
    }
  }
}

浅拷贝

1
2
3
4
5
6
7
function extend(p){
  var c={};
  for(var i in p){
    c[i]=p[i];
  }
  return c
}

深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
function deepCopy(p,c){
  var c=c||{};
  for(var i in p){
    if(typeof p[i] === 'object'){
      c[i]=(p[i].constructor == Array)?[]:{};
      deepCopy(p[i],c[i])
    }else{
      c[i]=p[i];
    }
  }
  return c
}

promise的回调函数执行时间和定时器的函数执行时间

1
2
3
4
5
6
7
8
9
10
11
12
//定时器
setTimeout(function(){
  console.log(1)
},0)

new Promise(function(resolve,reject){
  console.log(2)
  resolve()
}).then(function(){//promise实例的回调函数
  console.log(3)
})
console.log(4)

js代码执行会生产一个执行上下文环境,首先会预解析,遇到var function 以及参数都会再次预解析,
初始值都是undefined,预解析完毕,开始从上到下开始执行,
遇到定时器直接加到任务队列,接着遇到Promise实例化,就会直接执行实例化里面的代码首先打印出2,然后遇到Promise实例的回调函数then(),
这个其实就是微任务 微任务追加到本轮事件循环 ,然后再遇到console.log(4),直接打印出4,
然后再取出微任务的then(),执行打印出3,最后取出定时器,执行打印出1,
打印顺序:2431

说一下CSS盒模型

CSS盒模型包括内容、内边距,边框,外边距,
CSS盒模型又分为 W3C 标准盒模型 和 IE 怪异盒模型,
两则的区别是:
标准模型的宽高为content的宽高
IE模型的宽高包括border
通过设置:
标准模型:box-sizing:content-box
IE模型:box-sizing:border-box,
来转换一些常见的应用场景.

怎样让一个元素水平垂直居中

1.万能居中法,这个元素的定位设置为绝对定位,top,bottom,left,right都设置成0,margin设置为auto即可
2.伸缩盒子flex,这个元素的父元素转换成flex,justify-content设置为center,align-items设置为center即可.
3.元素设置为绝对定位,left设置为50%,top设置为50%,再设置transform为translate(-50%,-50%)即可

知道什么是同源策略吗

同协议同主机同端口,是一个安全策略

那怎么解决跨域问题

CORS 和 JSONP,或者后端设置代理

JSONP 原理

HTML 页面中在通过相应的标签从不同域名下加载静态资源,而被浏览器允许,
基于此原理,可以通过动态创建script,再请求一个带参网址实现跨域通信

重绘和重排的区别

重绘不一定需要重排,重排必然导致重绘

Cookie、localStorge、sessionStorage的区别?

1、数据的生命周期:
Cookie一般有服务器生成,可设置失效时间,如果在浏览器端生成,默认是关闭浏览器失效;
localStorge除非被清除,否则永久保存;
sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除;

2、存放数据大小:
Cookie是4k左右;
localStorge和sessionStorage一般在5MB左右;

3、与服务器通信:
Cookie每次都会携带在HTTP头中,不宜使用Cookie保存过多数据;
localStorge和sessionStorage仅在客户端(即浏览器)中保存,不参与和服务器的通信;

4、易用性:
Cookie源生API不够友好,需要程序员组件封装;
localStorge和sessionStorage源生接口可以接受,但也可再次封装,以对object和array有更好的支持;

用户输入url到按下enter返回页面,中间发生了什么?

1、解析DNS,发送到DNS服务器,并获取域名对应的web服务器对应的ip地址;
2、与web服务器建立TCP连接;
3、浏览器想web服务器发送http请求;
4、web服务器响应请求,并返回指定url的数据(或错误信息,或重定向的新的url地址);
5、浏览器下载web服务器返回的数据及解析html源文件;
6、生成DOM树和CSSOM,解析js,生成render数,layout(布局),GPU painting(像素绘制页面);

VUE篇

1、说说vue的生命周期?

vue的生命周期图
我们首先需要创建一个实例,也就是在 new Vue ( ) 的对象过程当中,首先执行了init(init是vue组件里面默认去执行的),在init的过程当中首先调用了beforeCreate,然后在injections(注射)和reactivity(反应性)的时候,它会再去调用created。所以在init的时候,事件已经调用了,我们在beforeCreate的时候千万不要去修改data里面赋值的数据,最早也要放在created里面去做(添加一些行为)。

当created完成之后,它会去判断instance(实例)里面是否含有“el”option(选项),如果没有的话,它会调用vm.$mount(el)这个方法,然后执行下一步;如果有的话,直接执行下一步。紧接着会判断是否含有“template”这个选项,如果有的话,它会把template解析成一个render function ,这是一个template编译的过程,结果是解析成了render函数:

1
2
3
render (h) {
  return h('div', {}, this.text)
}

解释一下,render函数里面的传参h就是Vue里面的createElement方法,return返回一个createElement方法,其中要传3个参数,第一个参数就是创建的div标签;第二个参数传了一个对象,对象里面可以是我们组件上面的props,或者是事件之类的东西;第三个参数就是div标签里面的内容,这里我们指向了data里面的text。

使用render函数的结果和我们之前使用template解析出来的结果是一样的。render函数是发生在beforeMount和mounted之间的,这也从侧面说明了,在beforeMount的时候,$el还只是我们在HTML里面写的节点,然后到mounted的时候,它就把渲染出来的内容挂载到了DOM节点上。这中间的过程其实是执行了render function的内容。

在使用.vue文件开发的过程当中,我们在里面写了template模板,在经过了vue-loader的处理之后,就变成了render function,最终放到了vue-loader解析过的文件里面。这样做有什么好处呢?原因是由于在解析template变成render function的过程,是一个非常耗时的过程,vue-loader帮我们处理了这些内容之后,当我们在页面上执行vue代码的时候,效率会变得更高。

beforeMount在有了render function的时候才会执行,当执行完render function之后,就会调用mounted这个钩子,在mounted挂载完毕之后,这个实例就算是走完流程了。

后续的钩子函数执行的过程都是需要外部的触发才会执行。比如说有数据的变化,会调用beforeUpdate,然后经过Virtual DOM,最后updated更新完毕。当组件被销毁的时候,它会调用beforeDestory,以及destoryed。
这就是vue实例从新建到销毁的一个完整流程,以及在这个过程中它会触发哪些生命周期的钩子函数。那说到这儿,可能很多童鞋会问,钩子函数是什么意思?

钩子函数,其实和回调是一个概念,当系统执行到某处时,检查是否有hook,有则回调。说的更直白一点,每个组件都有属性,方法和事件。所有的生命周期都归于事件,在某个时刻自动执行。

官网解释:

Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。

每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁。

① :实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载呢,只是一个空壳,无法访问到数据和真实的dom,一般不做操作

②:挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数,在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取

③:接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取

④:接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情...

⑤:当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿

⑥:当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的虚拟dom

⑦:当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等

⑧:组件的数据绑定、监听...去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以

2、说说vue的实现原理?

当一个Vue实例创建时,vue或遍历data选项的属性,用Objec.defineProperty将它们转为getter/setter并且在内容追踪相关依赖,在属性在访问和修改时通知变化.
每个组件实例都有相应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新.

3、谈谈你对MVVM开发模式的理解?

MVVM分为Model、View、ViewModel三者。
1):Model:代表数据模型,数据和业务逻辑都在Model层中定义;
2):View:代表UI视图,负责数据的展示;
3):ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了Model和View的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作dom。

4、v-if 和 v-show 有什么区别?

v-show 仅仅控制元素的显示方式,将 display 属性在 block 和 none 来回切换;而v-if会控制这个 DOM 节点的存在与否。当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。

5、delete和Vue.delete删除数组的区别

delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。

Vue.delete 直接删除了数组 改变了数组的键值。

6、如何优化SPA应用的首屏加载速度慢的问题?

1):将公用的JS库通过script标签外部引入,减小 app.bundel 的大小,让浏览器并行下载资源文件,提高下载速度;

2):在配置 路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundel 的体积,在调用某个组件时再加载对应的js文件;

3):加一个首屏loading图,提升用户体验;

7、什么是mvvm?

MVVM是Model-View-ViewModel的缩写。mvvm是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。

在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。

ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

8、mvvm和mvc区别?

mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到View 。

9、vue优点是什么?

低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,

9、路由之间跳转?h5>

声明式(标签跳转) 编程式( js跳转)

router.push('index')

10、vue如何实现按需加载配合webpack设置?

webpack中提供了require.ensure()来实现按需加载。以前引入路由是通过import 这样的方式引入,改为const定义的方式进行引入。

不进行页面按需加载引入方式:import home from '../../common/home.vue'

进行页面按需加载的引入方式:const home = r => require.ensure( [], () => r (require('../../common/home.vue')))

「两年博客,如果觉得我的文章对您有用,请帮助本站成长」

赞(3) 打赏

感谢您让我添加个鸡腿!

支付宝
微信
3

感谢您让我添加个鸡腿!

支付宝
微信
标签:

上一篇:

下一篇:

共有 0 条评论 - JavaScript基础相关

博客简介

一位不知名的前端工程师,专注全栈技术,分享各种所遇问题与个人心得,梦想成为一位知名大神!

精彩评论

服务热线:
 177****6038

 QQ在线交流

 旺旺在线