初级前端面试题

525 阅读1小时+

HTTP

HTTP 和 HTTPS

![image-20220302104322859](C:\Users\Captain Space\AppData\Roaming\Typora\typora-user-images\image-20220302104322859.png)

1.http 和 https 的基本概念

  • http: 超文本传输协议 HyperText Transfer Protocol ,是⼀个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
  • https:是以安全为目标的 HTTP 通道,即 HTTP 下 加入 SSL 层进行加密。其作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。

2.HTTP状态码

![image-20220302104819604](C:\Users\Captain Space\AppData\Roaming\Typora\typora-user-images\image-20220302104819604.png)

  • 200 ok 如果是非HEAD请求,返回的响应头有body
  • 204 No Content 响应头没有body数据
  • 206 Partial Content 响应返回的body数据并不是全部
  • 301 Moved Permanently 永久重定向
  • 302 Found 临时重定向 (301 和 302 都会在响应头⾥使⽤字段 Location ,指明后续要跳转的 URL,浏览器会⾃动᯿定向新的 URL。)
  • 304 Not Modified 资源未修改,重定向已存在的缓冲文件
    • 强制缓存:向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。
      • 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)。
      • 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。
      • 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
    • 协商缓存:强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
      • 协商缓存生效,返回304
      • 协商缓存失效,返回200和请求结果结果
  • 400 Bad Request 请求报文有错误(笼统)
  • 403 Forbidden 表示服务器禁止访问资源
  • 404 Not Found 请求的资源在服务器上不存在
  • 500 Internal Server Error 服务器发生了错误(笼统)
  • 501 即将开业,敬请期待
  • 502 服务器自身工作正常,访问后端服务器发生了错误
  • 503 服务器正忙,请稍后

3.Get与Post

GET 和 POST ⽅法都是安全和幂等的吗?

  • 在 HTTP 协议⾥,所谓的「安全」是指请求⽅法不会「破坏」服务器上的资源。
  • 所谓的「幂等」,意思是多次执⾏相同的操作,结果都是「相同」的。

显然,GET是安全且幂等,POST是不安全且不是幂等的

4.http 和 https 的区别及优缺点?

  • http 是超文本传输协议,信息是明文传输,HTTPS 协议要比 http 协议安全,https 是具有安全性的 ssl 加密传输协议
  • http 协议的默认端口为 80,https 的默认端口为 443。
  • http TCP三次握手,https 还要进行SSL握手,会使页面加载时间延长 50%,增加 10%~20%的耗电。
  • https 缓存不如 http 高效,会增加数据开销。
  • Https 协议需要 ca 证书,费用较高
  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名,IPV4 资源支持不了这种消耗。

