《前端工程师面试大揭秘:高频问题一网打尽》

目录

前言

一、HTML + CSS

1. 让一个宽高不定的子盒子在一个宽高不定的父盒子中垂直水平居中有哪些方法?

2. rem布局的原理是怎样的?在不同的手机端的表现是什么?你在实际开发中是如何做的?

3. 为什么要清除浮动?清除浮动的方法有哪些?

4. h5c3新特性有哪些?

5. 说说css的权重

6. src和href之间有什么区别?

7. 说说css3盒模型 box-sizing

8. px,rem和em的区别

9 .CSS样式的引入方式

10. flex和gird布局的区别?

11. 说说你解决过什么浏览器兼容性的问题,你是怎样解决的?

12. 外边距重叠怎么解决?

13. 什么是BFC?BFC的布局规则?哪些方式会创建BFC?

14. 屏幕分辨率不同,前端如何做适配?媒体查询具体怎么做?

二、JS + ajax

1. ES6的新特性有哪些?let、var、const的区别?

2. 普通函数和箭头函数内this指向有什么不同?如何改变this的指向?func.call func.apply 和 func.bind有什么异同?

3. 如何判断this的指向?

4. async、await 是什么?它有哪些作用?

5. 常用的数组方法有哪些?哪些会改变数组哪些不会。

6. js数组循环常用的几种方法?(重点记map、foreach、filter、map的区别)

7. 常用的字符串方法有哪些?

8. js常用的数据类型有哪些?哪些是简单的数据类型,哪些是复杂的数据类型?

9. 简单数据类型和复杂数据类型有什么区别?

10. ES6中新增了哪些数据类型? Symbol Set Map

11. 深浅拷贝

11. 如何实现对象的浅拷贝?

12、什么是原型,说说原型的作用,说说原型三角关系?什么是原型链,原型链有什么作用?

13. 什么是闭包?有什么作用?能否手写一个闭包的示例代码?

14. 谈谈你对http协议的理解?说说常见的http状态码有哪些,各有什么含义?

15. HTTP的请求方式有哪些? get和post有什么区别?

16. axios的底层是用什么实现的?

17. 什么是跨域? 跨域限制了什么?说说你知道有哪些解决跨域的办法?进一步说说他们的原理是怎样的?

18 说说axios中请求拦截器,响应拦截器各自的执行时机是怎样的?有什么作用?说说对token机制的理解?如何解决token可能过期的问题?

19. 说说你对Promise的理解?Promise提供了哪些API,列举下,分别说说其作用。Promise.all和Promise.race有什么区别?

20 防抖和节流:说说你对防抖和节流机制的认识,说说它们作用和应用场景有何不同,能手写出其核心代码吗?

21 JS代码的执行机制

22 手写JS数组去重

23 数据类型的判断有哪些方法?他们的优缺点及区别是什么?

24. cookie、localStorage和sessionStorage

25. 页面上的script标签内的代码是同步执行的还是异步执行的?

26. script 标签中 defer 和 async 的区别?

27. 说说js的事件执行机制?事件委托的原理是?

28. for in和for of 的区别?

三、Vue2

1. vue的生命周期钩子函数有哪些?各在什么时机执行?mounted和created有什么区别?销毁的钩子函数一般用来做什么处理?

2. VUE双向绑定(响应式)的原理是怎样的?

3. 什么是mvvm模型?

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

5. 组件中的data为什么要定义成一个函数,而不是写成对象的形式?

6. 组件之间的传值方式有哪些?

7. computed、watch和methods有何区别?

8 vuex是什么?有哪五个重要的属性?怎么使用?哪种功能场景使用它?

9 vue中 keep-alive 组件有什么作用?

10 vue组件的scoped属性的作用

11. vue-router如何响应 路由参数 的变化?

12. vue中子组件调用父组件的方法

13 虚拟DOM和DIFF算法

14 路由守卫/路由钩子函数

15. vue中有时会出现页面闪现的问题,如何解决?

16. 路由模式hash和history的区别

17.路由跳转方式有哪些?

18. Vue路由传参的方式?

19. 常用的插槽和使用方法

20. 父子组件钩子函数的执行顺序?

21、如何将获取data中某一个数据的初始状态?

22. 为什么有时候修改了data中的数据,视图中没有发生变化

23. $nextTick有什么用?$nextTick的原理是怎样的?

24. render函数(或h函数)怎样使用?

25. 数据很多有上万项,需要渲染怎么办?

 四、Vue3+Ts

1. vue3和vue2有什么区别?

2. vue3中的生命周期钩子函数相比vue2发生了什么变化?

3. vue3中的响应式和vue2中的响应式有什么不同?为什么要这样做?

4. TypeScript中有哪些数据类型?

  五、项目加其他

1. webpack是什么?具体有哪些作用?loader和plugin分别是干嘛的?列举几个你用到的plugin? webpack的工作流程是怎样的?

2. 谈谈你平时都用了哪些方法进行性能优化?

3 安全方面的CSRF攻击 和 XSS攻击(了解)

4. 这个人资/商城/...管理系统中权限是怎样做的?

5. 这个人资/商城/...管理系统中上传图片的组件是怎样封装的?

6. 项目中解决了从详情返回列表页的时候停留在原点的问题,说说你是怎样解决的?

7. 从浏览器地址栏输入 url 按回车后发生了什么

8 浏览器的渲染机制是怎样的?

9 什么是重排?什么是重绘?什么情况下会导致重排?如何减少重排重绘?

10. 说说你对浏览器缓存的认识?

11. 如何判断数据类型?

12. 在面试中有一个经常被问的问题就是:如何判断变量是否为数组?

13. HTTP和HTTPS的区别

14. 说说小程序微信登录的流程?

15. 说说小程序微信支付的流程是怎样的?

16. 发请求想中断怎么办?

17. 数字精度怎么处理?

18. 说说css的权重

19. 做过哪些性能优化,是怎样处理的?

20. 列表项有上万项,怎么办?

21. mixins有没有了解,在你做的项目中如何应用的?

22. null和undfined有什么区别?

23. $set有什么用?

24. 外边距重叠怎么解决?

25. $attrs 的作用

26. 为什么vue组件样式不会重叠覆盖呢?scoped的原理

27. less和sass的区别

28.小程序的生命周期?

29. 侧边栏收缩时,Echarts图表如何自适应?

30. 贵公司使用git的工作流程是怎样的?

31. 遇到冲突如何解决

32. 你使用uni-app开发过app吗?如何进行uni-app兼容多端的开发

33. npm和npx有什么区别

34. vue和react的区别时什么

35. 如何封装一个把扁形数组转成树型数组的函数?(亮点、代码性能优化)

总结


前言

当准备面试前端开发岗位时,熟悉一些常见的前端面试题是至关重要的。这些问题涵盖了各种前端开发技术和概念,帮助面试官评估你的技能水平和专业知识。通过准备和回答这些问题,你可以展现自己的学习能力、解决问题的能力和对前端开发的热情。在接下来的面试中,希望你能够表现出色,展示自己的实力,取得成功!


提示:以下面试题如有错误,仅供参考!

一、HTML + CSS

1. 让一个宽高不定的子盒子在一个宽高不定的父盒子中垂直水平居中有哪些方法?

答案:

  1. 主要有3种方式:定位、flex布局和grid栅格布局

  2. 定位:

    1. 采用子绝父相定位方式(父亲也可以是绝对定位)。

    2. 通过边偏移top: 50%; 和left: 50%; 让子盒子往下和往右移动父盒子高度和宽度的一半。

    3. 再通过位移transform: translate(-50%, -50%);, 让子盒子往上和往左移动自身高度和宽度的一半。

    body,html{
        height: 100%;
        width: 100%;
    }
    ​
    父盒子 {
        position: relative;
        
    }
    子盒子 {
        height: 100px
        width: 100px;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
  3. flex布局:

    1. 通过display: flex;把父盒子设置为flex容器

    2. 给父盒子添加 justify-content: center; 让子元素沿主轴居中

    3. 给父盒子添加 align-items: center; 让子元素沿侧轴居中

      父盒子 {
          display: flex;
          justify-content: center;
          align-items: center;
      }
  4. grid布局

    1. 给父盒子添加display: grid; 采用栅格布局

    2. 给父盒子添加 justify-items: center; 让子元素沿主轴居中

    3. 给父盒子添加 align-items: center; 让子元素沿侧轴居中

    .parent {
        display: grid;
        justify-items: center;  /* 主轴居中 */
        align-items: center;  /* 侧轴居中 */
    }

拓展问题:

  1. 面试官可能接下来会问,flex布局有什么缺陷?

    答:兼容性不太好,不能兼容ie9及以下版本的ie浏览器, 但是在移动端可以放心使用

  2. flex布局中justify-content样式的两个属性space-between和space-around有什么区别?

    • space-between:两端对齐,项目之间的间隔都相等。

    • space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。

  3. flex布局怎样使用?flex:1代表了什么?

    1. flex布局怎样使用:

      背黑色加粗部分即可

      1. 布局的传统解决方案,基于盒状模型,依赖 display属性 +position属性 + float属性。对于垂直居中之类的特殊布局非常不方便

      2. 2009年,W3C 提出了一种新的方案-Flex 布局可以简便、完整、响应式地实现各种页面布局。flex布局中只需要给盒子添加display: flex; 就可以把盒子设置为flex容器,而他的直接子元素就成了flex项目,然后通过给flex容器和flex项目添加flex系列样式进行布局。

      3. 一个注意点是,使用 Flex 布局以后,子元素的floatclearvertical-align属性将失效。

    2. flex:1代表了什么?

      参考:flex - CSS:层叠样式表 | MDN

      1. flex属性是此属性是flex-grow + flex-shrink + flex-basis 的简写形式,flex:1的完整写法是: flex: 1 1 0;,等同于写了以下三j个样式。

      2. flex-grow: 1, 它指定了flex容器中剩余空间的多少应该分配给项目(flex增长系数)。

        1. flex-shrink: 1 , flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值划分的。

          如何理解:flex-shrink - CSS:层叠样式表 | MDN

          如何理解,举例说明:
              1. 如果父盒子宽度为 500px;   
              2. 父盒子中一共有5个子盒子
                  1)每个子盒子的宽度为 120px
                  2)前3个子盒子的 flex-shrink:1
                  3)后2个子盒子的 flex-shrink:2
              3. 则实际每个盒子的宽度是如下计算的
                  1) 子元素的宽度之和 = 120px * 5 = 600px
                  2) 子元素超过父盒子的宽度  = 600px - 500px = 100px
                     就是说需要压缩的空间为 100px
                  3) flex-shrink一共要压缩的份数 = 3 * 1 + 2 * 2 = 7
                  4)每份要压缩的大小 = 100px / 7 约等于 14px
                  5) 前3个子盒子每个盒子要压缩的大小 = 14px * 1 = 14px
                     因此前3个子盒子的最终大小 = 120px - 14px = 106px
                  6) 后2个子盒子要压缩的大小 = 14px * 2 = 28px
                     因此后2个子盒子的最终大小 = 120px - 28px = 92px
        2. flex-basis: 0, flex-basis 指定了 flex 元素在主轴方向上的初始大小。

2. rem布局的原理是怎样的?在不同的手机端的表现是什么?你在实际开发中是如何做的?

答案:

  1. rem布局的原理是怎样的?

    1. rem是一个相对单位,1rem=根元素html的font-size的大小

    2. 只要让页面上的元素采用rem作为单位,只要改变html根元素font-size的大小,页面上所有元素都跟着变大变小了。

  2. 在不同的手机端的表现是什么?

    1. 在大屏手机所有内容可以实现整体变大。在小屏手机上整体变小。

  3. 在实际开发中是如何做的?

    1. 项目中安装和导入flexible 用于设置 rem 基准值。这个包会将html根元素font-size的大小设置为屏幕宽度的十分之一,如浏览器的宽度为750px, 则 1rem = 75px, 如浏览器的宽度为1500px, 则 1rem = 150px。

    2. 设计稿上的尺寸单位是像素,项目中使用postcss-pxtorem这款 PostCSS 插件,用于将 px 单位转化为 rem 单位。 我们会在配置文件中将rootValue的值设置为UI设计稿宽度的1/10,这样在编写代码的时候从设计稿上拷贝过来的像素,就能被转换成特定的rem值。如设计稿为750px, 我们会把rootValue的值设置为75px。假设这种情况下设计稿中某个元素的宽度为150px, 当把元素的宽度设置为150px的时候,插件自动就会用150 / 75 = 2rem作为元素的宽度,免去了我们亲自换算的过程。

3. 为什么要清除浮动?清除浮动的方法有哪些?

  1. 为什么要清除浮动?

    因为浮动的盒子脱离标准流,所以浮动的子盒子没有办法撑开父盒子。会导致如果父盒子没有设置高度的情况下,高度会为0,会影响后面的布局。

  2. 如何清除浮动?

    clear:both;
  3. 清除浮动的方法有?

    1. 额外标签法(在最后一个浮动标签后,新加一个标签,给其设置clear:both;)(不推荐)

    2. 父级添加overflow属性(父元素添加overflow:hidden)(不推荐)

    3. 使用after伪元素清除浮动(推荐使用)

      .clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
          content: "";
          display: block;
          height: 0;
          clear:both;
          visibility: hidden;
      }
      .clearfix{
          *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
      }
    4. 使用before和after双伪元素清除浮动

      .clearfix:after,.clearfix:before{
          content: "";
          display: table;
      }
      .clearfix:after{
          clear: both;
      }
      .clearfix{
          *zoom: 1;
      }

4. h5c3新特性有哪些?

H5新特性:

  1. 语义化更好的内容标签(header,nav,footer,aside,article,section)

  2. 音频、视频API(audio,video)

  3. 画布(Canvas) API

  4. 数据存储 localStorage、sessionStorage

  5. 表单控件calendar、date、time、email、url、search、number

  6. 地理(Geolocation) API

    做定位的时候可以用到

    navigator.geolocation.getCurrentPosition(
        position => { 
            var cords = position.coords;
            alert('纬度是' + cords.latitude + '经度是' + cords.longitude)
        },
        error => alert('获取地理位置失败')
    )
  7. 拖拽释放(Drag and drop) API

    实现拖拽排序的时候可以用到

CSS3的新特性

1.颜色:新增RGBA,HSLA模式

  1. 文字阴影(text-shadow)

  2. 边框圆角(border-radius)边框阴影: box-shadow

  3. 盒子模型:box-sizing

  4. 背景:background-size 设置背景图片的尺寸background-origin 设置背景图片的原点background-clip 设置背景图片的裁切区域,以”,”分隔可以设置多背景,用于自适应布局

  5. 渐变:linear-gradient、radial-gradient

  6. 过渡:transition,可实现动画

  7. 自定义动画

  8. 在CSS3中引入的伪元素::selection.

    实现选中元素有特殊样式时可以用到

  9. 媒体查询,flex布局

  10. border-image

  11. 2D转换:transform:translate(x,y) rotate(x,y) skew(x,y) scale(x,y)

  12. 3D转换

  13. 新增选择器:属性选择器、伪类选择器、伪元素选择器。

