前端高频面试题

一、计算机网络


1. 接口对接时有哪些常见HTTP状态码,如何处理?

  • 1xx(临时响应)
    表示临时响应并需要请求者继续执行操作的状态代码
  • 2xx (成功)
    表示成功处理了请求的状态码。
    常见的2开头的状态码有:200 – 服务器成功返回网页
  • 3xx (重定向)
    表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向
    常见的3字开头的状态码有:
    • 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应时,会自动将请求者转到新位置。
    • 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
    • 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
  • 4xx(请求错误) 这些状态代码表示请求可能出错,妨碍了服务器的处理。
    常见的4字开头的状态有:404 – 请求的网页不存在
  • 5xx(服务器错误)
    这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。
    常见的以5开头的状态码有:
    • 500 (服务器内部错误) 服务器遇到错误,无法完成请求。
    • 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。

与后台对接常见的状态码有:200、400、404、405、500

200代表请求成功,但请求成功不代表能拿到正确的数据,一般返回的数据里面会带有一个子状态码,当子状态码不为0的时候,说明请求的参数可能有问题,需要查看msg来获知错误原因。

400代表请求参数有问题,需要跟后台对下参数

404很可能是URL写错了,所以请求不到,也有可能是服务器的nginx解析规则没设置好,亦或者没有www文件夹的访问权限。

405一般是get/post的请求类型不一致,接口要求的是post类型,却用get请求数据

500则是服务器错误,需要让后台看看哪里出了问题。

2. 网络url输入到输出都做了什么?

  1. 输入网址www.baidu.com
  2. 查询缓存
  • 浏览器查找自身缓存,如果有域名的IP地址则返回,没有则继续查找
  • 系统查找自身缓存,即查找Hosts文件中是否有记录IP……
  • 路由器查找自身缓存……
  1. 获取IP
  • 本地域名服务器采用迭代查询,先向一个根域名服务器进行查询
  • 根域名服务器告诉本地域名服务器,下一次查询的顶级域名服务器dns.com的IP地址
  • 本地域名服务器向顶级域名服务器dns.com进行查询
  • 顶级域名服务器dns.com告诉本地域名服务器,下一次查询的权限域名服务baidu.dns.com的IP地址
  • 本地域名服务器向权限域名服务器baidu.dns.com进行查询
  • 权限域名服务器告诉本地域名服务器,所查询的主机www.baidu.com的IP地址
  • 本地域名服务器最后把IP地址告诉主机
  1. TCP三次握手
  • 浏览器所在客户机向服务器发出连接请求报文(SYN=1)
  • 服务器接收报文之后,同意建立连接,向客户机发送确认报文(SYN=1,ACK=1)
  • 客户机接收到确认报文之后,再次向服务器发出报文,确认已接收到报文(ACK=1)
  • 此时客户机与服务器之间的TCP连接已建立完成,开始通信
  1. 发送HTTP请求,获取资源
  • 浏览器发出取文件命令:GET
  • 服务器给出响应,将指定文件发送给浏览器
  • 浏览器释放TCP连接
  • 浏览器向服务器发出连接释放报文,然后停止发送数据
  • 服务器接收到释放报文后,向客户机发送确认报文,然后将未传送完的数据发送完
  • 服务器数据传输完毕之后,向客户机发送连接释放报文
  • 客户机接收到报文后,发出确认,然后等待一段时间,释放连接
  1. 浏览器解析资源并显示到窗口中
  • 解析HTML生成DOM树
  • 解析CSS生成CSSOM规则树
  • 将DOM树跟CSSOM规则树合并在一起生成渲染树
  • 遍历渲染树开始布局,计算每个节点的位置和大小
  • 将渲染树每个节点绘制到屏幕上

3. TCP为什么需要三次握手?四次挥手?

1) 三次握手
https跟TCP一样首先是有三次握手四次挥手,只有经过三次握手才能确认双发的收发功能都正常,缺一不可:

第一次握手(客户端发送 SYN 报文给服务器,服务器接收该报文):
客户端什么都不能确认;服务器确认了对方发送正常,自己接收正常

第二次握手(服务器响应 SYN 报文给客户端,客户端接收该报文):
客户端确认了:自己发送、接收正常,对方发送、接收正常;
服务器确认了:对方发送正常,自己接收正常

第三次握手(客户端发送 ACK 报文给服务器):
客户端确认了:自己发送、接收正常,对方发送、接收正常;
服务器确认了:自己发送、接收正常,对方发送、接收正常

2) 四次挥手
四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 IN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。此时客户端处于 FIN_WAIT1 状态,等待服务端的确认。
    FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;

  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
    CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
    此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待 2)状态,等待服务端发出的连接释放报文段。
    FIN-WAIT-2 - 从远程TCP等待连接中断请求;

  • 第三次挥手:如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,等待客户端的确认。
    LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;

  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值(seq=u+1),此时客户端处于 TIME_WAIT (时间等待)状态。
    TIME-WAIT - 等待足够的时间以确保远程TCP接收到连接中断请求的确认;

注意 !!!这个时候由服务端到客户端的 TCP 连接并未释放掉,需要经过时间等待计时器设置的时间 2MSL(一个报文的来回时间) 后才会进入 CLOSED 状态(这样做的目的是确保服务端收到自己的 ACK 报文。如果服务端在规定时间内没有收到客户端发来的 ACK 报文的话,服务端会重新发送 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文给服务端)。服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。

为什么要四次挥手
由于 TCP 的半关闭(half-close)特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。

通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。

举个例子:A 和 B 打电话,通话即将结束后,A 说 “我没啥要说的了”,B 回答 “我知道了”,于是 A 向 B 的连接释放了。但是 B 可能还会有要说的话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,于是 B 向 A 的连接释放了,这样整个通话就结束了。

4. 什么是https?https握手的流程是怎样的?https的原理是什么?https如何保证安全?非对成加密和对称加密?https是否完全安全?

简单来说, https 是 http + ssl,对 http 通信内容进行加密,是HTTP的安全版,是使用TLS/SSL加密的HTTP协议

1)流程
第一步 tcp三次握手

第二步:客户端发送client_hello
包含以下内容
a. 包含TLS版本信息
b. 随机数(用于后续的密钥协商)random_C
c. 加密套件候选列表
d. 压缩算法候选列表
e. 扩展字段
f. 其他

第三步:服务端发送server_hello
解释说明:服务端收到客户端的client_hello之后,发送server_hello,并返回协商的信息结果
a. 选择使用的TLS协议版本 version
b. 选择的加密套件 cipher suite
c. 选择的压缩算法 compression method
d. 随机数 random_S
e. 其他

第四步:服务端发送证书
解释说明:服务端发送完server_hello后,紧接着开始发送自己的证书(不清楚证书是什么的,可以移步到上一篇文章),从图可知:因包含证书的报文长度是3761,所以此报文在tcp这块做了分段,分了3个报文把证书发送完了

第五步:服务端发送Server Key Exchange
解释说明:对于使用DHE/ECDHE非对称密钥协商算法的SSL握手,将发送该类型握手。RSA算法不会进行该握手流程(DH、ECDH也不会发送server key exchange),也就是说此报文不一定要发送,视加密算法而定。

第六步:服务端发送Server Hello Done
解释说明:通知客户端 server_hello 信息发送结束

第七步:客户端发送.client_key_exchange+change_cipher_spec+encrypted_handshake_message
解释说明:
a. client_key_exchange,合法性验证通过之后,向服务器发送自己的公钥参数,这里客户端实际上已经计算出了密钥
b. change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信
c. encrypted_handshake_message,主要是用来测试密钥的有效性和一致性

第八步:服务端发送New Session Ticket
解释说明:服务器给客户端一个会话,用处就是在一段时间之内(超时时间到来之前),双方都以协商的密钥进行通信。

第九步:服务端发送change_cipher_spec
解释说明:服务端解密客户端发送的参数,然后按照同样的算法计算出协商密钥,并通过客户端发送的encrypted_handshake_message验证有效性,验证通过,发送该报文,告知客户端,以后可以拿协商的密钥来通信了

第十步:服务端发送encrypted_handshake_message
解释说明:目的同样是测试密钥的有效性,客户端发送该报文是为了验证服务端能正常解密,客户端能正常加密,相反:服务端发送该报文是为了验证客户端能正常解密,服务端能正常加密

第十一步:完成密钥协商,开始发送数据
解释说明:数据同样是分段发送的

第十二步:完成数据发送,4次tcp挥手
解释说明:红框的意思是:客户端或服务器发送的,意味着加密通信因为某些原因需要中断,警告对方不要再发送敏感的数据,服务端数据发送完成也会有此数据包,可不关注

  1. 原理
    https在传输之前需要客户端和服务器进行一次握手,在握手过程中确立双方加密传输数据的密码信息,TLS/SSL协议是一套加密传输协议,使用非对称加密,对称加密,以及HASH算法

3)对称加密与非对称加密
对称加密:通信双方使用相同的密钥进行加密。特点是加密速度快,但是缺点如果密钥泄露的话,那么加密就会被别人破解。常见的对称加密有AES,DES算法。
非对称加密:它需要生成两个密钥:公钥(Public Key)和私钥(Private Key)。公钥负责加密,私钥负责解密;或者,私钥负责加密,公钥负责解密。这种加密算法安全性更高,但是计算量相比对称加密大很多,加密和解密都很慢。常见的非对称算法有RSA。

https 加密是采用对称加密和非对称机密一起结合的。
在证书验证阶段,使用非对称加密。 在数据传输阶段,使用对称机密。
非对成加密虽然安全,但是加密效率较慢,采用对称非对称结合可以最大效率兼顾安全和效率。

  1. https有可能受到中间人攻击
    中间人攻击是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。
    HTTPS协议设计的过程中,考虑到了这种情况,所以对于获取的公钥的过程做了一个验证,就是证书验证机制。
    证书颁发机构,本质上是一个收钱的机构(狗头),收钱,然后颁发一个可信的证书给网站,同时,操作系统厂商(Windows、Linux、MacOS)信任“证书颁发机构”颁发的证书,在浏览器或者操作系统里默认集成了这些机构的根证书,这样就保证了证书是可信可靠的。每个证书都是有有效期的,如果有网站恶意使用了证书,那么“证书颁发机构”就会吊销对应的证书,之后浏览器再访问这些网站的时候,如果网站证书校验不通过,那么浏览器地址栏里的小锁头就不见了,取而代之的是一个浏览警告

5. 介绍一下websocket,如何使用,使用场景,优缺点?如何保证安全?


websocket是一种网络通信协议,是HTML5提供的一种在单个TCP连接上进行的全双工通信协议。而http是一种无状态、无连接、单向的应用层协议,在http协议下,服务器不能主动向客户端推送数据。

目前大部分浏览器都内置了WebSocket组件,直接使用就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

var ws = new WebSocket(\'ws://localhost:9998/echo\')

ws.onopen = function() {

ws.send(\'发送数据\')

}

// 接收消息

ws.onmessage = function(evt) {

var receive_msg = evt.data

}

// 断开连接

ws.onclose = function() {}

比较实用的场景就是在做数字大屏的时候,后台会监听数据库的数据变化,一旦数据改变了,后台就会通过websocket将新数据推送给前台,实现数据的实时更新。

缺点:

  • 服务器维护长连接需要一定成本,一般不会用在访问量的功能上面。
  • 容易受网络波动影响,需要处理断线重连。

安全:
可以使用wss来保证数据安全,即ws + TLS,其原理跟https类似。


二、浏览器


1. 什么是跨域,有几种解决方案?原理是什么?

浏览器处于安全的考虑,禁止与不同源的URL进行交互,这时就产生了跨域问题。

解决方案:

  1. 使用同源策略,即相同的协议、域名和端口

  2. 使用vue-cli自带的proxy功能
    这个方案只有在本地才能使用,因为它是将跨域请求转发给本地的node服务器,让node服务器帮忙将请求发出去。
    而这个node服务器是通过vue-cli建立起来的,我们并不能让用户在访问网页的时候在本地创建一台node服务器出来。
    常规配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 访问/api/xxx时,会被转发到http://localhost:3000/xxx下面
changeOrigin: true, // 本地就会虚拟一个服务器接收你的请求并代你发送该请求,解决跨域问题
}
}
}
};
  1. 使用带有src或href的标签,如script、img、link

  2. 实用jsonp
    jsonp实现原理:
    主要是利用标签没有跨域限制的漏洞,动态创建script标签请求后端接口地址,然后传递?callback=now参数,后端接收callback,后端经过数据处理,返回callback函数调用的形式,callback中的参数就是json
    jsonp的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var param = new Object();
    $.ajax({
    url: "**、",
    type: "POST",
    dataType: "jsonp", //json不支持跨域请求,只能使用jsonp
    data: param ,
    jsonp: "callback", //传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名,默认为callback
    jsonpCallback: "userHandler", //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
    success: function (data) {
    console.info(data);
    },
    error: function () {
    console.info("请求超时错误!");
    }
    })

jsonp的缺点:

  • 仅支持get方法,不支持post
  • 容易遭到XSS攻击
  1. 使用CORS
    全程Cross-Origin Resource Sharing,即跨域资源共享。是一种比jsonp更强大的跨域方案,可以发送几乎所有类型的请求。

这种方案首先需要前台在请求头中添加:

1
Orgin: 'http://xxx.com'

接着需要后台工程师在响应头之中添加来允许跨域:

1
2
'Access-Control-Allow-Origin': 'http://xxx.com'  // 如果值是*,则允许任何客户端的跨域请求
'Access-Control-Allow-Method': 'OPTION, PUT, GET, POST, DELETE' // 允许的请求类型

如果需要检验权限,那就需要带上Cookie,这个时候前端就需要在发送请求时将以下属性置为true:

1
xhr.withCredentials = true

最后在后台需要允许接受Cookie:

1
'Access-Control-Allow-Credentials': true
  1. 通过nginx实现
    访问同源下的某个路径,如/getCat
    在nginx配置文件中利用proxy_pass实现如下代码:
1
2
3
4
location /getCat { #添加访问目录为/getCat
rewrite ^/getCat/(.*)$ /$1 break;
proxy_pass http://csdn.b.com/cat/;
}

2. web安全问题有哪些?如何防御?

  • 接口安全
    指前后端联调的部分,也是最有可能出问题的地方

防御手段:

  • Token授权认证
    Token是一串随机生成的32位字符串码,一般可以利用UUID来生成。
    每次发送请求的时候都必须带上这个Token进行校验。
    Token要设置过期时间,过期后需要重新进行登录操作才能生成新的Token
    如果勾选了自动登录,那么当Token过期时,后台会比对用户的sessionID,直接生成一个新Token

  • 时间戳超过机制
    用户每次请求都会带上一个时间戳,如果用户时间戳与服务器时间戳超过一分钟,那就认为这是超时请求,直接拒绝。

  • URL签名
    这个在对接支付宝、微信支付的时候很常见。一般文档约定好所需参数和加密算法,后端根据文档要求生成对应的sign,传给接口,与接口自己生成sign进行比对。
    加签流程:
    首先约定加密算法,一般是RSA算法或MD5算法。
    接着获得当前应用的私钥。
    再然后将请求的参数按字母顺序进行排序,并以键值对的形式用&连接起来。
    最后使用加密算法生成sign,在请求参数中带上sign即可

  • 防重放
    这是为了防止攻击者使用相同的URL、参数再次访问接口
    主要判定点有三个:
    1)Token是有效的
    2)timestamp未超时
    3)缓存服务器中不存在相同的sign

  • 使用HTTPS
    保证传输的数据无法被截获分析。

  • xss:跨站脚本攻击。
    恶意攻击者借助输入框,往Web页面中插入恶意Script代码,当用户浏览该网页之时,嵌入Web里面的恶意代码会被执行,从而达到恶意攻击用户的目的。
    由于script标签中的src是不受跨域策略限制的,因此叫做跨站脚本攻击
    xss攻击的主要目的是获取目标攻击网站的Cookie,因为有了Cookie相当于session。
    避免措施:

    • 在提交表单的时候,过滤掉<script>、<a>、<img>这类标签
    • 将文本内容转为html实体编码
  • CSRF:跨站点伪装请求。
    举个例子,用户登录了某个银行的网站进行了转账操作。
    这个时候骗子想办法得知到该用户登录过目标银行的信息,然后就会仿造一个目标银行的网站,在网站中隐藏了目标银行转账的跨域请求。
    当用户被诱使打开网站的时候,就会将自身的Cookie发送过去,从而完成转账给骗子的操作。

防御手段:

  • 判断Origin和Refer(不推荐)
    Origin在302重定向的时候会被去掉
    Refer攻击者可以伪造

  • 使用CSRF Token
    服务器会给客户端分配一个token,这个token一般是随机字符串跟时间戳的加密,为了不被冒用,就存在了服务器的Session中。(另一种方案是保存到localStorage之中,用到时从localStorage取出来加到请求参数中,这个就跟双重Cookie验证类似了)
    用户在加载页面的时候,每个表单都会被插入这个token,这样请求的时候就会附带被验证到。
    问题:本方法并不适用于动态创建的标签,并且每个表单都要插入,很麻烦

  • 双重Cookie验证
    服务器给用户生成一个随机生成的csrfcookie,保存到用户的Cookie中
    在用户请求表单的时候,将csrfcookie单拎出来作为一个Get请求的参数发给服务器(CSRF攻击者只能转发Cookie,不能获取Cookie的内容)
    服务器验证该参数
    问题:如果被XSS攻击获取到Cookie,那就没用了。另外如果子域名有漏洞,被攻破后攻击者是能取用Cookie的。

  • 加验证码

  • 每次敏感操作都要求输入密码

  • SQL注入攻击
    与XSS攻击类似,攻击者将SQL命令插入到Web表单提交、输入域名、页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令

防御手段:

  • 使用参数化查询
    目前最有效的防御手段,原理就是在需要填入数值的地方都使用参数来代替,任何直接传值的操作都会被拒绝。

    1
    2
    3
    4
    5
    SqlCommand sqlcmd = new SqlCommand("INSERT INTO myTable (c1, c2, c3, c4) VALUES (@c1, @c2, @c3, @c4)", sqlconn);
    sqlcmd.Parameters.AddWithValue("@c1", 1); // 设定参数 @c1 的值。
    sqlcmd.Parameters.AddWithValue("@c2", 2); // 设定参数 @c2 的值。
    sqlcmd.Parameters.AddWithValue("@c3", 3); // 设定参数 @c3 的值。
    sqlcmd.Parameters.AddWithValue("@c4", 4); // 设定参数 @c4 的值。
  • 严格限制用户权限

  • 不允许用户输入特殊符号

3. 什么是HTTP缓存?有几种缓存方式?浏览器强制缓存和协商缓存是什么?有哪些参数?怎么用?什么时候用?如何取消缓存?

HTTP缓存指的是访问过一次网站之后,浏览器会将相关资源缓存到本地,下次访问会优先从本地获取资源,避免短时间多次访问服务器,给服务器造成压力。
HTTP缓存有两种方式:强制缓存和协商缓存

  • 强制缓存
    浏览器第一次请求资源后缓存到本地,下次请求时浏览器判断本地缓存资源是否过期,如果未过期就返回缓存数据,否则请求服务器获取新资源。
    利用http返回头中的Expires和Cache-Control两个字段来控制,用来表示资源的缓存时间

