题 1 :什么是防抖和节流?有什么区别?如何实现?
防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
思路:每次触发事件时都取消之前的延时调用方法
节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率
思路:每次触发事件时都判断当前是否有等待执行的延时函数
题 2 :get请求传参长度的误区、get和post请求在缓存方面的区别
误区:我们经常说get请求参数的大小存在限制,而post请求的参数大小是无限制的。
实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数的限制是来源与浏览器或 web 服务器,浏览器或 web 服务器限制了 url 的长度。为了明确这个概念,我们必须再次强调下面几点:
-
HTTP 协议未规定 GET 和 POST 的长度限制
-
GET 的最大长度显示是因为浏览器和 web 服务器限制了 URI 的长度
-
不同的浏览器和 WEB 服务器,限制的最大长度不一样
-
要支持 IE,则最大长度为 2083byte,若只支持 Chrome,则最大长度 8182byte
补充补充一个 get 和 post 在缓存方面的区别:
-
get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
-
post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此 get 请求适合于请求缓存。
题 3:模块化发展历程
可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、 这几个角度考虑。
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
IIFE:使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
AMD:使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
CMD:使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。
CommonJS:nodejs 中自带的模块化。
UMD:兼容 AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules:ES6 引入的模块化,支持 import 来引入另一个 js 。
题 4:npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?
1. npm 模块安装机制:
-
发出
npm install
命令 -
查询node_modules目录之中是否已经存在指定模块
-
若存在,不再重新安装
-
若不存在
-
-
-
-
npm 向 registry 查询模块压缩包的网址
-
下载压缩包,存放在根目录下的
.npm
目录里 -
解压压缩包到当前项目的
node_modules
目录
-
-
2. npm 实现原理
输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):
-
执行工程自身 preinstall
当前 npm 工程如果定义了 preinstall 钩子此时会被执行。
-
确定首层依赖模块
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
-
获取模块
获取模块是一个递归的过程,分为以下几步:
-
-
获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
-
获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
-
查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
-
-
模块扁平化(dedupe)
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。
从 npm3 开始默认加入了一个 dedupe的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
这里需要对重复模块进行一个定义,它指的是模块名相同且 semver兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。
而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。
-
安装模块
这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
-
执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
最后一步是生成或更新版本描述文件,npm install 过程完成。
题 5:ES5 的继承和 ES6 的继承有什么区别?
ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。
题 6:setTimeout、Promise、Async/Await 的区别
移步此处查看答案
https://gongchenghuigch.github.io/2019/09/14/awat/
题 7:定时器的执行顺序或机制?
因为js是单线程的,浏览器遇到setTimeout或者setInterval会先执行完当前的代码块,在此之前会把定时器推入浏览器的待执行事件队列里面,等到浏览器执行完当前代码之后会看一下事件队列里面有没有任务,有的话才执行定时器的代码。所以即使把定时器的时间设置为0还是会先执行当前的一些代码。
输出结果:
题 8:
['1','2','3'].map(parseInt) 输出什么,为什么?
输出:[1, NaN, NaN]
-
首先让我们回顾一下,map 函数的第一个参数 callback:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
这个 callback 一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。
而 parseInt 则是用来解析字符串的,使字符串成为指定基数的整数。
parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。
-
了解这两个函数后,我们可以模拟一下运行情况
-
parseInt('1', 0) //radix为0时,且string参数不以“0x”和“0”开头时,按照 10 为基数处理。这个时候返回 1
-
parseInt('2', 1) //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN
-
parseInt('3', 2) //基数为 2(2进制)表示的数中,最大值小于 3,所以无法解析,返回 NaN
-
map 函数返回的是一个数组,所以最后结果为[1, NaN, NaN]
Doctype声明于文档最前面,告诉浏览器以何种方式来渲染页面,这里有两种模式,严格模式和混杂模式。
-
严格模式的排版和 JS 运作模式是 以该浏览器支持的最高标准运行。
-
混杂模式,向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。
fetch 发送 post 请求的时候,总是发送 2 次,第一次状态码是 204,第二次才成功?
原因很简单,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。
题 11 :TCP 三次握手和四次挥手 三次握手之所以是三次是保证 client 和 server 均让对方知道自己的接收和发送能力没问题而保证的最小次数。
第一次 client => server 只能 server 判断出 client 具备发送能力 第二次 server => client client 就可以判断出 server 具备发送和接受能力。此时 client 还需让 server 知道自己接收能力没问题于是就有了第三次 第三次 client => server 双方均保证了自己的接收和发送能力没有问题
其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的 ACK 都会对这个 seq 进行加一来进行确认。
题 12 :img iframe script 来发送跨域请求有什么优缺点?
-
iframe
优点:跨域完毕之后 DOM 操作和互相之间的 JavaScript 调用都是没有问题的
缺点:1.若结果要以 URL 参数传递,这就意味着在结果数据量很大的时候需要分割传递,巨烦。2.还有一个是 iframe 本身带来的,母页面和 iframe 本身的交互本身就有安全性限制。
-
script
优点:可以直接返回 json 格式的数据,方便处理
缺点:只接受 GET 请求方式
-
图片ping
优点:可以访问任何 url,一般用来进行点击追踪,做页面分析常用的方法
缺点:不能访问响应文本,只能监听是否响应
题 13:Cookie、sessionStorage、localStorage 的区别共同点:都是保存在浏览器端,并且是同源的
-
Cookie:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下,存储的大小很小只有4K左右。(key:可以在浏览器和服务器端来回传递,存储容量小,只有大约4K左右)
-
sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持,localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。(key:本身就是一个回话过程,关闭浏览器后消失,session为一个回话,当页面不同即使是同一页面打开两次,也被视为同一次回话)
-
localStorage:localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。(key:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效)
XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了减轻这些攻击,需要在HTTP头部配上,set-cookie:
-
httponly-这个属性可以防止XSS,它会禁止javascript脚本来访问cookie。
-
secure - 这个属性告诉浏览器仅在请求为https的时候发送cookie。
结果应该是这样的:Set-Cookie=...
题 15:浏览器和 Node 事件循环的区别?其中一个主要的区别在于浏览器的 event loop 和 nodejs 的 event loop 在处理异步事件的顺序是不同的,nodejs 中有 micro event;其中Promise属于micro event 该异步事件的处理顺序就和浏览器不同.nodejs V11.0以上 这两者之间的顺序就相同了。
题 16:简述 HTTPS 中间人攻击HTTPS 协议由 HTTP + SSL 协议构成,具体的链接过程可参考SSL或TLS握手的概述https://github.com/lvwxx/blog/issues/3
中间人攻击过程如下:
-
服务器向客户端发送公钥。
-
攻击者截获公钥,保留在自己手上。
-
然后攻击者自己生成一个【伪造的】公钥,发给客户端。
-
客户端收到伪造的公钥后,生成加密hash值发给服务器。
-
攻击者获得加密hash值,用自己的私钥解密获得真秘钥。
-
同时生成假的加密hash值,发给服务器。
-
服务器用私钥解密获得假秘钥。
-
服务器用加秘钥加密传输信息
防范方法:服务端在发送浏览器的公钥中加入 CA 证书,浏览器可以验证 CA 证书的有效性。
题 17:说几条 Web 前端优化策略(1) 减少 HTTP 请求数
(2) 从设计实现层面简化页面
(3) 合理设置 HTTP 缓存
(4) 资源合并与压缩
(5) CSS Sprites
(6) Inline Images
(7) Lazy Load Images
题 18:你了解的浏览器的重绘和回流导致的性能问题重绘(Repaint)和回流(Reflow)
重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。
-
重绘是当节点需要更改外观而不会影响布局的,比如改变
color
就叫称为重绘 -
回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。
所以以下几个动作可能会导致性能问题:
-
改变 window 大小
-
改变字体
-
添加或删除样式
-
文字改变
-
定位或者浮动
-
盒模型
很多人不知道的是,重绘和回流其实和 Event loop 有关。
-
当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
-
然后判断是否有
resize
或者scroll
,有的话会去触发事件,所以resize
和scroll
事件也是至少 16ms 才会触发一次,并且自带节流功能。 -
判断是否触发了 media query
-
更新动画并且发送事件
-
判断是否有全屏操作事件
-
执行
requestAnimationFrame
回调 -
执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 -
更新界面
-
以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调。
减少重绘和回流
-
使用
translate
替代top
.test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } setTimeout(() => { // 引起回流 document.querySelector('.test').style.top = '100px' }, 1000)
-
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)把 DOM 离线后修改,比如:先把 DOM 给
display:none
(有一次 Reflow),然后你修改100次,然后再把它显示出来不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) { // 获取 offsetTop 会导致回流,因为需要去获取正确的值 console.log(document.querySelector('.test').style.offsetTop) }
-
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
-
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
-
CSS 选择符从右往左匹配查找,避免 DOM 深度过深
-
将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于
video
标签,浏览器会自动将该节点变为图层。
题 19:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff函数中(建议先了解一下 diff 算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比
没有结果时,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(这里对应的是一个 key => index 的 map 映射)。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言,map 映射的速度更快。
题 20:React 中 setState 什么时候是同步的,什么时候是异步的?
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。
原因:在 React 的 setState 函数实现中,会根据一个变量isBatchingUpdates 判断是直接更新 this.state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。
题 21:为什么虚拟 dom 会提高性能?虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
具体实现步骤如下:
1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。
2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。
把 2 所记录的差异应用到步骤 1 所构建的真正的DOM树上,视图就更新了。
题 22:清除浮动的方式有哪些?比较好的是哪一种?
常用的一般为三种:
.clearfix
, clear:both
,overflow:hidden
。
比较好是 .clearfix
,伪元素万金油版本,后两者有局限性。
clear:both
:若是用在同一个容器内相邻元素上,那是贼好的,有时候在容器外就有些问题了, 比如相邻容器的包裹层元素塌陷
overflow:hidden
:这种若是用在同个容器内,可以形成 BFC
避免浮动造成的元素塌陷
题 23:分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景
结构:
display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击,
visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击 opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击
继承:
display: none和opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。 visibility: hidden:是继承属性,子孙节点消失由于继承了hidden,通过设置visibility: visible;可以让子孙节点显式。
性能:
displaynone : 修改元素会造成文档回流,读屏器不会读取display: none元素内容,性能消耗较大 visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容 opacity: 0 :修改元素会造成重绘,性能消耗较少
联系:它们都能让元素不可见
题 24:css sprite 是什么,有什么优缺点概念:将多个小图片拼接到一个图片中。通过 background-position 和元素尺寸调节需要显示的背景图案。
优点:
-
减少 HTTP 请求数,极大地提高页面加载速度
-
增加图片信息重复度,提高压缩比,减少图片大小
-
更换风格方便,只需在一张或几张图片上修改颜色或样式即可实现
缺点:
-
图片合并麻烦
-
维护麻烦,修改一个图片可能需要重新布局整个图片,样式
-
link
是 HTML 方式,@import
是 CSS 方式 -
link
最大限度支持并行下载,@import
过多嵌套导致串行下载,出现FOUC -
link
可以通过rel="alternate stylesheet"
指定候选样式 -
浏览器对
link
支持早于@import
,可以使用@import
对老浏览器隐藏样式 -
@import
必须在样式规则之前,可以在 css 文件中引用其他文件 -
总体来说:link 优于@import
-
容器元素闭合标签前添加额外元素并设置
clear: both
-
父元素触发块级格式化上下文(见块级可视化上下文部分)
-
设置容器元素伪元素进行清理推荐的清理浮动方法
题 27:display,float,position 的关系
-
如果
display
为 none,那么 position 和 float 都不起作用,这种情况下元素不产生框。 -
否则,如果 position 值为 absolute 或者 fixed,框就是绝对定位的,float 的计算值为 none,display 根据下面的表格进行调整。
-
否则,如果 float 不是 none,框是浮动的,display 根据下表进行调整。
-
否则,如果元素是根元素,display 根据下表进行调整。
-
其他情况下 display 的值为指定值 总结起来:绝对定位、浮动、根元素都需要调整display。
工厂模式:简单的工厂模式可以理解为解决多个相似的问题;
单例模式:只能被实例化(构造函数给实例添加属性与方法)一次;
沙箱模式:将一些函数放到自执行函数里面,但要用闭包暴露接口,用变量接收暴露的接口,再调用里面的值,否则无法使用里面的值;
发布者订阅模式:就例如如我们关注了某一个公众号,然后他对应的有新的消息就会给你推送,代码实现逻辑是用数组存储订阅者, 发布者回调函数里面通知的方式是遍历订阅者数组,并将发布者内容传入订阅者数组。
题 29:下面代码的输出是什么?const obj = { 1: "a", 2: "b", 3: "c" }; const set = new Set([1, 2, 3, 4, 5]); obj.hasOwnProperty("1"); obj.hasOwnProperty(1); set.has("1"); set.has(1);
true
true
false
true
所有对象键(不包括Symbols
)都会被存储为字符串,即使你没有给定字符串类型的键。这就是为什么obj.hasOwnProperty('1')
也返回true
。
上面的说法不适用于Set
。在我们的Set
中没有“1”
:set.has('1')
返回false
。它有数字类型1
,set.has(1)
返回true
。
// example 1 var a={}, b='123', c=123; a[b]='b'; a[c]='c'; console.log(a[b]); --------------------- // example 2 var a={}, b=Symbol('123'), c=Symbol('123'); a[b]='b'; a[c]='c'; console.log(a[b]); --------------------- // example 3 var a={}, b={key:'123'}, c={key:'456'}; a[b]='b'; a[c]='c'; console.log(a[b]);
这题考察的是对象的键名的转换。
-
对象的键名只能是字符串和 Symbol 类型。
-
其他类型的键名会被转换成字符串类型。
-
对象转字符串默认会调用 toString 方法。
// example 1 var a={}, b='123', c=123; a[b]='b'; // c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。 a[c]='c'; // 输出 c console.log(a[b]); // example 2 var a={}, b=Symbol('123'), c=Symbol('123'); // b 是 Symbol 类型,不需要转换。 a[b]='b'; // c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。 a[c]='c'; // 输出 b console.log(a[b]); // example 3