2022年面试了二十家大厂,总结了这66道他们都会问的面试题!

197 阅读1小时+

不记住这66道前端面试题,不敢回家过年

现阶段由于很多原因可能会有职场焦虑,焦虑会带来很大的精神内耗;解决焦虑的最好办法是行动起来、把焦虑的时间用来锻炼能力提升自己,先尽人事然后才能心安理得的听天命。

本文内容结构、读者可获得

精选的66道前端面试题;

本文特别适合前端初学者、有几个月工作经验的前端开发者、中高级开发者等各种有兴趣的同学学习并牢记。

内容包括:html题目1题;css题目10题;原生js20题;vue题目20题;网络浏览器10题;其他5题;

建议

建议自己熟记之后抽空找个朋友以类似模拟面试方式提问自己几遍,让自己能准确流畅的回答每一个问题,避免面试时给面试官留下技术不扎实的印象,珍惜每一次来之不易的面试机会。

也可私信作者提供类似帮助

HTML 篇

1. 说一下Html5有哪些新特性?

  • 新增语义化标签,例如<header>、<body>、<footer>
  • 增强表单功能,例如新增input标签type类型,type: color,date
  • 新增视频和音频,audio、video 标签
  • 新增Canvas绘图
  • 新增SVG绘图
  • 新增WebSocket

CSS 篇

2. 来聊一下Css吧,介绍一下盒模型概念

在 CSS 中,所有的元素都被一个个的“盒子(box)”包围着,理解这些“盒子”的基本原理,是我们使用 CSS 实现准确布局、处理元素排列的关键。

广泛地使用两种“盒子” —— 块级盒子 (block box) 和 内联盒子 (inline box)。这两种盒子会在页面流和元素之间的关系方面表现出不同的行为。

完整的 CSS 盒模型应用于块级盒子,内联盒子只使用盒模型中定义的部分内容。模型定义了盒的每个部分 —— margin, border, padding, and content —— 合在一起就可以创建我们在页面上看到的内容。

CSS 中一个块级盒子有如下组成:

  • Content box: 这个区域是用来显示内容,大小可以通过设置 width 和 height.
  • Padding box: 包围在内容区域外部的空白区域;大小通过 padding 相关属性设置。
  • Border box: 边框盒包裹内容和内边距。大小通过 border 相关属性设置。
  • Margin box: 这是最外面的区域,是盒子和其他元素之间的空白区域。大小通过 margin 相关属性设置。

box-model.png 盒模型一般有两种:标准盒模型、IE盒模型;

标准盒模型和IE盒模型的唯一区别是:

标准盒模型给盒设置 widthheight,实际设置的是 content boxpaddingborder 再加上设置的宽高一起决定整个盒子的大小。 IE盒模型设置widthheight,则设置的是 content boxpaddingborder之和。

默认浏览器会使用标准模型。如果需要使用替代模型,您可以通过为其设置 box-sizing: border-box 来实现;

3. css有哪些样式属性是可以继承的?

  • 字体系列属性:font:组合字体 font-family:规定元素的字体系列 font-weight:设置字体的粗细 font-size:设置字体的尺寸 font-style:定义字体的风格

  • 文本系列属性:text-indent:文本缩进 text-align:文本水平对齐 line-height:行高 word-spacing:增加或减少单词间的空白(即字间隔) letter-spacing:增加或减少字符间的空白(字符间距) direction:规定文本的书写方向 color:文本颜色

  • 元素可见性:visibility、opacity

  • 列表属性:list-style-type、list-style-image、list-style-position、list-style

  • 光标属性:cursor

4. 如何隐藏页面中的一个元素,你有哪些方法?

  • css属性设置display: none
  • css属性设置width: 0;height: 0;
  • css属性设置opacity: 0,透明度为0
  • css属性设置position: absolute; + 大负left + 大负top,移出视线
  • css属性设置visibility: hidden
  • css属性设置color:和背景色一样

5. Css有哪些常用的选择器?他们的优先级权重怎么计算的?

常用的选择有:

  • 元素选择器 标签名{ }
  • id选择器 #id属性值{ }
  • 类选择器 ·class属性值{ }
  • 通配选择器 * { }
  • 后代元素选择器 祖先元素 后代元素{ }
  • 子元素选择器 父元素>子元素{ }
  • 伪类选择器 元素:hover{ }等
  • 属性选择器 元素[属性名=“属性值”]
  • 兄弟元素选择器 选择器1 + 选择器2 { }等
  • 并集选择器 选择器1,选择器2,选择器n{ }
  • 交集选择器 选择器1选择器2选择器n{ }

优先级权重计算

优先级从高到低顺序:!Important>行内样式>ID 选择器>类选择器>标签选择器>通配符>继承>浏览器默认属性;

权重:

  • 第一等:内联样式,如:style="color:red;",权值为 1000
  • 第二等:ID 选择器,如:#body,权值为 0100
  • 第三等:类、伪类、属性选择器如:.bar, 权值为 0010
  • 第四等:标签、伪元素选择器,如:div ::first-line 权值为 0001
  • 其他: 通配符,子选择器,相邻选择器等。如*,>,+, 权值为 0000;继承的样式没有权值

6. Css雪碧图的原理

应用场景: 有大量小图标和背景图像,让UI设计合并到一张图片上,然后利用css强大的 position 定位,来显示需要显示的图片部分。 优点:

  1. 减少加载网页图片时对服务器的请求次数,降低服务器压力

  2. 提高页面的加载速度

7. 说一下你对BFC的了解吧

BFC:块格式化上下文(Block Formatting Context)是 Web 页面的可视 CSS 渲染的一部分,是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

BFC是一个独立的布局环境,BFC内部的元素布局与外部互不影响。

