前端面试汇总

1、水平垂直居中的方式

第一种,用纯flex布局吧。

直接给对应的父盒子加一个display:flex开启布局。然后使用主轴居中;侧轴居中。(两者都是添加给父盒子的。)

第二种,用flex+margin的方式。

还是同样的先给父盒子添加一个display:flex开启布局,然后给子盒子添加一个margin:auto就可以水平垂直居中了。

第三种,用定位+margin的方式

首先根据子绝父相的方式进行定位,然后子盒子的绝对定位是top:50%,left:50%,但由于这里的50%都是以盒子的左上角为中心点的,所以需要用margin来返回自身宽高的一半,也就是margin-top:负的子盒子高度的一半。margin-left:负的子盒子宽度的一半。

第四种,用定位+transform的位移操作

这种方法可以说是上一种方法的改进,他省略了我们自己计算盒子宽高一半的过程。因为transform的位移,可以使用百分比的方式,并且百分比的对象就是盒子本身。也就是在子绝父相的情况下,直接使用transform:translate(-50%,-50%)即可。

第五种,用定位+margin的方式

在子绝父相的情况下,给子盒子添加上下左右都设置为0,最后再给子盒子添加一个margin:auto,就可以达到水平垂直居中的效果。

像很多人都会说直接采用margin:auto的方式,但是当盒子没有高度的情况下,这样的方式是不可取的。所以基本不会采用。

2、盒模型

盒模型由四个部分组成,分别是内容、内边距、外边距和边框

盒模型的分类有两种,content-box和border-box,可以通过box-sizing来设置。默认值是content-box。

两者的区别主要是对盒子的大小计算方式不同。

  • content-box又被称为w3c标准盒模型。
    • width设置的是内容的宽度
    • 实际宽度=padding+border+width
  • border-box又被称为c3模型/ie盒模型等
    • width设置的是实际的宽度
    • width=内容+padding+border

3、关于flex:1

flex其实是flex-grow、flex-grow:用来增大盒子。当父盒子有剩余空间的时候,可以利用flex-grow来设置盒子增大的比例。比如,有两个子盒子,剩余200,盒子a的flex-grow=1,盒子b的flex-grow=3,那么盒子a在原有的基础上加50,盒子b在原有的基础上加150.

flex-shrink:用来缩小盒子。当总数超过父盒子的时候,可以利用flex-shrink来设置盒子缩小的比例。比如,有两个子盒子,超出200,盒子a的flex-shrink为1,盒子b的flex-shrink为3,那么盒子a在原有的基础上减去50,盒子b在原有的基础上减去150.

flex-basis:用来设置盒子的基础宽度。(如果存在,会覆盖原本的width)直接用px设置大小。

flex:1表示1,1,0% 可扩大可缩小,一般为平均分

flex:auto表示1,1,auto 可扩大可缩小,根据内容的大小来分

flex:0表示0,1,0% 不可扩大,可缩小,最小内容宽度(一般为一个字的宽度)

flex:none表示0,0,auto 不可扩大,不可缩小,一般为内容本身的宽度

auto表示的是容器原本的大小,0%表示为零尺寸的。

4、css3的新特性

css3的新特性,那太多了。我讲几个常用的吧

  • (1)选择器
    • 新增了属性选择器,伪类选择器等等,最常用的应该是
    • :hover鼠标移动到元素上面
    • :nth-child(n)某个元素的第n个子元素
    • :last-child 某个元素的最后一个子元素
    • :first-child 某个元素的第一个子元素
  • (2)新样式
    • 边框方面
    • border-radius:圆角边框,如果设置50%则表示为圆形
    • box-shadow:添加阴影
    • 背景方面
    • background-size图片的缩放,cover表示要铺满整个盒子,contain表示宽高有一个铺满就结束
    • 颜色方面
    • rgba(),hsla()以及opacity的透明度
  • (3)转换transform
    • 主要是位移translate、旋转rotate、缩放scale几个技能
  • (4)过渡动画transition
    • CSS属性,花费时间,运动曲线(默认ease),延迟时间(默认0)
  • (5)自定义动画animation,和transition差不多,最大的区别就是自定义动画不需要触发,定义了就会有动画
  • (6)flex布局

5、BFC

BFC就是块级格式上下文,它是一种属性,可以让渲染区域独立,并且渲染区域中的布局不会影响外界。

BFC的触发情况有很多种,像overflow,float,position,display等等,最常用的应该是overflow:hidden,position:absolute吧。

BFC解决的问题有很多。

情况一:兄弟上下重叠

当上下两个盒子同时拥有上下间距,不是取两个间距之和,而是取最大值。

解决方式就是给盒子添加一个父盒子,并且给父盒子开启BFC。

情况二:外边距塌陷

就是父盒子不存在boder或者是padding的时候,子盒子有一个margin-top,那么父盒子也会对这个指令进行生效。解决方式是给父盒子开启BFC模式,当然,可以给父盒子加一个border或者padding,但是这样容易改变原本的样式,不可取。

情况三:左右布局问题

当A盒子浮动,B盒子不浮动的时候,两者就会产生覆盖。解决的方式就是给非浮动的盒子添加一个BFC模式。一定是给没有浮动的盒子进行添加。

情况四:清除浮动

在父盒子没有设置高度的时候,高度是由内容撑开的,所以子盒子浮动以后,父盒子就会没有高度。只要给父盒子添加BFC布局即可。

6、rem适配原理,响应式布局

rem是相对长度单位,相对于根元素的fomt-size计算值的大小。

1rem=根节点的字体大小

原理:在不同的屏幕下,修改根节点的字体大小。

在项目中:下载一个flexible包,即可动态更改html的字体大小

7、重绘和回流(重排)

重排和重绘是浏览器关键渲染路径上的两个节点, 浏览器的关键渲染路径就是 DOM 和 CSSOM 生成渲染树,然后根据渲染树通过一个布局步骤来确定页面上所有内容的大小和位置,确定布局后,将像素绘制到屏幕上。

其中重排就是当元素的位置发生变动的时候,浏览器重新执行布局这个步骤,来重新确定页面上内容的大小和位置,确定完之后就会进行重新绘制到屏幕上,所以重排一定会导致重绘。

如果元素位置没有发生变动,仅仅只是样式发生变动,这个时候浏览器重新渲染的时候会跳过布局步骤,直接进入绘制步骤,这就是重绘,所以重绘不一定会导致重排。

8、状态码 3

200;请求成功

201:请求成功,但是逻辑有问题

204:请求正常处理,但是没有数据可以返回

304:协商缓存

400:传的参数出现问题

401:一般都是登录过期

404:资源不存在

500:服务器出错

3XX表示重定向,表明浏览器需要执行某些特殊的处理以正确处理请求。

301永久移动,302临时移动

303和302状态码有着相同的功能,但303明确表示客户端应当采用get方法获取资源,这点与302状态码有区别。

9、浏览器的缓存机制

浏览器会将请求后的资源进行存贮为离线资源,当下次需要该资源时,浏览器会根据缓存机制决定直接使用缓存资源还是再次向服务器发送请求。

--作用:

  • 减少了不必要数据的传输、降低服务器的压力
  • 加快了客户端访问速度
  • 增强用户体验

缓存机制分为强缓存和协商缓存

--强缓存:

概念:不向服务端发送请求,强制使用缓存数据

实现方式:后端在响应头中返回 Expires 和 Cache-Control

Expires :http 协议 1.0 的字段,缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点

缺点:浏览器使用 expires 到期时间和本地时间进行对比,如果本地时间被修改或者和服务器时间差距较大,造成不准确的问题

Cache-Control: HTTP 1.1 的字段,约定过期时间的相对时间

!当 cache-control 和 expires 同时存在 cache-control 的优先级会比 expires 高。

--协商缓存:

当强缓存失效后,会使用协商缓存

协商缓存由服务器决定是否使用缓存

  1. 向服务器发送请求资源并携带标识
  • Etag 字段:表示请求资源在服务器的唯一标识,浏览器可以根据 ETag 值缓存数据,下次请求的时候以 If-None-Match 字段请求
  • Last-Modified 字段:用于标记请求资源的最后一次修改时间
  1. 服务器会进行判断浏览器缓存的资源是否真的失效(也就是是否更新)
  • 服务端已经更新,返回 200,重新返回最新资源和缓存标识
  • 浏览器再次存入缓存
  • 后续再次从强缓存开始
  1. 缓存时间到了,但是资源没更新,就还使用本地的,直接返回 304

10、slice,substr,substring

三者都是截取的意思。都可以截取字符串,slice可以截取数组。

第一个参数为开始位置,substr的第二个参数是个数,其他两个第二参数表示结束位置。

第一个参数大于第二个参数时,substring会自动调换顺序,slice会在第一参数大于第二参数时返回空,substr不在乎两者的比较

参数<0,substring无效,slice会给负数加上数据长度,substr第一参数为负数也会加上数据长度,第二参数不可为负数。

11、slice,splice,split

splice

  • 可以进行增删改
  • 修改原数组,返回修改的内容
  • 第一个参数为起始索引,第二个参数是个数,第三个参数开始是添加或修改的值
