面试资料

HTML 模块

HTML 语义化

语义化是指使⽤恰当语义的 html 标签,让⻚⾯具有良好的结构与含义,⽐如 <p> 标签就代表段落, <article> 代表正⽂

内容等等。

语义化的好处主要有两点:

  • 开发者友好:使⽤语义类标签增强了可读性,开发者也能够清晰地看出⽹⻚的结构,也更为便于团队的开发和维护
  • 机器友好:带有语义的⽂字表现⼒丰富,更适合搜索引擎的爬⾍爬取有效信息,语义类还可以⽀持读屏软件,根据⽂章可以⾃动⽣成⽬录

常⽤的 meta 标签

  • charset,⽤于描述 HTML ⽂档的编码形式
<meta charset="UTF-8" />
  • http-equiv,顾名思义,相当于 http 的⽂件头作⽤,⽐如下⾯的代码就可以设置 http 的缓存过期⽇期
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT">
  • viewport,移动前端最熟悉不过,Web 开发⼈员可以控制视⼝的⼤⼩和⽐例
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  • apple-mobile-web-app-status-bar-style,开发过 PWA 应⽤的开发者应该很熟悉,为了⾃定义评估⼯具栏的颜⾊。
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

src 和 href 的区别

  • src 是指向外部资源的位置,指向的内容会嵌⼊到⽂档中当前标签所在的位置,在请求 src 资源时会将其指向的资源下载并应⽤到⽂档内,如 js 脚本,img 图⽚和 frame 等元素。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执⾏完毕,所以⼀般 js 脚本会放在底部⽽不是头部。
  • href 是指向⽹络资源所在位置(的超链接),⽤来建⽴和当前元素或⽂档之间的连接,当浏览器识别到它他指向的 ⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理。

img 的 srcset 的作⽤是什么

可以设计响应式图⽚,我们可以使⽤两个新的属性 srcset 和 sizes 来提供更多额外的资源图像和提示,帮助浏览器选择
正确的⼀个资源。

  • srcset 定义了我们允许浏览器选择的图像集,以及每个图像的⼤⼩。
  • sizes 定义了⼀组媒体条件(例如屏幕宽度)并且指明当某些媒体条件为真时,什么样的图⽚尺⼨是最佳选择。

所以,有了这些属性,浏览器会:

  • 查看设备宽度
  • 检查 sizes 列表中哪个媒体条件是第⼀个为真
  • 查看给予该媒体查询的槽⼤⼩
  • 加载 srcset 列表中引⽤的最接近所选的槽⼤⼩的图像
<img
  src="clock-demo-thumb-200.png"
  alt="Clock"
  srcset="clock-demo-thumb-200.png 200w, clock-demo-thumb-400.png 400w"
  sizes="(min-width: 600px) 200px, 50vw"
/>

script 标签中 defer 和 async 的区别

  • defer:浏览器指示脚本在⽂档被解析后执⾏,script 被异步加载后并不会⽴刻执⾏,⽽是等待⽂档被解析完毕后执⾏。
  • async:同样是异步加载脚本,区别是脚本加载完毕后⽴即执⾏,这导致 async 属性下的脚本是乱序的,对于 script 有先后依赖关系的情况,并不适⽤。

前端储存的⽅式

cookies、localstorage、sessionstorage、Web SQL、IndexedDB

  • cookies: 在 HTML5 标准前本地储存的主要⽅式,优点是兼容性好,请求头⾃带 cookie ⽅便,缺点是⼤⼩只有 4k,⾃动请求头加⼊ cookie 浪费流量,每个 domain 限制 20 个 cookie,使⽤起来麻烦需要⾃⾏封装
  • localStorage:HTML5 加⼊的以键值对(Key-Value)为标准的⽅式,优点是操作⽅便,永久性储存(除⾮⼿动删除),⼤⼩为 5M,兼容 IE8+
  • sessionStorage:与 localStorage 基本类似,区别是 sessionStorage 当⻚⾯关闭后会被清理,⽽且与 cookie、localStorage 不同,他不能在所有同源窗⼝中共享,是会话级别的储存⽅式
  • Web SQL:2010 年被 W3C 废弃的本地数据库数据存储⽅案,但是主流浏览器(⽕狐除外)都已经有了相关的实现,web sql 类似于 SQLite,是真正意义上的关系型数据库,⽤ sql 进⾏操作,当我们⽤ JavaScript 时要进⾏转换,较为繁琐。
  • IndexedDB: 是被正式纳⼊ HTML5 标准的数据库储存⽅案,它是 NoSQL 数据库,⽤键值对进⾏储存,可以进⾏快速读取操作,⾮常适合 web 场景,同时⽤ JavaScript 进⾏操作会⾮常⽅便。

CSS 模块

CSS 选择器的优先级

内联 > ID 选择器 > 类选择器 > 标签选择器

  • link 属于 XHTML 标签,⽽ @import 是 CSS 提供的。
  • ⻚⾯被加载时,link 会同时被加载,⽽@import 引⽤的 CSS 会等到⻚⾯被加载完再加载。
  • import 只在 IE 5 以上才能识别,⽽ link 是 XHTML 标签,⽆兼容问题。
  • link ⽅式的样式权重⾼于@import 的权重。
  • 使⽤ dom 控制样式时的差别。当使⽤ javascript 控制 dom 去改变样式的时候,只能使⽤ link 标签,因为@import 不是 dom 可以控制的。

有哪些⽅式(CSS)可以隐藏⻚⾯元素

  • opacity:0 本质上是将元素的透明度将为 0,就看起来隐藏了,但是依然占据空间且可以交互
  • visibility:hidden 与上⼀个⽅法类似的效果,占据空间,但是不可以交互了
  • overflow:hidden 这个只隐藏元素溢出的部分,但是占据空间且不可交互
  • display:none 这个是彻底隐藏了元素,元素从⽂档流中消失,既不占据空间也不交互,也不影响布局
  • z-index:-9999 原理是将层级放到底部,这样就被覆盖了,看起来隐藏了
  • transform: scale(0,0) 平⾯变换,将元素缩放为 0,但是依然占据空间,但不可交互

em、px、rem 区别

  • px:绝对单位,⻚⾯按精确像素展示。
  • em:相对单位,基准点为⽗节点字体的⼤⼩,如果⾃身定义了 font-size 按⾃身来计算(浏览器默认字体是 16px),整个⻚⾯内 1em 不是⼀个固定的值。
  • rem:相对单位,可理解为“root em”, 相对根节点 html 的字体⼤⼩来计算,CSS3 新加属性,chrome、firefox、IE9+支持

