当前位置:   article > 正文

js 面试_js面试

js面试

1 JS中的8种数据类型及区别

包括值类型(基本对象类型)和引用类型(复杂对象类型)

js中数据类型分为基本数据类型和引用数据类型。


基本数据类型有Number,String.
Boolean,Null,Undefined,es6新增的Symbol以及es10新增的Biglnt(任意精度整数)七类。
引用数据类型即obiect类,比如对象数组,以及函数。


存储位置:

基本类型存储在栈内存中

而引用数据类型在栈内存中存的是对象指针,这个指针指向堆内存中的值,也就是说实际值是存在堆内存中,栈中存的是对象在堆内存中的引用地址,通过这个引用地址可以找到保存在堆内存中的对象。如果两个变量保存的是同一个地址值,说明指向的是同一份数据,当一个变量修改属性时,另一个也必然会受到影响。

使用场景:

Symbol:使用Symbol来作为对象属性名(key) 利用该特性,把一些不需要对外操作和访问的属性使用Symbol来定义

BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。

传送门 ☞# JavaScript 数据类型之 Symbol、BigInt

1.1JS中的数据类型检测方案

1.typeof

console.log(typeof 1);               // number
console.log(typeof true);            // boolean
console.log(typeof 'mc');            // string
console.log(typeof Symbol)           // function
console.log(typeof function(){});    // function
console.log(typeof console.log());   // function
console.log(typeof []);              // object 
console.log(typeof {});              // object
console.log(typeof null);            // object
console.log(typeof undefined);       // undefined

优点:能够快速区分基本数据类型

缺点:不能将Object、Array和Null区分,都返回object

2.instanceof