思考:如果问你可以说说Canvas画布怎样使用的吗?这种自己没有学过也没有研究过的知识怎么办?

不要说不会,说没搞过。

5. 说说css的权重

  • 选择器权重表如下:

    选择器选择器权重
    继承 或 *0, 0, 0, 0
    元素选择器0, 0, 0, 1
    类选择器,伪类选择器0, 0, 1, 0
    ID选择器0, 1, 0, 0
    行内样式 style1, 0, 0, 0
    !important无穷大
  • 权重是有4组数字组成, 复合选择器权重会累计,但是不会有进位。

  • 继承的权重是0, 如果该元素没有直接选中,不管父元素权重多高,子元素得到的权重都是 0。

6. src和href之间有什么区别?

  • 共同点:

    • src和href的作用都是用于请求资源。

  • 区别:

    • 请求资源类型不同 href,超文本引用,用于建立文档与资源的联系,常用的有:link、a。 src,将其所指向的资源下载并应用到当前页面,常见的有script、img。

    • 作用结果不同 href,用于文档与资源之间确立联系。 src,请求到的资源替换当前内容。

    • 浏览器的解析不同 href,将资源解析成css文件,并行加载请求资源,不会阻塞对当前文档的处理。 src,会暂停其他资源的处理,直到该资源加载、解析和执行完毕,将其所指向资源应用到当前内容。这也是为什么把js文件放在底部而不是头部的原因。

7. 说说css3盒模型 box-sizing

  1. 标准盒模型box-sizing: content-box;

    • 盒子的总宽度=width + padding(左右)+margin(左右)+border(左右)

    • 简单点说就是 width样式属性单指内容区域的宽度

  2. 新增盒模型box-sizing: border-box;

    • 盒子的总宽度=width+margin(左右)此时的width宽度已经包含了padding和border的值

    • 简单点说就是 width样式属性 包含内容 和内边距 和边框的宽度之和

8. px,rem和em的区别

  1. px是固定单位,是像素

  2. rem和em是相对单位

    1. 1rem = html根元素的font-size样式的字体大小, 一般用来做rem适配布局。

    2. 1em=父元素的font-size样式的字体大小,一般用在段落中设置首行缩进两个空格

      p { text-indent: 2em }  // text-indent 首行缩进

9 .CSS样式的引入方式

1、内联:HTML标签内联样式

2、行内:style标签内写css

3、link标签:link标签导入css样式

<link rel="stylesheet" type="text/css" href="style.css">

4、@import引入css样式

10. flex和gird布局的区别?

1.flex是一维布局(一维是指flexbox一次只能处理一个维度上的元素布局,一行或者一列) ,grid是二维布局也就是说grid布局可以更好的操作行和列。

2.flex布局它是简单的一维布局,最适合用在组件和小规模的布局中。更复杂的布局,Grid布局会比较好一些;