创建BFC的方式:

  • 根元素(<html>
  • float 值非 none,即 left、right
  • overflow 值非 visible、clip的块元素,即 auto、scroll、hidden
  • display 值为 inline-block、table-cell、table-caption、flex、inline-flex、flow-root
  • position 值为 absolute、fixed

浮动元素和绝对定位元素,非块级盒子的块级容器(例如 inline-blocks、table-cells、table-caption),以及 overflow 值不为 visiable 的块级盒子,都会为他们的内容创建新的BFC(块级格式上下文)。

特性:BFC影响布局,通常我们会为定位和清除浮动创建新的 BFC,而不是更改布局;

  • 包含内部浮动,解决浮动元素令父元素高度坍塌的问题
  • 排除外部浮动,解决非浮动元素被浮动元素覆盖的问题
  • 阻止外边距重叠,解决外边距垂直方向重合的问题
  1. 给父元素开启BFC
<!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>BFC</title>
  <style>
    * {
      margin: 0; padding: 0;
    }
    .outer {
      border: 10px solid aqua;
      /* 给父元素设置BFC */
      overflow: hidden;
    }
    .inner {
      width: 120px;
      height: 120px;
      background-color: antiquewhite;
      /* 子元素浮动令父元素高度坍塌 */
      float: left;
    }
  </style>
</head>
<body>
  <div class="outer">
    <div class="inner"></div>
  </div>
</body>
</html>
  1. 给非浮动元素开启BFC
<!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>BFC</title>
  <style>
    * {
      margin: 0; padding: 0;
    }
    .left {
      width: 80px;
      height: 80px;
      background-color: aqua;
      /* 浮动元素 */
      float: left;
    }
    .right {
      width: 120px; /* 另:这里宽度width: auto; 则左边宽度固定,右边宽度自适应的两列布局 */
      height: 120px;
      background-color: antiquewhite;
      /* 排除外部浮动,解决非浮动元素被浮动元素覆盖的问题 */
      overflow: hidden;
    }
  </style>
</head>
<body>
  <div class="left"></div>
  <div class="right"></div>
</body>
</html>
  1. 属于同一个BFC的两个相邻的Box的margin会发生重叠
<!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>BFC</title>
  <style>
    * {
      margin: 0; padding: 0;
    }
    .top {
      width: 80px;
      height: 80px;
      background-color: aqua;
      margin-bottom: 10px;
    }
    .bottom {
      width: 80px;
      height: 120px;
      background-color: antiquewhite;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <div class="top"></div>
  <!-- 包裹开启BFC -->
  <div style="overflow: hidden;">
    <div class="bottom"></div>
  </div>
</body>
</html>

8. Css布局时为什么有时需要清除浮动,有哪些方式清除浮动?

起初 float 属性是为了能让开发人员实现简单的布局,即在成块的文本内浮动图像,文字环绕在它的左边或右边。

但是由于任何元素都可以浮动,而不仅仅是图像,所以float布局被'滥用',浮动的使用范围扩大了,甚至曾被用来实现整个网站页面的布局。

过度的使用float布局,会出现问题:比如 由于父级盒子在很多的情况下不方便给高度,但是子盒子浮动又不占有位置,最后父级盒子的高度为0,就会影响父级盒子下面的标准流盒子。

此时就需要清除浮动

1.清除浮动的本质是清除浮动元素对后面的元素造成影响; 2.如果父盒子本身有高度,则不需要清除浮动。父级有了高度,就不会影响下面的标准流 3.清除浮动之后,父级就会根据浮动的子盒子自动检测高度。父级有了高度,就不会影响下面的标准流

清除浮动的方法

  1. 父级元素添加overflow:hidden等方式,使得触发BFC;
  2. 父级元素使用 after 伪元素,在伪元素里面清除浮动,见示例;
  3. 额外标签法,类似第二条;

示例:

.wrapper::after {
  content: "";
  clear: both;
  display: block;
}

目前出现了更好的页面布局技术,比如:flex布局、grid布局,很少使用浮动这种传统的布局方法来进行页面布局。

9. 介绍一下 position 的属性值有哪些,分别相对于什么定位

CSS 中 position 属性用于指定一个元素在文档中的定位方式。top,right,bottom 和 left 属性则决定了该元素的最终位置。

position 属性值有:

static:默认值;元素在文档常规流中位置,此时 top, right, bottom, left 和 z-index 属性无效。 relative:相对定位;元素先放置在文档常规流中位置,使用 top, right, bottom, left改变位置之后,原先所在的位置会留下空白; absolute:绝对定位;元素会被移出正常文档流,并不为元素预留空间;相对于父级元素定位; fixed:固定定位;元素会被移出正常文档流,并不为元素预留空间;相对于屏幕视口定位; sticky:粘性定位;元素根据正常文档流进行定位,并不为元素预留空间;然后相对它的最近滚动祖先定位;

10. 页面导入样式时,使用link和@import有什么区别?

1.他们都是css引入外部样式表文件的方式,link属于HTML标签,而@import是CSS提供的; 2.页面被加载的时,link会同时被加载;而@import引用的CSS会等到页面被加载完再加载; 3.link方式的样式的权重高于@import的权重; 4.import只在IE5以上才能识别,而link是HTML标签,无兼容问题;现在IE5极少遇到,此条几乎可以忽略;

html中使用:

<!-- link -->
<link href="CSSurl路径" rel="stylesheet" type="text/css" />
<!-- @import -->
<style type="text/css">
  @import url(CSS文件路径地址);
</style>

css中使用:

@import url(CSS文件路径地址);

11. 怎么理解回流、重绘?什么场景下会触发?

浏览器解析渲染页面的html、css时,会涉及到回流与重绘。

什么是回流、重绘?

回流:布局引擎会根据各种样式,计算每个元素盒子在页面上的大小与位置

重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个元素盒子特性进行绘制

在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变;

当我们对 DOM 的修改引发了 DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来;

当我们对 DOM的修改导致了样式的变化(color或background-color),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了回流。

如何触发回流

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染的时候(这避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
  • 获取一些特定属性的值,这些属性有一个共性,就是需要通过即时计算得到,比如:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
  • 调用一些方法时比如:getComputedStyle()方法

如何触发重绘

  • 触发回流一定会触发重绘
  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

如何减少回流、重绘

  • 如果想设定元素的样式,通过改变元素的 class 类名 (尽可能在 DOM 树的最里层)
  • 避免设置多项内联样式
  • 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值(如前文示例所提)
  • 避免使用 table 布局,table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算
  • 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响
  • 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
  • 避免使用 CSS 的 JavaScript 表达式

浏览器的优化措施

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列

当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据

因此浏览器不得不清空队列,触发回流重绘来返回正确的值


原生JS篇

12. js有哪些数据类型?

8种:number string boolean null undefined object Symbol bigInt

分为两类: 基本数据类型:String、Number、Boolean、Null、Undefined、Symbol 复杂数据类型:Object【Object是个大类,Function函数、Array数组、Date日期...等都归属于Object】

13. js如何判断一个变量的类型?

  • typeof方法
  1. 基本数据类型都返回对应的字符串;
  2. 例外的null返回object,NaN返回number;
  3. 复杂数据类型都返回object,函数返回function;

示例: typeof 1 // 'number' typeof 'abc' // 'string'

  • instanceof 方法
  1. 只能用来判断复杂数据类型,因为instanceof 是用于检测构造函数(右边)的 prototype 属性是否出现在某个实例对象(左边)的原型链上;

示例: [1,2] instanceof Array // true ({a: 1}) instanceof Object // true

  • Object.property.toString.call
  1. 可检查所有的数据类型
  2. 返回"[object, 类型]",注意返回的格式及大小写,前面是小写,后面是首字母大写

示例: Object.property.toString.call(9) // '[object Number]' Object.property.toString.call(true) // '[object Boolean]' Object.property.toString.call([1,2]) // '[object Array]'

  • constructor方法

示例: ({a: 1}).constructor == Object // true ([1,2]).constructor == Array // true

14. js如何判断一个变量是数组?

大概有四种方法:

  1. Array.isArray([1,2]) // true
  2. [1,2] instanceof Array // true
  3. [1,2] constructor === Array // true
  4. Object.prototype.toString.call([1,2]) === '[object Array]'

15. 工作中用过Es6吗?用过Es6哪些新特性

经常用的新特性有:

  • 声明变量let、const
  • 模板字符串
  • 解构赋值
  • 扩展运算符、
  • 箭头函数
  • Class类
  • 模块化,导入导出(import export
  • promise
  • async await
  • Symbol
  • 对象增加方法,Object.values()、Object.entries()、Object.assign()
  • 函数的参数默认值
  • for…of 和 for…in

16. let、const 以及 var 的区别是什么?

  • var
  1. 声明变量时有声明提升的特性,赋值不提升
  2. 可以重复声明变量
  3. 省略var,变量是全局变量
  4. 函数作用域
  • let
  1. let声明的变量具有块级作用域的特征
  2. 在同一块级作用域,不能重复声明变量
  3. let声明的变量不存在变量提升,即let声明存在暂时性死区。
  • const
  1. 除了具有let的特点外,const声明的为常量

17. 说一下 = == === 的区别

“=”:表示赋值操作

“==”:只比较大小是否相等,类型不同的会进行强制类型转换;

“===”:全等,要求左右两边的值完全相等,不仅大小相等,类型也要一样;类型不会进行强制转换;

18. 普通函数和箭头函数的区别是什么?

  1. 箭头函数的 this 指向其上下文,任何方法都改变不了其指向,如 call() , bind() , apply();普通函数的this指向调用它的那个对象;
  2. 箭头函数没有 arguments对象、prototype 原型对象,所以箭头函数不能用于构造函数,即不能使用new;
  3. 箭头函数不绑定arguments,取而代之用rest参数…解决;
function A(a){
  console.log(arguments);
}
A(1,2,3,4,5,8);  //  [1, 2, 3, 4, 5, 8, callee: ƒ, Symbol(Symbol.iterator): ƒ]

let B = (b)=>{
  console.log(arguments);
}
B(2,92,32,32);   // Uncaught ReferenceError: arguments is not defined

let C = (...c) => {
  console.log(c);
}
C(3,82,32,11323);  // [3, 82, 32, 11323]

19. 说一说script标签中添加属性defer和async的区别?

  • 浏览器是同步加载 JavaScript 脚本的,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,则需要先下载外部脚本,然后执行脚本,最后再向下渲染页面。

如果外部脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器卡死了,没有任何响应。这是很不好的体验,所以浏览器允许脚本异步加载。

<script>标签上添加defer或async属性是两种异步加载的语法,渲染引擎遇到这一属性,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

defer与async的区别是 defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;

async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

一句话,defer是 渲染完再执行,async 是 下载完就执行

另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

一般脚本放在</body>前面就可以了,如果有依赖的则按照顺序放好。如果一定要放在head标签里面,最好是加defer属性。

20. js获取元素dom的方法有哪些?

  1. document.getElementById(id)
  2. document.getElementsByName(name)
  3. document.getElementsByTagName(tagname)
  4. document.querySelector()
  5. document.querySelectorAll()
  6. document.getElementsByClassName(class)

21. js如何修改dom的样式?

  1. dom.style.属性 = 属性值
  2. dom.className = 'class-name'
  3. dom.setAttribute('class', 'class-name')
  4. dom.classList.add('class-name') 、dom.classList.remove('class-name')

22. js如何事件监听

大概有两种方式:

  • 传统的绑定事件方式 特点: 1.事件名称之间一定要加上on,比如:onclick、onload、onmousemove。 2.兼容主流的浏览器,包括低版本的IE。 3.当同一个元素绑定多个事件时,只有最后一个事件会被添加,并且传播模式只能是冒泡模式。
document.getElementById('id').onclick = () => {
  console.log('点击')
}
  • addEventListener() 绑定事件方式
  1. dom.addEventListener(event, function, useCapture)中的第三个参数可以控制指定事件是否在捕获或冒泡阶段执行。 true - 事件句柄在捕获阶段执行、false- 默认- 事件句柄在冒泡阶段执行;
  2. addEventListener() 可以给同一个元素绑定多个事件,不会发生覆盖的情况。如果给同一个元素绑定多个事件,那么采用先绑定先执行的规则;
  3. addEventListener() 在绑定事件的时候,事件名称之前不需带 on;
  4. 注意该方法的兼容性,如果要兼容 IE6-8 ,不能使用该方法;
  5. 可以使用 removeEventListener() 来移除之前绑定过的事件;
document.getElementById('id').addEventListener('click', () => {
  console.log('点击')
}, false)

23. 说说js的作用域、作用域链

js中作用域分为:全局作用域、函数作用域、块状作用域

  • 变量在函数或者代码块{ }外定义,即为全局作用域;js在不同的宿主环境中,全局作用域不同;
  • 在函数内部定义的变量,就是局部作用域。函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域;
  • Es6增加了块级作用域,{ } 就是“块”,这个里面的变量就是拥有这个块状作用域;

作用域链:作用域是分层的,子作用域可以访问父作用域,不能从父作用域引用子作用域中的变量;如果一个 变量 或者其他表达式不在 “当前的作用域”,那么js机制会继续沿着作用域链向上查找直到全局作用域(Node中的global或浏览器中的window),如果找不到则表示变量不可用。这种一层一层的关系,就是 作用域链;

24. 你对js中this的了解有多少?

this的指向在函数定义时是确定不了的,只有函数执行时才确定;this指向最终直接调用函数的对象;无对象直接调用的函数(其实默认是全局作用域调用),this指向全局作用域,浏览器环境是window对象,Node是global;

在严格模式种,默认的this不再是window,而是undefined

有下面5种情况:

  • 函数作为对象的属性被调用
  • 函数直接执行
  • 构造函数 配合 new调用
  • call,apply,bind改变this指向
  • Es6箭头函数中的this
// 1. 定义在全局的变量都为window对象的属性,window对象直接调用函数text,this指向window
var a = 1;
function text() {
 var a = 2;
 console.log(this, this.a);
}

text(); //window, 1
window.text(); //window, 1

// 2. 函数fun作为对象obj的属性被对象obj直接调用,this指向obj
var a = 1;
var obj = {
 a: 2,
 fun: function (){
  console.log(this, this.a)
 }
}
obj.fun(); // obj, 2

// 3. fun的直接上级调用者是b对象,this指向b,而不是指向obj
var a = 1;
var obj = {
 a: 2,
 b: {
  a: 3,
  fun: function (){
   console.log(this, this.a);
  }
 }
}
obj.b.fun(); //对象b, 3

// 4. text()的直接调用者为window对象,所以此时this指向window;this指向最终调用函数的对象;
var a = 1;
var obj = {
 a: 2,
 b: {
  a: 3,
  fun: function (){
   console.log(this, this.a);
  }
 }
}

var text = obj.b.fun;
text(); // window, 1

// 5. 虽然函数fun是当对象obj调用函数b才执行的。但是函数fun并没有对象直接调用,this指向window
var a = 1;
function fun() {
 console.log(this, this.a)
}
var obj = {
 a: 2,
 b: function (){
  fun();
 }
}
obj.b(); //window, 1

// 6. 构造函数
function Person() {
 this.name = "name";
 this.age = 500;
}

var p = new Person();
console.log(p.name);// name

// 7. 箭头函数中的this在函数定义的时候就已经确定,它this指向的是它的外层作用域this的指向
var a = 1
var test = () => {
  console.log(this.a)
}
var obj = {
  a: 2,
  test
}
obj.test() // 1

// 8. 和第7个例子对比
var a = 1
var test = function() {
  console.log(this.a)
}
var obj = {
  a: 2,
  test
}
obj.test() // 2

// 9. call()改变this的指向
var value=1;
function foo(x,y) {
  console.log(x, y) // 3 4
  console.log(this.value)
}
var obj = {
  value: 2
}
foo(); // 1 
foo.call(obj,3,4);  // 2

// 10. apply()改变this的指向,apply与call的唯一区别就是,调用apply方法时的参数,实参应该是以数组的形式来书写
var value = 1;
function foo(x, y) {
  console.log(x, y) // 3 4
  console.log(this.value)
}
var obj={
  value: 2
}
foo(); // 1 
foo.apply(obj,[3,4]);  // 2

// 11. bind()改变this的指向,bind与call,apply的区别就是:bind方法不会立即调用函数,它只是改变了新函数的this绑定
var value = 1;
function foo(x,y) {
  console.log(x, y) // 3 4
  console.log(this.value)
}
var obj={
  value: 2
}
var bar = foo.bind(obj,3,4);
bar(); // 2

// 12. 当我们使用bind方法创建一个新函数,这个新函数再使用call或者apply来更改this绑定时,还是以bing绑定的this为准。
var value = 1;
function foo(x,y) {
  console.log(this.value)
}
var obj = {
  value: 2
}
var o = {
  value: 3
}
var bar=foo.bind(obj,3,4);
bar.call(o); // 2

25. js如何改变this的指向?

有三种方式改变this的指向

  1. call()
  2. apply()
  3. bind()

26. 说一下js对象的原型和原型链?

什么是原型?

在js中,每当定义一个复杂数据类型(Object、Function、Arrry、Date等)的时候都会自带一个prototype对象,这个对象就是我们说的原型。 原型又分为显示原型和隐式原型; 显示原型是函数里面的prototype属性,每个prototype原型都有一个constructor属性,指向它关联的构造函数; 隐式原型是实例化的对象里面,有一个__proto__属性,__proto__属性指向自身构造函数的原型对象;

什么是原型链

每一个实例化对象都有一个__proto__属性,而这个__proto__属性指向构造函数的原型对象,原型对象上也有一个__proto__属性,就这样一层一层往上找,直到找到Object.prototype,就这样查找的过程就叫原型链;

proto prototype constructor的三角关系

函数在声明时会生成一个对象prototype 该对象中有一个constructor指向构造函数本身; 当构造函数实例化后,在实例化对象中会生成一个对象叫__proto__,指向构造函数的原型对象;

27. 介绍一下事件循环EventLoop?

js是单线程的,同一时间只能做一件事情,遇到同步任务直接执行,遇到异步任务怎么办呢?会一直等待异步任务执行吗?

答案是否定的。js在主线程依次执行同步任务,遇到异步任务之后开启一个异步任务队列,把异步任务推进异步队列后继续执行后面的代码;比如主线程中需要发一个 AJAX 请求,就把这个任务交给另一个浏览器线程(HTTP 请求线程)去真正发送请求,待请求回来了,再将 callback 里的代码交给 JS 引擎线程去执行。即浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理。

loop.png 异步任务队列又分为微任务(micro task)队列和宏任务(macro task)队列;事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环。

常见宏任务:

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

常见微任务:

  • promise.then()
  • promise.catch()
  • new MutaionObserver()
  • process.nextTick()

下面代码依次打印顺序是:

console.log('同步代码1');
setTimeout(() => {
    console.log('setTimeout')
}, 0)
new Promise((resolve) => {
  console.log('同步代码2')
  resolve()
}).then(() => {
    console.log('promise.then')
})
console.log('同步代码3')

// 输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

分析:

  1. setTimeout 回调和 promise.then 都是异步执行的,将在所有同步代码之后执行;
  2. 虽然 promise.then 写在后面,但是执行顺序却比 setTimeout 优先,因为它是微任务;
  3. new Promise 是同步执行的,promise.then 里面的回调是异步的。

下面代码的执行顺序:

setTimeout(() => {
  console.log('setTimeout start');
  new Promise((resolve) => {
    console.log('promise1 start');
    resolve();
  }).then(() => {
    console.log('promise1 end');
  })
  console.log('setTimeout end');
}, 0);
function promise2() {
  return new Promise((resolve) => {
    console.log('promise2');
    resolve();
  })
}
async function async1() {
  console.log('async1 start');
  await promise2();
  console.log('async1 end');
}
async1();
console.log('script end');

// async1 start、 promise2、 script end、 async1 end、 setTimeout start、promise1 start、setTimeout end、promise1 end

28. 介绍一下事件冒泡、事件捕获、事件委托/代理

  • 事件冒泡:在js事件传播过程中,当事件在一个元素上触发之后,事件会逐级传播给先辈元素,直到document为止;有冒泡的事件有:click、mousedown、mouseup、keydown、keyup、keypress;没有冒泡的事件有:blur、focus、load等;实现方式:1. dom.addEventListener('click', ()=>{}, false) 或者 2. dom.onclick = function (){}

  • 阻止事件冒泡的方式:e.stopPropagation()

  • 事件捕获:与事件冒泡相反的触发方向,父元素 -> 子元素,是DOM2级的事件;实现方式:dom.addEventListener('click', ()=>{}, true)

  • 事件委托:利用事件冒泡,只指定父级监听事件,通过e.target区分不同的子级元素,就可以管理某一类型的所有事件;

  • 事件委托的优点:

  1. 添加的事件减少,减少了内存占用, 性能更好;
  2. 动态添加的元素,因为在父元素上做了事件委托,则不需要为他们注册新的事件监听;

29. 说一说数组去重都有哪些方法?

  • 通过数组的双重循环去重
// 思路1
function uniqueArray(arr) {
  const newArr = [];
  let isRepeat;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    isRepeat = false;
    const newLen = newArr.length;
    for (let j = 0; j < newLen; j++) {
      if (newArr[j] === arr[i]) {
        isRepeat = true;
        break;
      }
    }
    if (!isRepeat) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}
// 思路2
function uniqueArr2(arr) {
  const newArr = [];
  let isRepeat;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    isRepeat = false;
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        isRepeat = true;
        break;
      }
    }
    if (!isRepeat) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}
  • 通过数组的filter()方法
function uniqueArray(arr) {
  return arr.filter((item,index) => {
    return arr.indexOf(item) === index;
  })
}
  • 通过数组方法forEach + indexOf/includes
// indexOf方法判断是否已有
function uniqueArr(arr) {
  const newArr = [];
  arr.forEach(item => {
    if (newArr.indexOf(item) === -1) {
      newArr.push(item)
    }
  });
  return newArr;
}
// includes方法判断是否已有
function uniqueArray(arr) {
  const newArr = [];
  arr.forEach(item => {
    if (!newArr.includes(item)) {
      newArr.push(item)
    }
  })
  return newArr;
}
  • 通过数组sort()方法,先排序,再比较相邻的是否相等
function uniqueArray(arr) {
  const newArr = [];
  arr.sort();
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] !==arr[i+1]) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}
  • 使用ES6的Set