console.log(1 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象

缺点:Number,Boolean,String基本数据类型不能判断

3.Object.prototype.toString.call()

var toString = Object.prototype.toString;
console.log(toString.call(1));      //[object Number]
console.log(toString.call(true));   //[object Boolean]
console.log(toString.call('mc'));   //[object String]
console.log(toString.call([]));     //[object Array]
console.log(toString.call({}));     //[object Object]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call(undefined));  //[object Undefined]
console.log(toString.call(null)); //[object Null]

优点:精准判断数据类型

缺点:写法繁琐不容易记,推荐进行封装后使用

instanceof 的作用

用于判断一个引用类型是否属于某构造函数;

还可以在继承关系中用来判断一个实例是否属于它的父类型。

1.2 instanceof 和 typeof 的区别:

typeof在对值类型number、string、boolean 、null 、 undefined、 以及引用类型的function的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回object

为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。

2 数组

数组的常用方法大全_数组方法_小黑七零八落的博客-CSDN博客

 什么是伪数组?

伪数组(又叫对象数组)
1. 伪数组数据类型是object,数组数据类型 array
2.伪数组长度不可变,数组长度可变
3.伪数组有length,索引,但没有foreach等方法,数组拥有数组全部方法
4.伪数组因为是对象数组所以用 for in遍历,数组更建议用for of
5. 伪数组转数组用 array.from或直接展开运算符展开在一个新数组里
6. 函数的 arguments,原生获取dom标签获得的都是伪数组

2.1伪数组怎么转化为数组

如何通过js将伪数组转为数组_企鹅博客

2.2 数组去重

数组去重的11种方法(史上最全)_数组去除重复的_南风知我意啊的博客-CSDN博客

2.3 获取数组最后一位元素的五种方法

【js】js获取数组最后一位元素的五种方法_js获取数组最后一个数组元素_嘿,小苹果的博客-CSDN博客

2.4随机输出数组中的值

 2.5 数组降维

 数组扁平化

2.6 判断变量是否为数组

方式一:isArray

var arr = [1,2,3];
console.log( Array.isArray( arr ) );

方式二:instanceof 【可写,可不写】

var arr = [1,2,3];
console.log( arr instanceof Array );

方式三:原型prototype

var arr = [1,2,3];
console.log( Object.prototype.toString.call(arr).indexOf('Array') > -1 );

方式四:isPrototypeOf()

var arr = [1,2,3];
console.log(  Array.prototype.isPrototypeOf(arr) )

方式五:constructor

var arr = [1,2,3];
console.log(  arr.constructor.toString().indexOf('Array') > -1 )

2.8 找出多维数组最大值

function fnArr(arr){
    var newArr = [];
    arr.forEach((item,index)=>{
        newArr.push( Math.max(...item)  )
    })
    return newArr;
}
console.log(fnArr([
    [4,5,1,3],
    [13,27,18,26],
    [32,35,37,39],
    [1000,1001,857,1]
]));

3 map和forEach的区别


(共同点]

都可以拿来遍历数组,且每次执行匿名函数都支持三个参数,分别为item (当前项),index (当前索引值),arr (原数组)。这个匿名函数中的this也都是指向window。

(区别]
map(有返回值,会分配内存空间,返回一个和原数组长度一致的新数组基于这一点,map可以进行链式操作,也就是说能够继续对新数组进行filter,every等等的操作,而forEach没有返回值,或者说返回的是一个undefined,因此自然也就不能再链式操作了。


forEach()对每项元素处理后会改变原数组的值。而map()不会改变原数组的数据,而是会返回一个新数组,这个新数组中的元素就是原数组元素调用函数处理之后的值。


使用场景]

当我们调用后端接口返回的数据不满足我们的使用要求,我们想改变数组的数据时,就可以用map,因为它会返回一个新数组,不影响原数据。如果并不打算改变数据,只是想用数据做一些其他事情,比如借助数组的值做一些和它自身值无关的事情,就可以用
forEach。

3.1 说一下js中for....in和for...of的区别。使用场景呢?

for..of
1.遍历的是数组元素值
2.适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.
3.不能遍历对象,因为没有迭代器iterator对象.
4.与forEach()不同的是,它可以正确响应break、continue和return语句
5.只遍历数组内元素,不会遍历原型上的

for-in
1.遍历的是数组的索引(即键名)
2.遍历顺序可能不是实际的内部顺序
3.遍历所有的可枚举属性,包括原型。例如的原型方法method和name属性
4.适合遍历对象

for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。

遍历对象用for in
遍历数组用for of

3.2 every和some的区别

3.3find和filter的区别

find找出来的是里面其中一项,而filter是对数组的过滤,一个返回项目,一个返回数组

3.4 slice是干嘛的,splice是否会改变原数组

1. slice是来截取的
    参数可以写slice(3)、slice(1,3)、slice(-3)

   -3就是倒数第三个,(1,3)就是1,2不包括3
    返回的是一个新的数组


2. splice 功能有:插入、删除、替换
    返回:删除的元素
    该方法会改变原数组

4 字符串常用的方法

10种常见的JavaScript 字符串操作方法汇总

4 创建对象,函数 

4.1创建函数的几种方式

  1. 3.1 函数声明式
  2. function sum(num1,num2){
  3.    return num1+num2;
  4. }
  5. 3.2函数表达式
  6. var sum=function(num1,num2){
  7.    return num1+num2;
  8.  }
  9. 3.3函数对象方式
  10. var sum=new function('num1','num2','return num2+num1')

4.2Javascript创建对象的几种方式

  1. 4.1 字面量方式
  2. const Cat={}
  3. Cat.name='ruiky'  //给对象添加属性并且赋值
  4. Cat.say=function(){
  5.    console.log('haha');
  6.  }  //给对象添加方法
  7.  
  8. Cat.say()  //调用对象的方法
  9. 4.2使用构造函数⭐⭐
  10. //构造函数模式
  11.         function Person(name, age, sex) {
  12.             this.name = name
  13.             this.age = age
  14.             this.sex = sex
  15.         }
  16.         let Person1 = new Person('小蘑', 18, '男')
  17.         console.log(Person1);

构造函数在js(ES5)中相当于其它面向对象编程语言中的类,对象称为类的实例,类称为对象公共特性的抽象。构造函数创建对象的过程又称为实例化。、


4.3new关键字的作用:

  1. 1. 创建了一个空的对象
  2. 2. 将空对象的原型,指向于构造函数的原型
  3. 3. 将空对象作为构造函数的上下文(改变this指向)
  4. 4. 对构造函数有返回值的处理判断:
  5. (如果是基本类型则无视,如果是引用类型则返回该对象,new不起作用了)

4.4如何区分数组和对象?

1.通过ES6中的Array.isArray 来识别

  1. Array.isArray([]) //true 
  2. Array.isArray({}) //false

2.通过instanceof 识别

  1. [] instanceof Array //true
  2. {} instanceof Array //false

3.通过调用constructor来识别

  1. {}.constructor //返回 object
  2. [].constructor //返回 Array

4.通过 Object.prototype.toString.call 

  1. Object.prototype.toString.call([]) //["object Array"]
  2. Object.prototype.toString.call({}) 

4.5 怎样判断两个对象相等

ES6中 Object.is() 方法来比较两个对象引用的内存地址是否一致来判断这两个对象是否相等。

当需求是比较两个对象内容是否一致时就没用了
想要比较两个对象内容是否一致,思路是要遍历对象的所有键名和键值是否都一致:
1、判断两个对象是否指向同一内存
2、使用 Object.getOwnPropertyNames 获取对象所有键名数组
3、判断两个对象的键名数组是否相等
4、遍历键名,判断键值是否都相等

 https://www.cnblogs.com/yuhuo123/p/16064070.html

4.6 如何判断对象是否存在某个属性

如何判断对象是否存在某个属性
需要判断的我们先假设这个对象是obi,属性是age。


1lobj.hasOwnProperty(age),顾名思义,hasOwnProperty意思就是是否有自己的属性age,这个方法返回的是一个布尔值,存在age就返回true,否则返回false。另外,需要知道的是,这个方法只判断自身上的属性,不会去查找原型上全早编程十点半是否有age属性


2判断obiage != undefined 或者obilage]! undefined,通过objage或者obi[age来获取对象的属性值,如果对象上不存在age属性,则会返回undefined。这里的不存在是指对象和对象原型链上都不存在age属性。换句话说,它会从自身和其原型链上找这个age属性。再拓展一下,这个方法不能用在对象的属性值存在,但属性值为 undefined的场景下,比如obj里有age属性,age的值是undefined,那么此时obj.age就等于undefined,这时我们不能就认为没有
age属性,实际这个属性是存在的,只是它的值和我们判断的标准一致了。如果可能出现这种情况,那我们就最好不使用这个方法。


3 运用in 运算符,这个方法和刚提到的第二种方式一样,会从自身和原型链上查找属性。如果age'in obi返回的是true那么说明在这个对象或者其原型链中存在
age属性。


[总结]

第一,obi.hasOwnProperty(属性)判断是否为true,true则存在,false则不存在。该方法只在自身上查找。
第二或者obi[届性]判断是否为obi.xxxundefined,是则不存在,不是则存在需要注意它的使用场景哦。该方法会在自
身和其原型链上查找。
第三,in运算符,xxx in obi,存在则会返回true,不存在返回false。这个方法也是会从自身和其原型链上查找。

5null 和 undefined 究竟有何区别?

1总的来说 null 和 undefined 都代表空,主要区别在于 undefined 表示尚未初始化的变量的值,而 null 表示该变量有意缺少对象指向。

  • undefined
    • 这个变量从根本上就没有定义
    • 隐藏式 空值
  • null
    • 这个值虽然定义了,但它并未指向任何内存中的对象
    • 声明式 空值

2.  null 转为数值是 0 ;  undefined 转为数值是 NAN(not a number)。

3.  null 通过 typeof 判断类型的时候结果的输出是 object ; 而 undefined 的类型是 undefined 。

 三、null 和 undefined分别在实际项目中出现的场景有哪些
1、 undefined

   a. 变量被声明了,但是没有被赋值;

   b. 调用函数的时候,应该给函数传参却没有给函数传这个参数打印出来就是 undefined;

   c. 访问一个对象中没有的属性;

   d. 函数没有返回值时,默认就会返回undefined。

2、 null

   a.作为对象原型链的终点出现;

   b.当我们访问一个不存在的dom节点的时候。

null 和 undefined 的区别_undefined和null的区别_一目子的博客-CSDN博客

6==和===有什么不同?

 "==" 只判断等号两边的值是否相等,而不判断类型是否相同。值相同则返回 true

 "===" 既要判断值是否相等,也要判断类型是否相同,即全等才能返回 true

js中==和===的区别以及总结_普通网友的博客-CSDN博客

7 var、let、const 区别?

var、let、const 共同点都是可以声明变量的

区别一:
    var 具有变量提升的机制
    let和const没有变量提升的机制
区别二:
    var 可以多次声明同一个变量
    let和const不可以多次声明同一个变量
区别三:
    var、let声明变量的
    const声明常量
    
    var和let声明的变量可以再次赋值,但是const不可以再次赋值了。
区别四:
    var声明的变量没有自身作用域
    let和const声明的变量有自身的作用域

var、let、const 共同点都是可以声明变量的

var 存在变量提升。
let 只能在块级作用域内访问。
const 用来定义常量,必须初始化,不能修改(对象特殊)

7.1  Let 中提到的暂时性死区到底是什么,它的产生原因是?

    ES6 规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。在初始化之前使用变量,就会形成一个暂时性死区。也就是说使用let声明的变量都是先声明再使用 ,不存在变量提升问题。
let暂时性死区理解_let 暂时性死区_小白大雪的博客-CSDN博客

面试题:作用域考题

考题一:let和const没有变量提升性

console.log( str );//undefined
var str = '你好';
​
console.log( num );//报错
let num = 10;

考题二:

function demo(){
    var n = 2;
    if( true ){
        var n = 1;
    }
    console.log( n );//1
}
demo();
​
​
function demo(){
    let n = 2;
    if( true ){
        let n = 1;
    }
    console.log( n );//2
}
demo();

考题三:可以修改

const obj = {
    a:1
}
obj.a = 11111;
console.log( obj )
​
const arr = ['a','b','c'];
arr[0]= 'aaaaa';
console.log( arr );

8 异步线程,轮询机制,宏任务微任务 ***

JS微任务和宏任务

1. js是单线程的语言。
2. js代码执行流程:同步执行完==》事件循环
    同步的任务都执行完了,才会执行事件循环的内容
    进入事件循环:请求、定时器、事件....
3. 事件循环中包含:【微任务、宏任务】
微任务:promise.then
宏任务:setTimeout..

要执行宏任务的前提是清空了所有的微任务

流程:同步==》事件循环【微任务和宏任务】==》微任务==》宏任务=》微任务...

为什么要声明异步操作? 

Javascript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。单线程导致的问题就是后面的任务等待前面任务完成,如果前面任务很耗时(比如读取网络数据),后面任务不得不一直等待!!

为了解决这个问题,利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制。于是,JS 中出现了同步任务和异步任务。

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务不进入主线程、而进入”任务队列”的任务,当主线程中的任务运行完了,才会从”任务队列”取出异步任务放入主线程执行。

异步任务的 ”任务队列”有分宏任务和微任务

常见宏任务:

  • setTimeout()
  • setInterval()
  • setImmediate()

微任务:

  • promise.then()、promise.catch()

----------------------

运行机制:

1  先看是同步任务还是异步任务,同步任务放在执行栈当中,异步任务又分为宏任务和微任务,

微任务 放到微任务队列里, 宏任务放到宏任务队列里边去依次排队。 原则都是先进先出

2  先执行执行栈里边所有的同步任务,所有的同步代码执行完毕之后,

先去微任务队列去看看, 有没有微任务进行排队,如果有的话,根据先进先出的原则,吧任务拿出来,通过事件循环的方式推到我们执行栈里边执行这个微任务,这个微任务执行完之后,再去看看微任务队列还有没排队的,如果有,再利用事件循环取出来推到执行栈里边执行 。 直到执行完毕。

微任务执行完成之后,去看宏任务队列有没有,根据先进先出的原则,通过事件循环的方式推到我们执行栈里边执行。 执到完毕

9  解释下JavaScript中this是如何工作的 **

this永远指向函数运行时(调用)所在的对象,而不是函数被创建时所在的对象。匿名函数或不处于任何对象中的函数指向window 。 普通的函数调用,函数被谁调用,this就是谁。

函数、定时器、立即执行函数指向window构造函数指向实例对象对象的方法指向对象本身,事件绑定的方法指向触发这个事件的对象

改变this的指针指向

call  改变并调用   第二个及以后的参数是原函数的参数

apply 改变并调用  第二个参数是一个数组

bind  只改变指向,不会调用,需要手动调用

如果是call,apply,bind,指定的this是谁,就是谁。

https://www.cnblogs.com/minb/p/6437370.html

his在js中是一个非常重要的特性,而且在不同的环境下this指向不同,也是比较容易出现错误的一个点,所以我们具体来看看this在不同环境下分别代表什么.

1. 全局中:this指向window

  1. 1.<script>
  2. 2.      console.log(this,’在全局中的this’)
  3. 3.    </script>

2. 普通函数: window

  1. 1. <script>
  2. 2.    function fun(){
  3. 3.      console.log(this,’在普通函数中的this’)
  4. 4.    }
  5. 5.   fun()
  6. 6.</script>

3. 对象的方法: 当前对象

  1. 1.   <script>
  2. 2.      var obj = {
  3. 3.        fun: function () {
  4. 4.        console.log(this,’对象中的this呀‘);
  5. 5.    }
  6. 6.};
  7. 7.obj.fun();
  8. 8.    </script>

4. 事件处理函数: 触发源 点谁就指向谁

  1. 1.<div>事件函数中的this呀呀呀</div>
  2. 2. <script>
  3. 3.     var oDiv = document.getElementsByTagName('div')[0];
  4. 4.     oDiv.onclick = function(){
  5. 5.         console.log(this,'事件函数中的this')
  6. 6.     }
  7.  </script>


5. 构造函数中 :指向创建的实例对象

  1. 1.  <script>
  2. 2.      function Fun() {
  3. 3.            that = this;
  4. 4.        }
  5. 5.        var fun1 = new Fun();
  6. 6.        console.log( that == fun1 ); // 结果为true
  7. 7.
  8. 8.        var fun2 = new Fun();
  9. 9.        console.log( that == fun1 ); // 结果为false
  10. 10.        console.log( that == fun2 ); // 结果为true
  11.     </script>


通过上图可以看出 构造函数中的this指向创建的实例对象

6. 箭头函数中的this: 在箭头函数中,this就失效了
1)当我们使用箭头函数的时候,箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的。

