web前端面试题(必背面试题)

css系列

面试官:说说你对盒子模型的理解

一、是什么

所有元素都可以有像盒子一样的平面空间和外形

一个盒子由四部分组成:context ,padding,margin,border

content:实际内容,显示文本和图像

padding:内边距,清除内容周边的区域,内边距是透明的,不能取负值,受盒子的background属性影响

margin:外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域

下面来段代码:


盒子模型

当我们在浏览器查看元素时,却发现元素的大小变成了240px

这是因为,在CSS中,盒子模型可以分成:

  • W3C 标准盒子模型
  • IE 怪异盒子模型

默认情况下,盒子模型为W3C 标准盒子模型

二、标准盒子模型

标准盒子模型,是浏览器默认的盒子模型

  • 盒子总宽度 = width + padding + border + margin;

  • 盒子总高度 = height + padding + border + margin

也就是,width/height 只是内容高度,不包含 padding 和 border

所以上面问题中,设置width为200px,但由于存在padding,但实际上盒子的宽度有240px

三、IE 怪异盒子模型

  • 盒子总宽度 = width + margin;

  • 盒子总高度 = height + margin;

也就是,width/height 包含了 padding和 border

Box-sizing

CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度

box-sizing: content-box|border-box|inherit:

  • content-box 默认值,元素的 width/height 不包含padding,border,与标准盒子模型表现一致
  • border-box 元素的 width/height 包含 padding,border,与怪异盒子模型表现一致
  • inherit 指定 box-sizing 属性的值,应该从父元素继承

 回到上面的例子里,设置盒子为 border-box 模型


盒子模型

这时,盒子所占据的宽度为200px

面试官:css选择器有哪些?优先级?哪些属性可以继承?

一、选择器

CSS选择器是CSS规则的第一部分

它是元素和其他部分组合起来告诉浏览器哪个HTML元素应当是被选为应用规则中的CSS属性值的方式

选择器所选择的元素,叫做“选择器的对象”

关于css属性选择器常用的有:

  • id选择器(#box),选择id为box的元素

  • 类选择器(.one),选择类名为one的所有元素

  • 标签选择器(div),选择标签为div的所有元素

  • 后代选择器(#box div),选择id为box元素内部所有的div元素

  • 子选择器(.one>one_1),选择父元素为.one的所有.one_1的元素

  • 相邻同胞选择器(.one+.two),选择紧接在.one之后的所有.two元素

  • 群组选择器(div,p),选择div、p的所有元素

 还有一些使用频率相对没那么多的选择器:

  • 伪类选择器
:link :选择未被访问的链接              
:visited:选取已被访问的链接            
:active:选择活动链接                   
:hover :鼠标指针浮动在上面的元素        
:focus :选择具有焦点的                 
:first-child:父元素的首个子元素
  • 伪元素选择器
:first-letter :用于选取指定选择器的首字母
:first-line :选取指定选择器的首行
:before : 选择器在被选元素的内容前面插入内容
:after : 选择器在被选元素的内容后面插入内容
  • 属性选择器
[attribute] 选择带有attribute属性的元素
[attribute=value] 选择所有使用attribute=value的元素
[attribute~=value] 选择attribute属性包含value的元素
[attribute|=value]:选择attribute属性以value开头的元素

CSS3中新增的选择器有如下:

  • 层次选择器(p~ul),选择前面有p元素的每个ul元素
  • 伪类选择器
:first-of-type 表示一组同级元素中其类型的第一个元素
:last-of-type 表示一组同级元素中其类型的最后一个元素
:only-of-type 表示没有同类型兄弟元素的元素
:only-child 表示没有任何兄弟的元素
:nth-child(n) 根据元素在一组同级中的位置匹配元素
:nth-last-of-type(n) 匹配给定类型的元素,基于它们在一组兄弟元素中的位置,从末尾开始计数
:last-child 表示一组兄弟元素中的最后一个元素
:root 设置HTML文档
:empty 指定空的元素
:enabled 选择可用元素
:disabled 选择被禁用元素
:checked 选择选中的元素
:not(selector) 选择与  不匹配的所有元素
  • 属性选择器
[attribute*=value]:选择attribute属性值包含value的所有元素
[attribute^=value]:选择attribute属性开头为value的所有元素
[attribute$=value]:选择attribute属性结尾为value的所有元素

二、优先级

!important >内联 > ID选择器 > 类选择器 > 标签选择器

三、继承属性

css中,继承是指的是给父元素设置一些属性,后代元素会自动拥有这些属性

关于继承属性,可以分成:

  • 字体系列属性
font:组合字体
font-family:规定元素的字体系列
font-weight:设置字体的粗细
font-size:设置字体的尺寸
font-style:定义字体的风格
font-variant:偏大或偏小的字体
  • 文本系列属性
text-indent:文本缩进
text-align:文本水平对刘
line-height:行高
word-spacing:增加或减少单词间的空白
letter-spacing:增加或减少字符间的空白
text-transform:控制文本大小写
direction:规定文本的书写方向
color:文本颜色
  • 元素可见性
visibility
  • 表格布局属性
caption-side:定位表格标题位置
border-collapse:合并表格边框
border-spacing:设置相邻单元格的边框间的距离
empty-cells:单元格的边框的出现与消失
table-layout:表格的宽度由什么决定
  • 列表属性
list-style-type:文字前面的小点点样式
list-style-position:小点点位置
list-style:以上的属性可通过这属性集合
  • 引用
quotes:设置嵌套引用的引号类型
  • 光标属性
cursor:箭头可以变成需要的形状

继承中比较特殊的几点:

  • a 标签的字体颜色不能被继承

  • h1-h6标签字体的大下也是不能被继承的

无继承的属性

  • display

  • 文本属性:vertical-align、text-decoration

  • 盒子模型的属性:宽度、高度、内外边距、边框等

  • 背景属性:背景图片、颜色、位置等

  • 定位属性:浮动、清除浮动、定位position等

  • 生成内容属性:content、counter-reset、counter-increment

  • 轮廓样式属性:outline-style、outline-width、outline-color、outline

  • 页面样式属性:size、page-break-before、page-break-after

面试官:元素水平垂直居中的方法有哪些?如果元素不定宽高呢?

一、背景

在开发中经常遇到这个问题,即让某个元素的内容在水平和垂直方向上都居中,内容不仅限于文字,可能是图片或其他元素

居中是一个非常基础但又是非常重要的应用场景,实现居中的方法存在很多,可以将这些方法分成两个大类:

  • 居中元素(子元素)的宽高已知
  • 居中元素宽高未知

二、实现方式

实现元素水平垂直居中的方式:

  • 利用定位+margin:auto

  • 利用定位+margin:负值

  • 利用定位+transform

  • table布局

  • flex布局

  • grid布局

利用定位+margin:auto

先上代码:


父级设置为相对定位,子级绝对定位 ,并且四个定位属性的值都设置了0,那么这时候如果子级没有设置宽高,则会被拉开到和父级一样宽高

这里子元素设置了宽高,所以宽高会按照我们的设置来显示,但是实际上子级的虚拟占位已经撑满了整个父级,这时候再给它一个margin:auto它就可以上下左右都居中了

利用定位+margin:负值

绝大多数情况下,设置父元素为相对定位, 子元素移动自身50%实现水平垂直居中


web前端面试题(必背面试题)_第1张图片

  • 初始位置为方块1的位置
  • 当设置left、top为50%的时候,内部子元素为方块2的位置
  • 设置margin为负数时,使内部子元素到方块3的位置,即中间位置

这种方案不要求父元素的高度,也就是即使父元素的高度变化了,仍然可以保持在父元素的垂直居中位置,水平方向上是一样的操作

但是该方案需要知道子元素自身的宽高,但是我们可以通过下面transform属性进行移动

利用定位+transform

实现代码如下:


translate(-50%, -50%)将会将元素位移自己宽度和高度的-50%

这种方法其实和最上面被否定掉的margin负值用法一样,可以说是margin负值的替代方案,并不需要知道自身元素的宽高

table布局

设置父元素为display:table-cell,子元素设置 display: inline-block。利用verticaltext-align可以让所有的行内块级元素水平垂直居中


flex弹性布局

还是看看实现的整体代码:


css3中了flex布局,可以非常简单实现垂直水平居中

这里可以简单看看flex布局的关键属性作用:

  • display: flex时,表示该容器内部的元素将按照flex进行布局

  • align-items: center表示这些元素将相对于本容器水平居中

  • justify-content: center也是同样的道理垂直居中

grid网格布局


这里看到,gird网格布局和flex弹性布局都简单粗暴

面试官:怎么理解回流跟重绘?什么场景下会触发?

一、是什么

HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:

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

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

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

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

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

二、如何触发

回流触发时机

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

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染的时候(这避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

还有一些容易被忽略的操作:获取一些特定属性的值

offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流

除此还包括getComputedStyle方法,原理是一样的

重绘触发时机

触发回流一定会触发重绘

可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)

除此之外还有一些其他引起重绘行为:

  • 颜色的修改

  • 文本方向的修改

  • 阴影的修改

浏览器优化机制

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

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

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

三、如何减少

我们了解了如何触发回流和重绘的场景,下面给出避免回流的经验:

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

在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能

但有时候,我们会无可避免地进行回流或者重绘,我们可以更好使用它们

例如,多次修改一个把元素布局的时候,我们很可能会如下操作

const el = document.getElementById('el')
for(let i=0;i<10;i++) {
    el.style.top  = el.offsetTop  + 10 + "px";
    el.style.left = el.offsetLeft + 10 + "px";
}

每次循环都需要获取多次offset属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求

// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft, offTop = el.offsetTop

// 在JS层面进行计算
for(let i=0;i<10;i++) {
  offLeft += 10
  offTop  += 10
}

// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop  + "px"

我们还可避免改变样式,使用类名去合并样式

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

使用类名去合并样式


前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),

都去触发一次渲染树更改,从而导致相应的回流与重绘过程

合并之后,等于我们将所有的更改一次性发出

我们还可以通过通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

离线操作后

let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
container.style.display = 'block'

面试官:什么是响应式设计?响应式设计的基本原理是什么?如何做?

一、是什么

响应式网站设计(Responsive Web design)是一种网络页面设计布局,页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整

描述响应式界面最著名的一句话就是“Content is like water”

大白话便是“如果将屏幕看作容器,那么内容就像水一样”

响应式网站常见特点:

  • 同时适配PC + 平板 + 手机等

  • 标签导航在接近手持终端设备时改变为经典的抽屉式导航

  • 网站的布局会根据视口来调整模块的大小和位置

web前端面试题(必背面试题)_第2张图片

 二、实现方式

响应式设计的基本原理是通过媒体查询检测不同的设备屏幕尺寸做处理,为了处理移动端,页面头部必须有meta声明viewport

媒体查询

CSS3中的增加了更多的媒体查询,就像if条件表达式一样,我们可以设置不同类型的媒体条件,并根据对应的条件,给相应符合条件的媒体调用相对应的样式表

使用@Media查询,可以针对不同的媒体类型定义不同的样式,如:

@media screen and (max-width: 1920px) { ... }

当视口在375px - 600px之间,设置特定字体大小18px

@media screen (min-width: 375px) and (max-width: 600px) {
  body {
    font-size: 18px;
  }
}

通过媒体查询,可以通过给不同分辨率的设备编写不同的样式来实现响应式的布局,比如我们为不同分辨率的屏幕,设置不同的背景图片

比如给小屏幕手机设置@2x图,为大屏幕手机设置@3x图,通过媒体查询就能很方便的实现

百分比

通过百分比单位 " % " 来实现响应式的效果

比如当浏览器的宽度或者高度发生变化时,通过百分比单位,可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果

heightwidth属性的百分比依托于父标签的宽高,但是其他盒子属性则不完全依赖父元素:

  • 子元素的top/left和bottom/right如果设置百分比,则相对于直接非static定位(默认定位)的父元素的高度/宽度

  • 子元素的padding如果设置百分比,不论是垂直方向或者是水平方向,都相对于直接父亲元素的width,而与父元素的height无关。

  • 子元素的margin如果设置成百分比,不论是垂直方向还是水平方向,都相对于直接父元素的width

  • border-radius不一样,如果设置border-radius为百分比,则是相对于自身的宽度

可以看到每个属性都使用百分比,会照成布局的复杂度,所以不建议使用百分比来实现响应式

vw/vh

vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度。 任意层级元素,在使用vw单位的情况下,1vw都等于视图宽度的百分之一

与百分比布局很相似,在以前文章提过与%的区别,这里就不再展开述说

rem

在以前也讲到,rem是相对于根元素htmlfont-size属性,默认情况下浏览器字体大小为16px,此时1rem = 16px

可以利用前面提到的媒体查询,针对不同设备分辨率改变font-size的值,如下:

@media screen and (max-width: 414px) {
  html {
    font-size: 18px
  }
}

@media screen and (max-width: 375px) {
  html {
    font-size: 16px
  }
}

@media screen and (max-width: 320px) {
  html {
    font-size: 12px
  }
}

为了更准确监听设备可视窗口变化,我们可以在css之前插入script标签,内容如下:

//动态为根元素设置字体大小
function init () {
    // 获取屏幕宽度
    var width = document.documentElement.clientWidth
    // 设置根元素字体大小。此时为宽的10等分
    document.documentElement.style.fontSize = width / 10 + 'px'
}

//首次加载应用,设置一次
init()
// 监听手机旋转的事件的时机,重新设置
window.addEventListener('orientationchange', init)
// 监听手机窗口变化,重新设置
window.addEventListener('resize', init)

无论设备可视窗口如何变化,始终设置remwidth的1/10,实现了百分比布局

除此之外,我们还可以利用主流UI框架,如:element uiantd提供的栅格布局实现响应式

 三、总结

响应式布局优点可以看到:

  • 面对不同分辨率设备灵活性强
  • 能够快捷解决多设备显示适应问题

缺点:

  • 仅适用布局、信息、框架并不复杂的部门类型网站
  • 兼容各种设备工作量大,效率低下
  • 代码累赘,会出现隐藏无用的元素,加载时间加长
  • 其实这是一种折中性质的设计解决方案,多方面因素影响而达不到最佳效果
  • 一定程度上改变了网站原有的布局结构,会出现用户混淆的情况

面试官:如果要做优化,CSS提高性能的方法有哪些?

一、前言

css主要是用来完成页面布局的,像一些细节或者优化,

减少css嵌套,最好不要套三层以上。

不要在ID选择器前面进行嵌套,ID本来就是唯一的而且人家权值那么大,嵌套完全是浪费性能。

建立公共样式类,把相同样式提取出来作为公共类使用,比如我们常用的清除浮动等。

不用css表达式,css表达式对性能的浪费可能是超乎你的想象的

二、实现方式

实现方式有很多种,主要有如下:

  • 内联首屏关键CSS
  • 异步加载CSS
  • 资源压缩
  • 合理使用选择器
  • 减少使用昂贵的属性
  • 不要使用@import

内联首屏关键CSS

在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联css关键代码能够使浏览器在下载完html后就能立刻渲染

而如果外部引用css代码,在解析html结构过程中遇到外部css文件,才会开始下载css代码,再渲染

所以,CSS内联使用使渲染时间提前

注意:但是较大的css代码并不合适内联(初始拥塞窗口、没有缓存),而其余代码则采取外部引用方式

异步加载CSS

CSS文件请求、下载、解析完成之前,CSS会阻塞渲染,浏览器将不会渲染任何已处理的内容

前面加载内联代码后,后面的外部引用css则没必要阻塞浏览器渲染。这时候就可以采取异步加载的方案,主要有如下:

// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

  • 设置link标签media属性为noexis,浏览器会认为当前样式表不适用当前类型,会在不阻塞页面渲染的情况下再进行下载。加载完成后,将media的值设为screenall,从而让浏览器开始解析CSS
  • 通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel设回stylesheet

资源压缩

利用webpackgulp/gruntrollup等模块化工具,将css代码进行压缩,使文件变小,大大降低了浏览器的加载时间

合理使用选择器

css匹配的规则是从右往左开始匹配,例如#markdown .content h3匹配规则如下:

  • 先找到h3标签元素
  • 然后去除祖先不是.content的元素
  • 最后去除祖先不是#markdown的元素

如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高

所以我们在编写选择器的时候,可以遵循以下规则:

  • 不要嵌套使用过多复杂选择器,最好不要三层以上
  • 使用id选择器就没必要再进行嵌套
  • 通配符和属性选择器效率最低,避免使用

减少使用昂贵的属性

在页面发生重绘的时候,昂贵属性如box-shadow/border-radius/filter/透明度/:nth-child等,会降低浏览器的渲染性能

不要使用@import

css样式文件有两种引入方式,一种是link元素,另一种是@import

@import会影响浏览器的并行下载,使得页面在加载时增加额外的延迟,增添了额外的往返耗时

而且多个@import可能会导致下载顺序紊乱

比如一个css文件index.css包含了以下内容:@import url("reset.css")

那么浏览器就必须先把index.css下载、解析和执行后,才下载、解析和执行第二个文件reset.css

面试官:说说JavaScript中的数据类型?存储上的差别?

js的数据类型分为两类,一个是基本数据类型,一个是引用数据类型

基本数据类型有undefinednullbooleannumberstringsymbol

引用数据类型有 object

在js的执行过程中,主要有三种数据类型内存空间,分别是代码空间,栈空间,堆空间,其中的代码空间主要是存储可执行代码的,原始类型的数据值都是直接保存在栈中的,引用数据类型的值是存放在堆空间中的, 原始数据类型存储的是变量的值,而引用数据类型存储的是其在堆空间中的地址

JavaScript系列 

面试官:typeof 与 instanceof 区别

一、typeof

typeof 对于原始数据类型来说,除了null都可以正确的显示类型

使用方法如下:

typeof operand
typeof(operand)

operand表示对象或原始值的表达式,其类型将被返回

举个例子

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

从上面例子,前6个都是基础数据类型。虽然typeof nullobject,但这只是JavaScript 存在的一个悠久 Bug,不代表null就是引用数据类型,并且null本身也不是对象

所以,null在 typeof之后返回的是有问题的结果,不能作为判断null的方法。如果你需要在 if 语句中判断是否为 null,直接通过===null来判断就好

同时,可以发现引用类型数据,用typeof来判断的话,除了function会被识别出来之外,其余的都输出object

如果我们想要判断一个变量是否存在,可以使用typeof:(不能使用if(a), 若a未声明,则报错)

if(typeof a != 'undefined'){
    //变量存在
}

 二、instanceof

 instanceof 可以正确显示数据类型, 因为它是通过对象的原型链来进行判断的,

使用如下:

object instanceof constructor

object为实例对象,constructor为构造函数

构造函数通过new可以实例对象,instanceof能判断这个对象是否是之前那个构造函数生成的对象

// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false

 关于instanceof的实现原理,可以参考下面:

function myInstanceof(left, right) {
    // 这里先用typeof来判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) return false;
    // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {                  
        if(proto === null) return false;
        if(proto === right.prototype) return true;//找到相同原型对象,返回true
        proto = Object.getPrototypeof(proto);
    }
}

 也就是顺着原型链去找,直到找到相同的原型对象,返回true,否则为false 