function uniqueArray(arr) {
  // return Array.from(new Set(arr))
  return [...new Set(arr)]
}
  • 利用对象
function uniqueArray(arr) {
  let tmpObj = {};
  let result = [];
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    if (!tmpObj[arr[i]]) {
      tmpObj[arr[i]] = 1;
      result.push(arr[i]);
    }
  }
  return result;
}
  • 当前循环数组项的下标是否 等于 indexOf方法获取此项的下标
function uniqueArray(arr) {
  var result = [];
  arr.forEach(function(item, index) {
      if (arr.indexOf(item) === index) {
        result.push(e);
      }
  })
  return result;
}

30. 说一说JS实现异步编程的方法?异步编程方案有哪些

  1. 回调函数

缺点:代码高耦合,结构混乱,回调地狱,不能使用 try catch 捕获错误,不能直接 return;

  1. Promise:有pending、resolved、rejected三种状态,三个状态不可逆转,只能pending -> resolved或者pending -> rejected;

插一句:Promise本身是同步任务,Promise.then 是微任务;

  1. Generator:可以暂停执行(分段执行)的函数,函数名前面要加星号,是一个状态机,yield表达式可以暂停函数执行,next() 方法用于恢复函数执行;
  2. async/await:基于Promise实现,使异步代码看起来更像同步代码;
  3. 事件监听:采用事件驱动模式。任务的执行不取决于代码的顺序,而取决与某个事件的发生。
  4. 发布/订阅:存在一个信号中心,某个任务执行完成,就向信号中心发布(publish)一个信号,其他任务可以向信号中心订阅(subcribe)这个信号,从而知道什么时候自己可以开始执行;发布/订阅性质与事件监听类似,但是明显优于后者,因为我们可以通过查看消息中心,了解存在多少信号,多少个订阅者,从而监听程序的运行。