var arr1 = ['a', 'b', 'c', 'd', 'e', 'f'];
// 删除
arr1.splice(1); //从index为1的位置开始,到最后的全部删除
arr2.splice(-2); //负数可以加上长度
arr3.splice(1, 3); //从index为1的位置开始删除元素,一共删除三个元素
// 增加系列
arr4.splice(1,0,'g','h') //纯增加情况,删除0个,增加2个
//先删除再增加,即替换
arr4.splice(1, 3, 'js', 'vue');//删除+增加 == 更改

slice

  • 进行截取
  • 原数组不变,返回截取的内容
  • 第一个参数为起始索引(包括),第二个参数为截止索引(不包括)
    • start (可选)

如果start为负数,则加上数组长度

如果start被省略,则从索引 0 开始。

如果start超出原数组的索引范围,则会返回空数组。

    • end (可选)

如果 end 被省略,则 slice 会一直提取到原数组末尾。

如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。

如果end小于start,则会返回空数组。

split:字符串 => 数组

    • 字符串的方法,不是数组的方法。
    • 返回一个字符串数组。
    • str.split(分隔符)

12、数据类型,检测数据类型 3

简单数据类型和复杂数据类型。

简单数据类型有number数字,boolen布尔值,string字符串,null,undefined,还有es6新增的symbol唯一值。

复杂数据类型有object对象,array数组,function函数以及特殊对象正则和日期

第一种,typeof,不常用,因为只能检测简单数据类型,函数,其他的对象,数组以及null都会被检测为object。

第二种instance of,用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

写法就是:某个实例对象A instanceof 某个构造函数B。

第三种toString.call(),toString是Object原型对象上的一个方法,必须通过Object.prototype.toString.call来获取。因为从原型链的角度讲,所有的对象最终都会指向object。但大部分的对象,本身也可能有toString的方法,这样就会导致没有查到object的toString方法就会被自身的发放给终止掉。所以需要用call来强制执行,确保执行的是object上的toString方法。

第四种是根据对象的contructor(con s jua k t)判断,检测的是由字面量方式创建出来的数据类型。

13、递增递减

x++ 先返回值后递增

++x 先递增后返回值

undefined++ = NaN

14、作用域

作用域就是所作用的一个范围。作用域和函数的定义有关。而和函数的调用有关的是this的指向

作用有全局作用域和局部作用域两种。

全局作用域是作用于所有代码执行的环境,局部作用域是作用于函数内部的代码环境。

相对应的还有全局变量和局部变量。

全局变量可以在任何一个地方使用,只有在浏览器关闭的时候才会被销毁,比较占内存。

这里有一个特殊就是,如果在函数内部,没有声明直接赋值的变量,也属于全局变量。

局部变量是在函数内部使用的,只有所在函数被执行,才会初始化,函数执行结束,就会被销毁。因此比较节省内存空间。

这里还有一个作用域链

内部函数访问外部函数的变量,采取的就是链式查找的方式

站在目标角度出发,一层一层往外找,并追求就近原则

15、预解析

JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行.

预解析会把变量和函数的声明在代码执行之前执行完成。

预解析有两种,变量预解析和函数预解析

变量预解析,也就是变量的提升,变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升。(只提升声明,不提升赋值,提升的是var的,和let,const无关)

函数预解析,函数的声明会被提升到当前作用域最上面,但是不会调用函数

16、new的过程 3

  1. 内存中创建一个空对象
  2. 将这个空对象的__proto__ 指向了 构造函数的Prototype属性

obj.__proto__=Person.prototype

  1. 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
  2. 调用构造函数,执行构造函数内部的代码(给新对象添加属性和方法)
  3. 构造函数默认返回return,也就是this的实例化对象
  • 如果写了return,那么看return后面的数据类型
  • 如果是简单数据类型,忽略return简单数据类型,return this
  • 如果是复杂数据类型,忽略return this,return复杂数据类型

17、静态成员和实例成员

实例成员就是构造函数通过this添加的成员,只能够通过实例化的对象来访问

静态成员就是在构造函数本身上添加的成员,只能够通过构造函数来访问

18、关于原型链、构造函数

构造函数的prototype和其实例的__proto__是等价的,都指向原型对象。

构造函数就是可以用来new的函数。(箭头函数不能当做构造函数)

也是在new的过程中,将实例.__proto__指向了对应的构造函数的prototype

1、每个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数所拥有的。

(通过构造函数创建的实例,实例再去访问属性的时候,自身没有就会去构造函数的prototype上面寻找。 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。也可以解决构造函数方法浪费内存的问题。)

2、每一个对象(除了null)都会有一个__proto__属性,指向构造函数的prototype。

(之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 的存在。 __proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype。)

3、每个原型对象都有一个constructor 属性,指向构造函数。

(constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。)

 

原型链的底层基础,首先是创建一个构造函数,然后new得到一个实例。构造函数的prototype和其实例的__proto__都指向原型对象。这就形成了一个三角形。并且原型对象的constructor指回构造函数本身。又因为原型对象也是一个对象,也有__proto__属性,这样一层一层往上就形成了原型链。原型链的最上面一层是Object的原型对象。再往上就指向了null。

实例对象在查找属性的时候,如果找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查不到,就去找原型的原型,直至查到顶层的Object.prototype,这也就是原型链的概念。或者也可以说顶层是Object.prototype.__proto__,也就是null。

19、继承有哪几种方式 3

继承和多态、封装共为面向对象的三个基本特征。继承可以使子类具有父类的属性和方法。

第一种原型链继承

关键核心:让父类的实例作为子类的原型。

将子类共有的方法,创建在父类的原型对象上。

将子类共有的属性,创建在父类的构造函数内。

缺点:一、创建子类的时候无法传递参数,就算传递了参数,也无法传递给父类。二、如果属性值是复杂数据类型,那么该属性会被所有的实例共享。

第二种借用构造函数继承

关键核心:在子类构造函数中使用call()调用父类构造函数

让父类的this指向子类的实例,在子类的构造函数中,用call改变this的指向

缺点:只能实现属性的继承,不能继承方法

第三种组合式继承

关键核心:使用 call() 调用父类的属性,使用 new 获取父类原型上的方法

通俗的讲,就是将原型链继承中的方法继承和借用构造函数继承中的属性继承进行组合

缺点:多次调用了父类构造函数。原型链继承方法的时候,会创建一个没有用的父类构造函数,比较浪费。

第四种寄生式组合继承

关键核心:使用 Object.create() 来解决多次调用父类构造函数问题

用Object.create(参数),作用是创建出来一个新对象,他的原型对象是括号内的参数。

子类的prototype=Object.create(父类的prototype),

也就是新对象的原型对象为父类的prototype,而实例的原型对象指向了新对象,也就是实例的原型对象的原型对象为父类的prototype

第五种es6类继承

使用class 父类,并且用extends使子类继承父类。

20、常用的es5数组方法 3

第一种forEach

参数为回调函数,该回调函数有三个参数,分别表示数组的当前项,索引以及该数组本身。

原数组不变,没有返回值。

第二种indexOf

第一个参数是需要查找的值,第二个参数是从第几个索引开始查找。

原数组不变,返回值为查到的索引,如果没有该元素,返回-1

第三种some

参数为回调函数,该回调函数有三个参数,分别表示数组的当前项,索引以及该数组本身。

原数组不变,返回布尔值。

有满足条件的,则返回true,并终止循环。

第四种every

参数为回调函数,该回调函数有三个参数,分别表示数组的当前项,索引以及该数组本身。

原数组不变,返回布尔值。

全部满足条件返回true,有不满足的,返回false并终止循环。

第五种map

参数为回调函数,该回调函数有三个参数,分别表示数组的当前项,索引以及该数组本身。

原数组不变,返回新数组。

新数组长度和原数组一致,新数组由原数组中的每个元素进行return之后得到,没有返回值,会返回undefind

第六种filter

参数为回调函数,该回调函数有三个参数,分别表示数组的当前项,索引以及该数组本身。

原数组不变,返回新数组

新数组长度小于原数组,新数组的数组来自于条件判断为true的原数组数组。

第七种reduce(最复杂的一种)

reduce(callback,[initialValue])

有两个参数,第一参数是回调函数,第二个参数是第一次回调函数的参数1。

第二个参数是可选的,如果没有,那么reduce会从索引1的地方开始执行回调。

回调函数有四个参数,

  • 参数1:上一次返回的值。
  • 参数2:当前项
  • 参数3:当前索引
  • 参数4,:调用reduce的数组本身

原数组不变,返回最后一次return的值

 

口诀:

单纯循坏数组,使用forEach

循环数组得到一个新数组,新数组和循环的数组长度一致循环数组得到一个新数组,新数组少于循环的数组,此时用filter

查找后需要返回true和false的查找后需要返回索引的,21、本地存储

localStorage, sessionStorage, cookie

cookie:

  • 生命周期:默认自己添加,可设置失效时间,关闭浏览器后失效。
  • 存储数据大小:4kB左右
  • 服务器端通信:参与通信,每次自动携带在请求头中,如果使用cookie保存过多数据会带来性能问题
  • 易用性很不友好,获取某个cookie,会获取到整个cookie字符串key:value;key:value’的形式,一般使用第三方库js-cookie进行处理

 