2) 箭头函数中的this引用的就是最近作用域中的this 

3) 向外层作用域中,一层一层查找this,直到有this的定义  

  1. 1.   <script>
  2. 2.        var obj = {
  3. 3.            fun: () => {
  4. 4.                console.log(this, '这是箭头函数中的this呀')
  5. 5.            }
  6. 6.        }
  7. 7.        obj.fun() // window
  8. 8.    </script>

此处对象中的箭头函数指向window


定时器中的this指向问题:

定时器的this指向是window,必须想要在定时器内部改变this指向,才能更好的使用定时器。

  1. 1.<script>
  2. 2.        const obj = {
  3. 3.            aaa: function () {
  4. 4.                console.log(this,'这是定时器外层的this呀') // obj
  5. 5.                setTimeout(function () {
  6. 6.            console.log(this,'这是定时器中的this呀');// window
  7. 7.                })
  8. 8.            },
  9. 9.        }
  10. 10.        obj.aaa();
  11.     </script>


如上会发现定时器外部的this会指向当前对象,定时器内部使用普通函数形式,指向的是window. 


那此时如果函数内部想要使用外部this, 我们可以将外部this赋值给一个变量that,内层使用that.

  1. 1. <script>
  2. 2.        const obj = {
  3. 3.           bbb: function () {
  4. 4.                console.log(this,'这是定时器外层的this呀')
  5. 5.                var that = this;
  6. 6.                setTimeout(function(){
  7. 7.                    console.log(that,'定时器内层')
  8. 8.                })
  9. 9.            }
  10. 10.        }
  11. 11.        obj.bbb();