通过链式调用使代码结构相对清晰;

31. new函数的执行过程,简单说一下

  1. 创建一个新对象 man
  2. 新对象会被执行[[prototype]]连接 man.__prototype__=People.prototype
  3. 新对象和构造函数调用this会绑定起来(改变this指向) People.call(man,'a')
  4. 执行构造函数的代码 man.pName
  5. 如果构造函数没有返回值,那么就会自动返回这个新对象 return this

构造函数有返回值,如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例;

// 1. 有返回值的示例1
function Fn() {  
  this.user = 'user';  
  return {};  
}
var a = new Fn;  
console.log(a.user); //undefined

// 2. 有返回值的示例2
function Fn() {  
  this.user = 'user';  
  return function(){};
}
var a = new Fn;  
console.log(a.user); //undefined

// 3. 有返回值的示例3
function Fn() {  
  this.user = 'user';  
  return 1;
}
var a = new Fn;  
console.log(a.user); //user

// 4. 有返回值的示例4 
function Fn() {  
  this.user = 'user';  
  return null;
}
var a = new Fn;  
console.log(a.user); //user

32. 说一下深拷贝,工作中你用哪种方式实现深拷贝的?

深拷贝:拷贝的是对象/数组内部数据的实体,重新开辟了内存空间存储数据;拷贝的对象修改后不会影响被拷贝的对象;

  1. 实现方式1:递归循环
function deepClone(obj) {
  var target = {};
  for(var key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      if (typeof obj[key] === 'object') {
        target[key] = deepClone(obj[key]); 
      } else {
        target[key] = obj[key];
      }
    }
  }
  return target;
}
  1. 实现方式2:JSON.parse(JSON.stringify(obj))
let obj = {
 id: 1,
 name: '张三',
 age: 10,        
}
let newObj = JSON.parse(JSON.stringify(obj))
  1. 其他方法:只有一级属性深拷贝,更深层的属性是浅拷贝
  • Object.assign(obj1, obj2)
let obj = {
 id: 1,
 name: '张三',
 age: 10,        
}
let newObj = Object.assign({}, obj)
  • 扩展运算符
var obj = {
  a: 1,
  b: 2
}
 
var obj1 = {...obj}
  • concat()、slice()
var arr1 = [1, 2, 3, 4]
var arr2 = arr1.concat([])
var arr3 = arr1.slice(0)

33. js实现继承有哪些方式?创建对象有哪几种方式?

  1. 原型链继承

原型链:实例、原型对象、构造函数三者关系,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针;那么,假如我们让原型对象等于另一个实例,结果会是怎么样呢?显然,此时的原型对象将包含一个指向另一个原型对象的内部指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。

核心:子类的原型对象是父类的实例,通过原型链让一个引用类型继承另一个引用类型的属性和方法;

缺点: 包含引用类型值的原型,该值会被所有实例共享,修改则会影响到其他的实例; 在创建子类的实例时,不能向父类的构造函数中传递参数;

function SuperClass() {
  this.name = 'father'
}
SuperClass.prototype.getFatherName = function() {
  return this.name
}
function SubClass() {
  this.name = 'child'
}
// 继承父类
SubClass.prototype = new SuperClass()
SubClass.prototype.getChildName = function() {
  return this.name
}
// 实例化
const sub = new SubClass()
console.log(sub.getChildName())// child
console.log(sub.getFatherName())// child
  1. 借用构造函数继承

核心:在子类的构造函数的内部调用父类构造函数,通过 apply,call 方法实现;可以在在类型构造函数中向父类构造函数传递参数;

缺点:方法在构造函数中定义,会定义了很多功能相同的函数,函数没有复用;