块级元素⽔平居中的⽅法

  • margin:0 auto ⽅法
  • flex 布局,⽬前主流⽅法

扩展阅读:16 种方法实现水平居中垂直居中

CSS 定位⽅式

  • static: 正常⽂档流定位,此时 top, right, bottom, left 和 z-index 属性⽆效,块级元素从上往下纵向排布,⾏级元素从左向右排列。
  • relative:相对定位,此时的『相对』是相对于正常⽂档流的位置。
  • absolute:相对于最近的⾮ static 定位祖先元素的偏移,来确定元素位置,⽐如⼀个绝对定位元素它的⽗级、和祖⽗级元素都为 relative,它会相对他的⽗级⽽产⽣偏移。
  • fixed:指定元素相对于屏幕视⼝(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变,⽐如那种回到顶部的按钮⼀般都是⽤此定位⽅式。
  • sticky:粘性定位,特性近似于 relative 和 fixed 的合体,其在实际应⽤中的近似效果就是 IOS 通讯录滚动的时候的『顶屁股』。

清除浮动⽅法

  • 空 div ⽅法: <div style="clear:both;"></div>
  • clearfix ⽅法:上⽂使⽤ .clearfix 类已经提到
  • overflow: autooverflow: hidden ⽅法,使⽤ BFC

对媒体查询的理解

媒体查询由⼀个可选的媒体类型和零个或多个使⽤媒体功能的限制了样式表范围的表达式组成,例如宽度、⾼度和颜⾊。媒体查询,添加⾃ CSS3,允许内容的呈现针对⼀个特定范围的输出设备⽽进⾏裁剪,⽽不必改变内容本身,⾮常适合 web ⽹⻚应对不同型号的设备⽽做出对应的响应适配。

<!-- link元素中的CSS媒体查询 -->
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />
<!-- 样式表中的CSS媒体查询 -->
<style>
  @media (max-width: 600px) {
    .facet_sidebar {
      display: none;
    }
  }
</style>

盒模型

盒模型由 content(内容)、padding(内边距)、border(边框)、margin(外边距)组成。

标准盒模型和怪异盒模型的区别

在 W3C 标准下,我们定义元素的 width 值即为盒模型中的 content 的宽度值,height 值即为盒模型中的 content 的⾼度值。

因此,标准盒模型下:

元素的总宽度 = margin-left + border-left + padding-left + width + padding-right + border-right + margin-right

⽽ IE 怪异盒模型(IE8 以下)width 的宽度并不是 content 的宽度,⽽是 border-left + padding-left + content 的宽度值 + padding-right + border-right 之和,height 同理。

在怪异盒模型下:

元素占据的宽度 = margin-left + width + margin-right

虽然现代浏览器默认使⽤ W3C 的标准盒模型,但是在不少情况下怪异盒模型更好⽤,于是 W3C 在 css3 中加⼊ box-sizing

box-sizing: content-box // 标准盒模型
box-sizing: border-box // 怪异盒模型
box-sizing: padding-box // ⽕狐的私有模型,没⼈⽤

BFC 块级上下文

BFC 触发条件:

  • 根元素,即 HTML 元素
  • position: fixed / absolute
  • float 不为 none
  • overflow 不为 visible
  • display 的值为 inline-block、table-cell、table-caption

作用

  • 防⽌ margin 发⽣重叠
  • 两栏布局,防⽌⽂字环绕等
  • 防⽌元素塌陷

为什么有时候⼈们⽤ translate 来改变位置⽽不是定位

translate() 是 transform 的⼀个值。改变 transform 或 opacity 不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。⽽改变绝对定位会触发重新布局,进⽽触发重绘和复合。transform 使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此 translate() 更⾼效,可以缩短平滑动画的绘制时间。

translate() 改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。

伪类和伪元素的区别

  • 伪类是⼀个以冒号:作为前缀,被添加到⼀个选择器末尾的关键字,当你希望样式在特定状态下才被呈现到指定的元素时,你可以往元素的选择器后⾯加上对应的伪类。
  • 伪元素⽤于创建⼀些不在⽂档树中的元素,并为其添加样式。⽐如说,我们可以通过 ::before 来在⼀个元素前增加⼀些⽂本,并为这些⽂本添加样式。虽然⽤户可以看到这些⽂本,但是这些⽂本实际上不在⽂档树中。

区别

其实上⽂已经表达清楚两者区别了,伪类是通过在元素选择器上加⼊伪类改变元素状态,⽽伪元素通过对元素的操作进⾏对元素的改变。

我们通过 p::before 对这段⽂本添加了额外的元素,通过 p:first-child 改变了⽂本的样式。

关于 CSS 的动画与过渡问题

深入理解 CSS 动画 animation

深入理解 CSS 过渡 transition

网络

HTTP 有哪些⽅法

  • GET: 通常⽤于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET ⽅法请求时返回的⼀致. 该请求⽅法的⼀个使⽤场景是在下载⼀个⼤⽂件前先获取其⼤⼩再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: ⽤于获取⽬的资源所⽀持的通信选项
  • POST: 发送数据给服务器
  • PUT: ⽤于新增资源或者使⽤请求中的有效负载替换⽬标资源的表现形式
  • DELETE: ⽤于删除指定的资源
  • PATCH: ⽤于对资源进⾏部分修改
  • CONNECT: HTTP/1.1 协议中预留给能够将连接改为管道⽅式的代理服务器
  • TRACE: 回显服务器收到的请求,主要⽤于测试或诊断

GET 和 POST 区别

  • 数据传输⽅式不同:GET 请求通过 URL 传输数据,⽽ POST 的数据通过请求体传输。
  • 安全性不同:POST 的数据因为在请求主体内,所以有⼀定的安全性保证,⽽ GET 的数据在 URL 中,通过历史记
    录,缓存很容易查到数据信息。
  • 数据类型不同:GET 只允许 ASCII 字符,⽽ POST ⽆限制
  • GET ⽆害: 刷新、后退等浏览器操作 GET 请求是⽆害的,POST 可能重复提交表单
  • 特性不同:GET 是安全(这⾥的安全是指只读特性,就是使⽤这个⽅法不会引起服务器状态变化)且幂等(幂等的概念是指同⼀个请求⽅法执⾏多次和仅执⾏⼀次的效果完全相同),⽽ POST 是⾮安全⾮幂等

PUT 和 POST 都是给服务器发送新增资源,有什么区别

PUT 和 POST ⽅法的区别是,PUT ⽅法是幂等的:连续调⽤⼀次或者多次的效果相同(⽆副作⽤),⽽ POST ⽅法是⾮幂
等的。

除此之外还有⼀个区别,通常情况下,PUT 的 URI 指向是具体单⼀资源,⽽ POST 可以指向资源集合。

举个例⼦,我们在开发⼀个博客系统,当我们要创建⼀篇⽂章的时候往往⽤ POST https://www.jianshu.com/articles,这个请求的语义是,在 articles 的资源集合下创建⼀篇新的⽂章,如果我们多次提交这个请求会创建多个⽂章,这是⾮幂等的。⽽ PUT https://www.jianshu.com/articles/820357430 的语义是更新对应⽂章下的资源(⽐如修改作者名称等),这个 URI 指向的就是单⼀资源,⽽且是幂等的,⽐如你把『刘德华』修改成『蔡徐坤』,提交多少次都是修改成『蔡徐坤』

PUT 和 PATCH 都是给服务器发送修改资源,有什么区别

PUT 和 PATCH 都是更新资源,⽽ PATCH ⽤来对已知资源进⾏局部更新。

⽐如我们有⼀篇⽂章的地址 https://www.jianshu.com/articles/820357430 ,这篇⽂章的可以表示为:

{
  "author": "dxy",
  "creationDate": "2019-6-12",
  "content": "我写⽂章像蔡徐坤",
  "id": 820357430
}

当我们要修改⽂章的作者时,我们可以直接发送 PUT https://www.jianshu.com/articles/820357430 ,这个时候的数据应该是:

{
  "author": "蔡徐坤",
  "creationDate": "2019-6-12",
  "content": "我写⽂章像蔡徐坤",
  "id": 820357430
}

这种直接覆盖资源的修改⽅式应该⽤ PUT,但是你觉得每次都带有这么多⽆⽤的信息,那么可以发送 PATCH
https://www.jianshu.com/articles/820357430 ,这个时候只需要:

{
  "author": "蔡徐坤"
}

HTTP 的状态码

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理 ✨
  • 201 Created 请求已经被实现,⽽且有⼀个新的资源已经依据请求的需要⽽建⽴
  • 202 Accepted 请求已接受,但是还没执⾏,不保证完成请求
  • 204 No content,表示请求成功,但响应报⽂不含实体的主体部分
  • 206 Partial Content,进⾏范围请求 ✨

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL ✨
  • 303 see other,表示资源存在着另⼀个 URL,应使⽤ GET ⽅法定向获取资源
  • 304 not modified,表示服务器允许访问资源,但因发⽣请求未满⾜条件的情况
  • 307 temporary redirect,临时重定向,和 302 含义相同

4XX 客户端错误

  • 400 bad request,请求报⽂存在语法错误 ✨
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 ✨
  • 403 forbidden,表示对请求资源的访问被服务器拒绝 ✨
  • 404 not found,表示在服务器上没有找到请求的资源 ✨
  • 408 Request timeout, 客户端请求超时
  • 409 Confict, 请求的资源可能引起冲突

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执⾏请求时发⽣了错误 ✨
  • 501 Not Implemented 请求超出服务器能⼒范围,例如服务器不⽀持当前请求所需要的某个功能,或者请求是服务器不⽀持的某个⽅法
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,⽆法处理请求
  • 505 http version not supported 服务器不⽀持,或者拒绝⽀持在请求中使⽤的 HTTP 版本

HTTP 的缓存的过程

  1. 客户端向服务器发出请求,请求资源
  2. 服务器返回资源,并通过响应头决定缓存策略
  3. 客户端根据响应头的策略决定是否缓存资源(这⾥假设是),并将响应头与资源缓存下来
  4. 在客户端再次请求且命中资源的时候,此时客户端去检查上次缓存的缓存策略,根据策略的不同、是否过期等判断是直接读取本地缓存还是与服务器协商缓存

什么时候会触发强缓存或者协商缓存

强缓存

强缓存离不开两个响应头 Expires 与 Cache-Control

  • Expires:Expires 是 http1.0 提出的⼀个表示资源过期时间的 header,它描述的是⼀个绝对时间,由服务器返回,Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效 Expires: Wed, 11 May 2018 07:20:00 GMT
  • Cache-Control: Cache-Control 出现于 HTTP / 1.1,优先级⾼于 Expires,表示的是相对时间 Cache-Control: max-age=315360000

⽬前主流的做法使⽤ Cache-Control 控制缓存,除了 max-age 控制过期时间外,还有⼀些不得不提

  • Cache-Control: public 可以被所有⽤户缓存,包括终端和 CDN 等中间代理服务器
  • Cache-Control: private 只能被终端浏览器缓存,不允许中继缓存服务器进⾏缓存
  • Cache-Control: no-cache 先缓存本地,但是在命中缓存之后必须与服务器验证缓存的新鲜度才能使⽤
  • Cache-Control: no-store 不会产⽣任何缓存

协商缓存

当第⼀次请求时服务器返回的响应头中没有 Cache-Control 和 Expires 或者 Cache-Control 和 Expires 过期抑或它的属性设置为 no-cache 时,那么浏览器第⼆次请求时就会与服务器进⾏协商。

如果缓存和服务端资源的最新版本是⼀致的,那么就⽆需再次下载该资源,服务端直接返回 304 Not Modified 状态码,如果服务器发现浏览器中的缓存已经是旧版本了,那么服务器就会把最新资源的完整内容返回给浏览器,状态码就是
200 Ok。

三次握⼿

所谓三次握⼿(Three-way Handshake),是指建⽴⼀个 TCP 连接时,需要客户端和服务器总共发送 3 个包。

三次握⼿的⽬的是连接服务器指定端⼝,建⽴ TCP 连接,并同步连接双⽅的序列号和确认号,交换 TCP 窗⼝⼤⼩信
息。在 socket 编程中,客户端执⾏ connect() 时,将触发三次握⼿。

  • 第⼀次握⼿(SYN=1, seq=x):

    客户端发送⼀个 TCP 的 SYN 标志位置 1 的包,指明客户端打算连接的服务器的端⼝,以及初始序号 X,保存在包头的序列号(Sequence Number)字段⾥。发送完毕后,客户端进⼊ SYN_SEND 状态。

  • 第⼆次握⼿(SYN=1, ACK=1, seq=y, ACKnum=x+1):

    服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为 1。服务器端选择⾃⼰ ISN 序列号,放到 Seq 域⾥,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加 1,即 X+1。发送完毕后,服务器端进⼊ SYN_RCVD 状态。

  • 第三次握⼿(ACK=1,ACKnum=y+1)

    客户端再次发送确认包(ACK),SYN 标志位为 0,ACK 标志位为 1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对⽅,并且在数据段放写 ISN 的+1 发送完毕后,客户端进⼊ ESTABLISHED 状态,当服务器端接收到这个包时,也进⼊ ESTABLISHED 状态,TCP 握⼿结束。

什么是浏览器同源策略

同源策略限制了从同⼀个源加载的⽂档或脚本如何与来⾃另⼀个源的资源进⾏交互。这是⼀个⽤于隔离潜在恶意⽂件的
重要安全机制。

同源是指”协议+域名+端⼝”三者相同,即便两个不同的域名指向同⼀个 ip 地址,也⾮同源。

浏览器中的⼤部分内容都是受同源策略限制的,但是以下三个标签可以不受限制:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

如何实现跨域

最经典的跨域⽅案 jsonp

jsonp 本质上是⼀个 Hack,它利⽤ <script> 标签不受同源策略限制的特性进⾏跨域操作。
jsonp 优点:

  • 实现简单
  • 兼容性⾮常好

jsonp 的缺点:

  • 只⽀持 get 请求(因为 <script> 标签只能 get)
  • 有安全性问题,容易遭受 xss 攻击
  • 需要服务端配合 jsonp 进⾏⼀定程度的改造
function JSONP({ url, params, callbackKey, callback }) {
  // 在参数⾥制定 callback 的名字
  params = params || {}
  params[callbackKey] = 'jsonpCallback'
  // 预留 callback
  window.jsonpCallback = callback
  // 拼接参数字符串
  const paramKeys = Object.keys(params)
  const paramString = paramKeys.map(key => `${key}=${params[key]}`).join('&')
  // 插⼊ DOM 元素
  const script = document.createElement('script')
  script.setAttribute('src', `${url}?${paramString}`)
  document.body.appendChild(script)
}

JSONP({
  url: 'http://s.weibo.com/ajax/jsonp/suggestion',
  params: {
    key: 'test',
  },
  callbackKey: '_cb',
  callback(result) {
    console.log(result.data)
  },
})

最流⾏的跨域⽅案 cors

最⽅便的跨域⽅案 Nginx

其它跨域⽅案

  1. HTML5 XMLHttpRequest 有⼀个 API,postMessage()⽅法允许来⾃不同源的脚本采⽤异步⽅式进⾏有限的通信,可以实现跨⽂本档、多窗⼝、跨域消息传递。
  2. WebSocket 是⼀种双向通信协议,在建⽴连接之后,WebSocket 的 server 与 client 都能主动向对⽅发送或接收数据,连接建⽴好了之后 client 与 server 之间的双向通信就与 HTTP ⽆关了,因此可以跨域。
  3. window.name + iframe:window.name 属性值在不同的⻚⾯(甚⾄不同域名)加载后依旧存在,并且可以⽀持⾮常⻓的 name 值,我们可以利⽤这个特点进⾏跨域。
  4. location.hash + iframe:a.html 欲与 c.html 跨域相互通信,通过中间⻚ b.html 来实现。 三个⻚⾯,不同域之间利⽤ iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
  5. document.domain + iframe: 该⽅式只能⽤于⼆级域名相同的情况下,⽐如 a.test.com 和 b.test.com 适⽤于该⽅式,我们只需要给⻚⾯添加 document.domain =’test.com’ 表示⼆级域名都相同就可以实现跨域,两个⻚⾯都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

前端⼯程化

babel

Vue 面试题

对 MVVM 的理解

MVVM 模式,顾名思义即 Model-View-ViewModel 模式。

  • Model 层:对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系⾥,它主要⽤于抽象出 ViewModel 中视图的 Model。
  • View 层:作为视图模板存在,在 MVVM ⾥,整个 View 是⼀个动态模板。除了定义结构、布局外,它展示的是 ViewModel 层的数据和状态。View 层不负责处理状态,View 层做的是 数据绑定的声明、 指令的声明、 事件绑定的声明。
  • ViewModel 层:把 View 需要的层数据暴露,并对 View 层的 数据绑定声明、 指令声明、 事件绑定声明 负责,也就是处理 View 层的具体业务逻辑。ViewModel 底层会做好绑定属性的监听。当 ViewModel 中数据变化,View 层会得到更新;⽽当 View 中声明了数据的双向绑定(通常是表单元素),框架也会监听 View 层(表单)值的变化。⼀旦值变化,View 层绑定的 ViewModel 中的数据也会得到⾃动更新。

示例

优点:

  1. 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的 “View” 上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel ⾥⾯,让很多 view 重⽤这段视图逻辑
  2. 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码
  3. ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放

缺点:

  1. Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的
  2. ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
  3. 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼

Vue 组件如何通信

  • props/$emit+v-on: 通过 props 将数据⾃上⽽下传递,⽽通过$emit 和 v-on 来向上传递信息。
  • EventBus: 通过 EventBus 进⾏信息的发布与订阅
  • vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流
  • $attrs/$listeners: Vue 2.4 中加⼊的 $attrs/$listeners 可以进⾏跨级的组件通信
  • provide/inject:以允许⼀个祖先组件向其所有⼦孙后代注⼊⼀个依赖,不论组件层次有多深,并在起上下游关系成⽴的时间⾥始终⽣效,这成为了跨组件通信的基础

computed 和 watch 有什么区别

computed:

  1. computed 是计算属性,也就是计算值,它更多⽤于计算值的场景
  2. computed 具有缓存性,computed 的值在 getter 执⾏后是会缓存的,只有在它依赖的属性值改变之后,下⼀次获取 computed 的值时才会重新调⽤对应的 getter 来计算
  3. computed 适⽤于计算⽐较消耗性能的计算场景

watch:

  1. 更多的是「观察」的作⽤,类似于某些数据的监听回调,⽤于观察 props$emit 或者本组件的值,当数据变化时来执⾏回调进⾏后续操作
  2. ⽆缓存性,⻚⾯重新渲染时值不变化也会执⾏

⼩结:

  1. 当我们要进⾏数值计算,⽽且依赖于其他数据,那么把这个数据设计为 computed
  2. 如果你需要在某个数据变化时做⼀些事情,使⽤ watch 来观察这个数据变化

Vue 是如何实现双向绑定的

利⽤ Object.defineProperty 劫持对象的访问器,在属性值发⽣变化时我们可以获取变化,然后根据变化进⾏后续响应,在 Vue 3.0 中通过 Proxy 代理对象进⾏类似的操作。

// 这是将要被劫持的对象
const data = {
  name: '',
}

function say(name) {
  if (name === '古天乐') {
    console.log('给⼤家推荐⼀款超好玩的游戏')
  } else if (name === '渣渣辉') {
    console.log('戏我演过很多,可游戏我只玩贪玩懒⽉')
  } else {
    console.log('来做我的兄弟')
  }
}
// 遍历对象,对其属性值进⾏劫持
Object.keys(data).forEach(function (key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log('get')
    },
    set: function (newVal) {
      // 当属性值发⽣变化时我们可以进⾏额外操作
      console.log(`⼤家好,我系${newVal}`)
      say(newVal)
    },
  })
})
data.name = '渣渣辉'
//⼤家好,我系渣渣辉
//戏我演过很多,可游戏我只玩贪玩懒⽉