或者我们也可以将 定时器第一个参数设置为箭头函数, 箭头函数内层this和外层this是一样的。

 关于JS的this指向问题 - 哔哩哔哩

10箭头函数和普通函数的区别?


1. 写法不同,箭头函数使用箭头定义,写法简洁。 普通函数使用function定义。
2. 箭头函数都是匿名函数,而普通函数既可以是匿名函数,也可以是具名函数。
3. 箭头函数不能作为构造函数来使用,普通函数可以用作构造函数,以此来创建一个对象的实例。
4. this指向不同,箭头函数没有this,在声明的时候,捕获上下文的this供自己使用,一旦确定不会再变化。在普通函数中,this指向调用自己的对象,如果用在构造函数,this指向创建的对象实例。普通函数可以使用call,apply,bind改变this的指向。
5. 箭头函数没有arguments(实参列表,类数组对象),每一个普通函数在调用后都有一个arguments对象,用来存储实际传递的参数。
6. 箭头函数没有原型,而普通函数有。

10.1箭头函数的使用注意事项

js中this的使用一直以来都比较难以理解,和php相比要复杂许多。自从ES6中新增了箭头函数的API,this的使用情况变得更加复杂。这里做几点简单的对比~
主要资料参考自大神阮一峰翻译的:阮一峰ES6教程 

1, 箭头函数没有prototype(原型),所以箭头函数本身没有this,也就不能用call()、apply()、bind()这些方法去改变this的指向。

  1. let a = () => {};
  2. console.log(a.prototype); // undefined

2, 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。如果箭头函数外层没有普通函数,它的this会指向window(全局对象)

  1. function test() {
  2. this.type = 'cat'
  3. this.obj = {
  4. name: 'tom',
  5. getType: () => {
  6. return this.name + ',' + this.type
  7. },
  8. getName: function () {
  9. return this.name + ',' + this.type
  10. }
  11. }
  12. }
  13. let t = new test()
  14. console.log(t.obj.getType())
  15. console.log(t.obj.getName())
  16. // "undefined,cat"
  17. // "tom,undefined"

值得注意的是:如果外层的普通函数的this指向发生改变,箭头函数的this跟着会发生改变。

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

  1. function foo() {
  2. setTimeout(() => {
  3. console.log('id:', this.id);
  4. }, 100);
  5. }
  6. var id = 21;
  7. foo.call({ id: 42 });
  8. // id: 42

本例通过call()方法替换了foo()的上下文context,使foo的this指向了新的对象。由于setTimeout 的参数为一个箭头参数,它的生效时间是在100毫秒之后,箭头函数输出了42