5.HTTPS解决了HTTP的哪些问题

  • 窃听:混合加密

    • 通信建立前采用非对称加密交换会话密钥,后续采用对称加密

      服务器将公钥传给客户端,客户端用公钥加密会话密钥,传回服务器,服务器用私钥解密拿到会话密钥(非对称加密),之后通过会话密钥加密通信(对称加密)

  • 篡改:摘要算法

    • 客户端在发送明⽂之前会通过摘要算法算出明⽂的「指纹」,发送的时候把「指纹 + 明⽂」⼀同加密成密⽂后,发送给服务器,服务器解密后,⽤相同的摘要算法算出发送过来的明⽂,通过比较客户端携带的「指纹」和当前算出 的「指纹」做⽐较,若「指纹」相同,说明数据是完整的。
  • 冒充:数字证书

    • 将服务器公钥放在数字证书(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。

6.TCP三次握手

  1. 客户端发送SYN,进入SYN_SENT状态

  2. 服务器发送SYN+ACK,进入SYN_RECV状态

  3. 客户端收到SYN+ACK,向服务器发送确认包ACK

    握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。

7.TCP四次挥手

  1. 客户端发出FIN,进入FIN-WAIT-1状态
  2. 服务器收到,发送ACK,进入CLOSE-WAIT状态
  3. 客户端进入FIN-WAIT-2状态
  4. 服务器发送FIN,进入LAST-ACK状态
  5. 客户端收到,发送ACK,进入TIME-WAIT状态
  6. 服务器收到,进入CLOSED状态

8. HTTP1/HTTP2/HTTP3

  • HTTP/1.0的问题就是每发起⼀个请求,都要新建⼀次 TCP 连接(三次握⼿),⽽且是 串⾏请求,做了⽆谓的 TCP 连接建⽴和断开,增加了通信开销。

  • HTTP/1.1提出

    • 长连接(持久连接)Connection:keep-alive
    • 管道传输:举例来说,客户端需要请求两个资源。以前的做法是,在同⼀个TCP连接⾥⾯,先发送 A 请求,然后等待服务器做 出回应,收到后再发出 B 请求。管道机制则是允许浏览器同时发出 A 请求和 B 请求。但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。要是前⾯的回应特别慢,后⾯就会有许多请求 排队等着。这称为「队头堵塞」。
  • HTTP/2

    • 二进制传输:把原来的"Header+Body"的消息"打散"为数个小片的二进制"帧"(Frame),用"HEADERS"帧存放头数据、"DATA"帧存放实体数据。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装
    • 头部压缩
    • 多路复用
    • Server Push:在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟

    队头阻塞没有彻底解决

    我们提到在HTTP/2中,多个请求是跑在一个TCP管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1 了。因为TCP为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该TCP连接中的所有请求。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

  • HTTP/3(基于UDP的QUIC)

    img

    • UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。

    • 快速握手

    • 集成了TLS加密功能

    • 多路复用

      img

9.HTTP请求跨域

  • 跨域:指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的

  • 同源策略:只要协议、域名、端口有任何一个不同,都被当作是不同的域。

  • 解决办法:

    • JSONP:只支持get请求

      //动态创建 script
      var script = document.createElement('script');
      
      // 设置回调函数
      function getData(data) {
          console.log(data);
      }
      
      //设置 script 的 src 属性,并设置请求地址
      script.src = 'http://localhost:3000/?callback=getData';
      
      // 让 script 生效
      document.body.appendChild(script);
      
      
    • CORS:服务器设置Access-Control-Allow-Origin

    • proxy代理

    • nginx反向代理

    • postMessage

输入网址到网页显示的整个流程

  1. 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。
  2. DNS解析
  3. 建立TCP连接
  4. 浏览器发送请求至服务器
  5. 服务器处理http请求返回http响应
  6. 关闭TCP连接
  7. 浏览器渲染

浏览器的渲染过程

  1. 构建对象模型

    在构建对象模型到构建渲染树的这一过程,还穿插着js脚本的加载执行

    • DOMTree的构建

      img

      1. 通过设置charset编码,将Bytes解析成字符
      2. 通过词法解析器,将字符串解析成Token
      3. 生成Nodes并构建DOM树
    • CSSOMTree的构建

      img

      如果JS试图在浏览器还未完成CSSOMTree的下载和构建时去操作CSS样式,浏览器会暂停脚本的运行和DOM的构建,直至浏览器完成了CSSOM的下载和构建。也就是说,JS脚本的出现会让CSSOM的构建阻塞DOM的构建img

      如果我们把css放到前面,js放到最后引入时,构建时间会变成:img

  2. 构建渲染树

  3. 布局

    到目前为止,浏览器计算出了哪些节点是可见的以及它的信息和样式,接下来就需要计算这些节点在设备视口内的确切位置和大小,这个过程我们称之为“布局”。

    布局最后的输出是一个“盒模型”:将所有相对测量值都转换成屏幕上的绝对像素。

  4. 渲染

    最后,既然我们知道了哪些节点可见、它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素:浏览器通过发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

重绘和重排

  • 重排(Reflow):元素的位置发生变动时发生重排,也叫回流。此时在关键渲染路径中的 Layout 阶段,计算每一个元素在设备视口内的确切位置和大小。当一个元素位置发生变化时,其父元素及其后边的元素位置都可能发生变化,代价极高

  • 重绘(Repaint): 元素的样式发生变动,但是位置没有改变。此时在关键渲染路径中的 Paint 阶段,将渲染树中的每个节点转换成屏幕上的实际像素,这一步通常称为绘制或栅格化

    重绘不会带来重新布局,并不一定伴随着重排

Cookie、sessionStorage、localStorage

  • cookie数据大小不能超过4k;sessionStorage和localStorage的存储比cookie大得多,可以达到5M+
  • cookie设置的过期时间之前一直有效;localStorage永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除
  • cookie的数据会自动的传递到服务器;sessionStorage和localStorage数据保存在本地

HTML

HTML5语义化、新特性

HTML5的语义化指的是合理正确的使用语义化的标签来创建页面结构。【正确的标签做正确的事】

  • header nav main article section aside footer
  • 在没CSS样式的情况下,页面整体也会呈现很好的结构效果
  • 代码结构清晰,易于阅读,
  • 利于开发和维护 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
  • 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

CSS

CSS盒模型

CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:外边距(margin)、边框(border)、内边距(padding)、实际内容(content)四个属性。

  • 标准模型和IE模型的区别

    • 计算宽度和高度的不同: 标准盒模型:盒子总宽度/高度 =width/height + padding + border + margin。( 即 width/height 只是 内容高度,不包含 padding 和 border 值 )
    • IE盒子模型:盒子总宽度/高度 =width/height + margin = (内容区宽度/高度 + padding + border) + margin。( 即 width/height 包含了 padding 和 border 值 )
  • CSS如何设置这两种模型

    标准:box-sizing: content-box;( 浏览器默认设置 ) IE:box-sizing: border-box;

BFC:BFC是CSS布局的一个概念,是一块独立的渲染区域,是一个环境,里面的元素不会影响到外部的元素

  • BFC原理(渲染规则|布局规则):

    (1)内部的Box会在垂直方向,从顶部开始一个接着一个地放置; (2)Box垂直方向的距离由margin(外边距)决定,属于同一个BFC的两个相邻Box的margin会发生重叠; (3)每个元素的margin Box的左边, 与包含块border Box的左边相接触,(对于从左到右的格式化,否则相反)。即使存在浮动也是如此; (4)BFC 在页面上是一个隔离的独立容器,外面的元素不会影响里面的元素,反之亦然。文字环绕效果,设置float; (5)BFC 的区域不会与float Box重叠(清浮动); (6)计算BFC的高度时,浮动元素也参与计算。

  • 创建出BFC

    1、浮动(float 的值不为 none) 2、绝对定位元素(position 的值为 absolute 或 fixed) 3、行内块(display 为 inline-block) 4、表格单元(display 为 table、table-cell、table-caption、inline-block 等 HTML 表格相关的属性) 5、弹性盒(display 为 flex 或 inline-flex) 6、overflow 不为 visible

  • 5.4 BFC作用(使用场景)

    1、自适应两(三)栏布局(避免多列布局由于宽度计算四舍五入而自动换行) 2、避免元素被浮动元素覆盖 3、可以让父元素的高度包含子浮动元素,清除内部浮动**(原理:触发父div的BFC属性,使下面的子div都处在父div的同一个BFC区域之内)** 4、去除边距重叠现象,分属于不同的BFC时,可以阻止margin重叠

CSS 的选择器、优先级

第一等:代表内联样式,如: style=””,权值为1000。 第二等:代表ID选择器,如:#content,权值为0100。 第三等:代表类,伪类和属性选择器,如.content,权值为0010。 第四等:代表类型选择器和伪元素选择器,如div p,权值为0001。 通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。 继承的样式没有权值。

CSS3

  • RGBA
  • shadow
  • flex
  • 圆角
  • 动画、过度

水平居中、垂直居中

水平居中

对于行内元素 : text-align: center;

对于确定宽度的块级元素:

  • width和margin实现。margin: 0 auto;
  • 绝对定位和margin-left: calc(父width - 子width)/2, 前提是父元素position: relative
  • 绝对定位加margin-left 负值

对于宽度未知的块级元素

  • table标签配合margin左右auto实现水平居中。使用table标签(或直接将块级元素设值为 display:table),再通过给该标签添加左右margin为auto。
  • inline-block实现水平居中方法。display:inline-block和text-align:center实现水平居中。
  • 绝对定位+transform,transform: translate(-50%,-50%);
  • (4)flex布局使用justify-content:center

垂直居中

  • 利用 line-height 实现居中,这种方法适合纯文字
  • 通过设置父容器 相对定位 ,子级设置 绝对定位,标签通过margin实现自适应居中
  • 弹性布局 flex :父级设置display: flex; align-items: center;
  • 父级设置相对定位,子级设置绝对定位,并且通过位移 transform 实现
  • table 布局,父级通过转换成表格形式,然后子级设置 vertical-align 实现。(需要注意的是:vertical-align: middle使用的前提条件是内联元素以及display值为table-cell的元素)。

用css画三角

/*记忆口诀:盒子宽高均为零,三面边框皆透明。 */
div:after {
  position: absolute;
  width: 0px;
  height: 0px;
  content: " ";
  border-left: 10px solid #000;
  border-right: 10px solid transparent;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

页面布局

1.Flex 布局

布局的传统解决方案,基于盒状模型,依赖 display 属性 + position 属性 + float 属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。

Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。指定容器 display: flex 即可。 简单的分为容器属性和元素属性。

容器的属性:

  • flex-direction:决定主轴的方向(即子 item 的排列方法)flex-direction: row | row-reverse | column | column-reverse;
  • flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse;
  • flex-flow: .box { flex-flow: || ; }
  • justify-content:对其方式,水平主轴对齐方式
  • align-items:对齐方式,竖直轴线方向
  • align-content

项目的属性(元素的属性):

  • order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0
  • flex-grow 属性:定义项目的放大比例,即使存在空间,也不会放大
  • flex-shrink 属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果 定义个 item 的 flow-shrink 为 0,则为不缩小
  • flex-basis 属性:定义了在分配多余的空间,项目占据的空间。
  • flex:是 flex-grow 和 flex-shrink、flex-basis 的简写,默认值为 0 1 auto。
  • align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖
  • align-items,默认属 性为 auto,表示继承父元素的 align-items 比如说,用 flex 实现圣杯布局

2.Rem 布局

首先 Rem 相对于根(html)的 font-size 大小来计算。简单的说它就是一个相对单例 如:font-size:10px;,那么(1rem = 10px)了解计算原理后首先解决怎么在不同设备上设置 html 的 font-size 大小。其实 rem 布局的本质是等比缩放,一般是基于宽度。

优点:可以快速适用移动端布局,字体,图片高度

缺点

①目前 ie 不支持,对 pc 页面来讲使用次数不多; ②数据量大:所有的图片,盒子都需要我们去给一个准确的值;才能保证不同机型的适配; ③在响应式布局中,必须通过 js 来动态控制根元素 font-size 的大小。也就是说 css 样式和 js 代码有一定的耦合性。且必须将改变 font-size 的代码放在 css 样式之前。

3.百分比布局

通过百分比单位 " % " 来实现响应式的效果。通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。 直观的理解,我们可能会认为子元素的百分比完全相对于直接父元素,height 百分比相 对于 height,width 百分比相对于 width。 padding、border、margin 等等不论是垂直方向还是水平方向,都相对于直接父元素的 width。 除了 border-radius 外,还有比如 translate、background-size 等都是相对于自身的。

缺点

(1)计算困难 (2)各个属性中如果使用百分比,相对父元素的属性并不是唯一的。造成我们使用百分比单位容易使布局问题变得复杂。

4.浮动布局

浮动布局:当元素浮动以后可以向左或向右移动,直到它的外边缘碰到包含它的框或者另外一个浮动元素的边框为止。元素浮动以后会脱离正常的文档流,所以文档的普通流中的框就变的好像浮动元素不存在一样。

优点

这样做的优点就是在图文混排的时候可以很好的使文字环绕在图片周围。另外当元素浮动了起来之后,它有着块级元素的一些性质例如可以设置宽高等,但它与inline-block还是有一些区别的,第一个就是关于横向排序的时候,float可以设置方向而inline-block方向是固定的;还有一个就是inline-block在使用时有时会有空白间隙的问题

缺点

最明显的缺点就是浮动元素一旦脱离了文档流,就无法撑起父元素,会造成父级元素高度塌陷

清除浮动的方式

  • 添加额外标签
<div class="parent">
    //添加额外标签并且添加clear属性
    <div style="clear:both"></div>
    //也可以加一个br标签
</div>
  • 父级添加overflow属性,或者设置高度
  • 建立伪类选择器清除浮动
//在css中添加:after伪元素
.parent:after{
    /* 设置添加子元素的内容是空 */
    content: '';
    /* 设置添加子元素为块级元素 */
    display: block;
    /* 设置添加的子元素的高度0 */
    height: 0;
    /* 设置添加子元素看不见 */
    visibility: hidden;
    /* 设置clear:both */
    clear: both;
}

z-index不生效的情况

1.在用z-index的时候,该元素没有定位(非static)

2.在有定位的情况下,该元素的z-index没有生效,是因为该元素的子元素后来居上,盖住了该元素,解决方式:将盖住该元素的子元素的z-index设置为负数,而该元素不设z-index属性.

flex

主轴(mian axis) main start ---------main end

交叉轴(cross axis) cross start--------cross end

注意:主轴不等于水平轴,当flex-direction:column时,主轴为垂直的

容器属性:

  • flex-direction
    • row:默认 从左到右
    • row-reverse:从右到左
    • column:从上到下
    • column-reverse:从下到上
  • flex-wrap
    • nowrap:默认 不换行
    • wrap:换行 第一行在上方
    • wrap-revers:换行 从下到上
  • flex-flow:是上面两个属性的简写
  • justify-content:主轴的对齐方式
    • flex-start :起点对齐
    • flex-end:终点对齐
    • center:中点对齐
    • space-around:每个两侧的间距相同
    • space-between:两端对齐
  • align-items:交叉轴对齐方式
    • flex-start:起点对齐
    • flex-end:终点对齐
    • center:中点对齐
    • baseline:第一行文字的基线对齐
    • stretch:默认 填充容器
  • align-content:定义多根轴线的对齐方式,前提需要设置flex-wrap: wrap
    • flex-start :左对齐
    • flex-end:右对齐
    • center:居中
    • space-around:每个两侧的间距相同
    • space-between:两端对齐
    • stretch:默认img

项目属性:

  • order:定义项目排列顺序,数越小越考前
  • flex-grow:flex容器中剩余空间的多少应该分配给项目
  • flex-shrink:flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值,默认值是1
  • flex-basis:指定了子项在容器主轴方向上的初始大小,优先级高于自身的宽度width
  • flex: flex-grow , flex-shrinkflex-basis 的简写,默认值为 0 1 auto
  • align-self:允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto ,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch

常见布局

1.圣杯布局

定位,浮动,负margin

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
  header,
  footer {
      background-color: aqua;
      text-align: center;

  }

  footer {
      clear: both;
  }

  .box {
      overflow: hidden;
      padding: 0 220px 0 200px;
  }

  .left,
  .main,
  .right {
      float: left;
      position: relative;
      text-align: center;
      border: solid 1px black;
      box-sizing: border-box;
  }

  .main {
      width: 100%;
  }

  .left {
      width: 200px;
      margin-left: -100%;
      left: -200px;
  }

  .right {
      width: 220px;
      margin-left: -220px;
      right: -220px;
  }
</style>
</head>

<body>
<header>圣杯布局</header>
<div class="box">
  <div class="main">
      main
  </div>
  <div class="left">
      left
  </div>
  <div class="right">
      right
  </div>
</div>
<footer>footer</footer>
</body>

</html>

flex实现:

<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
 <style>
     header,
     footer {
         background-color: aqua;
         text-align: center;

     }

     .box {
         display: flex;
     }

     .left,
     .main,
     .right {
         text-align: center;
         border: solid 1px black;
         box-sizing: border-box;

     }

     .main {
         flex: 1;
     }

     .left {
         width: 200px;
     }

     .right {
         width: 220px;
     }
 </style>
</head>

<body>
 <header>圣杯布局</header>
 <div class="box">
     <div class="left">
         left
     </div>
     <div class="main">
         main
     </div>
     <div class="right">
         right
     </div>
 </div>
 <footer>footer</footer>
</body>

</html>

JS

什么是闭包,什么是立即执行函数,它的作用是什么?简单说一下闭包的使用场景

闭包的作用:

  1. 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。

  2. 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化

    function Person(param) {
        var name = param.name; // 私有属性
        this.age = 18; // 共有属性
    
        this.sayName = function () {
            console.log(name);
        }
    }
    
    const tom = new Person({name: 'tom'});
    tom.age += 1; // 共有属性,外部可以更改
    tom.sayName(); // tom
    tom.name = 'jerry';// 共有属性,外部不可更改
    tom.sayName(); // tom
    

缺点:不当的使用会导致内存泄漏

解决办法:给其赋值null解除引用

闭包的使用场景:

  1. 设置私有变量

  2. 回调函数

  3. 防抖节流

经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
},1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

  • 第一种是使用闭包的方式(自执行函数)
for (var i = 1; i <= 5; i++) {  
    (function(j) {    
        setTimeout(function timer() {
            console.log(j)
        },1000)  
    })(i)
}

立即执行函数:声明一个函数,并马上调用这个匿名函数

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量

在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },1000,i)
}
  • 第三种就是使用 let 定义 i 来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, 1000)
}