三、区别 

typeofinstanceof都是判断数据类型的方法,区别如下:

  • typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值

  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型

  • typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串

如下

Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上结果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

了解了toString的基本用法,下面就实现一个全局通用的数据类型判断方法

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回
    return type;
  }
  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); 
}

使用如下

getType([])     // "Array" typeof []是object,因此toString返回
getType('123')  // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null)   // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined)   // "undefined" typeof 直接返回
getType()            // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g)      //"RegExp" toString返回

面试官:说说你对闭包的理解?闭包使用场景

闭包就是可以访问其他函数内部变量的函数,我们通常用它来定义私有化的变量和方法,创建一个闭包最简单的方法就是在一个函数内创建一个函数,它有三个特性是 函数内可以再嵌套函数,内部函数可以访问外部的方法和变量,方法和变量不会被垃圾回收机制回收,

一、是什么

闭包最简单的方法就是在一个函数内创建一个函数,闭包让你可以在一个内层函数中访问到其外层函数的作用域

二、使用场景

任何闭包的使用场景都离不开这两点:

  • 创建私有变量
  • 延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

创建私有变量:好比vue里的data 每个data都是一个闭包所以他们互不干扰

三、优缺点

它的优点就是可以实现封装和缓存,缺点就是可能会造成内存泄漏的问题

面试官:bind、call、apply 区别?如何实现一个bind?

关于call、apply、bind函数,它们主要用来改变this指向的

call的用法

fn.call(thisArg, arg1, arg2, arg3, ...)

调用fn.call时会将fn中的this指向修改为传入的第一个参数thisArg;将后面的参数传入给fn,并立即执行函数fn。

let obj = {
        name: "xiaoming",
        age: 24,
        sayHello: function (job, hobby) {
            console.log(`我叫${this.name},今年${this.age}岁。我的工作是: ${job},我的爱好是: ${hobby}。`);
        }
    }
    obj.sayHello('程序员', '看美女'); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 看美女。


    let obj1 = {
        name: "lihua",
        age: 30
    }
    // obj1.sayHello(); // Uncaught TypeError: obj1.sayHello is not a function
    obj.sayHello.call(obj1, '设计师', '画画'); // 我叫lihua,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

apply的用法

apply(thisArg, [argsArr])

fn.apply的作用和call相同:修改this指向,并立即执行fn。区别在于传参形式不同,apply接受两个参数,第一个参数是要指向的this对象,第二个参数是一个数组,数组里面的元素会被展开传入fn,作为fn的参数。

bind的用法

bind(thisArg, arg1, arg2, arg3, ...)

fn.bind的作用是只修改this指向,但不会立即执行fn;会返回一个修改了this指向后的fn。需要调用才会执行:bind(thisArg, arg1, arg2, arg3, ...)()bind的传参和call相同。

obj.sayHello.bind(obj1, '设计师', '画画'); // 无输出结果

obj.sayHello.bind(obj1, '设计师', '画画')(); // 我叫lihua,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

bind、call、apply的区别

1、相同点
三个都是用于改变this指向;
接收的第一个参数都是this要指向的对象;
都可以利用后续参数传参。
2、不同点
call和bind传参相同,多个参数依次传入的;
apply只有两个参数,第二个参数为数组;
call和apply都是对函数进行直接调用,而bind方法不会立即调用函数,而是返回一个修改this后的函数。 

面试官:说说你对事件循环的理解

一、是什么

JavaScript是一门单线程的语言,在JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行

  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

同步任务与异步任务的运行流程图如下:

web前端面试题(必背面试题)_第3张图片

 从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

二、宏任务与微任务

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then

  • MutaionObserver

  • Object.observe(已废弃;Proxy 对象替代)

  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示

web前端面试题(必背面试题)_第4张图片

 三、async与await

async

async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行

async函数返回一个promise对象,下面两种方法是等效的

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await

正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值

async function f(){
    // 等同于
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码 

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1fn232

面试官:DOM常见的操作有哪些?

一、DOM

任何 HTMLXML文档都可以用 DOM表示为一个由节点构成的层级结构

content

上述结构中,divp就是元素节点,content就是文本节点,title就是属性节点

二、操作

DOM操作才能有助于我们理解框架深层的内容

下面就来分析DOM常见的操作,主要分为:

  • 创建节点
  • 查询节点
  • 更新节点
  • 添加节点
  • 删除节点

创建节点

createEleme

创建新元素,接受一个参数,即要创建元素的标签名

const divEl = document.createElement("div");

createTextNode

创建一个文本节点

const textEl = document.createTextNode("content");

获取节点

querySelector

传入任何有效的css 选择器,即可选中单个 DOM元素(首个):

document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

关于获取DOM元素的方法还有如下,就不一一述说

document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器');  仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器');   返回所有匹配的元素
document.documentElement;  获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all[''];  获取页面中的所有元素节点的对象集合型

更新节点

innerHTML

不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树

// 获取

...

var p = document.getElementById('p'); // 设置文本为abc: p.innerHTML = 'ABC'; //

ABC

// 设置HTML: p.innerHTML = 'ABC RED XYZ'; //

...

的内部结构已修改

innerText、textContent

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本

 添加节点

innerHTML

如果这个DOM节点是空的,例如,

,那么,直接使用innerHTML = 'child'就可以修改DOM节点的内容,相当于添加了新的DOM节点

如果这个DOM节点不是空的,那就不能这么做,因为innerHTML会直接替换掉原来的所有子节点

appendChild

把一个子节点添加到父节点的最后一个子节点

举个例子


    

JavaScript

Java

Python

Scheme

 现在HTML结构变成了下面


Java

Python

Scheme

JavaScript

上述代码中,我们是获取DOM元素后再进行添加操作,这个js节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置

如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置

const list = document.getElementById('list'),
const haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

insertBefore

把子节点插入到指定的位置,使用方法如下:

parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

删除节点

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉

// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置

面试官:说说你对BOM的理解,常见的BOM对象你了解哪些?

一、是什么 

BOM (Browser Object Model),浏览器对象模型,提供了内容与浏览器窗口进行交互的对象

 其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新

 二、window

Bom的核心对象是window,它表示浏览器的一个实例

在浏览器中 即是浏览器窗口的一个接口,又是全局对象

三、location

window对象给我们提供了一个location属性用于获取或设置窗体的URL,并且可以用于解析URL。

四、navigator 

navigator 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂

 五、screen

保存的是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度

六、history

history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转

面试官:Javascript本地存储的方式有哪些?区别及应用场景?

javaScript本地缓存的方法我们主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB

Cookie,用户发送请求时会携带cookie到服务端,服务端会判断cookie来识别用户,cookie存储一般不超过 4KB 的小型文本数据,但是cookie在每次请求中都会被发送,如果不使用 HTTPS并对其加密,其保存的信息很容易被窃取,

localStorage

HTML5新方法,IE8及以上浏览器都兼容

特点

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
  • 存储的信息在同一域中是共享的
  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 大小:5M(跟浏览器厂商有关系)
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
  • 受同源策略的限制

sessionStorage

sessionStorage和 localStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据

扩展的前端存储方式 

indexedDB是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。

面试官:什么是防抖和节流?有什么区别?如何实现?

一、是什么

本质上是优化高频率执行代码的一种手段

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:

电梯第一个人进来后,15秒后准时运送一次,这是节流

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

二、区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

面试官:如何通过js判断一个数组?

1.通过instanceof判断

instanceof运算符用于检验构造函数的prototype属性是否出现在对象的原型链中的任何位置,返回一个布尔值

web前端面试题(必背面试题)_第5张图片

  2.通过constructor判断

实例的构造函数属性constructor指向构造函数,通过constructor属性可以判断是否为一个数组

  3.通过Object.prototype.toString.call()判断

Object.prototype.toString.call()可以获取到对象的不同类型

  4.通过Array.isArray()判断

Array.isArray()用于确定传递的值是否是一个数组,返回一个布尔值

 面试官:说说你对作用域链的理解?

 一、作用域

我们一般将作用域分成:

  • 全局作用域

  • 函数作用域

  • 块级作用域

二、作用域链

 function fn(){
            let age=18;
            function inner(){
                // 函数内访问变量时,优先使用自己内部声明变量
                // 如果没有,尝试访问外部函数作用域的变量
                // 如果外部函数作用域也没有这个变量,继续往下找
                // 知道找到全局,如果全局都没有,就报错
                let count=100;
                console.log(count);
                console.log(age);
                console.log(name);
            }
            inner()
        }
        fn()

如上图:

  • inner函数访问自身的变量时,没有找到。则访问外部中的变量,还是没有找到就往全局找
  • 这样一层一层形成的链叫做作用域链
  • 当然全局还没有找到最外面一层是null 会报错

面试官:什么是浏览器缓存?

浏览器缓存就是把一个已经请求过的web资源(如html页面,图片,JS,数据)拷贝一份放在浏览器中。缓存会根据进来的请求保存输入内容的副本。当下一个请求到来的时候,如果是相同的URL,浏览器会根据缓存机制决定是直接使用副本响应访问请求还是向源服务器再次发起请求。

使用缓存的原因

(1)减少网络带宽消耗

(2)降低服务器压力

(3)减少网络延迟

浏览器的缓存机制

过期机制:指的是缓存副本的有效期。一个缓存的副本必须满足以下条件,浏览器会认为它是有效的,足够新的:

1.含有完整的过期时间控制头信息(HTTP协议报头),并且仍在有效期内

2.浏览器已经使用过这个缓存的副本,并且会在一个会话中已经检查过新鲜度(即服务器上的资源是否发生改变)
满足以上两种情况的一种,浏览器会直接从缓存中获取副本进行渲染

校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签Etag(Entity Tag),它可以用来作为浏览器再次请求过程中的校验标识,如果发现校验标识不匹配,说明资源已经被修改或者过期,浏览器需要重新获取资源内容。

浏览器缓存的控制

(1)使用meta标签
Web开发者可以在HTML页面的节点中加入标签,代码如下:

  

上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器上拉取。但是这种禁用缓存的形式很有限:
1.仅有IE才能识别这段meta标签的含义,其他主流浏览器仅识别”Cache-Control:no-store”的meta标签
2.在IE中识别到该meta标签的含义,并不一定会在请求字段中加上Pragma,但的确会让当前页面每次都发起新请求(仅限页面,页面上的资源则不受影响)

(2)使用缓存有关的HTTP消息报头
在HTTP请求和响应的消息报头中,常见与缓存有关的消息报头有:

web前端面试题(必背面试题)_第6张图片

 web前端面试题(必背面试题)_第7张图片

 不同字段间的比较:

在配置Last-Modified/Etag的情况下,浏览器再次访问统一的URI资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器只发送一个304给浏览器,告诉浏览器直接从自己的本地缓存取数据,如果修改过,那就将整个数据重新发送给浏览器。

Cache-Control/Expires则不同,如果检测到本地的缓存还在有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用的时候,Cache-Control/Expires的优先级要高于Last-Modified/Etag。

即当当地副本根据Cache-Control/Expires发现还在有效期内,则不会再次发送请求去服务器询问修改时间(Last-Modified)或者实体标识符(Etag)了。
一般情况下,使用Cache-Control/Expires会配合Last-Modified/Etag一起使用,因为即使浏览器设置缓存时间,当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这是Last-Modified/Etag将能够很好的利用304,从而减少响应开销。

浏览器向服务器请求资源的过程

web前端面试题(必背面试题)_第8张图片

 关于缓存的两个概念

 强缓存:
用户发送的请求,直接从客户端缓存中获取,不发送请求到服务器,不与服务器发生交互行为。
协商缓存:
用户发送请求,发送到服务器之后,由服务器判定是否从缓存中获取资源。
两者共同点:客户端获取的数据最后都是熊客户端的缓存中取得。
两者区别:从名字就可以看出,强缓存不与服务器发生交互,而协商缓存则需要需服务器发生交互。

面试官:什么是原型和原型链?

一、原型

①所有引用类型都有一个__proto__(隐式原型) 属性,属性值是一个普通对象

②所有函数都有一个prototype(原型),属性值是一个普通对象

③所有引用类型的__proto__属性指向他的构造函数的prototype

var a = [1,2,3];
a.__proto__ === Array.prototype; // true

 二、原型链

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

举例,有以下代码

function Parent(month){
    this.month = month;
}

var child = new Parent('Ann');

console.log(child.month); // Ann

console.log(child.father); // undefined

在child中查找某个属性时,会执行下面步骤: 

web前端面试题(必背面试题)_第9张图片

 访问链路为:

web前端面试题(必背面试题)_第10张图片

 ①一直往上层查找,直到到null还没有找到,则返回undefined
Object.prototype.__proto__ === null
③所有从原型或更高级原型中的得到、执行的方法,其中的this在执行时,指向当前这个触发事件执行的对象

 面试官:JS中null和undefined的区别

  1. 相同点
     if 判断语句中,两者都会被转换为false

        都是是 JavaScript 基本类型之一
  2. 不同点

            Number转换的值不同Number(null)输出为0, Number(undefined)输出为NaN

Undefined,当声明的变量还未被初始化时,变量的默认值为undefined。
Null,null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。

面试官:JS中的“use strict” 严格模式

JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性JavaScript变体的一种方式,即在严格的条件下运行 JS 代码。

严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。

严格模式对正常的 JavaScript 语义做了一些更改:

消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
消除代码运行的一些不安全之处,保证代码运行的安全。
提高编译器效率,增加运行速度。
禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class,enum,export, extends, import, super 不能做变量名

面试官:同步与异步的区别

同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。

异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。

打个比方:同步的时候,你在写程序,然后你妈妈叫你马上拖地,你就必须停止写程序然后拖地,没法同时进行。而异步则不需要按部就班,可以在等待那个动作的时候同时做别的动作,打个比方:你在写程序,然后你妈妈让你马上拖地,而这时你就贿赂你弟弟帮你拖地,于是结果同样是拖好地,你可以继续敲你的代码而不用管地是怎么拖

面试官:箭头函数与普通函数的区别

一,普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。

// 具名函数
function func(){
  // code
}
 
// 匿名函数
let func=function(){
  // code
}

// 箭头函数全都是匿名函数
let func=()=>{
  // code
}

二、箭头函数不能用于构造函数,不能使用new

普通函数可以用于构造函数,以此创建对象实例。

三、箭头函数中this的指向不同

在普通函数中,this总是指向调用它的对象,如果用作构造函数,this指向创建的对象实例。

箭头函数没有prototype(原型),所以箭头函数本身没有this

箭头函数的this指向在定义的时候继承自外层第一个普通函数的this

面试官:JS 对象和数组的遍历方法,以及方式的比较

一、JS 遍历数组

1、for 循环遍历数组

// 1、for循环
let arr = ['d', 'a', 'w', 'n'];
for (let i = 0; i < arr.length; i++){
    console.log(arr[i]);
}

这种直接使用for循环的方法是最普遍的遍历数组和对象的方法;

2、使用for ……in 遍历数组

//2、for……in 循环
let arr = ['d', 'a', 'w', 'n'];
for (let key in arr) {
    console.log(arr[key]);
}

3、for……of 遍历数组

//3、for……of 遍历数组
let arr = ['d', 'a', 'w', 'n'];
for (let value of arr) {
    console.log(value);
}

es6新出的方法,for…of ,值得注意的是,for…of 和 for…in不一样,for…in是直接获取数组的索引,而for…of是直接获取的数组的值

ES6里引入了一种遍历器(Iterator)机制,为不同的数据结构提供统一的访问机制。只要部署了Iterator的数据结构都可以使用 for ··· of ··· 完成遍历操作

它既比传统的for循环简洁,同时弥补了forEach和for-in循环的短板。

循环遍历键值对的value,与for in遍历key相反

4、forEach 遍历数组

//4、forEach遍历数组
let arr = ['d', 'a', 'w', 'n'];
arr.forEach(function (k){
    console.log(k);
})

forEach这种方法也有一个小缺陷:你不能使用break语句中断循环,也不能使用return语句返回到外层函数。

5、map遍历数组

// 5、map 遍历数组
let arr = ['d', 'a', 'w', 'n'];
let res = arr.map(function (item) {
    return item;
})
console.log(res);

总结:

for…in 遍历(当前对象及其原型上的)每一个key,而 for…of遍历(当前对象上的)每一个value;
for in 以任意顺序遍历对象的可枚举属性,(最好不要用来遍历数组) 因此当迭代那些访问次序重要的 arrays 时用整数索引去进行 for 循环 (或者使用 Array.prototype.forEach() 或 for…of 循环) 。
(ES6)for…of 允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。不能遍历普通对象
forEach 遍历数组,而且在遍历的过程中不能被终止,必须每一个值遍历一遍后才能停下来

二、JS 遍历对象

1、for……in 循环遍历对象

//1、for……in遍历对象
var obj = {
    name: 'dawn',
    age: 21,
    address:'深圳'
}
for (let key in obj) {
    console.log(key+':'+obj[key]);
}

2、Object.keys 遍历对象

//2、for……of遍历对象
var obj = {
    name: 'dawn',
    age: 21,
    address: '深圳'
}
for (let key of Object.keys(obj)) {
    console.log(key+':'+obj[key]);
}

【注意】:for…of不能单独来遍历对象,要结合Object.keys一起使用才行

Object.keys()方法会返回一个由一个指定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用for...in循环遍历该对象时返回的顺序一致。

3、Object.getOwnPropertyNames(obj) 遍历对象

//3、Object.getOwnPropertyNames(obj) 遍历对象
var obj = {
    name: 'dawn',
    age: 21,
    address: '深圳'
}
Object.getOwnPropertyNames(obj).forEach(function (key){
    console.log(key+':'+obj[key]);
})

返回一个数组,包含对象自身的所有属性(包含不可枚举属性) 遍历可以获取key和value 

面试官:如何解决跨域问题

一、什么是跨域?跨域是如何产生的?

  • 同源策略:

浏览器内置的规则!是浏览器提供的一种安全机制,限制来自不同源的数据。如果当前页面的URL地址和Ajax的请求地址不同源,浏览器不允许得到服务器的数据;

  • 同源:、

协议 http||https、域名和 port端口 都相同则同源,只要有一项不同就是不同源  不同源就跨域

 二、如何解决跨域?

1.JSONP

注意:

JSONP和json没有任何关系
JSONP需要前后台配合

 1.实现原理:

**走获取文件资源script这个方式**

JSONP实现跨域请求的原理简单的说,就是动态创建 //数组的拷贝/合并 let aa = [1,2,3,4,5] let bb = [...aa] bb[1] = 999 console.log(bb) let aa = [1,2,3,4,5] let bb = [9,9,9] console.log([...aa,...bb]) 注意: const arr1 = ['a', 'b',[1,2]]; //这里数组中的数组是浅拷贝,即拷贝的地址值 const arr2 = ['c']; const arr3 = [...arr1,...arr2] arr1[2][0] = 9999 // 修改arr1里面数组成员值 console.log(arr3) // 影响到arr3,['a','b',[9999,2],'c'] //扩展运算符可以与解构赋值结合起来,用于生成数组 //注:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错 const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // [] const [first, ...rest] = ["foo"]; first // "foo" rest // [] //可以将字符串转化为真正的数组 console.log([..."hello"]) //定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组 let aa = new Map([ ["name","貂蝉"], ["age",12], ["like","跳舞"] ]) console.log([...aa.keys()]) //如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错 const obj = {a: 1, b: 2}; let arr = [...obj]; // TypeError: Cannot spread non-iterable object //补充:原生具备 Iterator 接口的数据结构如下。 - Array - Map - Set - String - TypedArray - 函数的 arguments 对象 - NodeList 对象 ### 2.构造函数新增的方法 1.Array.from() 将两类对象转为真正的数组:类似数组的对象和可遍历`(iterable)`的对象(包括 `ES6` 新增的数据结构 `Set` 和 `Map`)(对象也可以) let aaa = new Map([ ['name','貂蝉'], ["age",12], ["like","跳舞"] ]) console.log(Array.from(aaa)) 2.Array.of() //用于将一组值,转换为数组。 //注: //没有参数的时候,返回一个空数组 //当参数只有一个的时候,实际上是指定数组的长度 //参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组

面试官:谈谈你对ES6的理解

  • 新增模板字符串(为JavaScript提供了简单的字符串插值功能)
  • 箭头函数
  • for-of(用来遍历数据—例如数组中的值。)
  • arguments对象可被不定参数和默认参数完美代替。
  • ES6将promise对象纳入规范,提供了原生的Promise对象。
  • 增加了letconst命令,用来声明变量。
  • 增加了块级作用域。
  • let命令实际上就增加了块级作用域。
  • 还有就是引入module模块的概念

面试官:模块化

使用一个技术肯定是有原因的,那么使用模块化可以给我们带来以下好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题

