开宗明义,本瓜深知汝之痛点:前端面试知识点太杂,卿总为了面试而面试,忘了记,记了又忘,循环往复,为此叫苦不迭。
来,让本瓜带领各位都稍稍回顾一下,自己曾经在学生时代记忆元素周期表的光辉岁月。
氢、氦、锂、铍、硼、碳、氮、氧、氟、氖、钠、镁、铝、硅、磷、硫、氯、氩、钾、钙、钪、钛、钒、铬、猛、铁、钴、镍、铜、锌…
咱当初记这前三十位元素,是死记硬背的吗?答案是否定的,机智的我们用到了 串联记忆法 。
一定是像这样或类似这样去记:
第一周期:氢 氦 ———— 轻嗨:轻轻的打个了招呼:嗨!
第二周期:锂 铍 硼 碳 氮 氧 氟 氖 ———— 你皮捧碳 蛋养福奶:你很皮,手里捧了一把碳。鸡蛋能够滋养福气的奶妈
第三周期:钠 镁 铝 硅 磷 硫 氯 氩 ———— 那美女桂林留绿牙:那美女在桂林留绿色的牙齿
第四周期:钾 钙 钪 钛 钒 铬 猛 铁 钴 镍 铜 锌 ———— 贾盖坑太凡哥 猛铁骨裂痛心:“贾盖”坑了“太凡哥”,导致猛男铁汉骨头碎裂很痛心
串联联想与二元链式联想记忆的方法有相似之处,就是都要通过想象、创造和编故事来帮助我们达到双脑学习和记忆的目的。—— 出处
有木有?本瓜记得尤为清楚,以上串联起来的谐音故事简直可以写出一个狗血剧本了。尤其是“那美女(钠镁铝)”简单三字仿佛就能激起青春期的荷尔蒙。如此,学习能不有兴趣吗?兴趣是最好的老师!想忘掉都难啊!
于是乎,本瓜类比归化,将自己遇到过的高频面试问题运用串联联想法进行了“串联”整理,以期形成系统,与各位同好分享。
上图!
此题是经典中的经典,可挖掘的点非常之多,亦非常之深。
一图胜万言
串联知识点:URL解析、DNS查询、TCP握手、HTTP请求、浏览器处理返回报文、页面渲染
串联记忆:共计六步,归并为一句话来记忆:UDTH,处理返回加渲染。
“UDTH” 即URL解析、DNS查询、TCP握手、HTTP请求,
“处理返回加渲染”,即浏览器处理返回报文,和页面渲染。
同时,本瓜倾情在脑图上标注了每个步骤可能考察的知识点“关键词”,真的是个个重点,不容错过!
URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。
// 示例引自 wikipedia
hierarchical part
┌───────────────────┴─────────────────────┐
authority path
┌───────────────┴───────────────┐┌───┴────┐
abc://username:[email protected]:123/path/data?key=value&key2=value2#fragid1
└┬┘ └───────┬───────┘ └────┬────┘ └┬┘ └─────────┬─────────┘ └──┬──┘
scheme user information host port query fragment
scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,
其中最常见的类型是 http,而 https 则是进行加密的网络传输。
host - 定义域主机(http 的默认主机是 www)
domain - 定义因特网域名,比如 baidu.com
port - 定义主机上的端口号(http 的默认端口号是 80)
path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
filename - 定义文档/资源的名称
query - 即查询参数
fragment - 即 # 后的hash值,一般用来定位到某个位置
更多可见:
URL 编码
一般来说,URL 只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。此在 URL RFC 已做硬性规定。
这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致"URL编码"成为了一个混乱的领域。
阮老师早在 2010 年已解释了:关于URL编码- 阮一峰
这里可直接看结论:浏览器对 URL 编码会出现差异从而造成混乱,所以假设我们使用 Javascript 预先对 URL 编码,然后再向服务器提交。因为Javascript 的输出总是一致的,这样就保证了服务器得到的数据是格式统一的。
我们常使用到:encodeURI()、encodeURIComponent();前者对整个 URL 进行 utf-8 编码,后者是对 URL 部分进行编码。
本瓜请问:你能清楚的解释 ASCII、Unicode、UTF-8、GBK 含义和关系吗?
也许我们并不太了解我们常见、常用的东西。
类型 | 含义 |
---|---|
ASCII | 8位一个字节,1个字节表示一个字符.即: 2 ** 8 = 256,所以ASCII码最多只能表示256个字符。 |
Unicode | 俗称万国码,把所有的语言统一到一个编码里.解决了ASCII码的限制以及乱码的问题。unicode码一般是用两个字节表示一个字符,特别生僻的用四个字节表示一个字符。 |
UTF-8 | “可变长的编码方式”,如果是英文字符,则采用ASCII编码,占用一个字节。如果是常用汉字,就占用三个字节,如果是生僻的字就占用4~6个字节。 |
GBK | 国内版本,一个中文字符 == 两个字节 英文是一个字节 |
强缓存、协商缓存
言外之音:本瓜起初是将强缓存、协商缓存放在第三步 “HTTP 请求”中,后来了解到:如果命中了强缓存则可不再走DNS解析这步。遂将其归到此处。浏览器强缓存是按照ip还是域名缓存的?
强缓存、协商缓存是必考题。具体流程如下:
浏览器在加载资源时,根据请求头的 expires和 cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
expires:HTTP/1.0 时期,根据对比本地时间和服务器时间来判断。
cache-control:HTTP/1.1 时期,根据相对时间来判断,如设置max-age,单位为秒。
如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过Etag和Last-Modified-If验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回(304),告诉浏览器从缓存中读取数据。
【ETag、If-None-Match】成对:Etag 是服务器返回给浏览器的,If-None-Match 是浏览器请求服务器的。通过对比二者来判断,它们记录的是:文件生成的唯一标识。
【Last-Modified,If-Modified-Since】成对:Modified-Since 是服务器返回给浏览器的,If-Modified-Since 是浏览器请求服务器的。通过对比二者来判断,它们记录的是:最后修改时间。
注:ETag 的优先级比 Last-Modified 更高。大部分 web 服务器都默认开启协商缓存,而且是同时启用【ETag、If-None-Match】和 【Last-Modified,If-Modified-Since】。
如果前面两者都没有命中,直接从服务器加载资源。
综上,强缓存和协商缓存如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;不同的是:强缓存不会发请求到服务器,协商缓存会发请求到服务器进行对比判断得出是否命中。
以上还有另一个重点,就是cache-control的值的分类:如“no-cache”、“no-store”、“private”等,需要细扣。此处仅暂列二三、作初步释义。
更多可见:
递归查询
DNS 解析 URL(自右向左) 找到对应的 ip
// 例如:查找www.google.com的IP地址过程(真正的网址是www.google.com.):
// 根域名服务器 -> com顶级域名服务器 -> google.com域名服务器 -> www.google.com对应的ip
. -> .com -> google.com. -> www.google.com.
这是一个递归查询的过程。
关于根域名的更多知识,可见
根域名的知识-阮一峰。
DNS 缓存
请记住:有 DNS 的地方,就有 DNS 缓存。
DNS存在着多级缓存,从距离浏览器的距离排序的话,有以下几种:
1.浏览器缓存 2.系统缓存 3.路由器缓存 4.IPS 服务器缓存 5.根域名服务器缓存 6.顶级域名服务器缓存 7.主域名服务器缓存。
查看缓存:
浏览器查看 DNS 缓存:chrome://net-internals/#dns
win10 系统查看 DNS 缓存:win+R => cmd => ipconfig /displaydns
DNS 负载均衡
DNS负载均衡技术的实现原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。—— 百科
DNS解析返回域名的IP之后,接下来就是浏览器要和该IP建立TCP连接了。
言外之音:TCP 的相关知识在大学基础课程《计算机网络》都有,本瓜内心苦:出来混的迟早是要还的…
TCP/IP 模型:链路层-网络层-传输层-应用层。
与之对应,OSI(开放式系统互联模型)也不能忘。通常认为 OSI 模型的最上面三层(应用层、表示层和会话层)对应 TCP/IP 模型中的应用层。wiki
在 TCP/IP 模型中,像常用的 HTTP/HTTPS/SSH 等协议都在应用层上。
三次握手、四次挥手
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。
三次握手就跟早期打电话时的情况一样:1、A:听得到吗?2、B:听得到,你呢?3、A:我也听到了。然后才开始真正对话。
1. 第一次握手(SYN=1, seq=x):
客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
发送完毕后,客户端进入 SYN_SEND 状态。
2. 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
3. 第三次握手(ACK=1,ACKnum=y+1)
客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1
发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。
所谓四次挥手(Four-way handshake),是指 TCP 的连接的拆除需要发送四个包。
四次挥手像老师拖堂的场景:1、学生说:老师,下课了。2、老师:好,我知道了,我说完这点。3、老师:好了,说完了,下课吧。4、学生:谢谢老师,老师再见。
1. 第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
发送完毕后,客户端进入 FIN_WAIT_1 状态。
2. 第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
3. 第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
4. 第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。
服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
你若问我:三次握手、四次挥手的详细内容太难记了,还记不记?本瓜答:进大厂是必要的。
动图来源:两张动图-彻底明白TCP的三次握手与四次挥手
流量控制(滑动窗口)
为了增加网络的吞吐量,想将数据包一起发送过去,实现“流量控制”,这时候便产生了“滑动窗口”这种协议。
滑动窗口允许发送方在收到接收方的确认之前发送多个数据段。窗口大小决定了在收到目的地确认之前,一次可以传送的数据段的最大数目。窗口大小越大,主机一次可以传输的数据段就越多。当主机传输窗口大小数目的数据段后,就必须等收到确认,才可以再传下面的数据段。
窗口的大小在通信双方连接期间是可变的,通信双方可以通过协商动态地修改窗口大小。改变窗口大小的唯一根据,就是接收端缓冲区的大小。
拥塞控制
需求>供给 就会产生拥塞
通过“拥塞窗口”、“慢启动”、“快速重传”、“快速恢复”动态解决。
TCP 使用多种拥塞控制策略来避免雪崩式拥塞。TCP会为每条连接维护一个“拥塞窗口”来限制可能在端对端间传输的未确认分组总数量。这类似 TCP 流量控制机制中使用的滑动窗口。TCP在一个连接初始化或超时后使用一种“慢启动”机制来增加拥塞窗口的大小。它的起始值一般为最大分段大小(Maximum segment size,MSS)的两倍,虽然名为“慢启动”,初始值也相当低,但其增长极快:当每个分段得到确认时,拥塞窗口会增加一个MSS,使得在每次往返时间(round-trip time,RTT)内拥塞窗口能高效地双倍增长。—— TCP拥塞控制-wikipedia
滑动窗口(流量控制)和拥塞控制里的原理性的东西太多,本瓜表示无力,暂时要求尽力去理解去记忆。
推荐阅读:
HTTP是万维网的数据通信的基础 —— 维基百科
HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。
HTTP响应报文也是由三部分组成: 状态码, 响应报头和响应报文。
HTTP、HTTPS
HTTP 和 HTTPS 的区别?
HTTP报文是包裹在TCP报文中发送的,服务器端收到TCP报文时会解包提取出HTTP报文。但是这个过程中存在一定的风险,HTTP报文是明文,如果中间被截取的话会存在一些信息泄露的风险。
如果在进入TCP报文之前对HTTP做一次加密就可以解决这个问题了。HTTPS协议的本质就是HTTP + SSL(or TLS)。在HTTP报文进入TCP报文之前,先使用SSL对HTTP报文进行加密。从网络的层级结构看它位于HTTP协议与TCP协议之间。
HTTP2
http2 是完全兼容 http/1.x 的,并在此基础上添加了 4 个主要新特性:
更多了解,可见 HTTP2 详解
GET、POST
直观区别:
隐藏区别(存在浏览器差异):
更多了解:
RESTful API 设计指南 - 阮一峰
Keep-Alive
我们都知道使用 Keep-Alive 是为了避免重新建立连接。
目前大部分浏览器都是用 http1.1 协议,默认都会发起 Keep-Alive 的连接请求了,所以是否能完成一个完整的 Keep-Alive 连接就看服务器设置情况。
HTTP 长连接不可能一直保持,它有两个参数,例如 Keep-Alive: timeout=5, max=100,表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。
Keep-Alive 模式发送数据 HTTP 服务器不会自动断开连接,所有不能使用返回EOF(-1)来判断。
基于此,抛问题:当 HTTP 采用 keepalive 模式,当客户端向服务器发生请求之后,客户端如何判断服务器的数据已经发生完成?
使用消息首部字段 Conent-Length:Conent-Length表示实体内容长度,客户端可以根据这个值来判断数据是否接收完成。
使用消息首部字段 Transfer-Encoding:如果是动态页面,服务器不可能预先知道内容大小,这时就可以使用Transfer-Encoding:chunk 模式来传输数据了。即如果要一边产生数据,一边发给客户端,服务器就需要使用"Transfer-Encoding: chunked"这样的方式来代替Content-Length。chunked 编码的数据在最后有一个空 chunked 块,表明本次传输数据结束
本瓜之前面试腾讯 PCG 就被问到 Transfer-Encoding:chunk 这个,请大家格外注意。
参考:
状态码
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500。
切分返回头和返回体(腾讯 PCG 考点)
这里的考点其实和 http keep-alive 中的问题重合,但是还是想着重强调,因为本瓜掉过这个坑,再三点出,以示后人。
最终归为这个问题:Detect end of HTTP request body - stackoverflow
解答:
1. If the client sends a message with Transfer-Encoding: Chunked, you will need to parse the somewhat complicated chunked transfer encoding syntax. You don not really have much choice in the matter -- if the client is sending in this format, you have to receive it. When the client is using this approach, you can detect the end of the body by a chunk with a length of 0.
2. If the client instead sends a Content-Length, you must use that.
即:如何获取 HTTP 返回体?
先把 header 直到 \r\n\r\n(两个换行)整个读取,即整个请求头;
如果返回 Transfer-Encoding: Chunked,则读取,直到遇到空 chunked 块,则结束。
如果返回 Content-Length,则读从请求头的末尾开始计算 Content-Length 长度的字节。
其他情况,等待返回。
了解更多:HTTP 之响应篇
本地数据存储
浏览器缓存位置
按优先级从高到低:
更多:
离线缓存:
HTML5-离线缓存(Application Cache)
注:“浏览器缓存位置”和“离线缓存”了解相关,有个概念/印象即可。
CssTree+DomTree
本瓜知道你知道过程是这样的:
dom tree + css tree = render tree => layout =>painting?
但是你真的吃透了吗?
HTML → DOM树 转化过程:
CSS → CSSOM树 转化过程类同以上
Render Tree (生成渲染树,计算可见节点和样式)
layout:依照盒子模型,计算出每个节点在屏幕中的位置及尺寸
painting:按照算出来的规则,通过显卡,把内容画到屏幕上。
回流、重绘
回流:
当可见节点位置及尺寸发生变化时会发生回流
重绘:
改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
这里本瓜再抛两个问题。
Q1:浏览器在什么时候向服务器发送获取css、js外部文件的请求?
A1:解析DOM时碰到外部链接,如果还有connection,则立刻触发下载请求。
Q2:CSSOM DOM JavaScript 三者阻塞关系?
A2:CSSOM DOM互不影响,JavaScript会阻塞DOM树的构建但JS前的HTML可以正常解析成DOM树,CSSOM的构建会阻塞JavaScript的执行。对此句存疑?
css加载不会阻塞DOM树的解析
// DOM解析和CSS解析是两个并行的进程,所以这也解释了为什么CSS加载不会阻塞DOM的解析。css加载会阻塞DOM树的渲染
// 由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。css加载会阻塞后面js语句的执行
// 由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行。
参考阅读:
web 性能优化
假若你认为此题简单,一两句话就能说完?那当真浮于表面。此题实则一两天都说不完!它可以牵扯出 js 原理的大部分知识。是真正意义上的“母题”。
一图胜万言
串联知识点:闭包、作用域、原型链、js继承。
串联记忆:此题并非像上文题“从输入URL到页面加载发生了什么?”,后者“串联点”是按解答步骤来递进的。而这里的“串联点”,更多是你中有我,我中有你,前后互相补充,互相完善。当你领略完的时候,一定会有一种“万物归宗”的感觉。
归并为一五言诗来记忆:
闭包作用域
原型多考虑
继承八大法
基础好好叙
闭包含义
一言以蔽之。
在一个函数内有另外一个函数可以访问它的内部变量,并且另外一个函数在外部被调用,这样的词法环境叫闭包。
闭包应用
for(var i = 0; i < 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000);
})(i);
}
console.log(i);
垃圾回收机制
标记清除简单讲就是变量存储在内存中,当变量进入执行环境的时候,垃圾回收器会给它加上标记,这个变量离开执行环境,将其标记为“清除”,不可追踪,不被其他对象引用,或者是两个对象互相引用,不被第三个对象引用,然后由垃圾回收器收回,释放内存空间。
防抖、节流函数
function debounce(fn, delay) {
var timer; // 维护一个 timer
return function () {
var _this = this; // 取debounce执行作用域的this
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
}, delay);
};
}
function throttle(fn, delay) {
var timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args);
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)
}
}
全局作用域
函数作用域(局部作用域)
块级作用域
块级作用域 : 块级作用域指的就是使用 if () { }; while ( ) { } …这些语句所形成的语句块 , 并且其中变量必须使用 let 或 const 声明,保证了外部不可以访问语句块中的变量。
注:函数作用域和块级作用域没有直接关系。
作用域链
出现函数嵌套函数,则就会出现作用域链 scope chain。
用作用域链来解释闭包:
function outter() {
var private= "I am private";
function show() {
console.log(private);
}
// [[scope]]已经确定:[outter上下文的变量对象,全局上下文变量对象]
return show;
}
var ref = outter();
console.log(private); // outter执行完以后,private不会被销毁,并且只能被show方法所访问,
//直接访问它会出现报错:private is not defined
ref(); // 打印I am private
其实,我们要明白的是函数的声明和调用是分开的,如果不搞清楚这一点,很多基础面试题就容易出错。
变量生命周期
一个变量的声明意味着就是我们在内存当中申请了一个空间用来存储。这个内存也就是我们电脑的运行内存,如果我们一直的声明变量,不释放的话。会占用很大的内存。
在 c/c++ 当中是需要程序员在合适的地方手动的去释放变量内存,而 javascript 和 java 拥有垃圾回收机制(咱们在上文已说明)。
js 变量分为两种类型:全局变量和局部变量
实例与原型
一个原型对象的隐形属性指向构造它的构造函数的显示属性。
当一个对象去查找它的属性,找不到就去找他的构造函数的属性,一直向上找,直到找到 Object()。
判断数据类型
new 一个对象
步骤:
this 指向
this 指向 5 大规则:
参考:this指向记忆5大原则
更多:JS中的箭头函数与this
bind、call、apply
function sum(num1 , num2){
return num1 + num2;
}
function callSum(num1 , num2){
return sum.call(this , sum1 , sum2);
}
console.log(callSum(10 , 10)); // 20
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments); // 传入arguments类数组对象
}
function callSum2(num1,num2){
return sum.apply(this,[num1 , num2]); // 传入数组
}
console.log(callSum1(10 , 10)); // 20
console.log(callSum2(10 , 10)); // 20
call和apply的区别在于二者传参的时候,前者是一个一个的传,后者是传数组或类数组arguments
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
手写深浅拷贝
浅:
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};
深(递归):
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
了解更多,推荐阅读:如何写出一个惊艳面试官的深拷贝?
八种继承方式,详细请看此篇:JavaScript常用八种继承方案。
本瓜不做赘述,此处仅列二三关键必记。
本瓜不装了,摊牌了。其实本文的目录结构编写时间线在 《 Vue(v2.6.11)万行源码生啃,就硬刚!》
这篇文章之前。当时就是因为似懂非懂,才定下心来“生啃源码”。现在源码看完了,体会的确又不一样了。但由于细节太多,篇幅受限。此处也仅列框架、点出要点、注释链接,以便记忆。
一图胜万言
串联知识点:Vue初始化和生命周期、虚拟DOM、响应式原理、组件编译、Vue常用补充、Vue全家桶。
串联记忆:编一顺口溜,见笑。
V U E 真容易
初始化 有生命
虚拟 dom 好给力
响应式 看仔细
组件化 大家利
全家桶 笑嘻嘻
会打包 挣一亿
挂载和初始化
new Vue()发生了什么?
Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的。Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法。
初始化主要实现:合并配置(mergeOptions),初始化生命周期(initLifecycle),初始化事件中心(initEvents),初始化渲染(initRender),初始化 data、props、computed、watcher 等等。
流程图参考如下:
实例生命周期
生命周期图示,还是得看官网文档。还记得这句话吗?
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
注释版:
推荐:源码解读
要点注释:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // beforeCreate 钩子
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // created 钩子
// ...
}
在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mounted 钩子。(此点重要)
beforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中调用。update 的执行时机是在 flushSchedulerQueue 函数调用的时候。
beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段。
activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子。
重点说明:
Vue 2.0 相比 Vue 1.0 最大的升级就是利用了 Virtual DOM。
vdom
vdom 其实就是一颗 js 对象树,最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。原本对 DOM 节点的操作(浏览器将 DOM 设计的非常复杂)转成了对 js 对象的操作,加快处理速度、提升性能。
VNode 的创建是由 createElement 方法实现的。
欲知原理,推荐阅读:snabbdom
diff & patch
在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。即用 diff 算法比较差异,然后调用 patch 应用到真实 DOM 上去。patch 的过程即一个打补丁的过程。
图片来源
diff 算法是一个交叉对比的过程,大致可以简要概括为:头头比较、尾尾比较、头尾比较、尾头比较。
入门级别 diff 详情推荐看此篇:LINK
图片来源
注意:render函数返回的是 vdom,patch生成的才是真实DOM。
官方生图,高屋建瓴。
本瓜曾在《简析 vue 的双向绑定原理》这篇文章写过,如今看又是一番心情。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
发布订阅者模式(字节考题)
class emit {
}
cosnt eeee = new emit()
eeee.on('aa' , function() { console.log(1)})
eeee.on('aa' , function() {console.log(2)})
eeee.emit('aa')
//class emit{}
// 要求手写发布者-订阅模式
class Subject{
constructor () {
this.observers =[]
}
add (observer) {
this.observers.push(observer)
}
notify () {
this.observers.map((item, index) => {
item.update()
})
}
}
class Observer {
constructor (name) {
this.name = name
}
update () {
console.log("I`m " + this.name)
}
}
var sub = new Subject()
var obs1 = new Observer("obs1")
var obs2 = new Observer("obs2")
sub.add(obs1)
sub.add(obs2)
sub.notify() // I`m obs1 I`m obs2
除了“发布者订阅模式”,你还知道哪些 js 设计模式?这里留个坑,以后再补,东西太多了…
Observe
Observe 的功能就是用来监测数据的变化。它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:
这里贴一下源码片段,咱可以感受下:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
...
}
walk (obj: Object) {
...
}
observeArray (items: Array) {
...
}
}
有没有觉得和上面提到的“发布订阅者模式”中的相似。Observer 首先实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value 的 ob 属性上。(def 函数是一个简单的对 Object.defineProperty 的封装)。
Dep
Dep 是整个 getter 依赖收集的核心。
由于 Watcher 是有多个的,所以需要用 Dep 收集变化之后集中管理,再通知到对应的 Watcher。由此也好理解 Dep 是依赖于 Watcher 的。
贴源码片段,感受一下:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
...
}
Watcher
贴源码片段,感受一二:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array;
newDeps: Array;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
...
// parse expression for getter
...
}
get () {
pushTarget(this)
...
}
addDep (dep: Dep) {
const id = dep.id
...
}
cleanupDeps () {
...
}
// ...
}
Watcher 会通知视图的更新 re-render。
常见视图更新场景:
组件的思想也是 Vue 核心,将组件编译为 vdom ,则也是一重难点!
你可以发现在 Vue 这一节有很多引用的图,其实它们有的相似,更多的是侧重点不同,建议都可按照流程图理解理解,做到融会贯通。
组件
官方示例:
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: ''
})
new Vue({ el: '#components-demo' })
组件还涉及:组件之间的通信、插槽、动态组件等内容。后续再表(OS:
这是自己给自己挖了个多大的坑)。
parse
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。这个过程会用到大量的正则表达式对字符串解析,源码很难读。
但是我们需要知道的是 start、end、comment、chars 四大函数。
对于普通标签的处理流程大致:
参考阅读:Vue parse之 从template到astElement 源码详解
optimize
当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化 —— optimize。
optimize 中最重要的是标记静态根(markStaticRoots )、静态节点(markStatic )。如果是静态节点则它们生成的DOM永远不需要改变,这让模板的更新更搞笑(不变的节点不用更新)。
问题:为什么子节点的元素类型是静态文本类型,就会给 optimize 过程加大成本呢?
首先来分析一下,之所以在 optimize 过程中做这个静态根节点的优化,目的是什么,成本是什么?目的:在 patch 过程中,减少不必要的比对过程,加速更新。
成本:a. 需要维护静态模板的存储对象。b. 多层render函数调用。
推荐阅读
codegen
编译的最后一步就是把优化后的 AST 树转换成可执行的代码,即在 codegen 环节。
主要步骤(调用函数):
此节考的不多,仅做了解。了解更多,得看源码。
keep-alive(常考)
keepalive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存。
v-if、v-show、v-for
三个高频子问题:
答:v-if 相当于 display; v-show 相当于 visibility; 前者会控制是否创建,后者仅控制是否隐藏显示。
答:因为 v-for 优先级比 v-if 高,所以在每次重新渲染的时候会先遍历整个列表,再进行 if 判断是否展示,消耗性能。
答:key 是 diff 算法中用来对比的,用 index 作为 key 并未唯一识别,当插入元素时,key 也会变化。index 作为 key,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出(官网说明)。
本瓜这里不做细答,想了解更多请自行解决。
自定义指令
Vue 中的混入(Minxin)、自定义指令(directive)、过滤器(filter)有共通之处,在注册的时候,需要平衡局部注册和全局注册的优劣。
transition
本瓜读源码的过程中,发现源码中有较大的篇幅在描述关于transition。在官方文档中,transition 也是作为独立的重要一节来说明。进入/离开 & 列表过渡,Vue 动画&过渡,是容易忽视的点。
自从用上了 Vue 全家桶,腿也不疼了,腰也不酸了。咦,一口气写五个页面,妈妈再也不用担心我的学习了。(好像有点串广告了…)
Vue-Router
官方的路由管理器
分为两种模式:hash 和 history
原生:
HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。详情链接
Vuex
官方状态管理
由以下几部分核心组成:
VueCLI
官方脚手架
VuePress
静态网站生成器
NuxtJS
服务端渲染
只要你做前端有两年经验左右,那一样就得要求自己掌握一款打包工具了。
webpack 原理
官方解释:
webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个_bundle_。
核心概念:
功能:
代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验、自动发布。
优化打包速度
AMD、CMD
前端模块化有四种规范:CommonJS、AMD、CMD、ES6。
AMD(异步模块定义) | CMD(通用模块定义) |
---|---|
速度快 | 性能较差 |
会浪费资源 | 只有真正需要才加载依赖 |
预先加载所有的依赖,直到使用的时候才执行 | 直到使用的时候才定义依赖 |
Node.js是commonJS规范的主要实践者:module、module.exports(exports)、require、global。
ES6 在语言标准的层面上,实现了模块功能,主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
问:比较 import 和 require 的区别?
import | require |
---|---|
ES6标准中的模块化解决方案 | 是node中遵循CommonJS规范的模块化解决方案 |
不支持动态引入 | 支持动态引入 |
是关键词 | 不是关键词 |
编译时加载,必须放在模块顶部 | 运行时加载,理论上来说放在哪里都可以 |
性能较好 | 性能较差 |
实时绑定方式,即导入和导出的值都指向同一个内存地 | 导出时是值拷贝,就算导出的值变化了,导入的值也不会变化 |
会编译成require/exports来执行 | - |
更多:前端模块化:CommonJS,AMD,CMD,ES6
实现plugin插件(腾讯WXG考点)
从零实现一个 Webpack Plugin
其实本瓜想回答:我最喜欢减法!因为幸福生活需要用减法。
算法这一个 part 也已久远,既然逃不掉,那就正面挑战它!其实也没那么难。
串联知识点:数据结构、基础算法、排序算法、进阶算法。
串联记忆:
算法算法我不怕
数据结构打趴下
遍历排序我最溜
指针动态贪心刷
队列
先入先出。
栈
先入后出。
堆
堆通常是一个可以被看做一棵树的数组对象。
堆总是满足下列性质:
(本瓜曾被拷问过这个点,大厂就是会考《数据结构》,别逃避,出来混迟早是要还的)。
链表
// 循环链表
function Node(element){
this.element = element;
this.prev = null;
this.next = null;
}
function display(){
var current = this.head;
//检查头节点当循环到头节点时退出循环
while(!(current.next == null) && !(current.next.element=='head')){
print(current.next.element);
current = current.next;
}
}
function Llist(){
this.head = new Node('head');
this.head.next = this.head;
this.find = find;
this.insert = insert;
this.display = display;
this.findPrevious = findPrevious;
this.remove = remove;
}
哈希
散列函数(英语:Hash function)又称散列算法、哈希函数。以 Key:Value 的方式存储数据。
哈希表最大的特点是可以快速定位到要查找的数据,查询的时间复杂度接近O(1)。本瓜建议大家可以把这里所有的数据结构的“增删改查”操作的时间复杂度都理一下,也是会被考的。
时间复杂度、空间复杂度
简单理解:
时间复杂度更多重要一点,常见的时间复杂度:O(1)、O(n)、O(logn)、O(n2)。本瓜小TIP:面试/笔试如果不知道怎么算,就在这里面猜吧。实在不行就答:O(n) ~ O(n2) 之间,大概率不会错。
树的遍历(广度、深度)
广度优先遍历(BFS):
需要用到队列(Queue)来存储节点对象,队列的特点就是先进先出。示例
深度优先遍历(DFS):
递归思想
function result(){
if(n==1||n==2){
return 1
}
return reslt(n-2)+result(n-1)
}
函数柯里化:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。—— wiki
// 普通方式
var add1 = function(a, b, c){
return a + b + c;
}
// 柯里化
var add2 = function(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
// demo
var foo = function(x) {
return function(y) {
return x + y
}
}
foo(3)(4) // 7
这样处理参数,可以直接追加,而不需要初始传参就写完全。这里只是浅谈,更多请自行探索。
二分法
要求手写二分法,实在不行,能默写也可以啊!
// 二分法:先排序,再找目标
function binary_search(arr,target) {
let min=0
let max=arr.length-1
while(min<=max){
let mid=Math.ceil((min+max)/2)
if(arr[mid]==target){
return mid
}else if(arr[mid]>target){
max=mid-1
}else if(arr[mid]
排序是比较常用也比较重要的一块,此处并未全列出。仅强调快排和冒泡,会用双循环也行啊。
快速排序
// 快排:选取基准,比基准大的放右边,比基准小的放左边,然后两边用递归
function quickSort(arr, i, j) {
if(i < j) {
let left = i;
let right = j;
let pivot = arr[left];
while(i < j) {
while(arr[j] >= pivot && i < j) { // 从后往前找比基准小的数
j--;
}
if(i < j) {
arr[i++] = arr[j];
}
while(arr[i] <= pivot && i < j) { // 从前往后找比基准大的数
i++;
}
if(i < j) {
arr[j--] = arr[i];
}
}
arr[i] = pivot;
quickSort(arr, left, i-1);
quickSort(arr, i+1, right);
return arr;
}
}
冒泡排序
// 冒泡:双层循环
var arr=[10,20,50,100,40,200];
for(var i=0;iarr[j+1]){
var temp=arr[j]
arr[j]=arr[j+1]
arr[j+1]=temp
}
}
}
console.log(arr)
双指针
看到“有序”和“数组”。立刻把双指针法调度进你的大脑内存。普通双指针走不通,立刻想对撞指针!
示例:合并两个有序数组(双指针解法)
示例: 输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
const merge = function(nums1, m, nums2, n) {
// 初始化两个指针的指向,初始化 nums1 尾部索引k
let i = m - 1, j = n - 1, k = m + n - 1
// 当两个数组都没遍历完时,指针同步移动
while(i >= 0 && j >= 0) {
// 取较大的值,从末尾往前填补
if(nums1[i] >= nums2[j]) {
nums1[k] = nums1[i]
i--
k--
} else {
nums1[k] = nums2[j]
j--
k--
}
}
// nums2 留下的情况,特殊处理一下
while(j>=0) {
nums1[k] = nums2[j]
k--
j--
}
};
高级!
动态规划
动态规划的思想就是把一个大的问题进行拆分,细分成一个个小的子问题,且能够从这些小的子问题的解当中推导出原问题的解。
同时需要满足以下两个重要性质才能进行动态规划:
动态规划实例:斐波拉契数列(上文有提到)
斐波拉契数列:采用递归,虽然代码很简洁,但是明显随着次数的增加,导致递归树增长的非常庞大,耗时较久。
用动态规划实现斐波拉契数列,代码如下
function feiBoLaQie(n) {
//创建一个数组,用于存放斐波拉契数组的数值
let val = [];
//将数组初始化,将数组的每一项都置为0
for(let i =0 ;i<=n;i++){
val[i] = 0;
}
if (n==1 || n==2){
return 1;
} else{
val[1] = 1;
val[2] = 2;
for (let j =3; j<=n;j++){
val[j] = val[j-1] + val[j-2];
}
}
return val[n-1];
}
console.log(feiBoLaQie(40));//102334155
通过数组 val 中保存了中间结果, 如果要计算的斐波那契数是 1 或者 2, 那么 if 语句会返回 1。 否则,数值 1 和 2 将被保存在 val 数组中 1 和 2 的位置。
循环将会从 3 到输入的参数之间进行遍历, 将数组的每个元素赋值为前两个元素之和, 循环结束, 数组的最后一个元素值即为最终计算得到的斐波那契数值, 这个数值也将作为函数的返回值。
动态规划解决速度更快。
贪心算法
贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的解),从而达到全局的最优(全局最优解)。
注:贪心得到结果是一个可以接受的解,不一定总是得到最优的解。
示例:最少硬币找零问题
是给出要找零的钱数,以及可以用硬币的额度数量,找出有多少种找零方法。
如:美国面额硬币有:1,5,10,25
我们给36美分的零钱,看能得怎样的结果?
function MinCoinChange(coins){
var coins = coins;
var cache = {};
this.makeChange = function(amount){
var change = [], total = 0;
for(var i = coins.length; i >= 0; i--){
var coin = coins[i];
while(total + coin <= amount){
change.push(coin);
total += coin;
}
}
return change;
}
}
var minCoinChange = new MinCoinChange([1, 5, 10, 25]);
minCoinChange.makeChange(36);
//[25, 10, 1] 即一个25美分、一个10美分、一个1美分
串联知识点:跨域、XSS(跨站脚本攻击)、CRFS(跨站请求伪造)、SQL 注入、DNS 劫持、HTTP 劫持。
串联记忆:三跨两劫持一注入
跨域定义
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
跨域限制 是浏览器的一种保护机制,若跨域,则:
跨域解决
跨域解决:
// 普通跨域请求:只需服务器端设置 Access-Control-Allow-Origin。
// 带cookie跨域请求:前后端都需要进行设置。
// 如前端在 axios 中设置
axios.defaults.withCredentials = true
XSS 原理
XSS的原理是WEB应用程序混淆了用户提交的数据和JS脚本的代码边界,导致浏览器把用户的输入当成了JS代码来执行。XSS的攻击对象是浏览器一端的普通用户。
示例:
">
您搜索的关键词是:<%= getParameter("keyword") %>
当浏览器请求
http://xxx/search?keyword=">
恶意代码,就会其执行
反射型 XSS
存储型 XSS 的攻击步骤:
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
存储型 XSS
反射型 XSS 的攻击步骤:
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
DOM型 XSS
DOM 型 XSS 的攻击步骤:
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS 防御
CSRF 原理
跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
曾在 09 年发生了著名的“谷歌邮箱窃取”事件,利用的就是 “CSRF”。
主要流程:
注:攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。
这告诉我们冲浪的时候不能随便点链接,是有风险哒。
CSRF 防御
防御策略:
SQL 注入原理
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。
SQL 注入防御
防御:用sql语句预编译和绑定变量,是防御sql注入的最佳方法。还可以通过严格检查参数的数据类型的方式来防御。
DNS 劫持原理
DNS劫持又称域名劫持,是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。其实本质就是对DNS解析服务器做手脚
DNS 劫持防御
解决办法:
DNS的劫持过程是通过攻击运营商的解析服务器来达到目的。我们可以不用运营商的DNS解析而使用自己的解析服务器或者是提前在自己的App中将解析好的域名以IP的形式发出去就可以绕过运营商DNS解析,这样一来也避免了DNS劫持的问题。
HTTP 劫持原理
在运营商的路由器节点上,设置协议检测,一旦发现是HTTP请求,而且是html类型请求,则拦截进行恶意处理。
常见有两种:
HTTP 劫持防御
防御方法:
这一 part 更多的是概念上的东西,不求完全记住,但是也要大致上能说一下。咱老话说的好:知之为知之 不知为不知,面试官在非关键性的问题上是不做严格要求的,千万别满嘴跑火车或者直接说不知道,这是两个极端,都不可取。
显然,前端面试“串联”问题不仅局限以上,本瓜会继续按照这个“串联联想”的思路去整理,不断去打破体系、形成体系(自己给自己挖的天坑呐)。期待在这个反复的过程中找寻真理的痕迹!
如果你对此文有更多想法(持续完善中),或是想与本瓜同行,可以加 Ifuckinghigh,期待更多交流 ~
我是掘金安东尼,一个会持续输出的个人站长。