4, 箭头函数没有constructor,使用new调用箭头函数都会报错

  1. let a = () => {};
  2. let b = new a(); // a is not a constructor

同是也无法使用new.target
更多移步:阮一峰ES6教程 -- new-target-属性

5, 箭头函数的arguments

第一种情况:箭头函数的this指向全局对象,会报arguments未声明的错误。
第二种情况是:箭头函数的this如果指向普通函数,它的argumens继承于该普通函数。

  1. let foo = () => {
  2. console.log(arguments);
  3. };
  4. foo(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined
  1. function foo() {
  2. setTimeout(() => {
  3. console.log('args:', arguments);
  4. }, 100);
  5. }
  6. foo(2, 4, 6, 8)
  7. // args: [2, 4, 6, 8]

可以使用rest参数(扩展运算符...)来获取函数的不定数量参数

  1. let foo = (first, ...rest) => {
  2. console.log(first, rest) // 1 [2, 3, 4]
  3. };
  4. foo(1, 2, 3, 4)

6,不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

11闭包

闭包是这样的一种机制:函数嵌套函数,内部函数可以引用外部函数的参数和变量。参数和变量不会被垃圾回收机制收回。

1 子函数使用父函数变量的行为

闭包的好处:

1.可以让一个变量长期在内存中不被释放

2.避免全局变量的污染,和全局变量不同,闭包中的变量无法被外部使用

3.私有成员的存在,无法被外部调用,只能直接内部调用

闭包可以完成的功能:1.防抖、2.节流、3.函数柯里化

 闭包的缺点:

  • 闭包的缺点就是常驻内存,会增大内存使用量,使用不当会造成内存泄漏

什么是闭包?以及闭包的作用_fuyuyuki的博客-CSDN博客

11.1  javascript的内存(垃圾)回收机制?

  • 垃圾回收器会每隔一段时间找出那些不再使用的内存,然后为其释放内存
  • 一般使用标记清除方法(mark and sweep), 当变量进入环境标记为进入环境,离开环境标记为离开环境
  • 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
  • 还有引用计数方法(reference counting), 在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个 变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。
  • 在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的, 也就是说只要涉及BOM及DOM就会出现循环引用问题。
     

11.2 如何销毁闭包中的变量

1

2

3

4

5

6

7

8

9

10

11

var foo = (function() {

    var n = 0;

    return {

        add: function () {

            return ++n;

        },

        clearVariable: function () {

            n = null;

        }

    }

})();

外部无法访问到闭包里面的变量, 但可以在闭包内部返回一个方法, 该方法将闭包内部的变量设置为null, 让变量失去引用,会被系统自动回收。

实际上如果只是要把闭包内的一些变量给删除, 那么我觉得这样的程序设计是有问题的,  如果你的变量只需要用一次,就要删除,那么使用闭包来实现这样的方法显然是浪费。   如果闭包不需要了, 想删除闭包,直接 foo = null; 就可以了。

12 原型

每个函数都有prototype属性,称之为原型, 因为这个属性的值是个对象, 也称为原型对象

作用:

1 存放一些属性和方法

2 在js中实现继承

继承案例: 例如  只要创建一个数组,就可以使用reverse,sort等方法,就是因为原型的存在。 

Array 构造函数也是一个函数,他就会有一个原型Array.prototype,再原型身上已经挂载了很多方法。

通过构造函数生成一个实例arr,每一个对象身上都有一个__proto__属性,他指向原型对象

这样  arr就可以使用Array原型身上的方法了。 

__proto__:每个对象都有__proto__属性

作用: 这个属性指向他的原型对象

12.1 原型链

创建一个构造函数叫Person, 只要是函数就有一个原型对象Person.prototype.我们可以吧一些共享的属性方法挂载他身上。

通过new Pserson 创建一个对象实例, 只要是对象 ,就有__protp__。 这个__protp__指向原型对象了, 所以说person对象就可以使用原型身上的一些属性和方法了。 

如果原型的 一些属性和方法没有,他也不会断, 因为Person.prototype.也是一个对象,只要是对象 ,就有__protp__,他指向了更上一级的,就是Object最大的对象里边的原型。  我找不到就会继续往上走,看上一级有没有这些属性和方法。 如果没有继续往上找,直到null,找不到为止。

这条线路称之为原型链

12.2 什么数据存在对象中,什么数据存在prototype中

只要是对象的独有属性和方法,就应该定义在对象本身上

而对于一些多个对象需要共享的属性和方法,可以将它们定义在对象的原型(prototype)对象上。这样,多个对象就可以共享这些属性和方法,无需在每个对象上都定义一遍

一般来说,满足以下两点之一的属性或方法,应该定义在对象的原型对象上:

  1. 多个对象需要共享它;
  2. 它需要被继承。

13 防抖和节流

13.1 防抖

我在执行期间, 如果有新的新的事件进来,我会吧以前的作废,又重新开始新的一轮

2  什么时候用防抖?

 在我们输入搜索框的时候,我们不能每次进入输入的时候都调用接口,会非常繁琐。我们可以设定一个时间,再多少秒之后发送请求

3 代码 : 定时器

13.2 节流

 2  什么时候用节流?

设定一个时间,再这段时间之内,不管你触发多少次的事件,我都不会执行想要的代码。 等这段时间执行完了,才进行下轮的执行。 

 3 代码: 定时器

14 深拷贝和浅拷贝的区别

        

浅拷贝只拷贝对象的引用,是对指针的拷贝,拷贝后两个指针指向同一个内存,同一份数据,意味着当原对象发生变化的时候,拷贝对象也跟着变化;

深拷贝不但对指针进行拷贝,而且还对指针指向的内容进行拷贝,也就是另外申请了一块空间内存,内容和原对象一致,但是是两份独立的数据,更改原对象,拷贝对象不会发生变化。

大白话:假设B复制了A,当修改A时,如果B也跟着变了,说明只拷贝了指针,A,B实际共用一份数据,这是浅拷贝;如果A变,B没变,那就是深拷贝,复制对象不受原对象影响。因为不仅拷贝了指针,还拷贝了内容,他们自己有自己的内存,互相独立。

Tips:引用数据类型才有引用,指针这些概念,所以我们要知道深浅拷贝一般只针对引用数据类型的数据而言。

2 如何实现深拷贝

        浅拷贝很简单,把变量A直接赋值给变量B,这就是浅拷贝,B会随A的变化而变化。面试中一般重点都会放在深拷贝的实现方式上。

下面我们来看3个深拷贝的实现方式:

JSON.parse(JSON.stringify(obj))

js内置的JSON对象的序列化和反序列化方法结合可以实现深拷贝,但是需要注意的是这种方法是有局限的,比如无法实现对对象中方法的深拷贝,取不到原对象上值为 undefined 的 key等等。

② 递归实现(推荐)

       经常面试的时候,面试官会说出这样一道题,来,我们手写一个深拷贝。其实就是这第二种方法,实现思路:

a. 传入的原对象,遍历其属性,每个属性需要判断它的值是否是object类,如果不是,说明是基本数据类型,可以直接赋值;

b. 如果是object类,那么需要再具体判断这个数据是对象还是数组,是数组创建一个空数组[],是对象则创建一个空对象{},继续递归;

具体代码如下:

  1. //递归实现深拷贝
  2. function deepClone(origin, target){
  3. var target = target || {}; //防止不传target
  4. for(let key in origin){ //遍历origin中的属性
  5. if(origin.hasOwnProperty(key)){ //判断自身是否有该属性而非原型链上的
  6. if( origin[key] && typeof(origin[key]) == "object"){ //如果当前value是一个object类型,需要往下递归
  7. target[key] = Array.isArray(origin[key]) ? [] : {}; //判断这个object类型,具体是数组还是对象
  8. deepClone(origin[key], target[key]); //递归
  9. }else{
  10. target[key] = origin[key]; //如果当前value不是一个object类型,直接赋值即可
  11. }
  12. }
  13. }
  14. return target; //返回最终的拷贝对象
  15. }

③ 第三方库loadash的cloneDeep方法

loadash是一个很热门的函数库,我们引入这个库后,就可以直接使用这个方法了,但是如果项目本身没有引入这个库,就不要为了使用深拷贝专门引入整个库,这样有点得不偿失~

3 总结

       深拷贝不仅面试常见,同时在实际开发中也是非常有用的。例如后台返回了数据,我们需要对这些数据做操作,但是这些数据可能有其它地方也需要使用,直接修改就可能会造成很多隐性问题,而把数据做一次深拷贝就能让我们更安全安心的去操作这些数据,因为反正我们复制了一份下来。

15 阻止冒泡和捕获

浅谈事件冒泡和事件捕获 - 知乎

事件冒泡和捕获_柠檬mei的博客-CSDN博客

JS事件机制浅析:事件捕获、事件冒泡和事件委托 - 前端教程

在JavaScript中,事件是以事件流的形式出现的,事件流顺序分为捕获和冒泡两种方式。
事件流分为三个阶段:1.捕获阶段 2.目标阶段 3.冒泡阶段。

事件捕获和事件冒泡是处理DOM事件的两种不同的机制。

15.1 事件捕获和冒泡的顺序

事件捕获的顺序是从最外层的元素开始,逐级向内部元素传播,直到达到目标元素
例如:window -> document -> html -> body -> div。

事件冒泡的顺序是从目标元素开始,逐级向外层元素传播,直到达到最外层的元素
例如:div -> body -> html -> document -> window

15.2事件委托

事件委托也称为事件代理(Event Delegation),事件委托是一种将事件处理程序绑定到一个父元素上,而不是将事件处理程序绑定到每个子元素上的技术。通过事件委托,可以减少事件处理程序的数量,提高性能和代码的可维护性。

  1. <ul id="ul">
  2. <li>1</li>
  3. <li>2</li>
  4. <li>3</li>
  5. <li>4</li>
  6. <li>5</li>
  7. </ul>
  8. <script>
  9. let li = document.getElementsByTagName("li");
  10. for (let i = 0; i < li.length; i++) {
  11. li[i].addEventListener("click", () => {
  12. console.log(li[i].innerHTML)
  13. })
  14. }
  15. </script>

 

点击某一数字,就会输出对应内容。节点少的时候还好,如果节点多达上千上万个,就需要声明相当多的事件函数,比较消耗内存。而且如果列表经常发生动态变更,也会导致大量监听事件的移除和绑定。

在这种情况下,事件委托就可以体现它的优势了。

事件委托正是利用事件流的冒泡特性,将本来要绑定到多个元素的事件函数,委托到了其祖先元素上

  1. //事件代理 节约内存 提升性能(不需要注销子节点)
  2. let ul = document.getElementById("ul");
  3. ul.addEventListener("click", (event) => {
  4. console.log(event.target.innerHTML);
  5. })

我们通过将事件处理程序绑定到父元素ul上,当点击列表项时,通过 event 对象拿到必要的信息,会打印出被点击的列表项的内容。如此这般,不管li有多少,更新多频繁,我们只需要维护一个函数就够了

16 Promise

Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】和【事件】更合理、更强大。现已被 ES6 纳入进规范中。

简单来说,promise对象用来封装一个异步操作并可以获取其结果

语法:

  1. new Promise(function (resolve, reject) {
  2. ...
  3. } /* executor */)

executor:executor是带有 resolvereject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolvereject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回所建promise实例对象前被调用)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用resolve函数来将promise状态改成