function SuperClass(name) {
  this.name = name
  this.getName = function () {
    return this.name
  }
}
function SubClass() {
  // 继承了父类的属性、方法,传递参数
  SuperClass.call(this, '三哥')
  this.age = '30'
}
// 实例化
const sub = new SubClass()
console.log(sub.getName())// 三哥
console.log(sub.age)// 30
  1. 组合使用原型链和构造函数继承

主要是使用原型链继承父类的方法,使用构造函数继承父类的属性;

核心:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既能通过在原型上定义方法实现函数的复用,又能够保证每个实例有自己的属性。即每个实例都会有自己一份实例属性的副本,但同时又共享者对方法的引用,最大限度的节省了内存,这种模式还支持向构造函数传递参数。

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

function SuperClass(name) {
  this.name = name
  this.colors = ['red', 'green', 'blue', 'pink']
}
SuperClass.prototype.getName = function() {
  return this.name
}
function SubClass(name, age) {
  SuperClass.call(this, name)
  this.age = age
}
SubClass.prototype = new SuperClass()
SubClass.prototype.constructor = SubClass
SubClass.prototype.getAge = function() {
  return this.age
}
// 实例化
const sub = new SubClass('三哥', 20)
console.log(sub.getName())// 三哥
console.log(sub.age)// 20
  1. 原型式继承

核心:借助原型,可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型,Es6中 Object.create() 方法规范了原型式继承,这个方法接收两个参数,第一个是新对象原型的对象,第二个为新对象定义额外属性的对象(可选);

function Obj(o) {
  function F() {}
  F.prototype = o
  return new F()
}
  1. 寄生式继承

核心:创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回增强后的对象;

缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;

function CreateObj(original) {
  // 使用原型式继承增强对象
  const obj = Obj(original)
  obj.getName = function () {
    return this.name
  }
  return obj
}
  1. 寄生组合式继承

核心:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已,本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类的原型;解决了组合使用原型链和构造函数继承两次调用父类构造函数的缺点;Es6之前相对最完美的方式;

// 此函数作用是拷贝一份父对象
function Obj(o) {
  function F() {}
  F.prototype = o
  return new F()
}
function SuperClass(name) {
  this.name = name
  this.colors = ['red', 'green', 'blue', 'pink']
}
SuperClass.prototype.getName = function() {
  return this.name
}
function SubClass(name, age) {
  SuperClass.call(this, name)
  this.age = age
}
function inheritPrototype(subCls, superCls) {
  const obj = Obj(superCls.prototype)
  obj.constructor = subCls
  subCls.prototype = obj
}
inheritPrototype(SubClass, SuperClass)
SubClass.prototype.getAge = function() {
  return this.age
}
// 实例化
const sub = new SubClass('三哥', 20)
console.log(sub.getName())// 三哥
console.log(console.log(sub.getAge()))// 20

34. 什么是闭包?

定义:闭包是一个函数;正常情况下,根据变量作用域链,内层的函数作用域可以访问外层的函数作用域,反之则不行;但是通过闭包我们可以实现外层函数能访问到内层函数里的变量,闭包可视为这一行为的中介;

创建闭包:最常见的方式就是在一个函数内创建另一个函数,则这个另一个函数就是闭包;把这个另一个函数 return 出去,在可以通过闭包作为中介在函数外 访问函数内的变量;或者如下面的例子通过挂在全局作用域内传递出去;

function A() {
  let a = 1
  window.B = function () {
    console.log(a)
  }
}

闭包的优点:

  1. 让函数能够访问其他函数内部的变量变成可能;
  2. 变量会常驻在内存中,因为闭包函数保留了对这个变量的引用,所以这个变量不会被回收;
  3. 可以避免使用全局变量,防止全局变量污染;

闭包的坏处:使用不当可能会造成内存泄漏或溢出。

闭包的用途:

更改下面这个例子,使之按时间间隔依次打印1,2,3,4,5

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
// 一次打印出来5次5
  1. 闭包的方式解决
for (var i = 1; i <= 5; i++) {;
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}
  1. 其他方式,使用 setTimeout 的第三个参数
for (var i = 1; i <= 5; i++) {
  setTimeout(function timer(j) {
    console.log(j)
  }, i * 1000, i)
}
  1. 使用 let 定义 块作用域
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

Vue篇

35. 说一下Vue2.x的生命周期

生命周期|描述 --|:--:|-- beforeCreate | vue实例初始化后,数据观测(data observer)和事件配置之前。data、computed、watch、methods都无法访问 created | vue实例创建完成后立即调用 ,可访问 data、computed、watch、methods。未挂载 DOM,不能访问 el、ref beforeMount | 在 DOM 挂载开始之前调用 mounted | vue实例被挂载到 DOM beforeUpdate | 数据更新之前调用,发生在虚拟 DOM 打补丁之前 updated | 数据更新之后调用 beforeDestroy | 实例销毁前调用 destroyed | 实例销毁后调用

  • 调用异步请求可在created、beforeMount、mounted生命周期中调用,因为相关数据都已创建。最好的选择是在created中调用。
  • 获取DOM在mounted中获取,获取可用$ref方法

36. Vue 父组件和子组件生命周期执行顺序

加载渲染过程

父先创建,才能有子;子创建完成,父才完整。

顺序:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程

子组件更新 影响到 父组件的情况。 顺序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

子组件更新 不影响到 父组件的情况。 顺序:子 beforeUpdate -> 子 updated

父组件更新过程

父组件更新 影响到 子组件的情况。 顺序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新 不影响到 子组件的情况。 顺序:父 beforeUpdate -> 父 updated

销毁过程

顺序:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

37. 谈一下对MVVM 的理解

MVVM是Model-View-ViewModel的缩写。 Model 代表数据层,可定义修改数据、编写业务逻辑。 View 代表视图层,负责将数据渲染成页面。 ViewModel 负责监听数据层数据变化,控制视图层行为交互,简单讲,就是同步数据层和视图层的对象。 ViewModel 通过双向绑定把 View 和 Model 层连接起来,且同步工作无需人为干涉,使开发人员只关注业务逻辑,无需频繁操作DOM,不需关注数据状态的同步问题。

38. 用过Vue中的哪些指令?

v-html、v-bind、v-on、v-model、v-if、v-for

39. 如何实现v-model指令的?介绍一下v-model的原理

v-model指令用于实现input、select等表单元素的双向绑定,是个语法糖;

  • text/textarea 元素:使用v-bind 绑定响应属性 value, 用v-on监听 input 事件;
  • checkbox/radio: 使用v-bind 绑定响应 checked 属性,用v-on监听 change 事件;
  • select:使用v-bind 绑定响应 value值,用v-on监听 change 事件;

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

共同点:控制元素显示和隐藏。 不同点:

  • v-show 控制的是元素的CSS(display);
  • v-if 是控制元素本身的添加或删除;
  • v-show 由 false 变为 true 的时候不会触发组件的生命周期。
  • v-if 由 false 变为 true 则会触发组件的beforeCreate、create、beforeMount、mounted钩子,由 true 变为 false 会触发组件的beforeDestory、destoryed方法。
  • v-if 比 v-show有更高的性能消耗。

41. 为什么 v-if 不建议和 v-for 一起使用? 他们哪个的优先级高?

  • Vue2.x中v-for优先级比v-if更高,每次渲染列表都要先循环再进行条件判断,性能浪费。
  • v-for和v-if同时应用在一个元素上,vue会报错。
  • Vue3.x中v-if优先级高于v-for,但是这种逻辑最好在列表数据使用之前用数组filter方法处理掉。

42. v-for循环为什么必须要定义唯一值的key属性,而且不建议使用index作为key值?

主要和diff算法有关,diff算法采用同级比较,比较顺序如下:

  1. tag 标签不一致直接新节点替换旧节点

  2. tag 标签一样 先替换属性 再对比子元素

    1. 新旧都有子元素,采用sameVnode函数 判断tag和key完全相同为同一节点,进行节点复用
    2. 新的有子元素,旧的没有子元素。直接将子元素虚拟节点转化成真实节点插入即可
    3. 新的没有子元素,旧的有子元素。直接清空 innerHtml
  3. key的作用主要是为了diff算法比对时更高效的比对虚拟DOM两个新旧节点是否是相同节点,加快比对效率,假如是相同节点,则直接不用更改节点;

  4. Vue判断两个虚拟DOM是否相同时,主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则认为这是两个相同的节点,只能去做更新操作,这造成了大量的dom更新操作;

  5. 另外重复的 key 将会导致渲染异常,vue会报错;