Proxy 与 Object.defineProperty 的优劣对⽐

Proxy 的优势如下:

  • Proxy 可以直接监听对象⽽⾮属性
  • Proxy 可以直接监听数组的变化
  • Proxy 有多达 13 种拦截⽅法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的
  • Proxy 返回的是⼀个新对象,我们可以只操作新的对象达到⽬的,⽽ Object.defineProperty 只能遍历对象属性直接修改
  • Proxy 作为新标准将受到浏览器⼚商重点持续的性能优化,也就是传说中的新标准的性能红利

Object.defineProperty 的优势如下:

  • 兼容性好,⽀持 IE9

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进⾏ diff 检测差异

Vue 中的 key 到底有什么⽤

key 是为 Vue 中的 vnode 标记的唯⼀ id,通过这个 key,我们的 diff 操作可以更准确、更快速

Webpack ⾯试题

有哪些常⻅的 Loader

  • file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件
  • url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去
  • source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
  • image-loader:加载并且压缩图⽚⽂件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
  • style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

有哪些常⻅的 Plugin

  • define-plugin:定义环境变量
  • html-webpack-plugin:简化 html ⽂件创建
  • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
  • webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
  • webpack-bundle-analyzer: 可视化 webpack 输出⽂件的体积
  • mini-css-extract-plugin: CSS 提取到单独的⽂件中,⽀持按需加载