通过HTTP/1.0的Expire和HTTP/1.1的Cache-Control字段实现。

  • Expire是通过绝对时间来判断缓存失效的,所以当服务器和浏览器所在时区不同时,校验结果是不准确的;

  • Cache-Control使用max-age相对时间,相对的是文档第一次被请求时服务器记录的Request_time(请求时间),解决了Expire存在的问题。
    Cache-Control还可以实现更小粒度的缓存控制:

    • public:客户端和代理服务器都可以缓存。
    • private:只允许浏览器缓存,代理服务器不能缓存。
    • no-cache:不允许使用强制缓存,但可以使用协商缓存。在浏览器使用缓存前,会往返对比 ETag,如果 ETag 没变,返回 304,则使用缓存; 使用no-cache的目的就是为了防止从缓存中获取过期的资源
    • no-store:不允许使用缓存。
    • s-maxage:类似max-age,指定代理服务器缓存过期时长。
  • 协商缓存:
    浏览器第一次请求服务器,服务器会在响应头字段加上缓存tag,后续请求中如果未命中强缓存(过期或强制不使用),浏览器就会带上缓存tag请求服务器,服务器会根据这个缓存tag来决定资源是否更新。如果资源未更新则返回304,浏览器可以直接使用本地缓存;否则返回200和新资源。

    缓存tag分两种:

    • 资源文件更新时间:服务器返回时设置Last-Modified,浏览器请求时设置If-Modified-Since。
    • 资源唯一标识:服务器返回时设置ETag头字段,浏览器请求时设置If-None-Match头字段。

    Last-Modified VS ETag :

    • 精度上ETag优于Last-Modified:
      1) 编辑了资源文件,Last-Modified更新了,但是文件内容并没有更改,会造成缓存失效;
      2) Last-Modified的时间精度是秒,如果文件在 1 秒内改变了多次,此时 Last-Modified 并没有体现出修改了。
    • 性能:Last-Modified只需要记录一个时间点,而 Etag 需要根据文件的具体内容计算哈希值。
    • 优先级:两者同时存在时,服务器会优先选择ETag。
  • 用法:

    • 在meta标签中设置

      1
      2
      <meta http-equiv="Expires" content="Mon, 20 Jul 2013 23:00:00 GMT" />
      <meta http-equiv="Cache-Control" content="max-age=7200" />
    • 在nginx中设置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      # 禁止html使用强缓存
      location ~.*\.html$
      {
      # 禁止使用强缓存(不一定就会协商缓存)
      add_header Cache-Control no-cache;
      # 作用跟Cache-Control no-cache一致;兼容HTTP/1.0
      add_header Pragma no-cache;
      }
      # 对于更新不频繁的,或者更新后文件名改变的(加上版本号),可以强缓存
      location ~.*\.(js|css|png|jpg)$
      {
      expires 365d;
      }
      # 对于更新相对频繁且比较重要的脚本,可以把强缓存有效时间控制在一天内,强缓存失效后进行协商缓存。
      location ~.*consult\.js$
      {
      expires 86400s;
      }
    • 在node服务器中设置
      一般是添加到请求头即可

  • 设置策略:

    • html部分的特点是:内容重要并且更新频繁,所以适合协商缓存,但对于并发量大的页面,也可以使用有效期较短的强缓存
    • 而js、css和图片等静态文件,一般变更少,而且内容更新后可以重新命名文件进行刷新,适合强缓存。
  • 刷新缓存:

    • 手动刷新:快捷键F5,仅对协商缓存有效
    • 强制刷新:快捷键Ctrl+F5(Cmd+F5),对强制缓存和协商缓存都有效

4. Cookies,localStorage,sessionStorage和token之间的区别?什么是sessionID?什么是服务端session?实际场景中如何使用?token的过期时间如何处理?token的实效性验证?发送请求时,如何确保登录态?什么是JWT?

Cookie每次请求都会被自动发给服务端,在过期时间之前都有效,即使关闭窗口或浏览器
缺点:
数据不得超过4k
没有对应的API,只能通过document.cookie获取到字符串,想要拿到每个键值对需要手动封装方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function setCookie(cname, cvalue, exdays) {

let d = new Date()

d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000))

let expires = "expires=" + d.toGMTString()

document.cookie = cname + "=" + cvalue + ";" + expires

}

function getCookie(cname) {

let name = cname + "="

let ca = document.cookie.split(";")

for (let i = 0; i < ca.length; i ++) {

var c = ca[i].trim()

if (c.indexOf(name) === 0) return c.substring(name.length, c.length)

}

return ""

}

WebStorage指本地存储,包括localStorage和sessionStorage
其中sessionStorage仅在窗口开启时有效
localStorage则长期保存在本地,最高可以达到5M
这类存储的优点是存储空间大,有对应API(getItem、setItem、removeItem、clear)
缺点是需要手动发给服务器

关于Cookie和WebStorage的区别:

关于服务端session跟sessionID:

  1. 当浏览器首次访问服务器时,服务器会为客户端创建一个session(每个用户独有的房间,用来存放这个用户的相关信息和内容),并通过特殊算法算出一个sessionID(类似于双方都知道的唯一暗号),用来标识sesion对象。
  2. 当浏览器再次(session还在有效时间内)向服务器请求资源时,浏览器将sessionID和请求内容一起发送到服务端。服务端通过对比自身存储的sessionID来判断用户之前是否存在,并返回对应的内容给不同用户
  3. 因为标识符存在内存里,所以当浏览器关闭时,浏览器保存的sessionID就会消失,服务器将匹配失败,默认该用户为新用户

关于Token:
1) token也可以称做令牌,一般由 uid+time+sign(签名)+[固定参数] 组成,然后用md5加密

1
2
3
uid: 用户唯一身份标识
time: 当前时间的时间戳
sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库

2) token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中
3) token 的认证流程

1
2
3
用户登录,成功后服务器返回Token给客户端。
客户端收到数据后保存在客户端
客户端再次访问服务器,将token放入headers中 或者每次的请求 参数中 服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

4) token可以抵抗csrf,cookie+session不行
5) session时有状态的,一般存于服务器内存或硬盘中,当服务器采用分布式或集群时,session就会面对负载均衡问题。负载均衡多服务器的情况,不好确认当前用户是否登录,因为多服务器不共享 session
6) 客户端登陆传递信息给服务端,服务端收到后把用户信息加密(token)传给客户端,客户端将token存放于localStroage等容器中。客户端每次访问都传递token,服务端解密token,就知道这 个用户是谁了。通过cpu加解密,服务端就不需要存储session占用存储空间,就很好的解决负载 均衡多服务器的问题了。这个方法叫做JWT(Json Web Token)

关于JWT:
JWT即Json Web Token,它设计的初衷是为了验证用户身份,并附带防篡改功能。它由三个部分组成:header.payload.signature。
其中header存放加密算法的类型和token类型。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

payload用于存放用户信息

1
2
3
4
5
6
7
8
9
10
{
// 表示 jwt 创建时间
iat: 1532135735,

// 表示 jwt 过期时间
exp: 1532136735,

// 用户 id,用以通信
user_id: 10086
}

前两者最终都会转换成Base64编码
而signature则是通过指定的加密算法加上秘钥生成的。

这个JWT是由后台生成后发送给前台,前台保存在本地。当需要验证用户身份时,前台就会从本地找到JWT,通过URL参数或者在请求头中添加Authoration字段来实现数据传递。


三、HTML


1. HTML5的特性有哪些?语义化的做法有哪些?具体指什么?

1) 语义标签

标签 描述
<header> 文档头部
<footer> 文档脚部
<nav> 文档导航
<setion> 文档章节
<article> 文章节点
<aside> 侧边栏

语义化的好处:

  • 更好地呈现内容结构,让源码具有可读性
  • 利于SEO

2) 视频和音频
html5提供了音频和视频文件的标准,既使用<audio>元素。

音频:

1
2
3
4
5
<audio controls>    //controls属性提供添加播放、暂停和音量控件。
<source src="horse.ogg" type="audio/ogg">
<source src="horse.mp3" type="audio/mpeg">
您的浏览器不支持 audio 元素。 //浏览器不支持时显示文字
</audio>

视频:

1
2
3
4
5
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
您的浏览器不支持Video标签。
</video>

3) 拖放API
拖放是一种常见的特性,即捉取对象以后拖到另一个位置。
在html5中,拖放是标准的一部分,任何元素都能够拖放。

1
<div draggable="true"></div>

当元素拖动时,我们可以检查其拖动的数据。

1
2
3
4
5
6
<div draggable="true" ondragstart="drag(event)"></div>
<script>
function drap(ev){
console.log(ev);
}
</script>
拖动生命周期 属性名 描述
拖动开始 ondragstart 在拖动操作开始时执行脚本
拖动过程中 ondrag 只要脚本在被拖动就运行脚本
拖动过程中 ondragenter 当元素被拖动到一个合法的防止目标时,执行脚本
拖动过程中 ondragover 只要元素正在合法的防止目标上拖动时,就执行脚本
拖动过程中 ondragleave 当元素离开合法的防止目标时
拖动结束 ondrop 将被拖动元素放在目标元素内时运行脚本
拖动结束 ondragend 在拖动操作结束时运行脚本

4) WebWorker
传统的js是一个单线程的模型,而像Promise、Async/Await这些异步函数实际上是通过Event Loop(事件循环)来实现非阻塞的功能,但实际上它们还是单线程机制下的一个同步函数。
而Web Worker就是用来解决单线程这一问题,它可以通过加载一个脚本文件,进而创建一个独立工作的线程,在JS引擎主线程之外运行。
基本使用:
Web Worker的基本原理就是在当前javascript的主线程中,使用Worker类加载一个javascript文件来开辟一个新的线程,
起到互不阻塞执行的效果,并且提供主线程和新县城之间数据交换的接口:postMessage、onmessage。

javascript:

1
2
3
4
//worker.js
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);

html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script type="text/javascript">
//WEB页主线程
var worker =new Worker("worker.js"); //创建一个Worker对象并向它传递将在新线程中执行的脚本的URL
worker.postMessage("hello world"); //向worker发送数据
worker.onmessage =function(evt){ //接收worker传过来的数据函数
console.log(evt.data); //输出worker发送来的数据
}
</script>
</head>
<body></body>
</html>

限制:

  • 必须是同源
  • 无法读取主线程的document、window对象,但可以读取浏览器的navigator、location
  • 与主线程不能直接通信,必须通过消息完成
  • 不能读取本地文件

5) WebStorage
WebStorage是HTML新增的本地存储解决方案之一,但并不是取代cookie而指定的标准,cookie作为HTTP协议的一部分用来处理客户端和服务器的通信是不可或缺的,session正式依赖与实现的客户端状态保持。WebSorage的意图在于解决本来不应该cookie做,却不得不用cookie的本地存储。
websorage拥有5M的存储容量,而cookie却只有4K,这是完全不能比的。
客户端存储数据有两个对象,其用法基本是一致。
localStorage:没有时间限制的数据存储
sessionStorage:在浏览器关闭的时候就会清除。

1
2
3
4
5
localStorage.setItem(key,value);//保存数据
let value = localStorage.getItem(key);//读取数据
localStorage.removeItem(key);//删除单个数据
localStorage.clear();//删除所有数据
let key = localStorage.key(index);//得到某个索引的值

6) WebSocket
WebSocket协议为web应用程序客户端和服务端之间提供了一种全双工通信机制。
特点:
(1)握手阶段采用HTTP协议,默认端口是80和443
(2)建立在TCP协议基础之上,和http协议同属于应用层
(3)可以发送文本,也可以发送二进制数据。
(4)没有同源限制,客户端可以与任意服务器通信。
(5)协议标识符是ws(如果加密,为wss),如ws://localhost:8023

7) 增强型表单
8) 地理定位
9) SVG绘图
10) Canvas绘图

2. Viewport是什么?有什么用?怎么用?什么是vh、vw?

Viewport是移动端的一个特有的概念,指的是网页的窗口。
它一共分为三种:layout viewport、visual viewport和ideal viewport

layout viewport代表浏览器页面的整体窗口,这个窗口是为了让桌面上面页面在移动端上浏览时,不至于因为手机屏幕的窄距宽度,导致布局都挤在一起,让页面看起来很混乱。这个viewport通常会比手机屏幕要宽,大部分都是980px。
这个窗口宽度值可通过document.documentElement.clientWidth来访问

visual viewport是浏览器页面在手机屏幕上的可视区域,这个窗口的宽度可以通过window.innerWidth访问

最后是ideal viewport,这是一个理想窗口,当我们把layout viewport完美适配到visual viewport的时候,就是ideal viewport。这样的窗口不需要用户缩放或使用横向滚动条就能看到完整的页面内容。
这个窗口的宽度可以通过window.screen.width访问
想要达到这个效果,就需要将width设置为设备的屏幕宽度,即device-width,具体如下:

1
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />

说白了就是将页面的宽度设置成设备的宽度,这个时候页面的内容是等比例缩小的。

但要实现页面自适应这还远远不够,需要搭配vw跟vh使用。
vw和vh指的是将屏幕横竖各分成100等份,每一份代表1vw和1vh。
通过使用vw和vh单位即可实现页面元素的自适应布局。

但是浏览器对vw和vh的支持性不如rem,所以实际生产环境中使用rem来做自适应方案比较多。

3. get和post请求有什么区别?

get
一般用来查询数据

  • 优点
    请求头较小
  • 缺点
    请求参数的字符长度有限制,一般在2000个字符

post
一般用来添加数据

  • 优点
    参数字符长度无限制
    可以发送带有特殊字符的参数
    更安全,请求参数不会被缓存
  • 缺点
    请求头较大,传输速度较慢

4. 什么是ajax?如何使用ajax?如何使用Promise封装ajax?如何缓存?原生、vue、小程序中怎么用?

交互流程:
1)创建XHR对象即XMLHttpRequest()
2)open准备发送,open中有三个参数:一是提交方式get和post,二是接口地址,三是同步和异步
3)用send发送
4)发送的过程中通过onreadystatechange来监听接收的回调函数,可以通过判断readyState==4和status==200来判断是否成功返回,然后通过responseText接收成功返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
function ajax(optionsOverride) {

var options = {}

// ajaxOptions为ajax的默认配置

for (var k in ajaxOptions) {

options[k] = optionsOverride[k] || ajaxOptions[k]

}

options.async = !options.async ? false: true

var xhr = options.xhr || new XMLHttpRequest()

return new Promise((resolve, reject) => {

xhr.open(options.method, options.url, options.async)

xhr.timeout = options.timeout

for (var k in options.headers) {

xhr.setRequestHeader(k, options.headers[k])

}

xhr.onprogress = options.onprogress

xhr.upload.progress = options.onuploadprogress

xhr.responseType = options.dataType

xhr.onabort = function() {

reject(new Error({

errorType: 'abort_error',

xhr: xhr

}))

}

xhr.ontimeout = function() {

reject(new Error({

errorType: 'timeout_error',

xhr: xhr

}))

}

xhr.onerror = function() {

reject(new Error({

errorType: 'onerror',

xhr: xhr

}))

}

xhr.onloadend = function() {

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
return resolve(xhr)

reject(new Error({

errorType: 'status_error',

xhr: xhr

}))

}

try {

xhr.send(options.data)

}

catch(e) {

reject(new Error({

errorType: 'send_error',

error: e

}))

}

})

}

缓存:
答:通过在文件名后面添加随机数(也称为文件指纹)来实现,主要原理是浏览器对访问过的文件,首先会检测第二次请求的文件url在浏览器是否缓存过,如果缓存过就使用,否则如果是一个新的文件url,则从服务器重新请求

javaScript原生Ajax:用的是XMLHttpRequest对象
jQuery中的Ajax: $.ajax(),$.getJSON(),$.get(),$.post()等
vue中的Ajax:vue-resource(vue1.x中用),axios(主流)
微信小程序Ajax:用的是小程序内置的wx.request()写法和jquery的$.ajax()类似,参数url,success,data,method,fail等

5. 浏览器有几个进程?分别是干什么用的?JS有哪些引擎?异步函数的原理是什么?

浏览器主要包括 浏览器主进程、渲染进程、网络进程、GPU进程、第三方插件进程。
浏览器主进程主要控制URL地址栏、书签栏、前进后退按钮,并提供存储功能
渲染进程负责界面渲染、脚本执行、事件处理,默认每个Tab页面都是一个渲染进程
网络进程负责处理处理网络请求的,原来是在浏览器主进程里面的,现在独立出来了。
GPU进程负责3D绘制

其中渲染进程又分为GUI线程、JS引擎线程、事件触发线程、定时器线程、异步http线程
GUI线程负责当前Tab页面界面渲染,它会去解析HTML和CSS,构建DOM树,布局和绘制。当界面需要重绘或重排的时候,就会用到该线程。由于JS脚本有可能操作到DOM树,为了避免冲突,当JS引擎线程执行的时候,GUI线程通常都会被挂起,处于冻结状态
JS引擎线程负责处理JS脚本,通常所说的主线程就是JS引擎线程。Chrome所用的JS引擎叫做V8引擎。由于多线程操作UI会导致UI混乱,所以JS引擎线程在设计之初就被定位单线程。
定时器线程主要用来处理setTimeout和setInterval之类的定时器,由于JS引擎线程是单线程的,无法处理这种异步的操作,所以需要单独开一个线程。
事件触发线程主要用来控制事件循环(Event Loop),当事件监听、定时器以及像promise这类异步操作触发的时候,该操作会被加入到事件队列里面。当主线程处理完同步任务之后,就会开始处理这些异步任务。

四、JS

1. js的基本类型有哪些?引用类型有哪些?null和undefined的区别?引用类型跟基本数据类型有什么区别?堆栈关系了解吗?

  1. 基本数据类型
  • undefined

  • null

  • boolean

  • number

  • string

  1. 引用数据类型
  • function

  • object

  • array

  1. null和undefined的区别
  • undefined表示变量声明但未初始化时的值
  • null表示准备用来保存对象,还没有真正保存对象的值。从逻辑角度来讲,null表示一个空对象指针
基本类型 引用类型
访问方式 操作和保存的是实际变量的值 保存在内存中,js不许直接访问内存,操作的是对象的引用
存储位置

2. JS常用的DOM操作API,查询DOM返回值分别是什么?想要遍历怎么办?它们之间又什么优劣?

  1. 查询
  • getElementById

  • getElementsByClassName

  • getElementsByTagName

  • getElementsByName

  • querySelector

  • querySelectorAll

  • forms 获取当前页面所有form,返回一个HTMLCollection

  1. 创建
  • createElement

  • createTextNode

  • createDocumentFragment
    创建一个DocumentFragment,也就是文档碎片,它表示一种轻量级文档,主要用来临时存储节点,大量操作DOM时使用它可以大大提升性能

3) 删除

  1. 复制
  • cloneNode

查询的效率问题:
由于 getElement(s)By 系列获取到的NodeList是动态的,每次访问都相当于对查询范围内进行一次重新查询,因此就算你避免了死循环,每次访问集合时,它在 DOM 查询这块仍然会消耗不少时间。
querySelectorAll 获取到的NodeList是一个“快照”,它在访问时,内容是不会动态更新的,因此它的访问效率必然更快。
因此,我的结论是,如果查询非常简单,用 id,name,className,tagName 能够一步到位查询到结果,且不需要对其进行很多操作的情况下,用 getgetElement(s)By 系列更快。但是这样的场合下,执行逻辑是否会造成性能瓶颈?恐怕不会。如果查询条件稍微复杂,不能用传统方式直接查询到,或者,需要大量操作可能造成性能瓶颈时,那么则应该使用 querySelector(All)。

类数组的遍历:
NodeList是一个类数组的结构,可以用forEach遍历,但是没有map,filter之类的函数,如果需要用到,可以用Array.from转换成数组,如果考虑到兼容性,可以用Array.prototype.slice。

3. 什么是事件冒泡和事件捕获,如何阻止冒泡,如何阻止默认事件?如何实现事件代理?

当触发事件时,触发操作将会一层层向元素内部传递,被各级元素捕获,这就是事件捕获。之后每一层元素会触发对应的事件,从内往外一级级触发,这就是事件冒泡。

阻止冒泡:stopPropagation

阻止捕获:preventDefault

事件代理:
JS事件代理就是通过给父级元素(例如:ul)绑定事件,不给子级元素(例如:li)绑定事件,然后当点击子级元素时,通过事件冒泡机制在其绑定的父元素上触发事件处理函数,主要目的是为了提升性能,因为我不用给每个子级元素绑定事件,只给父级元素绑定一次就好了,在原生js里面是通过event对象的targe属性实现