Javascript的数据类型

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

数据类型检测的方式有哪些

  1. typeof

    数组、对象、null(空对象指针)都会被判断为object,其他判断都正确。

  2. instanceof

    只能正确判断引用数据类型

  3. constructor

    constructor有两个作用,一是判断数据的类型(除了null、undefined),二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

    function Fn(){};
    Fn.prototype = new Array();
    var f = new Fn();
    console.log(f.constructor===Fn);    // false
    console.log(f.constructor===Array); // true
    
  4. Object.prototype.toString.call()

0.1+0.2 ! == 0.3,如何让其相等

(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
function numberepsilon(arg1,arg2){                 
return Math.abs(arg1 - arg2) < Number.EPSILON   
}        
console.log(numberepsilon(0.1 + 0.2, 0.3)) // true

typeof NaN的结果,isNaN 和 Number.isNaN 函数的区别

typeof NaN; // "number"
  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

== 操作符的强制类型转换规则

img

其他值到数字值的转换规则:

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • String 类型如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。

其他值到布尔类型的值的转换规则:

  • 这些是假值: • undefined • null • false • +0、-0 和 NaN • ""

+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。

-、*、\ 只要一边有数字,则按数字计算

  • 第一个 []==false 转为数字 0==0
  • 第二个 ![]==false 转为布尔 false==false

|| 和 && 操作符的返回值

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

let、const、var的区别

  1. 变量提升
  2. 块级作用域
  3. 重复声明
  4. 暂时性死区
区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️
能否改变指针指向✔️✔️×

对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

new操作符的实现步骤

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

箭头函数与普通函数的区别

  • 箭头函数没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变
  • call(),apply(),bind()不能改变this的指向
  • 箭头函数不能作为构造函数使用
  • 没有arguments,prototype

ES6语法

  1. 模板字符串与标签函数

  2. for...of

    Array、Set、Map、伪数组arguments等等,在它们的原型上都有Symbol.iterator标识,并且有默认的Iterator实现,所以它们的实例对象都可以使用for of语法遍历。

  3. 解构赋值

  4. Symbol

  5. Set

    1. 去重
    2. 交并集
  6. Map

  7. Proxy

  8. Class

    1. 封装

      class Animal{
          constructor(name) {
              this.name = name;// 实例属性
          }
          cry() {// 实例方法
          	console.log('cry');
          }
          static getNum(){// 静态方法
              return AnimalES6.num
          }
      }
      Animal.num = 42;// 静态属性
      
    2. 继承

      class Cat extends Animal{
          constructor(name, type) {
              super(name);// 必须先构造父类空间
              this.type = type;
          }
      	cry() {
      		console.log('miao miao');// 方法重写
      	}
      }
      
    3. 多态:允许不同的对象对同一消息做出不同响应

  9. Module

  10. 默认参数

    function add(a, b = 2) {
        return a + b;
    }
    add(1); // 3
    

map和Object的区别

MapObject
意外的键Map默认情况不包含任何键,只包含显式插入的键。Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型Map的键可以是任意值,包括函数、对象或任意基本类型。Object 的键必须是 String 或是Symbol。
键的顺序Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。Object 的键是无序的
SizeMap 的键值对个数可以轻易地通过size 属性获取Object 的键值对个数只能手动计算
迭代Map 是 iterable 的,所以可以直接被迭代。迭代Object需要以某种方式获取它的键然后才能迭代。
性能在频繁增删键值对的场景下表现更好。在频繁添加和删除键值对的场景下未作出优化。

Map

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
["name","张三"],
["age",18],
]

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear()map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。