fulfilled,要么调用reject 函数将promise的状态改为rejected。如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略

promise 有三种状态

Pending(进行中,初始状态,既不是成功,也不是失败状态。)、Resolved(已完成,又称 Fulfilled)、Rejected(已失败)

  1. 这三种状态的变化途径只有2种:
  2. 异步操作从 未完成 pending => 已完成 resolved
  3. 异步操作从 未完成 pending => 失败 rejected
  4. 状态一旦改变,就无法再次改变状态,这也是它名字 promise-承诺 的由来,一个promise对象只能改变一次

async异步函数同步化

Promise对象执行的任务都是异步函数,返回值通过then方法的res参数接收。

给函数增加了 async、await 关键字,就可以直接获取promise对象返回的结果了,而不用使用then来接收结果。

上面函数的执行,看起来就好像是同步函数执行一样,这就是异步函数同步化处理。

注意:

1.必须增加 async、await 关键字;

2.异步函数同步化,其实代码逻辑只是看起来像普通的顺序执行逻辑,实际上每一步仍然需要等待成功返回,本质上仍然是异步的。

3.以上代码为了简化,缺少了reject失败的部分,添加reject部分后,只需要把执行任务的部分包在try...catch...里就可以了。
 

Promise精选面试题 - 简书

简单理解Promise对象(JavaScript)_什么是promise对象_胡十三刀的博客-CSDN博客