1
2
3
4
5
6
7
var ul = document.querySelector("ul");
ul.onclick = function(e){//e指event,事件对象
var target = e.target || e.srcElement; //target获取触发事件的目标(li)
if(target.nodeName.toLowerCase() == 'li'){//目标(li)节点名转小写字母,不转的话是大写字母
alert(target.innerHTML)
}
}

jq方式实现相对而言简单 $(“ul”).on(“click”,“li”,function(){//事件逻辑}) 其中第二个参数指的是触发事件的具体目标,特别是给动态添加的元素绑定事件,这个特别起作用

4. 对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点?如何避免?

闭包函数内部可以访问全局变量,函数外部无法读取函数内部的局部变量。

构成闭包的条件:内部函数对外部函数的变量有引用关系

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function outer() {

let bar = \'hello\'

function inner() {

console.log(bar)

}

inner()

}

用处:

  • 读取函数内部变量

  • 让这些变量始终保持在内存中

问题:

  • 耗费内存,引起内存泄漏

5. this指向(普通函数、匿名函数、箭头函数、setTimeout)

this大部分指向其调用者,但有两个特例并不如此。

一个是箭头函数,它本身不会修改this指向,this指向当前作用域的上一层作用域。注意:它在定义的时候就确定this指向,call、apply、bind无法改变其this指向

一个是setTimout和setInterval,它们是异步函数,会创建一个完全隔离的环境,这个环境中调用function,其实就相当于window.function,其调用者是window,所以this就指向了window。要解决这一问题,可以使用箭头函数,如上所述,箭头函数本身没有this,它的this是从上一层继承过来的,所以自然指向上一层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.number = 1

var obj = {
number: 4,
db: (function() {
this.number *= 2
return function() {
this.number *= 2
}
}) ()
}

var db1 = obj.db1
db1()
obj.db1()
console.log(window.number + obj.number)

6. call,apply,bind有什么区别?什么是柯里化函数?bind是如何实现的?

call和apply的第一个参数都是一样的,就是指定this的对象

call第一个后面的参数都是传入参数,函数调用的时候会接收这些参数

apply只有两个参数,第二个参数是用数组的方式传入函数所需的参数

bind调用之后不会立即执行函数,而是返回一个绑定了对象与传入的参数的函数,其参数跟call是一样的

柯里化函数:
就是把接收多个参数的函数变成接收单一参数的函数的一种技术,bind的本质就是柯里化函数

最简单的实现有:

1
2
3
4
5
6
7
8
9
function curryAdd(x) {

return function(y) {

return x + y

}

}

通用的封装实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function createCurry(func, args) {

let that = this

let len = func.length

let args = args || []

return function() {

let _args = [...args, ...arguments]

if (_args.length < len) {

return createCurry.call(that, func, _args)

}

return func.apply(this, _args)

}

}

function check(reg, targetStr) {

return reg.test(targetStr)

}

let _check = createCurry(check)

let checkPhone = _check(/^1[34578]d{9}$/)

console.log(checkPhone('18338384756'))

具体参考前端基础进阶(十):深入详解函数的柯里化

好处是可以实现参数复用、提前确认、延迟运行

坏处是用了闭包,容易造成内存消耗

bind的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Function.prototype.MyBind = function(context) {

if (typeof this !== 'function') {
throw new Error('MyBind必须作为函数调用')
}

let self = this
// 截取除了第一个的传入参数,并转换成数组
let args1 = Array.prototype.slice.call(arguments, 1)

// 建立一个空函数,用于保存MyBind的原型
let fNOP = function() {}

let fBound = function() {
let args2 = Array.prototype.slice.call(arguments)
// 看看fBound有没有被实例化,如果实例话其实例类型就是fNOP,那么返回this。
// 如果fBound没有被实例化,那么就是context
return self.apply(this instanceof fNOP ? this : context, args1.concat(args2))
}

// 空函数的显式原型指向MyBind
fNOP.prototype = this.prototype
// fBound的显式原型指向fNOP实例,这个时候fNOP实例的隐式原型指向fNOP的显式原型,因此当我们修改fBound的显式原型,就不会修改到fNOP的显式原型,也就是this
fBound.prototype = new fNOP()

return fBound
}

实际调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var value = 1

var foo = {
value: 2
}

function bar(name, age) {
this.habit = 'shopping'
console.log(this.value)
console.log(this.name)
console.log(this.age)
}

bar.prototype.friend = 'kevin'

var bindFoo = bar.MyBind(foo, 'daisy')

bindFoo('18')
// 2
// daisy
// 18

var obj = new bindFoo('18')
// undefined
// daisy
// 18

console.log(obj.habit) // shopping
console.log(obj.friend) // kevin

7. 什么是显式原型?什么是隐式原型?原型链是什么?为什么要有原型链?

JS中所有的变量跟函数本质上都是一个对象Object,所以我们可以说这些变量跟函数的原型就是对象,它们会继承所有Object的属性跟方法。然后将它们联通起来的工具就叫做原型链。实现方法就是通过一个隐式的原型属性__proto__,这个__proto__指向的值就是它的原型,当我们连续访问一个属性(函数)的__proto__的时候,最终就会找到Object。

另外当我们定一个函数Function的时候,JS就会给这个Function创建一个显式原型prototype。所有通过构造函数new出来的实例对象的__proto__都会指向这个prototype。所以当我们给prototype添加新的属性或者方法时,所以的实例对象都可以拿到这个新的属性或方法。

原型链图例:

为什么要使用原型链:

  • 为了实现继承,简化代码,实现代码的重用
  • 只要是这个链条上的属性,都可以被访问到

8. new一个对象具体做了什么?

  1. 创建一个新对象,如:var person = {}

  2. 新对象的__proto__属性指向构造函数的原型对象

  3. 将构造函数的作用域赋值给新对象。(也就是this对象指向新对象)

  4. 执行构造函数内部代码,将属性添加给person中的this对象

  5. 返回新对象person

实现代码:

1
2
3
4
5
6
function MyNew(fn, ...args) {
// 通过Object.create创建一个对象,并将obj的__proto__指向fn的prototype
const obj = Object.create(fn.prototype)
// 将构造函数中的this指向obj,传入args执行构造函数
return fn.apply(obj, args)
}

9. 变量提升是什么?为什么要有?如何触发?相关题目


  1. 使用var声明的变量会被提前声明
1
2
console.log(a) //undefined
var a = 10
  1. 没有使用var声明的变量不会被提前声明
1
2
console.log(a) //Uncaught ReferenceError: a is not defined
a = 10
  1. 用function声明的函数也有提前
1
2
3
4
func1() // 1
function func1() {
console.log(\'1\')
}
  1. 形参相当于var声明
1
2
3
4
function func1(e) {
console.log(e)
}
func1() //undefined

10. ===和==的区别

===是全等号,在==基础上增加了对数据类型的判断

11. 作用域是什么?有哪些作用域?题目

JS作用域也就是JS识别变量的范围,作用域链也就是JS查找变量的顺序

先说作用域,JS作用域主要包括全局作用域、局部作用域和ES6的块级作用域
全局作用域:也就是定义在window下的变量范围,在任何地方都可以访问,
局部作用域:是只在函数内部定义的变量范围
块级作用域:简单来说用let和const在任意的代码块中定义的变量都认为是块级作用域中的变量,例如在for循环中用let定义的变量,在if语句中用let定义的变量等等

注:尽量不要使用全局变量,因为容易导致全局的污染,命名冲突,对bug查找不利。

而所谓的作用域链就是由最内部的作用域往最外部,查找变量的过程.形成的链条就是作用域链

经典题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = 1

function f(x, y = function() { x = 3; console.log(x) }) {
console.log(x)
var x = 2
y()
console.log(x)
}
f()
console.log(x)

// 注释var x = 2
// 把参数x改成xx
// 把参数x改成x=4

12. 什么是惰性函数?如何实现?有什么好处?在哪里用到?

惰性函数可以在执行一次分支之后,就无需再次进行判断的技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function addEvent(type, element, fun) {

if (window.addEventListener) {

addEvent = function(type, element, fun) {

element.addEventListener(type, fun, false)

}

}

else if (window.attachEvent) {

addEvent = function(type, element, fun) {

element.attachEvent('on' + type, fun)

}

}

else {

addEvent = function(type, element, fun) {

element['on' + type] = fun

}

}

})()

具体参考js对惰性函数的理解

13. 字符串API有哪些?

charAt() 方法从一个字符串中返回指定的字符。
concat() 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。
includes() 方法用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false。
indexOf() 方法返回调用它的 String 对象中第一次出现的指定值的索引,从 fromIndex 处进行搜 索。如果未找到该值,则返回 -1。
match() 方法检索返回一个字符串匹配正则表达式的的结果。
padStart() 方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的 长度。填充从当前字符串的开始(左侧)应用的。 (常用于时间补0)
replace() 方法返回一个由替换值( replacement )替换一些或所有匹配的模式( pattern )后的新 字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要 调用的回调函数。
原字符串不会改变。
slice() 方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。
split() 方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为 子字符串,以确定每个拆分的位置。
substr() 方法返回一个字符串中从指定位置开始到指定字符数的字符。
trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符

14. 原生事件绑定(跨浏览器)?什么是dom0?什么是dom2?有什么区别?

JS原生绑定事件主要为三种:html事处理程序、DOM0级事件处理程序和DOM2级事件处理程序

  1. html事件现在早已不用了,就是在html各种标签上直接添加事件,类似于css的行内样式,缺点是不好维护,因为散落在标签中,也就是耦合度太高
    例如:
1
<button onclick=”事件处理函数”>点我</button>
  1. DOM0级事件,目前在PC端用的还是比较多的绑定事件方式,兼容性也好,主要是先获取dom元素,然后直接给dom元素添加事件
    例如:
1
2
3
4
var btn=document.getElementById(‘id元素’)
btn.onclick=function() {
//要处理的事件逻辑
}

DOM0事件如何移除呢?很简单:btn.onclick=null;置为空就行
优点:兼容性好
缺点:只支持冒泡,不支持捕获

  1. DOM2级事件,移动端用的比较多,也有很多优点,提供了专门的绑定和移除方法
    例如:
1
2
3
4
5
var btn=document.getElementById(‘id元素’)
//绑定事件
btn.addEventListener(‘click’,绑定的事件处理函数名,false)
//移除事件
btn.removeEventListener(‘click’,要移除的事件处理函数名,false)

优点:支持给个元素绑定多个相同事件,支持冒泡和捕获事件机制

15. clientWidth、offsetHeight、clientTop、offsetTop、offsetParent、getBoundingClientRect的区别

  1. clientWidth和clientHeight,指元素内容+padding的大小,不包括border、margin、滚动条

  2. offsetHeight和offsetWidth,指 元素内容+padding+border的大小,不包括margin和滚动条

  3. clientTop和clientLeft,指 border宽度

  4. offsetTop和offsetLeft,指border外边缘与offsetParent对象的距离

  5. offsetParent指元素最近定位(relative、absolute)的祖先元素,如果没有则返回null

  6. getBoundingClientRect方法,返回一个left、right、top、bottom对象,分别表示四个位置相对于视窗的坐标,不包含margin

16. 深拷贝和浅拷贝的区别是什么?手写深拷贝?Object.assign干什么用的?实用场景在哪?

深拷贝就是把引用数据类型中的数据都拷贝一份,得到一个完全独立于原对象的新对象,两个对象之间的引用数据不共享

深拷贝实现:

  • 正常实现
1
2
3
4
5
6
7
8
function deepCopy(obj) {
let newObj = {}
if (typeof obj != 'object') return obj
for (var attr in obj) {
newObj[attr] = deepCopy(obj[attr])
}
return newObj
}
  • 借助Json的stringify和parse

Object.assign:
用es6的Object.assign({},{})进行对象合并,如果是数组可以用es6的Array.from,或是es6的扩展运算符…arr

17. js的异步执行机制是怎么样的?什么是宏任务?什么是微任务?哪些是宏任务?哪些是微任务?执行顺序是什么?题目?

js的异步是通过事件循环机制实现的,也就是Event Loop。
事件循环将所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程, 某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务又分为宏任务和微任务

宏任务包括setTimeout、setInterval、setImmediate、I/O、UI rendering

微任务包括process.nextTick、Promise.then、MutationObserver

微任务首先执行,所有微任务执行完之后,再去取一个宏任务执行,如此往复。

因此Promise会在setTimeout之前执行

具体题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 // Step 1
console.log(1);

// Step 2
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);

// Step 3
new Promise((resolve, reject) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
})

// Step 4
setTimeout(() => {
console.log(6);
}, 0);

// Step 5
console.log(7);

// Step N
// ...

// Result
/*
1
4
7
5
2
3
6
*/

18. 浏览器的全局变量有哪些?window是什么?navigator是什么?location是什么?实际场景中都会用到哪些函数和变量?

window、navigator、location、alert()、setTimeout()

  1. navigator
  • back():后退

  • forward():前进

  • go():前往具体某个页面

  1. location
  • 包含当前URL的信息,可通过window.location

  • 方法:

  • hash 设置或返回从井号(#)开始的URL

  • host 设置或返回主机名和端口号

  • hostname 设置或返回当前主机名

  • href 设置或返回完整URL

  • pathname 设置或返回路径部分

  • port 设置或返回端口号

  • protocol 设置或返回URL协议

  • search 设置或返回问号(?)开始的部分

  • assign() 加载新的文档

  • reload() 重新加载当前文档

  • replace() 用新的文档替换当前文档

  1. history
  • 包含用户在浏览器窗口中访问过的URL,通过window.history访问

19. 什么是垃圾回收?原理是什么?如何实现?如何运用该机制?

实现思路:

  1. 引用计数
  • 给所有对象进行引用计数,计数为0的对象会被当作垃圾回收
  • 缺陷:互相引用的对象会被不停地计数
1
2
3
4
var obj1 = {}
var obj2 = {}
obj1.x = obj2
obj2.x = obj1
  1. 标记清除
  • 垃圾回收器获取到根并标记它

  • 然后访问它的所有子对象,逐一标记

  • 继续访问子对象的子对象,标记,知道没有引用,保证同一个对象以后不会访问第二次

  • 没有被标记到的对象全部删除

  • 参考彻底掌握js内存泄漏以及如何避免

20. 什么是内存泄漏?什么情况会触发?如何避免?

不会再被用的对象没有被垃圾回收机制清理掉,就会造成内存泄漏。

如何分析内存泄漏可以参考深入了解 JavaScript
内存泄露

场景:

  1. 引用计数下对象互相引用

  2. 在函数中声明全局变量

1
2
3
function foo() {
bar = 'hello world'
}
  1. 被遗忘的定时器
1
2
3
4
var superBigData = {}
setInterval(function() {
console.log(supderBigData)
}, 200)
  1. 被遗忘的事件监听器

一般指挂在在window下的事件监听器忘了移除监听,如resize事件

1
window.addEventListener('resize', function() {})

21. DOM事件中的target和currentTarget有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul>

<li>1</li>

<li>2</li>

<li>3</li>

<li>4</li>

<li>5</li>

</ul>

ul.addEventListener('click', function(e) {

console.log(e.target) // li

console.log(e.currentTarget) // ul

})

e.target指向触发事件的对象

e.currentTarget指向监听事件的对象

22. JS的设计模式有哪些?

  1. 工厂模式

  2. 单例模式

  3. 发布订阅模式

  4. 桥街模式

  5. ……

23. 实现防抖、节流函数,适用的场景

  1. 防抖
  • 在多次触发事件的操作停止之后,才开始计时,必须等到计时完成才执行事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function debounce(func, wait, immediate) {
var timer
return function() {
if (timer) clearTimeout(timer)
if (immediate) {
var callNow = !timer
timer = setTimeout(() => timer = null, wait)
if (callNow) func.apply(this, arguments)
}
else {
timer = setTimeout(() => func.apply(this, arguments), wait)
}
}
}
  • 使用场景:

  • 登录、发短信等按钮

  • 调整窗口大小

  • 文本编辑器实时保存

  1. 节流
  • 不管期间触发了多少次操作,每个一段时间触发一次事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function trottle(func, wait, options) {

var timer, context, args

var previous = 0

if (!options) options = {}

var later = function() {

previous = !options.leading ? 0 : new Date().getTime()

timer = null

func.apply(context, args)

if (!timer) context = args = null

}

var trottled = function() {

var now = new Date().getTime()

// previous=now时,事件不执行

if (!previous && !options.leading) previous = now

var remaining = wait - (now - previous)

context = this

args = arguments

if (remaining <= 0 || remaining > wait) {

if (timer) {

clearTimeout(timer)

timer = null

}

previous = now

func.apply(context, args)

if (!timer) context = args = null

}

else if (!timer && options.trailing) {

timer = setTimeout(later, remaining)

}

}

}

使用场景:

  • 提交按钮

24. 手写封装一个事件通用类

事件通用类主要用于解决不同浏览器对事件函数的兼容性问题,比如IE6到8就不支持addEventListener、stopPropagation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class BomEvent {
constructor(element) {
this.element = element;
}

addEvent(type, handle) {
if (this.element.addEventListener) {
//事件类型、需要执行的函数、是否捕捉
this.element.addEventListener(type, handle, false);
} else if (this.element.attachEvent) {
this.element.attachEvent("on" + type, function () {
handle.call(element);
});
} else {
this.element["on" + type] = handle;
}
}

removeEvent(type, handle) {
if (this.element.removeEventListener) {
this.element.removeEventListener(type, handle, false);
} else if (this.element.detachEvent) {
this.element.detachEvent("on" + type, handle);
} else {
this.element["on" + type] = null;
}
}
}

// 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
function stopPropagation(event) {
if (event.stopPropagation) {
event.stopPropagation(); // 标准w3c
} else {
event.cancelBubble = true; // IE
}
}

// 取消事件的默认行为
function preventDefault(event) {
if (event.preventDefault) {
event.preventDefault(); // 标准w3c
} else {
event.returnValue = false; // IE
}
}

25. 关于JS继承

我们知道 JavaScript 中并不存在 class,存在的只是原型链,都是通过函数和 prototype 去封装一些东西来模拟“类”。可以说任何一个函数都可以被视为一个“类”,只要你愿意。

说了那么多,笔者的体会是不要想着继承,不要想着继承,不要想着继承。。。
JavaScript 本身就不是面向对象的语言,干嘛要让它做它不擅长的事情 =_= 虽然语法糖已经提供了“类”的支持,那是照顾有面向对象想法的人,但它本质上不同于其他语言中的继承。不要把他人的宽容当作放任的理由,能模拟继承就不错了,就别再惦记“多继承”了。
再回过头来想一想,我们为什么需要继承?继承是一种强耦合关系,到底是否有必要用继承,可以考虑下在应用场景中是否需要用父类型去接收子类型的实例,即子类向父类的向上转型。在 JavaScript 中不会出现这样的需求,应该更多使用组合的方式以代替继承,以及函数式编程也许是更好的方案。

26. 说一下你对同步和异步的理解

同步即sync,形象的说就是代码一行行执行,前面代码和请求没有执行完,后面的代码和请求就不会被执行,
缺点:容易导致代码阻塞
优点:程序员容易理解(因为代码从上往下一行行执行,强调顺序)

异步:即async,形象的说就是代码可以在当前程序没有执行完,也可以执行后面的代码
缺点:程序员不易理解(因为不是按顺序执行的)
优点:可以解决代码阻塞问题,提升代码执行效率和性能

异步解决方案主要有三个:
1. 回调函数
2. promise(重点掌握)
3. generator(了解)
4. async和await(重点掌握)

27. 请写至少三种数组去重的方法?(原生js)

1
2
3
4
5
6
7
8
//利用filter
function unique(arr) {
return arr.filter(function(item, index, arr) { //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
}); }
var arr = [1,1,'true','true',true,true,15,15,false,false,
undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
1
2
3
4
5
6
7
//利用ES6 Set去重(ES6中最常用) function unique (arr) {
return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false,
undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//利用for嵌套for,然后splice去重(ES5中最常用) 
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){
arr.splice(j,1);
j--; }
}
}
//第一个等同于第二个,splice方法删除
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false,
undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {...}, {...}] //NaN和{}没有去重,两个null直接消失了