JavaScript脚本延迟加载的方式有哪些?

  • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。
  • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。
  • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

常见的类数组转换为数组的方法

(1)通过 call 调用数组的 slice 方法来实现转换

Array.prototype.slice.call(arrayLike);

(2)通过 call 调用数组的 splice 方法来实现转换

Array.prototype.splice.call(arrayLike, 0);

(3)通过 apply 调用数组的 concat 方法来实现转换

Array.prototype.concat.apply([], arrayLike);

(4)通过 Array.from 方法来实现转换

Array.from(arrayLike);

数组有哪些原生方法

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
  • 数组首部操作的方法 shift() 和 unshift()
  • 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组
  • 查找特定项的索引的方法,indexOf() 和 lastIndexOf()
  • 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 数组归并方法 reduce() 和 reduceRight() 方法

数组的遍历方法

方法是否改变原数组特点
forEach()数组方法,不改变原数组,没有返回值
map()数组方法,不改变原数组,有返回值,可链式调用
filter()数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用
for...offor...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every() 和 some()数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false.
find() 和 findIndex()数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight()数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作

实现一个AJAX请求

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 设置状态监听函数
xhr.onreadystatechange = function () {
 if (this.readyState !== 4) return;
 // 当请求成功时
 if (this.status === 200) {
     handle(this.response);
 } else {
     console.error(this.statusText);
 }
};
// 设置请求失败时的监听函数
xhr.onerror = function () {
 console.error(this.statusText);
};
// 发送 Http 请求
xhr.send();
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function (resolve, reject) {
     let xhr = new XMLHttpRequest();
     // 新建一个 http 请求
     xhr.open("GET", url, true);
     // 设置响应的数据类型
     xhr.responseType = "json";
     // 设置请求头信息
     xhr.setRequestHeader("Accept", "application/json");
     // 设置状态的监听函数
     xhr.onreadystatechange = function () {
         if (this.readyState !== 4) return;
         // 当请求成功或失败时,改变 promise 的状态
         if (this.status === 200) {
             resolve(this.response);
         } else {
             reject(new Error(this.statusText));
         }
     };
     // 设置错误监听函数
     xhr.onerror = function () {
         reject(new Error(this.statusText));
     };
     // 发送 http 请求
     xhr.send(null);
});
return promise;
}

ajax、axios、fetch的区别

(1)AJAX Ajax 即“Asyn chronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

(2)Fetch fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象

fetch的优点:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

fetch的缺点:

  • fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以
fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

(3)Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击
axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

原型与原型链

1. 对原型、原型链的理解

特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。 img

2. 原型修改、重写

function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true   p上没有constructor就去原型链上找
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
Person.prototype.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

3. 原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person

4. 原型链的终点是什么?如何打印出原型链的终点?

Object.prototype.__proto__=== null // true,所以,原型链的终点是null

原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__

5. 如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性

执行上下文/作用域链

1.对作用域、作用域链的理解

1)全局作用域和函数作用域

(1)全局作用域

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2)函数作用域

  • 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

2. 对执行上下文的理解

1. 执行上下文类型

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

2. 创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

1)创建阶段

(1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段 此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指:

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

this

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

异步编程

1. 异步编程的实现方式?

JavaScript中的异步机制可以分为以下几种:

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

2. setTimeout、Promise、Async/Await 的区别

(1)setTimeout

console.log('script start')	//1. 打印 script start
setTimeout(function(){
 console.log('settimeout')	// 4. 打印 settimeout
})	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')	//3. 打印 script start
// 输出顺序:script start->script end->settimeout

(2)Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
 console.log('promise1')
 resolve()
 console.log('promise1 end')
}).then(function () {
 console.log('promise2')
})
setTimeout(function(){
 console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当JS主线程执行到Promise对象时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

例如:

async function func1() {
   return 1
}
console.log(func1())

img func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。

func1().then(res => {
    console.log(res);  // 30
})

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

3. 对Promise的理解

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

(1)Promise的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

(2)Promise的实例有两个过程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise的特点:

  • 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

Promise的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总结: Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

注意: 在构造 Promise 的时候,构造函数内部的代码是立即执行的

4. Promise的基本用法

(1)创建Promise对象

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
 reject(error);
}
});

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11)代码中,会让promise对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;