什么是Promise,Promise的三种状态 - 掘金

promise如果后面有多个.then怎么传值

Promise实例可以通过.then()方法链式调用多个回调函数,每个回调函数可以处理前一个回调函数的返回值,即通过return语句将值传递给下一个回调函数。

promise.then(function(value1) {
  // 处理 value1,并返回 value2
  return value2;
}).then(function(value2) {
  // 处理 value2,并返回 value3
  return value3;
}).then(function(value3) {
  // 处理 value3
});

Promise中的值是通过Promise的链式调用不断传递的,每个.then()方法的返回值可以作为下一个.then()方法执行的参数,从而实现值的传递和处理。

async await怎么捕获异常?

可以使用 try/catch 语句来捕捉异步操作中的异常

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

async await的底层原理?generator的原理?

async/await的底层原理是基于ES6中引入的Generator函数和Promise对象实现的。

Generator函数可以通过function*关键字定义,它返回一个可迭代对象,可以通过.next()方法一步一步执行函数中的代码,每次执行到yield语句时,函数暂停并返回一个yield表达式的值。可以通过.next()方法恢复函数执行并传递参数。例如

16.2 什么是异步编程?异步编程的方法

异步编程是一种处理非阻塞I/O操作的技术,允许程序在某些操作执行的同时,执行其他操作,而不是等待这些操作执行完毕后才进行其他操作。

JavaScript中的异步编程可以通过回调函数、Promise、async/await、Generator函数等方式实现。

  1. 回调函数: 在函数调用的过程中传递一个函数作为参数,函数执行完毕后调用这个函数。回调函数虽然容易实现,但是嵌套过多会导致代码难以维护,产生回调地狱(Callback Hell)的问题。
  2. Promise: Promise是ES6中引入的一种异步编程方案。它是对回调函数的一种优化,可以解决回调地狱的问题。它的主要特点是提供了链式调用API,可以使用then()和catch()等方法让代码更加清晰和易于维护。
  3. async/await:async/await是一种基于Promise的语法糖,可以更好地解决回调地狱问题。async表示函数是异步的,await表示在等待异步操作完成后再继续执行。async/await提供了一种更加直观、流畅、易于理解的处理异步操作的方式。
  4. Generator函数+Promise:通过Generator函数的组合使用可以实现一种类似于async/await的效果。Generator函数通过yield关键字来暂停执行,然后通过next()方法继续执行。在Generator函数中,可以使用Promise来处理异步操作。
  5. 发布/订阅模式: 也称为观察者模式,通过定义一种一对多的依赖关系,当某个对象改变状态时,所有依赖它的对象都会得到通知并自动更新。

17 工厂模式

什么是工厂模式?

工厂模式是一种用来创建对象的设计模式。我们不暴露对象创建的逻辑,而是将逻辑封装在一个函数内,那么这个函数可以成为工厂。工厂模式根据抽象程度的不同可以分为:1.简单工厂 2.工厂方法 3.抽象工厂

工厂模式通俗点说就是:更方便地去创建实例

大家开发中应该使用过 axios.create 这个方法吧?这其实就是工厂模式的实践之一

https://www.cnblogs.com/xiaogua/p/10502892.html

JavaScript工厂模式_js工厂模式_北木南-的博客-CSDN博客

17.1(了解)JS设计模式有哪些(单例模式观察者模式等)

答:JS设计模式有很多,但我知道的有单例模式,观察者模式

单例模式

单例模式通俗点说就是:定义一个类,生成一个实例,并且整个项目仅此这一个实例

相信大家在项目中都封装使用过Axios

我们会先定义封装一个请求的实例然后暴露出去

代理模式

代理模式通俗易懂点说就是:为对象提供一种代理,便以控制对这个对象的访问,不能直接访问目标对象

最好的实践场景就是ES6 Proxy

https://juejin.cn/post/7205401322111500344

18 js事件委托


什么是事件委托:通俗的讲,事件就是onclick,onmouseover,onmouseout,等就是事件,委托呢,就是让别人来做,这个事件本来是加在某些元素上的,然而你却加到别人身上来做,完成这个事件。也就是利用冒泡的原理,把事件加到父级上,触发执行效果。
好处1: 提高性能

例如 需要触发每个li来改变他们的背景颜色

js事件委托(事件代理)的原理以及优缺点_teng28的博客-CSDN博客

19.mouseenter 和mouseover的区别 