Loader 和 Plugin 的不同

不同的作⽤:

  • Loader 直译为”加载器”。Webpack 将⼀切⽂件视为模块,但是 webpack 原⽣是只能解析 js ⽂件,如果想将其他⽂件也打包的话,就会⽤到 loader 。 所以 Loader 的作⽤是让 webpack 拥有了加载和解析⾮ JavaScript ⽂件的能⼒。
  • Plugin 直译为”插件”。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的⽤法:

  • Loader 在 module.rules 中配置,也就是说他作为模块的解析规则⽽存在。 类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options )
  • Plugin 在 plugins 中单独配置。 类型为数组,每⼀项是⼀个 plugin 的实例,参数都通过构造函数传⼊。

如何⽤ webpack 来优化前端性能

⽤ webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动 webpack 时追加参数 --optimize-minimize 来实现
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

解析 URL Params

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1] // 将 ? 后⾯的字符串取出来
  const paramsArr = paramsStr.split('&') // 将字符串以 & 分割后存到数组中
  let paramsObj = {}
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) {
      // 处理有 value 的参数
      let [key, val] = param.split('=') // 分割 key 和 value
      val = decodeURIComponent(val) // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val // 判断是否转为数字
      if (paramsObj.hasOwnProperty(key)) {
        // 如果对象有 key,则添加⼀个值
        paramsObj[key] = [].concat(paramsObj[key], val)
      } else {
        // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val
      }
    } else {
      // 处理没有 value 的参数
      paramsObj[param] = true
    }
  })
  return paramsObj
}