创建promise对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建promise对象;

  • Promise.reject

Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:

Promise.reject(new Error(“我错了,请原谅俺!!”));

就是下面的代码new Promise的简单形式:

new Promise(function(resolve,reject){
   reject(new Error("我错了!"));
});

下面是使用resolve方法和reject方法:

function testPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
   }else {
      reject("No thanks");
   }
  });
};
// 方法调用
testPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代码的含义是给testPromise方法传递一个参数,返回一个promise对象,如果为true的话,那么调用promise对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用promise对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

(2)Promise方法

Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。

  1. then()

当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?

promise.then(function(value) {
 // success
}, function(error) {
 // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

当要写有顺序的异步事件时,需要串行时,可以这样写:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
   return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
           resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{

})

那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。

2. catch()

Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
    }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

3. all()

all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

javascript
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3] 
})

调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。

(4)race()

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
      resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
	console.log(res);
	//结果:2
},rej=>{
    console.log(rej)};
)

那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

5. finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise
.finally(() => {
  // 语句
});
// 等同于
promise
.then(
 result => {
    // 语句
   return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

5. Promise解决了什么问题

在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
   console.log(data)
})
})
})

上面的代码有如下缺点:

  • 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。
  • 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。

Promise出现之后,代码变成这样:

let fs = require('fs')
function read(url){
 return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
     error && reject(error)
      resolve(data)
    })
 })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

这样代码看起了就简洁了很多,解决了地狱回调的问题。

6. Promise.all和Promise.race的区别的使用场景

(1)Promise.all Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

(2)Promise.race

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

7. 对async/await 的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:

async function testAsy(){
return 'hello world';
}
let result = testAsy(); 
console.log(result)

img

所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

async function testAsy(){
return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
 console.log(v)   // hello world
})

那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

8. await 到底在等啥?

await 在等待什么呢? 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
return "something";
}
async function testAsync() {
 return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
 const v2 = await testAsync();
console.log(v1, v2);
}
test();

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

来看一个例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
      resolve(x);
     }, 3000)
   }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
 console.log(result);    // 3秒钟之后出现hello world
  console.log('cuger')   // 3秒钟之后出现cug
}
testAwt();
console.log('cug')  //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。

9. async/await的优势

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:

/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
   return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
   });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
           console.log(`result is ${result}`);
            console.timeEnd("doIt");
       });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
   const result = await step3(time3);
    console.log(`result is ${result}`);
   console.timeEnd("doIt");
}
doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

10. async/await对比Promise的优势

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

11. async/await 如何捕获异常

async function fn(){
    try{
       let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

12. 并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

13. 什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

以下代码就是一个回调函数的例子:

ajax(url, () => {
 // 处理逻辑
})

回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,可能会有如下代码:

ajax(url, () => {
 // 处理逻辑
 ajax(url1, () => {
     // 处理逻辑
     ajax(url2, () => {
         // 处理逻辑
     })
})
})

以上代码看起来不利于阅读和维护,当然,也可以把函数分开来写:

function firstAjax() {
ajax(url1, () => {
 // 处理逻辑
 secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
 // 处理逻辑
})
}
ajax(url, () => {
// 处理逻辑
firstAjax()
})

以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。回调地狱的根本问题就是:

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

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return

14. setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有 setTimeoutsetIntervalrequestAnimationFrame。最常用的是setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
count++
// 代码执行所消耗的时间
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循环所消耗的时间
currentInterval = interval - offset 
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) 
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)
复制代码

接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

function demo() {
setInterval(function(){
console.log(2)
},1000)
sleep(2000)
}
demo()
复制代码

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:

function setInterval(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
setInterval(timer => {
console.log(1)
a++
if (a === 3) cancelAnimationFrame(timer)
}, 1000)
复制代码

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout

面向对象

1. 对象创建的方式有哪些?

Object构造函数模式

var person = new Object();
person.name = "Jhonha";
person.age = '27';
person.job = 'dancer';
person1.sayName = function () {
console.log(this.name);
}
person1.sayName(); // Jhonha
console.log(person1.age); // 27

构造函数在不返回值得情况下,默认会返回新对象实例。而通过构造函数末尾添加return语句,可以重写调用构造函数时候的返回值。

对象字面量模式

var person = {
 name: 'Jhonha',
 age: 22,
 job: 'dancer',
 sayName: function () {
   console.log(this.name);
 }
}
person2.sayName(); // dancer
复制代码

虽然Object构造函数和对象字面量都可以用来创建单个对象,但这些方法有个明显的缺点,就是一个接口创建很多对象,会产生大量的重复代码

工厂模式

function createPerson(name, age, job) {
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function () {
   console.log(this.name);
 };
 return o;
}
var person1 = createPerson('Jhonha', 22, 'dancer');
var person2 = createPerson('Ciris', 22, 'Software engineer');
console.log(person1.name); // Jhonha
console.log(person2.name); // Ciris

优点:解决了创建多个相似对象的问题,函数封装创建对象, 无需写重复创建对象的代码 缺点:没有解决对象识别的问题

构造函数模式

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function () {
     console.log(this.name);
 }
}
var person1 = new Person('Jhonha', 22, 'dancer');
var person2 = new Person('Ciris', 22, 'Software engineer');
console.log(person1.name); // Jhonha
console.log(person2.name); // Ciris

构造函数与其他函数唯一的区别,就在于调用他们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那他就可以作为构造函数。

// 当作构造函数使用
var person = new Person("Jhonha", 27, "dancer");
person.sayName(); //"Jhonha"

// 作为普通函数调用
Person("Ciris", 22, "software engineer"); // 添加到window
window.sayName(); //"Ciris"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

组合使用构造函数模式和原型模式

创建自定义类型是最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.friends = ["Jhonha","Nick"]
}

Person.prototype = {
 constructor: Person,
 sayName: function () {
     console.log(this.name);
 }
}
var person1 = new Person('bob', 22, 'frontend');
var person2 = new Person('lynn', 22, 'doctor');

person1.friends.push("Van");
console.log(person1.friends); // Jhonha Nick Van
console.log(person2.friends); // Jhonha Nick
console.log(person1.name); // bob
console.log(person2.name); // lynn
console.log(person1.sayName === person2.sayName); // true

这种构造函数和原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建定义类型的方法。

2. 对象继承的方式有哪些?

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

3.垃圾回收与内存泄漏

(1)垃圾回收的概念

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
  • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
  • 当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
(2)垃圾回收的方式

浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。

1)标记清除

  • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

2)引用计数

  • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
  • 这种方法会引起循环引用的问题:例如: obj1obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

这种情况下,就要手动释放变量占用的内存:

obj1.a =  null
 obj2.a =  null
(3)减少垃圾回收

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
  • object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
  • 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
(4)哪些情况会导致内存泄漏
  • 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

Vue