28. 图片懒加载是怎么实现的?

就是我们先设置图片的data-src属性(当然也可以是其他任意的,只要不会发送http请求就行了,作用 就是为了存取值)值为其图片路径,由于不是src,所以不会发送http请求。 然后我们计算出页面 scrollTop的高度和浏览器的高度之和, 如果图片距离页面顶端的坐标Y(相对于整个页面,而不是浏览 器窗口)小于前两者之和,就说明图片就要显示出来了(合适的时机,当然也可以是其他情况),这时 候我们再将 data-set 属性替换为 src 属性即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<head>
<style>
.img {
width: 200px;
height: 200px;
background-color: gray;
margin-bottom: 20px;
}

.pic {
width: 100%;
height: 100%;
}
</style>
</head>
<!-- 图片来自网络,侵删。 -->

<body>
<div class="container">
<div class="img">
<!-- 注意我们并没有为它引入真实的src -->
<img
class="pic"
alt="加载中"
data-src="https://tse1-mm.cn.bing.net/th/id/OIP.8OrEFn_rKe82kqAWFjTuMwHaEo?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://ssl.tzoo-img.com/images/tzoo.94911.0.910013.seoul-nami.jpg?width=1080"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse4-mm.cn.bing.net/th/id/OIP.ZitgAuABnwkrGn4lid2ZmQHaEK?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="http://pic34.photophoto.cn/20150315/0034034862056002_b.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="http://img.mp.sohu.com/upload/20170724/32d4409f34194b029ed287abf1c99b70_th.png"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075991913520.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse4-mm.cn.bing.net/th/id/OIP.PZdPKj3sXEX2jLrepx3MUwHaEo?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075831439349.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075468043336.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse2-mm.cn.bing.net/th/id/OIP.CRYz5Bv4vylsMh83G4CsLgHaFj?pid=Api&rs=1"
/>
</div>
</div>
<head>
<style>
.img {
width: 200px;
height: 200px;
background-color: gray;
margin-bottom: 20px;
}

.pic {
width: 100%;
height: 100%;
}
</style>
</head>
<!-- 图片来自网络,侵删。 -->

<body>
<div class="container">
<div class="img">
<!-- 注意我们并没有为它引入真实的src -->
<img
class="pic"
alt="加载中"
data-src="https://tse1-mm.cn.bing.net/th/id/OIP.8OrEFn_rKe82kqAWFjTuMwHaEo?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://ssl.tzoo-img.com/images/tzoo.94911.0.910013.seoul-nami.jpg?width=1080"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse4-mm.cn.bing.net/th/id/OIP.ZitgAuABnwkrGn4lid2ZmQHaEK?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="http://pic34.photophoto.cn/20150315/0034034862056002_b.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="http://img.mp.sohu.com/upload/20170724/32d4409f34194b029ed287abf1c99b70_th.png"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075991913520.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse4-mm.cn.bing.net/th/id/OIP.PZdPKj3sXEX2jLrepx3MUwHaEo?pid=Api&rs=1"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075831439349.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://pic6.wed114.cn/20180829/2018082910075468043336.jpg"
/>
</div>
<div class="img">
<img
class="pic"
alt="加载中"
data-src="https://tse2-mm.cn.bing.net/th/id/OIP.CRYz5Bv4vylsMh83G4CsLgHaFj?pid=Api&rs=1"
/>
</div>
</div>
<script>
// 获取所有的图片标签
const imgs = document.getElementsByTagName("img");
// 获取可视区域的高度
const viewHeight =
window.innerHeight || document.documentElement.clientHeight;
// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0;

function lazyload() {
console.log("滚动...");
for (let i = num; i < imgs.length; i++) {
// 用可视区域高度减去元素顶部距离可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top;
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
if (distance >= 0) {
// 给元素写入真实的src,展示图片
imgs[i].src = imgs[i].getAttribute("data-src");
// 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
num = i + 1;
}
}
}

// 防抖函数
function debounce(fn, delay = 500) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, args);
}, delay);
};
}

// 是的页面初始化是加载首屏图片
window.onload = lazyload;
// 监听Scroll事件,为了防止频繁调用,使用防抖函数优化一下
window.addEventListener("scroll", debounce(lazyload, 600), false);
</script>
</body>

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

判断数据类型的方法一般可以通过:typeof、instanceof、constructor、toString四种常用方法

不 同 类 型 的 优 缺 点 typeof instanceof constructor Object.prototype.toString.call
优 点 使用简单 能检测出引用 类型 基本能检测所有 的类型(除了 null和 undefined) 检测出所有的类型
缺 点 只能检测 出基本类 型(出 null) 不能检测出基 本类型,且不 能跨iframe constructor易被 修改,也不能跨 iframe IE6下,undefined和null均为 Object

constructor的使用方法:
constructor可以同时检测基本数据类型和引用类型

1
2
3
console.log(a.constructor === Array)    // true
console.log(b.constructor === Date) // false
console.log(c.constructor === Function) // false

prototype.toString的使用方法:

1
2
console.log(Object.prototype.toString.call(a) === '[object String]')    // true
console.log(Object.prototype.toString.call(b) === '[object Number]') // true

五、ES6


1. ES6有哪些新特性?

  1. let和const

let和const具有块级作用域,不存在变量提升的问题。

const可以通过defineProperty将writable置为false来实现

  1. iterable类型
  • Array、Map、Set都属于iterable类型,可以用for…of遍历
  1. 解构赋值

  2. 箭头函数

没有构造函数,不能new
this指向当前作用域的上一层作用域,在定义的时候就确定了,无法被apply等修改

  1. 字符串模板

  2. iterator,generator

iterator是一个迭代器,拥有一个next方法,这个方法返回一个对象{done,value},这个对象包含两个属性,一个布尔类型的done和包含任意值的value
generator: 它是一种特殊的iterator。通过function*来声明的,此函数内可以使用yield关键字。在yield出现的地方可以通过generator的next或throw方法向外界传递值。

8)Promise

9)ES6 Module

导出使用export
导入使用import

2. 为什么要有promise?如何使用?状态?特性?链式调用原理?实现?all和race的区别?使用场景?

promise的作用:
promise用于解决回调地狱的问题

promise的状态:
promise有三种状态,Fullfilled代表成功,Rejected代表失败,Pending代表进行中

promise的特性:

  • 对象的状态不受外界影响
  • 状态是不可逆的,一旦改变就不会再变

promise的使用:
实现异步读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import fs from 'fs'

function getFile(path) {

return new Promise((resolve, reject) => {

fs.readFile(fPath, 'utf-8', (err, dataStr) => {

if (err) return reject(err)

return resolve(dataStr)

})

})

}

getFile('01. txt')

.then(dataStr => console.log(dataStr))

.catch(err => console.log(err))

promise的链式调用实现:
首先new Promise的时候,内部的构造函数就会初始化一个state,将state置为PENDING。
接着定义两个回调函数的数组,即onFulfilledCallbacks和onRejectedCallbacks,用来收集回调函数
最后是定义两个函数变量,即resolve和reject,这俩会被当作参数传给回调函数,当resolve或reject被调用时,state就会改变。
resolve中状态变为Fulfilled,然后遍历onFulfilledCallbacks中的回调函数,将传入的参数透传给onFulfilledCallbacks中的回调函数
reject中状态变为Rejected,然后遍历onRejectedCallbacks中的回调函数,将传入的参数透传给onRejectedCallbacks中的回调函数
调用then的时候会返回一个new Promise对象,这个对象就可以实现链式调用的功能。
传入的参数是一个回调函数,它会被push到回调数组之中,等待resolve和reject的执行。
具体实现代码参考:Promise的具体实现

all和race:
all是等所有的Promise请求完成之后才会返回数组
可以用在插入编辑器图片的时候,可以上传到服务器再按顺序插入到编辑器

race是只要有一个Promise完成就立刻返回该数组
一般用在请求图片的时候,设置一个定时器,超过定时器的时间就显示超时

3. 什么是generator?什么是async/await?如何实现?

generator函数是一个封装的异步任务,定义时只需要在function后面加个星号(*)即可,函数内部要用yield关键字来定义异步操作,执行它会返回一个遍历器对象,通过next或throw方法执行下一个异步操作。

async的内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
function spawn(gen) {

const it = gen()

!function step(nextFn) {

try {

var { data, done } = nextFn()

}

catch(e) {

return reject(e)

}

if (done) return resolve(data)

Promise.resolve(data)

.then(value => step(() => it.next(value)))

.catch(value => step(() => it.throw(value)))

}(() => it.next(undefined))

}

function* gen() {

try {

var a = yield new Promise((resolve, reject) => setTimeout(() => reject(100), 1000))

}

catch(e) {

a = e

}

let b = yield new Promise((resolve, reject) => setTimeout(() => resolve(102), 1000))

return a + b

}

async function asyncDemo() {

try {

var a = await new Promise((resolve, reject) => setTimeout(() => reject(100), 1000))

}

catch(e) {

a = e

}

let b = await new Promise((resolve, reject) => setTimeout(() => resolve(102), 1000))

return a + b

}

spawn(gen)

.then(value => console.log('spawn --> onFulfilled:', value))

.catch(value => console.log('spawn --> onRejected:', value))

asyncDemo()

.then(value => console.log('asyncDemo --> onFulfilled:', value))

.catch(value => console.log('asyncDemo --> onRejected:', value))

async本质上就是一个function*的生成器,await其实就是一个yield关键字,然后这个生成器会被包装在一个spawn函数里面,这个spawn里面有一个自执行函数,负责使用next去反复调用这个生成器,直到状态done为true,也就是异步操作完成了。

async await 是es7里面的新语法、它的作用就是 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。它可以很好的替代promise 中的then。
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

4. 什么是模块化规范?有哪些规范?分别是怎么导出和导入的?

主流就两种,一种是commonJS,一种是ES6 Module

  • commonJS 常用在nodejs里面,使用module.export导出,使用require导入
  • ES6 Module则常用在使用ES6以上语法的项目中,使用export导出,使用import导入
    其中export default只能导出一个,export可以导出多个
    export在import的时候需要加{}

5. Array的基本函数有哪些?高阶函数有哪些?分别是干什么用的?实用场景在哪?返回值是什么?map和forEach有什么区别?如何中断map和forEach?

  • concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
  • find() 方法接收的是一个函数,数组会将元素作为参数传给这个回调函数,返回的是一个值,没有则返回undefined
  • findIndex() 同上,返回的是一个索引,没有就返回-1
  • includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true, 否则返回false。
  • indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 (通常用它判断数组中有没有这个元素)
  • join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。 如果数组只有一个项目,那么将返回该项目而不使用分隔符。
  • pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
  • push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
  • shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
  • unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。 splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内
  • 容。此方法会改变原数组。
  • 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删
  • 除元素,则返回空数组。
  • reverse() 方法将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组。
  • sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的
  • splice
  • forEach 对数组的每个元素执行一次提供的函数。
  • map
  • filter 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
  • some 这个some方法用于只要数组中至少存在一个满足条件的结果,返回值就为true,否则返回fasel, 写法和forEach类似
  • every 这个every方法用于数组中每一项都得满足条件时,才返回true,否则返回false, 写法和forEach类似

6. Map如何使用?有哪些API?实用场景在哪?

map 是es6 提供的一种新的数据结构,它类似于对象,也是键值对的集合,但是键的范围不仅限于字符 串,各种类型的值都可以当做键。
也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供 了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

用来实现Vue3.0的响应式

7. Set如何使用?有哪些API?实用场景在哪?

set 是es6 提供的一种新的数据结构,它类似于数组,但是成员的值都是唯一的。

API有add、delete、has、clear、size

多用来去重

WeakSet只能保存对象,并且保存的对象在没有其他引用时会被垃圾回收杀掉。

六、CSS


1. 什么是盒模型?由哪些部分构成?box-sizing又是做什么用的?

盒模型其实就是浏览器把一个个标签都看一个形象中的盒子,那每个盒子(即标签)都会有内容(width,height),边框(border),以及内容和边框中间的缝隙(即内间距padding),还有盒子与盒子之间的外间距(即margin)

box-sizing有以下三个属性:

  1. content-box,以content为盒子的宽度和高度,即W3C盒模型

  2. border-box,盒子的宽高包括边框和内边距,内容真正宽高会根据边框和内边距进行调整,IE盒模型

  3. inherit,从父元素继承值

2. CSS3有哪些新特性?

  1. 新特性
  • CSS实现圆角(border-radius)、阴影(box-shadow)、边框图片(border-image)

  • 旋转,缩放,定位,倾斜:transform: rotate(90deg) scale(.85, .9)
    translate(0px, -30px) skew(-9deg, 0deg)

  • 增加了更多的CSS选择器,多背景,rgba()

  • 在CSS3中唯一引入的伪元素是::selection;

  • 媒体查询(@media),多栏布局(flex)

  • 对字体加特效(text-shadow)、强制文本换行(word-wrap)、线性渐变(linear-gradient)

  1. 伪类:用于向某些选择器添加效果
  • :hover 将样式添加到鼠标悬浮的元素上

  • :active 将样式添加到被激活的元素上

  • :focus 将样式添加到获得焦点的元素上

  • :link 将样式添加到未被访问过的元素上

  • :visited 将样式添加到被访问过的元素上

  • :first-child 将样式添加到元素的第一个子元素

  • :lang 定义指定的元素中使用的语言

  1. 伪元素:代表某个元素的子元素,这个子元素虽然在逻辑上存在,但不存在于文档树之中
  • ::first-letter 将样式添加到文本的首字母

  • ::first-line 将样式添加到文本的首行

  • ::before 在某元素之前插入某些内容

  • ::after 在某元素之后插入某些内容

  1. 新增伪类
  • p:first-of-type 选择<p>元素中首个<p>元素

  • p:last-of-type 选择<p>元素中最后一个<p>元素

  • p:only-of-type
    选择某个父元素中有且只有一个<p>元素的元素(允许非p元素存在)

  • p:only-child
    选择某个父元素中有且只有一个<p>元素的元素(不允许任何元素存在)

  • p:nth-child(n) 选择第n个元素,并且这个元素的类型是p

  • p:nth-last-child(n)
    选择某个父元素中倒数第n个<p>元素,并且这个元素的类型是p

  • p

  • p:nth-of-type(n) 选择p元素中第n个元素

  • p:nth-of-last-type(n) 选择p元素中倒数第n个元素

  • p:last-child 选择最后一个元素,且这个元素是p

  • p:empty 选择没有子元素的p元素

  • p:target 选择当前活动中的p元素

  • :not(p) 选择所有非p元素

  • :enabled 控制表单控件的可用状态

  • :disabled 控制表单控件的不可用状态

  1. 通过opacity设置元素的透明度
1
.hide { opacity: 0; }
  1. visibility
1
.hide { visibility: hidden; }
  1. display
1
.hide { display: none; }
  1. position
1
.hide { position: absolute; top: -9999px; left: -9999px; }
  1. clip-path
1
.hide { clip-path: polygon(0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px); }

3. 实现水平垂直居中有哪些方案?

水平居中

  1. 行内元素
1
text-align: center;
  1. 定宽块状元素
1
margin: 0 auto;
  1. 不定宽块状元素
  • 在元素外加入table标签,该元素写在td之中,然后设置margin的左右为auto

  • 给元素设置display: inline,将元素转变成行内元素,然后使用text-align:
    center

  • 父元素设置position: relative; display: float; left:
    50%,该元素设置position: relative; left: -50%;

垂直居中:

  1. 父元素高度确定的单行文本,设置line-height等于父元素的height

  2. 父元素高度确定的多行文本

  • 父元素嵌入到table中,然后设置vertical-align: middle;
  • 先设置display: table-cell; 然后设置vertical-align: middle;

其他解决方案:

  1. 使用transform
1
2
3
4
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%)
  1. 使用flex
1
2
3
display: flex;
justify-content: center;
align-item: center;

4. position和display是做什么的,分别有哪些值?

  1. position
  • absolute

    • 绝对定位,相对于static定位以外的第一个父级元素左上角定位,脱离正常文档流
  • fixed

    • 固定定位,相对于浏览器窗口左上角定位,脱离正常文档流
  • relative

    • 相对定位,相对于其父元素定位,不脱离正常文档流
  • sticky

    • 粘性定位,会根据滚动条改变展示效果,比如设置top: 0时,当元素未滚动到页面顶端,元素为relative定位;当元素滚动到页面顶端时,元素变为fixed定位
  • static

    • 默认值,没有相对定位,在正常文档流之中
  • inherit

    • 继承父级元素的定位
  1. display
  • none

    • 不显示元素
  • block

    • 显示为块级元素,前后有换行符
  • inline

    • 行内元素
  • inline-block

    • 结合了inline和block的特点,既可以设置长宽、margin、padding,又能跟其他行内元素并排

5. 移动端有哪些适配方案?各有什么优缺点?

前端做适配没有最好的方法,只有适合的方法,目前前端主要做适配的方法有:百分比,em,rem,媒体查询(即media query),flex布局(即弹性盒),vw,vh等
目前我在项目中用的多的是rem,flex布局,有时会用到媒体查询,在做pc响应式布局时用

  • rem布局
    主要是用了一个手淘的js库flexible.js,在页面变化时,检测页面宽度,除以10份,动态的赋值给font-size.属性.;而页面的布局我是通过rem来进行布局的,所以就可以适配所有的移动端设备了

  • 响应式布局
    那么 Ethan Marcotte 在 2010 年 5 月份提出的一个概念,简而言之,就是一个网站能够兼容多 个终端。越来越多的设计师也采用了这种设计。
    CSS3 中的 Media Query(媒介查询),通过查询 screen 的宽度来指定某个宽度区间的网页布 局。
    超小屏幕(移动设备) 768px 以下
    小屏设备 768px-992px
    中等屏幕 992px-1200px
    宽屏设备 1200px 以上

  • flex布局
    即弹性盒模型,是目前最主流的适配方案,浏览器的支持度最高
    使用:

    1
    display: flex;

    属性:

    • flex-direction
      决定主轴方向
    • flex-wrap
      决定是否换行
    • flex-flow
      可以同时设置flex-direction和flex-wrap
    • justify-content 定义主轴的布局
      flex-start 左对齐
      flex-end 右对齐
      center 居中对齐
      space-between 居中平铺
      space-around 两边对齐平铺
    • align-items 定义副轴的布局
    • align-content

6. 什么是BFC?什么是Margin塌陷?为什么会有BFC?BFC有哪些特性?怎么创建BFC?

定义:
BFC即块级格式化上下文,它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。

Margin塌陷:
即Margin Collapse,是CSS1.0中规定的一种布局方案,当两个Box上下相邻时,它们之间的margin会发生重叠。这个最早是为了解决p标签上下排布时出现1em、2em、2em、1em这样不等距的布局表现。但有时候我们不需要系统为我们做多余的事情,为了取消Margin塌陷,这个时候就需要用到BFC。

为什么会有BFC:
如上所述,一方面是为了解决Margin塌陷问题,另一方面是为了解决浮动元素的一些意外情况,比如会导致父元素塌陷,相邻元素重叠等问题 。

特性:

  • BFC不会与外部的浮动元素重叠
  • 计算BFC的高度的时候,也会囊括内部的浮动元素
  • BFC内部的元素会发生Margin塌陷,但外部不会

如何创建BFC:

  • 根元素
  • float不为none
  • overflow不为visible
  • display为table-cell、table-caption、inline-block
  • position是absolute或fixed

7. CSS3动画

css3动画大致上包括两种:

第一种:过渡动画:主要通过transition来实现,通过设置过渡属性,运动时间,延迟时间和运动速度实现。

第二种:关键帧动画:主要通过animation配合@keyframes实现

transition动画和animation动画的主要区别有两点:
第一点transition动画需要事件来触发,animation不需要
第二点:transition只要开始结束两种状态,而animation可以实现多种状态,并且animation是可以做循环次数甚至是无限运动