为什么不建议使用index作为key值

key是虚拟DOM的重要标识,在页面更新的时候其起着重要的作用,能加快diff算法比对,复用DOM提高性能;

  • 下面是使用index作为key时在这个页面更新的过程中发生了什么:
  1. 在最前面添加一项之后,会产生一个新的虚拟DOM树
  2. 然后这个新的DOM树与旧的进行diff,diff遵循以下原则
    • 在旧的虚拟DOM中找到与新的虚拟DOM相同的key 内容没有发生变化,就直接使用原来的真实DOM 内容发生改变,就替换掉之前旧的虚拟DOM,生成新的真实DOM
    • 在旧的虚拟DOM中未找到与新的虚拟DOM相同的key 直接生成新的真实DOM
  3. key相同就会进行一次内容比较
  4. 内容发现不同,就生成新的真实DOM
  5. 然后循环上面的操作
  • 使用唯一值作为key时在这个页面更新的过程中发生了什么
  1. 添加一个新增项之后,会产生一个新的虚拟DOM树;
  2. 然后这个新的DOM树与旧的进行diff;
  3. 在旧的DOM中没有找到跟新增项相同的key,直接生成新的真实DOM;
  4. 然后循环上面的操作;
  5. 在旧的DOM中找到了相同的key,比较内容也一样,直接复用之前的真实DOM;

43. Vue中的数据流是单向的还是双向的?如何理解

可能是受 Vue 的双向数据绑定影响,认为 Vue 数据流是双向的,其实Vue中的数据流是单向的,即父组件传递到子组件,子组件内部可以定义依赖 props 中的值,但无权修改父组件传递的数据,这样做可以防止子组件意外变更父组件的数据,导致数据变更难以理解。

44. 组件之间的数据通信方式你知道哪些?

  • props 和 $emit
  • parent 和 children
  • EventBus
  • Vuex
  • provide 和 inject
  • attrs 和 listeners

最好每个方式再详细说说。

45. 父组件传过来的props在子组件中可以直接修改吗?假如需要修改应该怎么处理?

  • 通过 data 定义属性并将 prop 作为初始值。
  • 用 computed 计算属性去定义依赖 prop 的值。若页面需要更改当前值,可在 get 和 set 方法中更改。

46. 简单介绍一下computed 和 watch的区别

computed

计算属性,依赖其他属性值,且值具备缓存的特性。只有它依赖的属性值发生改变,下一次获取的值才会重新计算。

适用于数值计算,并且依赖于其他属性时。因为可以利用缓存特性,避免每次获取值,都需要重新计算。

watch

观察属性,监听属性值变动。每当属性值发生变化,都会执行相应的回调。

适用于数据变化时执行异步或开销比较大的操作。

47. 组件中的 data 为什么是个函数返回而不是直接一个对象?

对象在栈中存储的都是地址,函数的作用就是属性私有化,保证组件修改自身属性时不会影响其他复用组件。

48. 用到过哪些修饰符?

  • 表单修饰符

lazy: 失去焦点后同步信息 trim: 自动过滤首尾空格 number: 输入值转为数值类型

  • 事件修饰符

stop:阻止冒泡 prevent:阻止默认行为 self:仅绑定元素自身触发 once:只触发一次

  • 鼠标按钮修饰符

left:鼠标左键 right:鼠标右键 middle:鼠标中间键

49. 介绍一下slot 插槽吧

定义:

slot 插槽,可以理解为slot在组件模板中提前占据了位置。当复用组件时,使用相关的slot标签时,标签里的内容就会自动替换组件模板中对应slot标签的位置,作为承载分发内容的出口。

主要作用是复用和扩展组件,做一些定制化组件的处理。

分类:

  1. slot 标签没有name属性,则为默认插槽。
  2. 具备name属性,则为具名插槽。
  3. 作用域插槽:子组件在作用域上绑定的属性将子组件的信息传给父组件使用,这些属性会被挂在父组件接受的对象上。
// 子组件:
<slot name="footer" childProps="子组件">
  作用域插槽内容
</slot>

// 父组件:
<Child v-slot="slotProps">
  {{ slotProps.childProps }}
</Child>

50. 介绍一下 keep-alive?

定义: keep-alive是vue内置组件,keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。 keep-alive 组件有3个属性

  1. include:字符串或正则表达式,名称匹配的组件会被缓存。
  2. exclude:字符串或正则表达式,名称匹配的组件不会被缓存。
  3. max:缓存组件数量阈值

生命周期: 设置 keep-alive 的组件,会增加两个生命钩子(activated / deactivated)。

首次进入组件:beforeCreate -> created -> beforeMount -> mounted -> activated 离开组件触发deactivated,因为组件缓存不销毁,所以不会触发 beforeDestroy 和 destroyed 生命钩子。再次进入组件后直接从 activated 生命钩子开始。

常见业务场景 在列表页的第 2 页进入详情页,详情页返回,依然停留在第 2 页,不重新渲染。但从其他页面进入列表页,还是需要重新渲染。

51. Vue-Router 路由有几种模式?说说他们的区别?

Vue-Router 有 3 种路由模式:hash、history、abstract。

hash 模式 Vue-Router 默认为 hash 模式,基于浏览器的onhashchange事件,地址变化时,通过window.location.hash获取地址上的hash值;根据hash值匹配 routes 对象对应的组件内容。

特点

  1. hash值存在 URL 中,携带#,hash值改变不会重载页面。
  2. hash改变会触发onhashchange事件,可被浏览器记录,从而实现浏览器的前进后退。
  3. hash传参基于url,传递复杂参数会有体积限制。
  4. 兼容性好,支持低版本浏览器和 IE 浏览器。

history 模式 基于HTML5新增的 pushState 和 replaceState 实现在不刷新的情况下,操作浏览器的历史纪录。前者是新增历史记录,后者是直接替换历史记录。

特点

  1. URL 不携带#,利用 pushState 和 replaceState 完成 URL 跳转而无须重新加载页面。
  2. URL 更改会触发 http 请求。所以在服务端需增加一个覆盖所有情况的候选资源
  3. 兼容性 IE10+

abstract 模式 支持所有 JS 运行模式,Vue-Router 自身会对环境做校验,如果发现没有浏览器 API,路由会自动强制进入 abstract 模式。在移动端原生环境也是使用 abstract 模式。

52. 介绍一下Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。 对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。 Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

Vuex 有5个核心概念

  1. State

    1. Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态。
    const Counter = {
       template: `<div>{{ count }}</div>`,
       computed: {
         count () {
           return this.$store.state.count
         }
       }
     }
    
    1. 当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}
  1. Getter

    1. Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性。
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}
  1. mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
  import { mapGetters } from 'vuex'

  export default {
    // ...
    computed: {
    // 使用对象展开运算符将 getter 混入 computed 对象中
      ...mapGetters([
        'doneTodosCount',
        'anotherGetter'
      ])
    }
  }
  1. Mutation

    1. 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
    // vuex中定义
    mutations: {
      increment (state, n) {
        state.count += n
      }
    }
    // 组件中使用
    store.commit('increment', 10)
    
  2. Action

    1. Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。
// 注册一个简单的 action
actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
// 组件中分发
store.dispatch('increment')
  1. 使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用。
import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}
  1. Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

53. 对Vue3有哪些了解?

源码管理

  • vue3整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到packages 目录下面不同的子目录中,这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确。
  • 一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的

typeScritp

Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导。

性能

  • 体积优化:相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,通过Tree shanking没用到的模块都被摇掉,打包的整体体积变小。

  • 编译优化:vue3在diff算法中相比vue2增加了静态标记, 其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较,提高性能;

  • 数据劫持(响应式系统)优化:在vue2中,数据劫持是通过Object.defineProperty,这个 API 有一些缺陷,例如检测不到对象属性的添加和删除,需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题;Vue3中使用Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的;同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归;

支持组合式API(compositon API)

  • 在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合) , 即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API,同时能更好的复用逻辑。

网络、浏览器篇

54. 浏览器从输入URL到渲染页面,发生了什么?

  1. 解析URL: 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
  2. 缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
  3. DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
  4. 获取MAC地址(选说) 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
  5. TCP三次握手: ,确认客户端与服务器的接收与发送能力,下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
  6. HTTPS握手(选说): 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
  7. 发送HTTP请求 服务器处理请求,返回HTTP报文(响应)(文件)
  8. 页面渲染: 浏览器首先会根据 html 文件(响应) 建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
  9. TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。

55. 能不能说一说浏览器的本地存储?各自优劣如何