Vue 中双向数据绑定的实现原理是怎样的?

3.png

  1. observer 监听器,用来劫持并监听所有属性,属性变化,则通过dep通知订阅者

    Object.defineProperty()

    /**
      * 循环遍历数据对象的每个属性
      */
    function observable(obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) => {
            defineReactive(obj, key, obj[key])
        })
        return obj;
    }
    /**
     * 将对象的属性用 Object.defineProperty() 进行设置
     */
    function defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
            get() {
                console.log(`${key}属性被读取了...`);
                return val;
            },
            set(newVal) {
                console.log(`${key}属性被修改了...`);
                val = newVal;
            }
        })
    }
    //通过以上方法封装,我们可以直接定义 person:
    let person = observable({
        name: 'tom',
        age: 15
    });
    

    使用 Object.defineProperty() 来进行数据劫持有什么缺点?

    在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

    在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变

  2. dep 订阅器,对observer和watcher统一管理

    发布—订阅模式

  3. watcher订阅者,收到属性变化,执行更新函数,从而更新视图
  4. compile解析器,对模板数据初始化

谈谈对vue生命周期的理解?

vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

![生命周期](C:\Users\Captain Space\Desktop\面试题2.assets\生命周期.jpg)

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。

  • beforeCreate:是 new Vue() 之后触发的第一个钩子,在当前阶段 datamethodscomputed 以及 watch 上的数据和方法都不能被访问。
  • created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 Dom
  • beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated
  • mounted:在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用 $refs 属性对 Dom 进行操作。
  • beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 dom 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
  • updated:发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
  • beforeDestroy:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed:发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。

推荐在 created 钩子函数中调用异步请求,有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMountmounted 钩子函数,所以放在 created 中有助于一致性;

组件生命周期

  • 加载渲染过程 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
  • 子组件更新过程 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
  • 父组件更新过程 父beforeUpdate -> 父updated
  • 销毁过程 父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

MVVM、MVC、MVP的区别

(1)MVC

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。 image.png

(2)MVVM

MVVM 分为 Model、View、ViewModel:

  • Model代表数据模型,数据和业务逻辑都在Model层中定义;
  • View代表UI视图,负责数据的展示;
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。 image.png

组件中的data为什么是一个函数?

  1. 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
  2. 如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

  • 不会立即同步执行重新渲染。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

  • 如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。

  • 然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.thenMessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。

  • 当刷新队列时,组件会在事件循环队列清空时的下一个 tick 更新。
  • 多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。
  • 虽然 Vue.js 通常鼓励开发人员沿着 “数据驱动” 的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

vue的事件绑定

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的 $on 实现的。如果要在组件上使用原生事件,需要加 .native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。

on、emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器

虚拟 DOM 实现原理?

虚拟DOM的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM树的差异;
  • pach 算法 — 将两个虚拟 DOM对象的差异应用到真正的 DOM 树。

虚拟 dom 和真实 dom 的区别

  • 虚拟 DOM 不会进行排版与重绘操作
  • 虚拟 DOM 就是把真实 DOM 转换为 Javascript 代码
  • 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后并在真实 DOM 中进行排版与重绘,减少过多 DOM 节点排版与重绘损耗

computed与watch

通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能

computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量 改变时,计算属性也会跟着改变;更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

watch 属性监听

  • 它不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

computed 计算属性 属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed中的函数必须用return返回最终的结果 computed更高效,优先使用。data 不改变,computed 不更新。

使用场景 computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能 watch:当一条数据影响多条数据的时候使用,例:搜索数据

过滤器的作用,如何实现一个过滤器

filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。

<li>商品价格:{{item.price | filterPrice}}</li>

filters: {
filterPrice (price) {
  return price ? ('¥' + price) : '--'
}
}

nextTick

由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。

this.$nextTick(() => {    // 获取数据的操作...})

所以,在以下情况下,会用到nextTick:

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。

  • 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。

    因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。

使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽

vue中的插槽是一个非常好用的东西slot说白了就是一个占位的

  • 默认插槽:又名匿名查抄,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。

实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

为什么v-for和v-if不建议用在一起

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费。这种场景建议使用 computed,先对数据进行过滤

注意:3.x 版本中 v-if 总是优先于 v-for 生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。

Vue 项目中 key 的作用

  • key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;

    vue采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。

  • 为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用。

    当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。

v-if和v-show的区别

  • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

keep-alive的实现

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染

场景:tabs标签页 后台导航,vue性能优化

原理:Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin 多个组件有相同的逻辑,抽离出来 mixin并不是完美的解决方案,会有一些问题 vue3提出的Composition API旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】 场景:PC端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合 劣势:1.变量来源不明确,不利于阅读 2.多mixin可能会造成命名冲突 3.mixin和组件可能出现多对多的关系,使得项目复杂度变高

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

Vue中封装的数组方法有哪些,其如何实现页面更新

在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。

image.png

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。

vue组件的通信方式

(1) props / $emit

父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

    有两种常见的试图改变一个 prop 的情形 :

    • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    
    • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
    
  • props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。

  • props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式

// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>
// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
    name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
        this.currentIndex = idx
    }
  }
}
</script>
//子组件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
      emitIndex(index) {
          this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}
</script>

(2)eventBus事件总线($emit / $on

eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下:

(1)创建事件中心管理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假设有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
    components: { firstCom, secondCom }
}
</script>

firstCom组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
          num:this.num++
      })
    }
  }
}
</script>

(3)接收事件secondCom组件中发送事件:

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

(3)依赖注入(provide / inject)

这种方式就是Vue中的依赖注入,该方法用于在层数很深的情况

provide / inject是Vue提供的两个钩子,和datamethods是同级的。并且provide的书写形式和data一样。

  • provide 钩子用来发送数据或方法
  • inject钩子用来接收数据或方法

在父组件中:

provide() { 
    return {     
        num: this.num  
    };
}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意: 依赖注入所提供的属性是非响应式的。

(4)ref / $refs

这种方式也是实现父子组件之间的通信。

ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

在子组件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(5)$attrs / $listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A组件(APP.vue):

<template>
    <div id="app">
        //此处监听了两个事件,可以在B组件或者C组件中直接触发 
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

B组件(Child1.vue):

<template>
    <div class="child-1">
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{$attrs}}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
    </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1'); // 触发APP.vue中的test1方法
    }
};
</script>

C 组件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 触发APP.vue中的test2方法
    }
};
</script>

在上述代码中:

  • C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
  • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

(2)兄弟组件间通信

  • 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

(3)任意组件之间

  • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

  • props/$emit 父子组件通信

    父->子props,子->父 $on、$emit 获取父子组件实例 parent、children Ref 获取实例的方式调用组件的属性或者方法 父->子孙 Provide、inject 官方不推荐使用,但是写组件库时很常用

  • $emit/$on 自定义事件 兄弟组件通信

    Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue() 自定义事件

  • vuex 跨级组件通信

    Vuex、$attrs、$listeners Provide、inject

父组件如何监听子组件的生命周期?

父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
}, 
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
}, 
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:createdupdated 等都可以监听。

Vuex的理解及使用场景

Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。

  1. Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,

若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex主要包括以下几个核心模块:

  1. State:定义了应用的状态数据

  2. Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算

  3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数

  4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作

  5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