实现防抖函数(debounce)

// 防抖函数
const debounce = (fn, delay) => {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次

实现节流函数(throttle)

防抖函数原理:规定在⼀个单位时间内,只能触发⼀次函数。如果这个单位时间内触发多次函数,只有⼀次⽣效。

// 节流函数
const throttle = (fn, delay = 500) => {
  let flag = true
  return (...args) => {
    if (!flag) return
    flag = false
    setTimeout(() => {
      fn.apply(this, args)
      flag = true
    }, delay)
  }
}

实现一个 Promise

请求二次重发

135 页前后各种方法的自定义实现值得深入学习

原始类型、引用类型

原始类型:undefinednullbooleanstringnumbersymbol

typeof null === 'object' 是一个历史悠久的 bug,因为 000 开始表示对象, null 也是全零

原始类型存储的是值,引用类型存储的是指针

typeof 可以判断除了 null 以外的原始类型。 instanceof 可以判对象的正确类型,但是不能判断原始类型,因为它是通过原型链去判断的。

类型转换

略,详见掘金小册

this

  1. 定义一个函数 foo(),如果直接调用 foo ,不管 foo 函数放在了什么地方, this 一定是 window
  2. 对于 obj.foo() 等情形,谁调用了函数,谁就是 this 。这里 this 就是 obj 对象
  3. 对于 const f = new foo() 的方式来说, this 永远指向 f ,不会有任何改变
  4. 箭头函数是没有 this 的,箭头函数的 this 取决于包裹箭头函数的第一个普通函数的 this 。另外,箭头函数使用 bindnew 这类函数是无效的
  5. 对于 bind 这些改变上下文的 API, this 只取决于第一个参数,如果第一个参数为空,那么就是 window 。注意,一个函数无论我们 bind 几次, this 永远由第一次 bind 决定
const a = {}
const fn = function () {
  console.log(this)
}
fn.bind().bind(a)()

// 换种写法
const fn2 = function () {
  return function () {
    return fn.apply()
  }.apply(a)
}
  1. 综上: new 的方式优先级最高,其次 bind 等函数,然后是 obj.foo() 这种方式的调用,最后是 直接调用。同时,箭头函数的 this 一旦绑定,就不会再改变了

闭包

定义:函数和 声明 这个函数时的作用域结合起来,就是闭包

;(function () {
  var a = 1
  function add() {
    var b = 2
    var sum = b + a
    console.log(sum) // 3
  }
  add()
})()

add 函数本身,以及其内部可访问的变量,即 a = 1 ,这两个组合在⼀起就被称为闭包,仅此⽽已。

闭包最⼤的作⽤就是隐藏变量,闭包的⼀⼤特性就是内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后也可以访问。

基于此特性,JavaScript 可以实现私有变量特权变量储存变量

我们就以私有变量举例,私有变量的实现⽅法很多,有靠约定的(变量名前加_),有靠 Proxy 代理的,也有靠 Symbol 这种新数据类型的。

但是真正⼴泛流⾏的其实是使⽤闭包。

function Person() {
  var name = 'Jim'
  this.getName = function () {
    return name
  }
  this.setName = function (value) {
    name = value
  }
}
const Jim = new Person()
console.log(Jim.getName()) // Jim
Jim.setName('Tom')
console.log(Jim.getName()) // Tom
console.log(name) //name is not defined

其他例子

// 第一题
;(function () {
  function createIncrement() {
    let count = 0
    function increment() {
      count++
    }

    let message = `Count is ${count}`
    function log() {
      console.log(message)
    }

    return [increment, log]
  }

  const [increment, log] = createIncrement()
  increment()
  increment()
  increment()
  log() // => ?
})()

// 第二题
;(function () {
  function fn() {
    a = 0
    return function (b) {
      return b + a++
    }
  }
  var f = fn()
  console.log(f(5)) // 5
  console.log(fn()(5)) // 5
  console.log(f(5)) // 6
  console.log(a) // 2
})()

深浅拷贝

浅拷贝: Object.assign{...someObject}

深拷贝: JSON.parse(JSON.stringfy(object)) ,但是也有局限性

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
/**
 * 简易版深拷贝函数
 */
const deepClone = obj => {
  if (typeof obj !== 'object') {
    throw new Error('发生错误')
  }

  const newObj = obj instanceof Array ? [] : {}

  for (const key in obj) {
    if (Object.hasOwnProperty.call(obj, key)) {
      const value = obj[key]
      newObj[key] = typeof value === 'object' ? deepClone(value) : value
    }
  }
  return newObj
}

原型及原型链

当我们创建一个实例let person = new Person('Jim', '25', 'football player') ,我们可以发现能使用很多种函数,但是我们明明没有定义过它们,对于这种情况你是否有过疑惑?

当我们在浏览器中打印 person 时你会发现,在 person 上居然还有一个 __proto__ 属性。其实每个 JS 对象都有 __proto__ 属性,这个属性指向了该对象 Person 的原型 Person.prototype

原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 person 来说,可以通过 __proto__ 找到一个原型对象,在该对象中定义了很多函数让我们来使用。

__proto__ 对象中还有一个 constructor 属性,也就是构造函数。打开 constructor 属性我们又可以发现其中还有一个 prototype 属性,并且这个属性对应的值和先前我们在 __proto__ 中看到的一模一样。所以我们又可以得出一个结论:原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。

示意图

总结:

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • 函数的 prototype 是一个对象
  • 对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链

作用域及作用域链

作用域就是一个独立的地盘,最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了块级作用域,可通过新增命令 letconst 来体现。

全局作用域

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 所有 window 对象的属性拥有全局作用域

块级作用域

块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块(由一对花括号包裹)内部

块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部,所以会暂时性锁区
  • 禁止重复声明

作用域链

当在当前作用域中取值失败时,就要到创建这个函数的那个作用域中取值,这里强调的是“创建”,而不是“调用”。如果还是取值失败,继续往上,形成的链条,就叫作用域链。

什么是变量提升

JavaScript 引擎的⼯作⽅式是,先解析代码,获取所有被声明的变量,然后再⼀⾏⼀⾏地运⾏。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。存在提升的原因:解决函数之间的相互调用。

console.log(a) // undefined
var a = 1
function b() {
  console.log(a)
}
b() // 1

上⾯的代码实际执⾏顺序是这样的:

第⼀步: 引擎将 var a = 1 拆解为 var a = undefineda = 1 ,并将 var a = undefined 放到最顶端, a = 1 还在原来的位置,这样⼀来代码就是这样:

var a = undefined
console.log(a) // undefined
a = 1
function b() {
  console.log(a)
}
b() // 1

第⼆步就是执⾏,因此 js 引擎⼀⾏⼀⾏从上往下执⾏就造成了当前的结果,这就叫变量提升。

函数提升优于变量提升,函数提升会把 整个函数 挪到作用域顶部,变量提升只会把 变量声明 挪到作用域顶部

var、let 及 const

  • var 存在提升,我们能在声明之前使用。 let 、const 因为暂时性死区,不能在声明之前使用
  • 全局作用域下使用 letconst 声明变量,变量不会被挂载到 window 上,这和 var 不一样
  • letconst 作用基本一致,但是后者声明的变量不能再次赋值

原型继承和 Class 继承

/**
 * 组合继承:子类的构造函数中通过 Parent.call(this) 继承父类的属性,
 * 然后改变子类的原型为 new Parent() 来继承父类的函数
 */
function Parent(value) {
  this.value = value
}

Parent.prototype.getValue = function () {
  console.log(this.value)
}

function Child(value) {
  Parent.call(this, value) // 构造函数可以传参,不会与父类引用属性共享
}

Child.prototype = new Parent() // 可以复用父类的函数,但是子类原型上多了不需要的父类的属性

const child = new Child(1)

child.getValue() // 1

child instanceof Parent // true
/**
 * Class 继承,关键点在于 extends、super
*/
class Parent {
  constructor(value) {
    this.value = value
  }

  getValue() {
    console.log(this.value)
  }
}

Class Child extends Parent {
  constructor(value){
    super(value)
    this.value = value
  }
}

const child = new Child(1)

child.getValue() // 1

child instanceof Parent // true

模块化

Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

let proxy = new Proxy(target, handler)
  • target —— 是要包装的对象,可以是任何东西,包括函数。
  • handler —— 代理配置:带有“钩子”(“traps”,即拦截操作的方法)的对象。比如 get 钩子用于读取 target 属性, set 钩子写入 target 属性等等。

proxy 进行操作,如果在 handler 中存在相应的钩子,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

Handler 对象包含的方法:getsethas (in 运算符) 、 deleteProperty (delete 操作) 、 apply (proxy 对象作为函数被调用)、 construct (new 操作)、 defineProperty

/**
 * 通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数
 * 逻辑(回调函数),实现了在对对象任何属性进行读写时发出通知
 */
let obj = { a: 1 }

let onWatch = (target, setCallback, getCallback) => {
  return new Proxy(target, {
    set(target, key, value, receiver) {
      setCallback(key, value)
      return Reflect.set(target, key, value, receiver)
    },
    get(target, key, receiver) {
      getCallback(target, key)

      // return target[key]
      return Reflect.get(target, key, receiver)
    },
  })
}

let p = onWtch(
  obj,
  (k, v) => {
    // 数据变化,响应式监听
    console.log(`监测到属性${k}改变为${V}`)
  },
  (t, k) => {
    // 数据读取,响应式派发
    console.log(`'${k}' = ${t[k]}`)
  }
)

p.a = 3 // 监测到属性a改变为3
p.a // 'a' = 3

Proxy 有一些局限:

  • 内置对象(MapSetPromiseDate)具有“内部插槽”,对这些对象的访问无法被代理。
  • 私有类字段也是如此,因为它们是在内部使用插槽实现的。因此,代理方法的调用必须具有目标对象 this 才能访问它们
  • 对象相等性测试 === 不能被拦截
  • 性能:基准测试取决于引擎,但通常使用最简单的代理访问属性所需的时间要长几倍。实际上,这仅对某些“瓶颈”对象重要

Reflect

  1. Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty ),放到 Reflect 对象上。现阶段,某些方法同时在 ObjectReflect 对象上部署,未来的新方法将只部署在 Reflect 对象上
  2. 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false
  3. Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name],而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为
  4. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成原始对象的默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为(e.g. set()、get()),你总可以在 Reflect 上获取原始对象的默认行为。