概述: 浏览器的本地存储主要分为Cookie、WebStorage和IndexDB, 其中WebStorage又可以分为localStorage和sessionStorage。 共同点:

  1. 都是保存在浏览器端、是同源的;

不同点:

  • 是否在http请求中携带

    1. cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。
    2. sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。
  • 存储大小限制也不同

  1. cookie数据不能超过4K,sessionStorage和localStorage可以达到5M
  2. sessionStorage:仅在当前浏览器窗口关闭之前有效;
  3. localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;
  4. cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
  • 作用域不同
  1. sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;
  2. localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在;
  3. cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在;

56. 介绍一下浏览器缓存

定义:

第一次访问网站的时候,浏览器会把网站上的图片、数据等资源下载到本地,当我们再次访问这个网站的时候,浏览器会从本地直接加载资源,这就是缓存。

第一次访问网站,请求服务器,服务器会进行应答,浏览器根据服务器的应答response Header来判断是否对资源进行缓存,如果响应头中有expires、pragma或cache-control字段,代表是强缓存,浏览器就会把资源缓存在memory cache 或 disk cache中。

第二次请求时,浏览器判断请求参数,如果符合强缓存条件就直接返回状态码200,从本地缓存中拿数据。否则把响应参数存在request header请求中,看是否符合协商缓存,符合则返回状态码304,不符合则服务器返回全新资源。

浏览器缓存.png

缓存优点:

  1. 缓存服务器压力,不用每次请求都去服务器拿数据了;
  2. 提升性能,打开本地资源肯定会比请求服务器更快;
  3. 减少带宽消耗,当我们使用缓存时,只会产生很小的网络消耗。

浏览器缓存位置

Service Worker(一个独立线程) -> Memory Cache(内存) -> Disk Cache(硬盘存储) -> Push Cache

强缓存/协商缓存的区别

  1. 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器
  2. 大部分web服务器都默认开启协商缓存;

强缓存

强缓存是当我们访问URL的时候,不会向服务器发送请求,直接从缓存中读取资源,但是会返回200的状态码;

如何设置强缓存?

浏览器根据响应头返回的expires 和 Cache-Control字段判断强缓存;

expires 和 Cache-Control

Cache-Control 是HTTP1.1 中新增的响应头,使用的是相对时间; Expires 是HTTP1.0 中的响应头,指定的是具体的过期时间而不是秒数; Cache-Control 和 Expires同时使用的话,Cache-Control 会覆盖Expires。

Cache-Control都可以设置哪些属性?

  • max-age(单位为s):指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。
  • public : 指定响应可以在代理缓存中被缓存,于是可以被多用户共享。如果没有明确指定private,则默认为public。
  • private : 响应只能在私有缓存中被缓存,不能放在代理缓存上。对一些用户信息敏感的资源,通常需要设置为private。
  • no-cache : 表示必须先与服务器确认资源是否被更改过(依靠If-None-Match和Etag),然后再决定是否使用本地缓存。
  • no-store : 绝对禁止缓存任何资源,也就是说每次用户请求资源时,都会向服务器发送一个请求,每次都会下载完整的资源。通常用于机密性资源。

协商缓存

当浏览器判断不是强缓存,就会向服务器发请求,判断是否是协商缓存。如果是,服务器会返回304 Not Modified,浏览器从缓存中加载。所以协商缓存是强缓存之后的阶段。

即Cache-Control 的值为 no-cache,或者 max-age 过期了等强缓存失效时;

如何设置协商缓存?

  1. Last-Modified 和 If-Modified-Since字段

    • 浏览器第一次向服务器发请求,服务器返回资源并在response header加上Last-Modified字段,表示资源最后修改的时间。
    • 浏览器再次请求这个资源时,请求头会加上If-Modified-Since字段,通过此字段值告诉服务器该资源上次请求返回的最后修改时间。服务器据此判断资源有没有修改过,没有修改过就返回304Not Modified,浏览器从缓存中获取资源;资源有修改过,服务器返回最新资源。
  2. ETag、If-None-Match 字段

    • 浏览器第一次向服务器请求,服务器返回资源并在response header上返回ETag字段。表示资源本身,资源有变化,则该字段有变化。
    • 浏览器再次向服务器请求这个资源时,请求头携带If-None-Match字段。若这两个字段相同,则代表资源没有变化,服务器返回304Not Modified,浏览器从缓存中加载。若两个字段不同,证明资源有变动,服务器返回最新资源。

57. Http状态码有哪些?代表什么意思?

200响应成功 301永久重定向 302临时重定向 304资源缓存 403服务器禁止访问 404服务器资源未找到 500 502服务器内部错误 504 服务器繁忙

58. 遇到过跨域问题吗?如何解决的?

什么是跨域?

出于安全的考虑,浏览器使用同源策略,避免从一个域名的网页去直接请求另一个域名的资源;同源就是协议、域名和端口都相同;

解决方案

  1. 跨域资源共享(CORS)

    1. 跨域资源共享(Cross-Origin Resource Sharing,简称 CORS),是 HTML5 提供的标准跨域解决方案。相较于JSONP的方式,CORS具有更多优势:JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求;
    2. 使用CORS来解决跨域问题需要服务端支持,通过设置Access-Control-Allow-Origin来解决跨域问题;
  2. 反向代理

    1. 一般情况下,我们可以直接通过配置nginx服务器来实现反向代理功能;
  3. vue.config.js 中配置proxy

    devServer: {
      <!-- 跨域 -->
      proxy: {
        <!-- 只要axios请求中带有/api的url,就会触发代理机制 -->
        '/api': {
        <!-- 目标路径:target(我们要代理请求的地址) -->
          target: 'http://xxxxxxxxxxx.xxx/api',
          <!-- // 允许跨域 -->
          changOrigin: true,
          <!-- // 重写路径 api代替了目标路径 -->
          pathRewrite: {
            '^/api': ''
          }
        }
      }
    }
    
  4. 关闭浏览器跨域限制(仅本地开发测试时使用)

    1. 在桌面找到浏览器快捷图标并点击鼠标右键的属性一栏;
    2. 在属性页面中的目标输入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,user-data-dir可以是任意路径;
  5. jsonp方式

59. 说一下http和https区别

  1. HTTP 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
  2. HTTP 是不安全的,而 HTTPS 是安全的
  3. HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
  4. 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
  5. HTTP 无法加密,而HTTPS 对传输的数据进行加密
  6. HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书

60. 你知道有哪些常见的web安全及防护原理

XSS攻击(Cross Site Script跨站脚本攻击): XSS 攻击的本质是将用户数据当成了 HTML 代码一部分来执行,从而混淆原本的语义,产生新的语义。XSS 攻击方式有很多,所有和用户交互的地方,都有可能存在 XSS 攻击。

防护措施:

  1. 配置 HTTP 中的 http-only 头,让前端 JS 不能操作 Cookie。
  2. 输入检查,在用户提交数据时,使用 XssFilter 过滤掉不安全的数据。
  3. 输出检查,在页面渲染的时候,过滤掉危险的数据

CSRF 攻击(Cross-site request forgery跨站请求伪造): 是一种利用用户身份,执行一些用户非本意的操作。

防护措施:

  1. 验证码:每一次请求都要求用户验证,以确保请求真实可靠。 即:利用恶意脚本不能识别复杂的验证码的特点,保证每次请求都是合法的。
  2. Referer 检查:检查发起请求的服务器,是否为目标服务器。即:HTTP 请求中的 Referer 头传递了当前请求的域名,如果此域名是非法服务器的域名,则需要禁止访问。
  3. Token:利用不可预测性原则,每一请求必须带上一段随机码,这段随机码由正常用户保存,黑帽子不知道随机码,也就无法冒充用户进行请求了。

点击劫持 点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

防护措施:

  1. 由于点击劫持主要通过 iframe嵌入,正常网站使用 JS 脚本判断是否被恶意网站嵌入;
  2. 使用 HTTP 中的 x-frame-options 头,控制 iframe 的加载;
  3. 配置 iframe 的 sandbox 属性,sandbox = "allow-same-origin" 则只能加载与主站同域的资源;

SQL 注入攻击

SQL 注入和 XSS 一样,都是违背了数据和代码分离原则导致的攻击方式。

防护措施:

  1. 防止 SQL 注入的最好的办法就是,不要手动拼接 SQL 语句,使用预编译语句绑定变量;
  2. 严格限制数据类型,如果注入了其他类型的数据,直接报错,不允许执行;
  3. 使用安全的存储过程和系统函数。

