前端面试学习与总结

网络基础知识

1. HTTP

HTTP(Hypertext Transfer Protocol),超文本传输协议(应用层),是互联网上应用最为广泛的一种网络协议。HTTP 是一个客户端(用户)和服务端(网站)之间请求和响应的标准,用于从服务器传输超媒体文档到本地浏览器(用户代理)。HTTP 的连接很简单,是无连接(限制每次连接只处理一个请求)无状态(不保存请求和响应之间的通信状态)的,底层使用 TCP协议进行连接,数据都是明文传输的。

HTTP常用方法

  • GET:获取资源,用来请求已被 URI 识别的资源;
  • POST:传输实体的主体;
  • PUT:传输文件;
  • DELETE:删除文件;
  • HEAD:获取报文首部;
  • OPTIONS:查询支持的方法。

HTTP常见状态码

  • 200 OK:请求成功。一般用于 GET 与 POST 请求;
  • 301 Moved Permanently:永久性重定向。表示请求的资源被分配了新的 URI,以后应使用资源现在所指的 URI 进行请求;
  • 302 Found:临时性重定向。与 301 类似,但资源只是临时被移动,仅限本次使用新的 URI,之后客户端应继续使用原有 URI;
  • 304 Not Modified:服务器资源未修改。此时可直接使用客户端未过期的缓存;
  • 400 Bad Request:请求报文存在语法错误;
  • 403 Forbidden:服务器理解请求客户端的请求,但是拒绝执行此请求;
  • 404 Not Found:服务器无法根据客户端的请求找到资源;
  • 500 Internal Server Error:服务器内部错误,无法完成请求;
  • 502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应;
  • 504 Gateway Time-out:充当网关或代理的服务器,未及时从远端服务器获取请求。

HTTP 常见首部字段

HTTP 的头部字段根据不同上下文可分为通用首部字段请求首部字段响应首部字段实体首部字段;根据代理对其的处理方式又可分为端到端消息头逐跳消息头,逐跳首部只对单次转发有效,经过缓存或代理后不再转发,HTTP/1.1和之后的版本中,要使用逐跳首部时需提供 Connection 首部字段。端到端首部则会一直发送给最终接收目标。

通用首部字段

缓存相关:

  • Cache-Control:控制缓存的行为(客户端-(请求)->缓存服务器<-(响应)-源服务器)。
    • 请求指令
      • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证,即客户端不接收缓存过的响应而不代表不缓存;
      • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存;
      • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期,时间是相对于请求的时间;
      • max-stale[=<seconds>]:表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间;
      • min-fresh=<seconds>:表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应;
      • no-transform:不得对资源进行转换或转变。Content-EncodingContent-RangeContent-Type等HTTP头不能由代理修改;
      • only-if-cached:表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。
    • 响应指令
      • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存;
      • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容(对应用户的本地浏览器);
      • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证;
      • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存;
      • no-transform:不得对资源进行转换或转变。Content-EncodingContent-RangeContent-Type等HTTP头不能由代理修改;
      • must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求;
      • proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略;
      • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期,时间是相对于请求的时间;
      • s-maxage=<seconds>:与 max-age 功能相同,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
  • Pragma:HTTP/1.1 以前的遗留字段Pargma: no-cacheCache-Control: no-cache功能一致,只用在客户端发送请求时;

其他:

  • Connection:决定当前的事务完成后,是否会关闭网络连接。如果该值是 keep-alive,网络连接就是持久的,不会关闭,使得对同一个服务器的请求可以继续在该连接上完成。除此之外还用来控制不再转发给代理的首部字段。
    • close:表明客户端或服务器想要关闭该网络连接,这是HTTP/1.0请求的默认值;
    • keep-alive:表明客户端想要保持该网络连接打开,HTTP/1.1的请求默认使用一个持久连接;
    • 以逗号分隔的 HTTP 头:这个请求头列表由头部名组成,这些头将被第一个非透明的代理或者代理间的缓存所移除:这些头定义了发出者和第一个实体之间的连接,而不是和目的地节点间的连接。
  • Date:创建 HTTP 报文的时间和日期;
  • Keep-Alive:在 Connection 设置为 keep-alive 时有效,允许发送方设置连接超时和最大请求数;
  • Trailer:说明在报文主体后记录了哪些首部字段;
  • Transfer-Encoding:传输报文主体时采用的编码方式;
  • Upgrade:可用于将已经建立的客户端/服务器连接升级到其他协议(通过相同的传输协议);
  • Via:追踪客户端与服务器之前的请求和响应报文的传输路径;
  • Warning:可能出现的问题的常规警告信息。