(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊

CommonJS 

exports 和 module.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exports 和 module.exports享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  1. CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  2. CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  4. ES Module 会编译成 require/exports来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

扩展:vue3跳转路由实现动画效果

在router-view套上标签



      

 .v-enter, v-leave-to中的css一般相同,一个是进入时过渡(动画)的初始样式,一个是离开过渡(动画)结束时的样式。

.v-enter-active ,v-leave-active 中的css一般相同,一般都是用于定义过渡(动画)的过程时间,延迟和曲线函数。当然离开的过渡(动画)的过程时间,延迟和曲线函数和进入的可以是不同的。

面试官:Vue2和vue3的区别?

  1. 在vue3中新增了composition-api,入口就是setup函数,在组件创建之前,props被解析之后执行

  2. 在vue3中不在支持过滤器filters

  3. 在vue3中移除了$on$off$once$destroy方法

  4. 自定以指令的命名必须v自定以指令名

  5. 自定义指令的钩子函数新增了createdbeforeUpdatebeforeUnmount,移除了update; bind---->beforeMount inseted--->mounted componentUpdated--->updated unbind---->unmounted

  6. 全局属性的挂载和使用

    • vue2可以直接在vue的原型上挂载

    • vue3是app.config.globalProperites上挂载自定以属性

    • 通过引入getCurrentInstance函数中proxy来获取

  7. vue-router路由的使用

    • 编程式导航的路由跳转:引入了useRouter函数,通过函数的返回值调用push

    • 获取路由参数的时候引入了useRoute函数,通过useRoute函数获取

  8. vuex的使用

    • 创建store对象时,vue2是new Vue.Store(),vue3createStore()

    • 在组件中使用vuex中的state中的数据时,vue3中引入useStore函数

面试官:Symbol是什么,有什么作用?

SymbolES6引入的第七种原始数据类型(说法不准确,应该是第七种数据类型,Object不是原始数据类型之一,已更正),所有Symbol()生成的值都是独一无二的,可以从根本上解决对象属性太多导致属性名冲突覆盖的问

// symbol第一种使用方法
        const level=Symbol('level')
        const student={
            name:'小明',
            age:2,
            [level]:'优秀'
        }
        for (let i in student){
            console.log(i); // name,age
        }

        // symbol第二种使用方法
        const students={
            name:'小黑',
            age:3,
            [Symbol('level')]:'游戏',
            [Symbol('level')]:'有钱',
        }
        // 如何获取symbol的值
        let symList=Object.getOwnPropertySymbols(students)
        console.log(symList,'ss');//[Symbol(level), Symbol(level)] 
        
        for (let i of symList){
            console.log(students[i]);//有钱,游戏
        }
        let list=[1,2,3,4,5,6]
        console.log(students[Symbol.iterator]); //undefined
        console.log(list[Symbol.iterator]);//ƒ values() { [native code] }
        // 如果对象有Symbol.iterator这个属性
        //这个对象就可以使用for...of遍历

题。对象中Symbol()属性不能被for...in遍历,但是也不是私有属性

面试官:Iterator迭代器

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

 Iterator语法:

const obj = {
    [Symbol.iterator]:function(){}
}

[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。

  • 迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据
  • 每一次的 next 都会返回一个对象,该对象有两个属性
    • value 代表想要获取的数据
    • done 布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束

Iterator 的作用有三个:

  • 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  • 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

对象没有布局Iterator接口,无法使用for of 遍历。下面使得对象具备Iterator接口

  • 一个数据结构只要有Symbol.iterator属性,就可以认为是“可遍历的”
  • 原型部署了Iterator接口的数据结构有三种,具体包含四种,分别是数组,类似数组的对象,Set和Map结构

为什么对象(Object)没有部署Iterator接口呢?

  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
  • 对对象部署Iterator接口并不是很必要,因为Map弥补了它的缺陷,又正好有Iteraotr接口

面试题:Generator

Generator 是 ES6中新增的语法,和 Promise 一样,都可以用来异步编程。Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。

  • function* 用来声明一个函数是生成器函数,它比普通的函数声明多了一个*,*的位置比较随意可以挨着 function 关键字,也可以挨着函数名
  • yield 产出的意思,这个关键字只能出现在生成器函数体内,但是生成器中也可以没有yield 关键字,函数遇到 yield 的时候会暂停,并把 yield 后面的表达式结果抛出去
  • next作用是将代码的控制权交还给生成器函数
function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

上面这个示例就是一个Generator函数,我们来分析其执行过程:

  • 首先 Generator 函数调用时它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42

yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置

web前端面试题(必背面试题)_第35张图片

总结一下Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行

遍历器对象生成函数,最大的特点是可以交出函数的执行权

  • function 关键字与函数名之间有一个星号;
  • 函数体内部使用 yield表达式,定义不同的内部状态;
  • next指针移向下一个状态

这里你可以说说 Generator的异步编程,以及它的语法糖 async 和 awiat,传统的异步编程。ES6 之前,异步编程大致如下

  • 回调函数
  • 事件监听
  • 发布/订阅

传统异步编程方案之一:协程,多个线程互相协作,完成异步任务。

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

从以上代码可以发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以发现通过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次需要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

面试官:H5新增了哪些特性

1.语义化标签
HTML5新增的语义化标签主要有:

面试官: 介绍一下grid网格布局

Grid 布局即网格布局,是一个二维的布局方式,由纵横相交的两组网格线形成的框架性布局结构,能够同时处理行与列,如一些常见的 CSS 布局,如居中,两列布局,三列布局等等是很容易实现的。
擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。
总体兼容性还不错。
同样,Grid 布局属性可以分为两大类:
容器属性,
项目属性


关于容器属性有如下:

1.display:文章开头讲到,在元素上设置display:grid 或 display:inline-grid 来创建一个网格容器
display:grid 则该容器是一个块级元素
display: inline-grid 则容器元素为行内元素
2.grid-template-columns 属性,grid-template-rows 属性
grid-template-columns 属性设置列宽,grid-template-rows 属性设置行高
3.grid-row-gap 属性, grid-column-gap 属性, grid-gap 属性
grid-row-gap 属性、grid-column-gap 属性分别设置行间距和列间距。grid-gap 属性是两者的简写形式
grid-row-gap: 10px 表示行间距是 10px
grid-column-gap: 20px 表示列间距是 20px
grid-gap: 10px 20px 等同上述两个属性
4.grid-template-areas 属性
用于定义区域,一个区域由一个或者多个单元格组成
grid-template-areas: 'a a a'
                     'b b b'
                     'c c c';
5.grid-auto-flow 属性
划分网格以后,容器的子元素会按照顺序,自动放置在每一个网格。
6.justify-items 属性, align-items 属性, place-items 属性
justify-items 属性设置单元格内容的水平位置(左中右),align-items 属性设置单元格的垂直位置(上中下)
.container {
  justify-items: start | end | center | stretch;
  align-items: start | end | center | stretch;
}
7.justify-content 属性, align-content 属性, place-content 属性
justify-content属性是整个内容区域在容器里面的水平位置(左中右),align-content属性是整个内容区域的垂直位置(上中下)
.container {
  justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
  align-content: start | end | center | stretch | space-around | space-between | space-evenly;  
}
8.grid-auto-columns 属性和 grid-auto-rows 属性
有时候,一些项目的指定位置,在现有网格的外部,就会产生显示网格和隐式网格
比如网格只有3列,但是某一个项目指定在第5行。这时,浏览器会自动生成多余的网格,以便放置项目。超出的部分就是隐式网格
而grid-auto-rows与grid-auto-columns就是专门用于指定隐式网格的宽高


关于项目属性,有如下:

1.grid-column-start 属性、grid-column-end 属性、grid-row-start 属性以及grid-row-end 属性
grid-column-start 属性:左边框所在的垂直网格线
grid-column-end 属性:右边框所在的垂直网格线
grid-row-start 属性:上边框所在的水平网格线
grid-row-end 属性:下边框所在的水平网格线
例子:



   
1

   
2

   
3


2.grid-area 属性
grid-area 属性指定项目放在哪一个区域,与上述讲到的grid-template-areas搭配使用。
3.justify-self 属性、align-self 属性以及 place-self 属性
justify-self属性设置单元格内容的水平位置(左中右),跟justify-items属性的用法完全一致,但只作用于单个项目。
align-self属性设置单元格内容的垂直位置(上中下),跟align-items属性的用法完全一致,也是只作用于单个项目
.item {
  justify-self: start | end | center | stretch;
  align-self: start | end | center | stretch;
}这两个属性都可以取下面四个值。
start:对齐单元格的起始边缘。
end:对齐单元格的结束边缘。
center:单元格内部居中。
stretch:拉伸,占满单元格的整个宽度(默认值)

面试官: 说说React Jsx转换成真实DOM过程?

一、是什么

react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些「变化」更新到屏幕上

在前面文章了解中,JSX通过babel最终转化成React.createElement这种形式,例如:

 会被bebel转化成如下:

React.createElement(
  "div",
  null,
  React.createElement("img", {
    src: "avatar.png",
    className: "profile"
  }),
  React.createElement(Hello, null)
);

在转化过程中,babel在编译时会判断 JSX 中组件的首字母:

当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串

当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象

最终都会通过RenderDOM.render(...)方法进行挂载,如下:

ReactDOM.render(,  document.getElementById("root"));

1.使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。

2.createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象 3.ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

 面试官:css中,有哪些方式可以隐藏页面元素?区别?

通过css实现隐藏元素方法有如下:

  • display:none

特点:元素不可见,不占据空间,无法响应点击事件

  • visibility:hidden

特点:元素不可见,占据页面空间,无法响应点击事件

  • opacity:0

特点:改变元素透明度,元素不可见,占据页面空间,可以响应点击事件

  • 设置height、width模型属性为0

特点:元素不可见,不占据页面空间,无法响应点击事件

  • position:absolute(将元素移出可视区域)

特点:元素不可见,不影响页面布局

  • clip-path:通过裁剪的形式

    .hide {
      clip-path: polygon(0px 0px,0px 0px,0px 0px,0px 0px);
    }

特点:元素不可见,占据页面空间,无法响应点击事件

区别:

关于display: nonevisibility: hiddenopacity: 0的区别,如下表所示:

display: none visibility: hidden opacity: 0
页面中 不存在 存在 存在
重排 不会 不会
重绘 不一定
自身绑定事件 不触发 不触发 可触发
transition 不支持 支持 支持
子元素可复原 不能 不能
被遮挡的元素可触发事件 不能

 面试官:说说你对http的了解:

超文本传输协议(Hypertext Transfer Protocol,HTTP),是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议

它可以拆成三个部分:

1.超文本:HTTP 传输的内容是「超⽂本」。它就是超越了普通⽂本的⽂本,它是⽂字、图⽚、视频等的混合体,最关键有超链接,能从⼀个超⽂本跳转到另外⼀个超⽂本。(如HTML) 2.传输:HTTP 协议是⼀个双向协议。 我们在上⽹冲浪时,浏览器是请求⽅ A ,百度⽹站就是应答⽅ B。双⽅约定⽤ HTTP 协议来通信,于是浏览器把请求数据发送给⽹站,⽹站再把⼀些数据返回给浏览器,最后由浏览器渲染在屏幕,就可以看到图⽚、视频了。 3.协议:HTTP 是⼀个⽤在计算机世界⾥的协议。它使⽤计算机能够理解的语⾔确⽴了⼀种计算机之间交流通信的规范(两个以上的参与者),以及相关的各种控制和错误处理⽅式(⾏为约定和规范)。

优点:

HTTP 最凸出的优点是「简单、灵活和易于扩展、应⽤⼴泛和跨平台」。

缺点:

⽆状态 明⽂传输  不安全 HTTP ⽐较严重的缺点就是不安全

面试官:Vue 组件 data 为什么必须是函数

每个组件都是 Vue 的实例。 组件共享 data 属性,当 data 的值是同一个引用类型的值时,改变其中一个会影响其他

面试官:web常见的攻击方式有哪些?如何防御?

1.XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中 XSS涉及到三方,即攻击者、客户端与Web应用,根据攻击的来源,XSS攻击可以分成: 存储型 反射型 DOM 型

XSS的预防: (1)输入过滤,避免 XSS 的方法之一主要是将用户输入的内容进行过滤。对所有用户提交内容进行可靠的输入验证,包括对 URL、查询关键字、POST数据等,仅接受指定长度范围内、采用适当格式、采用所预期的字符的内容提交,对其他的一律过滤。(客户端和服务器都要) (2)输出转义 例如: 往 HTML 标签之间插入不可信数据的时候,首先要做的就是对不可信数据进行 HTML Entity 编码 HTML 字符实体 (3)使用 HttpOnly Cookie 将重要的cookie标记为httponly,这样的话当浏览器向Web服务器发起请求的时就会带上cookie字段,但是在js脚本中却不能访问这个cookie,这样就避免了XSS攻击利用JavaScript的document.cookie获取cookie。

2.CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求 利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目

CSRF的预防: CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性 防止csrf常用方案如下: 阻止不明外域的访问 同源检测 Samesite Cookie 提交时要求附加本域才能获取的信息 CSRF Token 双重Cookie验证  

3.SQL注入 Sql 注入攻击,是通过将恶意的 Sql查询或添加语句插入到应用的输入参数中,再在后台 Sql服务器上解析执行进行的攻击

预防方式如下: 严格检查输入变量的类型和格式 过滤和转义特殊字符 对访问数据库的Web应用程序采用Web应用防火墙  

面试官:为什么 0.1 + 0.2 != 0.3

原因,因为 JS 采用 IEEE 754双精度版本(64位),并且只要采用 IEEE 754的语言都有该问题

我们都知道计算机是通过二进制来存储东西的,那么 0.1 在二进制中会表示为 

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)

我们可以发现,0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS采用的浮点数标准却会裁剪掉我们的数字。

IEEE 754 双精度版本(64位)将 64 位分为了三段

  • 第一位用来表示符号
  • 接下去的 11 位用来表示指数
  • 其他的位数用来表示有效位,也就是用二进制表示 0.1 中的 10011(0011)

那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002

0.100000000000000002 === 0.1 // true

那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002

0.200000000000000002 === 0.2 // true

所以这两者相加不等于 0.3 而是 0.300000000000000004

0.1 + 0.2 === 0.30000000000000004 // true

那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为什么 console.log(0.1) 却是正确的呢?

因为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值,你也可以通过以下代码来验证

console.log(0.100000000000000002) // 0.1

 解决方法

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

面试官: 注册事件

通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false ,useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性

  • capture:布尔值,和 useCapture 作用一样
  • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive:布尔值,表示永远不会调用 preventDefault

一般来说,如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

面试官:什么是 MVVM?比之 MVC 有什么区别?

首先先来说下 View 和 Model

  • View 很简单,就是用户看到的视图
  • Model 同样很简单,一般就是本地数据和数据库中的数据

基本上,我们写的产品就是通过接口从数据库中读取数据,然后将数据经过处理展现到用户看到的视图上。当然我们还可以从视图上读取用户的输入,然后又将用户的输入通过接口写入到数据库中。但是,如何将数据展示到视图上,然后又如何将用户的输入写入到数据中,不同的人就产生了不同的看法,从此出现了很多种架构设计。

传统的 MVC 架构通常是使用控制器更新模型,视图从模型中获取数据去渲染。当用户有输入时,会通过控制器去更新模型,并且通知视图进行更新

web前端面试题(必背面试题)_第36张图片

  • 但是 MVC 有一个巨大的缺陷就是控制器承担的责任太大了,随着项目愈加复杂,控制器中的代码会越来越臃肿,导致出现不利于维护的情况。
  • 在 MVVM 架构中,引入了 ViewModel 的概念。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel

web前端面试题(必背面试题)_第37张图片

  • 以 Vue 框架来举例,ViewModel 就是组件的实例。View 就是模板,Model 的话在引入 Vuex 的情况下是完全可以和组件分离的。
  • 除了以上三个部分,其实在 MVVM 中还引入了一个隐式的 Binder 层,实现了 View 和 ViewModel 的绑定

 web前端面试题(必背面试题)_第38张图片

  • 同样以 Vue 框架来举例,这个隐式的 Binder 层就是 Vue 通过解析模板中的插值和指令从而实现 View 与 ViewModel 的绑定。
  • 对于 MVVM来说,其实最重要的并不是通过双向绑定或者其他的方式将 View 与 ViewModel 绑定起来,而是通过 ViewModel 将视图中的状态和用户的行为分离出一个抽象,这才是 MVVM 的精髓

面试官:mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的

Vue.mixin({
    beforeCreate() {
        // ...逻辑
        // 这种方式会影响到每个组件的 beforeCreate 钩子函数
    }
})
  • 虽然文档不建议我们在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。
  • mixins 应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。
  • 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并,具体可以阅读 文档

面试官: computed 和 watch 区别

  • computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
  • watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
  • 所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch
  • 另外 computer 和 watch 还都支持对象的写法,这种方式知道的人并不多。
vm.$watch('obj', {
    // 深度遍历
    deep: true,
    // 立即触发
    immediate: true,
    // 执行的函数
    handler: function(val, oldVal) {}
})
var vm = new Vue({
  data: { a: 1 },
  computed: {
    aPlus: {
      // this.aPlus 时触发
      get: function () {
        return this.a + 1
      },
      // this.aPlus = 1 时触发
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})

 面试官:Ajax

它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。

web前端面试题(必背面试题)_第39张图片

//1:创建Ajax对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
     if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
          console.log(xhr.responsetXML)
}

实现过程

实现 `Ajax`异步交互需要服务器逻辑进行配合,需要完成以下步骤:

- 创建 `Ajax`的核心对象 `XMLHttpRequest`对象
- 通过 `XMLHttpRequest` 对象的 `open()` 方法与服务端建立连接
- 构建请求所需的数据内容,并通过`XMLHttpRequest` 对象的 `send()` 方法发送给服务器端
- 通过 `XMLHttpRequest` 对象提供的 `onreadystatechange` 事件监听服务器端你的通信状态
- 接受并处理服务端向客户端响应的数据结果
- 将处理结果更新到 `HTML`页面中

promise 封装实现:

// promise 封装实现:

function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();

    // 新建一个 http 请求
    xhr.open("GET", url, true);

    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;

      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };

    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };

    // 设置响应的数据类型
    xhr.responseType = "json";

    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");

    // 发送 http 请求
    xhr.send(null);
  });

  return promise;
}

 面试官:深入数组

一、梳理数组 API

Array() 和 Array.of() 的区别

Array()

在JavaScript中,数组构造函数是new Array() 或 Array()

let arr = Array(1, 2, 3, 4)
console.log(arr) // 1, 2, 3, 4
console.log(arr.length) // 4

咋一看,好像什么毛病,我们改一下代码,将构造函数的参数设为只有一个数字时:

let arr = Array(4)
console.log(arr) // [empty × 4]
console.log(arr.length) // 4

这时就会发现,我们或许只是想构造一个只有一个数字4的数组,但却构造了一个数组长度为4的数组,怎么解决这个问题呢?
Array.of()来帮你解决这个问题吧!Array.of()和new Array()功能相同,但当只放入一个数字时,Array.of()会构造一个包含该数字的数组

Array.of()

Array.of ()方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

let arr = Array.of(1, 2, 3, 4)
console.log(arr) // [1, 2, 3, 4]
console.log(arr.length) // 4

这个方法的主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异

let arr = Array.of(4)
console.log(arr) // [4]
console.log(arr.length) // 1