在大多数情况下,我们可以不使用 Reflect 完成相同的事情,例如,使用 Reflect.get(target, prop, receiver) 读取属性可以替换为 target[prop] 。尽管有一些细微的差别。

let user = {
  _name: 'Guest',
  get name() {
    return this._name
  },
}

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]
  },
})

console.log(userProxy.name) // Guest

get 钩子在这里是“ 透明的 ”,它返回原来的属性,不会做别的任何事情。对于我们的示例而言,这就足够了。

但是对象 adminuser 继承后,我们可以观察到错误的行为

let user = {
  _name: 'Guest',
  get name() {
    return this._name
  },
}

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop] // (*) target = user
  },
})

let admin = {
  __proto__: userProxy,
  _name: 'Admin',
}

// Expected: Admin
alert(admin.name) // 输出:Guest (?!?)

问题实际上出在代理中,在 (*)

  1. 当我们读取 admin.name ,由于 admin 对象自身没有对应的的属性,搜索将转到其原型
  2. 原型是 userProxy
  3. 从代理读取 name 属性时, get 钩子会触发并从原始对象返回 target[prop] 属性,在 (*) 行当调用 target[prop] 时,若 prop 是一个 getter ,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 targetthis._name 即来自 user

更正后的变体

let user = {
  _name: 'Guest',
  get name() {
    return this._name
  },
}

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    // receiver = admin
    return Reflect.get(target, prop, receiver) // (*)
    // return Reflect.get(...arguments)
  },
})