CRLF 注入攻击(换行符注入攻击):

  1. 如果在 HTTP 请求头中注入 2 个换行符,会导致换行符后面的所有内容都被解析成请求实体部分。
  2. 攻击者通常在 Set-Cookie 时,注入换行符,控制请求传递的内容。

文件上传漏洞

上传文件是网页开发中的一个常见功能,如果不加处理,很容易就会造成攻击。

防护措施:

  1. 将文件上传的目录设置为不可执行;
  2. 判断文件类型,检查 MIME Type,配置白名单,检查后缀名,配置白名单;
  3. 使用随机数改写文件名和文件路径,上传文件后,随机修改文件名,让攻击者无法执行攻击;
  4. 单独设置文件服务器的域名,单独做一个文件服务器,并使用单独的域名,利用同源策略,规避客户端攻击,通常做法是将静态资源存放在 CDN 上。

登录认证攻击

登录认证攻击可以理解为一种破解登录的方法。

防护措施:

  1. 多因素认证,比如密码,动态口令,数字证书,短信验证码一起验证登陆。

应用层拒绝服务攻击(DDOS):

指的是利用大量的请求造成资源过载,导致服务器不可用。

防护措施:

  1. 应用代码做好性能优化,合理使用 Redis、Memcache 等缓存方案,减少 CPU 资源使用率。
  2. 网络架构上做好优化,后端搭建负载均衡,静态资源使用 CDN 进行管理。
  3. 限制请求频率,服务器计算所有 IP 地址的请求频率,筛选出异常的 IP 进行禁用,可以使用 LRU 算法,缓存前 1000 条请求的 IP,如果有 IP 请求频率过高,就进行禁用。
  4. 处理 DDOS 核心思路就是禁用不可信任的用户,确保资源都是被正常的用户所使用。

61. get和post区别

  1. 初级特点
    1. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数
    2. GET在浏览器回退时是无害的,而POST会再次提交请求。
    3. GET产生的URL地址可以被收藏,而POST不可以。
    4. GET请求会被浏览器主动cache,而POST不会,除非手动设置。
    5. GET请求只能进行url编码,而POST支持多种编码方式。
    6. GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
    7. GET请求在URL中传送的参数是有长度限制的,而POST每月。
    8. 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
    9. GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  2. 中级特点 GET和POST本质上没有区别?GET和POST是什么?是HTTP协议中的两种发送请求的方法。HTTP是什么?HTTP是基于TCP/IP的,关于数据如何在万维网中如何通信的协议。HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
  3. 高级特点
    1. GET和POST有一个最大区别:GET产生一个TCP数据包;POST产生两个TCP数据包。
    2. 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
    3. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
    4. GET与POST都有自己的语义,不能随便混用。
    5. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

62. 请介绍一下XMLHTTPRequest对象

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

尽管名称如此,XMLHttpRequest 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP 以外的协议(包括 file:// 和 FTP),尽管可能受到更多出于安全等原因的限制。

使用const xhr = XMLHttpRequest() 生成一个 XMLHttpRequest 实例对象xhr。

xhr有以下的函数:

  1. xhr.open():初始化一个请求;
  2. xhr.send():发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回;
  3. xhr.abort():如果请求已被发出,则立刻中止请求;
  4. xhr.setRequestHeader():设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用 setRequestHeader() 方法。

63. URL有哪些组成部分

一个完整的URL的组成由于:通信协议、主机名(host)、端口号(port)、路径(path)、参数(query)、锚点等组成。

例如:

https://www.baidu.com/?name=zs&&age=15#middle

协议://host:port/path?query#锚点

分解如下:

  1. 通信协议:http https ftp等;

  2. 主机名:这里 www.baidu.com 是域名,通过DNS服务解析成IP地址,当然也可以直接是IP地址。域名的使用是为了帮助用户更容易地记住这个地址。就像人为什么需要取个名字一样,因为身份证号码太难记了;

  3. 端口号:端口号是指Web服务器开放出的端口号,当我们访问一个网站时,只输入主机名还不行,还必须指定端口号。因为一块网卡上可以分配1-65535(2的16次方)个端口,如果只指定主机名,相当于只能定位到这块网卡,但是不知道哪个端口,就无法进行通信,这本身是由TCP/IP协议决定的;端口的分配有一些约定的内容,比如1~1024这些端口通常都是确定的端口,由操作系统或常见服务所占用,1024以后的端口留给用户自由分配。

    一些最常见应用服务的默认端口:http端口80,https端口443,ftp端口21,mysql数据库端口3306等

  4. 路径:就是我们要访问服务器上的哪一个文件,在Web系统中,网站的根目录都使用 '/' 来表示,也就是紧挨着主机名后面的那个 '/' 表示根目录;

  5. 参数:?后面接的就是查询字符串参数,多个查询字符串参数 & 拼接;

  6. 锚点:锚点使用 # 拼接。

  7. 浏览器对象模型BOM有location对象获取当前URL的信息

    1. window.location.hash 返回URL的锚点内容
    2. window.location.host 返回URL的主机名和端口
    3. window.location.hostname 返回URL的主机名
    4. window.location.href 返回完整URL
    5. window.location.pathname 返回URL路径
    6. window.location.port 返回URL端口
    7. window.location.protocol 返回URL协议
    8. window.location.seach 返回URL参数
    9. window.location.origin 返回基础URL,协议+主机名+端口
    10. window.location.domain 返回URL域名

webpack、工程化

64. 用过哪些plugin、loader,他们有什么作用?

Loader:

  1. babel-loader 使用 Babel 加载 ES2015+ 代码并将其转换为 ES5;
  2. ts-loader 像加载 JavaScript 一样加载 TypeScript 2.0+;
  3. html-loader 将 HTML 导出为字符串,需要传入静态资源的引用路径;
  4. style-loader 将模块导出的内容作为样式并添加到 DOM 中;
  5. css-loader 加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码;
  6. less-loader 加载并编译 LESS 文件;
  7. sass-loader 加载并编译 SASS/SCSS 文件;
  8. vue-loader 加载并编译 Vue 组件;

Plugin:

  1. html-webpack-plugin (打包html文件)自动生成一个index.html文件(可以指定模板)将打包的js文件,自动通过script标签插入到body中;
  2. uglifyjs-webpack-plugin 压缩和混淆代码,不支持es6压缩;
  3. terser-webpack-plugin 压缩和混淆代码,支持es6压缩;
  4. webpack-parallel-uglify-plugin 多进程执行代码压缩,提升构建速度;
  5. mini-css-extract-plugin 分离样式文件,css提取为独立文件,支持按需加载;
  6. clean-webpack-plugin 打包前清空目录;
  7. extract-text-webpack-plugin 将js文件中引用的样式单例抽离成css;
  8. HotModuleReplacementPlugin 热加载;
  9. optimize-css-assets-webpack-plugin 优化css,去掉重复的css;
  10. compression-webpack-plugin 生产环境可采用gzip压缩js和css;
  11. happypack 通过多进程模型,来加速代码构建。

65. Webpack构建项目的流程你能说一下吗?

Webpack启动后,从entry开始,去递归解析entry依赖的所有module,再找到每一个module的时候,会根据module.rules里配置的不同loader进行相应的转换,对module进行转换后再解析出当前module依赖的其他的一些模块,这些module在entry里面,它会进行分组,解析成一个个的chunk,最后webpack会将所有chunk转换成文件输出的output,在整个构建流程中,通过plugin注入钩子,最后输出多个模块组合成的文件。

根据入口文件entry的依赖关系(内部依赖图),将资源全部引进来,形成chunk代码块,chunk会根据不同的资源进行处理,这个处理过程叫打包;输出的东西,叫做bundle。

66. 为什么Vite的速度比Webpack快?

Vite快主要体现在两个方面:快速的冷启动和快速的热更新。而vite之所以能如此优秀,完全基于他借助了浏览器对ESM规范的支持,采取了与Webpack完全不同的unbundle机制。

Vite在开发时不需要构建、分解 模块依赖图,源文件之间的依赖关系完全通过浏览器对 ESM 规范的支持来解析。这就使得 dev server 在启动过程中只需做一些初始化的工作,剩下的完全由浏览器支持。

Webpack需要构建模块依赖图,建立整个项目源文件之间的依赖关系,将数量庞大的源文件合并为少量的几个输出文件,造成Webpack构建速度相对变慢。

小结

本文收集了66道最基础最常见前端面试题,希望能对你面试有帮助!