Array.from ()方法详解

1、将类数组对象转换为真正数组:

let arrayLike = {
	    0: 'tom', 
	    1: '65',
	    2: '男',
	    3: ['jane','john','Mary'],
	    'length': 4
	}
	let arr = Array.from(arrayLike)
	console.log(arr) // ['tom','65','男',['jane','john','Mary']]

如果把length去掉 他就成了一个长度为0的数组

就是具有length属性,但是对象的属性名不再是数字类型的,而是其他字符串型的 会出现以下情况

let arrayLike = {
    'name': 'tom', 
    'age': '65',
    'sex': '男',
    'friends': ['jane','john','Mary'],
    length: 4
}
let arr = Array.from(arrayLike)
console.log(arr)  // [ undefined, undefined, undefined, undefined ]

会发现结果是长度为4,元素均为undefined的数组

要将一个类数组对象转换为一个真正的数组,必须具备以下条件:

 1、该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。

 2、该类数组对象的属性名必须为数值型或字符串型的数字

 ps: 该类数组对象的属性名可以加引号,也可以不加引号

2、将Set结构的数据转换为真正的数组:


let arr = [12,45,97,9797,564,134,45642]
let set = new Set(arr)
console.log(Array.from(set))  // [ 12, 45, 97, 9797, 564, 134, 45642 ]

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如下:

let arr = [12,45,97,9797,564,134,45642]
let set = new Set(arr)
console.log(Array.from(set, item => item + 1)) // [ 13, 46, 98, 9798, 565, 135, 45643 ]

3、将字符串转换为数组:

let  str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]

4、Array.from参数是一个真正的数组:

console.log(Array.from([12,45,47,56,213,4654,154]))

 改变自身的方法

基于 ES6,会改变自身值的方法一共有 9 个,分别为 pop、push、reverse、shift、sort、splice、unshift,以及两个 ES6 新增的方法 copyWithin 和 fill

// pop方法
var array = ["cat", "dog", "cow", "chicken", "mouse"];
var item = array.pop();
console.log(array); // ["cat", "dog", "cow", "chicken"]
console.log(item); // mouse
// push方法
var array = ["football", "basketball",  "badminton"];
var i = array.push("golfball");
console.log(array); 
// ["football", "basketball", "badminton", "golfball"]
console.log(i); // 4
// reverse方法
var array = [1,2,3,4,5];
var array2 = array.reverse();
console.log(array); // [5,4,3,2,1]
console.log(array2===array); // true
// shift方法
var array = [1,2,3,4,5];
var item = array.shift();
console.log(array); // [2,3,4,5]
console.log(item); // 1
// unshift方法
var array = ["red", "green", "blue"];
var length = array.unshift("yellow");
console.log(array); // ["yellow", "red", "green", "blue"]
console.log(length); // 4
// sort方法
var array = ["apple","Boy","Cat","dog"];
var array2 = array.sort();
console.log(array); // ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // true
// splice方法
var array = ["apple","boy"];
var splices = array.splice(1,1);
console.log(array); // ["apple"]
console.log(splices); // ["boy"]
// copyWithin方法
var array = [1,2,3,4,5]; 
var array2 = array.copyWithin(0,3);
console.log(array===array2,array2);  // true [4, 5, 3, 4, 5]
// fill方法
var array = [1,2,3,4,5];
var array2 = array.fill(10,0,3);
console.log(array===array2,array2); 
// true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为10

不改变自身的方法

基于 ES7,不会改变自身的方法也有 9 个,分别为 concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、未形成标准的 toSource,以及 ES7 新增的方法 includes

// concat方法
var array = [1, 2, 3];
var array2 = array.concat(4,[5,6],[7,8,9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array); // [1, 2, 3], 可见原数组并未被修改
// join方法
var array = ['We', 'are', 'Chinese'];
console.log(array.join()); // "We,are,Chinese"
console.log(array.join('+')); // "We+are+Chinese"
// slice方法
var array = ["one", "two", "three","four", "five"];
console.log(array.slice()); // ["one", "two", "three","four", "five"]
console.log(array.slice(2,3)); // ["three"]
// toString方法
var array = ['Jan', 'Feb', 'Mar', 'Apr'];
var str = array.toString();
console.log(str); // Jan,Feb,Mar,Apr
// tolocalString方法
var array= [{name:'zz'}, 123, "abc", new Date()];
var str = array.toLocaleString();
console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23
// indexOf方法
var array = ['abc', 'def', 'ghi','123'];
console.log(array.indexOf('def')); // 1
// includes方法
var array = [-0, 1, 2];
console.log(array.includes(+0)); // true
console.log(array.includes(1)); // true
var array = [NaN];
console.log(array.includes(NaN)); // true

6. 数组遍历的方法

基于 ES6,不会改变自身的遍历方法一共有 12 个,分别为 forEach、every、some、filter、map、reduce、reduceRight,以及 ES6 新增的方法 entries、find、findIndex、keys、values

// forEach方法
var array = [1, 3, 5];
var obj = {name:'cc'};
var sReturn = array.forEach(function(value, index, array){
  array[index] = value;
  console.log(this.name); // cc被打印了三次, this指向obj
},obj);
console.log(array); // [1, 3, 5]
console.log(sReturn); // undefined, 可见返回值为undefined
// every方法
var o = {0:10, 1:8, 2:25, length:3};
var bool = Array.prototype.every.call(o,function(value, index, obj){
  return value >= 8;
},o);
console.log(bool); // true
// some方法
var array = [18, 9, 10, 35, 80];
var isExist = array.some(function(value, index, array){
  return value > 20;
});
console.log(isExist); // true 
// map 方法
var array = [18, 9, 10, 35, 80];
array.map(item => item + 1);
console.log(array);  // [19, 10, 11, 36, 81]
// filter 方法
var array = [18, 9, 10, 35, 80];
var array2 = array.filter(function(value, index, array){
  return value > 20;
});
console.log(array2); // [35, 80]
// reduce方法
var array = [1, 2, 3, 4];
var s = array.reduce(function(previousValue, value, index, array){
  return previousValue * value;
},1);
console.log(s); // 24
// ES6写法更加简洁
array.reduce((p, v) => p * v); // 24
// reduceRight方法 (和reduce的区别就是从后往前累计)
var array = [1, 2, 3, 4];
array.reduceRight((p, v) => p * v); // 24
// entries方法
var array = ["a", "b", "c"];
var iterator = array.entries();
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]
console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined
// find & findIndex方法
var array = [1, 3, 5, 7, 8, 9, 10];
function f(value, index, array){
  return value%2==0;     // 返回偶数
}
function f2(value, index, array){
  return value > 20;     // 返回大于20的数
}
console.log(array.find(f)); // 8
console.log(array.find(f2)); // undefined
console.log(array.findIndex(f)); // 4
console.log(array.findIndex(f2)); // -1
// keys方法
[...Array(10).keys()];     // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// values方法
var array = ["abc", "xyz"];
var iterator = array.values();
console.log(iterator.next().value);//abc
console.log(iterator.next().value);//xyz

总结

web前端面试题(必背面试题)_第40张图片

数组和字符串方法 

面试官:在生命周期中的哪一步你应该发起 AJAX 请求

我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下

  • React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。
  • 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

面试官:如何告诉 React 它应该编译生产环境版

通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。编译版本中 React会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小,React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息

面试官:概述下 React 中的事件处理逻辑

为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

面试官:createElement 与 cloneElement 的区别是什么

传入的第一个参数不同

React.createElement():JSX 语法就是用 React.createElement()来构建 React 元素的。

它接受三个参数,第一个参数可以是一个标签名。如 div、span,或者 React 组件。第二个参数为传入的属性。第三个以及之后的参数,皆作为组件的子组件。

React.createElement(type, [props], [...children]);

React.cloneElement()与 React.createElement()相似,不同的是它传入的第一个参数是一个 React 元素,而不是标签名或组件。新添加的属性会并入原有的属性,传入到返回的新元素中,而旧的子元素将被替换。将保留原始元素的键和引用。

React.cloneElement(element, [props], [...children]);

面试官: redux中间件

中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer。这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能

  • redux-logger:提供日志输出
  • redux-thunk:处理异步操作
  • redux-promise:处理异步操作,actionCreator的返回值是promise

面试官:redux有什么缺点

1. Redux 使状态可预测
在 Redux 中,状态始终是可预测的。如果将相同的状态和动作传递给减速器,则总是会产生相同的结果,因为减速器是纯函数。状态也是不可变的,永远不会改变。这使得执行诸如无限撤消和重做之类的艰巨任务成为可能。还可以在之前的状态之间来回移动并实时查看结果。

2. Redux 是可维护的
Redux 对代码的组织方式很严格,这使得具有 Redux 知识的人更容易理解任何 Redux 应用程序的结构。这通常使维护更容易。这也有助于用户将业务逻辑与组件树分离。对于大型应用程序,保持应用程序更具可预测性和可维护性至关重要。

3. Redux 调试简单
Redux 使调试应用程序变得容易。通过记录操作和状态,很容易理解编码错误、网络错误和生产过程中可能出现的其他形式的错误。

除了日志记录,它还有很棒的 DevTools,允许时间旅行操作、页面刷新时的持久操作等。对于中型和大型应用程序,调试比实际开发功能花费更多的时间。Redux DevTools 使您可以轻松利用 Redux 提供的所有功能。

4.性能优势
我们可能会假设保持应用程序的状态全局会导致一些性能下降。在很大程度上,情况并非如此,因为 React Redux 在内部实现了许多性能优化,因此我们自己的连接组件仅在实际需要时才重新渲染。

5.易于测试
由于函数用于更改纯函数的状态,因此测试 Redux 应用程序很容易。

6.状态持久化
我们可以将应用程序的一些状态持久化到本地存储并在刷新后恢复它。这真的很漂亮。

7.服务端渲染
Redux 也可用于服务器端渲染。有了它,我们可以通过将应用程序的状态连同它对服务器请求的响应发送到服务器来处理应用程序的初始呈现。然后所需的组件以 HTML 格式呈现并发送到客户端。

何时不选择 Redux
主要由简单的用户界面更改组成的应用程序通常不需要像 Redux 这样的复杂模式。有时,不同组件之间的老式状态共享也有效,并提高了代码的可维护性。

此外,如果用户的数据来自每个视图的单个数据源,则他们可以避免使用 Redux。换句话说,如果我们不需要多源数据,就没有必要引入Redux。每个视图访问单个数据源时,我们不会遇到数据不一致的问题。

因此,在介绍其复杂性之前,请务必检查我们是否需要 Redux。尽管这是一种促进纯函数的相当有效的模式,但对于仅涉及几个 UI 更改的简单应用程序来说,这可能是一种开销。最重要的是,我们不应该忘记 Redux 是一个内存状态存储。换句话说,如果我们的应用程序崩溃,我们将丢失整个应用程序状态。这意味着我们必须使用缓存解决方案来创建应用程序状态的备份,这又会产生额外的开销。

结论
我们已经讨论了 Redux 的主要特性以及为什么 Redux 对我们的应用程序有益。
虽然 Redux 有它的好处,但这并不意味着我们应该将 Redux 添加到我们所有的应用程序中。如果没有 Redux,我们的应用程序可能仍能正常运行。
Redux 的一个主要好处是增加了方向,将“发生的事情”与“事情如何变化”分开。
如果我们确定我们的项目需要状态管理工具,我们应该只实施 Redux。

面试官:react组件的划分业务组件技术组件? 

  • 根据组件的职责通常把组件分为UI组件和容器组件。
  • UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。
  • 两者通过React-Redux 提供connect方法联系起来

面试官:预加载

  • 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载
  • 预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好

 面试官: 如何解决a标点击后hover事件失效的问题?

改变a标签css属性的排列顺序

只需要记住LoVe HAte原则就可以了(爱恨原则):

link→visited→hover→active

比如下面错误的代码顺序:

a:hover{
  color: green;
  text-decoration: none;
}
a:visited{ /* visited在hover后面,这样的话hover事件就失效了 */
  color: red;
  text-decoration: none;
}

正确的做法是将两个事件的位置调整一下。

注意⚠️各个阶段的含义:

  • a:link:未访问时的样式,一般省略成a
  • a:visited:已经访问后的样式
  • a:hover:鼠标移上去时的样式
  • a:active:鼠标按下时的样式

面试官:Vue实例挂载过程发生了什么

web前端面试题(必背面试题)_第41张图片

面试官:Js有哪些数据类型

JS中有两种数据类型
1.简单数据类型(也称基本数据类型):Undefined;Null;Boolean;Number和String。

2.引用数据类型(也称复杂数据类型),其中包括Object;Array;Function等等。

面试官:谈谈变量提升?

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境

  • 接下来让我们看一个老生常谈的例子,var
b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}

变量提升

这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为
undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用

  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'

 复制代码var 会产生很多错误,所以在 ES6中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

面试官:实现一个trim方法

trim()是一个很适用的方法,作用是去除字符串两边的空白,但是js本身并未提供这个方法,下面介绍js使用trim()的方法

1.通过原型创建字符串的trim()

/去除字符串两边的空白

String.prototype.trim=function(){
  return this.replace(/(^\s*)|(\s*$)/g, "");
}

//只去除字符串左边空白
 String.prototype.ltrim=function(){
  return this.replace(/(^\s*)/g,"");
}

//只去除字符串右边空白
String.prototype.rtrim=function(){
  return this.replace(/(\s*$)/g,"");

}

 2.通过函数实现

例如:

function trim(str){

return str.replace(/(^\s*)|(\s*$)/g, "");

}

面试官:简述flux思想

1.什么是flux
Flux的提出主要是针对现有前端MVC框架的局限总结出来的一套基于dispatcher的前端应用架构模式。按照MVC的命名习惯,他应该叫ADSV(Action Dispatcher Store View)。
在Flux应用中,数据从action到dispatcher,再到store,最终到view的路线是不可逆的,各个角色之间不会像前段MVC模式那样存在交错的连线

 1.用户访问view
2.view发出用户的Action
3.dispatcher收到Action,要求Store进行响应的更新
4.Store更新后,发出一个"change"事件
5.view收到"change"事件后,更新页面

 Flux将一个应用分成四个部分;
1.view视图层
2.action(动作);视图层发出的消息(比如mouseClick)
3.Dispatcher(派发器):用来接收Actions,执行回调函数
4.Store(数据层):用来存放应用的状态,一旦发生改变,就提醒Views更新页面

Flux的最大特点:就是数据的"单向流动",数据总是"单项流动",任何相邻的部分都不会发生数据的"双向流动"。

web前端面试题(必背面试题)_第42张图片

面试官:什么是BFC

【透过现象看本质】:BFC就是符合一些特征的HTML标签

【BFC 是什么?】

BFC(Bloce Formatting Context)格式化上下文 指一个独立的渲染区域,或者说是一个隔离的独立容器 可以理解为一个独立的封闭空间。无论如何不会影响到他的外面