let admin = {
  __proto__: userProxy,
  _name: 'Admin',
}

alert(admin.name) // Admin

回调函数

回调地狱:

  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 嵌套函数一多,就很难处理错误

Generator

Promise

特点:

  • 三种状态( pendingresolvedrejected ),状态一旦切换,不能改变
  • new Promise 立即执行
  • 链式调用,.then 都是返回一个全新的 Promise 对象
  • 解决了回调地狱

缺点:

  • Promise 无法取消
  • 错误只能在回调函数里面捕获
/**
 * 手写 Promise
 * 作用:1、消灭嵌套调用;2、合并多个任务的请求结果
 * API: Promise.resolve, Promise.reject, Promise.prototype.catch,
 * Promise.prototype.finally, Promise.all, Promise.race
 */
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function MyPromise(executor) {
  this.status = PENDING
  this.value = undefined
  this.reason = undefined

  this.onResolvedCallbacks = []
  this.onRejectedCallbacks = []

  const resolve = value => {
    if (this.status === PENDING) {
      this.value = value
      this.status = RESOLVED

      this.onResolvedCallbacks.forEach(fn => fn())
    }
  }

  const reject = reason => {
    if (this.status === PENDING) {
      this.reason = reason
      this.status = REJECTED

      this.onRejectedCallbacks.forEach(fn => fn())
    }
  }

  try {
    executor(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  onResolved = typeof onResolved === 'function' ? onResolved : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : err => {
          throw err
        }

  if (this.status === RESOLVED) {
    onResolved(this.value)
  }

  if (this.status === REJECTED) {
    onRejected(this.reason)
  }

  if (this.status === PENDING) {
    this.onResolvedCallbacks.push(() => {
      onResolved(this.value)
    })

    this.onRejectedCallbacks.push(() => {
      onRejected(this.reason)
    })
  }
}

async / await

async 函数,就是 Generator 函数的语法糖,它建⽴在 Promises 上,并且与所有现有的基于 Promise 的 API 兼容。

  1. async 声明⼀个异步函数(async function someName(){…})
  2. ⾃动将常规函数转换成 Promise 函数,返回值也是⼀个 Promise 对象
  3. 只有 async 函数内部的异步操作执⾏完,才会执⾏ then ⽅法指定的回调函数
  4. 异步函数内部可以使⽤ await
  5. await 暂停异步的功能执⾏(var result = await someAsyncCall();)
  6. 放置在 Promise 调⽤之前,await 强制其他代码等待,直到 Promise 完成并返回结果
  7. 只能与 Promise ⼀起使⽤,不适⽤与回调
  8. await 只能在 async 函数内部使⽤

async/await 相⽐于 promise 的优势与劣势

  1. 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是 then 的链式调⽤也会带来额外的阅读负担
  2. Promise 传递中间值⾮常麻烦,⽽ async/await ⼏乎是同步的写法,⾮常优雅
  3. 错误处理友好,async/await 可以⽤成熟的 try/catch,Promise 的错误捕获⾮常冗余
  4. 调试友好,Promise 的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个 .then 代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的 .then 代码块,因为调试器只能跟踪同步代码的『每⼀步』
  5. 多个异步代码变为同步,浪费性能

定时器

setTimeoutsetIntervalrequestAnimationFrame,其中前两者的时间并不准确。但是最后 requestAnimationFrame 自带函数节流功能,基本可以保证 16.6 毫秒只执行一次,并且该函数的定时效果是精确的,不会有定时器时间不准的问题。

function mySetInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime

  const loop = () => {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime >= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }

  timer = window.requestAnimationFrame(loop)
  return timer
}

let a = 0
mySetInterval(timer => {
  console.log('1')
  a++
  if (a === 3) {
    window.cancelAnimationFrame(timer)
  }
}, 1000)

事件循环 (Event Loop)

渲染 Renderer 进程的主要线程

  • GUI 渲染线程
  • JS 引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步 http 请求线程

常见的宏任务(macrotask)

  • 主代码块 script
  • setTimeout
  • setInterval
  • setImmediate - Node
  • requestAnimationFrame - 浏览器

常见微任务(microtask)

  • process.nextTick() - Node
  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver

具体步骤

  • 首先,整体的script(作为第一个宏任务)开始执行之前,会把所有代码分为同步任务、异步任务两部分,其中异步任务会再分为宏任务和微任务
  • 同步任务会直接进入主线程依次执行
  • 当主线程内的任务执行完毕,主线程为空时,会检查微任务,如果有任务,就全部执行
  • 执行完微任务,就渲染页面
  • 开始下一轮 Event Loop,执行下一个宏任务(异步代码: setTimeout 诸如此类)

PS: 我们可以看到 setTimeout 等宏任务的回调函数在主线程执行,因此,回调函数的执行上下文(this)为 window

/**
 * Event loop(执行一个宏任务,执行所有微任务,再继续如此循环)
 * log:1,4,8,7,3,9,6,5,2
 */
;(function () {
  function test() {
    console.log(1)
    setTimeout(function () {
      console.log(2)
    }, 1000)
  }

  test()

  setTimeout(function () {
    Promise.resolve().then(() => {
      console.log(9)
    })
    console.log(3)
  })

  new Promise(function (resolve) {
    console.log(4)
    setTimeout(function () {
      console.log(5)
    }, 100)
    resolve()
  }).then(function () {
    setTimeout(function () {
      console.log(6)
    }, 0)
    console.log(7)
  })

  console.log(8)
})()

手写 applycallbind 函数

/**
 * apply、call 的模拟实现,这两个方法被调用时,函数会立即执行,并返回结果
 */
Function.prototype.myCall = function (context) {
  const context = context || window
  context.fn = this
  const args = []
  for (let i = 1; i < arguments.length; i++) {
    args.push('arguments[' + i + ']') // 由于后面会使用 eval 表达式,所以不能直接 push 具体的值
  }
  const result = eval('context.fn(' + args + ')')
  delete context.fn
  return result
}

Function.prototype.myApply = function (context, arr) {
  const context = Object(context) || window
  context.fn = this
  let result
  if (!arr) {
    result = context.fn()
  } else {
    const args = []
    for (let i = 0; i < arr.length; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }
  delete context.fn
  return result
}
/**
 * bind 的模拟实现。bind 方法会创建一个新函数,这个函数并不会立即执行。当这个新函数被调用时,bind的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
 */
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
  }

  const self = this
  const args = Array.prototype.slice.call(arguments, 1) // 此处的 arguments 为 bind 时传递的参数
  const fNOP = function () {}

  const fbound = function () {
    /**
     * 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面修改了 fbound.prototype 为 绑定函数的
     * prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。
     *
     * 当作为普通函数时,this 指向 window,self 指向绑定函数,此时结果为 false,当结果为 false 的时候,
     * this 指向绑定的 context。
     */
    self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments))) // 此处的 arguments 返回的函数执行时的参数,两处参数合并起来成为 bind 函数完整的参数
  }

  fNOP.prototype = this.prototype // 空函数中转,防止改变 fbound 函数的 prototype 时改变了原来函数的原型
  fbound.prototype = new fNOP()

  return fbound
}