请求首部字段

内容协商:

  • Accept:用来告知(服务器)客户端可以处理的内容类型及其优先级,这种内容类型用 MIME 类型来表示(如:text/htmltext/plainapplication/jsonimage/png 等);
    • <MIME_type>/<MIME_subtype>:单一精确的 MIME 类型;
    • */*:任意类型的 MIME 类型;
    • ;q=<weight>:值代表优先顺序,用相对质量价值表示,又称作权重。
  • Accept-Charset:告知服务器用户代理支持的字符集及其优先级;
  • Accept-Encoding 告知服务器用户代理支持的内容编码及其优先级;
    • gzip:由文件压缩程序 gzip 生成的编码格式;
    • compress:由 UNIX 文件压缩程序 compress 生成的编码格式;
    • deflate:组合使用 zlib 格式及由 deflate 压缩算法生成的编码格式;
    • identify:不执行压缩或不会变化的默认编码格式。
  • Accept-Language:告知服务器用户代理支持的语言集及其优先级(中文、英文等);
  • TE:告知服务器用户代理能够处理的传输编码及其优先级,还可以指定伴随 trailer 字段的分块传输编码方式。

条件请求:

  • Expect:客户端通过该字段告知服务器只有在满足期望条件的情况下才能妥善地处理请求,如果服务器无法处理该期望条件,就应该返回 417 Expectation Failed。目前规范中只规定了 “100-continue” 这一个期望条件;
  • If-Match:服务器会比对该字段的值和资源的 ETag 值,仅当两者一致时,才会执行请求,否则,返回 412 Precondition Failed。该字段值为 * 时,会忽略 ETag 值;
  • If-Modified-Since:该字段值应该是一个日期,如果服务器上资源的更新时间较该字段值新则处理该请求,否则,返回 304 Not Modified
  • If-None-Match:与 If-Match 相反,该字段的值与请求资源的 ETag 不一致时,处理该请求;
  • If-Range:该字段的值(ETag 或时间)与资源的 ETag 或时间一致时,作为范围请求处理(使用首部字段 Range),否则,返回全体资源;
  • If-Unmodified-Since:与 If-Modified-Since 相反,服务器上资源的更新时间早于该字段值时处理请求,否则,返回412 Precondition Failed
  • Range:范围请求,只获取部分资源。如 Range: bytes=5001-10000,表示获取从第 5001 字节至 10000 字节的资源。成功处理范围请求时返回 206 Partial Content 响应,无法处理范围请求时返回 200 OK 响应及全部资源。

验证相关:

  • Authorization:向服务器回应自己的身份验证信息。客户端收到来自服务器的 401 Authentication Required 响应后,要在其请求中包含这个首部;

  • Proxy-Authorization:与Authorization类似,用于客户端与代理服务器之间的身份验证。

安全相关:

  • Cookie:包含与服务器相关联的已存储的 Cookie(即服务器先前使用 Set-Cookie 发送的 Cookie,或使用 Document.cookie 在 Javascript 中设置的 Cookie)。

其他:

  • From:请求来自何方,格式是客户端用户的有效电子邮件地址;
  • Host:服务器的主机名和端口号;
  • Referer:这次请求的 URL 是从哪里获得的;
  • User-Agent:客户端的浏览器或代理信息。

响应首部字段

条件响应:

  • Accept-Ranges:服务器是否能处理范围请求,bytes 表示能,none 表示不能;
  • ETag:用于表示服务器资源特定版本的标识符,服务器资源更新时,ETag 值也会更新,如果服务器资源未更改,则 ETag 值不变。服务器通过对比 If-None-Match 发送的 ETag 值,决定Web 服务器不需要重新发送完整的响应;
  • Vary:指定一些首部名称,客户端后续请求相同资源时,这些首部与缓存的那次请求完全一致时才会返回缓存的资源,同时服务器可以使用它来指示在内容协商中使用了哪些首部名称。

验证相关:

  • Proxy-Authorizate:与 WWW-Authenticate 类似,用于代理与客户端之间的认证,407 Proxy Authentication Required 响应必须包含该首部;
  • WWW-Authenticate:告诉客户端访问所请求资源的认证方案,401 Unauthorized 响应中肯定有该首部。

安全相关:

  • Set-Cookie:用于将 Cookie 从服务器发送到用户代理。

其他:

  • Age:表示对象在代理缓存中的停留时间(以秒为单位);
  • Location:客户端应重定向到指定 URI,基本配合 3** 响应出现;
  • Retry-After:告诉客户端多久之后再次发送请求。主要配合 503 Service Unavailable 使用,或与 3** 响应一起使用;
  • Server:HTTP 服务器的应用程序信息。

实体首部字段

实体主体:

  • Content-Encoding:告诉客户端实体的主体部分选用的内容编码方式;
  • Content-Language:告诉客户端实体主体使用的自然语言(中文、英文等);
  • Content-Length:表明实体主体部分的大小(单位:字节)。对实体主体进行内容编码传输时,不能再使用该首部字段;
  • Content-Location:报文主体部分相对应的 URI;
  • Content-MD5:一串由 MD5 算法生成的值。对于检查在传输过程中数据是否被无意的修改非常有用,但不能用于安全目的,因为报文如果被有意的修改,该字段的值也可以计算后作相应修改;
  • Content-Range:针对范围请求,提供了请求实体在原始实体内的位置(范围),还给出了整个实体的长度;
  • Content-Type:响应报文中对象的媒体类型。

缓存相关:

  • Expires:资源失效日期,当 Cache-Control 有指定 max-age 指令时,会优先处理 max-age
  • Last-Modified:资源最终修改时间。

其他:

  • Allow:通知客户端可以对特定资源使用那些 HTTP 方法。405 Method Not Allowed 响应中必须包含该首部。

HTTP 缓存

HTTP 缓存按缓存地点可分为浏览器缓存和代理缓存,而根据缓存方式可分为强缓存(也称本地缓存)和协商缓存(也称弱缓存)。

640029216-5bad97d9bb48c_fix732

  • 强缓存:强缓存是利用 HTTP 首部字段中的 ExpiresCache-Control 两个字段来控制的,用来表示资源的缓存时间。使用普通刷新时会忽略它,使用强制刷新时会带上 Cache-Control:no-cachePragma:no-cache 首部字段;
  • 协商缓存:协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。普通刷新会启用弱缓存,忽略强缓存。协商缓存主要涉及到两组首部字段:EtagIf-None-MatchLast-ModifiedIf-Modified-Since。两组字段同时使用的情况下,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304 Not Modified

HTTP 安全策略

CSP(Content-Security-Policy):实质就是白名单制度,常用于防止跨站点脚本攻击(XSS)。开发者明确告诉客户端,哪些外部资源可以加载和执行。它的实现和执行全部由浏览器完成,开发者只需提供配置。CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。

启用 CSP 的方式有两种:

  • 通过 Content-Security-Policy 首部字段;
  • 通过 HTML 的 <meta> 标签。

资源加载限制指令:

  • script-src:外部脚本;
  • style-src:样式表;
  • img-src:图像;
  • media-src:媒体文件(音频和视频);
  • font-src:字体文件;
  • object-src:插件(比如 Flash);
  • child-src:框架;
  • frame-ancestors:嵌入的外部资源(比如 <frame><iframe><embed><applet>) ;
  • connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等);
  • worker-srcworker脚本;
  • manifest-src:manifest 文件;
  • default-src:来设置上面各个选项的默认值,如果同时设置某个单项限制(比如 font-src),则该单项会覆盖默认值。

URL 限制指令:

  • frame-ancestors:限制嵌入框架的网页;
  • base-uri:限制 <base#href>
  • form-action:限制 <form#action>

其他限制指令:

  • block-all-mixed-content:HTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启);
  • upgrade-insecure-requests:自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
  • plugin-types:限制可以使用的插件格式;
  • sandbox:浏览器行为的限制,比如不能有弹出窗口等;
  • report-uri:仅记录不阻止 XSS 攻击,参数为一个 URL。该指令使用POST方法,发送一个 JSON 对象报告攻击行为给这个地址。

指令可用值:

  • 主机名:example.orghttps://example.com:443
  • 路径名:example.org/resources/js/
  • 通配符:*.example.org*://*.example.com:*(表示任意协议、任意子域名、任意端口)
  • 协议名:https:data:
  • 关键字'self':当前域名,需要加引号
  • 关键字'none':禁止加载任何外部资源,需要加引号。

除了常规值,script-src还可以设置一些特殊值:

  • 'unsafe-inline':允许执行页面内嵌的 <script> 标签和事件监听函数;
  • unsafe-eval:允许将字符串当作代码执行,比如使用 evalsetTimeoutsetIntervalFunction 等函数;
  • nonce 值:每次HTTP回应给出一个授权 token,页面内嵌脚本必须有这个 token,才会执行;
  • hash 值:列出允许执行的脚本代码的 Hash 值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。

HTTP 持久连接

HTTP1.0 和HTTP1.1 都不支持持久性的链接,不过为了解决这个问题,HTTP1.1 提出了 keep-alive(HTTP connection reuse)的方法,在一定时间内保持连接状态。

HTTP 状态管理

由于 HTTP 本身是无状态的,为了实现类似保存用户登录状态的功能,引入了 Cookie 技术,通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。 Cookie 会根据服务端发送的 Set-Cookie 首部字段信息,通知客户端保存 Cookie,下次客户端发送请求时,会自动在请求报文内加入 Cookie。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息);
  • 个性化设置(如用户自定义设置、主题等);
  • 浏览器行为跟踪(如跟踪分析用户行为等)。

2. HTTPS

超文本传输安全协议,HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包,主要作用是在不安全的网络上创建一个安全信道,并可在使用适当的加密包和服务器证书可被验证且可被信任时,对窃听和中间人攻击提供合理的防护。

HTTPS 协议的工作原理

  • 客户端使用 HTTPS 请求访问服务器,要求 WEB 服务器建立 SSL 连接;

  • WEB 服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥)返回给客户端;

  • 客户端和 WEB 服务器端开始协商 SSL 连接的安全等级,也就是加密等级;

  • 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站;

  • WEB 服务器通过自己的私钥解密出会话密钥;

  • WEB 服务器通过会话密钥加密与客户端之间的通信。

HTTPS协议的优缺点

  • 优点:使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
  • 缺点:https握手阶段比较费时,会使页面加载时间延长50%,增加10%~20%的耗电;https缓存不如http高效,会增加数据开销;SSL证书成本较高。

3. TCP 连接的建立和释放

TCP 建立连接(三次握手)

服务器保持监听状态,客户端主动发起连接,收到服务器响应后建立连接,服务器收到客户端连接请求后进一步确认客户端可连接,收到客户端响应后建立连接,此时客户端服务端双方都已确认各自可收发。(最后一次握手的作用:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误)

image-20210312113504239

TCP连接释放(四次挥手)

客户端主动发起连接释放请求,并且停止发送数据,服务器收到后返回确认信息,同时开始传输最后的数据,此时客户端还要等待服务器传回的数据。传输完成后服务器也发起连接释放请求,等待客户端回应后直接关闭连接,而客户端还需要等待2MSL(Maximum Segment Lifetime)后才关闭连接。(等待2MSL的作用:保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,同时防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中)

image-20210312114150079

浏览器基础

1. HTML 基础

Doctype 的作用

Doctype 声明于文档最前面,告诉浏览器以何种方式来渲染页面,分为严格模式和混杂模式。

  • 严格模式:排版和 JS 的运作模式以该浏览器支持的最高标准运行;
  • 混杂模式:向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。

本地存储

Cookie、sessionStorage、localStorage 的异同:

  • 首先三者都是保存在浏览器端,并且是同源的 ;
  • Cookie:可以在浏览器和服务器端来回传递,存储容量小,只有大约4K左右;
  • sessionStorage:本身就是一个会话过程,关闭浏览器后消失,session为一个会话,当页面不同即使是同一页面打开两次,也被视为同一次会话;
  • localStorage:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效。

XSS 和 CSRF

  • XSS(Cross-Site Scripting):跨站脚本攻击,攻击者通过向页面注入代码,达到窃取信息等目的,本质是数据被当作程序执行,常使用输入检查/转义或 CSP 内容安全策略的方法避免攻击;
  • CSRF(Cross-Site Request Forgery):跨站请求伪造,与 XSS 不同的是,XSS 是攻击者直接对我们的网站 A 进行注入攻击,CSRF 是通过网站 B 对我们的网站 A 进行伪造请求。常使用同源检测、Samesite Cookie、Token 验证等方式来避免攻击。

CORS(Cross-Origin Resource Sharing)

跨域资源共享,是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它源(域,协议和端口),这样浏览器就可以访问加载这些资源。跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

cors_principle

  • 简单请求:不会触发 CORS 预检请求的请求称为简单请求,若请求满足所有下述条件,则该请求可视为“简单请求”:
    • 使用下列方法之一:GETHEADPOST
    • 允许人为设置的字段:AcceptAccept-LanguageContent-LanguageContent-Type
    • Content-Type 仅允许 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
    • 请求中没有在任何 XMLHttpRequest.upload 对象上注册事件侦听器;
    • 请求中没有使用 ReadableStream 对象。
  • 预检请求:需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
  • JSONP:解决跨域问题的另一种方法,通过 <script> 标签的 src 属性请求服务端返回数据和回调函数,浏览器执行后即可拿到数据。

2. CSS 基础

重绘和重排

DOM 的变化影响到了元素的几何属性比如宽高,浏览器重新计算元素的几何属性,其他元素的几何属性也会受到影响,浏览器需要重新构造渲染树,这个过程称之为重排,浏览器将受到影响的部分重新绘制在屏幕上的过程称为重绘。重排一定导致重绘,重绘不一定导致重排,引起重排重绘的原因有:

  • 添加或者删除可见的 DOM 元素;
  • 元素尺寸位置的改变;
  • 浏览器页面初始化;
  • 浏览器窗口大小发生改变。

BFC(Block Fomatting Context)

BFC,块级格式化上下文,用于解决浮动元素溢出和 margin 重叠的问题,常见的创建 BFC 的方式有:

  • 浮动元素(float 不为 none);
  • 绝对定位元素;
  • 行内块元素;
  • 表格单元格、表格标题、匿名表格单元格元素;
  • overflow 计算值不为 visible 的块元素;
  • display 值为 flow-root 的元素等。

CSS 动画

  • transition 和 animation 的区别:animation 和 transition 大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是 transition 需要触发一个事件才能改变属性,而 animation 不需要触发任何事件的情况下才会随时间改变属性值,并且 transition 为 2 帧,从from … to,而 animation 可以逐帧。

3.JavaScript 基础

JS 代码执行过程

JS 作为一种解释型语言,代码执行总共包含两个阶段:解释阶段和执行阶段。

  • 解释阶段:

    • 词法分析
    • 语法分析
    • 作用域规则确定(需要注意的是,作用域链在创建时确定而非执行时)

    执行阶段:

    • 创建执行上下文
    • 执行函数代码
    • 垃圾回收

事件循环(Event Loop)

JavaScript 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,JavaScript 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环”的原因。

v2-da078fa3eadf3db4bf455904ae06f84b_720w

实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task):

  • 微任务:
    • new Promise()
    • new MutaionObserver()
    • process.nextTick() (Node,实际上不属于事件循环)。
  • 宏任务:
    • script(主程序代码);
    • setInterval();
    • setTimeout();
    • setImmediate()(Node);
    • I/O;
    • UI交互事件。

二者的优先顺序为:当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

Node 环境下的事件循环

在 node 中,事件循环表现出的状态与浏览器中大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现是依靠的 libuv 引擎。我们知道 node 选择 chrome v8 引擎作为 JS 解释器,v8 引擎将 JS 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。因此实际上 node 中的事件循环存在于 libuv 引擎中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<--connections--- │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

这些阶段大致的功能如下:

  • timers:这个阶段执行定时器队列中的回调如 setTimeout()setInterval()
  • I/O callbacks:这个阶段执行几乎所有的回调。但是不包括 close 事件,定时器和 setImmediate() 的回调;
  • idle, prepare:这个阶段仅在内部使用,可以不必理会;
  • poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里;
  • check:setImmediate() 的回调会在这个阶段执行;
  • close callbacks:例如 socket.on('close', ...) 这种close事件的回调。

事件流(Event Flow)

事件流描述的是从页面中接收事件的顺序,早期的 IE 和 Netscape 提出了完全相反的事件流概念,IE 事件流是事件冒泡,而 Netscape 的事件流就是事件捕获。事件冒泡(事件委托就是利用冒泡实现的),即从下至上,从目标触发的元素逐级向上传播,直到 window 对象;事件捕获,即从 document 逐级向下传播到目标元素。目前主要有 DOM0~3 总共4个级别。

事件流的三个阶段:

  • 事件捕获阶段

  • 处于目标阶段

  • 事件冒泡阶段

eventflow

  • DOM0:使用elem.onXXX的形式绑定事件;
  • DOM2:DOM2级事件规定的事件流包括三个阶段: (1)事件捕获阶段 (2)处于目标阶段 (3)事件冒泡阶段,通过2个方法来绑定事件:addEventListener()removeEventListener()

垃圾回收

垃圾回收的方法:标记清除、计数引用。

  • 标记清除:这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
  • 计数引用:另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减1,当这个值的引用次数为0的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为0的这些值。

模块化

  • CommonJS:CommonJS是服务器模块的规范,Node.js采用了这个规范。根据 CommonJS 规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
  • AMD(Asynchronous Module Definition):AMD 规范加载模块是异步的,并允许函数回调,不必等到所有模块都加载完成,后续操作可以正常执行。AMD 中,使用 require 获取依赖模块,使用 exports 导出 API。
  • CMD(Common Module Definition):CMD规范和AMD类似,都主要运行于浏览器端,写法上看起来也很类似。主要是区别在于模块初始化时机:AMD中只要模块作为依赖时,就会加载并初始化,而CMD中,模块作为依赖且被引用时才会初始化,否则只会加载;CMD 推崇依赖就近,AMD 推崇依赖前置。
  • UMD(Universal Module Definition):UMD 是AMD 和 CommonJS 的糅合。UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。

继承

由于 JavaScript 是一门基于原型的语言,因此无法直接实现传统意义上的继承。

  • 原型链继承:将父类的实例作为子类的原型。
    • 缺点:父类的引用属性会被所有子类实例共享。
1
2
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
  • 构造函数继承:将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype的继承。
    • 优点:父类的引用属性不会被共享且子类构建实例时可以向父类传递参数。
    • 缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。
1
SuperType.call(SubType);
  • 组合继承:原型式继承和构造函数继承的组合,兼具了二者的优点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.name = 'parent';
this.arr = [1, 2, 3];
}

SuperType.prototype.say = function() {
console.log('this is parent')
}

function SubType() {
SuperType.call(this) // 第二次调用SuperType
}

SubType.prototype = new SuperType() // 第一次调用SuperType
  • 原型式继承:原型式继承使用一个空对象作为代理,本质上是对参数对象的一个浅复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function object(o){
function F(){}
F.prototype = o;
return new F();
}

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
  • 寄生式继承:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(original){ 
var clone=object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
  • 寄生组合继承:解决组合继承会两次调用父类的构造函数造成浪费的缺点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}

function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
alert(this.name);
};

function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}

JavaScript 应用开发框架

1. Vue.js 的原理和应用

a6054cb4746ac78184b75cd07b35fb75

JavaScript 常用功能实现

深拷贝

1
2
3
4
5
6
7
function deepClone(obj) {
var newObj = obj instanceof Array ? [] : {};
for (var i in obj) {
newObj[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
}
return newObj;
}

实现 bind 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function (obj) {
const args = Array.prototype.slice.call(arguments, 1);
const fn = this;

let F = function () {};
let bound = function () {
const params = Array.prototype.slice.call(arguments);
fn.apply(obj, args.concat(params));
};
F.prototype = fn.prototype;
bound.prototype = new F();
return bound;
};

使用 Promise 封装 ajax

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
function ajax(method, url, params) {
return new Promise((resolve, reject) => {
// 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
// 状态改变时的回调
xhr.onreadystatechange = function () {
// readyState为4的时候已接收完毕
if (xhr.readyState === 4) {
// 状态码200表示成功
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
}
};

// get
if (method === 'get' || method === 'GET') {
if (typeof params === 'object') {
// params拆解成字符串
params = Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
url = params ? url + '?' + params : url;
xhr.open(method, url, true);
xhr.send();
}

//post
if (method === 'post' || method === 'POST') {
xhr.open(method, url, true);
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
xhr.send(JSON.stringify(params));
}
});
}

尾递归优化

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
// 蹦床函数
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}

// 尾递归优化
function tco(f) {
let value;
let active = false;
let accumulated = [];

return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}

var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});

sum(1, 100000) // 100001

实现一个简单的 Promise

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
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}

myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!