【形成BFC的条件】

1、浮动元素,float除none以外的值

2、绝对定位元素,position(absolute,fixed)

3、diisplay 为一下其中之一的值 inline-block table-cell table-caption,flex

4、overflow除了visible以外的值(hidden,auto,scroll)

5、body 根元素

【BFC的特性

1、内部的Box会在垂直方向上一个接一个的放置

2、垂直方向上的距离margin决定

3、bfc的区域不会与float的元素区域重叠

4、计算bfc的高度时,浮动元素区域也参与计算

5、bfc就是页面上的一个独立的容器,容器里面的子元素不会影响外面的元素

前端工程化的特点

前端工程化可以分成四个方面来说,分别为模块化、组件化、规范化和自动化。

模块化
模块化是指将一个文件拆分成多个相互依赖的文件,最后进行统一的打包和加载,这样能够很好的保证高效的多人协作。其中包含

JS 模块化:CommonJS、AMD、CMD 以及 ES6 Module。
CSS 模块化:Sass、Less、Stylus、BEM、CSS Modules 等。其中预处理器和 BEM 都会有的一个问题就是样式覆盖。而 CSS Modules 则是通过 JS 来管理依赖,最大化的结合了 JS 模块化和 CSS 生态,比如 Vue 中的 style scoped。
资源模块化:任何资源都能以模块的形式进行加载,目前大部分项目中的文件、CSS、图片等都能直接通过 JS 做统一的依赖关系处理。
组件化
不同于模块化,模块化是对文件、对代码和资源拆分,而组件化则是对 UI 层面的拆分。

通常,我们会需要对页面进行拆分,将其拆分成一个一个的零件,然后分别去实现这一个个零件,最后再进行组装。 在我们的实际业务开发中,对于组件的拆分我们需要做不同程度的考量,其中主要包括细粒度和通用性这两块的考虑。 对于业务组件,你更多需要考量的是针对你负责业务线的一个适用度,即你设计的业务组件是否成为你当前业务的 “通用” 组件。

规范化
正所谓无规矩不成方圆,一些好的规范则能很好的帮助我们对项目进行良好的开发管理。规范化指的是我们在工程开发初期以及开发期间制定的系列规范,其中又包含了

项目目录结构
编码规范:对于编码这块的约束,一般我们都会采用一些强制措施,比如 ESLint、StyleLint 等。
联调规范
文件命名规范
样式管理规范:目前流行的样式管理有 BEM、Sass、Less、Stylus、CSS Modules 等方式。
git flow 工作流:其中包含分支命名规范、代码合并规范等。
定期 code review … 等等
自动化
从最早先的 grunt、gulp 等,再到目前的 webpack、parcel。这些自动化工具在自动化合并、构建、打包都能为我们节省很多工作。而这些只是前端自动化其中的一部分,前端自动化还包含了持续集成、自动化测试等方方面面。

以上就是我所了解的前端工程化,以工程的角度去理解我们的web前端。工程是工程,而不是某项技术。

面试官:CSS清除浮动的方法(多种)

清除浮动的含义是什么?
清除浮动带来的影响
影响:如果子元素浮动了,此时子元素不能撑开父元素
➢ 清除浮动的目的是什么?
需要父元素有高度,从而不影响其他网页元素的布局
注意:父子级标签, 子级浮动, 父级没有高度, 后面的标准流盒子会受影响, 显示到上面的位置

方法:

1、直接设置父元素高度

优点:简单粗暴,方便

缺点:有些布局中不能固定父元素高度。如:新闻列表、京东推荐模块

2、额外标签法

操作: 1. 在父元素内容的最后添加一个块级元素 2. 给添加的块级元素设置 clear:both

缺点:会在页面中添加额外的标签,会让页面的HTML结构变得复杂 

3、单伪元素清除法
操作:用伪元素替代了额外标签
基本写法

.clearfix::after{
    content:'';
    display:block;
    clear:both;
}

补充写法

/* 伪元素添加的标签是行内, 要求块 */
.clearfix::after{
    content:'';
    /* 伪元素添加的标签是行内, 要求块 */
    display:block;
    clear:both;
    /* 补充代码:在网页中看不到伪元素 *//* 为了兼容性 */
    height:0;
    visibility:hidden;
}

优点:项目中使用,直接给标签加类即可清除浮动
4、双伪元素清除法
操作:

/*  .clearfix::before 作用: 解决外边距塌陷问题
    外边距塌陷: 父子标签, 都是块级, 子级加margin会影响父级的位置
*/
/* 清除浮动 */
.clearfix::bofore,
.clearfix::after{
    content:'';
    display:table;
}
/* 真正清除浮动的标签 */
.clearfix::after{
    clear:both;
}

优点:项目中使用,直接给标签加类即可清除浮动

5、给父元素设置overflow : hidden
操作: 直接给父元素设置 overflow : hidden
优点:方便

面试官:keep-alive的属性

include

include:字符串或正则表达,只有匹配的组件会被缓存


……


如:

	

表示home、other组件可以被缓存

exclude

exclude:字符串或正则表达式,任何匹配的组件都不会被缓存


……


如:

	

表示home、other组件不能被缓存

也可以在router里meta设置true或者或false然后在keep-alive去做判来实现是否进行缓存

面试官:webpack中的代码分割

代码分割的方法

官网给出了三种常用的代码分割的方法

  • Entry Points:入口文件设置的时候可以配置
  • CommonsChunkPlugin:上篇文章讲了一下应用,更详细的信息可以查看官网
  • Dynamic Imports:动态导入。通过模块的内联函数调用来分割,这篇文章主要会结合 vue-router 分析一下这种方

Entry Points

这种是最简单也是最直观的代码分割方式,但是会存在一些问题。方法就是在 webpack 配置文件中的 entry 字段添加新的入口:

const path = require('path');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

将生成下面的构建结果:

 看上去是分割出来了一个新的 bundle。但是会有两个问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中
  • 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码

举个例子,index 和 another 这两个入口文件都包含了 lodash 这个模块,那分割出来的两个 bundle 都会包含 lodash 这个模块,冗余了。解决这个问题就需要 CommonsChunkPlugin 插件

CommonsChunkPlugin 

这个插件可以抽取所有入口文件都依赖了的模块,把这些模块抽取成一个新的bundle。具体用法如下:

const path = require('path');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common' // bundle 名称
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

构建结果如下:

 可以看到,原来的 index 和 another 两个bundle的体积大大的减小了。并且多了一个574k的 common bundle。这个文件就是抽离出来的 lodash 模块。这样就可以把业务代码,和第三方模块代码分割开了。

Dynamic Imports

Webpack 的动态分割主要方式是使用符合 ECMAScript 提案的 import() 语法。语法如下

import('path/to/module') -> Promise

传入模块的路径,import() 会返回一个Promise。这个模块就会被当作分割点。意味着这个模块和它的子模块都会被分割成一个单独的 chunk。并且,在 webpack 配置文件的 output 字段,需要添加一个 chunkFileName 属性。它决定非入口 chunk 的名称。

// vue-cli 生成的webpack.prod.conf.js
// 注意 output 的 chunkFilename 属性
// 这种写法分割出来的 bundle 会以 id + hash 的命名方式
// 比如 1.32326e28f3acec4b3a9a.js
output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},

这个动态代码分割功能是我们实现按需加载的前提。在 vue 的项目里,我们最终想要达到这样一个效果:

  1. 把每个路由所包含的组件,都分割成一个单独的 bundle
  2. 当路由被访问的时候才加载该路由对应的 bundle

第一个点通过上面的 import() 就已经可以实现了。要实现第二点,需要用到 vue 里面的异步组件特性。

Vue 允许将组件定义为一个工厂函数,异步地解析组件的定义。只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。工厂函数的写法:
 