mouseenter mouseleave 不会冒泡mouseover mouseout 冒泡

20.get和post的区别  *

get 请求参数在地址栏中显示,相对不安全 且请求长度在1k之内

post请求不会在地址栏中显示, 相对安全,请求参数长度没有限制

21 es6 常用的新特性有哪些?

答:ES6新增特性常用的主要有:let/const,箭头函数,模板字符串,解构赋值,模块的导入(import)和导出(export default/export),Promise,还有一些数组字符串的新方法,其实有很多,我平时常用的就这些

11个常用的ES6新特性整理,速看! - 知乎

22 JS继承有哪些方式

1es6 新增extends继承,子继承父,但是得在父类上添加super ,否则会报错,没啥缺点

2原型链 :父的实例给到子的原型上, 他的缺点的话: 更改一个子类的属性和方法,其他子类也跟着变了

3 构造函数实现继承,在子的里边改变了this指向,只能继承父类的实例属性和方法,不能继承原型属性或者方法

4 组合性继承:就是原型和构造函数结合

23 forech可以中断吗? 如果遍历到某一个索引, 跳出循环,怎么做?

我们可以选择使用try...catch的方式进行跳出循环。

  1. const arr = [0, 1, 2, 3, 4, 5];
  2. try {
  3. arr.forEach((item) => {
  4. console.log('正常循环:', item);
  5. if (item === 2) {
  6. throw item;
  7. }
  8. });
  9. } catch (e) {
  10. console.log('跳出循环:', e);
  11. }

这样就可以顺利跳出循环!!!!!!!!!!!!

24 0.1+0.2 !==0.3是进制问题

因为计算机在存储数字是是通过二进制来存储的,呈现的时候是通过十进制来存储的,所以有误差。

25 栈内存只有984KiB,如果一个字符串超级长,能存的下吗?

字符串数据存于堆内存中,栈内存中只存其地址指针

当我们新建一个字符串时,V8会从内存中查找一下是否已经有存在的一样的字符串,找到的话直接复用。如果找不到的话,则开辟一块新的内存空间来存这个字符串,并把地址赋给变量。

大家有没有想过,为什么字符串不能通过下标索引来进行修改呢?因为字符串的修改本质上只能是通过整个的修改,而不能局部修改。

26 浅谈Set和Map的区别

一、简述

set和map都是es6新增的数据结构。其中set是一个类数组结构,值是唯一的,没有重复的值。map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

二、区别

1. Map是键值对,Set是值的集合,当然键和值可以是任何的值;

2. Map可以通过get方法获取值,而set不能因为它只有值;

3. 都能通过迭代器进行for...of遍历;

4. Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储

5. map和set都是stl中的关联容器,map以键值对的形式存储,key=value组成pair,是一组映射关

系。set只有值,可以认为只有一个数据,并且set中元素不可以重复且自动排序。

三、操作方法

Set

1.add(value):添加某个值,返回 Set 结构本身(可以链式调用)。

2.delete(value):删除某个值,删除成功返回true,否则返回false。

3.has(value):返回一个布尔值,表示该值是否为Set的成员。

4.clear():清除所有成员,没有返回值。

5.size:返回Set实例的成员总数。

Map

1.set(key, val): 向Map中添加新元素

2.get(key): 通过键值查找特定的数值并返回

3.has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false

4.delete(key): 通过键值从Map中移除对应的数据

5.clear(): 将这个Map中的所有元素删除
 

26.1 set里边有两个对象 new Set([{},{}]) 可以去重吗?对象是引用地址,地址不一样

new Set([NaN,NaN]) 可以去重吗? 可以

27  常用的dom操作

28 绑定事件的参数有几个? 删除绑定的事件

1.addEventListener()与removeEventListener()用于处理指定和删除事件处理程序操作;

2.所有的DOM节点中都包含这两种方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最有这个布尔值参数是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序;

3.由于IE只支持事件冒泡,所以同大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器;

最好只在需要在是事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,不建议在事件捕获阶段注册事件处理程序。

29 作用域的理解

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合,作用域决定了代码区块中变量和其他资源的可见性。

一般将作用域分成:全局作用域,函数作用域,块级作用域

全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。

函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

块级作用域

ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。

作用域的理解_徐_三岁的博客-CSDN博客

30变量提升的理解

一、变量提升
变量提升即将变量声明提升到它所在作用域的最开始的部分。

通过var定义(声明)的变量,在定义语句之前就可以访问到;
值:undefined;

值:undefined;

  1.    console.log(a); //undefined
  2.    var a = 1;

因为有变量提升的缘故,上面代码实际的执行顺序为:

  1.  var a;
  2.  console.log(a);
  3.  a = 1;

什么是变量提升和函数提升?_什么是函数提升?_南城旧时的博客-CSDN博客

31 生成随机码

js生成随机数_js随机数代码_前端-文龙刚的博客-CSDN博客

32 继承

1 原型链继承

优点:写法方便简洁,容易理解。

缺点:对象实例共享所有继承的属性和方法。传教子类型实例的时候,不能传递参数,因为这个对象是一次性创建的(没办法定制化)。

2 构造函数继承

 3 组合继承(上边两个的集合)

优点: 解决了原型链继承和借用构造函数继承造成的影响。

缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

4寄生式继承

就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

Js实现继承的6种方式_js继承_雾里有果橙的博客-CSDN博客

33 js和ts的区别

js是一个面向过程的语言,而ts是面对对象的语言。

ts可以对类型做一些限制,比如前台向后台传递参数,可以限定传递参数的类型。这样如果传输的不对,在写代码的过程中就会发现, 不用等待代码运行时候才知道。 可以提升开发的效率吧

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/339815
推荐阅读
相关标签
  

闽ICP备14008679号