11. 说说你解决过什么浏览器兼容性的问题,你是怎样解决的?

  1. 不同浏览器的默认样式,如margin和padding等等各不相同?

    CSS里写 *{margin:0;padding:0;} 但是性能不好`

    ② 一般我们会引入reset.css 样式重置;

    reset-css是一个术语,指不同浏览器默认样式不同,可以通过引入一个重写这些样式的css文件

    统一这些样式

  2. 在开发一个项目的时候,页面上的按钮,在安卓设备上是正常的,但是在苹果设备上,按钮有默认的样式,原因是应为-webkit-appearance这个样式采用了默认值,之后我通过把样式设置为

    -webkit-appearance: none;

    就解决了这个问题,就把其默认的样式清除了。

  3. 在开发一个移动端项目的时候,渲染详情的时候,采用的默认的图片是webp格式的,测试在测试的时候发现,在ios苹果设备上不能正常的展示,因此我们在前端做了替换,把webp格式的图片换成了jpg格式的图片,就解决了这个问题。

    更多可以查看下面链接 webapp兼容问题解决 - 走看看

12. 外边距重叠怎么解决?

通过给父容器添加overflow: hidden 属性,使之成为 BFC

拓展:BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。

13. 什么是BFC?BFC的布局规则?哪些方式会创建BFC?

【面试题解】什么是外边距重叠?如何解决?什么是BFC? - 掘金

  • 什么是BFC?

    BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。外面也不会影响里面

  • BFC的布局规则(有什么特性)?

    内部的盒子会在垂直方向,一个个地放置;

    盒子垂直方向的距离由 margin 决定,属于同一个 BFC 的两个相邻盒子的上下 margin 会发生重叠;

    每一个元素的左边,与包含块的左边相接触(对于从右往左的布局,则相反),即使存在浮动也是如此;

    BFC 的区域不会与 float 重叠;

    BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此;

    计算 BFC 的高度时,浮动元素也參与计算。

    当一个元素设置了新的 BFC 后,就和这个元素外部的 BFC 没有关系了,这个元素只会去约束自己内部的子元素。

  • 以下方式会创建 BFC

    • overflow:不为 visible

    • float: 不为 none

    • display: 为 inline-blocktabletable-celltable-captionflexinline-flexgridinline-gridflow-root

    • position: 为 absolute 或者 fixed

    可以简单的理解成 OFDP ,当面试官问到你的时候,你回答个 OFDP,面试官大概率就知道你理解这个知识点了。

14. 屏幕分辨率不同,前端如何做适配?媒体查询具体怎么做?

  1. 使用rem 或 vw + 媒体查询

  2. 使用同一套html结构,使用@meidia,根据不同的设备宽度,写一套不同的样式,最终在移动端呈现移动端的样式,在PC端呈现PC端的样式

二、JS + ajax

1. ES6的新特性有哪些?let、var、const的区别?

答案:

  1. ES6的新特性有哪些?

    1. 函数默认参数

    2. 模版字符串

    3. 解构赋值

    4. 拓展运算符...

    5. 箭头函数

    6. Promise

    7. async await

    8. let、const 和块级作用域

    9. 类class

    10. ES6中的模块化

  2. var、let、const之间有什么区别?

    1. var存在变量提升,而 let,const声明的变量却不存在变量提升

    2. var声明的变量没有块级作用域,但let和const声明的变量具有块级作用域

    3. const声明的是常量,不可以改变,但let和var声明的是变量,可以改变。当然const声明常量不可改变对于简单数据类型而言指值不可以改变,对于复杂数据类型指地址不可以改变。

2. 普通函数和箭头函数内this指向有什么不同?如何改变this的指向?func.call func.apply 和 func.bind有什么异同?

答案:

  1. 普通函数和箭头函数内this指向有什么不同?

    1. 普通函数内this指向函数的调用者

    2. 箭头函数内this指向定义箭头函数处(也称上下文)this的指向,也就是说箭头函数定义的那里this指向什么,箭头函数内this就指向什么。

    3. 普通函数内有arguments指向参数组成的数组,箭头函数中没有arguments

  2. 如何改变this的指向?

    1. 可以通过 func.call func.apply 和 func.bind来改变函数内this的指向

  3. func.call func.apply 和 func.bind有什么异同?

    1. 相同点:都可以改变调用函数时,函数内this的指向

    2. 不同点:

      1. .call和.apply会立即调用函数,.bind不会立即调用函数,而是返回一个新函数

      2. .call和.bind内参数是用逗号分隔开,而.apply中参数需写成数组的形式

3. 如何判断this的指向?

  • 普通函数内: this指向调用者,谁调用指向谁

  • 箭头函数内:箭头函数定义的位置this指向什么,箭头函数的内部的this就指向什么

4. async、await 是什么?它有哪些作用?

  1. async await 是es6里面的新语法:

    • async 用于申明一个函数是异步的,调用async修饰的函数得到的是一个Promise的实例对象,不会阻塞线程。

    • await 用于等待一个异步方法执行完成, 可以替代promise中的then方法,优化写代码的方式。

    • 如果在函数内使用了await则该函数必须用async修饰。

      // 读取a文件内容,写入到b文件中的案例
      // npm i then-fs
      import thenFs from 'then-fs'
      ​
      ​
      async function readWriteFile(){
          try{
              // 1. 读取出a.txt中的内容
              const data =  await thenFs.readFile('./a.txt', 'utf8')
              console.log(data);
              // 2. 把读取出来的内容写入到b.txt中
              await thenFs.writeFile('./b.txt', data, 'utf8')
              console.log('写入成功');
          } catch(err){
              console.log('读写失败:' + err.message);
          }
      }
      ​
      var result = readWriteFile()
      console.log(result);  // result为一个Promise的实例对象

5. 常用的数组方法有哪些?哪些会改变数组哪些不会。

不要求都记住,能够说出3-5个就可以了,挑自己感觉用得最多的记

  • push() 末尾追加。

  • unshift() 头部追加。

  • pop() 删除最后一个元素。

  • shift() 删除第一个元素。

  • splice() 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。

  • reverse() 翻转。

  • sort() 对数组的元素进行排序

  • join() 把数组中的元素拼接成字符串(不会改变原数组)。

  • concat() 合并多个数组(不会改变原数组)。

  • forEach 迭代(不会改变原数组)。不能 对 只能终止回调函数

  • filter 过滤(不会改变原数组)。

  • map 映射(不会改变原数组,得到新数组)。

  • findIndex 查找符合条件的第一个元素的索引,如果都不符合条件,则返回-1。

  • every() 判断数组中是不是所有的元素都符合条件,如果都符合条件返回true,否则返回false。

  • some() 判断数组中是不是有一个元素符合条件,如果有一个符合条件返回true,否则返回false。

6. js数组循环常用的几种方法?(重点记map、foreach、filter、map的区别)

1、for循环:最基本的循环方式,也是速度比较快,效率比较高的循环方式

2、forEach循环 遍历数组中的每一项,没有返回值,对原数组没有影响,不支持IE,break无法跳出循环,retrun只能跳出当前循环,无法终止循环。

3、map循环:有返回值,支持return返回值,把每一次循环return返回的值组成一个新的数组,对原数组没有影响。

4、for of循环 :ES6新增的循环方法,没有索引,它可以正确响应break、continue和return语句,而且,这个方法避开了for-in循环的所有缺陷。for-in循环虽然可以循环数组,但是会有好多坑,它主要用于循环对象,今天我总结的是数组的循环,就把for-in排除在外了。

5、filter循环:返回新数组,不会改变原数组,新数组由符合条件的数组组成。

6、every循环:给数组每一项都运行一个函数,如果每一项都符合条件,就返回true,否则返回false。

7、some循环:给数组每一项都运行一个函数,同every相反,只要有一项符合条件,就跳出循环,返回true,否则返回false。

8、find循环:给数组每一项都运行一个函数,只要有一项符合条件,就跳出循环,返回第一个符合条件的元素,否则返回undefined。

7. 常用的字符串方法有哪些?

字符串的方法都不会改变字符串自身

  • trim() 去除前后空格

  • split() 把字符串按某个字符切分成数组

  • replace() 替换

  • toUpperCase 把字符串中的小写英文字母转成大写

  • toLowerCase 把字符串中的大写英文字母转成小写

  • slice() 提取某个字符串的一部分。

8. js常用的数据类型有哪些?哪些是简单的数据类型,哪些是复杂的数据类型?

  1. js常用的数据类型有:

    • 字符串

    • 数字

    • 布尔值

    • null

    • undefined

    • 数组

    • 对象

    • 集合

  2. 简单的数据类型包括

    1. 字符串

    2. 数字

    3. 布尔值

    4. null

    5. undefined

  3. 复杂的数据类型包括

    1. 数组

    2. 对象

    3. 集合

9. 简单数据类型和复杂数据类型有什么区别?

  • 简单数据类型,在赋值的时候传递的是值,进行值的拷贝。

  • 复杂数据类型,在赋值的时候传递的是地址。

  • 简单数据类型,是不可变的数据类型,在不改变内存地址的情况下,不能改变其值。

    var a = 1;
    a+= 1;
    console.log(a)  // 2
    // a的值虽然变成了2,  但是前提是:有赋值运算,a指向了全新的内存地址。
  • 复杂数据类型,是可变的数据类型,在不改变内存地址的情况下,可以改变其值。

    var arr = [1, 2, 3]
    arr.push(4)
    console.log(arr)  // [1, 2, 3, 4]
    // arr 指向的内存地址没变,但值发生了改变

10. ES6中新增了哪些数据类型? Symbol Set Map

  • Symbol: 生成独一无二的值,常作为对象的属性名,用来解决同名冲突问题,

    var s1 = Symbol("hello")
    var s2 = Symbol("hello")
    console.log(s1 == s2)   // false 虽然s1和s2长得一样 但是是不同的数据
  • Set: 集合,类似数组,但集合中元素的值是不能重复的, 用来做数据处理。

    对于简单数据类型指值不能重复,对于复杂数据类型指地址不能重复

    const s = new Set([1, 2, 2, 3])
    console.log(s)  // Set(3) {1, 2, 3}
  • Map: 类似对象, 也是key-value结构的,但是API比对象多, 用来存储key-value数据。

    const m1 = new Map([['name','tl'], ['age',18]])
    m1.get('age')                   // 18
    m1                              // Map(2) {'name' => 'tl', 'age' => 18}

11. 深浅拷贝

  1. 什么场景下需要用到拷贝?

    如果调用函数的时候,传入了一个对象,函数内可能会对对象进行修改,可能不符合你的期待,这时候就需要用到拷贝

    // 没有使用拷贝
    var obj = {}
    ​
    doSomething(obj)  // 调用某个会改变obj的函数
    ​
    obj可能发生变法
    ​
    ​
    // 使用了拷贝
    var obj = {}
    ​
    var newObj = JSON.parse(JSON.stringify(obj))
    ​
    doSomething(newObj)  // 调用某个会改变newObj的函数
    ​
    obj还是原来的obj
  2. 拷贝对于简单数据类型有意义没有?

    • 对简单数据类型没有意义,因为简单数据类型,是不可变的数据类型,在不改变内存地址的情况下,不能改变其值。

      var a = 1;
      a+= 1;
      console.log(a)  // 2
      // a的值虽然变成了2,  但是前提是:有赋值运算,a指向了全新的内存地址。
      • 复杂数据类型,是可变的数据类型,在不改变内存地址的情况下,可以改变其值。

        var arr = [1, 2, 3]
        arr.push(4)
        console.log(arr)  // [1, 2, 3, 4]
        // arr 指向的内存地址没变,但值发生了改变
  3. 什么情况下使用深浅拷贝?什么是浅拷贝?什么是深拷贝?有什么区别?

    1. 利用 复杂数据类型作为实参 调用某个函数的时候,如果担心函数内对形参对象进行的修改污染到原对象,就可以使用深浅拷贝。 可以使用深浅拷贝基于原对象进行拷贝,得到一个新对象,再利用新对象作为实参调用函数,就不用担心污染的问题了。

    2. 浅拷贝:指在进行数组或对象拷贝的时候只拷贝最外层,如果属性值是简单数据类型,拷贝其值,如果值是复杂数据类型,拷贝其地址。

    3. 深拷贝:是递归的拷贝,深层次的拷贝,不管是简单数据类型还是复杂数据类型,拷贝的都是值。

    4. 区别

      1. 浅拷贝,修改拷贝出来的对象,有可能影响到原对象

      2. 深拷贝,修改拷贝出来的对象,不会影响到原对象

  4. 能否手写代码实现深拷贝?

    1. 可以,通过递归或JSON模块可以实现。

    2. 通过JSON模块实现

      function deepClone(obj) {
          if(typeof obj != 'object') return obj;
          return JSON.parse(JSON.stringify(obj))
      }
      ​
      // 测试
      var obj = {
          name: {
              addr: '天边'
          }
      }
      var obj1 = deepClone(obj);
      obj1.name.addr = '眼前'
      console.log(obj.name.addr);  // 打印输出 ‘天边’
    3. 通过递归实现

      function deepClone(obj) {
          if (typeof obj != 'object') return obj;
          var temp = Array.isArray(obj) ? [] : {};
          for (let key in obj) {
              // 判断对象是否拥有该属性
              if (obj.hasOwnProperty(key)) {
                  if (obj[key] && typeof obj[key] == 'object') { // 如果obj[key]还是对象则执行递归
                      temp[key] = deepClone(obj[key]); // 递归
                  } else {
                      temp[key] = obj[key];
                  }
              }
          }
          return temp;
      }
      ​
      // 测试
      var obj = {
          name: {
              addr: '天边'
          }
      }
      var obj1 = deepClone(obj);
      obj1.name.addr = '眼前'
      console.log(obj.name.addr);  // 打印输出 ‘天边’

11. 如何实现对象的浅拷贝?

  1. 通过Object.assign 把原对象的属性拷贝到新对象身上

  2. 使用...拓展运算符

// 写法1
var user = {name: '浪哥', gf: {name: '全智贤'}};
var user1 = {}
Object.assign(user1, user)
​
​
// 写法2
var user = {name: '浪哥', gf: {name: '全智贤'}};
var user1 = {...user}
console.log(user1);

12、什么是原型,说说原型的作用,说说原型三角关系?什么是原型链,原型链有什么作用?

  1. 什么是原型,说说原型的作用和你对原型的理解?

    • 构造函数有一个prototype属性, 指向一个对象,这个对象我们称之为原型对象

    • 对原型的理解

      • 构造函数有一个prototype属性, 指向一个对象,这个对象我们称之为原型对象

      • 实例对象有一个__proto__属性,同样指向原型对象

      • 原型对象会有一个constructor的属性指回构造函数

    • 原型的作用

      • 当使用实例对象的属性和方法的时候,如果在自身身上查找不到就会去原型对象身上查找,所以原型能够实现属性和方法的共享

  2. 什么是原型链,原型链有什么作用?

    • 什么是原型链?

      • 对象会有一个__proto__属性,指向对象的原型, 对象的原型对象会有一个__proto__属性指向原型对象的原型,原型对象的原型回有一个__proto__属性指向原型对象的原型对象的原型,这样利用

        __proto__ 就可以形成链式查找关系,我们称之为原型链。

    • 原型链有什么作用?

      • 实现属性和方法的共享,如果在自身身上查找不到就通过__proto__属性找到原型对象,去原型对象身上去查找,如果在原型对象身上查不到,就通过__proto__属性找到原型对象的原型,去其身上查找,一直找到null对象身上为止。

13. 什么是闭包?有什么作用?能否手写一个闭包的示例代码?

  1. 什么是闭包?

    如果A函数能访问B函数的内部变量,这种现象我们就称之为闭包

  2. 有哪些优缺点

    优点:能延伸变量的作用范围

  3. 能否手写一个闭包函数?

    可以

    闭包最常见的使用场景:定制一个函数

    1. 外部函数嵌套内部函数

    2. 内部函数使用外部函数的局部变量

    3. 返回内部函数或对内部函数的引用

    function makeSizer(size) {
      // size是形参,等同于makeSizer函数的的局部变量
      return function() {
        // 内部函数使用了外部函数的size变量,因此形成了闭包  
        document.body.style.fontSize = size + 'px';
      };
    }
    ​
    var size12 = makeSizer(12);  // size12指向的函数,可以设置body的fontSize为12px
    var size14 = makeSizer(14);  // size14指向的函数,可以设置body的fontSize为14px
    var size16 = makeSizer(16);  // size16指向的函数,可以设置body的fontSize为16px

14. 谈谈你对http协议的理解?说说常见的http状态码有哪些,各有什么含义?

答案:

  1. 谈谈你对http协议的理解?

    1. http协议中文名为超文本传输协议,是网页及网页中嵌套内容的传输协议。

    2. http协议具有以下三个特点:

      1. 规定了请求报文和响应报文的格式和内容。

      2. http协议是无状态的。

      3. http协议是构建在TCP协议之上的应用层协议。

    3. 具体来说:

      1. 规定了请求报文和响应报文的格式和内容:

        1. 请求包括:请求url地址、请求方式、请求头和请求体

        2. 响应包括:响应状态码、响应头和响应体

      2. http协议是无状态的,两次请求之间没有任何关系,因此在实际的项目开发中,我们会使用特定的会话跟踪技术(如JWT token)来记录用户的登录状态。

      3. http协议是构建在TCP协议之上的应用层协议,因此是数据传输过程是面向连接的,类似打电话(先建立连接,再传输数据,再断开连接),数据传输可靠。

  2. 说说常见的http状态码有哪些,各有什么含义?

    答案:

    常见的状态码包含以4类

    1. 1xx 表示目前为止一切正常, 客户端应该继续请求。

    2. 2xx 代表成功,200代表成功、201代表创建成功...

    3. 3xx 代表重定向,301 代表永久性重定向,302代表临时重定向

    4. 4xx 代表客服端错误

      1. 400代表请求错误

      2. 401代表认证失败(服务器不知道你是谁,token过期了)

      3. 403代表没有权限(我可能知道你是谁,但是对不起你没有权限)

      4. 404 没找到,请求资源不存在

      5. 405 请求方式不支持(请求方式错误)

    5. 5xx 代表服务器错误

15. HTTP的请求方式有哪些? get和post有什么区别?

  • HTTP的请求方式有哪些?

    • get 常用来获取

    • post 常用来新增

    • put 常用来全量修改

    • patch 常用来部分修改

    • delete 常用来删除

  • get和post有什么区别?

    • get请求一般用来查询获取,post请求一般用来新增

    • get请求没有请求体,post请求有请求体

    • get请求可能有缓存,post请求不会有缓存

16. axios的底层是用什么实现的?

答案:

  1. axios的底层是用XMLHttpRequestPromise实现的

  2. XMLHttpRequest负责在浏览器中发送请求。

  3. Promise主要用来优化调用方式,解决回调地狱的问题。

    // 自己去封装一个axios的函数
    function axios(option){
        // 1. 返回一个Promise的实例对象(代表发送网路请求的异步操作)
        return new Promise((resolve, reject)=>{
          // 2. 在Promise的回调函数中,通过xhr执行发送网路请求的异步操作
          const xhr = new XMLHttpRequest()
          xhr.open(option.method, option.url)
          xhr.onreadystatechange = function(){
            // 当网络请求响应成功的时候 xhr.readyState==4
            if (xhr.readyState==4){
              // 3. 成功通过resolve告知成功的结果,失败通过reject告知失败的结果
              // 状态码以2开头代表成功
              if(xhr.status.toString().startsWith('2')){
                // console.log(xhr.responseText);
                resolve(JSON.parse(xhr.responseText))
              }else{
                reject()
              }
            }
          }
          xhr.send()
        }) 
    }
    ​
    // 测试
    const res = axios({
        method: 'get',
        url: 'http://123.57.109.30:3006/api/getbooks'
    }).then
    console.log(res);

17. 什么是跨域? 跨域限制了什么?说说你知道有哪些解决跨域的办法?进一步说说他们的原理是怎样的?

答案:

  1. 什么是跨域? 同源策略限制了什么?

    1. 如果两个资源地址之间存在协议、域名、端口号任何一个不相同,我们就说这两个资源地址之间跨域了。

    2. 同源策略限制了DOM、Cookie、ajax网络请求、LocalStorage和SessionStorage等的跨域访问。

  2. 说说你知道有哪些解决ajax跨域的办法

    1. jsonp

    2. cors

    3. 关掉谷歌chrome浏览器的安全机制

    4. vue中的代理

    5. 部署的使用 nginx反向代理

  3. 他们的原理是怎样的?

    1. jsonp

      jsonp主要利用了script标签src属性不受同源策略限制的特点实现,返回的是函数的调用。

    2. cors

      cors在服务器端实现,前端开发者并不需要做什么,服务器在响应时会响应一组响应头,这些响应头告诉浏览器,允许接口被跨域的网站脚本访问。

    3. 关掉谷歌chrome浏览器的安全机制

      直接把浏览器中的同源策略给禁掉了,就不会限制跨域资源访问了。由于我们只能关自己的,因此只能在开发的时候使用

    4. vue中的代理

      利用服务器调用接口不受跨域限制的特点实现,访问网页和Ajax请求都发给开发服务器,而ajax请求开发服务器帮会转发给真正的服务器,因此该技术只能在开发的时候使用。

    5. 部署的使用 nginx反向代理

      利用服务器之间调接口不会跨域的特点实现的。

18 说说axios中请求拦截器,响应拦截器各自的执行时机是怎样的?有什么作用?说说对token机制的理解?如何解决token可能过期的问题?

  1. 说说axios中请求拦截器,响应拦截器各自的执行时机是怎样的?有什么作用?

    1. 请求拦截器在调用axio请求方法后,浏览器真正发起请求前执行。

      最常见的作用是可以在这里统一的为请求添加携带token的请求头

    2. 响应拦截器在浏览器拿到服务器响应数据后,axios拿到响应数据前执行。

      最常见的作用是在这里对token过期进行处理。

      // 添加请求拦截器
      axios.interceptors.request.use(function (config) {
          // 在发送请求之前做些什么
          return config;
        }, function (error) {
          // 对请求错误做些什么
          return Promise.reject(error);
      });
      ​
      // 添加响应拦截器
      axios.interceptors.response.use(function (response) {
          // 对响应数据做点什么
          return response;
        }, function (error) {
          // 对响应错误做点什么
          return Promise.reject(error);
      });
  2. 说说对token机制的理解?

    • token是身份令牌,通俗一点理解就是暗号;

    • 前端调用后台接口的时候在请求头中携带token,后端就能够通过对token解析,知道你是谁;

  3. 如何解决token可能过期的问题?

    1. 我们怎么知道token过期了?

      1. 通过响应拦截器和状态码 。 状态码401:代表用户认证失败,token是过期的或者非法的

    2. token过期了怎么办?

      1. 重新登录

        1. 在响应拦截器中判断响应状态码是不是401,如果是401代表用户认证失败,token过期。

        2. 清空token, 并跳转到登录页,让用户重新登录。

      2. 使用 refresh_token 解决 token 过期:

        1. 在响应拦截器中判断响应状态码是不是401,如果是401代表用户认证失败,token过期。

        2. 就通过refresh_token调用接口获取新的token, 再重新调接口就不会出现401了。

        3. 如果通过refresh_token调用接口获取新的token也失败,则说明refresh_token也过期,那就没办法,只能跳转到登录页,重新登录了。

19. 说说你对Promise的理解?Promise提供了哪些API,列举下,分别说说其作用。Promise.all和Promise.race有什么区别?

  1. 说说你对Promise的理解?

    • Promise 是异步编程的一种解决方案,解决了传统解决方案回调函数容易形成回调地狱的问题,更便于开发与维护。Promise 并不会改变JS代码同步或异步执行的机制,只是一种新的写代码的方式。

    • Promise代表着某个异步任务,Promise存在三个状态,分别是pending 正在做,fulfilled完成了和rejected 失败了。

  2. Promise提供了哪些API,列举下,分别说说其作用?

    • new Promise: 用于创建Promise实例对象

    • PromiseObj.then():等待Promise异步任务执行完,在回调函数中拿到成功或失败的结果

    • PromiseObj.catch():捕获Promise异步任务执行过程出现的异常

    • Promise.resolve(): 将普通对象转换成一个一定会成功的Promise对象

      // 把'helloworld' 转换成一个Promise对象
      Promise.resolve('helloworld').then(data=>{    
          console.log(data) // 'helloworld'
      })
    • Promise.reject(): : 将普通对象转换成一个一定会失败的Promise对象

      Promise.reject('出错了').then(null, err=>{
          console.log(err)//出错了
      })
      ​
      // 等价于
      new Promise((resolve,reject)=>{
          reject('出错了')
      }).then(null,err=>{
          console.log(err) //出错了
      })
    • Promise.all(): 运行所有。

    • Promise.race():赛跑。

  3. Promise.all和Promise.race有什么区别?

    • Promise.all()调用得到的是一个Promise实例对象,会运行所有的子Promise异步任务,等待所有子Promise异步任务执行完,再把结果汇总到数组中通过resolve告知调用者,如果任意一个失败,则通过reject告知失败的结果。

      简单点说,就是等待所有的都执行完,汇总成功的结果,只要有任意一个失败了就失败了。

      Promise.all([promise1, promise2]).then(function(results) {
          // results 是promise1和promise2 的结果组成的数组
          // Both promises resolved
      })
      .catch(function(error) {
          // One or more promises was rejected
      });
    • Promise.race()调用得到的是一个Promise实例对象,会运行所有的子Promise异步任务,看其中哪个子Prmoise实例先执行完,哪个先运行完,就把哪个的结作为最终Promise.race()执行的结果。最快执行完得成功了就resolve其成功得结果,最快执行完得失败了就reject其错误。

      简单来说就是看谁跑得快,。剩下的就不管了。

      Promise.race([promise1, promise2]).then(function(result) {
          // result 是promise1和promise2中最先执行完的成功的结果
      }).catch(function(err) {
          // err  是promise1和promise2中最先执行完的失败的结果
      });

20 防抖和节流:说说你对防抖和节流机制的认识,说说它们作用和应用场景有何不同,能手写出其核心代码吗?

  1. 说说你对防抖和节流机制的认识

    • 防抖机制:指当事件被触发后,延迟 n 秒后再执行核心代码,如果在这 n 秒内事件又被触发,则重新计时。

    • 节流机制:是一种有效降低单位时间内,核心代码执行次数的机制。

    • (普通)节流阀:指当事件被触发后,延迟 n 秒后再执行核心代码,如果在这 n 秒内事件又被触发,则不进行任何处理。

    • (互斥)节流阀:指当事件被触发后,立即执行核心代码,在核心代码被执行完之前,再次触发,则不做任何处理。

  2. 说说它们作用和应用场景有何不同

    • 防抖:如果事件被频繁触发,防抖能保证只有最后一次触发生效!前面 N 多次的触发都会被忽略!如:可以使用防抖技术优化搜索框建议词33。

    • 节流:如果事件被频繁触发,节流能够减少事件触发的频率,因此,节流是有选择性地执行一部分事件!如:可以使用节流优化滚动条滚动事件的监听,避免滚动事件的回调函数太过频繁执行。

  3. 能手写出其核心代码吗

    • 一般如果基于一个普通函数,得到一个防抖函数我会使用lodash库,中有一个debounce函数去做这个事情

    • 如果让我封装?

    • 防抖:

      // 1. 定义一个变量指向延时器
      var timer = null;
      ​
      // 2. 某事件会触发的回调函数
      function doSomething(){
          // 2.1 先删除有可能已存在的延时器
          clearTimeOUt(timer);
          // 2.2 创建新的延时器
          timer = setTimeOut(function(){
              // 2.2.1 真正要执行的核心代码或调用核心函数
              // code...
              console.log('核心代码')
          }, 500)
      }
    • 普通节流

      // 1. 厕所门是打开的: null(打开)  有对象(关闭)
      var timer = null;
      ​
      // 2. 上厕所要调用的事件函数
      function doSomething(){
          // 2.1 如果厕所门是关着的,就不上测试了
          if(timer) return;
          // 2.2 如果厕所门是打开的
          // 2.2.1 上厕所之前先把厕所门给关上,而这个厕所有点特殊,至少得上16毫秒以上
          timer = setTimeOut(function(){
              // 2.2.2 上厕所(核心代码)
              // code...
              console.log('核心代码')
              // 2.2.3 上完厕所之后把门打开
              timer = null;
          }, 16)
      }
    • 互斥节流

      // 1. 厕所门是打开的: false(打开)  true(关闭)
      var flag = false;
      ​
      // 2. 上厕所要调用的事件函数
      function doSomething(){
          // 2.flag 如果厕所门是关着的,就不上测试了
          if(flag) return;
          // 2.2 如果厕所门是打开的
          // 2.2.1 上厕所之前先把厕所门给关上
          flag = true
          // 2.2.2 上厕所(核心代码)
          // code...
          console.log('核心代码')
          // 2.2.3 上完厕所之后把门打开
          flag = false;
      }

21 JS代码的执行机制

  1. 说说同步和异步的执行顺序

    1. 先执行同步代码:

    2. 再执行异步代码

      1. 先执行异步代码中的微任务:如:Promise.then、.catch 和 .finally . process.nextTick

      2. 再执行异步代码中的宏任务:如:异步 Ajax 请求、 setTimeout、setInterval、 文件操作

  2. 说说同步任务和异步任务的执行过程

    1. 同步任务由 JavaScript 主线程次序执行

    2. 异步任务委托给宿主环境执行

    3. 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行

    4. JavaScript 主线程的执行栈被清空后,会读取任务队列中的回调函数,按次序执行

    5. JavaScript 主线程不断重复上面的第 4 步

  3. 说说什么是EventLop(事件循环)机制

    JavaScript 主线程从“任务队列”中读取异步 任务的回调函数,放到执行栈中依次执行。这 个过程是循环不断的,所以整个的这种运行机 制又称为 EventLoop(事件循环)。

22 手写JS数组去重

方法无数种,挑其中的两三种实践下即可 https://www.cnblogs.com/MomentYY/p/15676743.html

  • 方式一:利用集合Set()+Array.from()

    const arr = [1, 2, 2]
    const result = Array.from(new Set(arr))
    console.log(result) // [ 1, 2]
  • 方式二: 利用数组的includes方法

    function removeDuplicate(arr) {
      const newArr = []
      arr.forEach(item => {
        if (!newArr.includes(item)) {
          newArr.push(item)
        }
      })
      return newArr
    }
    ​
    const arr = [1, 2, 2]
    const result = removeDuplicate(arr)
    console.log(result) // [ 1, 2]
  • 方式三

    function unique(arr) {
      return arr.filter(function(item, index, arr) {
        //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
        return arr.indexOf(item, 0) === index;
      });
    }
    ​
    const arr = [1, 2, 2]
    const result = unique(arr)
    console.log(result) // [ 1, 2]

23 数据类型的判断有哪些方法?他们的优缺点及区别是什么?

  • typeof 得到数据对象的类型,缺点:只能检测出基本类型

  • instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。缺点:不好检测基本类型。

  • constructor 基本能检测所有的类型(除了null和undefined) 缺点:constructor易被修改

  • Object.prototype.toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的[[Class]]。其格式为[object Xxx] ,其中 Xxx 即为目标的类型,能检测出所有的类型,缺点是 IE6下,undefined和null均为Object

// 利用 toString 封装的 typeOf 函数便能判断所有的js数据类型了
function typeOf(target){
    // Object.prototype.toString.call(target) 通过此便可获得目标的[[Class]]
    // 再由正则匹配 就能获得目标的类型
    // toLocaleLowerCase()将其转换为小写 留到下面扩展用
    return /^\[object (\w+)]$/.exec(Object.prototype.toString.call(target))[1].toLocaleLowerCase()
}

//原生typeof
typeOf new Set()   // 'object'
typeof null        // 'object'''
​

// 自己封装的typeOf
typeOf(new Set())   // 'set''
typeOf(null)        // 'null'
不同类型的优缺点TYPEOF得到类型INSTANCEOFCONSTRUCTOROBJECT.PROTOTYPE.TOSTRING.CALL
优点使用简单能检测出引用类型基本能检测所有的类型(除了null和undefined)检测出所有的类型
缺点只能检测出基本类型不能检测出基本类型,且不能跨iframeconstructor易被修改,也不能跨iframeIE6下,undefined和null均为Object

24. cookie、localStorage和sessionStorage

  1. cookie、localStorage和sessionStorage有什么异同?

    • 相同

      • 都是浏览器本地存储空间,都可以存储数据,如token等

    • 不同主要体现在生命周期

      • localStorage 除非手动清除,否则一直存在那里

      • sessionStorage 关闭浏览器会话后,就自动移除了

      • cookie 可以设置有效期: 1可以设置指定的过期时间,2也可以指定关闭浏览器会话自动移除等

      • cookie 具有自动携带的特性,访问网页的时候,浏览器自动的会把域名下存储的cookie数据放到请求头中请求服务器

  2. cookie和session有什么异同

    • 相同:

      • 由于http协议是无状态的,他们都是实现用户登录认证的一种会话跟踪技术。

    • 不同

      • cookie存储在浏览器客服端,不安全,不能把敏感的数据,直接明文放在cookie中。

      • session存储在服务器,是安全的,可以存储敏感数据。

      • session的实现依赖于cookie,session空间需要cookie来标识。

25. 页面上的script标签内的代码是同步执行的还是异步执行的?

  • 默认是同步执行的,所以我们一般习惯性的把script标签放在body标签结束之前

  • 但如果加上defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

  • 面试官可能进一步问:img标签图片的加载是同步的还是异步的?

    • 是异步的

26. script 标签中 defer 和 async 的区别?

面试官可能会问你:遇到一个script标签

  • script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。

  • async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。

  • defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

27. 说说js的事件执行机制?事件委托的原理是?

  1. 说说js的事件执行机制是怎样的?

    1. js的事件执行机制分为三个阶段:事件捕获阶段、当前目标阶段、事件冒泡阶段

  2. 事件委托的原理是?

    1. 事件冒泡

    2. ul里由一个百个li,我们需要监听这一百个li的点击事件,这样得操作一百次DOM。

      因此我们干脆直接监听ul的点击事件,当点击li得时候,事件会冒泡到ul身上,因此点击li也会间接触发ul的点击事件。

28. for in和for of 的区别?

  1. for in既可以遍历数组,也可以遍历对象,遍历数组拿到的是索引下标,遍历对象拿到的是属性名

  2. for of不能遍历对象,只能遍历带有iterator接口的对象,例如Set、Map、String和Array,拿到的是每一项的值

三、Vue2

1. vue的生命周期钩子函数有哪些?各在什么时机执行?mounted和created有什么区别?销毁的钩子函数一般用来做什么处理?

答案:

  1. vue的生命周期钩子函数有哪些?各在什么时机执行?

    答:

    一共有8个:

    • beforeCreate :初始化前

    • created :初始化后

    • beforeMount :组件挂载前

    • mounted : 组件挂载后

    • beforeUpdate :DOM更新前

    • updated :DOM更新后

    • beforeDestroy : 组件销毁前

    • destroyed : 组件销毁后

    如果使用keep-alive实现组件缓存,就还会多出2个

    • activated: 组件激活时调用。

    • deactivated: 组件停用时调用。

  2. mounted和created有什么区别?

    • created是初始化后,此时可以操作data中的数据

    • mounted是挂载后,此时可以操作DOM

  3. 销毁的钩子函数一般用来做什么处理?

    • beforeDestroy和destroyed里主要处理扫尾工作,如清除定时器,解绑绑定的事件等。

2. VUE双向绑定(响应式)的原理是怎样的?

vue2.0中双向绑定主要基于以下两点实现

  • 发布者-订阅者模式

  • ES5提供的Object.defineProperty()劫持数据

要实现Vue中的双向数据绑定,大致可以划分三个模块:Watcher,Observer 和 Compiler 如图:

  • Observer数据监听器, 负责对数据对象的所有属性进行劫持,监听到数据发生变化后通知订阅者。

  • Compiler指令解析器, 解析模板指令,初始化视图。并订阅数据变化,绑定更新函数。

  • Watcher订阅者, 关联Observer和Compiler;订阅并收到属性变动的通知,调用update(),是会触发Compiler中绑定的回调,更新视图。

总的来说:MVVM作为数据绑定的入口,整合Observer、Compiler和Watcher三者,通过Observer来监听自己的数据变化,通过Compile来解析编译模板指令,利用Watcher搭起Observer和Compile之间的通信桥梁,最终达到数据变化 -> 视图自动更新;视图变化-> 更新数据的效果,这就是双向数绑定的原理。

补充:vue3和vue2中双向数据数据绑定实现的主要不同之处,是把ES5中的Object.defineProperty()换成了

ES6中的Proxy

3. 什么是mvvm模型?

MVVM 即 Model-View-ViewModel,它是Vue中实现双向数据绑定的模式。其中:

  1. M 是Model 代表模型,或者说数据。

  2. V 是 View 代表视图,或者说用户看到的界面。

  3. VM 代表 ViewModel 是model数据层和view视图层的双向联动,数据改变会影响视图,视图改变会影响数据

可能会进一步问什么是MVC?(这是后端中才用到的知识,照着念即可)

MVC: MVC即model-view-controller(模型-视图-控制器)是项⽬的⼀种分层架构思想,它把复杂的业 务逻辑,抽离为职能单⼀的⼩模块,每个模块看似相互独⽴,其实⼜各⾃有相互依赖关系。它的好处 是:保证了模块的智能单⼀性,⽅便程序的开发、维护、耦合度低

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

  • v-if 是通过创建和销毁实现显示和隐藏。

  • v-show 元素总是会被创建,通过css样式display:none/block控制显示和隐藏。

  • 如果需要频繁地在显示和隐藏之间切换,则使用 v-show ;否则,使用 v-if

5. 组件中的data为什么要定义成一个函数,而不是写成对象的形式?

防止变量污染

  1. 每一个组件都可以被实例成多个组件实例对象;

    <van-button>1<van-button>   // 利用van-button组件得到第一个 van-button组件的实例对象
    <van-button>2<van-button>   // 利用van-button组件得到第二个 van-button组件的实例对象
  2. 如果data写成对象的形式,同一个组件的多个组件实例对象之间使用的将是同一个data对象中的数据,会相互干扰,相互影响。

  3. 而把data写成函数的形式则能避免这个问题,每次利用组件实例化组件的实例对象的时候,都会调用data函数得到一个全新的对象,这样多个组件实例对象之间使用数据都是全新的对象,实现了组件实例对象之间的独立和互不干扰。

6. 组件之间的传值方式有哪些?

  • 父传子,子组件通过props接收

  • 子传父,子组件使用$emit对父组件进行传值

    通过eventBus进行跨组件值传递

  • 通过vuex 进行全局状态管理

  • 父组件通过$ref获取子组件实例对象,进而传值

  • 路由传值

  • localStorage、sessionStorage、cookie

  • 父子之间通过$parent$chidren获取实例进而通信

  • provideinject,官方不建议使用

7. computed、watch和methods有何区别?

  • 使用场景上:

    • computed是计算属性,如果某个属性的值是通过其他变量计算出来的,可以使用计算属性

    • watch 是侦听器,用于监听变量或属性的变化,然后去做特定的事情

    • methods 是方法,往往用来完成特定的任务

  • 特点上:

    • computed:

      • 具有缓存,依赖值不变不会重新执行代码计算,会复用缓存中的计算结果。

      • computed中不能进行异步操作

      • 使用的时候当成变量使用即可,不要写成函数调用的形式

    • watch :

      • watch中可以进行异步操作

    • methods:

      • methods中可以进行异步操作

      • 使用的时候当成函数使用,需要写成函数调用的形式

8 vuex是什么?有哪五个重要的属性?怎么使用?哪种功能场景使用它?

是什么:vue框架中全局状态管理工具:

有哪五个重要的属性节点,分别是 State、 Getter、Mutation 、Action、 Module

使用:新建一个目录store文件夹,在其中完成Store的实例化操作后,再main.js中导入,并放到new Vue的身上。

场景:单页应用中,保存不同组件之间共享的状态。如登录状态、加入购物车等。

 vuex的State特性
A、Vuex就是一个仓库,仓库里面放了很多对象。其中state就是数据源存放地,对应于一般Vue对象里面的data

B、state里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新

C、它通过mapState把全局的 state 和 getters 映射到当前组件的 computed 计算属性中 ​

vuex的Getter特性

A、getters 可以对State进行计算操作,它就是Store的计算属性

B、 虽然在组件内也可以做计算属性,但是getters 可以在多组件之间复用

C、 如果一个状态只在一个组件内使用,是可以不用getters ​

vuex的Mutation特性

改变store中state状态的唯一方法就是提交mutation,就很类似事件。 每个mutation都有一个字符串类型的事件类型和一个回调函数,我们需要改变state的值就要在回调函数中改变。

我们要执行这个回调函数,那么我们需要执行一个相应的调用方法:store.commit。 ​

Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态; Action 可以包含任意异步操作,Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,

因此你可以调用 context.commit 提交一个 mutation, 或者通过 context.state 和 context.getters 来获取 state 和 getters。 Action 通过 store.dispatch 方法触发:eg。 store.dispatch('increment') ​

vuex的module特性

Module其实只是解决了当state中很复杂臃肿的时候,module可以将store分割成模块, 每个模块中拥有自己的state、mutation、action和getter

9 vue中 keep-alive 组件有什么作用?

答:

  • keep-alive 是 Vue 内置的一个组件,可以使被包含的组件被缓存起来,避免重新渲染。

<keep-alive include="A,B">
  <component>
    <!-- 该组件将被缓存! -->
  </component>
</keep-alive>
​
<keep-alive exclude="A,B">
  <component>
    <!-- 该组件将被缓存! -->
  </component>
</keep-alive>
​
​
// 如果只想 router-view 里面某个组件被缓存
var routes = [
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: {
      keepAlive: true // 需要被缓存
    }
  }, {
    path: '/:id',
    name: 'edit',
    component: Edit,
    meta: {
      keepAlive: false // 不需要被缓存
    }
  }
]
​
<keep-alive>
    <router-view v-if="$route.meta.keepAlive">
        <!-- 这里是会被缓存的视图组件,比如 Home! -->
    </router-view>
</keep-alive>
 
<router-view v-if="!$route.meta.keepAlive">
    <!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>

10 vue组件的scoped属性的作用

在style标签上添加scoped属性,以表示它的样式作用于当下的模块,很好的实现了样式私有化的目的;

但是会导致:样式不易修改,很多时候,我们是需要对公共组件的样式做微调的;

解决办法:

①:使用混合型的css样式:(混合使用全局跟本地的样式)

<style> /* 全局样式 */ </style>
<style scoped> /* 本地样式 */ </style>

②:深度作用选择器(>>>)如果你希望 scoped 样式中的一个选择器能够作用得更深,影响子组件,你可以使用 >>> 操作符:

注意:/deep/ 和 >>> 是一个意思,可以理解是其别名

<style scoped> 
.a >>> .b { /* ... */ } 
</style>
1. ::v-deep  在scss中使用
 
<style scoped lang="scss">
 
  ::v-deep img{
    width: 100px;
    height: 100px;
  }
 
</style>
 
 2. >>> 在less中使用
 
<style scoped lang="less">
 
  >>> img{
    width: 100px;
    height: 100px;
  }
 
</style>
 
 3. /deep/ 在less中使用
 
<style scoped lang="less">
 
  /deep/  img{
    width: 100px;
    height: 100px;
  }
 
</style>

11. vue-router如何响应 路由参数 的变化?

你可以简单地 watch (监测变化) $route 对象:

{
  ...,
  watch: {
    '$route' (to, from) {
      // 对路由变化作出响应...
    }
  }
}

12. vue中子组件调用父组件的方法

第一种方法是直接在子组件中通过this.$parent.event来调用父组件的方法 第二种方法是在子组件里用$emit向父组件触发一个事件,父组件监听这个事件就行了。 第三种方法是把父组件的方法,以属性props的方式传给子组件,然后子组件调用(参考下方代码)

三种 都可以实现子组件调用父组件的方法,

<child :fatherMethod="fangfa"/>
​
methods: {
    fangfa(){
        ...父组件中的某个方法
    }
}
<template>
  <div>
    <button @click="childMethod()">点击</button>
  </div>
</template>
<script>
  export default {
    props: {
      fatherMethod: {
        type: Function,
        default: null
      }
    },
    methods: {
      childMethod() {
        if (this.fatherMethod) {
          this.fatherMethod();
        }
      }
    }
  };
</script>

13 虚拟DOM和DIFF算法

  1. vue中是如何提高操作DOM的性能的?

    • 运用虚拟DOM + DIFF算法。

    • 运用虚拟DOM,主要目的是尽量减少对真实DOM的(查询)操作。

    • 运用DIFF算法,主要目的是对比新旧虚拟DOM的差异,找出差异的部分,然后去操作DOM更新有差异的部分,能够尽量减少对真实DOM的操作,只改变应该改变的那部分,从而提高渲染性能。

  2. 什么是虚拟DOM, 

    1. 本质是保存节点信息, 属性和内容的一个JS对象

  3. 为什么要使用虚拟DOM

    1. 减少了DOM的操作,优化了性能

      减少了对DOM的操作。页面中的数据和状态变化,都通过Vnode对比,只需要在比对完之后更新DOM,不需要频繁操作,提高了页面性能。虚拟DOM的频繁修改操作不会引发回流和重绘,它会一次性对比差异并修改真实DOM,最后才依次进行回流重绘,减少了真实DOM中多次回流重绘引起的性能损耗。

    2. 跨平台性

      虚拟DOM本质是JS对象,所以不管Node还是浏览器环境,都可以操作;它抽象了原本的渲染过程,实现了跨平台的能力,它不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是小程序,也可以是各种GUI。

  4. 什么是diff算法

    1. diff算法是将新虚拟 DOM 和老虚拟 DOM 进行比较,算出最小差异的算法

  5. 为什么需要diff算法

    1. 要尽量减少DOM操作,就得找出新旧虚拟DOM中得最小差异然后去更新,如何找出其中得最小差异,就得通过diff算法

  6. diff算法的工作流程是

    1. 逐层从上往下比较

    2. 根元素变化,删除整个重建

    3. 根元素不变,属性改变更新属性

    4. 子元素比较的时候,如果无key则就地更新

    5. 子元素比较的时候,如果有key则按照key比较更新

14 路由守卫/路由钩子函数

  1. 说说路由钩子函数有哪几个?执行时机是?

    1. 前置路由守卫beforeEach 执行时机:跳转路由前

    2. 后置路由守卫 afterEach 执行时机: 路由跳转后

  2. 路由守卫应用场景?

    1. 可以在路由守卫中统一进行用户是否登录认证的判断,如没有token就让用户不能访问需要进行用户认证才能访问的页面,跳回登录页。

  3. 路由钩子函数分为哪3类

    1. 全局钩子

      针对所有路由生效

      router.beforeEach(async(to, from, next) => {
          // 所有路由切换前先执行的代码
      })
    2. 路由独享钩子

      针对特定路由生效

      routes: [
        {
          path: '/xxx',
          component: xxx,
          beforeEnter: (to, from, next) => {
            // 跳转路由到/xxx前先执行的代码
          }
        }
      ]
    3. 组件内路由钩子

      • beforeRouteEnter(to, from, next):跳转路由渲染组件时触发

        <script>
        export default {
          name: 'feedbackMgt',
          beforeRouteEnter(to, from, next) {
              // 跳转路由渲染本组件时触发执行的代码
          }
        }
        </script>    
      • beforeRouteUpdate(to, from, next):跳转路由且组件被复用时触发

      • beforeRouteLeave(to, from, next):跳转路由且离开组件时触发

15. vue中有时会出现页面闪现的问题,如何解决?

使用v-clock指令即可解决

<!DOCTYPE html>
<html lang="en">
​
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <style>
        /* 1. 给[v-clock]属性选择器添加隐藏样式 */
        [v-clock]{
            display: none;
        }
    </style>
</head>
​
<body>
    <!-- 2. 给需要控制不闪现的标签或组件添加v-clock指令(vue会再完成渲染后移除v-clock属性) -->
    <div id="app" v-clock>
        {{ message }}
    </div>
    
    <script>
​
        setTimeout(()=>{
            var app = new Vue({
            el: '#app',
            data: {
                message: 'Hello Vue!'
            }
        })
        }, 2000)     
    </script>
</body>
​
</html>

16. 路由模式hash和history的区别

  1. 路径上:

    1. history模式的路径中没有#, 更好看

      http://localhost:8889/setting
    2. hash模式的路径中有#, 不太好看

      http://localhost:8889/index.html#/setting
  2. 具体实现时

    1. hash模式使用简单,开发和部署不需要做任何额外的处理

    2. history模式,部署的时候需要服务器的支持

  3. 实现原理上

    1. hash模式: 通过监听 window.onhashchange 实现

    2. history模式, 通过history的api实现,主要是history.pushState这个API

  4. 面试的时候不要背和说这一部分:具体实现原理

    1. hash模式: 通过监听 window.onhashchange 实现

      hash值变化的时候不会重写发获取网页内容的请求

      location.hash 获取hash值

      
      <!DOCTYPE html>
      <html lang="en">
      <body>
          <body>
              <a href="#/a">a</a>
              <a href="#/b">b</a>
              <h1>默认</h1>
              <script>
                  window.onhashchange = function(){
                      console.log('最新的hash值', location.hash);   // #/b
                      // console.log(location.hash.slice(2));
                      document.querySelector('h1').innerHTML = location.hash.slice(2)
                  }
              </script>
          </body>
      </body>
      </html>
    2. history模式, 通过history的api实现

      • history.pushState方法接受三个参数,依次为:

        • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。

        • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。

        • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

      • history.pushState(null, '', '/newpath')调用的三个特点

        • 会修改url地址

        • 会往history中推入历史浏览记录

        • 不会向服务器发送网络请求获取网页

      • location.pathname 获取路径

        • 前端代码

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>history原理</title>
          </head>
          <body>
              显示区域:
              <div id="app"></div>
              <a href="javascript:;" onclick="goHistory('/user')">user</a>
              <a href="javascript:;" onclick="goHistory('/goods')">goods</a>
              <script>
                  // 1. 模拟vue中重写pushState(不用管)
                  const bindHistoryEvent = function(type) {
                  const historyEvent = history[type];
                  return function() {
                      const newEvent = historyEvent.apply(this, arguments); //执行history函数
                      const e = new Event(type);  //声明自定义事件
                      e.arguments = arguments;
                      window.dispatchEvent(e);  //抛出事件
                      return newEvent;  //返回方法,用于重写history的方法
                    };
                  };
                  history.pushState =  bindHistoryEvent('pushState');
          ​
          ​
                  // 2. 点击改变路径
                  goHistory = (url)=>{
                      // 改变url地址
                      history.pushState(null, '', url);
                  }
          ​
                  // 3. 监听路径变化的事件,呈现不同的内容
                  window.addEventListener('pushState', function(e) {
                      console.log('THEY DID IT AGAIN! pushState');
                      let path = location.pathname;
                      let text = '';
                      // 判断用户点击哪个界面
                      switch(path){
                          case '/user':
                              text = '用户界面';
                              break;
                          case '/goods':
                              text = '商品界面';
                              break;
                      }
                      document.getElementById('app').innerHTML = text;
                  });
          ​
                  // 4. 当页面加载刷新的时候,取到路径,根据路径,展示不同的内容
                  window.onload = ()=>{
                      let text2 = '';
                      //获取当前的path路径
                      let path = location.pathname;
                      // 判断用户点击哪个界面
                      switch(path){
                          case '/user':
                              text2 = '用户界面';
                              break;
                          case '/goods':
                              text2 = '商品界面';
                              break;
                      }
                      document.getElementById('app').innerHTML = text2;
                  }
              </script>
          </body>
          </html>
        • 服务器代码: 不管访问什么地址都返回该网页

          需要先安装express npm i express
          
          // 1. 导入 express
          const express = require('express')
          const fs = require('fs')
          const path = require('path')
          ​
          // 2. 创建 web 服务器
          const app = express()
          ​
          // 3. 通过服务器实现,不管访问 /  还是 /user  还是 /goods都返回index.html的内容 
          app.get('/', (req, res)=>{
              fs.readFile(path.join(__dirname, './index.html'), 'utf8', (err, data)=>{
                res.send(data)
              })
          })
          ​
          app.get('/user', (req, res)=>{
            fs.readFile(path.join(__dirname, './index.html'), 'utf8', (err, data)=>{
              res.send(data)
            })
          })
          ​
          ​
          app.get('/goods', (req, res)=>{
            fs.readFile(path.join(__dirname, './index.html'), 'utf8', (err, data)=>{
              res.send(data)
            })
          })
          ​
          // 4. 启动 web 服务器
          app.listen(80, () => {
            console.log('express server running at http://127.0.0.1')
          })

17.路由跳转方式有哪些?

1.声明式导航 <router-link to=""></router-link>

<router-link>标签会将路由转成<a>标签,通过点击跳转路由,结构简单使用方便,但局限性很大

2.编程式导航

  • this.$router.push() 是将当前路由压栈后跳转到新的路由,通过回退按钮是回退到栈顶。

  • this.$router.replace() 是直接替换当前路由到新路由,并不会保存历史记录到回退按钮上。

  • this.$router.go() 是指定到历史记录的哪里,例如-1就是返回到上一个历史记录,1就可以表示为前进一步

  • this.$router.back() 是返回到上一个历史记录,通常用作返回按钮。

18. Vue路由传参的方式?

  • query传参(查询字符串)

    /part/?name=cs&age=18
    • 不需要在路由规则定义数据的名字

      const routes = [
          {
              path: '/parts',
              name: 'parts',
              component: Parts
          }
      ]
    • 通过 this.$route.query 获取参数

  • params传参(路径参数)

    /users/cs/18
    • 需要在路由规则中定义数据的名字

      const routes = [
          {
              path: '/parts/:name/:age',
              name: 'parts',
              component: Parts
          }
      ]
    • 通过 this.$route.params 获取参数

19. 常用的插槽和使用方法

  1. 具名插槽:(有名字的插槽) template + v-slot:插槽名

    <Com>
        <template v-slot:title>
            要往title这个坑中填入的内容
        </template>
        <template v-slot:content>
            要往content这个坑中填入的内容
        </template>
    </Com>
  2. 作用域插槽(父组件填充插槽内容时需要使用子组件内部的数据): template + v-slot:插槽名字="变量",变量上就会绑定slot传递过来的的属性和值

    <Com>
        <template v-slot:title="scope">
            {{ scope.title }}
        </template>
        <template #content="scope">
            我是{{ scope.uname }}  我今年 {{ scope.age }}岁 
        </template>
    </Com>

20. 父子组件钩子函数的执行顺序?

  • 创建时生命周期钩子函数的执行顺序

    父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

  • 销毁时生命周期钩子函数的执行顺序

    父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

  • 更新时生命周期钩子函数的执行顺序

    父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

21、如何将获取data中某一个数据的初始状态?

  • 使用this.$options.data().xxx获取初始值

22. 为什么有时候修改了data中的数据,视图中没有发生变化

  • 因为vue在监听数据变化的时候

    • 如果是对象,是在一开始的时候通过Object.defineproperty来劫持对象属性的变化的。

    • 而对于数组而言vue是通过重写数组的方法来监听数组的变化的。

  • 所以,如果我们给对象添加新属性,或者通过中括号+索引的方式 如:this.arr[1] = 'aa' 来修改数组的时候,vue是探测不到的。

  • 这时候我们可以通过$set来解决这个问题。

    export default {
      name: 'App',
      data () {
        return {
          arr: [1, 2, 3]
        }
      },
      created () {
        // 1.1 下面这行代码vue探测不到数组数据的变化  
        this.arr[1] = 'test'
        // 1.2 改成$set之后vue就能探测到数据的变化  this.$set(traget, key, value)
        this.$set(this.arr, 1, 'test')
          
        // 1.3 下面这行代码vue探测不到添加了新属性
        this.age = 18
        // 1.4 改成下面这行代码之后vue就能探测到新属性的添加了
        this.$set(this, 'age', 18)  
      }
    }

23. $nextTick有什么用?$nextTick的原理是怎样的?

nextTick 原理解析 - 掘金

  • $nextTick有什么用?

    • 在数据变化后等待DOM更新完毕再执行回调(或者去做特定的事情,比如操作DOM)等

    • 比如改变数据之后,我们想拿到数据变化后DOM

  • $nextTick的原理是怎样的?

    • 把回调函数通过Promise.then放到微任务中执行

    • 当然浏览器可能不支持Promise,其会按照Promise.then => MutationObserver => setImmediate => setTimeout的顺序,找到一个支持的方式,把回调函数放到异步任务中执行。

24. render函数(或h函数)怎样使用?

渲染函数 & JSX — Vue.js

  1. render函数可以让我们使用js的方式去定义一个vue组件

  2. 对象的render配置项需要配置成一个函数,我们叫render函数

  3. 该函数接收一个形参,为h函数,有时候也写成 createElement是用来创建虚拟节点的

  4. createElemen接收三个参数:第一个参数是标签名, 第二个是属性对象,第三个是标签里嵌套的子节点

    // @returns {VNode}
    createElement(
      // {String | Object | Function}
      // 一个 HTML 标签名、组件选项对象,或者
      // resolve 了上述任何一种的一个 async 函数。必填项。
      'div',
    ​
      // {Object}
      // 一个与模板中 attribute 对应的数据对象。可选。
      {
        // (详情见下一节)
      },
    ​
      // {String | Array}
      // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
      // 也可以使用字符串来生成“文本虚拟节点”。可选。
      [
        '先写一些文字',
        createElement('h1', '一则头条'),
        createElement(MyComponent, {
          props: {
            someProp: 'foobar'
          }
        })
      ]
    )

举几个例子:

  1. 案例1:

    • 要渲染如下结构

      // Hello.vue
      <template>
          <h1 title="a">abc</h1>
      </template>
      import Hello from 'Hello.vue'
      ​
      Vue.component('Hello', Hello)
    • 使用h函数

      // Hello.js
      export default {
        render: function (h) {  
          return h('h1', {title:'a'}, 'abc')
        }
      }
      import Hello from 'Hello.js'
      ​
      Vue.component('Hello', Hello)
  2. 案例2:

    • 要渲染如下结构

      // Hello.vue
      <template>
          <ul title="abc">
              <li>a</li>
              <li>b</li>
              <li>c</li>
          </ul>
      </template>
      import Hello from 'Hello.vue'
      ​
      Vue.component('Hello', Hello)
    • 使用h函数

      // Hello.js
      export default {
          return h('ul', {title:'abc'}, [
              h('li', null, 'a'),
              h('li', null, 'b'),
              h('li', null, 'c'),
          ])
      }
      import Hello from 'Hello.js'
      ​
      Vue.component('Hello', Hello)
  3. 案例3:

    • 要实现如下效果

    • main.js中

      Vue.component('anchored-heading', {
        render: function (createElement) {
          // createElement 非常重要是用来创建虚拟节点的方法  
          return createElement(
            'h' + this.level,   // 标签名称
            this.$slots.default // 子节点数组
          )
        },
        props: {
          level: {
            type: Number,
            required: true
          }
        }
      })
  • App.vue中

    <template>
      <div>
        <anchored-heading level="1">hello,h1</anchored-heading>
        <anchored-heading level="2">hello,h2</anchored-heading>
      </div>
    </template>

25. 数据很多有上万项,需要渲染怎么办?

 四、Vue3+Ts

1. vue3和vue2有什么区别?

  1. Vue3支持大多数的Vue2的特性

  2. vue3中设计了一套强大的组合APi代替了vue2中的选项API, 复用性更强了

  3. vue3更好的支持TS

  4. vue3中使用了Proxy配合 Reflect 代替了 Vue2 中 Object.defineProperty()方法实现数据的响应式(数据代理)

  5. vue3中重写了虚拟DOM,速度更快了

  6. 设计了一个新的脚手架工具Vite,速度比webpack更快(使用浏览器原生的方式编译)

  7. 重写了虚拟DOM,速度更快了

  8. 生命周期钩子函数也发生了变化beforeDestroy变成了beforeUnmount, destroyed变成了unMounted

  9. vue3支持按需使用,你用到哪些,就只把vue3中哪些代码进行打包,但是vue2中会把整个vue.js打包进去。

2. vue3中的生命周期钩子函数相比vue2发生了什么变化?

  1. vue3中依然支持选型式的生命周期钩子函数,只是beforeDestroy变成了beforeUnmount, destroyed变成了unMounted

  2. vue3中提供了组合式的生命周期钩子函数,没有了beforeCreate和created,变成了setup。 组合式的生命周期钩子函数都是以on开头,比如说onMouted()就是挂载后

3. vue3中的响应式和vue2中的响应式有什么不同?为什么要这样做?

  • vue3中使用了Proxy配合 Reflect 代替了 Vue2 中 Object.defineProperty()方法实现数据的响应式(数据代理)

  • 为什么要这样做?

    • Object.defineProperty 需要递归遍历劫持所有属性的变化

    • Object.defineProperty 无法劫持数组的变化,因此在vue2中还需要额外重写数组的方法,实现对数组的劫持

    • 如果添加了新的属性,需要使用Object.defineProperty重新劫持

      使用Proxy则完美避开了所有这些缺点

4. TypeScript中有哪些数据类型?

  • 原始类型: number/string/boolean/null/undefined/symbol

    let age: number = 18
  • 数组类型

    let arr: string[] = ['a', 'b', 'c']
  • 联合类型

    let arr: (number | string)[] = [1, 'a', 3, 'b']
  • 函数

    function add(num1: number, num2: number): number {
      return num1 + num2
    }
    // 1. 定义类型别名
    type AddFn = (num1:number, num2: number) => number
    ​
    // 2. 使用自定义类型AddFn作为函数 add 的类型
    const add: AddFn = (num1, num2) => {
      return num1 + num2
    }
  • 对象

    let person: { name: string } = { name: '程序员'}
  • 接口

    interface IPerson {
      name: string
      sayHi(): void
    }
    ​
    let person: IPerson = {
      name: 'jack',
      sayHi() {}
    }

  • 交叉类型

    // 使用 type 自定义类型来模拟 Point2D 和 Point3D
    type Point2D = {
      x: number
      y: number
    }
    
    // 使用 交叉类型 来实现接口继承的功能:
    // 使用 交叉类型 后,Point3D => { x: number; y: number; z: number }
    // 也就是同时具有了 Point2D 和 后面对象 的所有的属性了
    type Point3D = Point2D & {
      z: number
    }
    ​
    let o: Point3D = {
      x: 1,
      y: 2,
      z: 3
    }
  • 元组类型

    // 该示例中,元素有两个元素,每个元素的类型都是 number
    let position: [number, number] = [116.2317, 39.5427]
  • 字面量类型

    type Direction = 'up' | 'down' | 'left' | 'right'
  • 枚举类型

    enum Direction { Up, Down, Left, Right }
  • 任意(any)类型

    let obj: any = { x: 0 }
  • 泛型

    // 1. 定义泛型函数
    function id<T>(value: T): T { return value }
    // 2. 调用泛型函数
    // 函数参数和返回值类型都为:number
    const num = id<number>(10)
    ​
    // 函数参数和返回值类型都为:string
    const str = id<string>('a')

  五、项目加其他

1. webpack是什么?具体有哪些作用?loader和plugin分别是干嘛的?列举几个你用到的plugin? webpack的工作流程是怎样的?

  1. webpack 是一个模块打包工具,是node.js中的一个第三方包

  2. 具体来说有以下4点作用:

    1. 语法降级:如ES6/7/8 => ES5

    2. 翻译:如less/sacc => css

    3. 压缩代码:如除去空格换行

    4. 合并:多个文件合成一个文件

  3. loader和plugin分别是干嘛的?

    1. loader是加载器:把原本不支持的模块转换为webpack支持的模块

    2. plugin是插件: 用于扩张webpack 的功能,在webpack构建生命周期的过程中,在合适的时机做了合适的事情。

  4. 列举几个你用到的plugin?

    HtmlWebpackPlugin 输出html内容的插件

    ESLintWebpackPlugin ESlint语法检查的插件

    CopyPlugin 拷贝public的内容到dist目录下的插件

  5. webpack的工作流程是怎样的?

    webpack 是一种模块打包工具,可以将各类型的资源,例如图片、CSS、JS 等,转译组合为 JS 格式的 bundle 文件。

    webpack 构建的核心任务是完成内容转化和资源合并。主要包含以下 3 个阶段:

    1. 初始化阶段 ① 初始化参数:从配置文件、配置对象和 Shell 参数中读取并与默认参数进行合并,组合成最终使用的参数。 ② 创建编译对象:用上一步得到的参数创建 Compiler 对象。 ③ 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等。

    2. 构建阶段 ① 开始编译:执行 Compiler 对象的 run 方法,创建 Compilation 对象。 ② 确认编译入口:进入 entryOption 阶段,读取配置的 Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。 ③ 编译模块(make) : 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转 化为 AST 对象,然后递归分析依赖,依次处理全部文件。 ④ 完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图。

    3. 生成阶段 ① 输出资源(seal) :根据入口和模块之间的依赖关系,组装成多个包含多个模块的Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输 出内容的最后机会。 ② 写入文件系统(emitAssets) :确定好输出内容后,根据配置的 output 将内容写入文件系统也就是磁盘上。

2. 谈谈你平时都用了哪些方法进行性能优化?

  1. 配置webapck的externals排除体积比较大的包,然后通过cdn外链的方式引入

  2. 路由懒加载

  3. 动态导入

  4. 减少http请求次数

  5. 打包压缩上线代码

  6. 图片懒加载

    原理

    1. 页面加载完后,img标签没有src属性,真实的图片路径存在另外的属性如data-src属性中, 所以默认并不会发送获取图片的网络请求。

      <img data-src="aaa.jpg"/>
    2. 当js探测到img标签出现在可视区域的时候,拿到data-src属性的属性值,作为src属性的属性值,这时候才会发送获取图片的网络请求。

      <img data-src="aaa.jpg" src="aaa.jpg"/>
  7. 使用雪碧图

  8. vue路由懒加载

  9. CDN加载包。

    各地搞很多的服务器,不同地方的人访问的时候,就找到最近的服务器来下载资源,如下方bootstrap官网上访问的外链资源地址中有cdn,就应该是部署在专门的cdn服务器上的,这种资源访问起来就会比较快。

3 安全方面的CSRF攻击 和 XSS攻击(了解)

  1. 什么是CSRF?

    跨站请求伪造 Cross-site request forgery

    用户同时在浏览器上打开了AB两个网站

    A银行网站已登录,登录信息记录在cookie中

    B网站 通过表单给A银行网站发起转账请求。由于A网站事先已登录 钱被转走。

  2. 什么是XSS

    跨站脚本攻击 Cross-site scripting

    A银行网站登录,攻击者在A网站内嵌套一段script脚本,用户访问A网站嵌套了脚本的网页的的时候,执行script脚本代码,钱被转走。

  3. CSRF和XSS有什么区别

    XSS利用的是用户对指定网页的信任,CSRF利用的是网站对用户网页浏览器的信任。

  4. 如何避免CSRF攻击

    方法一、Token 验证:(用的最多)

    1. 服务器发送给客户端一个token

    2. 客户端提交的表单中带着这个token

    3. 如果这个 token 不合法,那么服务器拒绝这个请求。

    方法二:隐藏令牌:

    token 隐藏在 httphead头中。

    方法二和方法一有点像,本质上没有太大区别,只是使用方式上有区别。

    方法三、Referer 验证:

    Referer 指的是页面请求来源。意思是,只接受本站的请求,服务器才做响应;如果不是,就拦截

  5. 如何避免XSS攻击

    1. 编码

    对用户输入的数据进行HTML Entity编码。

    如上图所示,把字符转换成 转义字符。

    Encode的作用是将$var`等一些字符进行转化,使得浏览器在最终输出结果上是一样的。

    比如说这段代码:

    <script>alert(1)</script>

    若不进行任何处理,则浏览器会执行alert的js操作,实现XSS注入。进行编码处理之后,L在浏览器中的显示结果就是<script>alert(1)</script>,实现了将`$var作为纯文本进行输出,且不引起JavaScript的执行。

    2、过滤:

    • 移除用户输入的和事件相关的属性。如onerror可以自动触发攻击,还有onclick等。(总而言是,过滤掉一些不安全的内容)

    • 移除用户输入的Style节点、Script节点、Iframe节点。(尤其是Script节点,它可是支持跨域的呀,一定要移除)。

    3、校正

    • 避免直接对HTML Entity进行解码。

    • 使用DOM Parse转换,校正不配对的DOM标签。

    备注:我们应该去了解一下DOM Parse这个概念,它的作用是把文本解析成DOM结构。

    比较常用的做法是,通过第一步的编码转成文本,然后第三步转成DOM对象,然后经过第二步的过滤。

4. 这个人资/商城/...管理系统中权限是怎样做的?

  1. 我们采用了基于角色的权限设计思想,具体来说就是给用户分配角色,给角色分配权限,用户就间接拥有角色上的所有权限。

  2. 总的来说分为两大模块:权限基本信息维护和权限控制,具体来说

    1. 权限基本信息维护包含:用户管理、权限管理、角色管理、为用户分配角色, 为角色分配权限

    2. 权限控制包含,菜单路由权限的控制 和 按钮权限的控制

  3. 具体实现上:

    1. 权限基本信息维护没有什么好说的,就是一些增删改查的操作

    2. 我重点说说权限控制吧

      1. 先说路由权限的控制,在实现路由权限控制的时候

        1. 我们首先在路由守卫中调用接口获取用户信息,用户信息身上会有权限标识数组

        2. 然后我们会利用所有动态路由数组,和刚刚拿到的权限标识数组,得到当前用户能访问的动态路由数组

        3. 我们会把 用户能访问的动态路由数组 丢到vuex里面,基于vuex中的数据,渲染出后台主页左侧的导航菜单

        4. 最后我们还会通过 router.addRoutes 把 用户能访问的动态路由数组 添加到路由对象中,这样用户通过导航菜单访问的时候就不是空白页

      2. 再说按钮权限的控制,在实现按钮权限控制的时候:

        1. 我们首先会在混入mixins中定义一个方法,该方法接收特定权限标识,比如添加用户就可以约定是 add_users, 在函数内我们会去权限标识数组中查找有无该权限标识,如果有返回true, 如果没有返回false

        2. 之后我们通过Vue.mixin()对该混入做全局混入,这样每个组件内都可以调用该方法了

        3. 最后我们在模板template中通过调用该方法判断有无权限,如果没有权限就隐藏按钮

      3. 当然还要提一嘴的是,接口权限控制是由后台控制的,如果我没有新增用户的权限,去调用新增用户的接口的时候,接口就应该返回一个403没有权限的状态码

      面试官你好,这就是我们这个后台管理项目中权限的做法

5. 这个人资/商城/...管理系统中上传图片的组件是怎样封装的?

  • 首先我先说,为什么要封装组件?

    在管理系统中,多处都有的处理,为了实现代码的复用,我们把对头像的处理封装成了一个单独的组件

  • 然后我说说封装组件达到了什么样的目的?

    • 父组件使用子组件的时候会传入一个图片地址,或者不传入,如果传入的话,子组件会利用父组件传过来的图片地址,把图片显示出来

    • 把图片通过腾讯云cos的sdk上传到腾讯云服务器

    • 子组件拿到上传之后的地址会通过子传父告诉父组件

  • 最后我来讲讲具体怎样实现的?

    • 父传子子传父我们使用了v-model的指令, 子组件通过vlaue属性接收父组件传过来的默认图片地址,当图片上传完成后,通过$emit触发input的自定义事件,把上传之后的图片地址,传给父组件,让父组件去使用。

    • 实现图片上传的时候,我们主要对element的el-upload组件进行了二次封装

      下图是el-upload支持的属性,先不要说,问的时候再说

    • 图片上传我们是通过腾讯云cos的sdk,然后参考文档实现的,具体的步骤是

      1. 安装cos-js-sdk-v5的sdk包

        npm i cos-js-sdk-v5 --save
      2. 导入和创建COS核心对象

        import COS from 'cos-js-sdk-v5'
        // SECRETID 和 SECRETKEY请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理
        
        const cos = new COS({
          SecretId: 'AKIDqZ4OVO2SeaMuCF3buxlIacOuCH9mAJDs',
          SecretKey: 'UjjV2KNAk5A0bx4xc2D9qJXWGl23pRkn'
        })
      3. 调用cos.putObject完成图片的上传

        cos.putObject({
            Bucket: 'examplebucket-1250000000', /* 填入您自己的存储桶,必须字段 */
            Region: 'COS_REGION',  /* 存储桶所在地域,例如ap-beijing,必须字段 */
            Key: '1.jpg',  /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),必须字段 */
            StorageClass: 'STANDARD',
            Body: fileObject, // 上传文件对象
            onProgress: function(progressData) {
                console.log(JSON.stringify(progressData));
            }
        }, function(err, data) {
            console.log(err || data);
        });

  • 你好面试官,图片上传的组件我大概就是这样封装出来的。

6. 项目中解决了从详情返回列表页的时候停留在原点的问题,说说你是怎样解决的?

  1. 使用keep-alive缓存列表页组件。

  2. 监听列表页组件中滚动的盒子的滚动条滚动事件scroll,把滚动条滚动的距离存储起来(比如可以存储到某个DOM元素的属性中)。

  3. 在列表页组件的重新激活的钩子函数activated中,拿到之前存储的滚动的距离,设置为滚动的盒子的scrollTop属性属性值,这样问题就解决了。

7. 从浏览器地址栏输入 url 按回车后发生了什么

  1. 浏览器首先会从URL地址中解析出协议、主机、端口、路径等信息并构造一个 HTTP 请求。

  2. DNS域名解析,把域名翻译成IP地址。 (字节面试被虐后,是时候搞懂 DNS 了

  3. 浏览器和服务器建立TCP 连接。

    总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。

  4. 浏览器基于TCP协议发送HTTP请求报文。

  5. 服务器处理请求并返回HTTP 报文。

  6. 浏览器和服务器断开TCP 连接。

  7. 浏览器渲染页面。

8 浏览器的渲染机制是怎样的?

参考:你真的了解回流和重绘吗 - 掘金

浏览器渲染过程如下分为5步:

注意:回流和重排是一个意思 回流===重排

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树

  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)

  3. Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)

  4. Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素

  5. Display: 将像素发送给GPU,展示在页面上。

9 什么是重排?什么是重绘?什么情况下会导致重排?如何减少重排重绘?

参考:

你真的了解重排和重绘吗?_panel的backcolor属性导致持续重绘-CSDN博客

你真的了解回流和重绘吗 - 掘金

面试官问的时候可能会这样问:

问:修改盒子的宽度和修改盒子的背景颜色哪个更消耗性能

答:修改盒子的宽度?

问:为什么?

答:因为修改盒子的宽度属于修改几何属性会导致重排,而修改背景演示属于非几何属性的修改,只会导致重绘

什么是重排?什么是重绘?

  • 重排Layout:当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。

  • 重绘Painting:完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。

  • 重排一定会导致重绘,重绘不一定需要重排。

哪些操作会导致重绘和重排?

  • 添加或删除可见的DOM元素

  • 元素的位置发生变化

  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。

  • 页面一开始渲染的时候(这肯定避免不了)

  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

  • 以及获取如下属性的时候

    1. offsetTop、offsetLeft、offsetWidth、offsetHeight

    2. scrollTop、scrollLeft、scrollWidth、scrollHeight

    3. clientTop、clientLeft、clientWidth、clientHeight

    4. getComputedStyle()

    5. getBoundingClientRect

如何减少重排和重绘?

  • 最小化重绘和重排,比如样式集中改变,使用添加新样式类名 .classcssText

  • 批量操作 DOM

    • 隐藏元素,应用修改,重新显示

    • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。

    • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

  • 对于复杂动画效果,使用绝对定位让其脱离文档流,这在制作复杂的动画时对性能的影响比较明显。

10. 说说你对浏览器缓存的认识?

参考+必读:浏览器缓存 - 掘金

  1. 什么是缓存?

    • 简单点说就是:第一次访问服务器之后,浏览器把服务器返回的结果存储起来

    • 下次再访问的时候,直接从缓存中取,就不需要再请求服务器了。

  1. 为什么需要缓存?

    • 减少网络请求,降低了服务器的压力

    • 优化访问资源的速度,提升了用户体验。

  1. 哪些资源可以被缓存?

    • 一段事件内不会变化的服务器资源,比如静态资源js css img。

  2. 浏览器缓存的两种类型?

    • 强缓存

    • 协商缓存

  3. 缓存的优先级是怎样的?

    • 先走强缓存,强缓存无效,再走协商缓存

  4. 强缓存

    主要是看 response headers 中的 Cache-Control 的值,如果max-age=60,就是说在60秒内,都直接使用缓存,超过了就继续请求服务器

    Cache-Control所有取值: Cache-Control - HTTP | MDN

    Cache-Control的取值包含:

    • public: 表明响应可以被任何对象(包括:浏览器,代理服务器,等等)缓存。

    • private: 表明响应只能被用户本地浏览器缓存。

    • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。

    • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。

    • max-age: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。

    • must-revalidate: 一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。

    Cache-Control示例:

    • 禁止缓存:发送如下响应头可以关闭缓存。

      Cache-Control: no-store
    • 缓存静态资源:对于应用程序中不会改变的文件,你通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,CSS文件和JavaScript文件。

      Cache-Control:public, max-age=31536000
    • 需要重新验证

      指定 no-cachemax-age=0, must-revalidate 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。

      Cache-Control: no-cache
      xxxxxxxxxx Cache-Control: max-age=0, must-revalidate	
  5. 协商缓存

    强缓存没有命中就会走协商缓存

    协商缓存触发条件如下:

    1. Cache-Control 的值不能是no-store(既不强缓存,也不协商缓存)

    2. Cache-Control 的值为 no-cache (不强缓存)或者 过去的秒数超过max-age(强缓存,但总有过期的时候)

    协商缓存也有响应头和请求头决定

    1. 第一次请求的时候,服务器会返回资源,并在响应头中携带一个 etaglast-modified,

      etag是资源标识(如 51586dff-686),last-modified是资源在服务器上的最近一次修改的时间(如 Sun, 31 Mar 2013 17:10:23 GMT

    2. 第二次请求的时候该资源的时候,浏览器会自动在请求头中,携带etaglast-modified,

      只是名字变了etag -> if-none-match (如 51586dff-686)和last-modified -> if-modified-since(如 Sun, 31 Mar 2013 17:10:23 GMT

      服务器接收到请求后,会根据这两个请求头判断资源有无更新

      如果资源没有更新过,这时候服务器会返回 304的状态码,浏览器知道服务器上资源没有更新过,因此就直接去缓存中取出资源数据使用。

      如果有更新,这时候会返回200状态码新的资源etaglast-modified,浏览器会重新缓存数据。

    概况来说

    1. 请求资源时,把用户本地该资源的 ETag 同时带到服务端,服务端和最新资源做对比。

    2. 如果资源没更改,返回304,浏览器读取本地缓存。

    3. 如果资源有更改,返回200,返回最新的资源。

  6. 其他:

    1. 命中显示

      1. 从服务器获取新的资源

      2. 命中强缓存,且资源没过期,直接读取本地缓存

      3. 命中协商缓存,且资源未更改,读取本地缓存

        注意:协商缓存无论如果,都要向服务端发请求的,只不过,资源未更改时,返回的只是header信息,所以size很小;而资源有更改时,还要返回body数据,所以size会大。

    2. 为什么有了last-modified 还需要 etag

      你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现(也就是说,ETag是新增的,为了解决之前只有If-Modified的缺点)主要是为了解决几个Last-Modified比较难解决的问题:

      • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

      • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

      • 某些服务器不能精确的得到文件的最后修改时间

    3. 强缓存和协商缓存的区别

    4. 用户行为对缓存的影响

    5. 项目中如何应用缓存?

      比如 vue 项目,脚手架已经将更改的文件做 hash 处理了,因此一般的 js、css 文件不需要我们再去操作。

      而对于 index.html,我们需要在 nginx 上做 no-store 处理,即完全不缓存 index.html,每次都请求最新的html。。。因为 html 中会外链 css、js,如果我 html 还是走的缓存,那链接的还是老的 css 啊,想想???

  7. 缓存的流程图

深入理解缓存: 掘金小册

11. 如何判断数据类型?

  1. typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object

    console.log(typeof undefined); // undefined
    console.log(typeof 2); // number
    console.log(typeof true); // boolean
    console.log(typeof "str"); // string
    console.log(typeof Symbol("foo")); // symbol
    console.log(typeof 2172141653n); // bigint
    console.log(typeof function () {}); // function
    // 不能判别
    console.log(typeof []); // object
    console.log(typeof {}); // object
    console.log(typeof null); // object
  2. instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:

    其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype 即为 true 。比如这里的 vortesnail 作为实例,顺着原型链能找到 Student.prototypePeople.prototype ,所以都为 true

    class People {}
    class Student extends People {}
    
    const vortesnail = new Student();
    
    console.log(vortesnail instanceof People); // true
    console.log(vortesnail instanceof Student); // true
  3. Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。

    Object.prototype.toString.call(2); // "[object Number]"
    Object.prototype.toString.call(""); // "[object String]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(Math); // "[object Math]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(function () {}); // "[object Function]"

12. 在面试中有一个经常被问的问题就是:如何判断变量是否为数组?

var arr = [1, 2, 3]
// 1. Array.isArray
Array.isArray(arr); // true

// 2. instanceof
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true

// 3. Object.prototype.toString.call
Object.prototype.toString.call(arr); // "[object Array]"

13. HTTP和HTTPS的区别

  • HTTPS = SSL(加密协议) + HTTP

  • 区别:

    • HTTP是明文传输的,不安全。

    • HTTPS是会加密传输,是安全的。

14. 说说小程序微信登录的流程?

  1. 调用wx.login得到一个code

  2. 利用code调用后台接口,得到token

  3. 把token存储起来方便下一次使用

15. 说说小程序微信支付的流程是怎样的?

  1. 客户端调用创建订单的接口,拿到订单id

  2. 利用订单id,调用预支付的接口,得到支付的相关参数

  3. 利用支付相关参数调用wx.requestPayment在微信客服端发起微信支付

  4. 利用订单id,调用检查订单是否支付成功的接口,实现了闭环

16. 发请求想中断怎么办?

  • 原生的xhr中可以通过 .abort中断请求的发送

    const xhr = new XMLHttpRequest()
    xhr.open('get', 'http://127.0.0.1:3000/get', true)
    xhr.send()
    // 通过调用xhr的abort方法取消请求的发送
    xhr.abort()
  • axios中可以通过cancelToken取消请求的发送,但本质上还是通过调用xhr的abort实现

    import axios from 'axios'
    
    let cancel
    axios({
        method: 'GET',
        url: 'http://127.0.0.1:3000/get',
        cancelToken: new axios.CancelToken(function executor (c) {
            // executor 函数接收一个 cancel 函数作为参数
            cancel = c
        })
    })
    // 取消发送网络请求
    cancel('取消请求发送')

17. 数字精度怎么处理?

解决JS数字精度丢失 - 掘金

JS精度问题-CSDN博客

  • 什么是数字精度问题?

    我们在计算0.1 + 0.2的结果并不是0.3,而是0.30000000000000004这种现象就称为数字精度问题

  • 为什么会出现数字精度问题?

    因为js采用IEEE 754的双精度标准进行数字存储,这是一种64位双精度浮点数储存方法,会出现精度的丢失

  • 解决这个问题的思路:

    把小数转换成整数,再计算就不会出问题

  • 具体项目中怎么做的?

    • 写一个自己的用来计算加减乘除的方法, 如下面就写了一个加法

      function add(num1, num2){
        let r1, r2, m;
        r1 = num1.toString().split('.')[1].length
        r2 = num2.toString().split('.')[1].length
        m = Math.pow(10, Math.max(r1, r2))
        return (num1 * m + num2 * m) / m
      }
      
      // 以后计算 0.1 + 0.2 的时候就可以写成
      add(0.1, 0.2)
    • 或者使用第三方包 decimal.js

      decimal.js API

      0.1 + 0.2                                // 0.30000000000000004
      x = new Decimal(0.1)
      y = x.plus(0.2)                          // '0.3'
      new Decimal(0.7).plus(x).plus(y)         // '1.1'

18. 说说css的权重

  • 选择器权重表如下:

    选择器选择器权重
    继承 或 *0, 0, 0, 0
    元素选择器0, 0, 0, 1
    类选择器,伪类选择器0, 0, 1, 0
    ID选择器0, 1, 0, 0
    行内样式 style1, 0, 0, 0
    !important无穷大
  • 使用选择器时的注意事项

    1. 权重是有4组数字组成,但是不会有进位。

    2. 可以理解为类选择器永远大于元素选择器, id选择器永远大于类选择器,以此类推..

    3. 等级判断从左向右,如果某一位数值相同,则判断下一位数值。

    4. 可以简单记忆法: 通配符和继承权重为0, 标签选择器为1,类(伪类)选择器为 10, id选择器 100, 行内样式表为 1000, !important 无穷大.

    5. 继承的权重是0, 如果该元素没有直接选中,不管父元素权重多高,子元素得到的权重都是 0。

19. 做过哪些性能优化,是怎样处理的?

前端性能优化 24 条建议(2020) - 掘金

写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践 - 掘金

  • 减少http请求次数

  • 使用HTTP2.0

  • 使用服务端渲染SSR

  • 静态资源使用cdn

  • 使用缓存

  • 使用字体图标和精灵图

  • 图片懒加载

  • 路由懒加载

  • 减少重排重绘

  • 事件委托

  • css放头部js放尾部

  • 不要优化过渡,不能为了优化而优化

20. 列表项有上万项,怎么办?

  • 会导致什么问题?页面上DOM元素太多

  • 使用长列表技术:原理:只渲染可视区域内的DOM

  • 比如在vue中可以使用 vue-virtual-scroller 实现长列表

21. mixins有没有了解,在你做的项目中如何应用的?

Vue — 详解mixins混入使用 - 掘金

  • mixins是混入,是一种分发 Vue 组件中可复用功能的非常灵活的方式

  • mixins是一个js对象,它可以包含我们组件中script项中的任意功能选项,如data、components、methods 、created、computed等。我们只要将共用的功能以对象的方式传入mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来,这样就可以提高代码的重用性,使你的代码保持干净和易于维护。

22. null和undfined有什么区别?

深入探究:null 和 undefined 究竟有何区别? - 掘金

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

  • undefined

    • 这个变量从根本上就没有定义

    • 隐藏式 空值

  • null

    • 这个值虽然定义了,但它并未指向任何内存中的对象

    • 声明式 空值

23. $set有什么用?

  • 有时候我们修改组件内部data中的数据,但是发现vue探测不到数据的变化,视图上的内容没有跟着改变,这时候我们就可以使用$set来解决这个问题

    举例:见注释

    export default {
      name: 'App',
      data () {
        return {
          arr: [1, 2, 3]
        }
      },
      created () {
        // 1.1 下面这行代码vue探测不到数组数据的变化  
        this.arr[1] = 'test'
        // 1.2 改成$set之后vue就能探测到数据的变化  this.$set(traget, key, value)
        this.$set(this.arr, 1, 'test')
          
        // 1.3 下面这行代码vue探测不到添加了新属性
        this.age = 18
        // 1.4 改成下面这行代码之后vue就能探测到新属性的添加了
        this.$set(this, 'age', 18)  
      }
    }

24. 外边距重叠怎么解决?

【面试题解】什么是外边距重叠?如何解决?什么是BFC? - 掘金

25. $attrs 的作用

vue $attrs和$listeners的使用 - 掘金

26. 为什么vue组件样式不会重叠覆盖呢?scoped的原理

你知道 Vue scoped 原理吗?这波你在第几层? - 掘金

27. less和sass的区别

sass、less、stylus的区别 - 掘金

  1. 变量符不一样: SASS 变量符是 $, LESS 变量符是 @

  2. 变量的作用域也不一样

  3. 输出设置不同, Sass提供4中输出选项:nested, compact, compressedexpanded。可以选择。 Less没有输出设置

  4. 支持语句不同, Sass支持条件语句,可以使用if{}else{},for{}循环等等。Less不支持。

28.小程序的生命周期?

1.应用生命周期

  • 用户首次打开小程序,触发 onLaunch(全局只触发一次)。

  • 小程序初始化完成后,触发onShow方法,监听小程序显示。

  • 小程序从前台进入后台,触发 onHide方法。

  • 小程序从后台进入前台显示,触发 onShow方法。

  • 小程序后台运行一定时间,或系统资源占用过高,会被销毁。

    App(Object object) | 微信开放文档

    属性类型默认值必填说明最低版本
    onLaunchfunction生命周期回调——监听小程序初始化。
    onShowfunction生命周期回调——监听小程序启动或切前台。
    onHidefunction生命周期回调——监听小程序切后台。

2.页面生命周期

  • 小程序注册完成后,加载页面,触发onLoad方法。

  • 页面载入后触发onShow方法,显示页面。

  • 首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次。

  • 当小程序后台运行或跳转到其他页面(使用wx.navigateTo)时,触发onHide方法。

  • 当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。

  • 当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload

    属性类型默认值必填说明
    onLoadfunction生命周期回调—监听页面加载
    onShowfunction生命周期回调—监听页面显示
    onReadyfunction生命周期回调—监听页面初次渲染完成
    onHidefunction生命周期回调—监听页面隐藏
    onUnloadfunction生命周期回调—监听页面卸载
  1. 组件生命周期

    组件生命周期 | 微信开放文档

    生命周期参数描述最低版本
    created在组件实例刚刚被创建时执行1.6.3
    attached在组件实例进入页面节点树时执行1.6.3
    ready在组件在视图层布局完成后执行1.6.3
    moved在组件实例被移动到节点树另一个位置时执行1.6.3
    detached在组件实例被从页面节点树移除时执行1.6.3
    errorObject Error每当组件方法抛出错误时执行

29. 侧边栏收缩时,Echarts图表如何自适应?

  1. 关键就是,Echarts实例所在div盒子大小发生变化的时候,需要调用Echarts实例的resize方法

  2. 我们可以new一个ResizeObserver对象,调用其的observe方法监听div盒子大小的变化,div盒子大小变化的时候就会调用ResizeObserver对象的回调函数,里面我们调用Echarts实例的resize方法,就可以实现其自适应了

    // 假设该div就是echarts实例所在的div
    const div = document.querySelector('div')
    
    // 创建ResizeObserver实例对象
    const resizeObserver = new ResizeObserver(entries => {
        // div大小发生变化的时候回回调的函数
        console.count("resize变化啦");
        // 调用Echarts实例的resize方法
        myEcharts.resize()
    });
    
    // 使用ResizeObserver实例对象监听div大小尺寸的变化
    resizeObserver.observe(div);

30. 贵公司使用git的工作流程是怎样的?

常见 git 工作流程 - 知乎

  1. 当组长给我分配某个功能的开发任务之后,我会基于开发的主分支,创建一个功能分支

  2. 在功能分支上完成开发,就把功能分支推送到远程仓库,并告诉组长

  3. 组长会切换查看该功能分支的代码(进行代码审核)

  4. 如果有问题,就让你修改,在重新推送,组长再重新拉取

  5. 如果没问题,组长就会把功能分支合并到开发的主分支上,本次开发就完成了。

31. 遇到冲突如何解决

  1. 和别人进行协商,根据实际的业务需求,决定保留哪些代码

  2. 删除冲突的时候产生的特殊符号,并根据需要适当修改代码

  3. 重新提交推送

32. 你使用uni-app开发过app吗?如何进行uni-app兼容多端的开发

  1. 我没有搞过,因为公司里面没有这样的需求,我们公司只要求使用它开发小程序

  2. 但是我是知道怎样搞的,实际上我们只需要写一套代码,打包成各种应用比如小程序,h5和app即可

  3. 只是在打包的过程中会遇到兼容性的问题,使用 #ifdef和 #ifndef的语法,根据不同的环境写不同的代码即可。

具体如何进行兼容性的开发

<template>
	<view class="content">
    <!-- #ifdef MP-WEIXIN -->
    <view @click="sayHello">我是小程序</view>
    <!-- #endif -->
    <!-- #ifndef MP-WEIXIN -->
    <div @click="sayHello">我是网页</div>
    <!-- #endif -->
	</view>
</template>

<script>
	export default {
		methods: {
          sayHello(){
            // #ifdef MP-WEIXIN
            console.log('我是小程序');
            // #endif
            // #ifdef H5
            alert('我是网页')
            // #endif
          }
		}
	}
</script>

33. npm和npx有什么区别

npx 有什么作用跟意义?为什么要有 npx?什么场景使用? - 掘金

  1. npx 可以直接执行二进制文件,但是npm不可以需要在script中配置了命令之后,才可以间接的执行。

  2. npx 可以采用临时安装,用完后移除,让你使用的包保持最新版本。npm使用第三方包不是临时安装。

  3. npx在某些场景下,还可以灵活的切换使用的node的版本

34. vue和react的区别时什么

  1. 共同点:vue和react是目前最流行的两个前端框架

  2. 不同:

    1. vue是一个框架,在开发的时候,我们需要遵循其规范来开发,比如循环渲染就是v-for,你只能使用v-for。所以在开发的时候我们需要遵守其规定的语法格式来写代码。

    2. 而react更准确点说是一个构建界面的js库,并不限定你用什么语法,使用起来要更灵活。

35. 如何封装一个把扁形数组转成树型数组的函数?(亮点、代码性能优化)

面试的时候可以这么说:

  1. 在实现管理后台组织架构管理的时候,组织架构是一个树型结构,el-tree组件要求传入的是树型数组,但是后台返回给我的是一个扁平的数组,因此我需要把偏平数组转成树型数组。

  2. 首先,我是使用递归封装了一个把扁形数组转成树型数组的函数,但是我发现他的执行效率低下,for循环内部的代码执行次数较多,随便一个简单的数据都要执行200多次。

  3. 之后,我想了一段时间,想出使用对象或Map来进行优化,经过优化处理之后,循环里的代码只执行了20多次,效率得到了大大的提高。

[
    { id: 1, name: '总裁办', pid: '' },
    { id: 11, name: '秘书处', pid: 1 },
    { id: 2, name: 'IT部', pid: '' }
]
[
    { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处', pid: 1 }] },
    { id: 2, name: 'IT部', pid: '' }
]
  • 递归:

    // 得到指pid下所有子对象组成的新数组
    function tranListToTreeData(list, rootValue) {
        const arr = []
        list.forEach(item => {
            if (item.pid === rootValue) {
                // 递归调用
                const children = tranListToTreeData(
                    list, item.id)
                if (children.length) {
                    item.children = children
                }
                arr.push(item)
            }
        });
        return arr
    }
    
    // 测试代码:
    var arr = [
        { id: 1, name: '总裁办', pid: '' },
        { id: 11, name: '秘书处', pid: 1 },
        { id: 2, name: 'IT部', pid: '' }
    ]
    
    tranListToTreeData(arr, 11)  // []
    tranListToTreeData(arr, 1)   // [{id: 11, name: '秘书处', pid: 1}]
    tranListToTreeData(arr, '')
    // [ 
    //  {id: 1, name:'总裁办', pid: '', children:[{ id: 11, name: '秘书处', pid: 1 }] },
    //  { id: 2, name: 'IT部', pid: '' }
    //]

  • 对象:{} 或 Map: 执行效率更高

        // 把扁平数组转成树型数组   对象  Map(ES6新的数据类型)
        function tranListToTreeData(list, pid){
          // 1. 得到一个对象
          // { 
          //    '': [{ id: 1, name: '总裁办', pid: '' },  { id: 2, name: 'IT部', pid: '' }], 
          //    1: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }]
          // }
          var map = {}
          list.forEach(item => {
             if(map[item.pid]){
                map[item.pid].push(item)
             }else{
                map[item.pid] = [item]
             }
          });
    
          // 2. 利用第1步骤得到的对象对原数组list进行修改
          // [
          //   { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
          //   { id: 11, name: '秘书处1', pid: 1 },
          //   { id: 12, name: '秘书处2', pid: 1 },
          //   { id: 2, name: 'IT部', pid: '' }
          // ]
          list.forEach(item=>{
            if(map[item.id]){
              item.children = map[item.id]
            }
          })
    
          // 3. 过滤
          // [
          //   { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
          //   { id: 2, name: 'IT部', pid: '' }
          // ]
          return list.filter(item => item.pid == pid)
        }
    
    
        // 测试代码:
        var arr = [
            { id: 1, name: '总裁办', pid: '' },
            { id: 11, name: '秘书处1', pid: 1 },
            { id: 111, name: '妲己', pid: 11 },
            { id: 12, name: '秘书处2', pid: 1 },
            { id: 2, name: 'IT部', pid: '' }
        ]
    
    
        const result = tranListToTreeData(arr, '')
        console.log(result);
    
        // [
        //     { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
        //     { id: 2, name: 'IT部', pid: '' }
        // ]

  • map基础的api

    map和js中的对象的作用几乎完全一样,只是多了一些api

 // 把扁平数组转成树型数组   对象  Map(ES6新的数据类型)
    function tranListToTreeData(list, pid){
      // 1. 得到一个对象
      // { 
      //    '': [{ id: 1, name: '总裁办', pid: '' },  { id: 2, name: 'IT部', pid: '' }], 
      //    1: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }]
      // }
      var map = new Map()
      list.forEach(item => {
         if(map.has(item.pid)){
            map.get(item.pid).push(item)
         }else{
            map.set(item.pid, [item])
         }
      });

      // 2. 利用第1步骤得到的对象对原数组list进行修改
      // [
      //   { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
      //   { id: 11, name: '秘书处1', pid: 1 },
      //   { id: 12, name: '秘书处2', pid: 1 },
      //   { id: 2, name: 'IT部', pid: '' }
      // ]
      list.forEach(item=>{
        if(map.has(item.pid)){
          item.children = map.get(item.id)
        }
      })

      // 3. 过滤
      // [
      //   { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
      //   { id: 2, name: 'IT部', pid: '' }
      // ]
      return list.filter(item => item.pid == pid)
    }


    // 测试代码:
    var arr = [
        { id: 1, name: '总裁办', pid: '' },
        { id: 11, name: '秘书处1', pid: 1 },
        { id: 111, name: '妲己', pid: 11 },
        { id: 12, name: '秘书处2', pid: 1 },
        { id: 2, name: 'IT部', pid: '' }
    ]


    const result = tranListToTreeData(arr, '')
    console.log(result);

    // [
    //     { id: 1, name: '总裁办', pid: '', children: [{ id: 11, name: '秘书处1', pid: 1 }, { id: 12, name: '秘书处2', pid: 1 }] },
    //     { id: 2, name: 'IT部', pid: '' }
    // ]

总结

通过准备和练习这些高频问题,相信你能在前端工程师面试中游刃有余,展现出色的技能和知识。加油,为成功而努力!

  • 34
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值