Vue.component('async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

最后在 vue-router 的路由配置中,我们只需要这么写:

const router = new VueRouter({
  routes: [
    { path: '/login', component: () => import('@/views/login'), },
    { path: '/home', component: () => import('@/views/home'), }
  ]
})

结合 Vue 的异步组件和 Webpack 的代码分割功能,在 vue-router 中,我们轻松实现了路由组件的按需加载加载。所以,文章开头的问题在这里就可以解答了。以0-7数字开头的 js 文件,就是每个路由对应的组件构建出来的 bundle。只有用户访问对应的路由时,才会加载相应的 bundle,提高页面加载效率。

面试官:Vue数据更新但页面没有更新的多种情况

1、Vue 无法检测实例被创建时不存在于 data 中的 变量

原因:由于 Vue 会在初始化实例时对 data中的数据执行 getter/setter 转化,所以 变量必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

例如:


new Vue({
  data:{},
  template: '
{{message}}
' }) this.message = 'Hello world !' // `message` 不是响应式的页面不会发生变化

解决方法:

new Vue({
  data: {
    message: '',
  },
  template: '
{{ message }}
' }) this.message = 'Hello world!'

2、vue也不能检测到data中对象的动态添加和删除

例如:

 new Vue({
  data:{
    obj: {
      id: 1
    }
  },
  template: '
{{ obj.message }}
' }) this.obj.message = 'hello' // 不是响应式的 delete this.obj.id // 不是响应式的

解决:

// 动态添加 - Vue.set
Vue.set(this.obj, 'id', 002)
 
// 动态添加 - this.$set
this.$set(this.obj, 'id', 002)
 
// 动态添加多个
// 代替 Object.assign(this.obj, { a: 1, b: 2 })
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })
 
// 动态移除 - Vue.delete
Vue.delete(this.obj, 'name')
 
// 动态移除 - this.$delete
this.$delete(this.obj, 'name')

3、数组的时候,不能通过索引直接修改或者赋值,也不能修改数组的长度

例如

new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
this.items[1] = 'x' // 不是响应性的
this.items[3] = 'd' // 不是响应性的
this.items.length = 2 // 不是响应性的

解决:


// Vue.set
Vue.set(this.items, 4, 'd')
 
// this.$set
this.$set(this.items, 4, 'd)
 
// Array.prototype.splice
this.items.splice(indexOfItem, 4, 'd')
//修改长度
this.items.splice(3)

4、异步获取接口数据,DOM数据不发现变化

原因:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

{{message}}
var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false vm.$el.style.color = 'red' // 页面没有变化

解决办法:

var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
//使用 Vue.nextTick(callback) callback 将在 DOM 更新完成后被调用
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
  vm.$el.style.color = 'red' // 文字颜色变成红色
})

5、循环嵌套层级太深,视图不更新

当嵌套太深时,页面也可以不更新,此时可以让页面强制刷新

this.$forceUpdate()   (不建议用)

6、 路由参数变化时,页面不更新(数据不更新)

拓展一个因为路由参数变化,而导致页面不更新的问题,页面不更新本质上就是数据没有更新。

原因:路由视图组件引用了相同组件时,当路由参会变化时,会导致该组件无法更新,也就是我们常说中的页面无法更新的问题。

场景:


  • To Foo
  • To Baz
  • To Bar
const Home = { template: `
{{message}}
`, data() { return { message: this.$route.params.name } } } const router = new VueRouter({ mode:'history', routes: [ {path: '/home', component: Home }, {path: '/home/:name', component: Home } ] }) new Vue({ el: '#app', router })

上段代码中,我们在路由构建选项 routes 中配置了一个动态路由 '/home/:name',它们共用一个路由组件 Home,这代表他们复用 RouterView 。

当进行路由切换时,页面只会渲染第一次路由匹配到的参数,之后再进行路由切换时,message 是没有变化的。

解决办法:

  解决的办法有很多种,这里只列举我常用到几种方法。

通过 watch 监听 $route 的变化。 

const Home = {
      template: `
{{message}}
`, data() { return { message: this.$route.params.name } }, watch: { '$route': function() { this.message = this.$route.params.name } } } new Vue({ el: '#app', router })
  1. 给  绑定 key 属性,这样 Vue 就会认为这是不同的 

弊端:如果从 /home 跳转到 /user 等其他路由下,我们是不用担心组件更新问题的,所以这个时候 key 属性是多余的。

面试官:vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,可以实现数据之间的共享,它由五部分组成:
分别是:
state (用来存放数据)
actions (可以包含异步操作)
mutations (唯一可以修改state数据的场所)
getters (类似于vue组件中的计算属性,对state数据进行计算(会被缓存))
modules (模块化管理store(仓库),每个模块拥有自己的 state、mutation、action、getter)

二. vuex的使用步骤 

代码如下(示例):
在根目录下新建一个store文件夹,里面创建一个index.js文件,
最后在main.js中引入,并挂载到实例上,之后那个组件中需要用到vuex就调用就行

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  	//存放数据  this.$store.state.xxx
	state: {},	
	// 唯一修改state的地方 this.$store.commit('事件名',参数)
	mutations: {},
	// 执行异步操作 		  this.$store.dispatch('事件名')
	actions: {},
	// 模块,每个模块拥有自己的state,mutations,actions,getters
	modules: {},
	// 计算state
	getters:{}   		  this.$store.getters.xxx
})

面试官:Vue Router的路由模式hash和history的实现原理

一、Vue-router 中hash模式和history模式的关系

最直观的区别就是在url中 hash 带了一个很丑的 # 而history是没有#的。

二、hash模式实现原理

早期前端路由的实现就是基于location.hash来实现的,其实实现原理很简单,location.hash的值就是URL中#后面的内容

hash路由模式的实现主要特性:

URL中的hash值只是客户端的一种状态,也就是说当向服务器发送请求时,hash部分不会被发送;
hash值的改变,都会在浏览器的访问历史中增加一个记录,因此在开发时,也可以通过浏览器的回退和前进按钮来控制hash的切换;
也可以通过href属性来改变URL的hash值,或者使用location.hash进行赋值,改变URL的hash值;
可以使用hashchange事件来监听hash值的变化,从而对页面进行跳转。

三、history模式的实现原理

HTML5提供了History API来实现URL的变化,其中最主要的两个API有以下两个
history.pushState()和history.replaceState()。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

window.history.pushState(null,null,path)
window.history.replaceState(null,null,path)

 面试官:Vue的路由钩子函数有哪些?

1、全局的路由钩子函数

       1.1、beforeEach(全局前置钩子),意思是在每次每一个路由改变的时候都要执行               一遍

        它有三个参数:

        to: route:即将要进入的目标 路由对象

        from:route:当前导航正要离开的路由

        next:function:一定要调用该方法来resolve这个钩子。执行效果依赖next方法

        应用场景:

        1、进行一些页面跳转前的处理,例如跳转到的页面需要进行登录才可以访问时,                   就会做登录的跳转

        2、进入页面登录判断、管理员权限判断、浏览器判断

       1.2、afterEach(全局后置钩子)

        beforeEach是在页面加载之前的,而afterEach是在页面加载之后的,所以这些钩子是            不会接受next函数,也不会改变导航本身

2、单个路由内的钩子函数

        2.1、beforeEnter

        可以直接在路由配置上直接定义beforeEnter,这些守卫与全局前置守卫的方法参数是            一样的

3、组件内的路由钩子函数

        beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

        应用场景

        1、清除组件中的定时器

        2、当页面有未关闭的窗口,或未保存的内容时,组织页面跳转

        3、保存相关内容到Vuex和Session中

 面试官:为什么Vue中data一定要是一个函数?

data必须是一个函数, 函数的好处就是每一次组件复用之后都会产生一个全新的data. 组件与组件之间各自维护自己的数据, 互不干扰

面试官:Proxy 相比较于 defineProperty 的优势

Object.defineProperty 是监听对象的字段而非对象本身,因此对于动态插入对象的字段,它无能为了,只能手动为其设置设置监听属性。

同时,Object.defineProperty 无法监听对象中数组的变化

Proxy 叫做代理器,它可以为一个对象设置代理,即监听对象本身,任何访问当前被监听的对象的操作,无论是对象本身亦或是对象的字段,都会被 Proxy 拦截,因此可以使用它来做一些双向绑定的操作。

鉴于兼容性的问题,目前仍然主要是使用 Object.defineProperty 更多,但是随着 Vue/3 的发布,Proxy 应该会逐渐淘汰 Object.defineProperty。

面试官:ES5、ES6和ES2015有什么区别?

ES2015特指在2015年发布的新一代JS语言标准,ES6泛指下一代JS语言标准,包含ES2015、ES2016、ES2017、ES2018等。现阶段在绝大部分场景下,ES2015默认等同ES6。ES5泛指上一代语言标准。ES2015可以理解为ES5和ES6的时间分界线。

面试官:babel是什么,有什么作用?

babel是一个 ES6 转码器,可以将 ES6 代码转为 ES5 代码,以便兼容那些还没支持ES6的平台。

 面试官:let有什么用,有了var为什么还要用let?

在ES6之前,声明变量只能用var,var方式声明变量其实是很不合理的,准确的说,是因为ES5里面没有块级作用域是很不合理的,

没有块级作用域回来带很多难以理解的问题,比如for循环var变量泄露,变量覆盖等问题。let 声明的变量拥有自己的块级作用域,且修复了var声明变量带来的变量提升问题

面试官:举一些ES6对String字符串类型做的常用升级优化?

1、优化部分:
ES6新增了字符串模板,在拼接大段字符串时,用反斜杠(`)取代以往的字符串相加的形式,能保留所有空格和换行,使得字符串拼接看起来更加直观,更加优雅。

2、升级部分:
ES6在String原型上新增了includes()方法,用于取代传统的只能用indexOf查找包含字符的方法(indexOf返回-1表示没查到不如includes方法返回false更明确,语义更清晰), 此外还新增了startsWith(), endsWith(),padStart(),padEnd(),repeat()等方法,可方便的用于查找,补全字符串。

面试官:举一些ES6对Array数组类型做的常用升级优化?

1、优化部分:
a. 数组解构赋值。ES6可以直接以let [a,b,c] = [1,2,3]形式进行变量赋值,在声明较多变量时,不用再写很多let(var),且映射关系清晰,且支持赋默认值。

b. 扩展运算符。ES6新增的扩展运算符(...)(重要),可以轻松的实现数组和松散序列的相互转化,可以取代arguments对象和apply方法,轻松获取未知参数个数情况下的参数集合。

(尤其是在ES5中,arguments并不是一个真正的数组,而是一个类数组的对象,但是扩展运算符的逆运算却可以返回一个真正的数组)。扩展运算符还可以轻松方便的实现数组的复制和解构赋值(let a = [2,3,4]; let b = [...a])。

2、升级部分:
ES6在Array原型上新增了find()方法,用于取代传统的只能用indexOf查找包含数组项目的方法,且修复了indexOf查找不到NaN的bug([NaN].indexOf(NaN) === -1).此外还新增了copyWithin(), includes(), fill(),flat()等方法,可方便的用于字符串的查找,补全,转换等。

 面试官:举一些ES6对Number数字类型做的常用升级优化?

1、优化部分:
ES6在Number原型上新增了isFinite(), isNaN()方法,用来取代传统的全局isFinite(), isNaN()方法检测数值是否有限、是否是NaN。ES5的isFinite(), isNaN()方法都会先将非数值类型的参数转化为Number类型再做判断,这其实是不合理的,最造成isNaN('NaN') === true的奇怪行为--'NaN'是一个字符串,但是isNaN却说这就是NaN。而Number.isFinite()和Number.isNaN()则不会有此类问题(Number.isNaN('NaN') === false)。(isFinite()同上)

2、升级部分:
ES6在Math对象上新增了Math.cbrt(),trunc(),hypot()等等较多的科学计数法运算方法,可以更加全面的进行立方根、求和立方根等等科学计算。

面试官:举一些ES6对Object类型做的常用升级优化?(重要)

1、优化部分:
a. 对象属性变量式声明。ES6可以直接以变量形式声明对象属性或者方法,。比传统的键值对形式声明更加简洁,更加方便,语义更加清晰。

let [apple, orange] = ['red appe', 'yellow orange'];
let myFruits = {apple, orange};    // let myFruits = {apple: 'red appe', orange: 'yellow orange'};

尤其在对象解构赋值(见优化部分b.)或者模块输出变量时,这种写法的好处体现的最为明显:

let {keys, values, entries} = Object;
let MyOwnMethods = {keys, values, entries}; // let MyOwnMethods = {keys: keys, values: values, entries: entries}

可以看到属性变量式声明属性看起来更加简洁明了。方法也可以采用简洁写法:

let es5Fun = {
    method: function(){}
};
let es6Fun = {
    method(){}
}

b. 对象的解构赋值。ES6对象也可以像数组解构赋值那样,进行变量的解构赋值:

let {apple, orange} = {apple: 'red appe', orange: 'yellow orange'};

c. 对象的扩展运算符(...)。ES6对象的扩展运算符和数组扩展运算符用法本质上差别不大,毕竟数组也就是特殊的对象。对象的扩展运算符一个最常用也最好用的用处就在于可以轻松的取出一个目标对象内部全部或者部分的可遍历属性,从而进行对象的合并和分解。

let {apple, orange, ...otherFruits} = {apple: 'red apple', orange: 'yellow orange', grape: 'purple grape', peach: 'sweet peach'};
// otherFruits  {grape: 'purple grape', peach: 'sweet peach'}
// 注意: 对象的扩展运算符用在解构赋值时,扩展运算符只能用在最有一个参数(otherFruits后面不能再跟其他参数)
let moreFruits = {watermelon: 'nice watermelon'};
let allFruits = {apple, orange, ...otherFruits, ...moreFruits};

d. super 关键字。ES6在Class类里新增了类似this的关键字super。同this总是指向当前函数所在的对象不同,super关键字总是指向当前函数所在对象的原型对象。

2、升级部分:
a. ES6在Object原型上新增了is()方法,做两个目标对象的相等比较,用来完善'==='方法。'==='方法中NaN === NaN //false其实是不合理的,Object.is修复了这个小bug。(Object.is(NaN, NaN) // true)

b. ES6在Object原型上新增了assign()方法,用于对象新增属性或者多个对象合并。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注意:assign合并的对象target只能合并source1、source2中的自身属性,并不会合并source1、source2中的继承属性,也不会合并不可枚举的属性,且无法正确复制get和set属性(会直接执行get/set函数,取return的值)。

c. ES6在Object原型上新增了getOwnPropertyDescriptors()方法,此方法增强了ES5中getOwnPropertyDescriptor()方法,可以获取指定对象所有自身属性的描述对象。结合defineProperties()方法,可以完美复制对象,包括复制get和set属性。

d. ES6在Object原型上新增了getPrototypeOf()和setPrototypeOf()方法,用来获取或设置当前对象的prototype对象。这个方法存在的意义在于,ES5中获取设置prototype对象是通过__proto__属性来实现的,然而__proto__属性并不是ES规范中的明文规定的属性,只是浏览器各大厂商“私自”加上去的属性,只不过因为适用范围广而被默认使用了,再非浏览器环境中并不一定就可以使用,所以为了稳妥起见,获取或设置当前对象的prototype对象时,都应该采用ES6新增的标准用法。

d. ES6在Object原型上还新增了Object.keys(),Object.values(),Object.entries()方法,用来获取对象的所有键、所有值和所有键值对数组。

面试官:举一些ES6对Function函数类型做的常用升级优化?(重要)

 1、优化部分:
a. 箭头函数(核心)。箭头函数是ES6核心的升级项之一,箭头函数里没有自己的this,这改变了以往JS函数中最让人难以理解的this运行机制。主要优化点:

Ⅰ. 箭头函数内的this指向的是函数定义时所在的对象,而不是函数执行时所在的对象。ES5函数里的this总是指向函数执行时所在的对象,这使得在很多情况下this的指向变得很难理解,尤其是非严格模式情况下,this有时候会指向全局对象,这甚至也可以归结为语言层面的bug之一。ES6的箭头函数优化了这一点,它的内部没有自己的this,这也就导致了this总是指向上一层的this,如果上一层还是箭头函数,则继续向上指,直到指向到有自己this的函数为止,并作为自己的this。

Ⅱ. 箭头函数不能用作构造函数,因为它没有自己的this,无法实例化

Ⅲ. 也是因为箭头函数没有自己的this,所以箭头函数 内也不存在arguments对象。(可以用扩展运算符代替)

b. 函数默认赋值。ES6之前,函数的形参是无法给默认值的,只能在函数内部通过变通方法实现。ES6以更简洁更明确的方式进行函数默认赋值。

function es6Fuc (x, y = 'default') {
    console.log(x, y);
}
es6Fuc(4) // 4, default


2、升级部分:
ES6新增了双冒号运算符,用来取代以往的bind,call,和apply。(浏览器暂不支持,Babel已经支持转码)

foo::bar;
// 等同于
bar.bind(foo);
 
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

 面试官:CSS预编译

一、什么是CSS预编译?

        CSS 预编译,就是预先编译处理CSS。它扩展了 CSS 语言,增加了变量、Mixin、函数等编程的特性,使 CSS 更易维护和扩展。CSS预编译的工作原理是提供便捷的语法和特性供开发者编写源代码,随后经过专门的编译工具将源码转化为CSS语法。

二、为什么要使用CSS预编译?

  1. CSS缺点:
  • 语法不够强大,比如无法嵌套书写,导致模块化开发中需要书写很多重复的选择器;
  • 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。
  1. CSS预编译优点:
  • 可以提供 CSS 缺失的样式层复用机制、减少冗余代码,提高样式代码的可维护性。大大提高了开发效率。
  • 增强编程能力;增强可复用性;增强可维护性;更便于解决浏览器兼容性
  1. CSS预编译缺点:
  • CSS的好处在于简便、随时随地被使用和调试。预编译CSS步骤的加入,让我们开发工作流中多了一个环节,调试也变得更麻烦了。更大的问题在于,预编译很容易造成后代选择器的滥用。所以我们在实际项目中衡量预编译方案时,还是得想想,比起带来的额外维护开销,CSS预处理器有没有解决更大的麻烦
  1. 不同的预编译器特性虽然有所差异,但核心功能均围绕这些目标打造,比如:
  • 嵌套(所有预编译器都支持的语法特性,也是原生CSS最让开发者头疼的问题之一)
  • 变量(增强了源码的可编程能力)
  • 运算(增强了源码的可编程能力)
  • mixin/继承(为了解决hack和代码复用)
  • 模块化(不仅更利于代码复用,同时也提高了源码的可维护性)

四、CSS预处理器的选择

1、Sass

**优点**
1)    用户多,更容易找到会用scss的开发,更容易找到scss的学习资源;
2)    可编程能力比较强,支持函数,列表,对象,判断,循环等;
3)    相比less有更多的功能;
4)    Bootstrap/Foundation等使用scss;
5)    丰富的sass库:Compass/Bourbon;

**缺点**
安装node-sass会经常失败或者报错,需要使用cnpm或者手工安装

2、Less

**优点**
可以在浏览器中运行,实现主题定制功能;

**缺点**
编程能力弱,不直接支持对象,循环,判断等;
@variable 变量命名和css的@import/media/keyframes等含义容易混淆;
mixin/extend的语法比较奇怪;
mixin的参数如果遇到多参数和列表参数值的时候容易混淆;

3、Stylus

**优点**
来自NodeJS社区,所以和NodeJS走得很近,与JavaScript联系非常紧密。还有专门JavaScript API:
http://learnboost.github.io/stylus/docs/js.html
支持Ruby之类等等框架3.更多更强大的支持和功能

**缺点**
人气不高和教程较少

总结:
        Sass看起来在提供的特性上占有优势,但是LESS能够让开发者平滑地从现存CSS文件过渡到LESS,而不需要像Sass那样需要将CSS文件转换成Sass格式。Stylus功能上更为强壮,和js联系更加紧密。

面试官:DNS的解析过程 

什么是 DNS

DNS 即域名系统,全称是 Domain Name System。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道服务器的 IP,对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址

DNS 是:

  1. 一个由分层的 DNS 服务器实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议

 也就是,DNS 是一个应用层协议,我们发送一个请求,其中包含我们要查询的主机名,它就会给我们返回这个主机名对应的 IP;

其次,DNS 是一个分布式数据库,整个 DNS 系统由分散在世界各地的很多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据可以让我们最终查到主机名对应的 IP。

所以 DNS 的查询过程,说白了,就是去向这些 DNS 服务器询问,你知道这个主机名的 IP 是多少吗,不知道?那你知道去哪台 DNS 服务器上可以查到吗?直到查到我想要的 IP 为止。

分布式、层次数据库 

 什么是分布式?

这个世界上没有一台 DNS 服务器拥有因特网上所有主机的映射,每台 DNS 只负责部分映射。

什么是层次?

 DNS 服务器有 3 种类型:根 DNS 服务器、顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。它们的层次结构如下图所示:

web前端面试题(必背面试题)_第43张图片

  • 根 DNS 服务器

首先我们要明确根域名是什么,比如 www.baidu.com,有些同学可能会误以为 com 就是根域名,其实 com 是顶级域名,www.baidu.com 的完整写法是 www.baidu.com.,最后的这个 . 就是根域名。

根 DNS 服务器的作用是什么呢?就是管理它的下一级,也就是顶级域 DNS 服务器。通过询问根 DNS 服务器,我们可以知道一个主机名对应的顶级域 DNS 服务器的 IP 是多少,从而继续向顶级域 DNS 服务器发起查询请求。

  • 顶级域 DNS 服务器

除了前面提到的 com 是顶级域名,常见的顶级域名还有 cnorgedu 等。顶级域 DNS 服务器,也就是 TLD,提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。

  • 权威 DNS 服务器

权威 DNS 服务器可以返回主机 - IP 的最终映射。

关于这几个层次的服务器之间是怎么交互的,接下来我们会讲到 DNS 具体的查询过程,结合查询过程,大家就不难理解它们之间的关系了。

本地 DNS 服务器 

之前对 DNS 有过了解的同学可能会发现,上一节的 DNS 层次结构,为什么没有提到本地 DNS 服务器?因为严格来说,本地 DNS 服务器并不属于 DNS 的层次结构,但它对 DNS 层次结构是至关重要的。那什么是本地 DNS 服务器呢?

每个 ISP 都有一台本地 DNS 服务器,比如一个居民区的 ISP、一个大学的 ISP、一个机构的 ISP,都有一台或多台本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,本地 DNS 服务器起着代理的作用,并负责将该请求转发到 DNS 服务器层次结构中。

接下来就让我们通过一个简单的例子,看看 DNS 的查询过程是怎样的,看看客户端、本地 DNS 服务器、DNS 服务器层次结构之间是如何交互的。

 递归查询、迭代查询

 如下图,假设主机 m.n.com 想要获取主机 a.b.com 的 IP 地址,会经过以下几个步骤:

web前端面试题(必背面试题)_第44张图片

  1. 首先,主机 m.n.com 向它的本地 DNS 服务器发送一个 DNS 查询报文,其中包含期待被转换的主机名 a.b.com

  2. 本地 DNS 服务器将该报文转发到根 DNS 服务器;

  3. 该根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器(TLD)的 IP 地址列表。

    意思就是,我不知道 a.b.com 的 IP,不过这些 TLD 服务器可能知道,你去问他们吧;

  4. 本地 DNS 服务器则向其中一台 TLD 服务器发送查询报文;

  5. 该 TLD 服务器注意到 b.com 前缀,便向本地 DNS 服务器返回权威 DNS 服务器的 IP 地址。

    意思就是,我不知道 a.b.com 的 IP,不过这些权威服务器可能知道,你去问他们吧;

  6. 本地 DNS 服务器又向其中一台权威服务器发送查询报文;

  7. 终于,该权威服务器返回了 a.b.com 的 IP 地址;

  8. 本地 DNS 服务器将 a.b.com 跟 IP 地址的映射返回给主机 m.n.comm.n.com 就可以用该 IP 向 a.b.com 发送请求啦。

“你说了这么多,递归呢?迭代呢?”

这位同学不要捉急,其实递归和迭代已经包含在上述过程里了。

主机 m.n.com 向本地 DNS 服务器 dns.n.com 发出的查询就是递归查询,这个查询是主机 m.n.com 以自己的名义向本地 DNS 服务器请求想要的 IP 映射,并且本地 DNS 服务器直接返回映射结果给到主机。

而后继的三个查询是迭代查询,包括本地 DNS 服务器向根 DNS 服务器发送查询请求、本地 DNS 服务器向 TLD 服务器发送查询请求、本地 DNS 服务器向权威 DNS 服务器发送查询请求,所有的请求都是由本地 DNS 服务器发出,所有的响应都是直接返回给本地 DNS 服务器

那么问题来了,所有的 DNS 查询都必须遵循这种递归 + 迭代的模式吗?

当然不是。

从理论上讲,任何 DNS 查询既可以是递归的,也可以是迭代的。下图的所有查询就都是递归的,不包含迭代。

web前端面试题(必背面试题)_第45张图片

 看到这里,大家可能会有个疑问,TLD 一定知道权威 DNS 服务器的 IP 地址吗?

还真不一定,有时 TLD 只是知道中间的某个 DNS 服务器,再由这个中间 DNS 服务器去找到权威 DNS 服务器。这种时候,整个查询过程就需要更多的 DNS 报文。

 DNS 缓存

​​​​​​​

为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。

事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。

面试官:为什么Vue中的v-if和v-for不建议一起用?

v-for 优先级是比 v-if 高

这时候我们可以看到,v-for 与 v-if 作用再不同标签时候,是先进性判断,再进行列表的渲染

注意事项

  1. 永远不要把 v-if 和 v-for 同时用在一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  2. 如果避免出现这种情况,则在外层嵌套 template (页面渲染不生成dom节点),再这一层进行 v-if 判断,然后再内部进行 v-for 循环
  3. 如果条件出现再循环内部,可通过计算属性 computed 提前过滤掉那些不需要显示的项

面试官:vue插槽(slot)的使用

vue的slot分为三种::匿名插槽,具名插槽, 作用域插槽

作用:让父组件可以向子组件指定位置插入 html 结构,也是一种组件间通信的方式,适用于父组  件=>子组件

一.匿名插槽 

1)带有插槽 的组件 TipsText.vue(子组件)

  作为我们想要插入内容的占位符——就这么简单!

//子组件


//父组件

二.具名插槽 (给插槽加入name属性就是具名插槽)

//子组件

//父组件

三.作用域插槽

//子组件



//父组件


面试官:如何创建插件

1、新建一个plugins.js的文件(文件名可以不是plugins)
2、plugins.js内容:
必须有install()函数,第一参数是Vue的构造函数,剩余参数可自己传入

// plugins.js
export default {
    install(Vue) {
        console.log('使用插件了')

        // 自定义全局获取焦点指令
        Vue.directive('focus',{
            inserted: function(el){
                // el代表绑定的元素
                el.focus();
            }
        })

        // 定义一个全局过滤器 ---- 字母转大写
        Vue.filter('toUpperCase', function(val){
            return val.toUpperCase();
        });

        // 添加一个混入
        Vue.mixin({
            data() {
                return {
                    mixData: '这是mixin中数据',
                    x: 100,
                }
            },
            created () {
              console.log(this.y)
            },
            methods: {
                handleClick() {
                    console.log(this.name)
                }
            },
            computed: {
                showNumber() {
                    return this.x + this.y
                }
            }
        })

        // 给Vue原型上添加一个方法
        Vue.prototype.hello = () => {
            alert("你好啊")
        }
    }
}

面试官:HTTP 和 HTTPS 

HTTP请求的格式

HTTP的请求分成四个部分:

1、请求行;

HTTP方法:大概,描述了这个请求想要干什么,例如get意思就是想从服务器获取到什么

URL:描述了要访问的网络上的资源具体是在哪

版本号:表示当前使用的HTTP的版本是什么,目前常用的版本是1.1

2、请求报头;

这一部分一般有很多行,每一行都是一个键值对,键和值之间通过 :空格 来分割。

3、空行;

请求头的结束标志

4、请求正文

这一部分可有可无,有时候会存在有时候没有。

 HTTP响应的格式

1,状态行 

版本号:代表当前HTTP协议的版本

状态码:描述了这个响应是表示成功还是失败的,以及不同的状态码也描述了失败的原因,常见的如404

 状态码的描述:通过一个或是一组简短的单词描述了当前状态码的含义

2,响应头

也是键值对结构,每个键值对占一行,每个键和值之间通过 :空格 进行分割。响应头中的键值对个数是不确定的,不同的键值对也代表了不同的关系。

3, 空行

响应头的结束标志

4 ,响应正文

是服务器返回客户端的具体数据,可能会有各种不同的格式,其中最常见的格式就是HTNL。

 HTTP方法

HTTP协议的方法有很多,其中常用的是GET和POST。

 1、GET方法
GET方法用于使用给定的URI从给定服务器中检索信息,即从指定资源中请求数据。使用GET方法的请求应该只是检索数据,并且不应对数据产生其他影响。

2、POST方法
POST方法用于将数据发送到服务器以创建或更新资源,它要求服务器确认请求中包含的内容作为由URI区分的Web资源的另一个下属。

POST请求永远不会被缓存,且对数据长度没有限制;我们无法从浏览器历史记录中查找到POST请求。

3、HEAD方法

HEAD方法与GET方法相同,但没有响应体,仅传输状态行和标题部分。这对于恢复相应头部编写的元数据非常有用,而无需传输整个内容。

4、PUT方法
从客户端向服务器传送的数据取代指定的文档的内容。
PUT方法用于将数据发送到服务器以创建或更新资源,它可以用上传的内容替换目标资源中的所有当前内容。

它会将包含的元素放在所提供的URI下,如果URI指示的是当前资源,则会被改变。如果URI未指示当前资源,则服务器可以使用该URI创建资源。

5、DELETE方法

DELETE方法用来删除指定的资源,它会删除URI给出的目标资源的所有当前内容。

6、CONNECT方法
HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
CONNECT方法用来建立到给定URI标识的服务器的隧道;它通过简单的TCP / IP隧道更改请求连接,通常实使用解码的HTTP代理来进行SSL编码的通信(HTTPS)。

7、OPTIONS方法

OPTIONS方法用来描述了目标资源的通信选项,会返回服务器支持预定义URL的HTTP策略。

8、TRACE方法

TRACE方法用于沿着目标资源的路径执行消息环回测试;它回应收到的请求,以便客户可以看到中间服务器进行了哪些(假设任何)进度或增量。

HTTP特点:

1.无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作
2.无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
3.基于请求和响应:基本的特性,由客户端发起请求,服务端响应
4.简单快速、灵活
5.通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性

 

HTTPS特点:

  1. 内容加密:采用混合加密技术,中间者无法直接查看明文内容
  2. 验证身份:通过证书认证客户端访问的是自己的服务器
  3. 保护数据完整性:防止传输的内容被中间人冒充或者篡改

 常见状态码

1xx:信息提示

2xx:成功

3xx:重定向

4xx:客户端错误

5xx:服务器错误

  • 1xx: 接受,继续处理
  • 200: 成功,并返回数据
  • 201: 已创建
  • 202: 已接受
  • 203: 成为,但未授权
  • 204: 成功,无内容
  • 205: 成功,重置内容
  • 206: 成功,部分内容
  • 301: 永久移动,重定向
  • 302: 临时移动,可使用原有URI
  • 304: 资源未修改,可使用缓存
  • 305: 需代理访问
  • 400: 请求语法错误
  • 401: 要求身份认证
  • 403: 拒绝请求
  • 404: 资源不存在
  • 500: 服务器错误

 面试官:你们项目RESTful API设计规范是什么?

动词+宾语

RESTful的核心思想就是,客户端发出的数据+操作指令都是“动词+宾语”的结构,比如GET /articles这个命令,GET是动词,/articles是宾语,动词通常就有5种HTTP请求方法,对应CRUD操作,根据 HTTP 规范,动词一律大写。

# GET:读取(Read)

# POST:新建(Create)

# PUT:更新(Update)

# PATCH:更新(Update),通常是部分更新

# DELETE:删除(Delete)

动词的覆盖

有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法(PUT、PATCH、DELETE)。这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。

宾语必须是名词

宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/articles这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。

复数 URL

  既然 URL 是名词,那么应该使用复数,还是单数?这没有统一的规定,但是常见的操作是读取一个集合,比如GET /articles(读取所有文章),这里明显应该是复数。

避免多级 URL

  常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取卖手游账号平台某个作者的某一类文章。

  # GET /authors/12/categories/2

  这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。

总 结:Restful API的接口架构风格中制定了一些规范,极大的简化了前后端对接的时间,以及增加了开发效率,在实际开发中,比如在获取列表分页的时候,对于查询参数过多的接口,会导致uri的长度过长、请求失败,在这种情况下的接口就不能完全按照Restful API的请求规范来。Restful API也就只是一种接口架构的风格,接口API永远不会强约束于此,因按照实际需求做出相应的接口需改。

面试官:git代码回滚

git回退历史,有以下步骤:

1.已push后回退:
(1) 使用git log命令,查看分支提交历史,确认需要回退版本的
(2) 使用git reset --hard 命令或者git revert ,进行版本回退(此时本地已回退)
(3) 在git commit后,再次使用git push origin <分支名> --force命令,推送至远程分支(此时线上版本已回退)

快捷命令:git reset --hard HEAD^
注:

HEAD是指向当前版本的指针,HEAD^表示上个版本,HEAD^^表示上上个版本
revertreset的区别:
(1) 在实际生产环境中,代码是基于master分支发布到线上的,会有多人进行提交。可能会碰到自己或团队其他成员开发的某个功能在上线之后有Bug,需要及时做代码回滚的操作。
在确认要回滚的版本之后,如果别人没有最新提交,那么就可以直接用reset命令进行版本回退,否则,就可以考虑使用revert命令进行还原修改,不能影响到别人的提交。
(2) reset命令会把要回退版本之后提交的修改都删除。要从第三次修改回退到第一次修改,那么会删除第二、三次的修改。【注:这里并不是真正的物理删除】
如果发现第二次修改有错误,想要恢复第二次修改,却要保留第三次修改,使用revert命令,其产生了新的commit_id

2.提交到暂存区后(执行git add后)回退:
git reset HEAD 命令撤销提交到暂存区的内容,再使用git checkout -- 命令,来清空工作区的修改

 3.工作区(执行git add前)回退:
git checkout -- 命令,来清空工作区的修改

  面试官:react的Component和PureComponent

介绍
React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过props和state的浅对比来实现 shouldComponentUpate()。

在PureComponent中,如果包含比较复杂的数据结构,可能会因深层的数据不一致而产生错误的否定判断,导致界面得不到更新。

如果定义了 shouldComponentUpdate(),无论组件是否是 PureComponent,它都会执行shouldComponentUpdate结果来判断是否 update。如果组件未实现 shouldComponentUpdate() ,则会判断该组件是否是 PureComponent,如果是的话,会对新旧 props、state 进行 shallowEqual 比较,一旦新旧不一致,会触发 update。

浅对比:通过遍历对象上的键执行相等性,并在任何键具有参数之间不严格相等的值时返回false。 当所有键的值严格相等时返回true。shallowEqual

不同:
PureComponent自带通过props和state的浅对比来实现 shouldComponentUpate(),而Component没有。
PureComponent缺点
可能会因深层的数据不一致而产生错误的否定判断,从而shouldComponentUpdate结果返回false,界面得不到更新。

PureComponent优势
不需要开发者自己实现shouldComponentUpdate,就可以进行简单的判断来提升性能。

 面试官: CSS3新增伪类有那些

  • :root 选择文档的根元素,等同于 html 元素
  • :empty 选择没有子元素的元素
  • :target 选取当前活动的目标元素
  • :not(selector) 选择除 selector 元素意外的元素
  • :enabled 选择可用的表单元素
  • :disabled 选择禁用的表单元素
  • :checked 选择被选中的表单元素
  • :after 在元素内部最前添加内容
  • :before 在元素内部最后添加内容
  • :nth-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n
  • :nth-last-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n,从后向前数
  • :nth-child(odd)
  • :nth-child(even)
  • :nth-child(3n+1)
  • :first-child
  • :last-child
  • :only-child
  • :nth-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n
  • :nth-last-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n,从后向前数
  • :nth-of-type(odd)
  • :nth-of-type(even)
  • :nth-of-type(3n+1)
  • :first-of-type
  • :last-of-type
  • :only-of-type
  • ::selection 选择被用户选取的元素部分
  • :first-line 选择元素中的第一行:first-letter 选择元素中的第一个字符

你可能感兴趣的:(web面试题,前端,css3,css)