七、Vue


1. Vue全家桶有哪些?

  • vue-cli脚手架
  • vue-router路由
  • vuex状态管理工具
  • axios请求库
  • UI组件库(Element-UI、Vant、iView)

2. 什么是MVVM?Vue如何体现MVVM思想?它与MVC有什么区别?

MVVM:是Model-View-ViewModel的缩写。
Model即业务层,我们在script标签中写的data、methods、computed这些就属于业务层的,用来处理页面的业务逻辑。
View即视图层,也就是HTML+CSS部分,用来展示页面的布局和效果。
ViewModel即用来沟通View和Model的中间层,也就是我们平常使用的Vue,一般来说View跟Model之间的通信都需要经过ViewModel。
但Vue只是体现了MVVM的思想,并不严格遵守MVVM思想,严格的MVVM思想是不允许Model直接操作View的。而Vue中增加了$refs属性,让这一功能得以实现。

MVC则是Model-View-Controller的缩写
其中视图(View)代表用户界面,控制器(Controller)代表业务逻辑,模型(Model)代表数据保存。
它们之间的关系是:
1)View传送指令到Controller
2)Controller完成业务逻辑之后,要求Model改变状态
3)Model将新的数据发送到View,用户得到反馈

MVC与MVVM之间的主要区别有两个:
1)MVC的Model主要指与数据库交互这部分逻辑,而MVVM中的Model其实是MVC的Controller,也就是业务层,而与数据库交互的这部分逻辑是交给后台来完成的。
2)MVC是一个环状结构,三个模块之间都有联系。而MVVM是一个横向结构,业务层跟视图层产生联系必须经过中间层ViewModel,不能直接产生联系。

3. Vue2的双向数据绑定原理(响应式)是什么?代码如何实现?有什么问题?如何动态添加响应式属性?

双向数据绑定是采用数据劫持结合发布者-订阅者的设计模式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 具体步骤:
第一步:
把data中的数据变量传给Ovserver,Observer会对数据对象进行递归遍历,包括子对象的属性,借助Object.defineProperty给每个属性都加上 setter 和 getter。当触发getter时候,就会给通知器Dep绑定一个Watcher,这个Watcher里面就包含了更新视图的逻辑。当触发setter的时候,就说明数据变化了,那就会调用Dep,通知到Watcher,Watcher在把新数据更新到视图上面。
第二步:
Compile解析模板指令,将模板中的变量替换成数据,给自定指令(v-)添加监听事件,修改input组件时更新数据。然后初始化渲染页面视图,并给每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
第三步:
Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往视图通知器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的 update() 方法,并触发Compile中绑定的回调,则功成身退。
第四步:
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer 来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和 Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向 绑定效果。

问题:

  • 无法直接监听Array跟Object的变化,需要做特殊处理
    比如Array的原型会被重写,所有修改数组的函数都被加上监听器
  • defineProperty通过递归给每个变量添加getter和setter存在一定的性能问题

动态添加响应式属性:
一般在data中定义的属性是响应式的,会根据数据的变化刷新视图。这是因为在初始化的时候Vue在背后做了双向数据绑定。
但由于ES5无法监听对象属性的添加或删除,所以动态给对象添加的属性并不支持响应式,比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
data() {
return {
student: {
age: 17
}
}
},
methods: {
addAttr() {
this.student.name = '小明'
}
}

如果希望给属性添加响应式,就需要用到$set。具体用法如下:

1
this.$set(this.student, 'name', '小明')

注意:$set无法动态添加根级属性

如:

1
2
3
4
5
6
7
const app = new Vue({
data:{
a: 1
}
})

Vue.set(app.data, 'b', 2) // 根级不存在b属性,这样添加会报错

4. Vue组件之间如何通信?

  1. 父传子prop和子传父emit
  • 父传子
    父组件通过import引入子组件,并注册,在子组件标签上通过v-bind添加要传递的属性,子组件通过props接收,接收有两种形式一是通过数组形式[‘要接收的属性’ ],二是通过对象形式{ }来接收,对象形式可以设置要传递的数据类型和默认值,而数组只是简单的接收

  • 子传父
    子组件通过通过绑定emit事件触发函数

1
this.$emit(‘eventx’,要传递的值) // $emit中有两个参数一是要派发的自定义事件,第二个参数是要传递的值

父组件通过在子组件的标签中定义对应的监听@eventx,或者直接监听on事件来获取通知

1
this.$on('自定义事件', 要运行的回调函数)

如果想通过子组件的prop给父组件传值就需要借助update:prop,例如:

1
2
3
4
5
6
7
8
9
// 子组件
this.$emit('update:myMessage', '小明')

// 父组件
<div :myMessage.sync="name"></div>
// 父组件的methods
updateName(e) {
this.name = e
}
  1. 使用vuex

  2. 使用eventBus

本质上就是运用了emit和on的事件机制,通过new一个空的Vue来作为事件总线。然后在各个页面中引入EventBus,就可以利用emit发射事件,然后在另一个地方使用接收事件
缺点:在各处使用数据会导致数据变得混乱,导致难以维护,所以不推荐使用这种传值方式

  1. 将组件封装成template,提高上一级

在子组件挂一个slot占位,然后在父组件中使用template去实现这个slot,这个时候就可以将父组件的数据传给子组件

5. 什么是vuex?vuex的应用场景?mutaions跟actions的区别?实现原理?如何让state数据持久化?为什么不用全局变量代替?如何使用?

vuex是一个状态管理工具,主要解决大中型复杂项目的数据共享问题,主要包括state,actions,mutations,getters和modules 5个要素。

主要流程:组件通过dispatch触发actions中的异步任务,也可以直接调用mutation中的同步任务,如果需要在mutations内部调用其他函数,就需要借助commit,最后mutations中的可以直接修改state中的数据。而getters相当于组件的计算属性对,组件中获取到的数据做提前处理的.再说到辅助函数的作用.

mutations和actions的区别:
actions可以异步执行,但mutations只能同步

State数据持久化:
因为vuex中的state是存储在内存中的,一刷新就没了,例如登录状态,解决方案有:

  • 利用H5的本地存储(localStorage,sessionStorage)
  • 利用第三方封装好的插件,例如:vuex-persistedstate
  • 使用vue-cookie插件来做存储
  • 可以把数据传递到后台,存储到数据库中,比较耗费资源

Vuex 和单纯的全局对象有以下两点不同:
1.Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
2.不能直接改变 store 中的状态。组件改变state的唯一方法是通过显式地提交mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

使用:
在组件中引入mapState、mapMutations、mapActions、mapGetters,通过在对应的模块中引入以上属性来获得:

1
2
3
4
5
6
7
import { mapState } from 'vuex'

...

computed: {
...mapState('m_user', ['token'])
}

6. Vue的生命周期有哪些?分别在什么时候触发?每个周期都会用来做什么事情?使用了keep-alive会发生什么?

vue生命周期即为一个组件从出生到死亡的一个完整周期,主要包括以下4个阶段:创建,挂载,更新,销毁
创建前:beforeCreate, 创建后:created
在beforeCreate,data和method 还没有初始化
在created,data和method已经初始化完成
挂载前:beforeMount, 挂载后:mounted
在beforeMount 生命周期函数执行的时候,已经编译好了模版字符串、但还没有真正渲染到页面中去
在mounted 生命周期函数执行的时候,已经渲染完,可以看到页面
更新前:beforeUpdate, 更新后:updated
在beforeUpdate生命周期函数执行的时候,已经可以拿到最新的数据,但还没渲染到视图中去。
在updated生命周期函数执行的时候,已经把更新后的数据渲染到视图中去了。
销毁前:beforeDestroy, 销毁后:destroyed
在beforeDestroy 生命周期函数执行的时候,实例进入准备销毁的阶段、此时data 、methods 、指令 等还是可用状态
在destroyed生命周期函数执行的时候,实例已经完成销毁、此时data 、methods 、指令等都不可用
我平时用的比较多的钩子是created和mounted,created用于获取后台数据,mounted用于dom挂载完后做一些dom操作,以及初始化插件等.beforeDestroy用户清除定时器以及解绑事件等,
另外还新增了使用内置组件 keep-alive 来缓存实例,而不是频繁创建和销毁(开销大)
activated 实例激活
deactivated 实例失效

6. 什么是keep-alive?keep-alive原理?如何强制刷新缓存?

keep-alive:主要用于保留组件状态或避免重新渲染。

比如: 有一个列表页面和一个 详情页面,那么用户就会经常执行打开详情=>返回列表=>打开详情这样的话 列表 和 详情 都是一个频率很高的页面,那么就可以对列表组件使用进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染。

属性:
include:字符串或正则表达式。只有匹配的组件会被缓存。
exclude:字符串或正则表达式。任何匹配的组件都不会被缓存。
max:用来定义缓存组件的上限,把最长时间未访问过的页面替换成最新页面
注意:include中定义的值将会匹配组件的name属性。

原理:
被keep-alive包裹的组件,组件会被缓存到一个componentInstance属性中,在创建组件的时候,Vue会判断该节点的布尔值keepAlive,如果为true,并且缓存的实例componentInstance不为空,那就将缓存直接插入到DOM中。

刷新缓存:
主要有两种方法。一种是通过修改key属性来触发刷新,另一种是通过this.$forceUpdate来出发刷新。

7. 使用Element-ui遇到的问题

下拉框的快捷键冲突
级联选择器数据过多导致卡顿
input输入框无法输入

8. methods、computed和watch的区别?

methods中都是封装好的函数,无论是否有变化只要触发就会执行

computed:是vue独有的特性计算属性,一般用来简化模板字符串中的计算复杂度,防止模版太过冗余。computed是可缓存的,computed中的依赖项没有变化,则computed中的值就不会重新计算。

watch可以监控data中的属性,也可以监控computed中的属性,它比较适合的场景是 一个数据影响多个数据,它不具有缓存性
watch:监测的是属性值, 只要属性值发生变化,都会触发执行回调函数来执行一系列操作。 computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才 会重新计算。
watch和computed的区别:

  • watch监控的变量要么在data中声明,要么是computed属性
  • 计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算 属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性

9. vue-router路由有几种模式?有什么区别?原理是什么?histroy为什么在部署时会无法访问?如何实现导航跳转?如何实现动态添加路由?怎么传参?

前端路由可以实现在不刷新页面的情况下修改页面的某部分组件,相比传统的切换,会给用户更好的体验。
这里就出现一个问题,如何在改变页面URL的情况下避免刷新页面,实现原理主要通过以下两种技术实现的

第一种:利用H5的history API实现
主要通过监听popState事件来感知浏览器地址的变化。借助history.pushState 和 history.replaceState来实现内容的切换,不同之处在于,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录[发布项目时,需要配置下apache]

第二种:利用url的hash实现
我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,路由里的 # 不叫锚点,我们称之为 hash。修改hash路径默认不会刷新页面,只要利用监听哈希值的变化来触发事件 —— hashchange 事件来做页面局部更新

导航跳转的API:

  • this.$router.push(location, onComplete?, onAbort?)
    这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。并且点击 等同于调用 router.push(…)。
  • this.$router.replace(location, onComplete?, onAbort?)
    这个方法不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录,所以,当用户点击浏览器后退按钮时,并不会回到之前的 URL。replace可以避免反复切换页面。
  • this.$router.go(n)
    这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

动态添加路由:

1
router.addRoutes(routes: Array<RouteConfig>)

传参:

1
2
3
this.$router.push({
path: '/describe/${id}'
})

对应的路由配置:

1
2
3
4
5
{
path: '/describe/:id',
name: 'Describe',
component: Describe
}

10. 如何避免样式互相污染?scoped的原理是什么?如何做到样式穿透?

避免污染:css没有局部样式的概念,vue脚手架通过实现了,即在style标签上添加scoped
scoped的实现原理:vue通过postcss给每个dom元素添加一个以data-开头的随机自定义属性实现的
样式穿透:
全局穿透可以使用!important
局部穿透可以使用deep
css、sass和less的穿透符号都不同

  • css

    1
    2
    3
    .conBox >>> .el-input__inner{
    padding:0 10px;
    }
  • sass

    1
    2
    3
    .conBox ::v-deep .el-input__inner{
    padding:0 10px;
    }
  • less

    1
    2
    3
    .conBox /deep/ .el-input__inner{
    padding:0 10px;
    }

11. 路由懒加载怎么使用?如何做到按组分块?它的原理是什么?解决了什么问题?

使用:在引入组件的时候使用箭头函数

1
const User = () => import('./components/User')

按组分块:给同组的组件定一个webpackChunkName,如:

1
2
3
const UserDetails = () => import(/* webpackChunkName: "group-user" */  './components/UserDetails')
const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

原理:在打包的时候将js文件分开打包

解决问题:vue路由懒加载主要解决打包后文件过大的问题,事件触发才加载对应组件中的js

12. 什么是导航守卫?有哪些API?可以用来做什么事情?

vue路由钩子是在路由跳转过程中拦截当前路由和要跳转的路由的信息,有三种路由钩子:

  • 全局路由钩子

    1
    beforeEach(to,from,next) {   }
  • 路由独享的钩子

    1
    beforeEnter(to,from,next) {   }
  • 组件内的钩子

    1
    2
    3
    beforeRouteEnter(to,from,next) {  }
    beforeRouteUpdate(to,from,next) { }
    beforeRouteLeave(to,from,next) { }

适用场景:动态设置页面标题,判断用户登录权限等:代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//全局路由导航守卫
vueRouter.beforeEach(function (to, from, next) {
const nextRoute = [ 'detail'];
const auth = sessionStorage.getItem("username");
let FROMPATH = from.path;
//跳转至上述3个页面
if (nextRoute.indexOf(to.name) >= 0) {
//上述数组中的路径,是相当于有权限的页面,访问数组列表中的页面就应该是在登陆状态下
if (!auth) {
let params = Object.assign({frompath:FROMPATH},from.query);
next({path: '/newlogin',query:params});
}
}
//已登录的情况再去登录页,跳转至首页
if (to.name === 'newlogin') {
if (auth) {
// vueRouter.push({name: 'index'});
next({path: '/'});
}
}
next();
});

13. 如何高质量封装axios组件?什么是axios拦截器?有哪些API?实用场景在哪?如何取消发送的请求?

封装axios主要关注一下几个方面:

  1. 创建实例
    创建实例的同时需要根据不同环境为axios配置不同的后台API地址,例如开发环境、生产环境、测试环境等。
    并且配置请求超时的时间
  2. 拦截request和response
    在request中添加用户Token,开启loading效果,删除重复的请求
    在response中关闭loading效果,清除请求缓存,根据返回的code处理错误信息。
  3. 处理异常状态码,超时异常,网络异常

其中删除重复请求是借助axios.CancelToken来实现的,这个CancelToken会向回调函数传入一个cancel函数作为参数,当调用这个cancel函数时,就可以取消掉之前的请求,因此只需要将cancel缓存起来即可。

具体的封装代码可以参考:axios封装实例

拦截器:
用于在发送请求之前,跟接收到回复之后处理相关逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 拦截发送请求
axios.interceptors.request.use(config => {
// doSomeThing
return config
},
error => {
// doSomeThing
}
)
// 拦截接收请求
axios.intercepters.response.use(response => {
// doSomeThing
},
error => {
// doSomeThing
}
)

取消重复请求:
在axios有两种方案

  • 使用CancelToken生成取消令牌和取消方法cancel

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const CancelToken = axios.CancelToken
    const source = CancelToken.source()

    axios.get('/user/12345', {
    cancelToken: source.token
    }).catch(function(thrown) {
    if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message)
    }
    else {
    // handle error
    }
    })

    axios.post('/user/12345', {
    name: 'new Name'
    }, {
    cancelToken: source.token
    })

    source.cancel('取消请求')
  • 通过构造CancelToken生成取消函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const CancelToken = axios.CancelToken
    let cancel

    axios.get('/user/12345', {
    cancelToken: new CancelToken(function executor(c) {
    cancel = c
    })
    })

    cancel()

封装的时候可以利用Map将url跟cancel函数绑定在一起。
然后在拦截request的时候统一取消掉上一次请求,再重新添加到Map里面。
被取消的请求会被axios的response的error接收到,可以在那里进行相关逻辑处理。
最后在切换路由的时候需要清楚掉所有请求

实用场景:

  • 在需要用户验证的请求中,给请求头添加上Token
  • 增加Loading效果

14. 什么是虚拟DOM?key的原理?diff算法?v-for循环为什么一定要绑定key?key为什么不能是index?

在现实开发中,如果频繁去操作真实DOM,其开销是巨大的。
比如说反复修改一百次一个DOM节点的文本内容,如果每次都去操作真实DOM,那就会造成不必要的性能开销。因为其实真正应用到真实DOM的文本内容只有最后一次。
这个时候如果使用虚拟DOM来接收这些操作,那其实就只是将VNode对象中的text属性修改一百次,这样的性能开销基本可以小到忽略不计。
最后将虚拟DOM的最终结果应用到真实DOM上面去,这远比操作一百次真实DOM来的更有效率一些。
在实际使用中,虚拟DOM更多时候是配合diff算法来优化性能的,以应对更复杂的DOM操作场景。

接着说说diff算法,diff算法是一种对比算法,它能够高效率的比较出新旧两颗虚拟DOM树之间的差异,最后将差异的部分应用到真实DOM上面去 。
目前最主流的diff算法是snabbdom算法,像Vue用的就是改进版的snabbdom算法。

具体diff算法的实现可以参考虚拟DOM和diff算法的原理和实现

v-for循环绑定key的原因:
页面上的标签都对应具体的虚拟dom对象(虚拟dom就是js对象), 循环中 ,如果没有唯一key , 页面上删除 一条标签, 由于并不知道删除的是那一条! 所以要把全部虚拟dom重新渲染, 如果知道key为x标签被删除 掉, 只需要把渲染的dom为x的标签去掉即可!

key为什么不能是index:
现在有abcd四个组件,通过index定义的key就是1234。
这时删除掉c,变成abd,理论上说key应该是124,但实际上变成了123。这个d的key改变了,所以d会被重新创建。
如果d后面还有两千个组件,那么这些组件都会重新创建,这样会让性能的开销变得特别大。

15. 什么是mixins?实用场景在哪?混入策略?

mixins混入主要用来分发Vue组件中可复用的功能。
实用场景:
购物车的角标刷新。在其他页签打开时和修改购物车选中状态时都需要刷新购物车页签的角标。

混入策略:

  • 覆盖
    包括:data、props、computed、methods
    优先级:组件本身data > 组件mixin data > 组件mixin嵌套的mixin data > 全局mixin data
    前者会覆盖后者的值
  • 继承
    包括:钩子函数、watch、component、directives、filters
    优先级:全局mixin hook -> 组件mixin嵌套的mixin hook -> 组件mixin hook -> 组件本身mixin hook 这个顺序执行。
    按照优先级执行各个mixin的逻辑

16. Vue的自定义指令有哪些(v开头的)?分别有什么作用?常用的修饰符有哪些?如何使用directives来自定义新的指令?有哪些实用的自定义指令?自定义指令的生命周期?

系统自带的指令:
v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
v-show:根据表达式之真假值,切换元素的 display CSS 属性。
v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key值 使用。
v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
v-model:实现表单输入和应用状态之间的双向绑定
v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