img

(2)各模块在核心流程中的主要功能:

  • Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
  • dispatch∶操作行为触发方法,是唯一能执行action的方法。
  • actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  • commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  • mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  • state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

Vue 单页应用与多页应用的区别

概念:

  • SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。

    优点:

    • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
    • 基于上面一点,SPA 相对对服务器压力小;
    • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

    缺点:

    • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
    • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
    • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
  • MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

区别: 775316ebb4c727f7c8771cc2c06e06dd.jpg

如何获取页面的hash变化

(1)监听$route的变化

// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
},
复制代码

(2)window.location.hash读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。

routeroute 和router 的区别

  • routerVueRouter的实例,在script标签中想要导航到不同的URL,使用router` 是 `VueRouter` 的实例,在 `script` 标签中想要导航到不同的 `URL`,使用 `router.push方法。返回上一个历史history$router.to(-1)
  • $route 为当前 router 跳转对象。里面可以获取当前路由的 name,path,query,parmas 等。

如何定义动态路由?如何获取传过来的动态参数?

区别:query通过url传参,刷新页面还在;params属性页面不在

(1)param方式

  • 配置路由格式:/router/:id
  • /router/:id?意思是id可不传
  • 传递的方式:在path后面跟上对应的值
  • 传递后形成的路径:/router/123

1)路由定义

//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>    

//在index.js
{
   path: '/user/:userid',
   component: User,
},

2)路由跳转

// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link

// 方法2:
this.$router.push({name:'users',params:{uname:wade}})

// 方法3:
this.$router.push('/user/' + wade)

3)参数获取 通过 $route.params.userid 获取传递的值

(2)query方式

  • 配置路由格式:/router,也就是普通配置
  • 传递的方式:对象中使用query的key作为传递方式
  • 传递后形成的路径:/route?id=123

1)路由定义

//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>

// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>    

profileClick(){
  this.$router.push({
    path: "/profile",
    query: {
        name: "kobi",
        age: "28",
        height: 198
    }
  });
}

2)跳转方法

// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>

// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})

// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>

// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})

// 方法5:
this.$router.push('/user?uname=' + jsmes)

3)获取参数

  • 通过$route.query 获取传递的值

Vue-router 路由钩子在生命周期的体现

一、Vue-Router导航守卫

有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。

  1. 全局路由钩子

vue-router全局有三个路由钩子;

  • beforeEach 全局前置守卫 进入路由之前
  • beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
  • afterEach 全局后置钩子 进入路由之后

具体使用∶

  • beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {  
    let ifInfo = Vue.prototype.$common.getSession('userData');  // 判断是否登录的存储信息
    if (!ifInfo) { 
        // sessionStorage里没有储存user信息    
        if (to.path == '/') { 
            //如果是登录页面路径,就直接next()      
            next();    
        } else { 
            //不然就跳转到登录      
            Message.warning("请重新登录!");     
            window.location.href = Vue.prototype.$loginUrl;    
        }  
    } else {    
        return next();  
    }
})
  • afterEach (跳转之后滚动条回到顶部)
router.afterEach((to, from) => {  
    // 跳转之后滚动条回到顶部  
    window.scrollTo(0,0);
});
  1. 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next

export default [    
    {        
        path: '/',        
        name: 'login',        
        component: login,        
        beforeEnter: (to, from, next) => {          
            console.log('即将进入登录页面')          
            next()        
        }    
    }
]
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

这三个钩子都有三个参数∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:

beforeRouteEnter(to, from, next) {      
    next(target => {        
        if (from.path == '/classProcess') {          
            target.isFromProcess = true        
        }      
    })    
}

二、Vue路由钩子在生命周期函数的体现

  1. 完整的路由导航解析流程(不包括其他生命周期)

  • 触发进入其他路由。

  • 调用要离开路由的组件守卫beforeRouteLeave

  • 调用局前置守卫∶ beforeEach

  • 在重用的组件里调用 beforeRouteUpdate

  • 调用路由独享守卫 beforeEnter。

  • 解析异步路由组件。

  • 在将要进入的路由组件中调用 beforeRouteEnter

  • 调用全局解析守卫 beforeResolve

  • 导航被确认。

  • 调用全局后置钩子的 afterEach 钩子。

  • 触发DOM更新(mounted)。

  • 执行beforeRouteEnter 守卫中传给 next 的回调函数

  1. 触发钩子的完整顺序

路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。

  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。

  • beforeEnter:路由独享守卫

  • beforeRouteEnter:路由组件的组件进入路由前钩子。

  • beforeResolve:路由全局解析守卫

  • afterEach:路由全局后置钩子

  • beforeCreate:组件生命周期,不能访问tAis。

  • created;组件生命周期,可以访问tAis,不能访问dom。

  • beforeMount:组件生命周期

  • deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。

  • mounted:访问/操作dom。

  • activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。

  • 执行beforeRouteEnter回调函数next。

  1. 导航行为被触发到导航完成的整个过程

  • 导航行为被触发,此时导航未被确认。
  • 在失活的组件里调用离开守卫 beforeRouteLeave。
  • 调用全局的 beforeEach守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  • 在路由配置里调用 beforeEnteY。
  • 解析异步路由组件(如果有)。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
  • 触发 DOM 更新。
  • 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
  • 导航完成

params和query的区别

用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name

url地址显示:query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示

注意:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。

手写代码

深拷贝

//浅拷贝 
1. Object.assign(target,source)
2. es6对象扩展运算符。
//深拷贝
function deepClone(obj) {
 //这里注意, 数组的instanceof是Object 所有这里要判断instanceof Array 
 let copy = obj instanceof Array ? [] : {}
 for (let key in obj) {
     //for in 会遍历到原型上的属性
     if (obj.hasOwnProperty(key)) {
         copy[key] =
             typeof obj[key] === 'object'
                 ? deepClone(obj[key]) : obj[key]
     }
 }
 return copy
}

call

Function.prototype.call = function (context, ...args) {
    context = context || window // 判断上下文是否传入,默认window
    context.fn = this // 通过挂载到context上,实现this的转移
    const res = context.fn(...args) // 立即执行
    delete context.fn
    return res 
}

apply

Function.prototype.apply = function (context, ...args) {
    context = context || window // 判断上下文是否传入,默认window
    context.fn = this // 通过挂载到context上,实现this的转移
    const res = context.fn(...args[0]) // 立即执行
    delete context.fn
    return res 
}

bind

Function.prototype.bind = function(context,...args) {
	context = context || window // 判断上下文是否传入,默认window
    context.fn = this // 通过挂载到context上,实现this的转移
    // 返回闭包
    return function() {
    	const res = context.fn(...args) // 立即执行
    	delete context.fn 
        return res 
    }
}

jsonp

var newscript = document.createElement('script')
newscript.src = 'https://www.adb.com?callback=fn'
document.body.appendChild(newscript);

promise