new

new 过程中发生的四件事儿

  1. 创建了一个空对象
  2. 链接到原型
  3. 绑定 this
  4. 返回对象
/**
 * 模拟实现 new 操作。e.g. myNew(Person,18)
 * @returns 新对象
 */
function myNew() {
  const obj = new Object(), // 用new Object() 的方式新建了一个对象 obj
    Constructor = [].shift.call(arguments) // 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数

  obj.__proto__ = Constructor.prototype

  // function Person(name, age) {
  //   this.strength = 60
  //   this.age = age

  //   return {
  //     name: name,
  //     habit: 'Games',
  //   }
  // }

  // var person = new Person('Kevin', '18')

  // console.log(person.name) // Kevin
  // console.log(person.habit) // Games
  // console.log(person.strength) // undefined
  // console.log(person.age) // undefined

  const result = Constructor.apply(obj, arguments)

  return typeof result === 'object' ? result : obj // 构造函数返回值如果是一个对象,就返回这个对象,如果不是,就该返回什么就返回什么
}

instanceof 原理

通过判断对象(左边)的原型链(__proto__)是不是能找到类型(右边)的 prototype

/**
 * 自定义instanceof函数
 */
function myInstanceof(left, right) {
  const prototype = right.prototype
  let left = left.__proto__

  while (true) {
    if (left === null || left === undefined) {
      return false
    }

    if (prototype === left) {
      return true
    }

    left = left.__proto__
  }
}

其他函数

/**
 * 防抖:事件触发,N秒之后执行。期间再次触发,则重新计算
 */
const debounce = (fn, wait, immediate) => {
  let timer

  const debounced = function (...args) {
    timer && clearTimeout(timer)
    if (immediate) {
      const callNow = !timer

      if (callNow) {
        fn.apply(this, args)
      }

      timer = setTimeout(() => {
        timer = null
      }, wait)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, wait)
    }
  }

  return debounced
}

/**
 * 节流:事件触发,马上执行,N秒之内,事件不再执行,N秒结束之时,再执行一次
 */
const throttle = (fn, wait) => {
  let timer,
    previous = 0
  const throttled = function (...args) {
    const now = +new Date()
    const remaining = wait - (now - previous)

    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      previous = now
      fn.apply(this, args)
    } else if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        previous = +new Date()
        timer = null
      }, remaining)
    }
  }

  return throttled
}

虚拟滚动的原理

  1. 起始状态,根据视窗的高度以及元素的高度,确定元素的数量(N),渲染 N 个元素
  2. 监听滚轮事件/触摸事件,记录列表的总偏移量。
  3. 根据总偏移量计算列表的可视元素起始索引。
  4. 从起始索引渲染元素至视口底部。
  5. 当总偏移量更新时,重新渲染可视元素列表。
  6. 为可视元素列表前后加入缓冲元素。
  7. 在滚动量比较小时,直接修改可视元素列表的偏移量。
  8. 在滚动量比较大时(比如拖动滚动条),会重新渲染整个列表。
  9. 事件节流。

Webpack 优化

减少 webpack 打包时间

  1. 优化 loader,转化的代码越多,需要的时间越久,可以优化、配置 loader 搜索文件的范围
  2. happypack,将 loader 的同步执行转化为并行
  3. DllPlugin,将特定的类库提前打包并引入。可以极大的减少打包类库的次数

减少 Webpack 打包后文件的体积

  1. 按需加载
  2. Tree Shaking