localStorage和sessionStorage除了生命周期,其他都相同

  • 生命周期:localStorage除非手动清除,否则永久存储。

sessionStorage仅在当前会话下有效,关闭页面和浏览器后会被清除。

  • 存储数据大小:一般为5-20MB
  • 服务器端通信:仅在浏览器中保存,不参与和服务器的通信
  • 易用性:还可以,但存储的时候只能存字符串(用到JSON.stringify和JSON.parse进行处理

22、this指向

除了箭头函数,和函数的定义无关,和函数的调用有关。

  • 直接调用,this指向window,严格模式下指向undefined
  • 谁调用指向谁
  • new,this指向实例
  • 箭头函数的this指向上下文

改变this指向的方法

  • fn.call(this指向的,函数需要的实参)
  • fn.apply(this指向的,[函数需要的实参])
  • fn.bind(this指向的),bind不会调用函数,而是根据该函数生成一个新函数,并且新函数的this指向了()的对象

23、异步

js 是单线程的,也就代表 js 只能一件事情一件事情执行,那如果一件事情执行时间太久,后面要执行的就需要等待,需要等前面的事情执行完成,后面的才会执行。

所以为了解决这个问题,js 委托宿主环境(浏览器)帮忙执行耗时的任务,执行完成后,在通知 js 去执行回调函数,而宿主环境帮我们执行的这些耗时任务也就是异步任务

js 本身是无法发起异步的,是委托给宿主环境发起异步的,但是 es5 之后提出了 Promise 可以进行异步操作

--执行流程如下:

  1. 主线程先判断任务类型
    • 如果是同步任务,主线程自己执行
    • 如果是异步任务,交给宿主环境(浏览器)执行
  1. 宿主环境进行异步任务的执行,每个异步执行完后,会将回调放进任务队列,先执行完成的先放进任务队列。
  2. 等主线程任务全部执行完后,会取任务队列中的任务,根据先进先出原则
  3. 在任务队列中取出来的任务,会回到主线程执行,执行完成后,在取下一个,依次重复,这个过程也称为 --而我们所提道的异步任务,也分为宏任务和微任务。

     

    由宿主环境发起的异步被称为宏任务。(setTimeOut、setInterval)

    由js自身发起的异步被称为微任务。(promise)

    (判断由谁发起的,看是否是ecma的,如果是,则表示是js自身发起的。也就被称为微任务。但是我们用到的微任务基本只有promise————还有一个特殊情况MutationObserver,虽然是由web发起的,但是也是微任务)

    promise不是异步的,(.then)才是异步的微任务

    await下面的也可以看作是微任务

    --执行顺序

    1. 先执行宏任务(将整个script标签的代码段看作是一次宏任务)----同步任务
    2. 宏任务执行完后看微任务队列是否有微任务
    3. 没有微任务执行下一个宏任务
    4. 有微任务将所有微任务执行
    5. 执行完微任务,执行下一个宏任务

    解决异步,最常用的就是promise

    24、Generator

    async和await是Generator一个语法糖

    Generator函数的作用是可以将函数执行时进行暂停,而普通函数是一直执行到return,同样也是异步的一种解决方式。

    定义函数时通过 function* 进行区分为 Generator 函数,

    当调用Generator函数后,只是得到一个Generator对象,但并不会执行,需要使用yield和next进行配合来执行。

    函数内部使用 yield 进行暂停并向外传递数据。函数外部使用next进行逐步执行并接收函数内传递出来的数据

    而向内传递数据的话就返回来。由next向内进行传参,而yield进行接收。但是它的第一次传参是无效的。因为第一次的传参,在使用fn的调用的时候就传过去了。

    Generator配合promise可以处理异步问题。可以得到async和await的效果。这里推荐使用一个co库,它可以让Generator函数进行自动执行。

    25、闭包 3

    闭包的概念就是让你可以在一个内层函数中访问到其外层函数的作用域

    闭包的原理就是作用域链。就是利用作用域链的特性,首先在当前作用域访问数据,当前作用域访问不到,则向父级访问,父级也没有,一直找到全局。

    闭包的作用就是数据私有化闭包的缺点是如果使用不当,会造成内存泄漏,因为闭包的数据没有被回收

    解决方案就是将全局指向的函数重新置为 null,利用标记清除的特性。也就是让内层函数置为null,这样没有使用到外层函数的数据,数据就会被回收。

    (而数据的回收就需要说道垃圾回收。)

    闭包的使用场景:

    vue源码中的dep使用

    柯里化函数,高阶函数的使用

    可以使用闭包来模拟私有方法

    26、垃圾回收 3

    垃圾回收,简称GC

    垃圾回收的概念是,js 的内存是自动进行分配和回收的,内存在不使用的时候会被垃圾回收器自动进行回收,那就需要了解垃圾回收的机制,从而防止内存泄漏(内存无法被回收)

    --关于垃圾回收的生命周期

    在声明一个变量、对象或者函数等的时候,都会创建一段内存

    而在适应变量、函数或者对象的时候,会对内存进行读写,也就是会使用内存

    最后再变量、函数、对象等不再需要使用的时候,就会被垃圾回收自动回收掉,进行内存销毁

    所以说,全局的是不会被垃圾回收的。

    --垃圾回收的核心算法就是判断内存是否需要再使用,如果不需要使用,则进行回收。

    那么整个垃圾回收的重点就是如何判断是不是垃圾。这里有多种算法策略。我记得的比较常见的是引用计数和标记清除。

    先说引用计数:ie会使用,就是计算当前内存被引用的次数,被引用一次计数+1,不再引用则-1,当计数为0,则表示该内存不再被需要,就进行垃圾回收,释放内存。

    优点:简单有效。缺点:循坏引用导致内存泄漏。

    标记清除:现在多数浏览器采用的就是这个。它是通过根节点(全局),标记所有从根节点开始的能够访问到的对象。未被标记的对象就是未被全局引用的垃圾对象。这里说的根节点,并不是指window。因为在全局使用的let和const,它是根节点可以访问到的,但是window并不可以。

    27、es6及以上 3

    (1)let和const的变量声明

    es6新增了let声明变量,const用来声明常量,const所申明的值,后期不能修改。

    和var相比,let和const都具有块级作用域,但是不存在变量提升。

    这里还会有一个暂时性死区的概念。只要写了let或者const,就会形成一个块级作用域。这个块级作用域就像一个封闭区域,里面的变量不会再受到外界的影响。所以,块级作用域内,在let和const声明之前使用的这些变量,会产生报错。这样不能使用外界的值,作用域内的值又还没声明的情况,就被称为暂时性死区。

    (2)解构赋值

    (3)模板字符串

    为了方便字符串的拼接,使用反引号``,反引号内部可以通过${}插入表达式,变量甚至是函数调用。

    (4)字符串的几种方法

    includes:用来查找一个字符串中是否包含另一个字符串,返回值为布尔

    stratsWith:用来判断某字符串是否以某字符串开头,返回值为布尔

    endsWith:用来判断某字符串是否以某字符串结尾,返回值为布尔

    (5)箭头函数

    箭头函数就是函数的一种简写形式。使用小括号包裹参数,跟随一个 =>,紧接着是花括号包裹的函数体;

    1. 箭头函数如果只有一个参数,可以省略小括号
    2. 如果只有一行函数体,可以省略花括号,省略后会默认return
    3. 尖头函数中没有绑定this,this指向上下文

    (6)函数的形参部分

    形参默认值:可以给形参直接设置默认值。

    形参解构:函数的形参也可以解构

    形参剩余参数:...语法变量名,返回值是数组,可以将剩余的实参用数组进行获取。

    单独的...是扩展运算符,是展开内容的。

    (7)对象的使用

    对象的简写:当对象内的key和值相同时,可以只写一个可以。

    对象方法的简写:方法名:function(){},可以简写为方法名(){}

    对象内key写法:对象中的key值写成一个表达式,用['字符串']或者[变量名]

    (8)对象的方法

    Object.assign(目标对象,源对象):类似于合并数据。如果值是复杂数据类型,那么指向的还是同一个值。实现的是浅拷贝。

    Object.create(参数):创建一个对象,并将对象的__proto__指向参数。Object.create(null)可以用来创建一个没有原型的对象。

    (9)promise

    (10)模块化

    commonjs模块化规范,导出用module.exprots,导入用require(),最早出来的,一般nodejs里面会使用。

    但现在web更流行的是es6模块化规范浏览器是不识别es6的模块化规范的。

    web过渡时期的产品:

    AMD: AMD加载完模块后,就立马执行该模块 代表(require.js)

    CMD:CMD加载完某个模块后没有立即执行而是等到遇到require语句的时再执行(Sea.js)

     

    运行某个文件:import ‘路径’

    默认导入:import 名字(任意) from ‘路径’

    默认导出:export default 名字

    按需导入:import {名字(必须和导出一致)} from ‘路径’

    import {名字 as 修改的名字} from ‘路径’

    按需导出:export const 名字=值

    同时引入默认和按需:import 名字,{名字} from ‘路径’(必须先默认再按需)

    全部引入:import * as 名字 from ‘路径’

    • 必须要as起别名
    • 返回是一个对象,对象的属性名为导出的名字

    属性值为等号后面的值

    28、深拷贝和浅拷贝

    • 浅拷贝:

    概念:对数据拷贝的时候只拷贝一层,深层次的只拷贝了地址。

    ...和object.assign都属于浅拷贝

    我们会发现通过浅拷贝更深层次的引用类型,如果修改 b.googs,最终 obj.goods 也会跟着修改,是因为在拷贝的时候,我们只是将引用地址拷贝给了 b.goods,也就是说 b.goods 和 ob.goodsj 引用的是同一个对象

    缺点:拷贝复杂数据类型的时候,新数据和旧数据都会指向同一个地址。

    • 深拷贝:

    方法一:JSON 方法实现深拷贝

    我们先将需要拷贝的代码利用 JSON.stringify 转成字符串,

    然后再利用JSON.parse 将字符转转回对象,即完成拷贝

    问题:造成数据丢失和数据异常

    • function、undefined 直接丢失
    • NaN、Infinity 和-Infinity 变成 null
    • RegExpError对象只得到空对象;

    方法二:递归深拷贝

    1. 定义一个方法,返回一个深拷贝的数据
    2. 既然要返回一个数据,我们首先就要定义一个数据,但是数据是对象还是数组?所以需要判断,如果要拷贝的数据是数组,即定义一个数组,如果是一个对象,即定义一个对象
    3. 方法里面怎么拷贝啊?还是一样的利用 for in 循环,在循环内部,需要判断,如果是类型是简单类型,直接拷贝,如果是引用类型,就需要在一次的将引用类型里面的值取出来
    4. 但是递归也会遇到上面同样的问题
      1. 数据丢失和异常处理:处理函数 Symbol 正则 Error 等数据类型正常拷贝
      2. 循环引用问题:数据自己引用自己,此时拷贝就会进入死循环

    解决思路(循环引用问题)

    将每次拷贝的数据进行存储,每次在拷贝之前,先看该数据是否拷贝过,如果拷贝过,直接返回,如果没有拷贝,对该数据进行拷贝并记录该数据以拷贝

    存储数据的方式:

    1. 使用数组
    2. 使用 map 数据:强引用,无法被垃圾回收,key可以是任何形式
    3. 使用 hash 表:弱引用,可被垃圾回收

    真正在开发中,我们一般都是使用一个lodash的包。

    29、数组去重的方法 3

    1. 双重for循环
    2. for循环就数组,indexOf 遍历新数组,判断新数组中有没有for循环的每一项,没有就放入新数组。最后新数组就是去重后的数组。
    3. 用filter加indexOf,用filter代替for来循环旧数组的每一项
    4. 用sort进行排序,然后for循环判断相邻两个是否相等,不相对就放入新数组中
    5. for循环每一项,然后用对象的方式放入,计算个数。
    6. 用set与解构赋值。set数组类型的最大特点就是数据不重复

    30、你能不能自己配置一个webpack搭建一个项目 3

    简单来说webpack就是一个打包工具。但是作用很多:

    • js高级语法转换兼容
    • css兼容/预处理语言处理(less、scss)
    • 代码压缩混淆
    • jsx转换
    • 图片压缩

    项目搭建过程:

    1. 初始化npm init,搭建项目环境,搭建一个基本的项目目录架构。
    2. 安装包webpack和webpack-cil
    3. package.json中添加运行命令‘build’:‘webpack’
    4. 添加默认打包入口src/index.js
    5. 配置打包模式,根目录创建webpack.config.js。添加commonjs模块化规范的导出module.exports={mode:'development'}
    6. 执行打包命令npm run build
    7. 正确操作应该会生成dist/main.js的默认出口文件

    出入口是可以进行修改的。

    31、webpack的打包流程 3

    1. 查找配置文件
    2. 根据配置文件的入口和出口进行打包
    3. 找到对应的入口,根据import文件依赖查找进行打包
    4. 但是webpack默认只能打包低级的js文件,遇到其他类型的文件,需要借助相应的loader进行打包。遇到高级的语法文件,需要借助babel来进行降级处理。
    5. 打包完成后需要生成html文件,用html-webpack-plugin进行处理打包。
    6. 最后输出到出口,将文件进行类型,单独打包,使用plugin插件。
    7. 通过output多出口

    32、bable和plugin、loader

    • webpack默认只能处理js文件,如果想处理其他类型文件则借助各种loader实现,用来处理非js文件。
      1. css-loader:负责webpack打包到css文件时(import './css/index.css'),加载该css文件,并将css文件转换为commonjs对象
      • css开启模块化,开启模块化后,引入的css样式类名会被重新编译,并通过对象中key返回原始类名,value返回编译后的类名,防止样式冲突。开启方式就是给css-loader添加一个modules为true。
      1. style-loader:负责将样式生成style标签插入到DOM中
      2. postcss-loader:可以进一步打包css,处理兼容性,或者将单位统一转为px等
      3. file-loader
    • babel是一个JavaScript编译器,会对js高级语法进行降级处理。配置babel可以在根目录创建 .babelrc文件
    • plugin 插件可以扩展丰富webpack功能
      1. html-webpack-plugin:简化了 HTML 文件的创建,可以根据模版html生成新的html,并自动引入打包的js文件

    babel: 将高级语法转换成浏览器可以识别的语法

    loader: 加载器, 结合 webpack 来处理非 js 资源文件 .css .less .sass .png

    plugin: webpack 的各种各样的插件,能够增强 webpack 的功能

     

    babel工作原理:

    • parse:通过 parser 把源码转成抽象语法树(AST)
    • transform:遍历 AST,调用各种 transform 插件对对抽象语法树(AST)进行变换操作
    • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

    33、computed和watch的区别 3

    计算属性computed:

    • 支持缓存只有依赖数据发生改变,才会重新进行计算
    • 不支持异步,因为有return的存在
    • 定义的时候是方法,使用的是属性

    侦听属性watch:

    • 不支持缓存,数据变,直接会触发相应的操作
    • watch支持异步
    • 监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
    • watch可以监视:props\data\computed\$route
    • 监视的是一个对象: 开启深度监听 deep:true

    34、生命周期 1

    生命周期:是指一个对象从创建到运行到销毁的整个过程,被称为生命周期

    生命周函数:在不同的生命周期阶段会自动执行对应的函数,而这些函数则被成为生命周期函数

    生命周期有四个阶段,常用的八个钩子函数

    1. 阶段1:创建阶段
      1. beforeCreate:开始创建实例,此时实例的数据和方法还没有。
      2. created:实例已经创建完成,数据和方法都已存在

    应用场景:发送请求获取数据,页面一进入需要立即执行

    !如果非要在created中操作dom也可以利用$nextTick

    1. 阶段2:挂载阶段
      1. beforeMount:开始挂载dom,真正的dom元素还没有挂载完成,不可以操作dom
      2. mouted:dom已经挂载完成,可以操作真实dom

    应用场景:页面一进去就需要操作dom元素

    1. 阶段3:更新阶段
      1. beforeUpdate:数据变了,但是视图没有变
      2. updated:数据和视图都变了
    1. 阶段4:销毁阶段
      1. beforeDestory:即将销毁
      2. destoryed:组件销毁

    应用场景:清除挂载在window相关的行为,例如定义器/事件

    父子生命周期

    ==创建挂载阶段

    父beforeCreated > 父created > 父beforeMounted >子TbeforeCreate > 子Tcreated > 子TbeforeMount > 子>mounted > 父mounted

    ==更新阶段

    如果更新的数据不涉及到子组件,只会父组件更新父beforeUpdate>父updated

    如果更新的数据涉及到子组件, 父beforeUpdate >子TbeforeUpdate > 子Tupdated > 父updated

    ==销毁阶段

    父beforeDestory >子 TbeforeDestory > 子Tdestoryed> 父destoryed

    35、vue组件通信(传值)

    (1)父传子

    父组件属性方式传值,子组件用props进行接收

    (2)子传父

    子组件通过$emit传值,父组件通过自定义事件接收

    (3)eventbus:

    本质:就是一个vue实例对象,实现原理是利用发布订阅模式,至于发布订阅模式的话,

    发布订阅模式是一对多的依赖关系,发布方是一,订阅方是多。发布方通过eventBus的$emit发布自定义事件,并传递数据。订阅方通过eventBus的$on订阅自定义事件,并通过回调函数接收数据。

    (4)vuex

    (5)v-model

    (6).sync

    (7)ref获取子组件

    ref加在普通的元素上,可以用this.$refs来获取相应的dom元素

    ref加在子组件上,用this.$refs获取到的是组件实例,可以使用组件上的所有方法,用this.$refs.方法名()进行使用。

    ref必须在dom渲染完成之后才会有

    (8)$children:

    可以获取当前组件的所有子组件,并以数组的格式返回数据

    $children 并不保证顺序,也不是响应式的。

    (9)$parent:

    可以获取到当前组件的父组件, 返回当前组件的父组件

    (10)provide/inject

    provide可以给当前组件所有的后代组件提供数据

    使用方式和data类似,只不过data是给当前组件用的数据,而provide是给后代用的数据

    inject在后代组件上使用,表示接受值,用数组加字符串的形式接收,和props的最初方式相同。

    (11)$attrs

    获取到当前组件节点上的所有属性集合

    父组件在引用的时候传值,子组件不使用props接收,而是在需要的时候用$attrs进行使用

    36、动态路由和静态路由

    静态路由是管理员手动配置的,不便于拓展网络拓扑结构,一但网络拓扑发生改变,静态路由配置量会很大。动态路由是路由器通过网络协议,动态的学习路由,当网络拓扑发生变化的时候,路由器会根据路由协议自动学习新的路由。

    动态路由,因为OSPF,RIP等路由协议都会有周期更新,所以更新量大,占用宽带大。

    使用静态路由的好处是网络安全保密性高。动态路由因为路由器之间频繁交替,需要经常使用路由表,而路由表可以分析出拓扑结构和网络地址等,所以安全性低。

    动态路由:灵活性高

    静态路由:安全,占用宽带小,简单,高效。

    37、路由传参

    • 动态路由传参
      • 路由配置,需要在path上进行参数设置
      • params方法 path: '/xxx/:uid',
      • query方法 path: '/xxx?uid=值',
      • 路由跳转,this.$router.push('地址');
      • 在对应页面中拿到路由参数,this.$route.params.uid / this.$route.query.uid;
    • query方式传参
      • 路由配置不变
      • 路由跳转
      • 用path跳转,this.$router.push(),参数为对象,path:对应路由配置的值,query参数
      • 用name跳转,this.$router.push(),参数为对象,name:对应路由配置的值,query参数
      • 在对应页面中拿到路由参数,this.$route.query.uid;
    • params方式传参,只能通过name跳转
      • 路由配置不变
      • 路由跳转,this.$router.push(),参数为对象,name:对应路由配置的值,params参数
      • 在对应页面中拿到路由参数,this.$route.params.uid;

    页面跳转的方法:

    1、<router-link to="需要跳转到页面的路径">

    2、this.$router.push()跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面

    3、this.$router.replace()跳转到指定的url,但是history中不会添加记录,点击回退到上上个页面

    4、this.$router.go(n)向前或者后跳转n个页面,n可以是正数也可以是负数

    38、路由守卫

    路由守卫分为三种,全局守卫,路由独享守卫以及组件内守卫。

    全局守卫

    • 全局前置守卫
      • 写法router.beforeEach()
      • 每一个路由进入前触发
      • 可以用来进行路由权限的控制
      • 在回调中有三个参数,to,from,next,其中next()必写。
    • 全局后置守卫
      • 写法router.afterEach()
      • 每一个路由进入后触发
      • 用于提示语之类的

    路由独享守卫

    • 写法beforeEnter(),写在路由规则配置里面,和path,name同级
    • 单独进入某个路由前触发
    • 在回调中有三个参数,to,from,next,其中next()必写

    组件内守卫

    • 写在组件内,和created等生命周期同级
    • 在回调中有三个参数,to,from,next
    • 写法有三个
      • beforeRouteEnter
      • 渲染组件前触发
      • 不能获取组件实例 `this`
      • beforeRouteUpdate
      • 在当前路由改变,但是该组件被复用时调用
      • 一般就是是同一个页面,不同的参数这样,此时组件复用,没有被创建,也就无法重新创建。
      • 可以访问组件实例 `this`
      • beforeRouteLeave:路由离开
      • 路由离开时调用
      • 可以访问组件实例 `this`
      • 比如在编辑过程中,要离开给页面,就会跳出没有保存,是否需要离开的提示。

    39、聊聊vuex

    对于vuex的话。

    vuex是一个状态管理的库,是用来实现组件之间的数据共享的。

    是数据的响应式vuex包含了5个属性:

    state,用于定义和存储共享的数据。用$store.state或者是辅助函数mapState来进行触发。

    mutations,用来修改数据,也是修改数据的唯一来源。(当然,其实state数据是可以直接进行赋值修改的,但是并不建议使用,就像是所有人都可以更改公司数据一样,存在着各种不安全隐患。)用commit或者辅助函数mapmutations来进行触发修改数据。

    actions:说道actions,就要提到刚刚说的mutations,mutations的同步的,而actions是异步的。用dispatch或者是辅助函数mapactions来触发。但是actions只能够进行异步处理,不能修改state的数据。所以如果需要修改数据,还是需要调用mutations来处理。

    所以我们的同步流程就是直接触发mutations,而异步流程则是先触发actions,再由actions进行异步处理以后再去触发mutations。

    getters:基于state进行派生数据

    moudles:模块化,将数据模块化后,会有上述的四种属性。模块化后的数据方便维护和管理。每一个模块可以设置命名空间,如果不设置,那么mutations和actions的使用和全局并无差别。如果开启命名空间,则需要通过模块名进行访问。

    vuex的缺点就是不能持久化。我们在使用的时候,如果数据是从后台请求回来的,那么可以直接忽略持久化的问题。如果不是请求回来的,那么就需要解决持久化的问题。

    解决的方式就是本地存储。除了localStorage, sessionStorage, cookie以外,我们还常用一个自动存储的插件

    vuex-persistedstate。

     

    mutations是处理同步的数据的,如果处理异步的话,会导致devtool的记录出现问题,也就无法及时的知道状态是何时更新的,无法追踪状态,给调试带来困难。那我们公司就是不管同步还是异步,都会先进行一个actions的异步处理,再去调用mutations来处理修改数据。

    40、vue.use()的原理

    通过全局方法vue.use()使用插件

    Vue.use会自动阻止多次注册相同插件

    需要在调用new Vue()启动应用之前完成

    Vue. use()至少传入一个参数

    如果参数是一个Object对象,那么这个对象必须提供一个install函数

    如果参数是一个function函数,那么就相当于install函数

    Vue. use()的其他参数,都会依次传给install函数

    install函数默认的第一个参数是vue,后面的参数来自于Vue.use后续参数

    vue.use本质是无法注册全局组件或者给vue原型添加方法的,但是我们在使用路由或者vuex或者element ui,实际还是在install函数内部通过vue.component注册了全局组件或者给Vvue.prototype手动添加方法。

    41、v-if和v-show

    v-show和v-if都是可以用来控制元素的隐藏和显示的。true的时候表示显示,false的时候表示隐藏。

    v-show的隐藏使用的是css样式display:none的方式,不管是true还是false,它都会渲染对应的dom元素。

    优点:不会频繁创建dom

    缺点:首次渲染为false也会创建

    场景:适用于频繁切换的场景。

    v-if的隐藏是直接从dom上移出,不会产生对应的元素。在true的时候,再创建相对应的整个标签。

    优点:他是懒渲染,默认首次如果是false,元素不会创建。如果是组件,利用v-if可以缺点:频繁的删除和重建

    场景:适用于一进入页面,就确定是显示和隐藏,后期不会改变的场景。

    或者组件需要重新触发生命周期的场景

    v-if还有一个v-show没有的高级语法,就是和v-else搭配使用的判断条件的用法。

    42、$route和$router

    $route:用来获取当前路由信息的每个路由都会有一个$route对象,是一个局部的对象

    可以写作$route.path(当前路径)params,name等

     

    $router:全局路由实例,用来操作路由的

    等价于 new VueRouter

    包含所有的路由,路由的跳转方式,钩子函数等等。

    最常用的就是this.$router.push()进行跳转

    43、v-model

    作用:一是数据双向绑定,二是实现组件通信

    原理:v-model就是一个语法糖,动态绑定了value和注册了input事件

    使用场景:

    • 是在表单中使用,比如input输入框之类的,需要双向绑定这个数据,就可以绑定一个v-model,绑定的就是input内输入的值
    • 二是在组件上使用,在组件上使用的情况就是:父组件数据要传给子组件,并且子组件需要修改数据(两件事情都需要的情况下才会使用)

    缺点44、MVVM 的设计思想的优势

    • mvc的改进版
    • 双向绑定技术,当 数据变化时,视图也会自动变化,视图发生更新,数据也跟着同步
    • 我们减少了 dom 的操作,因为我们只需要关注数据就可以
    • mvvm 的设计思想大大提高了代码的耦合性
    • 数据响应式的原理

    45、keep-alive

    keep-alive是一个内置组件,它会缓存不活动的组件实例,而不是将其销毁。不会渲染dom元素

    它提供了include和exclude属性。

    它包含了activated和deactivated钩子函数

    include:指定缓存的组件

    exclude:指定不缓存的组件

    activate:激活状态

    deactivated:失去激活状态

    46、图片懒加载

    图片懒加载的原理:优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。

    一张图片就是一个<img>标签,浏览器是否发起请求图片是根据<img>的src属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给<img>的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。

    47、虚拟dom和diff算法

    虚拟dom的本质就是一个js对象,用来描述真实dom是什么样子的,这个对象就是我们常说的虚拟dom。

    虚拟dom的出现可以进行高效更新,时可以使用虚拟dom进行跨平台。

    我们在初始化渲染的时候,会根据数据和模板生成一个虚拟dom树,当数据发生变化的时候,又会根据新的数据和模板,生成一个新的虚拟dom树。然后将新旧两颗虚拟dom树进行对比。对比的过程使用的就是diff算法。说道diff算法。它的特点是同级比较,深度优先,且采用双指针算法。这里的双指针算法会产生四个指针,新旧虚拟dom树各有两根指针,都是一个指向开始位置,一个指向结束位置。在进行循环的时候,开始位置的指针会在对比完以后向后推,结束位置的指针在对比玩以后会向前推,从而达到高效更新。

    diff对比情况分为三种。

    一是元素不同,会直接删除重建。

    二是元素相同,属性不同,那么元素会进行复用,只会更新属性。

    三是v-for循环的情况。这也会分成两种,

    一是没有key的时候,如果数据的变化没有影响到顺序,那么性能没有影响。如果数据的变化影响到了顺序,那么性能也会受到影响。因为没有key的时候,我们是根据顺序进行对比的。

    二是有key的时候,key是不建议使用索引的,因为索引是会变化的。我们推荐使用唯一值,对比的使用会根据key值进行对比

    48、路由模式

    路由模式分为三种

    1. abstract,支持所有 JavaScript 运行环境,如果发现没有浏览器的 API,路由会自动强制进入这个模式。
    2. hash模式:
      1. 有#/,
      2. 通过window.location.href进行跳转
      3. 通过window.onhashchange进行监听
    1. history模式:(推荐使用)
      1. 没有#/
      2. 通过history.pushStatehistory.repleaceState进行跳转,
      3. 通过onpopState进行监听,只能监听前进和后退,无法监听history.pushState和history.repleaceState, 源码中将history.pushState和history.repleaceState,进行了统一的包装,通过pushState函数进行包装,不管是history.pushState还是history.repleaceState实际底层最终都会通过pushState这个函数进行跳转,通过pushState进行监听
      4. 刷新会404,需要后端的支持。

    49、为什么会出现跨域(同源策略)

    跨域产生的原因就是同源策略的存在。

    同源策略是浏览器提供的一种安全机制,可以防止跨站脚本攻击,

    也就是A网站请求B网站的资源,若是不同源,则不能够请求。

    满足同源的条件是:协议、域名/IP地址、端口号,三者完全一致则表示同源,可以进行资源共享

    有一项不同既不同源,代表是两个网站,此时资源不共享

    跨域的本质是浏览器,在浏览器中才会出现。

    那么为什么会出现跨域呢?

    是因为当下,最流行的就是前后分离项目,也就是前端项目和后端接口并不在一个域名之下,那么前端项目访问后端接口必然存在跨域的现象。

    跨域存在着两种,一种是开发环境的跨域,一种是生产环境的跨域

    生产环境的跨域的话,等我们项目打包上线,会有运维人员处理的。

    我讲一下开发环境的跨域的解决方式吧。

    解决的方法有:JSONP,CORS(后端开启),代理服务器

    如果需要我们自己解决的话,就要用到代理服务器。

    我们在使用浏览器进行请求的时候,不再直接请求服务器的接口,而是向本地服务器进行请求,这就不会出现跨域。再让本地服务器向服务器的接口进行请求,两个服务器之间也不存在跨域问题。再反向进行响应。

    这整个过程就是本地服务器启到一个代理服务器的作用。

    当然也可以使用jsonp,但他只支持get,不支持post。

    50、环境变量

    一个项目在开发的过程中,会经历各种过程才会发布,每个过程的环境不同,就会需要不同的配置参数,所以就可以用环境变量来方便我们管理。

    每个不同的环境有一个不同的文件,这些文件都和src同级

    一个项目,基准地址(环境)会有3套,分别是开发期间的、测试的、线上的

    在package.json中会有相对应的配置,运行不同的命令代表不同的环境,使用的就是不同文件内,环境变量的值。

    环境变量是存放基准地址的,不同的环境(比如测试,开发期间,上线等)的基准地址可能会不同。用环境变量来存储。环境变量的值来自于不同的环境文件.env.的文件中

    51、nextTick

    作用:数据发生变化后,可以利用nextTick获取最新的视图

    原理:数据发生变化会驱动视图,这是一个异步操作。为了性能,等所有数据变化后,进行合并更新视图。因为这个原因,导致数据发生变化后,无法立即获取最新视图。

    解决方案就是使用nextTick

    nextTick不传回调的时候,则内部是promise对象

    nextTick传回调的时候,则内部是setTimeout定时器

    52、token过期问题

    token一般的过期时间是2个小时。这里要引入一个其他内容,refresh_token过期时间较长(一周、两周)

    在token过期的时候,我们会在用户不知情的情况下,偷偷的发送一个请求,获取新的token。通过refresh__token偷偷进行换取,登录状态就可以继续维持一周到两周。如果refresh__token也过期了,那么就会跳转到登录页,需要重新进行登录。

    如何判断token过期

    1、前端主动处理

    当判断token过期时候,不再发送请求,就可以优化性能。减少网络请求的次数

    借助时间戳:

    登录成功的时候存下token的时间戳

    发送请求前,获取当前的时间戳-存下token的时间戳,等到的值如果超过了token的有效期(两个小时),那么就不发送请求,而是跳转到登录页面,要求重新登录。

    2、前端被动处理,由后端主动处理。也就是需要判断请求的时候,返回的状态码,一般401是登录过期,也就是token过期的情况(当然具体是要看后端的返回,这里的401只是一般情况下)

    53、.sync修饰符

    修饰符有哪些:

    v-on的

    • .stop - 阻止事件冒泡
    • .prevent - 阻止默认行为
    • .once - 程序运行期间, 只触发一次事件处理函数(不可以和其他修饰符连用)
    • .native - 在某个组件的根元素上监听一个原生事件

    v-model的

    • .number 以parseFloat转成数字类型
    • .trim 去除首尾空白字符
    • .lazy 在change时触发而非inupt时

     

    作用: 语法糖,也可以实现组件通信, 类似双向绑定(父向子传,子向父改)

    原理: .sync解析出一个动态绑定的数据,解析一个自定义事件,@update:属性名,组件内部可以通过this.$emit('update:属性名的')进行触发

    .sync: 可以使用多次, 而且.sync可以和v-bind结合直接传递一个对象,将对象的每个属性单独传递进去,单独的绑定v-on事件

     

    :属性名.sync=‘变量’

    等价于

    :属性名=‘变量’(父向子传值)

    @update:属性名=‘变量=$event’(@update表示自定义事件)  

    54、你们的项目是如何打包部署的

    1. 运行npm run build 进行打包,可以进行打包优化,打包之后将dist文件交给后端
    2. 首先自测,没问题我们将代码合并到development分支,我们的development分支是受保护的,所以需要进行合并申请,审核通过合并成功,代码合到release分支,测试人员进行自动化测试,从release合到master,从master发布一个tag发布到线上.实际上我们的是cicd.会进行自动打包部署gitlab+docker+Jenkins

    55、webpack打包优化 3

    1. 移除console
    2. soucemap,可以映射,精确的定位到开发代码的哪一行
    3. splitChunks:将公共代码进行提取,设置那些重复使用的代码,用合并方式进行打包
    4. vue-cli3默认开启prefetch,在加载首页的时候,就会提前获取用户可能访问的内容,提前加载其他路由模,所以我们要关闭该功能

    这里要注意:不会影响首屏的加载速度,实际是为了优化子页面,可以快速打开子页面,但是像移动端,用户可能只会访问首页,也预加载其他模块的资源,浪费用户流量。

    1. 打包成gzip,可以进行资源请求的时候速度更快
    2. runtimeChunk:运行时代码,也就是异步加载的代码,比如路由懒加载的都属于运行时代码。

    没有开启runtimeChunk,运行时代码或者代码没有发生变化,重新打包时,主模块也会重新打包hash会发生变化。会导致项目部署后,强缓存失效。

    开启runtimeChunk,会将运行时代码的信息单独存放在runTime中,主模块就不会被影响,也就不会重新打包,可以继续使用本地缓存。(但要配合7使用)

    1. script-ext-html-webpack-plugin:用这个插件可以将runTime代码生成到行内
    2. 通过image-webpack-loader进行图片的打包压缩
    3. 开启路由懒加载 将每个路由进行单独打包
    4. 排除打包,用externals将比较大的包排除掉,然后引入响应的CDN资源

    56、首屏优化 3

    1. soucemap: 关闭
    2. 路由懒加载
    3. cdn资源
    4. splitChunks: 提取公共资源
    5. 图片压缩
    6. gzip
    7. runtimeChunks
    8. ssr: 服务端渲染
    • 解决首屏加载速度慢的问题,因为首屏服务端直接返回,不需要加载js文件
    • 还可以解决seo,html不再是只有一个id为app的标签,更加有利于seo搜索
    • 实现ssr的方法是使用vue结合nuxt

    spa单页面应用

    我们vue是使用spa的,spa页面响应速度快,可以减轻服务器压力,但是不利于seo,首屏加载也会比较慢。

    这时候就会用到ssr。也就是在vue中使用nuxt框架。虽然可以让爬虫更容易爬到数据,响应速度也更快,但是会增加服务器的压力,并且开发的难度也比较大。

    57、技术栈(了解一下)

    1. vue(全家桶 vue、vuex、vueRouter、axios、elementUi、)echarts-cos-js-sdk-v5dayjs、js-cookievuex-persistedstate
    2. xlsxfile-saver excel导入导出

    58、封装创建组件

    (1)组件封装思想

    1. 组件的结构:结构考虑复用灵活,一般使用插槽、允许自定义
    2. 组件的样式:考虑支持自定义,一般使用属性传值(通过样式或者类名)
    3. 组件的数据:通过数据传递
    4. 暴露事件:例如弹框组件,点击遮罩弹框关闭,用户使用组件的时候,也需要监听到点击遮罩的行为,用户可以进行自定义的逻辑

    (2)如何创建一个全局组件

    通过 Vue.component 来创建一个全局组件,第一个参数是组件名字,第二个参数是组件的配置对象,可以通过 template 配置组件的结构,data 定义数据等等

    (3)如何创建一个局部组件

    在组件内部通过 components 来创建一个局部组件

    全局组件和局部组件的区别

    局部组件:只能在当前的父组件中使用

    全局组件: 在任意地方使用

    (4)如何定义局部自定义指令

    在组件内部通过 directives 来创建一个局部指令

    全局指令和局部指令的区别

    局部指令:只能在当前的组件中使用

    全局指令: 在任意地方使用

    (5)如何定义局部过滤器

    在组件内部通过 filters 来创建一个局部过滤器

    全局过滤器和局部过滤器的区别

    局部过滤器:只能在当前的组件中使用

    全局过滤器: 在任意地方使用

    59、出现bug怎么解决

    线上出现bug,首先要去评估bug的影响范围。

    首先看是在新版本上的还是旧版本上

    新版本上,回退到上一个版本。然后去fixBug进行修复新版本上产生的bug。

    旧版本上,说明这个bug很长时间才出现,说明不是很严重,可以定位bug的位置进行修复,完成后将bug修复的代码发布到线上。也可以等下个版本上线的时候,进行覆盖。

    总的来说,就是要将影响范围缩到最小为主要思想,来使用不同的方式进行bug的解决。

    60、工作流程

    首先我们组长去拿到项目需求,然后他会开会召集我们去进行开发时间评估,然后ui会根据需求出设计稿,前后端会沟通API接口,沟通完以后我们开始静态页面的开发,后端也开始API接口的开发,后面我们静态页面开发完了,如果后端接口写好了我们直接调用渲染页面,后端还没完成的话我们可以先用mock模拟数据进行测试。后边如果发现啥问题的话再跟后端协商处理(联调),我们自己写的时候是会边写边测试的,基本完成感觉没啥问题了就会交给测试人员测试,然后就开始测bug,测试人员会把bug发布到协作平台上(禅道、ones),然后我们去解决bug。 等到项目可以上线的时候就由运维人员发布上线

    61、如何和后端进行联调

    概念:在我们开发的过程中,发送请求的ajax数据都不是后端返回的真数据,而是我们自己通过接口mock模拟的假数据,当前端的代码编写完成后,后端的接口也写好后,我们就需要把mock数据换点,尝试使用后端提供的数据,进行一个前后端的调试,我们会把这个过程叫做前后端接口联调。

    我们需要测试后端的数据和我们所使用的mock数据是否适配,格式是否正确。看所返回的数据是否够用。比如你想实现分页或者列表功能,可是后端就只写了两条数据, 这些问题就会在联调的过程中进行协商解决。

    我们公司开发是前后端分离,部署时是一个域名和一台服务器。

    62、link与@import的区别是什么

    1、从属关系区别

    @import是 CSS 提供的语法规则,只有导入样式表的作用;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。

    2、加载顺序区别

    加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。

    3、兼容性区别

    @import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。

    4、DOM可控性区别

    可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。

    63、http和https

    http超文本传输协议,用于在浏览器和服务器之间传递信息。不适合传输敏感信息。

    https安全套接字超文本传输协议,在http的基础上加上了SSL协议。

    http的默认端口是80,https是443

    http是传输过程是明文的,https是加密传输的

    http是无状态连接,https由ssl+http构成的

    http是基于7层协议中的应用层,https是基于传输层的

    https使用非对称加密进行密钥传输,使用对称加密进行数据传输

    对称加密(数据传输)

    发送方和接收方使用同一个密钥(一串字符串)进行加密和解密

    1. 服务端使用密钥进行加密
    2. 客户端使用密钥进行解密

    但是第一次要传输一次密钥,如果密钥被拦截,就被破解了

    性能好,速度快,缺点,密钥被拦截被破解

    非对称加密(密钥传输)

    一个公钥,一个私钥

    1. 服务端使用私钥解密
    2. 客户端使用公钥加密

    优点:安全

    缺点:性能差,耗时间

    64、双向数据绑定? 2

    数据发生变化,同步视图,视图发生变化,同步数据。

    v-model可以完成数据双向绑定。

    但原理是v-on绑定事件和v-bind绑定数据,是一个语法糖。

    v-bind可以实现数据变同步视图,这是因为数据响应式的原理。

    v-on绑定事件可以实现视图变同步数据,这是数据响应式的原理。

    65、是单向数据流? 1

    在父向子传值的时候,如果改变父组件的值,子组件会跟着同步更新,反之不允许

    66、事件传参 2

    • 事件函数没有括号,表示没有传递参数,则默认的第一个参数是事件对象。
    • 事件函数有括号,就算为空,也表示传递参数。则形参和实参一一对应。如果需要事件对象,实参用$event进行传递。

     

     

    67、自定义指令:directive

    当vue 提供的系统指令满足不了我们的需求时,我们就需要自定义指令

    全局通过 Vue.directive 进行自定义指令的定义。

    局部通过directives进行定义。key为自定义指令的名字,value为对象,对象内有下面三个钩子函数。

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

    68、vue 的两个核心

    组件系统、数据驱动

    69、组件插槽

    • 默认插槽:
      • 在组件标签中间可以传递一些子节点
      • 组件内部利用 slot 标签进行接收
    • 具名插槽(可以传多个插槽)
      • 在组件标签中间通过定义 slot 的名字传递子节点
    <my-banner>
      <div slot="header">
        头部
      </div>
      <div slot="footer">
        底部
      </div>
    </my-banner>
      • 组件内部利用 slot 的 name 进行对应接收
    <template id="banner">
      <div>
        <slot name="header"></slot>
        <slot name="footer"></slot>
      </div>
    </template>
    • 作用域插槽
      • 在组件内部定义数据,将数据传递给插槽的结构
      • 通过给 slot 动态绑定属性
    <template id="my-li">
        <ul>
          <li v-for="item in arr">
            <slot :row="item"></slot>
          </li>
        </ul>
      </template>
    
      • 插槽内部:通过 slot-scope=“scope”来接收
    <my-li>
      <template slot-scope="scope">
        <p>{{scope.row}}</p>
      </template>
    </my-li>
    <my-li>
      <template slot-scope="scope">
        <a href="04-侦听器.html">{{scope.row}}</a>
      </template>
    </my-li>

    70、vue 单页面应用的优缺点

    缺点:

    • 不利于 seo
    • 兼容到 ie9
    • 初次加载耗时相对增多

    优点

    • 用户体验好,不用重新刷新整个页面
    • 前后端分离
    • mvvm 设计模式
    • 减轻服务期压力,只需要服务器提供数据

    71、v-if和v-for为什么避免同时使用

    v2中:

    v-for的优先级高于v-if,所以还是会先循环创建虚拟dom,再利用v-if进行移除

    解决方式:

    • v-if写到外层
    • 先通过计算属性将数据计算好

    v3中: v-if优先级高

    72、mock假数据

    现在的项目都是前后端分离的,在前后端同时开发的过程中,后端接口数据没有出来,前端可以使用mock假数据。

    优点:

    团队可以并行工作。更好的进行前后端分离。

    增加测试的真实性,通过随机数据,模拟各种场景。

    开发无侵入,不需要修改既有代码,就可以拦截ajax请求,返回模拟的响应数据。

    数据类型丰富,支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。

    方便扩展,支持扩展更多数据类型,支持自定义函数和正则。

    不涉及跨域问题

    73、mixins

    mixins: 将组件中的逻辑功能进行复用,复用部分可以提取到一个js文件中,然后通过mixins这个选项将该文件中暴漏的对象进行混入即可

    可以混入哪些: 正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中

    优先级:

    • 生命周期:组件和混入的都会调用(混入的先调用)
    • data数据::进行合并,发生冲突以组件为主,mixins中的会被覆盖
    • methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对

    74、axios取消重复请求

    1. 场景:如果在输入框中输入12,然后先发1的请求,再发12的请求,但是如果关键词1的请求响应慢,12请求的数据先回来,那么1请求回来的数据会覆盖12的数据。和我们所需要的结果产生了出入。
    2. 解决方案:使用取消请求的方式。
    3. 原生的ajax可以使用abort()
    4. 取消axios请求,则使用axios内部的cancelToken方法

    75、浏览器的进程和线程

    当我们启动某个程序时,操作系统会给该程序创建一块内存(当程序关闭时,该内存空间就会被回收),用来存放代码、运行中的数据和一个执行任务的主线程,这样的一个运行环境就叫进程

    线程是依附于进程的,在进程中使用多线程并行处理能提升运算效率,进程将任务分成很多细小的任务,再创建多个线程,在里面并行分别执行。

    • 进程与进程之间完全分离,互不影响。
    • 进程与进程之间传递数据使用进程通信管道IPC
    • 一个进程中可以并发多个线程,每个线程中可以并发执行不同的任务
    • 一个线程出错会导致所在的整个进程奔溃
    • 同一个进程内的线程之间可以相互通信和共享数据
    • 进程关闭,会被操作系统回收至内存空间。

     

    一个浏览器会包含多个进程,一般为:

    1. 浏览器主进程: 负责控制浏览器除标签页外的界面,同时提供存储功能
    2. GPU进程:负责整个浏览器界面的渲染。
    3. 网络进程:负责发起和接受网络请求。
    4. 插件进程:主要是负责插件的运行。
    5. 渲染进程:负责控制显示tab标签页内的所有内容。

    我们在浏览器中的工作,多数都是通过渲染进程完成的。

    渲染进程中的线程:

    1. GUI渲染线程:负责渲染页面
    2. JS引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。和GUI不能同时运行。
    3. 计时器线程:负责计时器工作
    4. 异步请求线程: 处理异步的
    5. 事件触发线程:主要用来控制事件循环eventLoop

    76、三次握手和四次挥手 1

    三次握手

    • 客户端向服务端发送信息,“我发信息,你收得到吗”
    • 服务端向客户端发送信息,“收到了,我发信息你收得到吗”
    • 客户端向服务端发送信息,“收到了,我们连接成功了”

    四次挥手

    • 客户端向服务端发送信息,“我说完了”
    • 服务端向客户端发送信息,“好的,我知道了,我看看我还有没有要说的”
    • 服务端向客户端发送信息,“我也说完了”
    • 客户端向服务端发送信息,“好的,我知道了” 2MSL之后挂断

    77、浏览器输入url,敲下回车后发生的事情 1

    • URL解析
      • 首先判断你输入的是一个合法的 URL 还是一个待搜索的关键词,并且根据你输入的内容进行解析。如果是一个关键字,会使用浏览器默认的搜索引擎去添加关键字。
    • DNS查询
      • 通过域名解析得到IP地址(先进行本地解析,没有再通过DNS解析)
    • 三次握手
      • 得到服务器的IP地址后,进行tcp连接
    • 发送http请求
      • tcp连接成功后,浏览器发送http请求到目标服务器(数据请求)
    • 响应请求
      • 返回http响应消息(返回数据资源)
    • 四次挥手(http1.0的时候)
      • 数据请求完成以后,为避免资源占用和损耗,关闭连接。
    • 页面渲染
      • 对返回的资源进行解析渲染
    • 四次挥手(http1.1的时候,会判断有没有keep-alive,有,就会在关闭网页的时候进行断开。没有keep-alive,请求结束就直接断开连接。)

    78、防抖和节流

    防抖和节流都是为了避免函数被多次调用导致页面调用,但它们的本质不一样,防抖是将多次执行变为最后一次执行,节流是将多次执行变为每个一段时间执行一次。

     

    防抖是指触发事件函数后,函数在n秒后只能执行依稀,如果n秒内再次触发,会重新计算时间。也就说说连续触发,只执行最后一次。

    场景:

    • 搜索框搜索输入,只需要用户最后一次输入完成,再发送请求。

    节流会限制一个函数在n秒内只能执行一次,过了n秒又可以执行一次。

    场景:

    • 发送验证码的时候,60秒内只能发送一次。

    79、假值有哪些

    假值就是值boolean转出来是false的

    1. 0
    2. Null
    3. NaN
    4. False
    5. undefined
    6. 空字符串

    80、get和post的区别

    • 从标准上来说:

    GET 用于获取信息,是无副作用的,是幂等的,且可缓存 ,安全性差

    POST 用于修改服务器上的数据,有副作用,非幂等,不可缓存

    • 从请求报文上来说:

    GET 和 POST 只是 HTTP 协议中两种请求方式(异曲同工),而 HTTP 协议是基于 TCP/IP 的应用层协议,无论 GET 还是 POST,用的都是同一个传输层协议,所以在传输上,没有区别。

    在报文格式上,不带参数时,基本一致,带参数时,在约定中,GET 方法的参数应该放在 url 中,POST 方法参数应该放在 body 中。

    81、响应拦截器里面都做什么事情

    • 请求拦截器
      在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;
    • 响应拦截器
      同理,响应拦截器也是如此功能,只是在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常来判断登录失效等。

    82、实现实时更新

    websocket

    比如直播间弹幕啊,股票的实时数据更新,进入页面客服的自动发送信息等等

    websocket是一种数据通信协议,常见的是http协议。

    http协议的缺陷:通信只能由客户端发起,http基于请求响应实现。

    83、vue 组件中的 data 为什么是一个函数,返回一个对象?

    如果不是一个函数返回一个新的对象,组件如果多次使用,实际公用的是同一个数据

    但是如果是通过函数 返回一个新的对象,这样的话,每个组件的使用数据是独立的

    83、babel的使用过程

    babel是一个降级语法的工具

    首先需要下载安装这个包

    在根目录比如babel。config。js中babelrc进行配置

    在plugins中配置babel的插件

    84、浅谈事件冒泡和事件捕获

    事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

    阻止事件冒泡e.stopPropagation()

    简单来说,当你鼠标在浏览器上点击了一下。

    1. 浏览器捕获到了click事件。
    2. 然后浏览器根据你点击的事件,从window开始向下,就会触发每个父祖element捕获模式的事件回调。
    3. 直到找到点击所在的最终(最小的element)
    4. 然后浏览器开始继续又向上冒泡其父祖element的click事件,直至window。
    5. 默认的事件都是冒泡模式下触发的。

    85、二维转一维数组,扁平化

    1、flat

    flat是ES10新增的一个数组处理的方法,非常的好用,它专门用来扁平化数组。

    合并返回新数组

    参数是层级数,默认为1,层级未知可以用Infinity

    2、concat + 扩展符

    concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

    扩展运算符(...)可以将数组转为用逗号分隔的参数序列。

    只能二维转一维,多维就不行

    3、reduce + concat

    reduce() 方法接收一个函数作为累加器,reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(上一次回调的返回值),当前元素值,当前索引,原数组 。

    callback:函数中包含四个参数

    - previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))

    - currentValue (数组中当前被处理的元素)

    - index (当前元素在数组中的索引)

    - array (调用的数组)

    initialValue (作为第一次调用 callback 的第一个参数。)

    将初始值设置为了[]空数组,然后将需要扁平化的数组的每一项都用concat重新连接,最终得到一个一维数组。

    4、toString + split

    先使用 toString 把数组转成字符串,再使用 split 把字符串转回数组:

    该方法存在局限性,不适用于一些包含相对特殊子元素的数组,比如包含 null、undefined、对象类型等。

    使用map是为了让数组元素变为Number类型。

    86、前端性能优化方式

    • 缩小html、css和js
      • 使用到插件Gulp和JSMin

    87、单点登录

    概念: 一个大型公司有很多系统,用的是同一个账号,登录一个系统时,其它系统也可以正常访问

    cookie:

    某个系统登陆成功,再次去登录其它系统系带token,token如何在多个网站中共享

    domain/path

    domain: 设置网站域名 设置为主域名(父级域名)/二级域名是可以获取到cookie数据

    path: 路径 /

    脱离父级域名不可以共享了

    认证中心

    iframe

    88、webpack+browserify+gulp+grunt四个工具的区别

    • gulp和grunt是前端自动化构建的工具,帮助用户完成js\css压缩、less编译等(只不过现在webpack也可以完成压缩等任务,可以替代gulp的这部分功能)。
    • webpack和browserify是前端模块化方案,与seajs和requirejs是一个东西,只不过seajs和requirejs是在线编译方案,引入一个CMD\AMD编译器,让浏览器能认识export、module、define等,而webpack和browserify是预编译方案,提前将es6模块、AMD、CMD模块编译成浏览器认识的js。
    • 他们之间的区别见以上两点,只不过相互之间也会有一些相似的功能。
    • grunt配置复杂繁重,是基于文件流的操作,比较慢;gulp是基于内存流的操作,配置轻量级,代码组织简单易懂,异步任务。
    • webpack的话,就是配置复杂,文档杂乱,插件繁多,难于上手。

     

    转载自https://www.yuque.com/yuqueyonghuzeka30/evgeli/yp5ppv?#CEaup

posted @ 2023-05-29 10:30  strugglezlbstruggle  阅读(7)  评论(0编辑  收藏  举报