Frontend Learning
收集于疯狂面试前端寻找实习工作的一段时间内。。。
最终在淘天实习啦🥰
TIP
上班摸鱼期间将具体的前端知识进行了一个分类
块级元素&行内元素
块级元素
- 总是从新的一行开始,即各个块级元素独占一行,默认垂直向下排列;
- 高度、宽度、margin及padding都是可控的,设置有效,有边距效果;
- 宽度没有设置时,默认为100%;
- 块级元素中可以包含块级元素和行内元素。
行内元素
- 和其他元素都在一行,即行内元素和其他行内元素都会在一条水平线上排列;
- 高度、宽度是不可控的,设置无效,由内容决定。
- 根据标签语义化的理念,行内元素最好只包含行内元素,不包含块级元素。
很多人经常会把块元素、行内块元素、行内元素的知识混淆掉,今天我归纳下,做个小结:
1、块元素独占一行,而行内块元素和行内元素可以和别人共处一行;
2、常见的块元素有div、p、h1、ol、ul、dl、tabel、form;
3、常见的行内元素有a、span、i、strong、em、label、cite、code;
4、常见的行内块元素有img、input、select、textarea、button;
5、行内块元素的特性和块元素完全一样,除了一点:它不独占一行;
6、块元素、行内块、行内元素都有各自的盒模型。三者的盒模型有相同的地方,也有不同的地方;
7、行内元素不能设置“width”和“height”,块元素和行内块元素可以设置“width”和“height”;
8、行内元素在水平方向上的padding、border、margin的特性和块元素一模一样;
9、行内元素在垂直方向上的padding、border、margin的特性和块元素很不一样:padding和border可以设置,但是不参与布局;margin设置都不能设置;
10、行内元素不能包含块元素,除了a元素;
11、行内元素可以包含别的行内元素;
12、行内元素脱离文档流后,会变成块元素;
13、可以给行内元素设置背景图片(“background-image”);
14、块元素的宽默认是父元素宽的100%,高由内容撑起来;
15、行内元素的宽和高都由内容撑起来;
16、其实块元素、行内块元素、行内元素之间可以互换,只需要给display属性赋不同的值(block、inline-block、inline)就可以了。
箭头函数
箭头函数没有自己的this,内部this值由外部非箭头函数的this决定。不能作为构造函数,会报错。内部是没有arguments对象,依赖于外部非箭头函数。 如果箭头函数想要拿到全部参数,也可以通过剩余参数语法获取到全部参数。
箭头函数与构造函数相比,缺少了很多东西,比如:caller,arguments,prototype,但同时也可以看到,箭头函数是有__proto__属性的,所以箭头函数本身是存在原型链的,他也是有自己的构造函数的,但是原型链到箭头函数这一环就停止了,因为它自己没有prototype属性,没法让他的实例的__proto__属性指向,所以箭头函数也就无法作为构造函数。 同时我们可以看到,由于箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数,不能绑定this,所以call()和apply()的第一个参数会被忽略。
浏览器缓存
强缓存:
强制缓存就是直接从浏览器缓存查找该结果,并根据结果的缓存规则来决定是否使用该缓存的过程。
- 不存在该缓存结果和标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)
- 存在缓存结果和标识,但结果已失效,强制缓存失效,则使用协商缓存
- 存在缓存结果和标识,并且结果未失效,强制缓存生效,直接返回该结果
控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。
Expires
Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。
到了HTTP/1.1,Expires已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间和服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区)发送误差,那么强制缓存则会直接失效。
TIP
Cache-Control是时间段,Expires是一个具体的时间点。
Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:
- public:所有内容都将被缓存(客户端/代理服务器/CDN等)
- private:只有客户端可以缓存,Cache-Control默认值
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- max-age=xxx:缓存将在xxx秒后失效
Cache-Control/Expires同时存在时,只有Cache-Control生效
协商缓存:
- Last-Modified和If-Modified-Since(绝对时间)
- ETag和If-None-Match
Last-Modified:服务器向浏览器发送最后的修改时间
If-Modified-Since:当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次向服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。
TIP
Last-Modifed/If-Modified-Since的时间精度是秒,而Etag可以更精确。 Etag优先级是高于Last-Modifed的,所以服务器会优先验证Etag Last-Modifed/If-Modified-Since是http1.0的头字段
跨域问题
指当一个网页试图访问来自不同源(域名、协议、端口)的资源时,浏览器会出于安全考虑而限制这种访问。这是因为浏览器的同源策略防止了恶意网站获取其他网站的敏感信息。
- CORS 是一种 W3C 标准,它允许服务器明确地允许来自其他源的请求。通过在服务器端设置适当的响应头,例如 Access-Control-Allow-Origin,可以指定允许哪些源进行跨域请求。这种方法需要服务器端的配合,因为需要在服务器上配置 CORS。
- JSONP 是一种利用 script 标签的跨域请求方式。它通过动态创建一个 script 标签,并将跨域请求的 URL 作为其 src 属性。服务器端需要将响应的数据以函数调用的形式返回,客户端通过定义对应的回调函数来接收数据。
INFO
事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。
- 代理:可以在本地搭建一个代理服务器,将跨域请求通过代理服务器转发到目标服务器。代理服务器可以处理 CORS 相关的问题,并将响应返回给客户端。
XSS&CSRF
XSS
XSS攻击又称CSS,全称Cross Site Script(跨站脚本攻击),其原理是攻击者向有XSS漏洞的网站中输入恶意的 HTML 代码,当用户浏览该网站时,这段 HTML 代码会自动执行,从而达到攻击的目的。
XSS攻击可以分成两种类型:
- 非持久型XSS攻击
- 持久型XSS攻击
非持久型xss攻击:顾名思义,非持久型xss攻击是一次性的,仅对当次的页面访问产生影响。非持久型xss攻击要求用户访问一个被攻击者篡改后的链接,用户访问该链接时,被植入的攻击脚本被用户游览器执行,从而达到攻击目的。
持久型xss攻击:持久型xss,会把攻击者的数据存储在服务器端,攻击行为将伴随着攻击数据一直存在。
解决方式:
- 输入验证:对所有用户输入进行严格的验证,确保它们符合预期的格式和类型。
- 输出编码:在将用户输入的数据插入到HTML页面时,对所有输出进行适当的编码,以防止浏览器将其解释为代码。
- 使用安全库:使用成熟的安全库来处理用户输入和输出,这些库通常提供了自动的转义功能。
- 内容安全策略(CSP):通过设置CSP,可以限制浏览器执行哪些类型的代码,从而减少XSS攻击的风险。
CSRF
一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。可以简单的理解为:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。攻击者只要借助少许的社会工程学的诡计,例如通过 QQ 等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使 Web 应用的用户去执行攻击者预设的操作。
预检请求
只有复杂请求才发送预检请求。非简单请求是复杂请求。
简单请求只有:
- 只可能有GET + POST +HEAD三种请求类型。而且必须满足下面的2和3两个特征,(POST请求也不全是简单请求)
TIP
HEAD请求是HTTP协议中的一种请求方法,与GET请求类似,但不返回实际的响应主体。它主要用于获取资源的元数据,检查资源是否存在以及验证资源是否被修改。
- 不能有自定义头字段 ,只能有以下几种:
- Accept
- Accept-Language
- Content-Type
- Content-Language
- Content-Type的值只能是以下三种:
- text/plain
- multipart/form-data 上传文件用的
- application/x-www-form-urlencoded 表单提交的数据类型,一般的post请求的数据类型
除此之外都是复杂请求。
复杂请求会先发送一个预检请求,方法为OPTIONS,只包含头部信息,不包含请求体,单独一个包发给服务器。包含如下字段:
- Origin: 发送请求的页面的源
- Access-Control-Request-Methods: 请求希望使用的方法
- Access-Control-Request-Headers: (可选)要使用的逗号分隔的自定义头部列表
WARNING
触发option条件:
前提是发生跨域请求 触发一定条件,例如post请求的Request headers 的 content-type为application/json 注意:
- 必须是request header
- get请求设置不了content-type,因为get会把参数拼接在url上。
例如:
Origin: http://www.nczonline.net
Access-Control-Request-Methods: POST
Access-Control-Request-Headers: NCZ, GGG
服务器拿到之后,可以确定是否允许这种类型的请求,然后在响应中发送如下头部信息(不发送响应体):
- Access-Control-Allow-Origin: http://www.xxx.xxx
- Access-Control-Allow-Methods: POST, GET
- Access-Control-Allow-Headers: NCZ, GGG
- Access-Control-Max-Age: 172800 // 这是缓存预检请求的秒数
缓存有效期内不会再次发送预检请求。只有当这个OPTIONS请求的响应状态为200时,才会继续发送真正的请求。
INFO
get&post传参区别: get参数放在url里面,2kb~4kb post通过请求体
HTTP不同版本区别
HTTP1.1
- 一个链接一次发送一个请求,如果先前一个请求没有到会阻塞后面的请求
- Keep-alive:多了容易被攻击,因此浏览器会对keep-alive进行数量的限制,例如六个(针对同一个域名),因此可以用多个域名,这样就可以同时进行下载(域名分片),或者合并文件等等等
缺陷:
- 队头阻塞(http):有些请求服务端处理很慢,这时候就会阻塞
- 头部信息没有压缩,cookies这种东西会一直携带
TIP
出现了雪碧图(精灵图),只用一个请求
Http2
- 多路复用 报文首部和内容都压缩了 报文变成了帧:人的可读性差了且多路复用
- 服务器推送:可以提前发送多个文件,但是存在安全问题
- 使用TCP但是TCP太慢了:传输层要求数据包必须按顺序串行传输
在http2中,新增二进制分帧层。核心概念:帧和流
- 帧是数据传输的最小单位,http2会将请求和响应报文分为头部帧和数据帧。
- 流即是一次资源的请求。一个流由多个帧组成,用流ID标识。通过流ID关联,因此可以乱序请求(eg:流2帧2,流2帧1)。
http2通信双方可以并发请求和响应。不同文件标识不同的流,然后可以在一个连接中发送,这就是多路复用!!!
压缩
hpack算法:减少传输文件体积,尤其是http头部信息。
多路复用
多路复用是指在网络通信中,通过在单个连接上同时传输多个独立的数据流,从而实现并行传输和提高网络利用率的技术。在传统的TCP协议中,每个TCP连接只能传输一个数据流,当其中一个数据流出现延迟或丢包时,整个连接都会受到影响,导致其他数据流也受阻。而多路复用则改变了这种情况,它允许在同一个连接上同时传输多个数据流,各个数据流之间相互独立,互不影响。
具体来说,多路复用可以让客户端和服务器在同一个连接上建立多个逻辑通道(也称为流),每个逻辑通道负责传输不同的数据流。这样,在一个连接上可以同时进行多个数据流的传输,而且各个数据流之间是独立的,不会相互影响。这样做可以有效避免头阻塞(Head-of-Line Blocking)等问题,提高数据传输的效率和性能。
Http3
- 使用QUIC和UDP
- QUIP约等于TLS+https
- 封装成quic帧
- 如果IP变了,可以通过连接ID快速识别同一个会话
QUIC
有多路复用,流量控制,拥塞控制,加密等等。UDP是没有顺序要求的。 UDP包中有QUIC包,QUIC包中有QUIC帧,帧组合成QUIC流。(QUIC感觉是应用层协议)
TIP
TCP+TLS1.2= 3RTT
TCP+TLS1.3=2RTT
QUIC = 1RTT TCP和TLS的握手整合在一起了
Type&Interface
- 类型别名可以是基本类型,脸和类型或是元祖类型,接口不行
- 同名接口可以自动扩展,类型不行。
- 类型用&扩展,接口用implements扩展
事件委托
- 核心:事件冒泡,父节点来统筹响应子节点的事件,为dom树最高层添加事件即可理解成事件委托。
- 优点:节省内存开销,提高性能,可以从性能优化角度来概述。
- 缺点:应当根据实际情况来使用,容易造成事件误判。
this关键字
- 在函数体中,非显式或隐式地调用函数时,在严格模式下,函数内的this会被绑定到 undefined 上,在非严格模式下则会绑定到全局对象 window/global 上
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会绑定到实例对象上
- 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到 call/apply/bind 方法指定参数的对象上
- 一般通过执行上下文对象调用函数时,this会指向最后调用它的对象
- 在箭头函数中,this的指向是由外层(函数或全局)作用域决定的
Cookies
Cookie 的存储大小限制是对整个域名的,而不是每个单独的 cookie。这个限制是由浏览器设定的,不同的浏览器可能会有不同的限制,但通常,对于整个域名,cookie 的最大存储大小约为 4KB。
这意味着,对于一个特定的域,所有的 cookie(包括名称、值、过期日期、路径等)加起来的大小不能超过这个限制。如果你尝试添加超过限制的 cookie,可能会导致新的 cookie 无法设置,或者旧的 cookie 被覆盖。
WARNING
请注意,由于这个大小限制,cookie 不适合存储大量的数据。如果你需要在客户端存储大量数据,应该考虑使用其他技术,如 Web Storage(localStorage 和 sessionStorage)或 IndexedDB,这些技术提供了更大的存储空间。
JWT
json web token 存在本地的token,例如cookies可能会被篡改,通过签名保证本地不会被篡改。 服务端发送info和服务器私钥加密过的info(签名),客户端返回要返回info和签名。
session在浏览器崩溃后,点击“恢复”按钮是否还会存在?
会话(session)的生命周期取决于你如何设置它。在大多数情况下,如果浏览器崩溃,会话可能会丢失,除非你的应用程序特别设计来持久化会话。
如果你在浏览器中使用了会话存储(sessionStorage),那么当浏览器崩溃并重新启动后,数据将不会存在。sessionStorage 是为每个会话(窗口或标签页)保存数据的,当会话结束(即窗口或标签页关闭)时,数据将被清除。
另一方面,如果你使用了本地存储(localStorage),那么即使浏览器崩溃,数据也会持久化。localStorage 保存的数据没有过期时间,除非用户手动清除浏览器数据或者通过代码清除。
如果你在服务器端处理会话,那么会话的持久性取决于你的服务器如何处理会话。例如,你可以将会话数据存储在数据库中,这样即使服务器或浏览器崩溃,只要用户的会话令牌(通常在 cookie 中)仍然有效,他们就可以恢复会话。但是,如果会话令牌丢失,或者会话在服务器端过期,那么用户将无法恢复会话。
http-only
HttpOnly 是一个设置在 HTTP cookie 中的标志。当设置为 HttpOnly,这个 cookie 不能通过客户端脚本(例如 JavaScript)来访问。这是一种安全措施,用来防止跨站脚本攻击(XSS)。
在跨站脚本攻击中,攻击者可能会尝试通过在用户浏览器中执行恶意脚本来窃取他们的 cookie。如果 cookie 包含敏感信息(如用户的会话 ID),这可能会导致未授权的用户访问和信息泄露。通过将 cookie 设置为 HttpOnly,可以防止这种攻击,因为 cookie 不能被 JavaScript 访问。
然而,虽然 HttpOnly 可以提高安全性,但它并不能防止所有类型的攻击。例如,它不能防止跨站点请求伪造(CSRF)攻击,这种攻击通过欺骗用户浏览器发送恶意请求来进行。
这就是为什么在处理 cookie 时,你应该使用多种安全措施。除了使用 HttpOnly,还应该设置 Secure 标志(这将限制 cookie 只能通过 HTTPS 连接发送),并考虑使用同源策略和内容安全策略来进一步保护你的网站。
页面如何渲染的?
- 处理HTML标记并构建DOM树
- 字符编码
- 令牌化:将字符串转化为各种令牌
- 生成节点对象
- 构建DOM树
- 处理CSS标记并构建CSSOM树
INFO
CSSOM 树描述了选择器之间的层级关系,可以在浏览器开发者工具 Console 面板通过document.styleSheets
命名查看
将DOM和CSSOM合并成一个渲染树
根据渲染树来布局,以计算每个节点的几何信息 盒子模型在这里出现。
将各个节点绘制到屏幕上
我们所说的渲染:
- 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
- 将布局绘制(paint)在屏幕上,显示出整个页面。
重绘 & 重排
- 重绘:某些元素的外观被改变,例如:元素的填充颜色
- 重排:重新生成布局,重新排列元素。
就如上面的概念一样,单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分。比如改变元素高度,这个元素乃至周边dom都需要重新绘制。
也就是说:重绘不一定导致重排,但重排一定会导致重绘。
下面情况会发生重排:
- 页面初始渲染,这是开销最大的一次重排
- 添加/删除可见的DOM元素
- 改变元素位置
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等
- 改变元素内容,比如文字数量,图片大小等
- 改变元素字体大小
- 改变浏览器窗口尺寸,比如resize事件发生时
- 激活CSS伪类(例如::hover)
- 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
- 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用 getComputedStyle方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。
重排优化建议:
重排的代价是高昂的,会破坏用户体验,并且让UI展示非常迟缓。通过减少重排的负面影响来提高用户体验的最简单方式就是尽可能的减少重排次数,重排范围。下面是一些行之有效的建议,大家可以用来参考。
减少重排范围
我们应该尽量以局部布局的形式组织html结构,尽可能小的影响重排的范围。
尽可能在低层级的DOM节点上,而不是像上述全局范围的示例代码一样,如果你要改变p的样式,class就不要加在div上,通过父元素去影响子元素不好。
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
减少重排次数
- 样式集中改变
不要频繁的操作样式,对于一个静态页面来说,明智且可维护的做法是更改类名而不是修改样式,对于动态改变的样式来说,相较每次微小修改都直接触及元素,更好的办法是统一在 cssText 变量中编辑。虽然现在大部分现代浏览器都会有 Flush 队列进行渲染队列优化,但是有些老版本的浏览器比如IE6的效率依然低下。
- 分离读写操作
DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
将 DOM 离线 “离线”意味着不在当前的 DOM 树中做修改,我们可以使用 display:none
使用 absolute 或 fixed 脱离文档流
优化动画
优化渲染关键方案
通过优化渲染关键路径,可以优化页面渲染性能,减少页面白屏时间。
优化JS:JavaScript文件加载会阻塞DOM树的构建,可以给<script>
标签添加异步属性async,这样浏览器的HTML解析就不会被js文件阻塞。
优化CSS:浏览器每次遇到<link>
标签时,浏览器就需要向服务器发出请求获得CSS文件,然后才继续构建DOM树和CSSOM树,可以合并所有CSS成一个文件,减少HTTP请求,减少关键资源往返加载的时间,优化渲染速度。
其他方案
- 加载部分HTML
- 压缩
- 图片加载优化
- 小图标合并成雪碧图,进而减少img的HTTP请求次数;
- 图片加载较多时,采用懒加载的方案,用户滚动页面可视区时再加载渲染图片。
- HTTP缓存
typeof
typeof 对于确定基本类型(如 number,string,boolean,undefined,symbol)非常有用,但对于识别更复杂的对象类型(如数组或日期)则不太有用。例如,typeof [] 和 typeof {} 都会返回 'object',尽管前者是数组,后者是对象。
如果你需要区分数组和其他类型的对象,可以使用 Array.isArray() 函数,如 Array.isArray([]) 将返回 true。
DANGER
typeof null 返回 'object' 是 JavaScript 的一个历史遗留问题,实际上 null 不是对象。
js简单数据类型&引用数据类型
- 简单(原始)数据类型:这些数据类型的值直接存储在栈(Stack)内存中。当你将一个原始类型的变量赋值给另一个变量时,JavaScript 会创建这个值的一个副本,并将这个副本赋值给新的变量。因此,改变一个变量的值不会影响到其他变量。
- 引用数据类型:对象(包括数组和函数)是引用数据类型。这些类型的值存储在堆(Heap)内存中。当你将一个对象赋值给另一个变量时,实际上是将内存地址(也就是引用)赋值给了新的变量,因此两个变量实际上指向的是堆内存中的同一个对象。因此,改变一个变量会影响到其他所有指向同一个对象的变量。
这种存储方式的差异也解释了为什么在 JavaScript 中,简单数据类型是按值传递,而对象是按引用传递。
悲观锁&&乐观锁
悲观锁(Pessimistic Locking):
悲观锁假定数据往往会发生并发修改的情况,因此在数据被读取时就直接加锁,以防止数据被其他事务修改。这种方式可以确保数据的一致性,但如果并发冲突并不频繁,那么它可能会带来不必要的开销,因为每次读取数据时都需要加锁和解锁。悲观锁主要用在数据并发修改概率较高,或者需要保证每次读取的数据都是最新的场景。
乐观锁(Optimistic Locking):
乐观锁假定数据一般情况下不会发生并发修改,因此在读取数据时不会加锁,而是在更新数据时检查数据是否已经被其他事务修改。如果在当前事务读取数据后,数据已经被其他事务修改过,那么当前事务会回滚并给出错误。乐观锁主要用在数据并发修改概率较低,或者读操作远多于写操作的场景。
流量控制和拥塞控制
- 流量控制:流量控制是一种防止发送方发送过多数据给接收方的机制,防止接收方来不及处理这些数据导致数据丢失。这是通过滑动窗口机制实现的,接收方通过 ACK 报文告诉发送方它的接收窗口的大小(即还能接收的数据量),发送方根据这个窗口的大小来控制发送的数据量。
- 拥塞控制:拥塞控制是一种防止过多的数据注入到网络中,避免网络拥塞的机制。主要有四种算法:慢启动、拥塞避免、快重传和快恢复。
- 慢启动(Slow Start):慢启动算法的思想是当一个连接建立时,首先设置一个小的拥塞窗口大小,然后每收到一个 ACK,窗口大小就增加1,这样窗口大小就以指数方式增长,直到达到一个阈值或者出现丢包。
- 快速重传(Fast Retransmit):在正常情况下,如果一个报文段丢失,发送方需要等待超时后才能重传这个报文段。但是如果发送方连续收到三个重复的 ACK,那么可以确定有一个报文段丢失,发送方就会立即重传这个报文段,而不需要等待超时,这就是快速重传。
event.currentTarget
event.currentTarget 属性总是指向绑定事件处理器的元素,而 event.target 属性指向触发事件的元素。
在事件冒泡过程中,event.target 是固定不变的,而 event.currentTarget 是随着事件的传播而变化的。如果你在父元素上使用事件委托,那么 event.target 可能是子元素,而 event.currentTarget 是父元素。
事件循环
宏任务和微任务
宏任务主要包含:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI 交互事件。
微任务主要包含:Promise、MutationObserver 等。
Why 事件循环?
由于 JavaScript 是单线程的,且 JavaScript 主线程和渲染线程互斥,如果异步操作(如上图提到的 WebAPIs)阻塞 JavaScript 的执行,会造成浏览器假死。而事件循环为浏览器引入了任务队列(task queue),使得异步任务可以非阻塞地进行。
浏览器事件循环在处理异步任务时不会一直等待其返回结果,而是将这个事件挂起,继续执行栈中的其他任务。当异步事件返回结果,将它放到任务队列中,被放入任务队列不会立刻执行回调,而是等待当前执行栈中所有任务都执行完毕,主线程处于空闲状态,主线程会去查找任务队列中是否有任务,如果有,取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,执行其中的同步代码。
Node.js 中的事件循环
在 Node.js 中,事件循环表现出的状态与浏览器中大致相同。不同的是 Node.js 中有一套自己的模型。 Node.js 中事件循环的实现是依靠的 libuv 引擎。下图简要介绍了事件循环操作顺序:
BFC
BFC 全称:Block Formatting Context, 名为 "块级格式化上下文"。
W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性。
这里简单列举几个触发BFC使用的CSS属性
- overflow: hidden
- display: inline-block
- position: absolute
- position: fixed
- display: table-cell
- display: flex
规则:
- BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列
- BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签
- 垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠
- 计算BFC的高度时,浮动元素也参与计算
ES Module
ECMAScript模块(简称ES模块)是一种JavaScript代码重用的机制,于2015年推出,一经推出就受到前端开发者的喜爱。在2015之年,JavaScript 还没有一个代码重用的标准机制。多年来,人们对这方面的规范进行了很多尝试,导致现在有多种模块化的方式。
你可能听说过AMD模块,UMD,或CommonJS,这些没有孰优孰劣。最后,在ECMAScript 2015中,ES 模块出现了。我们现在有了一个“正式的”模块系统。
INFO
在早期JavaScript模块都是通过script标签引入js文件代码。当然这写基本简单需求没有什么问题,但当我们的项目越来越庞大时,我们引入的js文件就会越多,这时就会出现以下问题:
- js文件作用域都是顶层,这会造成变量污染
- js文件多,变得不好维护
- js文件依赖问题,稍微不注意顺序引入错,代码全报错
为了解决以上问题JavaScript社区出现了CommonJs,CommonJs是一种模块化的规范,包括现在的NodeJs里面也采用了部分CommonJs语法在里面。那么在后来Es6版本正式加入了Es Module模块,这两种都是解决上面问题,那么都是解决什么问题呢。
- 解决变量污染问题,每个文件都是独立的作用域,所以不存在变量污染
- 解决代码维护问题,一个文件里代码非常清晰
- 解决文件依赖问题,一个文件里可以清楚的看到依赖了那些其它文件
- 导出
在Es Module中导出分为两种,单个导出(export)、默认导出(export default),单个导出在导入时不像CommonJs一样直接把值全部导入进来了,Es Module中可以导入我想要的值。那么默认导出就是全部直接导入进来,当然Es Module中也可以导出任意类型的值。
- 混合导出
可以使用export和export default同时使用并且互不影响,只需要在导入时地方注意,如果文件里有混合导入,则必须先导入默认导出的,在导入单个导入的值。
- 导入
Es Module使用的是import语法进行导入。如果要单个导入则必须使用花括号{} ,注意:这里的花括号跟解构不一样。
- 混合导入
混合导入,则该文件内用到混合导入,import语句必须先是默认导出,后面再是单个导出,顺序一定要正确否则报错
注意
- 导入的值是引用,仅可读
- 是静态,不可用if等方式控制import
CommonJs
基本语法
- 导出
CommonJs中使用module.exports导出变量及函数,也可以导出任意类型的值,看如下案例。
// 导出一个对象
module.exports = {
name: "蛙人",
age: 24,
sex: "male"
}
// 导出任意值
module.exports.name = "蛙人"
module.exports.sex = null
module.exports.age = undefined
- 混合导出
混合导出,exports和module.exports可以同时使用,不会存在问题。
- 导入
CommonJs中使用require语法可以导入,如果想要单个的值,可以通过解构对象来获取。
// index.js
module.exports.name = "蛙人"
module.exports.age = 24
let data = require("./index.js")
console.log(data) // { name: "蛙人", age: 24 }
CommonJs和Es Module的区别
CommonJs
- CommonJs可以动态加载语句,代码发生在运行时
- CommonJs混合导出,还是一种语法,只不过不用声明前面对象而已,当我导出引用对象时之前的导出就被覆盖了
- CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染
Es Module
- Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
- Es Module混合导出,单个导出,默认导出,完全互不影响
- Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改
ECMAScript
ECMAScript是由网景的布兰登·艾奇开发的一种脚本语言的标准化规范;最初命名为Mocha,后来改名为LiveScript,最后重命名为JavaScript。1995年12月,升阳与网景联合发表了JavaScript。1996年11月,网景公司将JavaScript提交给欧洲计算机制造商协会进行标准化。ECMA-262的第一个版本于1997年6月被Ecma组织采纳。ECMA Script是ECMA-262标准化的脚本语言的名称。尽管JavaScript和JScript与ECMAScript兼容,但包含超出ECMA Script的功能。
ECMAScript是一种可以在宿主环境中执行计算并能操作可计算对象的基于对象的程序设计语言。ECMAScript最先被设计成一种Web脚本语言,用来支持Web页面的动态表现以及为基于Web的客户机—服务器架构提供服务器端的计算能力。但作为一种脚本语言, ECMAScript具备同其他脚本语言一样的性质,即“用来操纵、定制一个已存在系统所提供的功能,以及对其进行自动化”。
Web Worker
一个 worker 是使用一个构造函数创建的一个对象(例如 Worker())运行一个命名的 JavaScript 文件——这个文件包含将在 worker 线程中运行的代码; worker 运行在另一个全局上下文中,不同于当前的window。因此,在 Worker 内通过 window 获取全局作用域(而不是self)将返回错误。
在专用 worker 的情况下,DedicatedWorkerGlobalScope 对象代表了 worker 的上下文(专用 worker 是指标准 worker 仅在单一脚本中被使用;共享 worker 的上下文是 SharedWorkerGlobalScope (en-US) 对象)。一个专用 worker 仅能被首次生成它的脚本使用,而共享 worker 可以同时被多个脚本使用。
在 worker 线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在 worker 内,不能直接操作 DOM 节点,也不能使用 window 对象的默认方法和属性。但是你可以使用大量 window 对象之下的东西,包括 WebSockets,以及 IndexedDB 等数据存储机制。查看 Web Workers 可以使用的函数和类 获取详情。
workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用 postMessage() 方法发送各自的消息,使用 onmessage 事件处理函数来响应消息(消息被包含在 message 事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。
只要运行在同源的父页面中,worker 可以依次生成新的 worker;并且可以使用 XMLHttpRequest 进行网络 I/O,但是 XMLHttpRequest 的 responseXML 和 channel 属性总会返回 null。
js为什么是单线程?
因为 Javascript 作为一门浏览器端的脚本语言,主要的任务就是处理用户的交互,而用户的交互无非就是响应 DOM 上的一些事件/增删改 DOM 中的元素。
对于响应事件是异步处理的,但事件循序也是在单线程中进行的,所有的(可能不太准确)都是被加入到 macro 事件队列中的,一次事件循环也只处理一个事件响应。
所以说 Javascript 被设计成单线程,主要的原因还是在于操作 DOM ,包括在异步的事件处理器中操作 DOM。
设想,如果 Javascript 被设计为多线程的程序,那么操作 DOM 必然会涉及到资源的竞争,那么这么语言必然会被实现的非常臃肿,那么在 Client 端中跑这么一门语言的程序,资源消耗和性能都将是不乐观的,同时在 Client 实现多线程还真没有那么刚性的需求;但是如果设计成单线程,并辅以完善的异步队列来实现,那么运行成本就会比多线程的设计要小很多了。
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。大部分浏览器使用结构化克隆来实现该特性。
分为专用worker和共享worker
BOM是什么?
- BOM即浏览器对象模型。
- BOM提供了独立于内容而与浏览器窗口进行交互的对象;
- 由于BOM主要用于管理窗口与窗口之间的通讯,因此其核心对象是window;
- BOM由一系列相关的对象构成,并且每个对象都提供了很多方法与属性;
- BOM缺乏标准,JavaScript语法的标准化组织是ECMA,DOM的标准化组织是W3C,BOM最初是Netscape浏览器标准的一部分。
总体来说,BOM主要针对的是浏览器窗口和子窗口,但是通常会把任何特定于浏览器的扩展都归于在BOM的范畴内。下面是一些拓展:
- 弹出新浏览器窗口的能力。
- 移动、缩放和关闭浏览器窗口的详近信息。
- navigator对象,提供关于浏览器的详尽信息。
- location对象,提供浏览器加载页面的详尽信息。
- screen对象,提供关于用户屏幕分辨率的详尽信息。
- performance对象,提供浏览器内存占用、导航行为和时间统计的详尽信息。
- 对cookie的支持。
eg:
- 页面加载事件
window.onload 是窗口 (页面)加载事件,当文档内容完全加载完成会触发该事件(包括图像、脚本文件、CSS 文件等), 就调用的处理函数。
DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash等等。(IE9以上才支持!!!)如果页面的图片很多的话, 从用户访问到onload触发可能需要较长的时间, 交互效果就不能实现,必然影响用户的体验,此时用 DOMContentLoaded 事件比较合适。
- 调整窗口大小事件
window.onresize 是调整窗口大小加载事件, 当触发时就调用的处理函数。
注意:只要窗口大小发生像素变化,就会触发这个事件。
我们经常利用这个事件完成响应式布局。 window.innerWidth 当前屏幕的宽度
async和defer
异步编程有哪些方法可以实现?
实现异步编程的方法包括:回调函数、Promise对象、async/await、事件监听、Generator函数等。
v8引擎的垃圾回收算法
V8引擎使用了分代式垃圾回收算法,将内存分为新生代和老生代。
新生代采用Scavenge算法,将内存一分为二,一部分为From空间,一部分为To空间,通过对象的存活与复制来实现垃圾回收。
老生代采用标记-清除和标记-整理相结合的方式进行垃圾回收,通过标记存活对象和整理内存来保证内存的高效利用。
虚拟dom
虚拟 DOM 是一个虚拟的、存在于内存中的 DOM 树结构,它是对真实 DOM 的抽象表示。通过比较虚拟 DOM 和上一次渲染的虚拟 DOM 的差异,可以最小化对实际 DOM 的操作,从而提高页面渲染效率。
虚拟dom一定能提高性能吗?为什么?
虚拟DOM并非一定能提高性能。其优势在于减少直接操作DOM所带来的性能损耗,但在小规模应用中可能带来额外开销。合理使用虚拟DOM需要权衡其带来的复杂性和实际性能提升,结合具体情况进行评估。
怎么分析性能?
分析前端性能可通过浏览器开发者工具(Network、Performance)、Lighthouse、WebPageTest等工具监测加载时间、资源占用、DOM操作等;利用性能指标(TTFB、FCP、FID、LCP、CLS)评估页面加载速度和交互性;检查代码质量、优化图片、减少HTTP请求、启用缓存等优化手段提升性能。
node
Node.js是一个基于事件驱动、非阻塞式的I/O模型来实现的服务端JavaScript运行环境,是基于Google的V8引擎来实现的单线程、高性能运行在服务端的JavaScript语言。
- 事件驱动
从上图可以看出如下几部分:
- Application应用层,即JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs等
- V8这一层是V8引擎层,这一层的主要作用是解析JavaScript,同时和应用层和NodeApi层交互
- NodeApi为上层模块提供系统调用,和操作系统进行交互 。
- Libuv是跨平台的底层封装,实现了线程池、事件循环、文件操作等,是 Node.js 实现异步的核心 。
在Libuv层维护了一个Event Queue的事件队列,当有请求过来时,经过Node.js的应用层和NodeApi层将请求作为一个事件放到Event Queue事件队列中,并设置回调事件函数,然后继续接受新的请求。
在Libuv层的Event Loop事件循环不断读Event Queue中的事件,在读取事件的过程中如果遇到非阻塞事件,会自已处理,并且在处理完后调用回调函数向上一层返回结果;对于阻塞事件,会委托给后台线程池来处理,当这些阻塞操作完成后,执行结果与提供的回调函数一起再被放入事件队列中。当Event Loop再次读到这个事件时,会再次执行被放到队列中的事件回调函数,最后将结果返回给上一层。具体流程可以参考下图:
- 异步非阻塞 当遇到比较耗时的操作时,采用异步和非阻塞的方式进入事件队列,不影响后面请求的执行。事件循环会读取到这个耗时请求,交给线程池来处理。当这些耗时的操作处理完后会再次进入事件队列,通过事件循环和回调来将请求结果返回给上一层应用,最后返回给客户端。通过以上方式减少了高并发时的等待,从而可以从容应对高并发。
DANGER
- libuv 主要负责处理异步 I/O 操作和事件循环,使得 Node.js 能够实现非阻塞的 I/O 编程模型。
- Chrome V8 是 JavaScript 引擎,负责解释和执行 JavaScript 代码,提供高性能的代码执行环境。
- 在 Node.js 中,libuv 和 Chrome V8 共同协作,libuv 负责处理底层的 I/O 操作和事件循环,而 Chrome V8 负责执行 JavaScript 代码,二者共同构成了 Node.js 的核心功能。
node如何在多核cpu上跑满?
DANGER
node是单线程的。
- 使用 Cluster 模块: Node.js 提供了 Cluster 模块,可以创建子进程来利用多核 CPU。
- 使用 PM2 进程管理工具: PM2 是一个流行的 Node.js 进程管理工具,它可以帮助管理应用程序的生命周期,并支持多进程部署。
- 使用反向代理负载均衡: 通过 Nginx、Apache 等反向代理服务器,可以将请求转发给多个 Node.js 实例,实现负载均衡。
Ajax
Ajax(Asynchronous Javascript And XML),即是异步的JavaScript和XML,Ajax其实就是浏览器与服务器之间的一种异步通信方式
非Ajax方式
- from表单提交数据。
- 超链接发送请求。
- window.localtion.href。
api:
XMLHttpRequest (axios基于此)
Fetch
默认无Cookie
与XMLHttpRequest不同,Fetch并不会默认发送cookie,因此应用的身份验证可能会失败。
错误不会被拒绝
令人惊讶的是,HTTP错误(例如404 Page Not Found 或 500 Internal Server Error)不会导致Fetch返回的Promise标记为reject;.catch()也不会被执行。想要精确的判断 fetch是否成功,需要包含 promise resolved 的情况,此时再判断 response.ok是不是为 true
不支持超时
Fetch不支持超时,只要浏览器允许,请求将继续。
原型和原型链
原型:prototype 原型链:__proto__ / [[prototype]]
prototype属性是function特有的!!!
每一个构造函数都有一个 prototype 属性,指向另一个对象。这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
它是为了解决构造函数存在浪费内存的问题,达到资源共享。
let a = function() {}
console.log(a.prototype) //这里是不会报错的,打印出来是{}
a.prototype.name = 'handsome'
为什么要有这个特性?因为我们要用这个进行继承。可以给函数加载属性。
function Person() {
}
Person.prototype.age = 18
Person.prototype.getAge = function() {
console.log(this.age)
}
let person1 = new Person()
person1.age = 28
person1.getAge() //这里会打印28,若当前实例就会顺着原型链上一层一层找直到找到null,如果都没有返回undefined
判断这个age是谁的:
person1.hasOwnProperty(age)
备注:
使用 __proto__ 是有争议且不被鼓励的。它的存在和确切行为仅作为遗留特性被标准化,以确保 Web 兼容性,但它存在一些安全问题和隐患。为了更好的支持,请优先使用 Object.getPrototypeOf()/Reflect.getPrototypeOf() 和 Object.setPrototypeOf()/Reflect.setPrototypeOf()。
所有都有原型链。
- prototype:每一个构造函数对象都有一个prototype属性,指向的是该构造函数的原型对象。
- __proto__:每一个实例对象都有一个__proto__属性,指向构造函数的原型对象。
- 所有的引用类型的__proto__属性值均指向它的构造函数的prototype的属性值
- constructor:实例对象原型__proto__和构造函数prototype原型对象里面都有一个属性 constructor 属性 ,都指向了构造函数。
Object->Function->Person->person1
SSR
vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。
服务端渲染就是先向后端服务器请求数据,然后生成完整首屏html返回给浏览器
服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬虫可以抓取到完整的页面信息
SSR另一个很大的作用是加速首屏渲染,因为无需等待所有的JavaScript都完成下载并执行,才显示服务端渲染的标记,所以用户会更快地看到完整渲染的页面.
js引擎
js
JavaScript 是一门解释型语言,与 C/Golang 等静态编译语言不同。静态编译型语言通过编译器直接将代码转化为机器码,然后运行在机器上;JS 是先经过编译产生字节码,然后在虚拟机上运行字节码(这点与 Java&JVM 很相似),性能虽不及静态编译型语言,但获得了更多的灵活性。
JS Engine 其实就可以理解为上文中所说的虚拟机。机器底层的 CPU 只能执行指令集中的指令(即对应的汇编代码),无法直接识别高级语言。JS Engine 可以将 JS 代码编译为字节码,然后执行代码,同时还提供了分配内存和垃圾回收的功能,极大程度上减轻了开发人员的工作量,何乐而不为。
从本质上来讲,JS Engine 就是一段程序,用于实现上述功能。
互联网中最常见、使用最广泛的 JS Engine 是 Google V8。Google V8 是用 C++ 编写的开源高性能 JS Engine(同时也是 WebAssembly Engine),目前已被用于 Chrome 浏览器、Node.js、MongoDB 等多个知名项目。Chrome 占据了全球浏览器市场 60% 的份额,而 Node.js 已然成为服务器端 JS 代码的执行标准,由此可见 V8 使用之广泛。
除此之外,还有一些常见的 JS Engine:
- 由 Mozilla 为 Firefox 开发的 SpiderMonkey
- 为 Safari 浏览器提供支持的 JavaScriptCore
- 为 IE 提供支持的 Chakra
- 本篇接下来的 JS Engine 都默认为 Google V8。
V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
js runtime 运行时
我们可以把 JS Runtime 理解为一栋房子,JS 代码都需要在这栋房子中运行。而这栋房子由很多部分共同组成,包括 JS Engine、外部 API 和回调队列(callback queue)。有时也把 JS 用到的 core lib 核心库看作 Runtime 的一部分。
以 Chrome 浏览器的 JS Runtime 举例,浏览器的 Runtime 由对应的 JS Engine、Web API 和回调队列组成。JS Engine 在上文中有讲,不再赘述;Web API 是浏览器提供给 Engine 的一系列接口,并不是 JS 的一部分,目的是方便操纵数据和增强浏览器的功能,常用的 Web API 包括 DOM、Web Worker 等;回调队列包括准备好执行的回调函数,回调队列确保回调以先进先出(FIFO)方法执行,并在堆栈为空时将其传递到堆栈中。
与浏览器 Runtime 不同的是,Node.js 没有 Web API,而是有叫作 C++ 绑定和线程池的其他部分。
我们可以这样说,Chrome 和 Node.js 中的 JS 代码都依赖于 V8 运行,但它们运行在不同的 Runtime 中。
内核、引擎、runtime
浏览器内核又可以分成两部分:渲染引擎(layout engineer 或者 Rendering Engine)和 JS 引擎。
浏览器内核负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。
JS 引擎则是解析 Javascript 语言,执行 javascript 语言来实现网页的动态效果。最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。有一个网页标准计划小组制作了一个 ACID 来测试引擎的兼容性和性能。内核的种类很多,如加上没什么人使用的非商业的免费内核,可能会有 10 多种,但是常见的浏览器内核可以分这四种:
- Trident
- Gecko
- Blink
- Webkit
垃圾回收机制
- 引用计数
最早的也是最简单的垃圾回收实现方法,这种方法为占用物理空间的对象附加一个计数器,当有其他对象引用这个对象时计数器加一,反之引用解除时减一。这种算法会定期检查尚未被回收的对象的计数器,为零的话则回收其所占物理空间,因为此时的对象已经无法访问。
- 标记清除法
标记清除法主要将 GC 的垃圾回收过程分为标记阶段和清除两个阶段:
- 标记阶段:把所有活动对象做上标记;
- 清除阶段:把没有标记(也就是非活动对象)销毁。
JavaScript 中最常用的垃圾回收方式就是标记清除(mark-and-sweep),当变量进入环境时,就将这个变量标记 “进入环境”,当变量离开环境时,就将其标记为 “离开环境”。
V8:
- 通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
- 通过 GC Root 遍历到的对象,我们就认为该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,我们也称可访问的对象为活动对象;
- 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,我们称不可访问的对象为非活动对象。
在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):
- 全局的 window 对象(位于每个 iframe 中);
- 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
- 存放栈上变量。
- 回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
- 做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)。
惰性解析
所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
基本原理如下:
标记阶段(Marking Phase):从根对象开始,通过遍历对象引用关系,标记所有活动对象(即可访问到的对象)。垃圾回收器会从全局对象(如window对象)开始,递归遍历对象的属性、字段等,将可访问的对象进行标记。
清除阶段(Sweeping Phase):在清除阶段,垃圾回收器会扫描堆内存中的所有对象,对于未被标记的对象,即未被标记为活动对象的对象,将其判定为垃圾对象,并进行回收释放内存。
内存整理(Compaction):清除阶段后可能会产生不连续的内存空间,为了纠正这种内存碎片化的情况,还需要进行内存整理。内存整理的过程是将活动对象按照一定规则移动到一端,从而形成连续的内存块,以便后续的内存分配。
优缺点
- 优点:可以处理循环引用(两个或多个对象相互引用)的情况。
- 缺点:垃圾回收的过程会造成一些暂停,可能导致性能问题。
闭包怎么释放变量
内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。闭包里面的变量就是我们需要的变量,不能说是内存泄露。
闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript的问题。在 IE浏览器中,由于BOM和DOM中的对象是使用 C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
避免内存泄漏的方法
- 少用全局变量,避免意外产生全局变量
- 使用闭包要及时注意,有Dom元素的引用要及时清理。
- 计时器里的回调没用的时候要记得销毁。
- 为了避免疏忽导致的遗忘,我们可以使用 WeakSet 和 WeakMap结构,它们对于值的引用都是不计入垃圾回收机制的,表示这是弱引用。基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
Map & WeakMap
Map 和 WeakMap 之间的主要区别:
- Map 对象的键可以是任何类型,但 WeakMap 对象中的键只能是对象引用;
- WeakMap 不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制);
- WeakMap 对象是不可枚举的,无法获取集合的大小。
原生的 WeakMap 持有的是每个键对象的 “弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。
babel
用通俗的话解释就是它主要用于将高版本的JavaScript代码转为向后兼容的JS代码,从而能让我们的代码运行在更低版本的浏览器或者其他的环境中。
Babel的运行方式总共可以分为三个阶段:解析(parsing)、转换(transforming)和生成(generating);负责解析阶段的插件是@babel/parser,其作用就是将源码解析成AST;而负责生成阶段的插件是@babel/generator,其作用就是将转好好的AST重新生成代码。
Taro就是利用 babel 完成的小程序语法转换
hash路由和history路由
hash路由
vue-router默认为hash模式,使用URL的hash来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;#就是hash符号,中文名为哈希符或者锚点,在hash符号后的值称为hash值。
路由的hash模式是利用了windo可以监听onhashchange事件来实现的,也就是说hash值是用来指导浏览器动作的,对服务器没有影响,HTTP 请求中也不会包括hash值,同时每一次改变hash值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash模式是根据hash值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。
特点
- url中带一个 # 号
- 可以改变URL,但不会触发页面重新加载(hash的改变会记录在 window.hisotry 中)因此并不算是一次 HTTP 请求,所以这种模式不利于 SEO 优化
- 只能修改 # 后面的部分,因此只能跳转与当前 URL 同文档的 URL
- 只能通过字符串改变 URL
- 通过 window.onhashchange 监听 hash 的改变,借此实现无刷新跳转的功能。
- 每改变一次 hash( window.location.hash),都会在浏览器的访问历史中增加一个记录。
- 路径中从 # 开始,后面的所有路径都叫做路由的哈希值,并且哈希值它不会作为路径的一部分随着 http 请求,发给服务器
history路由
- history是路由的另一种模式,在相应的router配置时将mode设置为history即可。
- history模式是通过调用window.history对象上的一系列方法来实现页面的无刷新跳转。
- 利用了 HTML5 History Interface中新增的pushState()和replaceState()方法。
- 这两个方法应用于浏览器的历史记录栈,在当前已有的back、forward、go的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。
特点
- 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
- 通过参数stateObject可以添加任意类型的数据到记录中。
- 可额外设置title属性供后续使用。
- 通过pushState、replaceState实现无刷新跳转的功能。
- 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
- 由于History API的缘故,低版本浏览器有兼容行问题。
虚拟dom
虚拟DOM是一个JavaScript对象,它代表了真实的DOM树的一个副本。通过比较虚拟DOM和真实DOM的差异,可以高效地更新页面。这意味着,只有在数据发生变化时,才需要重新渲染相关组件。
工作原理:
- 当组件的状态发生变化时,会重新创建虚拟DOM对象。
- 虚拟DOM与之前的虚拟DOM进行对比(Diff算法),找出差异。
- 根据差异,真实DOM只更新需要改变的部分。
diff算法
Diff算法是虚拟DOM的核心,它的目标是找出最小数量的DOM操作,以最小化重绘和重排页面的次数。常用的Diff算法有深度优先遍历(React)和广度优先遍历(Vue)。
- 深度优先遍历:从根节点开始,递归地比较每个节点的子节点。如果子节点或其属性发生变化,则更新相应的DOM节点。
- 广度优先遍历:从根节点开始,先比较所有直接子节点。在所有子节点比较完之后,再比较上一级的节点。这样可以减少不必要的比较。
优化建议:
- 避免频繁触发重新渲染:尽量减少不必要的状态更新,或者使用防抖(debounce)和节流(throttle)等技术来限制重新渲染的频率。
- 使用key属性:在列表渲染时,确保每个元素的key属性唯一。这样可以使得Diff算法更快地识别出元素的变化。
- 使用React.memo或Vue的v-once:对于不会频繁更新的组件,可以使用这些技术来避免不必要的重新渲染。
- 自定义Diff算法:在某些情况下,可能需要根据具体需求来优化Diff算法。例如,对于大型列表,可以使用“窗口化”策略来减少比较的节点数量。
场景题:如果网站被大量爬虫,被恶意攻击,如何防范爬虫?
- 使用验证码:在关键的操作(如登录、注册、提交表单等)前添加验证码,以区分人类用户和爬虫。验证码可以是文字验证码、图像验证码、滑动验证码等形式。这样可以有效地防止自动化程序进行恶意操作。
- 限制访问频率:通过设置访问频率限制,限制同一 IP 地址或相同用户在一定时间内的访问次数。可以使用反向代理服务器(如 Nginx)或防火墙来实现访问频率限制,从而减轻服务器负载和阻止恶意爬虫。
- 检测用户行为:通过分析用户的行为特征,如点击模式、访问时间间隔等,来识别异常行为。可以使用机器学习算法或规则引擎来进行行为分析,及时发现并阻止恶意爬虫。
- 使用 User-Agent 过滤:通过分析 User-Agent 字段,识别并过滤出常见的爬虫 User-Agent,禁止它们的访问。同时,还可以使用黑名单或白名单机制,限制或允许特定的 User-Agent 访问网站。
- 使用验证码令牌:在敏感操作(如密码重置、账号删除等)前,发送一个验证码到用户的注册邮箱或手机号码,要求用户输入该令牌以完成操作。这样可以确保只有真正的用户才能进行敏感操作。
- 使用反爬虫技术:可以采用一些反爬虫技术来识别和阻止爬虫,如使用 JavaScript 加密页面内容、设置访问限制、动态生成页面等。此外,还可以使用机器学习算法来识别恶意爬虫的行为模式。
- 使用 CAPTCHA 服务:可以使用第三方的 CAPTCHA 服务,如 Google reCAPTCHA、hCaptcha 等,来验证用户的人类身份。这些服务可以提供更强大的验证码验证功能,同时减轻自身服务器的负载。
受控组件 & 非受控组件
在 HTML 中,像 input、textarea 这样的表单标签,通常会根据用户的输入来进行更新。即,不同时候使用 value 属性会得到当前状态下用户直接输入的值。
我们可以这样描述非受控组件:非受控组件并不是为每个状态更新都编写新的数据处理函数。
如果理解了上面受控组件所描述的“双向绑定”,那其实我们可以这样来描述非受控组件:
相比于“双向绑定”,非受控组件实际上取消了 ViewModel 将处理后的数据重新发送给 View 视图层这一步,因而变成了“单向绑定”:仅仅是 View 发送消息给了 ViewModel。
因此,非受控组件我们只需要 ViewModel 获取 View 视图层的数据。想要实现这一点有很多方式:绑定事件使用 event 对象;使用 ref 属性等等。
在 React 组件中,如果想要为非受控组件指定一个初始值,可以使用 defaultValue 属性,而不必使用 value 属性指定初始值。
object.assign
(非常好的解释)[https://blog.csdn.net/weixin_44296929/article/details/103879019]
解构对象
多种继承方式
- 原型链继承
父类的实例作为子类的原型
function Woman(){
}
Woman.prototype= new People();
Woman.prototype.name = 'haixia';
let womanObj = new Woman();
- 借用构造函数继承(伪造对象、经典继承)
复制父类的实例属性给子类
function Woman(name){
//继承了People
People.call(this); //People.call(this,'wangxiaoxia');
this.name = name || 'renbo'
}
let womanObj = new Woman();
- 实例继承(原型式继承)
function Wonman(name){
let instance = new People();
instance.name = name || 'wangxiaoxia';
return instance;
}
let wonmanObj = new Wonman();
- 组合式继承
调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用
function People(name,age){
this.name = name || 'wangxiao'
this.age = age || 27
}
People.prototype.eat = function(){
return this.name + this.age + 'eat sleep'
}
function Woman(name,age){
People.call(this,name,age)
}
Woman.prototype = new People();
Woman.prototype.constructor = Woman;
let wonmanObj = new Woman(ren,27);
wonmanObj.eat();
- 寄生组合继承
通过寄生的方式来修复组合式继承的不足,完美的实现继承
//父类
function People(name,age){
this.name = name || 'wangxiao'
this.age = age || 27
}
//父类方法
People.prototype.eat = function(){
return this.name + this.age + 'eat sleep'
}
//子类
function Woman(name,age){
//继承父类属性
People.call(this,name,age)
}
//继承父类方法
(function(){
// 创建空类
let Super = function(){};
Super.prototype = People.prototype;
//父类的实例作为子类的原型
Woman.prototype = new Super();
})();
//修复构造函数指向问题
Woman.prototype.constructor = Woman;
let womanObj = new Woman();
- es6继承
最新的图片格式
- AVIF
AVIF 是由 AOMedia(开放媒体联盟)在 2019 年发布,基于 AV1 视频编解码器(源自谷歌的 VP9 ), 由 Netflix 主导推动的图片格式。如果你常常使用 Netflix 的客户端,你就已经正在使用 AVIF 了。在浏览器对 AVIF 的支持的进度还是不错的,截止 2022 年 1 月,Chrome 85+、Firefox 93+ 都已经支持了 AVIF。
- WEBP2
WebP2 是谷歌在 2021 年公开的 WebP 继任者,主要目标是让其有损压缩能力达到 AVIF ,并且增加 HDR10、快速解码等 WebP 缺失的功能,其无损压缩也有提升
- JPEGXL
JPEG XL 是 JPEG(联合影像专家组) 2021 年完成的 JPEG 的又一个继任者(JPEG 2000、JPEG XR)。JPEG XL 是一个令人惊喜的图片格式,一改 JPEG(联合影像专家组) 保守、专利限制严格的印象,不仅开源、免专利还与开源社区有积极联系,吸收了FLIF 格式最为其无损压缩部分。
在次世代的图片格式中,JPEG XL 是唯一可以无损重编码旧有的 JPEG 图片的格式(由于 JPEG 的特性,重编码不是无损的,如果想把旧有 JPEG 转换为其他格式会有一定损失) ,这种对现实的考量非常值得肯定。
JPEG 之前推出的继任者都失败了,无论是苹果大力支持的 JPEG 2000(甚至让对图片格式支持极其保守的 Safari 内置了支持 😅) ,还是微软大力支持的 JPEG XR 都没能流行起来,而 JPEG XL 让人看到了希望。
- HEIF
HEIF 是 MPEG(动态影像专家组) 于 2015 年发布,基于 HEVC (H.265) 视频编解码器。HEIF 的特点是支持非破坏新编辑和动画、音频(苹果的 LivePhoto)。HEIF 目前在苹果设备上被广泛使用,iPhone 相机保存的照片已经是 HEIF 了。但是苹果的 Safari 浏览器并不支持,原因应该是它使用的 HEVC (H.265) 的专利限制。
视频解析
RTMP——Real Time Messaging Protocol(实时消息传输协议)
RTSP(Real Time Streaming Protocol)
TCP/UDP协议体系中的一个应用层协议,由哥伦比亚大学, 网景和RealNetworks公司提交的IETF RFC标准.该协议定义了一对多应用程序如何有效地通过IP网络传输多媒体数据。RTSP在体系结构上位于RTP和RTCP之上,它使用TCP或者RTP完成数据传输,目前市场上大多数采用RTP来传输媒体数据。
- 面试官说现在很多基于QUIC了
设计模式
(真的不是很懂为什么会有公司喜欢问这个)😅
8种常用模式——在底下也进行了标注
创建型
- 单例模式
一个类只有一个实例,并提供一个访问它的全局访问点。
- 原型模式
- 工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
- 抽象工厂模式
- 建造者模式
结构型
- 适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。 (调用的api接口名一样且返回的格式一样)
- 装饰器模式
给对象动态地增加职责的方式称为装饰器(decorator)模式。装饰器模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
- 代理模式
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
行为型
- 观察者模式
- 迭代器模式
- 策略模式
- 模板方法模式
- 职责链模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
微前端实现原理
INFO
微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。你可以将其看作是在架构层次而非获取服务的类上应用很多SOLID原则。微服务架构是个很有趣的概念,它的主要作用是将功能分解到离散的各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。
概念: 把一个大型的单个应用程序和服务拆分为数个甚至数十个的支持微服务,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议。
定义: 围绕业务领域组件来创建应用,这些应用可独立地进行开发、管理和迭代。在分散的组件中使用云架构和平台式部署、管理和服务功能,使产品交付变得更加简单。
本质: 用一些功能比较明确、业务比较精练的服务去解决更大、更实际的问题。
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git来管理。
在 toB 的前端开发工作中,我们往往就会遇到如下困境:
- 工程越来越大,打包越来越慢
- 团队人员多,产品功能复杂,代码冲突频繁、影响面大
- 内心想做 SaaS 产品,但客户总是要做定制化
- 微前端的实现,意味着对前端应用的拆分。拆分应用的目的,并不只是为了架构上好看,还为了提升开发效率。
优点:
- 应用自治。只需要遵循统一的接口规范或者框架,以便于系统集成到一起,相互之间是不存在依赖关系的。
- 单一职责。每个前端应用可以只关注于自己所需要完成的功能。
- 技术栈无关。你可以使用 Angular 的同时,又可以使用 React 和 Vue。
缺点:
- 应用的拆分基础依赖于基础设施的构建,一旦大量应用依赖于同一基础设施,那么维护变成了一个挑战。
- 拆分的粒度越小,便意味着架构变得复杂、维护成本变高。
- 技术栈一旦多样化,便意味着技术栈混乱。
微前端已经有比较成熟的框架了,比如qiankun
sandbox
沙箱是一种隔离对象/线程/进程的机制,控制浏览器访问系统资源的权限,从而达到保护用户的系统不被网页上的恶意软件侵入、保护用户系统的输入事件(键盘/鼠标)不被监视、保护用户系统中的文件不被偷取等目的。最初的浏览器沙箱是基于Hook实现的,后来的Chrome沙箱是利用操作系统提供的一些安全机制实现的。
chromeV8引擎就是一种VM
默认情况下,一个应用程序是可以访问机器上的所有资源的,比如CPU、内存、文件系统、网络等等。但是这是不安全的,如果随意操作资源,有可能破坏其他应用程序正在使用的资源,或者造成数据泄漏。为了解决这个问题,一般有下面两种解决方案: (1) 为程序分配一个限定权限的账号:利用操作系统的权限管理机制进行限制 (2) 为程序提供一个受限的运行环境:这就是沙箱机制
以Chromium为例,它是在进程的粒度下来实现沙箱模型,也就是说需要运行在沙箱下的操作都在一个单独的进程中。所以,对于使用沙箱模型至少需要两个进程。
代理进程是需要负责创建目标进程并为目标进程设置各种安全策略,同时建立 IPC 连接,接受目标进程的各种请求,因为目标进程是不能访问过多资源的。
沙箱其实就是一个硬盘过滤文件驱动,具体来说,就是你把要写的东西写到了硬盘上,但实际上并没有写到硬盘,而是到了一个转存处,读取内容需要判断是沙箱开启之前就存在的内容还是开沙箱之后写入的内容,要分别从不同的地方读取内容,重启之后把转存的地方清零。
symbol
iterator 迭代器
for...of要遍历iterator。
一个迭代器对象具有一个 next 方法,每次调用 next 方法都会返回一个结果,这个结果是一个 Object ,它包含两个属性,value 和 done。
- value - 表示具体的返回值
- done - 是一个布尔值,表示集合是否遍历完成或者是否后续还有可用的数据, 没有可用的数据则返回 true ,否则返回 false
一个自定义的迭代器:
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
generator 生成器
虽然自定义迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此创建时要格外谨慎。生成器函数(Generator 函数)提供了一个强大的替代选择:它允许你定义一个非连续执行的函数作为迭代算法。生成器函数使用 function 语法编写*。
最初调用时,生成器函数不执行任何代码,而是返回一种称为生成器的特殊迭代器。通过调用 next() 方法消耗该生成器时,生成器函数将执行,直至遇到 yield 关键字。
可以根据需要多次调用该函数,并且每次都返回一个新的生成器,但每个生成器只能迭代一次。
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
return iterationCount;
}
可以使用[symbol.iterator]查看迭代器