中级前端面试题目

说说CSS选择器以及这些选择器的优先级

  • !important

  • 内联样式(1000

  • ID选择器(0100

  • 类选择器/属性选择器/伪类选择器(0010

  • 元素选择器/伪元素选择器(0001

  • 关系选择器/通配符选择器(0000

你知道什么是BFC么

小提示:这个问题重点是BFC是什么,BFC触发的条件有哪些,BFC可以干什么。这里我试着讲解了一下Boostrap的清除浮动(display:table创建匿名table-cell间接触发BFC),如果有看到别的场景使用或者自身有使用的场景可以尝试讲解一下使用技巧。这样可以让面试官觉得你不仅仅知道他问的东西是什么,你还能很好的使用它。

什么是BFC

BFC 全称为块级格式化上下文 (Block Formatting Context) 。BFC是 W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行定位以及与其他元素的关系和相互作用,当涉及到可视化布局的时候,Block Formatting Context提供了一个环境,HTML元素在这个环境中按照一定规则进行布局。一个环境中的元素不会影响到其它环境中的布局。比如浮动元素会形成BFC,浮动元素内部子元素的主要受该浮动元素影响,两个浮动元素之间是互不影响的。这里有点类似一个BFC就是一个独立的行政单位的意思。可以说BFC就是一个作用范围,把它理解成是一个独立的容器,并且这个容器里box的布局与这个容器外的box毫不相干。

触发BFC的条件

  • 根元素或其它包含它的元素

  • 浮动元素 (元素的 float 不是 none)

  • 绝对定位元素 (元素具有 position 为 absolute 或 fixed)

  • 内联块 (元素具有 display: inline-block)

  • 表格单元格 (元素具有 display: table-cell,HTML表格单元格默认属性)

  • 表格标题 (元素具有 display: table-caption, HTML表格标题默认属性)

  • 具有overflow 且值不是 visible 的块元素

  • 弹性盒(flex或inline-flex)

  • display: flow-root

  • column-span: all

BFC的约束规则

  • 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流)

  • 处于同一个BFC中的元素相互影响,可能会发生外边距重叠

  • 每个元素的margin box的左边,与容器块border box的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然

  • 计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算

  • 浮动盒区域不叠加到BFC上

BFC可以解决的问题

  • 垂直外边距重叠问题

  • 去除浮动

  • 自适用两列布局(float + overflow

了解盒模型么

包括内容区域、内边距区域、边框区域和外边距区域。

box-sizing: content-box(W3C盒子模型):元素的宽高大小表现为内容的大小。 box-sizing: border-box(IE盒子模型):元素的宽高表现为内容 + 内边距 + 边框的大小。背景会延伸到边框的外沿。

IE5.x和IE6在怪异模式中使用非标准的盒子模型,这些浏览器的width属性不是内容的宽度,而是内容、内边距和边框的宽度的总和。

如何实现左侧宽度固定,右侧宽度自适应的布局

小提示:这个问题面试官会要求说出几种解决方法。

DOM结构

<div class="box">
<div class="box-left"></div>
<div class="box-right"></div>
</div>

利用float + margin实现

.box {
height: 200px;
}

.box > div {
height: 100%;
}

.box-left {
200px;
float: left;
background-color: blue;
}

.box-right {
margin-left: 200px;
background-color: red;
}

利用calc计算宽度

.box {
height: 200px;
}

.box > div {
height: 100%;
}

.box-left {
200px;
float: left;
background-color: blue;
}

.box-right {
calc(100% - 200px);
float: right;
background-color: red;
}

利用float + overflow实现

.box {
height: 200px;
}

.box > div {
height: 100%;
}

.box-left {
200px;
float: left;
background-color: blue;
}

.box-right {
overflow: hidden;
background-color: red;
}

利用flex实现

这里不是最佳答案,应该是使用flex-basis实现更合理

.box {
height: 200px;
display: flex;
}

.box > div {
height: 100%;
}

.box-left {
200px;
background-color: blue;
}

.box-right {
flex: 1; // 设置flex-grow属性为1,默认为0
overflow: hidden;
background-color: red;
}

了解跨域吗,一般什么情况下会导致跨域

小提示:如果平常自身有使用场景可结合使用场景进行讲解,比如我在这里使用过的场景是CORSNginx反向代理

跨域行为

  • 同源策略限制、安全性考虑

  • 协议、IP和端口不一致都是跨域行为

JSONP

小提示:如果你提到JSONP,面试官肯定会问你整个详细的实现过程,所以一定要搞懂JSONP的实现原理,如果不是很理解可以自己起一个Express服务实践一下

  • Web前端事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。

  • 缺点:JSONP只能发起GET请求

如何实现一个JSONP

  • https://segmentfault.com/a/1190000015597029

  • https://zhangguixu.github.io/2016/12/02/JSONP/

  • https://www.cnblogs.com/iovec/p/5312464.html

JSONP安全性问题

1. CSRF攻击

  • 前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。

  • 解决方法:验证JSONP的调用来源(Referer),服务端判断Referer是否是白名单,或者部署随机Token来防御。

2. XSS漏洞

不严谨的 content-type导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://youdomain.com?callback=douniwan, 然后返回 douniwan({ data }),那假如请求 http://youdomain.com?callback=<script>alert(1)</script> 不就返回 <script>alert(1)</script>({ data })了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback 参数,直接当 html 解析了,就是一个赤裸裸的 XSS 了。

  • 解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成&lt,>换成&gt)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。

3. 服务器被黑,返回一串恶意执行的代码

可以将执行的代码转发到服务端进行校验JSONP内容校验,再返回校验结果

CORS(跨域资款共享)

小提示:如果你回答跨域解决方案CORS,那么面试官一定会问你实现CORS的响应头信息Access-Control-Allow-Origin

1. 什么是CORS

  • 浏览器端会自动向请求头添加origin字段,表明当前请求来源。

  • 服务器端需要设置响应头的Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin等字段,指定允许的方法,头部,源等信息。

  • 请求分为简单请求和非简单请求,非简单请求会先进行一次OPTION方法进行预检,看是否允许当前跨域请求。

简单请求

请求方法是以下三种方法之一:

  • HEAD

  • GET

  • POST

HTTP的请求头信息不超出以下几种字段:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

后端的响应头信息:

  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

  • Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie

  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获

JSONP和CORS的对比

  • JSONP只支持GET请求,CORS支持所有类型的HTTP请求

  • JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

其他跨域解决方案

  • Nginx反向代理

  • postMessage

  • document.domain

HTTP2和HTTP1有什么区别

相对于HTTP1.0,HTTP1.1的优化:

  • 缓存处理:多了Entity tag,If-Unmodified-Since, If-Match, If-None-Match等缓存信息(HTTTP1.0 If-Modified-Since,Expires)

  • 带宽优化及网络连接的使用

  • 错误通知的管理

  • Host头处理

  • 长连接:HTTP1.1中默认开启Connection:keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

相对于HTTP1.1,HTTP2的优化:

  • HTTP2支持二进制传送(实现方便且健壮),HTTP1.x是字符串传送

  • HTTP2支持多路复用

  • HTTP2采用HPACK压缩算法压缩头部,减小了传输的体积

  • HTTP2支持服务端推送

你能说说缓存么

小提示:如果平常有遇到过缓存的坑或者很好的利用缓存,可以讲解一下自己的使用场景。如果没有使用注意过缓存问题你也可以尝试讲解一下和我们息息相关的Webpack构建(每一次构建静态资源名称的hash值都会变化),它其实就跟缓存相关

缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商

1. 强缓存

Expires(HTTP1.0):Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差。另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代

缺点:使用的是绝对时间,如果服务端和客户端的时间产生偏差,那么会导致命中缓存产生偏差

Pragma(HTTP1.0):HTTP1.0时的遗留字段,当值为"no-cache"时强制验证缓存,Pragma禁用缓存,如果又给Expires定义一个还未到期的时间,那么Pragma字段的优先级会更高。服务端响应添加'Pragma': 'no-cache',浏览器表现行为和刷新(F5)类似。

Cache-Control(HTTP1.1):有很多属性,不同的属性代表的意义也不同

  • private:客户端可以缓存

  • public:客户端和代理服务器都可以缓存

  • max-age=t:缓存内容将在t秒后失效

  • no-cache:需要使用协商缓存来验证缓存数据

  • no-store:所有内容都不会缓存

请注意no-cache指令很多人误以为是不缓存,这是不准确的,no-cache的意思是可以缓存,但每次用应该去想服务器验证缓存是否可用。no-store才是不缓存内容。当在首部字段Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。命中强缓存的表现形式:Firefox浏览器表现为一个灰色的200状态码。Chrome浏览器状态码表现为200 (from disk cache)或是200 OK (from memory cache)

2. 协商缓存

协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了

  • Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。

  • if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可

    • 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK

    • 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified

  • if-Unmodified-Since: 从某个时间点算起, 是否文件没有被修改,使用的是相对时间,不需要关心客户端和服务端的时间偏差

    • 如果没有被修改:则开始`继续'传送文件,服务器返回: 200 OK

    • 如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误)

这两个的区别是一个是修改了才下载一个是没修改才下载。如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag

  • Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)

  • If-Match:条件请求,携带上一次请求中资源的ETag,服务器根据这个字段判断文件是否有新的修改

  • If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比

    • 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。

    • 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304.

但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。

  • 浏览器地址栏中写入URL,回车浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿(最快)

  • F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就胆胆襟襟的发送一个请求带上If-Modify-since

  • Ctrl+F5告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作

3. 缓存场景

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存

  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新

  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

能说说首屏加载优化有哪些方案么

小提示:如果做过类似优化的同学,可能就比较好回答,没有做过类似优化的同学可以重点讲解一下懒加载(当然我这里被面试官追问过懒加载的Webpack配置问题)。同时不知道使用Vue技术栈的同学们有没有仔细观察过Vue CLI 3构建的html文件中的link标签的rel属性。

  • Vue-Router路由懒加载(利用Webpack的代码切割)

  • 使用CDN加速,将通用的库从vendor进行抽离

  • Nginx的gzip压缩

  • Vue异步组件

  • 服务端渲染SSR

  • 如果使用了一些UI库,采用按需加载

  • Webpack开启gzip压缩

  • 如果首屏为登录页,可以做成多入口

  • Service Worker缓存文件处理

  • 使用link标签的rel属性设置 prefetch(这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低,prefetch通常用于加速下一次导航)、preload(preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度)

如何在Node端配置路径别名(类似于Webpack中的alias配置)

  • 全局变量

  • 环境变量

  • 自己HACK一个@符号,指向特定的路径

  • HACK require方法

谈谈你对作用域链的理解

小提示:同类型的问题还可以是原型链、继承、闭包等,这种概念性的问题你肯定不是一句两句能说清楚的,建议在理解之后自己尝试总结一下,如何把重要的知识点用简短的话语说明白

了解作用域链之前我们要知道一下几个概念:

  • 函数的生命周期

  • 变量和函数的声明

  • Activetion Object(AO)、Variable Object(VO)

函数的生命周期:

  • 创建:JS解析引擎进行预解析,会将函数声明提前,同时将该函数放到全局作用域中或当前函数的上一级函数的局部作用域中。

  • 执行:JS引擎会将当前函数的局部变量和内部函数进行声明提前,然后再执行业务代码,当函数执行完退出时,释放该函数的执行上下文,并注销该函数的局部变量

变量和函数的声明:如果变量名和函数名声明时相同,函数优先声明

Activetion Object(AO)、Variable Object(VO):

  • AO:Activetion Object(活动对象)

  • VO:Variable Object(变量对象)

VO对应的是函数创建阶段,JS解析引擎进行预解析时,所有的变量和函数的声明,统称为Variable Object。该变量与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。VO是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容

  • 变量 (var, 变量声明);

  • 函数声明 (FunctionDeclaration, 缩写为FD);

  • 函数的形参

AO对应的是函数执行阶段,当函数被调用执行时,会建立一个执行上下文,该执行上下文包含了函数所需的所有变量,该变量共同组成了一个新的对象就是Activetion Object。该对象包含了

  • 函数的所有局部变量

  • 函数的所有命名参数

  • 函数的参数集合

  • 函数的this指向

作用域链:

  • 当代码在一个环境中创建时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。如果是函数执行阶段,那么将其activation object(AO)作为作用域链第一个对象,第二个对象是上级函数的执行上下文AO,下一个对象依次类推。

  • 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

Vue响应式原理

小提示:如果面试者使用的是Vue技术栈,那么响应式原理是一个必问的问题,同时面试官经常也会问Vue 3.0在响应式原理上的优化方案。

了解Event Loop么

小提示:这个题目问到的概率还是蛮大的,这里面试官询问了我浏览器端和Node端的Event Loop有什么不同点

事件触发线程管理的任务队列是如何产生的呢?事实上这些任务就是从JS引擎线程本身产生的,主线程在运行时会产生执行栈,栈中的代码调用某些异步API时会在任务队列中添加事件,栈中的代码执行完毕后,就会读取任务队列中的事件,去执行事件对应的回调函数,如此循环往复,形成事件循环机制。JS中有两种任务类型:微任务(microtask)和宏任务(macrotask),在ES6中,microtask称为 jobs,macrotask称为 task

  • 宏任务:script (主代码块)、setTimeout 、setInterval 、setImmediate 、I/O 、UI rendering

  • 微任务:process.nextTick(Nodejs) 、Promise 、Object.observe 、MutationObserver

Node.js中Event Loop和浏览器中Event Loop有什么区别

   ┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks、timers、setImmediate)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)
└───────────────────────┘

Node.js中宏任务分成了几种类型,并且放在了不同的task queue里。不同的task queue在执行顺序上也有区别,微任务放在了每个task queue的末尾:

  • setTimeout/setInterval 属于 timers 类型;

  • setImmediate 属于 check 类型;

  • socket 的 close 事件属于 close callbacks 类型;

  • 其他 MacroTask 都属于 poll 类型。

  • process.nextTick 本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;

  • 所有 MicroTask 的执行时机在不同类型的 MacroTask 切换后。

  • idle/prepare 仅供内部调用,我们可以忽略。

  • pending callbacks 不太常见,我们也可以忽略。

如何避免回流和重绘

  • 浏览器使用流式布局模型 (Flow Based Layout)

  • 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree

  • 有了RenderTree就能知道所有节点的样式,计算节点在页面上的大小和位置,把节点绘制到页面上

  • 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,通常需要多次计算且要花费3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一

浏览器渲染过程如下:

  • 解析HTML,生成DOM树

  • 解析CSS,生成CSSOM树

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

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

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

  • Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会会写一篇博客)

何时触发回流和重绘

何时发生回流:

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

  • 元素的位置发生变化

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

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

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

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

何时发生重绘(回流一定会触发重绘):

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化,浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。你访问以下属性或方法时,浏览器会立刻清空队列:

  • clientWidth、clientHeight、clientTop、clientLeft

  • offsetWidth、offsetHeight、offsetTop、offsetLeft

  • scrollWidth、scrollHeight、scrollTop、scrollLeft

  • width、height

  • getComputedStyle()

  • getBoundingClientRect()

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列 如果要使用它们,最好将值缓存起来。

如何避免触发回流和重绘

CSS:

  • 避免使用table布局。

  • 尽可能在DOM树的最末端改变class

  • 避免设置多层内联样式。

  • 将动画效果应用到position属性为absolutefixed`的元素上

  • 避免使用CSS表达式(例如:calc()

  • CSS3硬件加速(GPU加速)

JavaScript:

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性

  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘

  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来

  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

 

二面

小提示:进入现场面试需要注意好好准备自己的简历,面试官一般会根据项目进行问答。

笔试题环节

一开始面试官就发了两张笔试题试卷,总共四道题目,大致考了以下知识点:

  • 作用域

  • 原型链(例如实例属性和原型属性一样,删除实例属性后可以继续访问原型属性问题)

  • 宏任务和微任务的打印顺序

  • Array.prototype.map的第二个参数

项目问答环节

答完试卷面试官就开始问简历上的一些项目,我记得其中几个问题如下(事实上他问的一些问题和简历不是很相关):

  • 你们产品的服务器部署在哪里

  • 你是如何实现一个Tooltip组件的,能写一下怎么使用这个组件么(这算什么问题...)

  • 我认识你们海康的一些开发,我知道你们的产品按套数卖的...

我当场就感受到了面试官问的问题很敷衍,可能他觉得我的简历不够好,又或者觉得我能力不行,接下来面试官又让我做了一道算法题...

算法题环节

1块、4块、5块,求总数n块的最小硬币数

当时没做出来,非科班出身可能做这些确实有些困难,也没有系统的学习,面试官看我很困难的样子,就换了一道题。

1、1、2、3、5、8...计算第n个数的值(斐波那契数列)

这道题还是做出来了,毕竟比较简单,然后面试官说今天先到这里,面试结果会在一星期内通知,然后回来的那天晚上就收到了面试没过的通知。

 

浏览器事件循环的方式

宏任务和微任务如何划分

如何判断 js 变量类型

说一下vdom 和 diff

diff 算法的时间复杂度

hooks 相对于之前的开发方式有什么特点

react 开发中有哪些性能优化的方式

setState 是异步吗

如何实现批量更新的策略

HTTP2 相对于 HTTP1 的优化

 

你知道哪些安全问题,如何避免

小提示:这里我简单讲解了一下Vue中的v-html防范XSS攻击。

1. XSS(跨站脚本攻击)

XSS,即 Cross Site Script,中译是跨站脚本攻击;其原本缩写是 CSS,但为了和层叠样式表(Cascading Style Sheet)有所区分,因而在安全领域叫做 XSS。

  • XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。

  • 攻击者对客户端网页注入的恶意脚本一般包括 JavaScript,有时也会包含 HTML 和 Flash。有很多种方式进行 XSS 攻击,但它们的共同点为:将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。

  • XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM。

2. 反射型

反射型 XSS 只是简单地把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接(攻击者可以将恶意链接直接发送给受信任用户,发送的方式有很多种,比如 email, 网站的私信、评论等,攻击者可以购买存在漏洞网站的广告,将恶意链接插入在广告的链接中),或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。最简单的示例是访问一个链接,服务端返回一个可执行脚本:

const http = require('http');
function handleReequest(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
res.write('<script>alert("反射型 XSS 攻击")</script>');
res.end();
}

const server = new http.Server();
server.listen(8001, '127.0.0.1');
server.on('request', handleReequest);

3. 存储型

存储型 XSS 会把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。比较常见的一个场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码

// 例如在评论中输入以下留言
// 如果请求这段留言的时候服务端不做转义处理,请求之后页面会执行这段恶意代码
<script>alert('xss 攻击')</script>

4. 基于DOM

基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击:

<h2>XSS: </h2>
<input type="text" id="input">
<button id="btn">Submit</button>
<div id="div"></div>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
const div = document.getElementById('div');

let val;

input.addEventListener('change', (e) => {
val = e.target.value;
}, false);

btn.addEventListener('click', () => {
div.innerHTML = `<a href=${val}>testLink</a>`
}, false);
</script>

点击 Submit 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:

'' onclick=alert(/xss/)

用户提交之后,页面代码就变成了:

<a href onlick="alert(/xss/)">testLink</a>

此时,用户点击生成的链接,就会执行对应的脚本。

5. XSS攻击防范

  • HttpOnly 防止劫取 Cookie:HttpOnly 最早由微软提出,至今已经成为一个标准。浏览器将禁止页面的Javascript 访问带有 HttpOnly 属性的Cookie。上文有说到,攻击者可以通过注入恶意脚本获取用户的 Cookie 信息。通常 Cookie 中都包含了用户的登录凭证信息,攻击者在获取到 Cookie 之后,则可以发起 Cookie 劫持攻击。所以,严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。

  • 输入检查:不要相信用户的任何输入。对于用户的任何输入要进行检查、过滤和转义。建立可信任的字符和 HTML 标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。在 XSS 防御中,输入检查一般是检查用户输入的数据中是否包含 <,> 等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。而在一些前端框架中,都会有一份 decodingMap, 用于对用户输入所包含的特殊字符或标签进行编码或过滤,如 <,>,script,防止 XSS 攻击

// vuejs 中的 decodingMap
// 在 vuejs 中,如果输入带 script 标签的内容,会直接过滤掉
const decodingMap = {
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&amp;': '&',
'&#10;': ' '
}

输出检查:用户的输入会存在问题,服务端的输出也会存在问题。一般来说,除富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。例如利用 sanitize-html 对输出内容进行有规则的过滤之后再输出到页面中。

CSRF/XSRF(跨站请求伪造)

CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。

1. Cookie

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)

  • 个性化设置(如用户自定义设置、主题等)

  • 浏览器行为跟踪(如跟踪分析用户行为等)

而浏览器所持有的 Cookie 分为两种:

  • Session Cookie(会话期 Cookie):会话期 Cookie 是最简单的Cookie,它不需要指定过期时间(Expires)或者有效期(Max-Age),它仅在会话期内有效,浏览器关闭之后它会被自动删除。

  • Permanent Cookie(持久性 Cookie):与会话期 Cookie 不同的是,持久性 Cookie 可以指定一个特定的过期时间(Expires)或有效期(Max-Age)

res.setHeader('Set-Cookie', ['mycookie=222', 'test=3333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);

上述代码创建了两个 Cookie:mycookie 和 test,前者属于会话期 Cookie,后者则属于持久性 Cookie

2. CSRF攻击

  • 使登录用户访问攻击者的网站,发起一个请求,由于 Cookie 中包含了用户的认证信息,当用户访问攻击者准备的攻击环境时,攻击者就可以对服务器发起 CSRF 攻击。

  • 在这个攻击过程中,攻击者借助受害者的 Cookie 骗取服务器的信任,但并不能拿到 Cookie,也看不到 Cookie 的内容。而对于服务器返回的结果,由于浏览器同源策略的限制,攻击者也无法进行解析。(攻击者的网站虽然是跨域的,但是他构造的链接是源网站的,跟源网站是同源的,所以能够携带cookie发起访问)- 但是攻击者无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。例如删除数据、修改数据,新增数据等,无法获取数据。

3. CSRF攻击防范

  • 验证码:验证码被认为是对抗 CSRF 攻击最简洁而有效的防御方法。从上述示例中可以看出,CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。但验证码并不是万能的,因为出于用户考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。

  • Referer Check:根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的"源"。

  • 添加token验证:要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

介绍一下Graphql

小提示:这道题是给自己挖了一个坑,抱着学习的心态尝试使用Graphql技术,却没有好好理解是在什么场景下为了解决什么问题才应该使用,也没有好好准备如何描述新技术,往往这种不熟悉的技术自己在简历中应该留存一些心眼,尽量不要提,否则答不上来会很尴尬,让面试官怀疑你的项目成分。

什么是Graphql

GraphQL是一种API查询语言。API接口的返回值可以从静态变为动态,即调用者来声明接口返回什么数据,可以进一步解耦前后端。在Graphal中,预先定义Schema和声明Type来达到动态获取接口数据的目的:

  • 对于数据模型的抽象是通过Type来描述的

  • 对于接口获取数据的逻辑是通过Schema来描述的

为什么要使用Graphql:

  • 接口数量众多维护成本高

  • 接口扩展成本高

  • 接口响应的数据格式无法预知

  • 减少无用数据的请求, 按需获取

  • 强类型约束(API的数据格式让前端来定义,而不是后端定义)

Type(数据模型的抽象)

Type简单可以分为两种,一种叫做Scalar Type(标量类型),另一种叫做Object Type(对象类型):

  • Scalar Type(标量类型):内建的标量包含,String、Int、Float、Boolean、Enum

  • Object Type(对象类型):感觉类似于TypeScript的接口类型

  • Type Modifier(类型修饰符):用于表明是否必填等

Schema(模式)

  • 定义了字段的类型、数据的结构,描述了接口数据请求的规则

Query(查询、操作类型)

查询类型:query(查询)、mutation(更改)和subscription(订阅)

  • query(查询):当获取数据时,应当选取Query类型

  • mutation(更改):当尝试修改数据时,应当使用mutation类型

  • subscription(订阅):当希望数据更改时,可以进行消息推送,使用subscription类型

Resolver(解析函数)

提供相关Query所返回数据的逻辑。Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来。解析的过程可能是递归的,只要遇到非标量类型,会尝试继续解析,如果遇到标量类型,那么解析完成,这个过程叫做解析链。

说说Vue中$nextTick的实现原理

小提示:如果面试者使用的是Vue技术栈,那么$nextTick的原理是一个高频问题,面试者借此可以追问的东西较多,例如浏览器的Event Loop、微任务和宏任务、Node.js的Event Loop、异步更新DOM(响应式的数据for循环改变了1000次为什么视图只更新了一次)、$nextTick历史版本问题等等。

如何实现居中

水平居中

  • 若是行内元素,给其父元素设置text-align:center即可实现行内元素水平居中

  • 若是块级元素,该元素设置margin:0 auto即可(元素需要定宽)

  • 若是块级元素,设置父元素为flex布局,子元素设置margin:0 auto即可(子元素不需要定宽)

  • 使用flex 2012年版本布局,可以轻松的实现水平居中,子元素设置如下:

// flex容器
<div class="box">
// flex项目
<div class="box-center">
</div>
</div>


.box {
200px;
height: 200px;
display: flex;
// 使内部的flex项目水平居中
justify-content: center;
background-color: pink;
}

/* .box-center {
50%;

} */

使用绝对定位和CSS3新增的属性transform(这个属性还和GPU硬件加速、固定定位相关)

.box {
200px;
height: 200px;
position: relative;
background-color: pink;
}

.box-center {
position: absolute;
left:50%;
// 50%;
height: 100%;
// 通过 translate() 方法,元素从其当前位置移动,根据给定的 left(x 坐标) 和 top(y 坐标) 位置参数:
// translate(x,y) 定义 2D 转换。
// translateX(x) 定义转换,只是用 X 轴的值。
// translateY(y) 定义转换,只是用 Y 轴的值。
// left: 50% 先整体向父容器的左侧偏移50%,此时是不能居中的,因为元素本身有大小
// 接着使用transform使用百分比向左偏移本身的宽度的一半实现水平居中(这里的百分比以元素本身的宽高为基准)
transform:translate(-50%,0);
background-color: greenyellow;
}

使用绝对定位和margin-left(元素定宽)

.box {
200px;
height: 200px;
position: relative;

}

.box-center {
position: absolute;
left:50%;
height: 100%;
// 类似于transform
// 50%;
// margin-left: -25%;
100px;
margin-left: -50px;

}

垂直居中

  • 若元素是单行文本, 则可设置line-height等于父元素高度

  • 若是块级元素,设置父元素为flex布局,子元素设置margin: auto 0即可(子元素不需要定宽)

  • 若元素是行内块级元素,基本思想是使用display: inline-block, vertical-align: middle和一个伪元素让内容块处于容器中央:

.box {
height: 100px;
}

.box::after, .box-center{
display:inline-block;
vertical-align:middle;
}
.box::after{
content:'';
height:100%;
}

居中元素高度不定

可用 vertical-align 属性(vertical-align只有在父层为 td 或者 th 时才会生效,,对于其他块级元素,例如 div、p 等,默认情况是不支持的),为了使用vertical-align,我们需要设置父元素display:table, 子元素 display:table-cell;vertical-align:middle:

.box {
height: 100px;
display: table;
}
.box-center{
display: table-cell;
vertical-align:middle;
}

可用 Flex 2012版, 这是CSS布局未来的趋势。Flexbox是CSS3新增属性,设计初衷是为了解决像垂直居中这样的常见布局问题:

.box {
height: 100px;
display: flex;
align-items: center;
}

优点:内容块的宽高任意, 优雅的溢出. 可用于更复杂高级的布局技术中. 缺点:IE8/IE9不支持、需要浏览器厂商前缀、渲染上可能会有一些问题

可用 transform ,设置父元素相对定位:

.box {
height: 100px;
position: relative;
background-color: pink;
}

.box-center {
position: absolute;
top: 50%;
transform: translate(0, -50%);
background-color: greenyellow;
}

缺点:IE8不支持, 属性需要追加浏览器厂商前缀,可能干扰其他 transform 效果,某些情形下会出现文本或元素边界渲染模糊的现象。

居中元素高度固定

设置父元素相对定位,子元素如下css样式:

.box {
position:relative;
height: 100px;
background-color: pink;
}

.box-center{
position:absolute;
top:50%;
// 注意不能使用百分比
// margin的百分比计算是相对于父容器的width来计算的,甚至包括margin-top和margin-bottom
height: 50px;
margin-top: -25px;
}

设置父元素相对定位, 子元素如下css样式:

.box {
position:relative;
200px;
height: 200px;
background-color: pink;
}

.box-center{
position:absolute;
top: 0;
bottom: 0;
margin: auto 0;
height: 100px;
background-color: greenyellow;
}

水平垂直居中

Flex布局(子元素是块级元素)

.box {
display: flex;
100px;
height: 100px;
background-color: pink;
}

.box-center{
margin: auto;
background-color: greenyellow;
}

Flex布局

.box {
display: flex;
100px;
height: 100px;
background-color: pink;
justify-content: center;
align-items: center;
}

.box-center{
background-color: greenyellow;
}

绝对定位实现(定位元素定宽定高)

.box {
position: relative;
height: 100px;
100px;
background-color: pink;
}

.box-center{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin: auto;
50px;
height: 50px;
background-color: greenyellow;
}

用过Flex么,能简单介绍一下么

小提示:如果在项目中使用过,可简单介绍一下自己使用Flex解决过什么问题,这里我在项目中印象比较深刻的是使用Flex解决上面内容高度不固定,下面内容高度自动撑满父容器剩余高度的问题

bind的源码实现

小提示:这里我回答使用函数柯里化加上apply或者call可实现bind,面试官追问了一些具体的实现细节。

后来我自己粗糙的实现了一下,仅供参考:

Function.prototype.myCall = function (obj) {
obj.fn = this
let args = [...arguments].splice(1)
let result = obj.fn(...args)
delete obj.fn
return result
}

Function.prototype.myApply = function (obj) {
obj.fn = this
let args = arguments[1]
let result
if (args) {
result = obj.fn(...args)
} else {
result = obj.fn()
}

delete obj.fn

return result
}

Function.prototype.myBind = function (obj) {
let context = obj || window
let _this = this
let _args = [...arguments].splice(1)

return function () {
let args = arguments
// 产生副作用
// return obj.fn(..._args, ...args)
return _this.apply(context, [..._args, ...args])
}
}

function myFun (argumentA, argumentB) {
console.log(this.value)
console.log(argumentA)
console.log(argumentB)
return this.value
}

let obj = {
value: 'ziyi2'
}
console.log(myFun.myCall(obj, 11, 22))
console.log(myFun.myApply(obj, [11, 22]))
console.log(myFun.myBind(obj, 33)(11, 22))

伪类和伪元素的区别

小提示:这个问题我当时懵了一下,一下子没反应过来面试官想要问什么,就答了这两者在CSS优先级上有区别,然后由于遇到不会的问题有些紧张就多说了一些废话,但显然这不是面试官想要的答案并且消耗了面试官面试的耐心,说他问的不是这个。这里再次提示大家,如果你感觉你说不清楚,但是你又知道一点,我建议你说不知道,不要纠结,面试官不会因为你不知道一个问题就PASS你,相反你说了一些无关紧要的废话,反而在消耗面试官的耐性,增加负面印象。

  • 伪类和伪元素是用来修饰不在文档树中的部分,比如,一句话中的第一个字母,或者是列表中的第一个元素。下面分别对伪类和伪元素进行解释:

  • 伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过:hover来描述这个元素的状态。虽然它和普通的css类相似,可以为已有的元素添加样式,但是它只有处于dom树无法描述的状态下才能为元素添加样式,所以将其称为伪类。

  • 伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。

区别

  • 伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档树外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素。

  • CSS3规范中的要求使用双冒号(::)表示伪元素,以此来区分伪元素和伪类,比如::before和::after等伪元素使用双冒号(::),:hover和:active等伪类使用单冒号(:)。除了一些低于IE8版本的浏览器外,大部分浏览器都支持伪元素的双冒号(::)表示方法。

 


文章就分享到这,欢迎关注“前端大神之路

原文地址:https://www.cnblogs.com/cczlovexw/p/14231106.html