class MyPromise {
    constructor(executor) {
        this.status = 'pending' // 初始状态为等待
        this.value = null // 成功的值
        this.reason = null // 失败的原因
        this.onFulfilledCallbacks = [] // 成功的回调函数存放的数组
        this.onRejectedCallbacks = [] // 失败的回调函数存放的数组
        let resolve = value => {
            if (this.status === 'pending') {
                this.status = 'fulfilled'
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn()) // 调用成功的回调函数
            }
        }
        let reject = reason => {
            if (this.status === 'pending') {
                this.status = 'rejected'
                this.reason = reason
                this.onRejectedCallbacks.forEach(fn => fn()) // 调用失败的回调函数
            }
        };
        try {
            executor(resolve, reject)
        } catch (err) {
            reject(err)
        }
    }
    then(onFulfilled, onRejected) {
        // onFulfilled如果不是函数,则修改为函数,直接返回value
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        // onRejected如果不是函数,则修改为函数,直接抛出错误
        onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
        return new MyPromise((resolve, reject) => {
            if (this.status === 'fulfilled') {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                    } catch (err) {
                        reject(err)
                    }
                })
            }
            if (this.status === 'rejected') {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason)
                        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                    } catch (err) {
                        reject(err)
                    }
                })
            }
            if (this.status === 'pending') {
                this.onFulfilledCallbacks.push(() => { // 将成功的回调函数放入成功数组
                    setTimeout(() => {
                        let x = onFulfilled(this.value)
                        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                    })
                })
                this.onRejectedCallbacks.push(() => { // 将失败的回调函数放入失败数组
                    setTimeout(() => {
                        let x = onRejected(this.reason)
                        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                    })
                })
            }
        })
    }
}

promise.all

Promise.all = function (promises) {
    const values = new Array(promises.length)
    var resolvedCount = 0 //状态为resolved的promise的数量
    return new Promise((resolve, reject) => {
        // 遍历promises,获取每个promise的结果
        promises.forEach((p, index) => {
            Promise.resolve(p).then(
                value => {
                    // p状态为resolved,将值保存起来
                    values[index] = value
                    resolvedCount++;
                    // 如果全部p都为resolved状态,return的promise状态为resolved
                    if (resolvedCount === promises.length) {
                        resolve(values)
                    }
                },
                reason => { //只要有一个失败,return的promise状态就为reject
                    reject(reason)
                }
            )
        })
    })
}

promise.race

/*
  返回一个promise对象,状态由第一个完成的promise决定
  */
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((p, index) => {
            Promise.resolve(p).then(
                value => {
                    // 只要有一个成功,返回的promise的状态九尾resolved
                    resolve(value)

                },
                reason => { //只要有一个失败,return的promise状态就为reject
                    reject(reason)
                }
            )
        })
    })
}


防抖

//防抖
function debounce(fn, timeout) {
    let timer = null
    return function (...arg) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arg)
        }, timeout);
    }
}

节流

//节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if (timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}

数组去重

//filter加indexOf
function unique(arr) {
 let res = arr.filter((item, index, array) => {
     return array.indexOf(item) === index
 })
 return res
}
//ES6  Set
let unique2 = arr => [...new Set(arr)]
//先排序,然后比较相邻项是否相同
function unique3(arr) {
 arr = arr.sort()
 let res = [arr[0]]
 for (let i = 1; i < arr.length; i++) {
     if (arr[i] !== arr[i - 1]) {
         res.push(arr[i])
     }
 }
 return res
}
//reduce+includes
function unique4(arr) {
    arr.reduce((acc, cur) => {
        if (!acc.includes(cur)) {
            acc.push(cur)
        }
        return acc
    }, [])
}
//Map
function unique5(arr) {
 let map = new Map()
 arr.forEach(item => {
     if (!map.has(item)) {
         map.set(item,item)
     }
 });
 return [...map.values()]
}
//test
a = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique5(a));

数组对象去重

//对象
const list = [{age:18,name:'张三'},{age:18,name:'李四'},{age:18,name:'王五'}]
let hash = {};
const newArr = arr.reduce((item, next) => {
    hash[next.age] ? '' : hash[next.age] = true && item.push(next);
    return item;
}, []);
console.log(list);

数组扁平化

function flatten(arr) {
      return arr.reduce((result, item) => {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
      }, []);
}

排序算法

arr = [5, 4, 3, 2, 1]
//冒泡排序
function mp(arr) {
    const { length } = arr
    let temp
    for (let i = 0; i < length; i++) {
        for (let j = 0; j < length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    console.log(arr);
}

//选择排序
function xz(arr) {
    const { length } = arr
    for (let i = 0; i < length - 1; i++) {
        let indexMin = i
        for (let j = i + 1; j < length; j++) {
            if (arr[i] > arr[j]) {
                indexMin = j
            }
        }
        if (i !== indexMin) {
            let temp = arr[i]
            arr[i] = arr[indexMin]
            arr[indexMin] = temp
        }
    }
    console.log(arr);
}
//插入排序
function cr(arr) {
    const { length } = arr
    let temp
    let j, i
    for (i = 1; i < length; i++) {
        temp = arr[i]
        for (j = i - 1; j >= 0; j--) {
            if (arr[j] > temp) {
                arr[j + 1] = arr[j]
            }
            else {
                break
            }
        }
        arr[j + 1] = temp

    }
    console.log(arr);
}
//快速排序1开始
function quickSort(arr) {
    return quick(arr, 0, arr.length - 1)
}
function quick(arr, left, right) {
    let index
    if (arr.length <= 1) return arr
    index = part(arr, left, right)
    if (left < index - 1) {
        quick(arr, left, index - 1)
    }
    if (index < right) {
        quick(arr, index, right)
    }
    console.log(arr);
}
//分区操作
function part(arr, left, right) {
    const pivot = (left + right) >> 1
    let i = left, j = right, temp
    while (i <= j) {
        while (arr[i] < arr[pivot]) {
            i++
        }
        while (arr[j] > arr[pivot]) {
            j--
        }
        //在主元左侧找到了比主元大的,在主元右侧找到了比主元小的
        //如果此时仍满足i<=j,则交换
        if (i <= j) {
            temp = arr[i]
            arr[i] = arr[j]
            arr[j] = temp
            i++
            j--
        }
    }
    return i
}
//快速排序1结束
//快速排序2
function quickSort2(arr) {
    if (arr.length <= 1) return arr;
    //取中间数
    let pivotIndex = arr.length >> 1;
    //splice改变原数组
    let pivot = arr.splice(pivotIndex, 1)[0];
    let left = [], right = [];
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] <= pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort2(left).concat(pivot, quickSort2(right));

}

//归并排序
function mergeSort(arr) {  // 采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    let middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right)
{
    let result = [];
    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }
    while (left.length)
        result.push(left.shift());
    while (right.length)
        result.push(right.shift());
    console.log(result);
    return result;
}

let arr=[2,3,5,1,9]
mergeSort(arr)




继承

组合继承

function Animal(name) {
    this.name = name
    this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
    return this.name
}
function Dog(name, age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype =  new Animal()
Dog.prototype.constructor = Dog

let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2) 
// { name: "哈赤", colors: ["black", "white"], age: 1 }

寄生式组合继承

Dog.prototype =  new Animal()    改为   Object.create(Animal.prototype)

Class实现继承

class Animal {
    constructor(name) {
        this.name = name
    } 
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}

事件总线(发布订阅模式)

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
	console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
	console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'

函数柯里化

function curry(fn) {
    let judge = (...args) => {
        if (args.length >= fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}