常用的修饰符:
v-on 指令常用修饰符:
.stop - 调用 event.stopPropagation(),禁止事件冒泡。
.prevent - 调用 event.preventDefault(),阻止事件默认行为。
.capture - 添加事件侦听器时使用捕获模式,在捕获阶段触发事件
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
.native - 监听组件根元素的原生事件。
.once - 只触发一次回调。
.left - (2.2.0) 只当点击鼠标左键时触发。
.right - (2.2.0) 只当点击鼠标右键时触发。
.middle - (2.2.0) 只当点击鼠标中键时触发。
.passive - (2.3.0) 以 { passive: true } 模式添加侦听器
注意: 如果是在自己封装的组件或者是使用一些第三方的UI库时,会发现并不起效果,这时就需要用`·.native修饰符了,如:

1
2
3
4
5
6
7
//使用示例:
<el-input
v-model="inputName"
placeholder="搜索你的文件"
@keyup.enter.native="searchFile(params)"
>
</el-input>

v-bind 指令常用修饰符:
.prop - 被用于绑定 DOM 属性 (property)。(差别在哪里?)
.camel - (2.1.0+) 将 kebab-case 特性名转换为 camelCase. (从 2.1.0 开始支持)
.sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。

v-model 指令常用修饰符:
.lazy - 当光标离开输入框时,才改变数据
.number - 输入字符串转为数字
.trim - 输入首尾空格过滤

自定义新的指令:
自定义指令创建有全局自定义指令和局部自定义指令
全局自定义指令:Vue.directive(‘指令名’,{ inserted(el) { } })
局部自定义指令:directives:{ }

钩子函数:

  • bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
  • inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
  • update: 元素更新时,子元素尚未更新,将会调用这个钩子
  • componentUpdated: 一旦组件和子级更新,就会调用这个钩子
  • unbind: 只调用一次, 指令与元素解绑时调用。

实用的自定义指令:

  • v-copy
  • v-longpress
  • v-permission
  • v-LazyLoad
  • v-debounce
  • v-emoji
  • v-watermaker
  • v-draggable

17. 什么是长列表?长列表会导致什么问题?如何优化?什么是虚拟列表?如何实现?

无法用分页加载的超长列表,就是长列表。长列表会导致渲染时间过长,浏览器卡顿,屏幕出现空白等问题。
要解决这个问题,目前有两种方案:

  • 分片加载(懒渲染)
    无限滚动核心思路就是通过检测scrollTop(滑动条到滚动条顶部的距离)+ clientHeight(页面可视高度)到达scrollHeight(滚动内容的高度),即到达滚动的底部,就加载新的数据

  • 虚拟列表
    只渲染可视部分的列表,随着滚动渲染新的数据。
    虚拟列表主要有几个要素:

    • 用来撑起整个滚动高度的div,高度为数据的总长度*item高度
    • 一个用来展示可视区域的div
    • 一个保存可视部分数据的visibleData,配合v-for即可在数据变化时渲染新的列表视图
    • 一个记录start用来记录数据开始的索引,一个end用来记录数据结束的索引
    • 监听滚动事件,滚动过程中不停刷新visibleData,并偏移可视区域的div
    • 为了避免一下滚动到地步

    如果想自定义条目内容,可以用slot
    滚动时可以使用

    问题: iOS 上 UIWebView 的 onscroll 事件并不能实时触发。会出现滚动时体验不佳的问题(会有白屏时间)。

18. $refs是什么?有什么用?

$refs是ref的集合,用于获取在标签中定义ref属性的组件,可以跨过MVVM模型来直接操作视图。

19. 怎么使用Vue的过滤器?

vue过滤器主要用于对渲染出来的数据进行格式化处理。
例如:后台返回的数据性别用0和1表示,但渲染到页面上不能是0和1我得转换为“男“和”女”,这时就会用到过滤器
还有商品价格读取出来的是普通数值,例如:230035,但我要在前面加个货币符号和千分分隔等,例如变成:¥230,035,都得需要vue过滤器

如何创建过滤器呢,跟创建自定义指令类似,也有全局和局部过滤器的形式

  • 全局过滤器:
1
2
3
4
5
Vue.filter(‘过滤器名’,function(参数1,参数2,…) {

//………..
return 要返回的数据格式
})
  • 局部过滤器:在组件内部添加filters属性来定义过滤器
1
2
3
4
5
6
7
fitlers:{
过滤器名(参数1,参数2,,…参数n) {
//………..
return 要返回的数据格式

}
}

20. 说一下vue最大特点是什么或者说vue核心是什么

答:vue最大特点我感觉就是“组件化“和”数据驱动“
组件化就是可以将页面和页面中可复用的元素都看做成组件,写页面的过程,就是写组件,然后页面是由这些组件“拼接“起来的组件树
数据驱动就是让我们只关注数据层,只要数据变化,页面(即视图层)会自动更新,至于如何操作dom,完全交由vue去完成,咱们只关注数据,数据变了,页面自动同步变化了,很方便

21. Vue 组件中 data 为什么必须是函数

因为一个组件是可以共享的,但他们的data是私有的,所以每个组件都要return一个新的data对象,返回一个唯一的对象,不要和其他组件共用一个对象。

22. 说一下vue封装组件中的slot作用

slot是一个用来占位的插槽,在子组件中定义一个slot,就可以在父组件中自定义slot的内容。这可以让组件的复用性大大提高
slot分为具名的slot和匿名的slot
具名slot就是在slot标签中添加一个name属性,然后在父组件中通过v-slot:name或#name来使用插槽
也可以直接使用name标签
另外如果是用循环渲染出来的列表,想要在单个项中获取到数据,就需要用到作用域插槽,使用方法就是给v-slot:name赋值{row},这样就可以从row里面获取对应的数据。

23. 说一下vue转场动画如何实现的

答:vue转场动画主要通过vue中的提供的transition组件实现的,例如:

1
2
3
<transition name=”名称”>
<router-view></router-view>
</transition>

其中name为转场的名称,自己定义,可通过定义进入和离开两种转场动画,格式为:

1
2
3
4
5
6
.名称-enter {   }  //将要进入动画
.名称-enter-active { } //定义进入的过程动画
.名称-enter-to { } //定义已经进入的动画
.名称-leave { } //将要离开的动画
.名称-leave-active { } //定义离开过程中的动画
.名称-leave-to { } //定义已经离开的动画

24. 说一下nextTick的作用和使用场景?

vue中的nextTick主要用于处理数据动态变化后,DOM还未及时更新的问题,用nextTick就可以获取数据更新后最新DOM的变化

适用场景:

  • 有时需要根据数据动态的为页面某些dom元素添加事件,这就要求在dom元素渲染完毕时去设置,但是created与mounted函数执行时一般dom并没有渲染完毕,所以就会出现获取不到,添加不了事件的问题,这回就要用到nextTick处理
  • 在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法,例如:应用滚动插件better-scroll时
  • 比如在选择商品规格的时候需要展示商品的分类,但是商品的词条有长有短,为了排版更好看,我们根据渲染出来的li宽度进行排版优化。但是在刚拿到数据进行渲染的时候,这个li宽度是拿不到的,因此需要用到nextTick,等待li渲染完之后才拿到它的宽度。


  • 数据改变后获取焦点

  • 我们先来看这样一个场景:有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是 false,直接去获取div内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为 true,div才会被创建,此时再去获取,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
el : "#app",
data:{
showDiv : false
},
methods:{
getText:function(){
this.showDiv = true;
var text = document.getElementById('div').innnerHTML;
console.log(text);
}
}
})
</script>

这段代码并不难理解,但是运行后在控制台会抛出一个错误:Cannot read property ‘innnerHTML of null,意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。
异步更新队列
Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的所以数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。
知道了Vue异步更新DOM的原理,上面示例的报错也就不难理解了。事实上,在执行this.showDiv = true时,div仍然还是没有被创建出来,直到下一个vue事件循环时,才开始创建。$nextTick就是用来知道什么时候DOM更新完成的,所以上面的示例代码需要修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
el : "#app",
data:{
showDiv : false
},
methods:{
getText:function(){
this.showDiv = true;
this.$nextTick(function(){
var text = document.getElementById('div').innnerHTML;
console.log(text);
});
}
}
})
</script>

这时再点击事件,控制台就打印出div的内容“这是一段文本“了。
理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如 popper.js、swiper等,这些基于原生javascript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。

25. v-for 与 v-if 的优先级

当它们处于同一节点,v-for的优先级比v-if更高,这意味着 v-if将分别重复运行于每个 v-for循环中。当你想为仅有的一些项渲染节点时,这种优先级的机制会十分有用,如下:

1
2
3
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>

上面的代码只传递了未完成的 todos。
而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>)上

26. 知道lodash吗?它有哪些常见的API ?

Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。
_.cloneDeep 深度拷贝
_.reject 根据条件去除某个元素。
_.drop(array, [n=1] ) 作用:将 array 中的前 n 个元素去掉,然后返回剩余的部分.
防抖和节流

27. 什么是模板编译?原理是什么?

模板编译有点类似于代码的编译器,主要用于将HTML解析成AST树,方便后续Vue对网页内容进行操作。

模板编译模块主要分为三个部分:parser(解析器)、optimizer(优化器)和code generator(代码生成器)

其中:
parser(解析器)用于解析HTML、模板变量和属性,最终生成AST树
optimizer(优化器)用于标记静态节点,被标记的静态节点不会参与重新渲染,达到优化性能的目的
code generator(代码生成器)会将AST树拼装成一段以“with(this)”开头的字符串,把它交给JS引擎执行就能生成对应的虚拟DOM

模板编译的核心要点:

  • 通过parse函数生成AST树,通过optimize函数标记静态节点,通过generate函数生成代码字符串
  • 在parse函数中调用了parseHTML来解析HTML
  • parseHTML将会借助正则表达式来分别匹配开始标签、文本内容以及结束标签。
  • 这里的开始标签匹配到之后,会push到stack中,之后在处理结束标签的时候,找到开始标签并出栈
  • 在处理开始标签、文本标签和结束标签时,最终会将结果返回给回调函数start、chars、end
  • 在回调函数中会将节点转换成ASTElement,最后生成AST树,返回给外部
  • 生成AST树之后,就交给optimize进行优化。
  • optimize会标记所有AST树静态节点,接着标记所有静态根节点
  • 标记完成之后,会将AST树交给generate生成代码字符串
  • 这些代码字符串会被保存在render函数中
  • 当监听器Watcher监听到数据变化,就会调用render函数生成虚拟节点,然后通过对比新旧虚拟节点之间的区别,将最新的变化应用到真实DOM上面去

具体代码参考Vue的模板编译原理及源码解析


八、小程序


1. 简单描述下微信小程序的相关文件类型?

微信小程序项目结构主要有一下几个文件类型,如下
1、WXML (WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构 建出页面的结构。内部主要是微信自己定义的一套组件。
2、WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式,
3、js 逻辑处理,网络请求
4、json 小程序设置,如页面注册,页面标题及 tabBar。
5、app.json 必须要有这个文件,如果没有这个文件,项目无法运行,因为微信框架把这个作为配置文 件入口,整个小程序的全局配置。包括页面注册,网络设置,以及小程序的 window 背景色,配置导航 条样式,配置默认标题。
6、app.js 必须要有这个文件,没有也是会报错!但是这个文件创建一下就行 什么都不需要写以后我 们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量。

2. uni-app的生命周期?

  • onLoad:首次页面加载时触发,传入参数是通过路由传入的路径参数
  • onShow:加载完成后,后台切换为前台以及重新进入页面时触发
  • onReady:页面首次渲染完成时触发
  • onHide:从前台切到后台,或进入其他页面时触发
  • onUnload:页面卸载时触发
  • onPullDownRefresh:监听用户下拉时触发
  • onReachBottom:页面上拉触底时触发
  • onShareAppMessage:点击小程序右上角分享时触发

3. 什么是分包?

参考2-分包

类似于路由的懒加载,会将指定的页面分开打包

4. 什么是条件编译?有哪些平台?

根据不同平台执行不同的逻辑,使用方法

1
2
3
#ifdef 平台名

#endif

平台:

平台
APP-PLUS 5+APP
H5 H5
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
MP-TOUTIAO 头条小程序
MP-BAIDU 百度小程序
MP-QQ QQ小程序
MP 所有MP前缀的平台

5. uni-app有哪些优缺点?

  • 优点
    • 一套代码生成多端
    • 基于vue,学习成本低
    • 有自家的IDE:Hbuilder,编译起来很方便
  • 缺点
    • 问世时间短,很多不完善
    • 社区小
    • 官方反馈不及时
    • 文件命名受限
    • 对安卓的支持比iOS和小程序差
    • 每次更新版本都不太稳定

6. 什么是rpx?与rem有什么区别?

rpx是小程序独有的尺寸单位,它把屏幕分成750份,每一份代表1px
而rem是根据根元素的fontSize来确定大小的。

7. 怎么使用上拉和下拉加载功能?

在package.json的对应页面上开启以下配置:

1
2
3
4
"style" : {
"onReachBottomDistance": 150, // 触底距离
"enablePullDownRefresh": true, // 开启下拉加载
}

然后监听勾子函数onReachBottom和onPullDownRefresh即可


九、前端工程化


1. 什么是webpack?如何使用webpack?webpack和vite的区别?loader和plugin的区别?babel是什么?

我们公司用的vue官方的脚手架(vue-cli),vue-cli版本有3.0
webpack是一个前端模块化打包构建工具,vue脚手架本身就用的webpack来构建的,webpack本身需要的入口文件通过entry来指定,出口通过output来指定,默认只支持js文件,其他文件类型需要通过对应的loader来转换,例如:less需要less-loader,sass需要sass-loader,css需要style-loader,css-loader来实现。当然本身还有一些内置的插件plugin来对文件进行压缩合并等操作

babel可以帮助我们转换一些当前浏览器不支持的语法,它会把这些语法转换为低版本的语法以便浏览 器识别。

webpack是先打包再启动开发服务器,vite是直接启动开发服务器,然后按需编译依赖文件。

2. webpack 中的 entry 是做什么的?

entry: 用来写入口文件,它将是整个依赖关系的根。当我们需要多个入口文件的时候,可以把 entry 写成一个对象。

1
2
var baseConfig = { entry: {
main: './src/index.js'} }

3. webpack 中的 output 是做什么的?

output: 即使入口文件有多个,但是只有一个输出配置,如果你定义的入口文件有多个,那么我们需要使用占位符来确保输出文件的唯一性。

1
2
3
4
5
6
7
8
9
var baseConfig = { 
entry: {
main: './src/index.js' },
output: {
filename: '[name].js',
path: path.resolve('./build')
}
}
module.exports = baseConfig

4. webpack 中的 Loader 的作用是什么?

1、实现对不同格式的文件的处理,比如说将 scss 转换为 css,或者 typescript 转化为 js
2、转换这些文件,从而使其能够被添加到依赖图中 这里介绍几个常用的 loader:
babel-loader: 让下一代的 js 文件转换成现代浏览器能够支持的 JS 文件。
babel 有些复杂,所以大多数都会新建一个.babelrc 进行配置 css-loader,style-loader:两个建议配合使用,用来解析 css 文件,能够解释@import,url()如果需要解析 less 就 在后面加一个 less-loader
file-loader: 生成的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名
url-loader: 功能类似 file-loader,但是文件大小低于指定的限制时,可以返回一个 DataURL 事实上,在使用 less,scss,stylus 这些的时候,npm 会提示你差什么插件,差什么,你就安上就行了

5. webpack 中的 Plugins 的作用是什么?

loaders 负责的是处理源文件的如 css、jsx,一次处理一个文件。而 plugins 并不是直接操作单个文件,它直
接对整个构建过程起作用
1、ExtractTextWebpackPlugin: 它会将入口中引用 css 文件,都打包都独立的 css 文件中,而不是内嵌在 js 打包文件中。
2、HtmlWebpackPlugin:依据一个简单的 index.html 模版,生成一个自动引用你打包后的 js 文件的新 index.html。
3、HotModuleReplacementPlugin: 它允许你在修改组件代码时,自动刷新实时预览修改后的结果注意永远 不要在生产环境中使用 HMR。

6. webpack 中什么是 bundle,什么是 chunk,什么是 module?

1、bundle 是由 webpack 打包出来的文件,
2、chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块。 3、module 是开发中的单个模块。
4、chunk 打包后变成 bundle.

7. webpack-dev-server 和 http 服务器如 nginx 有什么区别?

webpack-dev-server 使用内存来存储 webpack 开发环境下的打包文件,并且可以使用模块热更新,他比传统
的 http 服务对开发更加有效。

8. 什么是长缓存?在 webpack 中如何做到长缓存优化?

浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升 级或者更新,都需要浏览器去下载新的代码,最方便和最简单的更新方式就是引入新的文件名称。在 webpack 中, 可以在 output 给出输出的文件制定 chunkhash,并且分离经常更新的代码和框架代码,通过 NamedModulesPlugin 或者 HashedModulesIdsPlugin 使再次打包文件名不变。


十、数据结构和算法


1. 基本数据结构有哪些?

  1. 数组
  • 数组是最基本的数据结构,使用一块连续的内存空间保存数据
  1. 队列
  • 是一种先入后出的逻辑结构,对于元素的操作分别在队头和队尾,元素插入在队尾,元素的删除在队头。
  1. 链表
  • 存储的数据在地址空间上可连续,可不连续,链表中每一个节点包括数据和指向下一个地址的指针,查找数据的时间复杂度O(n),方便数据增删
  • 栈是一种先入后出的逻辑结构,每次加入新的元素和拿走元素都在顶部操作
  1. 二叉树
  • 每个节点至多只有两个子树的结构,在父节点中有指向左右子节点的指针

  • 先序遍历:根-左-右,中序遍历:左-根-右,后序遍历:左-右-根

  • 查找二叉树:左子树的值小于根节点的值,右子树的值大于根节点的值,在插入数据时,从根节点开始往下比较,小于比较值则放在左边,大于比较值就放在右边。插入一个值的时间复杂度是O(logn)

  • 平衡二叉树:左右子树的高度差绝对值不超过1

  1. 哈希表

十一、兼容性


1. 什么是滚动穿透?出现滚动穿透的原因?如何解决?

滚动穿透是移动端常见的兼容性问题,主要表现为设置遮罩层之后,滚动页面,遮罩层底下的内容也会跟着滚动,滚动到底部或顶部时,还会出现拖拽空白的部分。
出现滚动穿透的原因是scroll事件本身的规范问题,它会在目标层无法滚动的时候,尝试在document上面进行滚动。
根据不同情况有以下三种解决方案:

  • 让document不能滚动
    当遮罩层不能滚动时,可以采用这个方案
    定义一个class,设置overflow为hidden,在出现遮罩层的时候给html或body加上这个class
1
2
3
4
.modal--open {
height: 100%;
overflow: hidden;
}

这个方案有个问题,那就是设置hidden之后,底下的内容会滚动回顶部,如果想要在关闭遮罩层之后保持滚动进度,那就还需要记录下位置。

  • 阻止遮罩层的滚动事件
    如果遮罩层是半透明的,那么上一个方案就不适合了。这个时候就可以通过阻止touch move的默认事件,来阻止document的滚动事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<div class="mask">mask</div>
<div class="dialog">dialog</div>
</div>
const mask = document.querySelector('.mask')
const dialog = document.querySelector('.dialog')
const preventTouchMove = $el => {
$el.addEventListener(
'touchmove',
e => {
e.preventDefault()
},
{ passive: false }
)
}
preventTouchMove(mask)
preventTouchMove(dialog)

由于在 Chrome 56 开始将会默认开启 passive event listener 所以不能直接在 touch 事件中使用 preventDefault,需要先将 passive 选项设置为 false 才行。

  • 遮罩层可滚动,解决滚动到极限时的空白区域问题
    当遮罩层可以滚动时,只有滚动到底部或者顶部时,才会滚动底下的内容,所以这个时候可以监听滚动的位置,到顶/底时阻止滚动事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const modalHeight = $modal.clientHeight
const modalScrollHeight = $modal.scrollHeight
let startY = 0

$modal.addEventListener('touchstart', e => {
startY = e.touches[0].pageY
})

$model.addEventListener('touchmove', e => {
let endY = e.touches[0].pageY
let delta = endY - startY

if (($modal.scrollTop === 0 && delta > 0) ||
($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)) {
e.preventDefault()
}
},
{ passive: false })

2. ios上300ms的click问题

原因:2007年初,苹果公司在发布首款iPhone前夕,遇到了一个问题——当时的网站都是为大屏幕设备设计的。于是苹果的工程师做了一些约定,应对iPhone这种小屏幕浏览桌面端站点的问题。这当中最出名的,当属双击缩放(double tap to zoom)。这也是上述会有300毫秒延迟的原因。

当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是要进行单击操作。因此,iOS Safari就等待300毫秒,以判断用户是否再次点击

解决:
在meta中通过设置user-scalabel=no禁止双击缩放
或者使用FastClick,FastClick是FTLabs专门为解决移动端浏览器300毫秒点击延迟问题所开发的一个轻量级的库。

实现原理:
FastClick在检测到touchend事件后,先阻止iOS默认的点击事件,然后自己通过document.createEvent创建一个MouseEvent,最后通过targetElement.dispatchEvent触发这个MouseEvent

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var targetElement = null

document.body.addEventListener('touchstart', e => {
targetElement = e.target
})

document.body.addEventListener('touchend', e => {
e.preventDefault()
var touch = e.changedTouches[0]
clickEvent = document.createEvent('MouseEvents')
clickEvent.initMouseEvent('click', true, true, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null)
clickEvent.forwardedTouchEvent = true
targetElement.dispatchEvent(clickEvent)
})

另外点击穿透也是由于下层点击事件存在300ms延迟的原因,可以按上面的方法解决

3. ios安全区域如何处理?如何识别刘海屏?

从iPhoneX开始,苹果就开始使用刘海屏。为了让显示内容不会被刘海挡住,就引入了一个概念,即安全区域(Safe Area)。
如果没有特殊需求,内容默认会显示在安全区域内。
但当我们想让内容充满整个屏幕的时候,就需要做特殊的设置,这个时候苹果就引入了一个新的概念——viewport-fit,它的作用是设置可视区域的尺寸。
这个viewport-fit一共有三个值:auto、contain和cover。我们可以在meta中设置

1
<meta name="viewport" content="viewport-fit=cover" />

默认值是contain,也就是显示内容在安全区域内。而auto默认情况下也是指向contain的。不过这个模式下安全区域外的部分可能会难看的边框。
而当设置为cover时,显示内容将充满整个屏幕。这个时候为了不让显示的内容被刘海屏跟底下的虚拟Home键挡住。就需要借助iOS11引入的两个CSS函数evn和constant了。
constant在iOS < 11.2的版本中有效,而env在iOS > 11.2的版本中有效。所以平时都是两个一起使用的

1
2
3
4
body {
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: constant(safe-area-inset-bottom);
}

识别刘海屏:
首先当然是通过UserAgent确认当前设备的iPhone。
接着查看资料发现目前只有iPhone XS Max、iPhone XS、iPhone XR、iPhone X系列手机有刘海屏。而它们的dips只有两类,即(375,812)跟(414,896)。所以只需要判断screen的width和height是否匹配这两个尺寸即可。

4. 为什么不同设备的1px边框看起来厚度不同?如何解决这个问题?

因为CSS中的px是根据dpr(设备像素比)来的。
dpr则是dp(物理像素)跟dip(设备独立像素)的比值。
当dpr为2的时候,实际上是用两个像素点来渲染1px的边框。这就导致了不同dpr设备的1px边框厚度不同。

解决方案:

  • viewport + rem
    这个方案就是用 1 / dpr将原来的dpr抵消掉,具体实现:

    1
    2
    3
    4
    5
    6
    7
    8
    const scale = 1 / window.devicePixelRatio
    const viewport = document.querySelector('meta[name="viewport"]')
    if (!viewport) {
    viewport = document.createElement('meta')
    viewport.setAttribute('name', 'viewport')
    window.document.head.appendChild(viewport)
    }
    viewport.setAttribute('content', 'width=device-width, user-scalable=no, initial-scale=' + scale + ', maximun-scale=' + scale + ', minimun-scale=' + scale)

    这个方案需要在一开始立项的时候使用,否则会影响到其他元素的布局。

  • 伪类 + transform
    通过scale和@media来将边框缩放到1物理像素的效果,实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    .border-1px::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    transform-orgin: left top;
    border: 1px solid #000;
    -webkit-transform-origin: left top;
    }

    @media only screen and (min-device-pixel-ratio: 3), (-webkit-min-device-pixel-ratio: 3) {
    .border-1px::before {
    -webkit-transform: scale(.33)
    transform: (.33)
    }
    }

    @media only screen and (min-device-pixel-ratio: 2), (-webkit-min-device-pixel-ratio: 2) {
    .border-1px::before {
    -webkit-transform: scale(.5)
    transform: (.5)
    }
    }

5. 在ios键盘中首字母大写的问题?

1
<input type="text" autocapitalize='off'>

6. 移动端h5底部输入框被键盘遮挡问题

h5页面有个很蛋疼的问题就是,当输入框在最底部,点击软键盘后输入框会被遮挡。可采用如下方式解决

1
2
3
4
5
6
7
8
9
10
11
var oHeight = $(document).height(); //浏览器当前的高度 
$(window).resize(
function() {
if($(document).height() < oHeight){
$("#footer").css("position","static");
}
else{
$("#footer").css("position","absolute");
}
}
);

11. 消除 transition 闪屏

1
2
-webkit-transform-style: preserve-3d;     /*设置内嵌的元素在 3D 空间如何呈现:保留 3D*/
-webkit-backface-visibility: hidden; /*(设置进行转换的元素的背面在面对用户时是否可见:隐藏)*/

14. android下取消输入语音按钮

1
input::-webkit-input-speech-button {display: none}

15. fixed定位缺陷

ios下fixed元素容易定位出错,软键盘弹出时,影响fixed元素定位
android下fixed表现要比iOS更好,软键盘弹出时,不会影响fixed元素定位
ios4下不支持position:fixed
解决方案: 可用iScroll插件解决这个问题

16. input 的placeholder会出现文本位置偏上的情况

input的placeholder会出现文本位置偏上的情况:PC端设置line-height等于height能够对齐,而移动端仍然是偏上,解决是设置line-height:normal

17. android里line-height不居中

把字号内外边距等设置为需求大小的2倍,使用transform进行缩放。(不适用)
把字号内外边距等设置为需求大小的2倍,使用zoom进行缩放,可以完美解决。
把line-height设置为0,使用padding值把元素撑开,说是可以解决(不适用)。

19. ios日期转换NAN问题

具体就是,new Date(‘2020-11-12 00:00:00’)在ios中会为NAN
解决方案:用new Date(‘2020/11/12 00:00:00’)的日期格式,或者写个正则转换

20. 禁止数字识别为电话号码

1
<meta name = "format-detection" content = "telephone=no">

21. H5页面窗口自动调整到设备宽度,并禁止用户缩放页面

1
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,er-scalable=no">

22. 忽略Android平台中对邮箱地址的识别

1
<meta name="format-detection" content="email=no">

十二、性能优化


1. 常见的性能优化手段有哪些?

  • 加载优化

    1. 压缩代码
      Vue生成的代码默认就通过去掉空格来压缩代码
      开启gzip,有两种方案,一种是在Vue项目中安装compression-webpack-plugin插件,另一种是在nginx中开启gzip

    2. 代码分割(code spliting)
      具体做法就是在Webpack.config.js中配置CommonChunkPlugin,将第三方库单独打成一个vendor.bundle.js。
      配置如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      entry: {
      bundle: './src/main.js',
      vendor: ['./src/lib/jquery-1.10.2.min.js', './src/lib/response.min.js']
      },
      plugins: [
      new webpack.optimize.CommonChunkPlugin({
      name: 'vendor',
      filename: 'vendor.bundle.js'
      })
      ]

      最后在index.html中引入即可

    3. 第三方模块放在CDN
      用外链引用css和js,可以有效减少HTML体积,并且外链之后,css和js作为静态资源可以给他设置合适的缓存的响应头,能够利用浏览器的缓存

    4. 小模块适度合并,将一些零散的小模块合并一起加载,速度较快
      使用webpackChunkName合并打包

    5. 使用defer、async或type=”module”对js脚本进行异步加载

    6. 使用prefetch和preload对css资源进行预加载

      • prefetch
        prefetch的加载优先级比较低,浏览器只有在空闲的时候才会在后台加载这类资源
        用法:
        1
        <link ref="prefetch" />
        实用场景:
        • 加载大多数页面要用到的图片
        • 加载轮播图中即将展示的图片
        • 提前加载搜索结果的下一页
          缺点:prefetch不被safari支持
      • preload
        preload是告诉浏览器哪些资源比较重要,需要提前加载。
        preload是在windown.onload的过程中发起的一个异步加载的请求,因此不会阻塞浏览器的渲染
        推荐使用as来指明资源类型,可以更精确地优化加载的优先级。
        被preload加载的文件不会立即执行,只有在遇到使用该资源的标签,才会执行。
        用法:
        1
        <link ref="preload" />
        实用场景:
        • 加载字体、图片等资源(加载字体资源时必须使用crossorigin属性,否则按匿名CORS资源处理,即请求头不带origin,这会导致缓存的资源无法被识别,出现二次加载的情况)
        • 搭配media属性实现响应式加载
          1
          <link ref="preload" as="style" href="./main.css" media="(min-width: 768px)">

      不要同时使用preload和prefetch,会导致重复加载

    7. 使用dns-prefetch或者preconnect对dns进行预加载

      • dns-prefetch
        DNS请求虽然消耗的带宽少,但延迟却很高,一次DNS查询大概要耗费50~300ms,尤其是在移动端的网络下更加明显。因此对页面中使用到的静态资源域名进行DNS预解析是很有必要的。
        用法:

        1
        <link rel="dns-prefetch" href="//*.com" />

        需要做预解析的域名:

        • 静态资源的域名
        • js中动态加载的链接
        • 重定向的新域名
          不需要做预解析的域名:
        • 页面中的超链接,这些浏览器会自动做dns-prefetch
      • preconnect
        跟dns-prefetch类似,但是preconnect会跟目标域名建立TCP连接和TLS协议
        这个一般用在流媒体上,让视频播放更加顺畅

    8. 数据预获取
      在Vue项目中,数据请求接口一般会用在created钩子中,这就需要等待Vue解析完毕,时间上比较滞后。
      为了加快首屏的数据渲染,我们可以直接利用原生JS写一个ajax,然后利用这个ajax异步加载第一份数据,就可以最大程度提高首屏加载的效率

    9. 路由懒加载或动态加载

  • 图片优化

    1. 小图使用雪碧图,iconFont,base64内联
      雪碧图可以使用background-position来定位
      iconFont和base64可以使用相关工具制作

    2. 图片使用懒加载
      src加载一张通用的占位图,data-src设置真正的图片url,当滚动条滚动到图片附近时,就将data-src的值覆盖到src上面去,这样就实现了图片的懒加载

    3. webp代替其他格式
      用webp压缩过的图片在无损的情况下压缩率有20%~30%,在有损的情况下压缩率高达70%~80%。
      而且压缩之后图片质量降低得没那么明显,至少肉眼上看不到太大的差距,非常适合替代png和jpg类型图片

    4. 图片一定要压缩
      常用的压缩图片的工具:”PNGCrush”、”JPEGTRAN”、”PNGQUANT”

    5. 可以使用的img的srcset
      即响应式图像加载,可以根据不同分辨率显示不同尺寸图片,这样既保证显示效果,又能节省带宽,提高加载速度。
      使用:

      1
      <img src="1400.png" srcset="640.png 640w, 1024.png 1024w, 1400.png 1400w">

      w代表最大宽度

  • css优化

    1. css写在头部
      会跟HTML同步解析,无需重新生成渲染树
    2. 避免css表达式
      不要使用css表达式,css的表达式一方面是兼容性问题,虽然看起来比较强大,但实际性能开销很大,因为它实际的执行效率是远远超出预期的,如果使用css表达式,会导致页面卡顿
    3. 移除空置的css规则
    4. 避免行内style样式
  • js优化

    1. js写在body底部
    2. js用defer放在头部,提前加载时间,又不阻塞dom解析
    3. script标签添加crossorigin,方便错误收集
    4. 事件监听利用冒泡机制,再通过target来区分目标对象
  • 渲染优化

    1. 尽量减少重绘和重排,具体参考后面
  • 首屏优化
    原则:显示快,滚动流畅,懒加载,懒执行,渐进展现

    1. 代码分离,将首屏不需要的代码分离出去
    2. 服务端渲染或预渲染,加载完html直接渲染,减少白屏时间
    3. DNS prefetch,使用dns-prefetch减少dns查询时间,PC端域名发散,移动端域名收敛
    4. 减少关键路径css,可以将关键的css内联,这样可以减少加载和渲染时间
      关键路径:获取关键资源所需的往返次数和时间
  • 打包优化(主要是webpack优化)

    1. 拆包 externals dllPlugin
      • 拆包就是路由懒加载
      • externals排除打第三方包,可以使用CDN加速
      • dllPlugin可以用来把Vue全家桶打成一个包,以后就不需要打包这一部分,又由于生成hash编码不变,缓存起来也提高了效率
    2. 提取公共包 commonChunkPlugin或splitChunks
    3. 缩小范围 各种loader配置include和exclude,noParse跳过文件,可以有效缩短构建包的时间
      exclude可以排除node_modules
      include一般包括src
    4. 开启缓存 各种loader开启cache
    5. 多线程加速 happypack或thead-loader
    6. tree-shaking ES模块分析,移除死代码
    7. Scope Hoisting ES6模块分析,将多个模块合并到一个函数里,减少内存占用,减小体积,提示运行速度
  • webpack长缓存优化

    1. js文件使用chunkhash,不使用hash
    2. css文件使用contenthash,不使用chunkhash,不受js变化影响
    3. 提取vendor,公共库不受业务模块变化影响
    4. 内联webpack runtime到页面,chunkId变化不影响vendor
    5. 保证module Id稳定,不使用数字作为模块id,改用文件内容的hash值,使用HashedModuleIdsPlugin,模块的新增或删除,会导致其后面的所有模块id重新排序,为避免这个问题
    6. 保证chunkhash稳定,使用webpack-chunk-hash,替代webpack自己的hash算法。webpack自己的hash算法,对于同一个文件,在不同开发环境下,会计算出不用的hash值,不能满足跨平台需求。
  • vue优化

    1. 路由懒加载组件
    2. keep-alive缓存组件,保持原显示状态
    3. 列表项添加key,保证唯一,不要用index来作为key的值
    4. 列表项绑定事件,使用事件代理(v-for)
    5. 如果需要用到v-if来决定是否渲染v-for。
    6. 参考下文
  • react优化

    1. 路由组件懒加载,使用react-loadable
    2. 类组件添加shouldComponent或PureComponent
    3. 函数组件添加React.memo
    4. 列表项添加key,保证唯一
    5. 函数组件使用hook优化,useMemo,useCallback
  • SEO优化
    参考下文

  • 请求优化

    1. 缓存axios的请求数据

2. 什么是脚本阻塞?如何解决?defer和async的区别?

当脚本不得不放在head之中时,就可以添加异步加载属性,异步加载属性有两种,一种是defer,一种是async

  • defer延迟加载
    defer在文档解析完成后执行,并且在DOMContentLoaded事件之前执行完成,效果和放在前面一样
    一些动态插入的<script>如polyfill可以使用defer加载
1
<script src="" defer></script>

注意:用了defer不要使用document.write()方法。使用defer时最好不要加载样式信息,因为样式表可能尚未加载,浏览器会禁止脚本等待样式表加载完成,相当于样式表阻塞脚本加载

  • type=”module”延迟加载
    使用了ES6 Module的导入语法,可以一次性加载多个脚本,表现个defer差不多,但由于是ES6,兼容性差了一点
1
2
3
<script>
import { app } from './math.js'
</script>
  • async异步加载

async异步加载就是告诉浏览器不必等到加载完外部文件,可以边渲染边下载,什么时候渲染完什么时候执行。
适用于广告,数据分析等独立于页面的模块功能。

1
<script type="text/javascript" src="a.js" async></script>

区别:
defer会在DOM树完成后再执行,async会在加载完成后立即执行。

优先级:
当同时设置defer和async属性时,只会触发async,不会触发defer,只有async不被兼容时才会触发defer。

3. 重绘、重排(回流)是什么?它们如何运用在性能优化上?如何分析渲染时间?

重绘:元素外观的改变所触发的浏览器行为,例如改变visibility、outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,并不一定伴随重排。

重排:是更明显的一种改变,可以理解为渲染树需要重新计算。将对性能产生很大影响。

下面是常见的触发重排的操作:

  • 添加或删除DOM元素
  • 改变元素的位置、尺寸(margin、padding、边框、宽高)、内容(文字数量、图片大小、字体大小)
  • 改变浏览器窗口尺寸,触发resize函数
  • 激活CSS伪类(如:hover)
  • 设置style属性的值
  • 有些属性和方法为了即时性和准确性都需要重新计算,所以会触发重排
    • 属性
      width、height、margin、padding、display、border-width、border、position、overflow、font-size、vertical-align、min-height、clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft
    • 方法
      scrollIntoView()、scrollTo()、getComputedStyle()、getBoundingClientRect()、scrollIntoViewIfNeeded()

重排优化:

  • 减少重排范围
    • 尽量减少操作深层级的DOM节点,这会导致上层节点跟同层级节点发生重排。
    • 不要使用table布局,一个小小的改动就会影响整个table。万不得已可以设置table-layout:auto;或者table-layout:fixed;
    • 使用absolute或fixed脱离文档流
      绝对定位会让该元素单独成为渲染树body的一个子元素,修改该元素不会对其他元素造成影响,减少重排的开销。
  • 减少重排次数
    • 样式集中改变
      同一个标签的style尽量一起修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // bad
      var left = 10
      var right = 10
      el.style.left = left + 'px'
      el.style.right = right + 'px'

      //better
      el.style.cssText += 'left: ' + left + 'px; right: ' + right + 'px;'

      // better
      el.className += 'className'
    • 分离读写操作
      读写一次元素的几何属性会触发一次重排,当你把读写操作分开放。就会触发浏览器的渲染队列机制。将所有操作攒在一起一次性执行完了之后,再触发重排

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // bad 强制刷新 触发四次重排+重绘
      div.style.left = div.offsetLeft + 1 + 'px'
      div.style.right = div.offsetRight + 1 + 'px'
      div.style.top = div.offsetTop + 1 + 'px'
      div.style.bottom = div.offsetBottom + 1 + 'px'

      // good 缓存布局信息 相当于读写分离,只触发一次重排
      var curLeft = div.offsetLeft
      var curTop = div.offsetTop
      var curRight = div.offsetRight
      var curBottom = div.offsetBottom

      div.style.left = curLeft + 1 + 'px'
      div.style.right = curRight + 1 + 'px'
      div.style.top = curTop + 1 + 'px'
      div.style.bottom = curBottom + 1 + 'px'
    • 将DOM离线
      也就是不再DOM中直接操作元素,有三种方式来实现:

      • 设置display: none
        设置之后,该元素就不存在于渲染树之中,无论做多少次改变,最终触发重排也只有两次。
      • 通过documentFragment创建碎片
      • 通过cloneNode复制节点,再通过replaceNode替换节点

分析性能:
在开发者工具中使用”性能”工具即可查看绘制页面时各个部分的用时。
在Event Log里面也可以看到不同事件的用时

4. 如何优化操作DOM的性能?

  1. 查找元素优化。查找速度:ID > 类 > 属性

  2. 减少操作元素

  3. 减少通过DOM修改样式,修改样式也会导致浏览器重新渲染。

  4. 批量修改DOM时从文档流中摘除该元素,对其应用多重改变,将元素带回文档中,这样可以最小化重绘和重排版

  • 具体方法:

  • 隐藏元素,进行修改,然后显示它

  • 将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素

  1. 减少iframe,iframe需要消耗

  2. 多次访问同一DOM,要用局部变量暂存

  3. 使用querySelector代替getElement,前者返回的是静态NodeList,消耗的性能比后者小

5. 单页面SEO有什么方案?

  • 采用history模式
  • 使用Nuxt.js渲染页面(SSR服务器渲染)
  • 使用预渲染生成首屏页面
    插件:prerender-spa-plugin
  • tdk(title、description、keyword)
  • 语义化标签
  • site-map
    直接用vue-router的route生成即可

7. 会用什么性能分析工具?怎么用?达到什么样的效果?

  • Chrome自带的开发者工具中的性能
    可以看到页面的使用内存,渲染过程各个部分的用时,各种事件的日志

  • Lighthouse
    可以直观得到优化建议
    详细的内容,可以去参考git:https://github.com/GoogleChrome/lighthouse

  • 测试网站

  • Vue项目分析工具
    可以通过以下指令生成项目分析报告:

    1
    vue-cli-service build --report

    生成之后即可通过占比图看到各个文件的体积

  • webpack-bundle-analyzer

8. Vue可以做哪些性能优化?

  1. 利用Object.freeze()提升性能,Vue 在遇到像 Object.freeze() 这样被设置为不可配置之后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法。

代码:

1
2
3
4
5
6
7
8
9
10
11
export default{
data () {
return {
list:[]
}
},
async created(){
let list = await this.axios.get('/api/list');
this.list = Object.freeze(list);
}
}
  1. v-if 和 v-show 区分使用场景

v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不 做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

  1. computed 和 watch 区分使用场景

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

  1. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

(1)v-for 遍历必须为 item 添加 key

在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。

(2)v-for 遍历避免同时使用 v-if

v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。

推荐:

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}

不推荐:

1
2
3
4
5
6
7
8
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>
  1. 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内

created() {
addEventListener(‘click’, this.click, false)
},
beforeDestroy() {
removeEventListener(‘click’, this.click, false)
}

  1. 图片资源懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

  • 安装插件
1
npm install vue-lazyload --save-dev
  • 在入口文件 man.js 中引入并使用
1
import VueLazyload from 'vue-lazyload'

然后再 vue 中直接使用

1
Vue.use(VueLazyload)

或者添加自定义选项

1
2
3
4
5
6
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
  • 在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
1
<img v-lazy="/static/img/1.png">

以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址。

  1. 路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

路由懒加载:

1
2
3
4
5
6
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
  1. 第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:

(1)首先,安装 babel-plugin-component :

1
npm install babel-plugin-component -D

(2)然后,将 .babelrc 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

(3)在 main.js 中引入部分组件:

1
2
3
4
5
import Vue from 'vue';
import { Button, Select } from 'element-ui';

Vue.use(Button)
Vue.use(Select)
  1. 优化无限列表性能

如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。

  1. 服务端渲染 SSR or 预渲染

服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。

  • 服务端渲染的优点:

更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;

更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

  • 服务端渲染的缺点:

更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;

更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。

如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。


十三、项目经验


1. 系统后端是什么语言

根据公司的招聘的后端岗位来说

2. 项目上线了吗

一般肯定要说上线了
如果是toC的项目,就先把APP自己把玩一遍
如果是toB的项目,那就没什么好担心的了

3. 项目盈利额怎么样?

4. 产品线上都有哪些人?

一个产品两个UI三个前端四个JAVA一个技术总监

5. 产品线的整个工作流程是怎样的?

1)开产品需求会
2)在Redmaid上提挂需求单
3)与UI对接资源
4)与后端调试接口
5)转测试
6)发布上线

6. andriod跟ios的上架流程

安卓的上家平台有应用宝、小米、华为、360等
ios则需要visa信用卡支付,还要688的苹果个人账户,还有各种规范问题,以及强行升级Xcode带来的问题

7. Git的工作流程?Git常用命令有哪些?Git遇到冲突怎么办?如何回退版本?

Git Flow:

  • master:用于存放产品代码
  • dev:用于功能迭代
  • test:用于给测试一个稳定的测试环境
  • feature:用于开发单个功能
  • hotfix:用于修复BUG
  • release:用于发布产品

常用命令:
我工作中常用的有git add ,git status,git commit –m,git push,git pull等

冲突:
当遇到多人协作修改同一个文件时出现冲突,我先将远程文件先git pull下来,手动修改冲突代码后,再git add ,git commit,git push再上传到远程仓库。如果pull也pull不下来提示冲突的话,可以先通过git stash暂存下来,然后再pull拉取,然后git stash pop,取出原来写的,手动修改,然后提交

回退版本:

  • 本地未add到暂存区

    1
    git checkout <file>
  • 已经add到暂存区

    1
    git reset HEAD <file>
  • 已经commit

    1
    git reset --hard "HEAD^"

    ^代表上一版本

8. Web前端工作流程是什么?

老板/甲方的提出需求 –> 产品梳理需求,并画出原型图 –> 开会扯皮 –> 在项目管理软件上派单子 –> UI出图 –> 前端制作静态页面,给UI审核 –> 开发动态功能,用Mock调通接口 –> 与后台联调 –> 把单子转给测试

9. 支付流程?支付失败如何处理?

向接口发送商品信息 –> 接口返回OrderId,token等参数 –> 向后台
后台配置回调url

若是停留时间过长导致订单失效,可以由前台再次提交
如果不是就让他重新提单

10. web扫码登录怎么实现,思路?

制作思路:
1)前台获取用户的设备信息,发送给后台,后台生成二维码ID,回给前台。
2)前台生成二维码,展示给用户,并设置一个定时器,定期向后台轮询二维码的状态。另外会设置一个过期时间,用户长时间未扫码就会失效。
3)用户扫描二维码,将二维码ID和手机端的账户身份信息发给服务端
4)后台向用户发送一个一次性的token作为登录凭证,更新二维码状态。
5)前台将二维码更新为待确认的状态
6)用户确认登录,向后台返回这个临时token
7)后台生成一个正式的token给用户,之后用户便持有该token进行操作。

11. 解决input[type=file]打开时慢、卡顿问题 *****

1
<input type="file" accpet="image/*">  卡顿	

经过测试发现,在mac里面safari、Firefox、Chrome(opera不知道为啥老闪退)都没有卡顿问题
在windows里面,Firefox不卡顿,只有Chrome卡顿
原因 chrome上传文件 检测文件,网速快没问题,慢的话就断掉了解了
解决: accpet=”image/gif,image/png,image/jpeg,image/jpg,image/bmp”

12. 苹果手机 微信调用百度地图Javascript API 频繁闪退问题

在网页中调用百度地图API,在IOS8系统中,缩放的时候频繁闪退
百度地图 新api中 有些接口 数据不稳定,降低api的版本即可

13. 获取视频网站,比如优酷中的视频,嵌入到页面或者移动端

调用官方的api ,调用视频的时候报错
后来发现主要原因 vid(视频id引起的) 
把视频id保存在 localstorge,然后在项目index.html 里面获取id
在请求的时候 就没问题了
向后台接口传递数据的时候: 需要传递用户id 和 视频id(在localStorage)

14. ios 键盘遮挡问题 *****

scrollIntoView(alignWithTop): 滚动浏览器窗口或容器元素,以便在当前视窗的可见范围看见当前元素
alignWithTop = true 或 空 窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐 (元素向上滚动)
alignWithTop = false 调用元素会尽可能全部出现在视口中,可能的话,调用元素的底部会与视口顶部平齐,不过顶部不一定平齐。(元素不会滚动)
常见浏览器:IE、Chrome、Firefox、Safari和Opera。 都支持
let _this = this
setTimeOut(function(){ //每100毫秒 滚动1次
_this.scrollIntoView(true)
},100)
面试的时候 要说一下
当input 得到焦点的时候 出发方法,方法作用 从新计算 元素到顶部的距离,元素向上移动的距离=软键盘的高度
ios手机型号太多,滚动数值需要多次获取

15. mounted钩子函数中请求数据导致页面闪屏问题 ****

就是加载时机问题,放在created里会比mounted触发早一点,
如果在页面挂载完之前请求完成的话就不会看到闪屏了

16. 为什么请求要放在mounted里面?

1)同一组件在切换页面时,destoryed会在created之后立刻触发的问题,导致注册的回调函数被移除,目前没有避免的方案,只能把相关逻辑写在mounted里面
2)SSR

17. 如何禁止 iPhone Safari下的video标签视频自动全屏

把视频转码,用ajax去分段请求数据来填充到canvas,
从而可以调整canvas的大小来实现视频video的大小

18. JS无法获取display为none的隐藏元素的宽度和高度的解决方案

获取元素(拿宽高那个)所有隐藏的祖先元素直到body元素,包括自己。
获取所有隐藏元素的style的display、visibility 属性,保存下来。
设置所有隐藏元素为 visibility:hidden;display:block !important;
获取元素(拿宽高那个)的宽高。
恢复所有隐藏元素的style的display、visibility 属性。
返回元素宽高值。

19. audio元素和video元素在ios和andriod中无法自动播放

产生原因:浏览器都为了节省流量,做出优化,在用户没有行为动作时(交互)不予许自动播放
audio元素的autoplay属性在IOS及Android上无法使用,在PC端正常,
对象.play() 执行播放

20. 使用overflow:hidden,在ios下卡顿的问题解决

在css 属性上添加 -webkit-overflow-scrolling: touch;

21. 判断用户设备?判断更细致的环境

简单判断用户设备:

1
2
3
4
5
6
7
8
9
10
var u = navigator.userAgent.toLowerCase()
if (u.indexOf('android') > -1 || u.indexOf('linux') > -1) {
// 安卓
}
else if (u.indexOf('iphone') > -1) {
// 苹果
}
else if (u.indexOf('windows phone') > -1) {
// 微软
}

详尽判断用户设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var browser = {
versions: function() {
var u = navigator.userAgent
return {
trident: u.indexOf('Trident') > -1, // IE内核
presto: u.indexOf('Presto') > -1, // opera内核
webKit: u.indexOf('AppleWebKit') > -1, // 苹果,谷歌内核
gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') > -1, // 火狐内核
mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
andriod: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, // Android
iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或QQ HD浏览器
iPad: u.indexOf('iPad') > -1, // 是否iPad
webApp: u.indexOf('Safari') > -1
}
},
language: (navigator.browserLanguage || navigator.language).toLowerCase()
}

22. 说一下你是如何与后端进行数据交互的

答:我和后端通过ajax来进行数据交互的,通过统一制定的接口文档,来实现前后端高效开发,如果接口文档不能详细说明,或者接口文档上的参数请求不出数据,我会主动和后端工程师沟通,直到完成跟接口相关的业务开发。当然这其中为了验证一些接口问题,会用到一些辅助工具,比方说,runapi这种在线测试工具

23. 如果后端数据接口没有准备好,你是如何工作的

答:如果后端接口还没有准备好,我会和后端工程师沟通,通过制定接口返回数据的格式,然后前端通过一些mock数据的工具(上家公司使用的是easymock,贼简单)来批量生成假数据,可以让前端和后端同时开发,而无需等待后端数据接口写好再开发,这样提升项目整体的开发效率

24. 后台管理系统中的权限管理是怎么实现的?

登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token, 拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会 根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
权限验证:通过token获取用户对应的 权限,动态根据用户的 权限算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。
具体思路:
登录成功后,服务端会返回一个 token(该token的是一个能唯一标示用户身份的一个key),之后我 们将token存储在本地cookie之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不 用再去登录页面重新登录了。
ps:为了保证安全性,我司现在后台所有token有效期(Expires/Max-Age)都是Session,就是当浏览器关 闭了就丢失了。重新打开游览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新 token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账 号。
用户登录成功之后,我们会在全局钩子 router.beforeEach 中拦截路由,判断是否已获得token,在 获得token之后我们就要去获取用户的基本信息了
页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有token, 就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。 当然如果是做了单点登录得功 能的话,用户信息存储在本地也是可以的。当你一台电脑登录时,另一台会被提下线,所以总会重新登 录获取最新的内容。
先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登 录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过
router.addRoutes 动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是 绝对安全的,后端的权限验证是逃不掉的。
我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也 做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每 一个后台的请求不管是 get 还是 post 都会让前端在请求 header 里面携带用户的 token,后端会根据 该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状 态码,做出相对应的操作。
使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。 具体实现:
创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页 面。
当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路 由表。
调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。


十四、算法

1. 请写出至少两种常见的数组排序的方法(原生js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//快速排序
function quickSort(elements){
if(elements.length <=1){
return elements;
}
var pivotIndex=Math.floor(elements.length / 2);
var pivot=elements.splice(pivotIndex,1)[0];
var left=[];
var right=[];
for(var i=0;i<elements.length;i++){
if(elements[i] < pivot){
left.push(elements[i]);
}else{
right.push(elements[i]);
} }
return quickSort(left).concat([pivot],quickSort(right)); //concat()方法用于连接两个或者多个数组;该方法不会改变现有的数组,而仅仅会返回被连接数组 的一个副本。
};
var elements=[3,5,6,8,2,4,7,9,1,10];
document.write(quickSort(elements));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//插入排序
function sort(elements){
// 假设第0个元素是一个有序数列,第1个以后的是无序数列, // 所以从第1个元素开始将无序数列的元素插入到有序数列中去 for (var i =1; i<=elements.length; i++) {
} }
// 升序
if(elements[i] < elements[i-1]){
// 取出无序数列中的第i个作为被插入元素
var guard=elements[i]; //记住有序数列的最后一个位置,并且将有序数列的位置扩大一个 var j=i-1;
elements[i]=elements[j];
// 比大小;找到被插入元素所在位置
while (j>=0 && guard <elements[j]) {
elements[j+1]=elements[j];
j--; }
elements[j+1]=guard; //插入 }
var elements=[3,5,6,8,2,4,7,9,1,10]; document.write('没调用之前:'+elements); document.write('<br>'); sort(elements); document.write('被调用之后:'+elements);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//冒泡排序
function sort(elements){
for(var i=0;i<elements.length-1;i++){
for(var j=0;j<elements.length-1-i;j++){
if(elements[j] > elements[j+1]){
var swap=elements[j];
elements[j]=elements[j+1];
elements[j+1]=swap;
} }
} }
var elements=[3,5,6,8,2,4,7,9,1,10];
console.log('before'+elements);
sort(elements);
console.log('after'+elements);

十五、其他


1. 前端渲染与后端渲染的区别?

  • 前端渲染:

指的是后端只返回JSON数据,前端利用预先写的html模版,循环读取JSON数据,拼接字符串(es6的模版字符串特性大大减少了拼接字符串的成本),并插入页面。

好处:网络传输数据量小,不占用服务端运算资源(解析模版),模版在前端(很可能仅部分在前端),改结构变交互都前端自己来,改完自己调就行。

坏处:前端耗时较多,对前端工作人员要求比较高,前端代码多,因为部分以前在后台处理的交互逻辑交给了前端处理,占用少部分客户端运算资源用于解析资源

  • 后端渲染:

  • 前端请求,后端用后台模版引擎直接生成html,前端接受数据后,直接插入页面。

  • 好处:前端耗时少,即减少首屏时间,模版统一在后端,前端(相对)省事,不占用客户端运算资源(解析模版)

  • 坏处:占用服务器资源

  • 对比

前端渲染 后端渲染
页面呈现速度 主要受限于带宽和客户端的性能,优化得好,可以逐步动态展开内容,感觉上会更好一点 快,受限于用户的带宽
流量消耗 多一点点(一个前端框架大概50KB) 少一点点(可以省去前端框架部分的代码)
可维护性 好,前后端分离,各施其职,一目了然 差(前后端东西放在一起,掐架多年,早就闹分手了)
SEO友好度 差,大量使用ajax,多数浏览器不能抓去ajax数据
编码效率 高,前后端只做自己的事情 低(不同团队可能有不同效率)

2. 前端什么最重要?

用户体验最重要



参考:
【干货】十分钟读懂浏览器渲染流程
关于 TCP 三次握手和四次挥手,满分回答在此
HTTPS中间人攻击原理
面试官系列 - https 真的安全吗,可以抓包吗,如何防止抓包吗
前端安全系列(二):如何防止CSRF攻击?
保障接口安全的5种常见方式
支付宝签名算法
http缓存详解,http缓存推荐方案
HTTP缓存的应用场景和实现
面试: H5新特性:十个新特性
webworker应用场景_Web Workers 的使用场景有哪些
Web Worker 使用场景
Web Worker 的内部构造以及 5 种你应当使用它的场景
浅析Web Worker使用技巧及实战场景
Web Worker 使用教程
冒死录制面试一个工作3年前端,看看都说了些什么
web前端开发流程是什么?
git-flow 的工作流程
Git发现冲突?如何解决git冲突?带你通过领略真实环境解决团队协作时
iPhoneX安全区域(Safe Area)底部小黑条在微信小程序和H5的屏幕适配
Vuex的Action
如何理解Scoped CSS
了解 vue组件样式穿透 /deep/ ::v-deep >>> 区别 ===
vue中keep-alive的使用及详解
彻底揭秘keep-alive原理
ES6 - 箭头函数、箭头函数与普通函数的区别 (注:定义对象的大括号并非块级作用域)
W3C 代码标准规范
前端面试,面试官问到:性能优化,面试者如何回答?
前端模拟面试-promise有哪些方法?
vue如何封装高质量的组件?
你应该明白的跨域和解决办法
不废话,代码实践带你掌握 强缓存、协商缓存!
吐血整理的webpack入门知识及常用loader和plugin
浅谈前端性能优化
自研前端性能监控平台之 Lighthouse 篇
前端的性能优化,真的有必要做吗?
性能优化对前端,真的很重要吗?2
性能优化对前端,真的重要吗?终极篇
聊聊设计模式(1):桥接模式
聊聊设计模式(2):享元模式
聊聊设计模式(3):门面模式
聊聊设计模式(4):装饰模式
程序员小姐姐被面试官问了1个小时,整个人都懵了… …
一年经验面试字节抖音电商,分享下面经!
从例子来看BFC
浅析xss攻击原理、模拟xss攻击及防止xss攻击的方法
前端为什么还在用cookie?
querySelector(All)相比getElement(s)By在生产环境中有哪些优势?
hash路由和history路由的区别,对前段路由的理解
Vue 动态添加路由及生成菜单
为什么要用vuex,而不是直接使用全局变量
Vuex原理解析
ES6新特性概览
Promise.then是如何实现链式调用的
generator 到 async的简单理解。
Set和Map数据结构介绍及使用场景分析
vue2.0的数组劫持
vue3.0中的数据监听、响应式原理简析
深入剖析:Vue核心之虚拟DOM
Vue模板编译原理
彻底揭秘keep-alive原理
axios如何取消重复请求
封装Axios思路及代码
Vue关于data为什么是函数这件事
分享8个非常实用的Vue自定义指令
Vue.set()和this.$set()介绍
聊聊前端开发中的长列表
Vue 超长列表渲染性能优化实战
vue 2.x 的 v-bind 指令的 .prop 事件修饰符详解
Vue中transition过渡组件全掌握
为什么Vue的:key不建议是index
解析移动端滚动穿透
3分钟搞懂FastClick原理解析
深入浅出移动端适配(总结版)
Web移动端最强适配方案总结,没想到这么好用!
极细边框(1px边框)实现方式
移动端适配的最佳实践
flex深度剖析-解决移动端适配问题!
重排(reflow)和重绘(repaint)
使用 preload 和 prefetch 预加载关键资源
H5加载优化最佳实践
Preload,Prefetch 和它们在 Chrome 之中的优先级
你想知道的 preload
预加载系列一:DNS Prefetching 的正确使用姿势
响应式图片实践研究(html5 picture/srcset 等多种方法对比)
webpack使用-详解DllPlugin
Vue SEO的四种方案
『Webpack系列』—— SplitChunks插件用法详解
浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)
用 preload 预加载页面资源
深入浅出浏览器架构
浏览器的进程与线程详解
深度剖析Margin塌陷,BFC,Containing Block之间的关系
CSS: What is Block Formatting Context (BFC)?
MVC,MVP 和 MVVM 的图示
聊一聊二维码扫描登录原理
扫码登录实现原理
三种方式实现扫码登录
JavaScript中new的原理以及实现
JavaScript深入之bind的模拟实现
jwt 实践应用以及特殊案例思考
JSON Web Token 入门教程