51
网上找到的各种面试题整理,长期更新。大部分答案整理来自网络,有问题的地方,希望大家能指出,及时修改;技术更新迭代,也会及时更新
博客原地址:https://finget.github.io/2019...

前端

前端性能优化

1.清理 HTML 文档

HTML,即超文本标记语言,几乎是所有网站的支柱。HTML 为网页带来标题、子标题、列表和其它一些文档结构的格式。在最近更新的 HTML5 中,甚至可以创建图表。

HTML 很容易被网络爬虫识别,因此搜索引擎可以根据网站的内容在一定程度上实时更新。在写 HTML 的时候,你应该尝试让它简洁而有效。此外,在 HTML 文档中引用外部资源的时候也需要遵循一些最佳实践方法。

a.恰当放置 CSS

Web 设计者喜欢在网页建立起主要的 HTML 骨架之后再来创建样式表。这样一来,网页中的样式表往往会放在 HTML 的后面,接近文档结束的地方。然而推荐的做法是把 CSS 放在 HTML 的上面部分,文档头之内,这可以确保正常的渲染过程。

这个策略不能提高网站的加载速度,但它不会让访问者长时间看着空白屏幕或者无格式的文本(FOUT)等待。如果网页大部分可见元素已经加载出来了,访问者才更有可能等待加载整个页面,从而带来对前端的优化效果。这就是知觉性能

b.正确放置 Javascript

另一方面,如果将 JavaScript 放置在 head 标签内或 HTML 文档的上部,这会阻塞 HTML 和 CSS 元素的加载过程。这个错误会导致页面加载时间增长,增加用户等待时间,容易让人感到不耐烦而放弃对网站的访问。不过,您可以通过将 JavaScript 属性置于 HTML 底部来避免此问题。

此外,在使用 JavaScript 时,人们通常喜欢用异步脚本加载。这会阻止<script>标签在 HTML 中的呈现过程,如,在文档中间的情况。

虽然对于网页设计师来说, HTML 是最值得使用的工具之一,但它通常要与 CSS 和 JavaScript 一起使用,这可能会导致网页浏览速度减慢。 虽然 CSS 和 JavaScript 有利于网页优化,但使用时也要注意一些问题。使用 CSS 和 JavaScript 时,要避免嵌入代码。因为当您嵌入代码时,要将 CSS 放置在样式标记中,并在脚本标记中使用 JavaScript,这会增加每次刷新网页时必须加载的 HTML 代码量。

2.优化 CSS 性能

CSS,即级联样式表,能从 HTML 描述的内容生成专业而又整洁的文件。很多 CSS 需要通过 HTTP 请求来引入(除非使用内联 CSS),所以你要努力去除累赘的 CSS 文件,但要注意保留其重要特征。

如果你的 Banner、插件和布局样式是使用 CSS 保存在不同的文件内,那么,访问者的浏览器每次访问都会加载很多文件。虽然现在 HTTP/2 的存在,减少了这种问题的发生,但是在外部资源加载的情况下,仍会花费较长时间。要了解如何减少 HTTP 请求以大幅度缩减加载时间,请阅读WordPress 性能。

此外,不少网站管理员在网页中错误的使用 @import 指令 来引入外部样式表。这是一个过时的方法,它会阻止浏览并行下载。link 标签才是最好的选择,它也能提高网站的前端性能。多说一句,通过 link 标签请求加载的外部样式表不会阻止并行下载。

3.减少外部HTTP请求

在很多情况下,网站的大部分加载时间来自于外部的 Http 请求。外部资源的加载速度随着主机提供商的服务器架构、地点等不同而不同。减少外部请求要做的第一步就是简略地检查网站。研究你网站的每个组成部分,消除任何影响访问者体验不好的成分。这些成分可能是:

  • 不必要的图片
  • 没用的 JavaScript 代码
  • 过多的 css
  • 多余的插件

在你去掉这些多余的成分之后,再对剩下的内容进行整理,如,压缩工具、CDN 服务和预获取(prefetching)等,这些都是管理 HTTP 请求的最佳选择。除此之外,减少DNS路由查找教程会教你如何一步一步的减少外部 HTTP 请求。

4.压缩 CSS, JS 和 HTML

压缩技术可以从文件中去掉多余的字符。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。

  1. 使用预先获取

预先获取可以在真正需要之前通过取得必需的资源和相关数据来改善访问用户的浏览体验,主要有3类预先获取:

  • 链接预先获取
  • DNS 预先获取
  • 预先渲染

在你离开当前 web 页面之前,使用预先获取方式,对应每个链接的 URL 地址,CSS,图片和脚本都会被预先获取。这保证了访问者能在最短时间内使用链接在画面间切换。

幸运的是,预先获取很容易实现。根据你想要使用的预先获取形式,你只需在网站 HTML 中的链接属性上增加 rel=”prefetch”,rel=”dns-prefetch”,或者 rel=”prerender” 标记。

6.使用 CDN 和缓存提高速度

内容分发网络能显著提高网站的速度和性能。使用 CDN 时,您可以将网站的静态内容链接到全球各地的服务器扩展网络。如果您的网站观众遍布全球,这项功能十分有用。 CDN 允许您的网站访问者从最近的服务器加载数据。如果您使用 CDN,您网站内的文件将自动压缩,以便在全球范围内快速分发。

CDN 是一种缓存方法,可极大改善资源的分发时间,同时,它还能实现一些其他的缓存技术,如,利用浏览器缓存。

合理地设置浏览器缓存,能让浏览器自动存储某些文件,以便加快传输速度。此方法的配置可以直接在源服务器的配置文件中完成。

7.压缩文件

虽然许多 CDN 服务可以压缩文件,但如果不使用 CDN,您也可以考虑在源服务器上使用文件压缩方法来改进前端优化。 文件压缩能使网站的内容轻量化,更易于管理。 最常用的文件压缩方法之一是 Gzip。 这是缩小文档、音频文件、PNG图像和等其他大文件的绝佳方法。

Brotli 是一个比较新的文件压缩算法,目前正变得越来越受欢迎。 此开放源代码算法由来自 Google 和其他组织的软件工程师定期更新,现已被证明比其他现有压缩方法更好用。 这种算法的支持目前还比较少,但作为后起之秀指日可待。

8.使用轻量级框架

除非你只用现有的编码知识构建网站,不然,你可以尝试使用一个好的前端框架来避免许多不必要的前端优化错误。虽然有一些更大,更知名的框架能提供更多功能和选项,但它们不一定适合你的 Web 项目。

所以说,不仅确定项目所需功能很重要,选择合适的框架也很重要——它要在提供所需功能的同时保持轻量。最近许多框架都使用简洁的 HTML,CSS 和 JavaScript 代码。

一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

参考链接:
详细解读https://segmentfault.com/a/1190000006879700
详细解读https://mp.weixin.qq.com/s/jjL4iA7p6aYEAQyWhn4QbQ

输入地址
1.浏览器查找域名的 IP 地址
2.这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存…
3.浏览器向 web 服务器发送一个 HTTP 请求
4.服务器的永久重定向响应(从 http://example.comhttp://www.example.com
5.浏览器跟踪重定向地址
6.服务器处理请求
7.服务器返回一个 HTTP 响应
8.浏览器显示 HTML
9.浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
10.浏览器发送异步请求

URL 到底是啥

URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。
比如 http://www.w3school.com.cn/ht...,遵守以下的语法规则:

scheme://host.domain:port/path/filename
各部分解释如下:
scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。
host - 定义域主机(http 的默认主机是 www)
domain - 定义因特网域名,比如 w3school.com.cn
port - 定义主机上的端口号(http 的默认端口号是 80)
path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
filename - 定义文档/资源的名称

讲tcp/ip网络层、三次握手,为什么不能两次握手

客服端和服务端在进行http请求和返回的工程中,需要创建一个TCP connection(由客户端发起),http不存在连接这个概念,它只有请求和响应。请求和响应都是数据包,它们之间的传输通道就是TCP connection。

位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)

第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;(第一次握手,由浏览器发起,告诉服务器我要发送请求了)

第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功;(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)

谢希仁著《计算机网络》中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

这种情况是:一端(client)A发出去的第一个连接请求报文并没有丢失,而是因为某些未知的原因在某个网络节点上发生滞留,导致延迟到连接释放以后的某个时间才到达另一端(server)B。本来这是一个早已失效的报文段,但是B收到此失效的报文之后,会误认为是A再次发出的一个新的连接请求,于是B端就向A又发出确认报文,表示同意建立连接。如果不采用“三次握手”,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源。如果采用“三次握手”的话就不会出现这种情况,B端收到一个过时失效的报文段之后,向A端发出确认,此时A并没有要求建立连接,所以就不会向B端发送确认,这个时候B端也能够知道连接没有建立。

问题的本质是,信道是不可靠的,但是我们要建立可靠的连接发送可靠的数据,也就是数据传输是需要可靠的。在这个时候三次握手是一个理论上的最小值,并不是说是tcp协议要求的,而是为了满足在不可靠的信道上传输可靠的数据所要求的。

这个网上转载的例子不错:

三次握手:
A:“喂,你听得到吗?”A->SYN_SEND
B:“我听得到呀,你听得到我吗?”应答与请求同时发出 B->SYN_RCVD | A->ESTABLISHED
A:“我能听到你,今天balabala……”B->ESTABLISHED

四次挥手:
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala…..”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSE

iframe有那些缺点?

  1. iframe会阻塞主页面的Onload事件;
  2. 搜索引擎的检索程序无法解读这种页面,不利于SEO;
  3. iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
  4. 使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript动态给iframe添加src属性值,这样可以绕开以上两个问题

websocket握手过程

在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” (handshaking)。

客户端请求web socket连接时,会向服务器端发送握手请求

请求头大致内容:

请求包说明:
  • 必须是有效的http request 格式;
  • HTTP request method 必须是GET,协议应不小于1.1 如: Get / HTTP/1.1;
  • 必须包括Upgrade头域,并且其值为”websocket”;
  • 必须包括”Connection” 头域,并且其值为”Upgrade”;
  • 必须包括”Sec-WebSocket-Key”头域,其值采用base64编码的随机16字节长的字符序列;
  • 如果请求来自浏览器客户端,还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接;
  • 必须包括”Sec-webSocket-Version” 头域,当前值必须是13;
  • 可能包括”Sec-WebSocket-Protocol”,表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应之;
  • 可能包括”Sec-WebSocket-Extensions”, 协议扩展, 某类协议可能支持多个扩展,通过它可以实现协议增强;
  • 可能包括任意其他域,如cookie.

服务端响应如下:

应答包说明:
*必须包括Upgrade头域,并且其值为”websocket”;
*必须包括Connection头域,并且其值为”Upgrade”;
*必须包括Sec-WebSocket-Accept头域,其值是将请求包“Sec-WebSocket-Key”的值,与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接,然后对拼接后的字符串进行sha-1运算,再进行base64编码,就是“Sec-WebSocket-Accept”的值;
*应答包中冒号后面有一个空格;
*最后需要两个空行作为应答包结束

参考链接:
Websocket协议之握手连接

跨域以及解决办法

  • 同源

符合”协议+域名+端口”三者相同,就是同源

  • 同源策略

同源策略,其初衷是为了浏览器的安全性,通过以下三种限制,保证浏览器不易受到XSS、CSFR等攻击。

- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 Js对象无法获得
- AJAX 请求不能发送
  • 跨域解决方案

    • 通过jsonp跨域
    • document.domain + iframe跨域
    • location.hash + iframe
    • window.name + iframe跨域
    • postMessage跨域
    • 跨域资源共享(CORS)
    • nginx代理跨域
    • nodejs中间件代理跨域
    • WebSocket协议跨域

前端持久化的方式、区别

最容易想到的解决方案是:

1.使用前端cookie技术来保存本地化数据,如jquery.cookie.js;
2.使用html5提供的Web Storage技术来提供解决方案;

用cookie存储永久数据存在以下几个问题:
1.大小:cookie的大小被限制在4KB。
2.带宽:cookie是随HTTP事务一起被发送的,因此会浪费一部分发送cookie时使用的带宽。
3.复杂性:要正确的操纵cookie是很困难的。

针对这些问题,在HTML5中,重新提供了一种在客户端本地保存数据的功能,它就是Web Storage。
具体来说,Web Storage又分为两种:
1.sessionStorage:将数据保存在session对象中。所谓session,是指用户在浏览某个网站时,从进入网站到浏览器关闭所经过的这段时间,也就是用户浏览这个网站所花费的时间。session对象可以用来保存在这段时间内所要求保存的任何数据。
2.localStorage:将数据保存在客户端本地的硬件设备(通常指硬盘,也可以是其他硬件设备)中,即使浏览器被关闭了,该数据仍然存在,下次打开浏览器访问网站时仍然可以继续使用。

这两者的区别在于,sessionStorage为临时保存,而localStorage为永久保存。

前端持久化--evercookie

介绍http2.0

  • 所有数据以二进制传输。HTTP1.x是基于文本的,无法保证健壮性,HTTP2.0绝对使用新的二进制格式,方便且健壮
  • 同一个连接里面发送多个请求不再需要按照顺序来
  • 头信息压缩以及推送等提高效率的功能

Http 2.0协议简介
HTTP 2.0 详细介绍,http2.0详细介绍
HTTP/2.0 相比1.0有哪些重大改进

通过什么做到并发请求

我能想到的只有Promise.all(),欢迎补充

b和strong的区别

<b> 粗体文本,<strong> 用于强调文本,他们的样式是一样的
有一种说法,是<strong>貌似在盲人用的机器上会读两遍。因为没有对应的测试条件,所以没做验证。

Access-Control-Allow-Origin在服务端哪里配置

header('Access-Control-Allow-Origin:*');

csrf跨站攻击怎么解决

CSRF,全称为Cross-Site Request Forgery,跨站请求伪造,是一种网络攻击方式,它可以在用户毫不知情的情况下,以用户的名义伪造请求发送给被攻击站点,从而在未授权的情况下进行权限保护内的操作。

具体来讲,可以这样理解CSRF。攻击者借用用户的名义,向某一服务器发送恶意请求,对服务器来讲,这一请求是完全合法的,但攻击者确完成了一个恶意操作,比如以用户的名义发送邮件,盗取账号,购买商品等等

一般网站防御CSRF攻击的方案:
(1)验证token值。
(2)验证HTTP头的Referer。
(3)在HTTP头中自定义属性并验证
(4)服务器端表单hash认证
在所有的表单里面随机生成一个hash,server在表单处理时去验证这个hash值是否正确,这样工作量比较大

CSRF(跨站请求伪造攻击)漏洞详解


CSS

清除浮动的方式

// 第一种
.ovh{
  overflow:hidden;
}
// 第二种
.clear{
  clear:both;
}
// 第三种
.clearfix:after{ 
 content:"";//设置内容为空
 height:0;//高度为0
 line-height:0;//行高为0
 display:block;//将文本转为块级元素
 visibility:hidden;//将元素隐藏
 clear:both//清除浮动
}
.clearfix{
 zoom:1;为了兼容IE
}

免费公开课带你彻底掌握 CSS 浮动

当给父元素设置"overflow:hidden"时,实际上创建了一个超级属性BFC,此超级属性反过来决定了"height:auto"是如何计算的。在“BFC布局规则”中提到:计算BFC的高度时,浮动元素也参与计算。因此,父元素在计算其高度时,加入了浮动元素的高度,“顺便”达成了清除浮动的目标,所以父元素就包裹住了子元素。

垂直居中的几种方式

// 第一种
.center {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
// 第二种
.center {
  width: 100px;
  height: 100px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-top: -50px;
  margin-left: -50px;
}
// 第三种
.center {
  position: absolute;
  margin:auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
// 第四种
.parent {
  display: flex;
  align-items: center;
  justify-content: center;
}
// 第五种
.parent{
  display: flex;
}

.content{
  margin: auto; /*自动相对于父元素水平垂直居中*/
}
// 第六种
.parent {
    display: table;
}

.child {
    display: table-cell;
    vertical-align: middle;
}

BFC是什么

BFC(Block Formatting Context),块级格式化上下文,是Web页面中盒模型布局的CSS渲染模式。它的定位体系属于常规文档流。

原理(渲染规则):

  1. 在BFC元素的垂直方向上的边距,会发生重叠
  2. BFC的区域不会与浮动元素的box重叠
  3. BFC在页面上是一个独立的容器,外面的元素不会影响里面的元素
  4. 计算BFC高度时,浮动元素也会参与计算
<!-- 边距重叠 -->
<section id="margin">
  <style>
    #margin{
        background: green;
        overflow: hidden;
    }
    #margin p{
        background: red;
        margin: 10px 0;
    }
  </style>
  <p>1</p>
  <!-- 在增加一个BFC父级就可以消除边距重叠 -->
  <div style="overflow: hidden;">
    <p>2</p>
  </div>
  <p>3</p>
</section>
<!-- 左右布局 BFC的区域不会与浮动元素的box重叠-->
<section id="layout">
  <style>
    #layout .left{
      float: left;
      width: 200px;
      height: 300px;
      background: pink;
    }
    #layout .right{
      height: 500px;
      background: yellow;
      overflow: hidden;
    }
  </style>
  <div class="left"></div>
  <div class="right"></div>
</section>
<!-- 计算BFC高度时,浮动元素也会参与计算 清除浮动 -->
<section id="float">
  <style>
    #float{
      background: red;
      overflow: auto;
    }
    #float .float-left{
      float: left;
    }
  </style>
  <div class="float-left">我是浮动元素</div>
</section>

怎么创建BFC:

  1. float的值不为none
  2. position的值不为static或者relative
  3. display的值为 table-cell, table-caption, inline-block, flex, 或者 inline-flex中的其中一个
  4. overflow的值不为visible
浮动,绝对定位元素,inline-blocks, table-cells, table-captions,和overflow的值不为visible的元素,(除了这个值已经被传到了视口的时候)将创建一个新的块级格式化上下文。

上面的引述几乎总结了一个BFC是怎样形成的。但是让我们以另一种方式来重新定义以便能更好的去理解.

参考链接:
理解CSS中BFC

讲flex,手写出flex常用的属性,并且讲出作用

这个直接看 阮一峰:Flex 布局教程

介绍css3中position:sticky

单词sticky的中文意思是“粘性的”,position:sticky表现也符合这个粘性的表现。基本上,可以看出是position:relative和position:fixed的结合体——当元素在屏幕内,表现为relative,就要滚出显示器屏幕的时候,表现为fixed。

详细讲解的还是看大神的吧,张鑫旭:position:sticky


JavaScript

js三座大山

原型与原型链,作用域及闭包,异步和单线程。
三座大山,真不是一两句可以说清楚的,只有靠大家多看,多用,多理解,放点链接吧。

原型,原型链,call/apply
JavaScript从初级往高级走系列————prototype
JavaScript从初级往高级走系列————异步
JavaScript的预编译过程
内存空间详解
作用域和闭包
JavaScript深入之词法作用域和动态作用域
JavaScript深入之作用域链
事件循环机制

什么是闭包

参考链接:
什么是闭包?https://mp.weixin.qq.com/s/OthfFRwf-rQmVbMnXAqnCg
作用域与闭包https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/si-3001-zuo-yong-yu-lian-yu-bi-bao.html

简言之,闭包是由函数引用其周边状态(词法环境)绑在一起形成的(封装)组合结构。在 JavaScript 中,闭包在每个函数被创建时形成。

这是基本原理,但为什么我们关心这些?实际上,由于闭包与它的词法环境绑在一起,因此闭包让我们能够从一个函数内部访问其外部函数的作用域。

要使用闭包,只需要简单地将一个函数定义在另一个函数内部,并将它暴露出来。要暴露一个函数,可以将它返回或者传给其他函数。

内部函数将能够访问到外部函数作用域中的变量,即使外部函数已经执行完毕。

在 JavaScript 中,闭包是用来实现数据私有的原生机制。当你使用闭包来实现数据私有时,被封装的变量只能在闭包容器函数作用域中使用。你无法绕过对象被授权的方法在外部访问这些数据。在 JavaScript 中,任何定义在闭包作用域下的公开方法才可以访问这些数据。

宏任务 与 微任务

参考链接:
js引擎执行机制https://segmentfault.com/a/1190000012806637
事件循环机制

  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
    console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

promise里面和then里面执行有什么区别

promise里面的是宏任务,then后面的是微任务。

JS为什么要区分微任务和宏任务

这个问题本质就是为啥需要异步。如果js不是异步的话,由于js代码本身是自上而下执行的,那么如果上一行代码需要执行很久,下面的代码就会被阻塞,对用户来说,就是”卡死”,这样的话,会造成很差的用户体验。

JavaScript 实现异步编程的4种方法

你可能知道,Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

  • 回调函数

假定有两个函数f1和f2,后者等待前者的执行结果。
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数

function f1(callback){
 setTimeout(function () {
 // f1的任务代码
  callback();
 }, 1000);
}

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

  • 事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1(){
 setTimeout(function () {
 // f1的任务代码
  f1.trigger('done');
 }, 1000);
}
  • 发布订阅

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

jQuery.subscribe("done", f2);
function f1(){
 setTimeout(function () {
    // f1的任务代码
    jQuery.publish("done");
 }, 1000);
}
  • Promise
f1().then(f2).then(f3);

new 的过程

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象
function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

原型继承与类继承

JS原型继承和类式继承http://www.cnblogs.com/constantince/p/4754992.html

// 类继承
var father = function() {
  this.age = 52;
  this.say = function() {
    alert('hello i am '+ this.name ' and i am '+this.age + 'years old');
  }
}
 
var child = function() {
  this.name = 'bill';
  father.call(this);
}
 
var man = new child();
man.say();
// 原型继承
var father = function() {
}
father.prototype.a = function() {
}
var child = function(){}
//开始继承
child.prototype = new father();
var man = new child();
man.a();

和原型对比起来,构造函数(类)式继承有什么不一样呢?首先,构造函数继承的方法都会存在父对象之中,每一次实例,都会将funciton保存在内存中,这样的做法毫无以为会带来性能上的问题。其次类式继承是不可变的。在运行时,无法修改或者添加新的方法,这种方式是一种固步自封的死方法。而原型继承是可以通过改变原型链接而对子类进行修改的。另外就是类式继承不支持多重继承,而对于原型继承来说,你只需要写好extend对对象进行扩展即可。

== 和 ===的区别,什么情况下用相等==

==是===类型转换(又称强制),==只需要值相等就会返回true,而===必须值和数据类型都相同才会返回true。

bind、call、apply的区别

1.每个函数都包含两个非继承而来的方法:call()方法和apply()方法。
2.相同点:这两个方法的作用是一样的。
都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
一般来说,this总是指向调用某个方法的对象,但是使用call()和apply()方法时,就会改变this的指向。
3.不同点:接收参数的方式不同。

apply()方法 接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
语法:apply([thisObj [,argArray] ]);,调用一个对象的一个方法,2另一个对象替换当前对象。
说明:如果argArray不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提供argArray和thisObj任何一个参数,那么Global对象将用作thisObj。
call()方法 第一个参数和apply()方法的一样,但是传递给函数的参数必须列举出来。
语法:call([thisObject[,arg1 [,arg2 [,…,argn]]]]);,应用某一对象的一个方法,用另一个对象替换当前对象。
说明: call方法可以用来代替另一个对象调用一个方法,call方法可以将一个函数的对象上下文从初始的上下文改变为thisObj指定的新对象,如果没有提供thisObj参数,那么Global对象被用于thisObj。

bind和call、apply最大的区别就是,call、apply不仅改变this的指向,还会直接支持代码,而bind不会。

var cat = {
  name: '咪咪'
}
function beatTheMonster(){
  console.log(this.name);
}
beatTheMonster.call(cat);

// 1.call 改变了this的指向。改变到了cat上。
// 2.beatTheMonster函数/方法执行了
// 3.bind(),保存了方法,并没有直接调用它

图片预览

<input type="file" name="file" onchange="showPreview(this)" />
<img id="portrait" src="" width="70" height="75">

function showPreview(source) {
  var file = source.files[0];
  if(window.FileReader) {
      var fr = new FileReader();
      fr.onloadend = function(e) {
        document.getElementById("portrait").src = e.target.result;
      };
      fr.readAsDataURL(file);
  }
}

扁平化多维数组

var result = []
function unfold(arr){
     for(var i=0;i< arr.length;i++){
      if(typeof arr[i]=="object" && arr[i].length>1) {
       unfold(arr[i]);
     } else {        
       result.push(arr[i]);
     }
  }
}
var arr = [1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
unfold(arr)
var c=[1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
var b = c.toString().split(',')
var arr=[1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
var result = flatten(arr)

this的指向问题

参考链接:
归纳总结this的指向问题https://finget.github.io/2018/11/28/this/
ECMAScript规范解读thishttps://github.com/mqyqingfeng/Blog/issues/7

function foo() {
    console.log(this.a)
}
var a = 1
foo()

var obj = {
    a: 2,
    foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

箭头函数中的this:

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

async/await

理解 JavaScript 的 async/awaithttps://segmentfault.com/a/1190000007535316

async function async1() {
  console.log( 'async1 start')
  await async2()
  console.log( 'async1 end')
}
async function async2() {
  console.log( 'async2')
}
async1()
console.log( 'script start')

这里注意一点,可能大家都知道await会让出线程,阻塞后面的代码,那么上面例子中, async2script start 谁先打印呢?
是从左向右执行,一旦碰到await直接跳出,阻塞 async2() 的执行?
还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?
实践的结论是,从右向左的。先打印async2,后打印的 script start。
之所以提一嘴,是因为我经常看到这样的说法,「一旦遇到await就立刻让出线程,阻塞后面的代码」。

Promise 和 async/await 和 callback的区别

我的理解:callback是解决异步的早期方案,但是会导致‘回调地狱’,然后就出现了Promise,利用.then优化了回调地狱的问题,而async/await是在promise 进一步封装,利用看似同步的方式解决异步问题。Promise和async/await都是语法糖。就是写起来更简单,阅读性和维护性增强。

Promise 和 async/await在执行时都干了什么,推荐看看:8 张图帮你一步步看清 async/await 和 promise 的执行顺序

手写实现promise

直接粘贴大神的代码:

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}

Promise.all实现原理

MyPromise.all = (arr) => {
  if (!Array.isArray(arr)) {
    throw new TypeError('参数应该是一个数组!');
  };
  return new MyPromise(function(resolve, reject) {
    let i = 0, result = [];
    next();
    function next() {
      //如果不是MyPromise对象,需要转换
      MyPromise.resolve(arr[i]).then(res => {
        result.push(res);
        i++;
        if (i === arr.length) {
            resolve(result);
        } else {
            next();
        };
      }, reject);
    };
  })
};

参考链接:
原生es6封装一个Promise对象

手写函数防抖和函数节流

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

我们先来看一个袖珍版的防抖理解一下防抖的实现:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

节流:

/**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
 *                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 如果设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 获得当前时间戳
      var now = _.now();
      // 首次进入前者肯定为 true
      // 如果需要第一次不执行函数
      // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
       // 如果设置了 trailing,只会进入这个条件
      // 如果没有设置 leading,那么第一次会进入这个条件
      // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
      // 其实还是会进入的,因为定时器的延时
      // 并不是准确的时间,很可能你设置了2秒
      // 但是他需要2.2秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定时器就清理掉否则会调用二次回调
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
        // 没有的话就开启一个定时器
        // 并且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

图片懒加载与预加载

懒加载也就是延迟加载
原理:
页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,只有通过javascript设置了图片路径,浏览器才会发送请求。
懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,把正真的路径存在元素的“data-url”(这个名字起个自己认识好记的就行)属性里,要用的时候就取出来,再设置
// 懒加载
function loadImg(src){
  let promise = new Promise(function (resolve, reject) {
    let img = document.createElement('img')
    img.onload = function () {
      resolve(img)
    }
    img.onerror = function () {
      reject('图片加载失败')
    }
    img.src = src
  })
  return promise
}
预加载 提前加载图片,当用户需要查看时可直接从本地缓存中渲染

实现预加载的三种方法:

  1. 用CSS和JavaScript实现预加载
  2. 仅使用JavaScript实现预加载
  3. 使用Ajax实现预加载
  • 用CSS和JavaScript实现预加载
#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; }  
#preload-02 { background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px; }  
#preload-03 { background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px; }

将这三个ID选择器应用到(X)HTML元素中,我们便可通过CSS的background属性将图片预加载到屏幕外的背景上。只要这些图片的路径保持不变,当它们在Web页面的其他地方被调用时,浏览器就会在渲染过程中使用预加载(缓存)的图片。简单、高效,不需要任何JavaScript。

该方法虽然高效,但仍有改进余地。使用该法加载的图片会同页面的其他内容一起加载,增加了页面的整体加载时间。为了解决这个问题,我们增加了一些JavaScript代码,来推迟预加载的时间,直到页面加载完毕。代码如下:

function preloader() {  
    if (document.getElementById) {  
        document.getElementById("preload-01").style.background = "url(http://domain.tld/image-01.png) no-repeat -9999px -9999px";  
        document.getElementById("preload-02").style.background = "url(http://domain.tld/image-02.png) no-repeat -9999px -9999px";  
        document.getElementById("preload-03").style.background = "url(http://domain.tld/image-03.png) no-repeat -9999px -9999px";  
    }  
}  
function addLoadEvent(func) {  
    var oldonload = window.onload;  
    if (typeof window.onload != 'function') {  
        window.onload = func;  
    } else {  
        window.onload = function() {  
            if (oldonload) {  
                oldonload();  
            }  
            func();  
        }  
    }  
}  
addLoadEvent(preloader);
  • 仅使用JavaScript实现预加载
var images = new Array()  
function preload() {  
  for (i = 0; i < preload.arguments.length; i++) {  
    images[i] = new Image()  
    images[i].src = preload.arguments[i]  
  }  
}  
preload(  
  "http://domain.tld/gallery/image-001.jpg",  
   "http://domain.tld/gallery/image-002.jpg",  
   "http://domain.tld/gallery/image-003.jpg"  
)  
  • 使用Ajax实现预加载
window.onload = function() {  
    setTimeout(function() {  
        // XHR to request a JS and a CSS  
        var xhr = new XMLHttpRequest();  
        xhr.open('GET', 'http://domain.tld/preload.js');  
        xhr.send('');  
        xhr = new XMLHttpRequest();  
        xhr.open('GET', 'http://domain.tld/preload.css');  
        xhr.send('');  
        // preload image  
        new Image().src = "http://domain.tld/preload.png";  
    }, 1000);  
};

上面代码预加载了“preload.js”、“preload.css”和“preload.png”。1000毫秒的超时是为了防止脚本挂起,而导致正常页面出现功能问题。

window.onload = function() {  
    setTimeout(function() {  
        // reference to <head>  
        var head = document.getElementsByTagName('head')[0];  
        // a new CSS  
        var css = document.createElement('link');  
        css.type = "text/css";  
        css.rel  = "stylesheet";  
        css.href = "http://domain.tld/preload.css";  
        // a new JS  
        var js  = document.createElement("script");  
        js.type = "text/javascript";  
        js.src  = "http://domain.tld/preload.js";  
        // preload JS and CSS  
        head.appendChild(css);  
        head.appendChild(js);  
        // preload image  
        new Image().src = "http://domain.tld/preload.png";  
    }, 1000);  
};

这里,我们通过DOM创建三个元素来实现三个文件的预加载。正如上面提到的那样,使用Ajax,加载文件不会应用到加载页面上。从这点上看,Ajax方法优越于JavaScript。

参考链接:
Javascript图片预加载详解

使用es5实现es6的class

借用babel工具可以学习一下,es6的class 编译成es5时,长什么样

// ES6
class Person{
  constructor(name,age){
      this.name = name
   this.age = age
  }
  say() {
    console.log(this.name)
  }
  run() {
   console.log('run fast')
  }
  // 静态方法,类调用
  static getGirl(){
    console.log('girl friend')
  }
}
// ES5
var _createClass = function() {
  function defineProperties(target, props) { 
      for (var i = 0; i < props.length; i++) { 
          var descriptor = props[i];
            // 枚举
          descriptor.enumerable = descriptor.enumerable || false;
          // 可配置
          descriptor.configurable = true; 
      if ("value" in descriptor) 
        // 可写
          descriptor.writable = true;
          
      Object.defineProperty(target, descriptor.key, descriptor); 
    } 
  } 
  return function(Constructor, protoProps, staticProps) { 
      if (protoProps) 
          defineProperties(Constructor.prototype, protoProps); 
      if (staticProps) 
          defineProperties(Constructor, staticProps); 
      return Constructor; 
  }; 
}();

// 禁止 直接调用 Person()
function _classCallCheck(instance, Constructor) { 
    if (!(instance instanceof Constructor)) { 
        throw new TypeError("Cannot call a class as a function");
    }
}


var Person = function () {
  function Person(name, age) {
    _classCallCheck(this, Person);

    this.name = name;
    this.age = age;
  }

  _createClass(Person, [{
    key: 'say',
    value: function say() {
      console.log(this.name);
    }
  }, {
    key: 'run',
    value: function run() {
      console.log('run fast');
    }
  }], [{
    key: 'getGirl',
    value: function getGirl() {
      console.log('girl friend');
    }
  }]);
  return Person;
}();

关于对象的enumerablewritableconfigurable,可以看看Javascript properties are enumerable, writable and configurable

JavaScript的sort方法内部使用的什么排序

默认排序顺序是根据字符串Unicode码点

函数式编程

函数式编程的本质,函数式编程中的函数这个术语不是指计算机中的函数,而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,无论什么时候调用,调用几次,值都是不变的。
函数式的最主要的好处是不可变性带来的。没有可变的状态,函数就是引用透明的没有副作用。函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试更容易。

参考链接:
js函数式编程指南

回调函数的坏处

回调地狱、代码的可阅读性和可维护性降低

如何实现一个可设置过期时间的localStorage

直接上链接:如何给localStorage设置一个过期时间?

用JavaScript的异步实现sleep函数

async function test() {
  console.log('Hello')
  let res = await sleep(1000)
  console.log(res)
}
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}
test()

参考链接:
JavaScript的sleep实现--Javascript异步编程学习

手写实现jsonp

window._pt_lt = new Date().getTime();
window._pt_sp_2 = [];
_pt_sp_2.push('setAccount,2953009d');
var _protocol = (("https:" == document.location.protocol) ? " https://" : " http://");
(function() {
  var atag = document.createElement('script');
  atag.type = 'text/javascript';
  atag.async = true;
  atag.src = _protocol + 'js.ptengine.cn/2953009d.js';
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(atag, s);
})();

浅拷贝和深拷贝的区别

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

参考链接:
js浅拷贝和深拷贝

for..in 和 for..of 的区别

  1. 推荐在循环对象属性的时候,使用for...in,在遍历数组的时候的时候使用for...of。
  2. for…in…遍历对象会遍历出对象的所有可枚举的属性
  3. for...in循环出的是key,for...of循环出的是value
  4. 注意,for...of是ES6新引入的特性。修复了ES5引入的for...in的不足
  5. for...of不能循环普通的对象,需要通过和Object.keys()搭配使用

cookie和localStorage的区别

特性 cookie sessionStorage localStorage
数据生命期 生成时就会被指定一个maxAge值,这就是cookie的生存周期,在这个周期内cookie有效,默认关闭浏览器失效 页面会话期间可用 除非数据被清除,否则一直存在
存放数据大小 4K左右(因为每次http请求都会携带cookie) 一般5M或更大
与服务器通信 由对服务器的请求来传递,每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 数据不是由每个服务器请求传递的,而是只有在请求时使用数据,不参与和服务器的通信
易用性 cookie需要自己封装setCookie,getCookie 可以用源生接口,也可再次封装来对Object和Array有更好的支持
共同点 都是保存在浏览器端,和服务器端的session机制不同

JS执行过程中分为哪些阶段

数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少

时间一样。引用类型的变量都是堆内存。堆内存就像书架一样,只要你知道书名,就能直接找到对应的书。

内存空间

var a = {b: 1} 存放在哪里?
var a = {b: {c: 1}}存放在哪里?
var a = {name: "前端开发"}; var b = a; a = null, 那么b输出什么?

js变量可以用来保存两种类型的值:基本类型值和引用类型值。在ES6之前共有6种数据类型:Undefined、Null、Boolean、Number,String和Object,其中前5种是基本类型值。
  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中。
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本。
  • 引用类型的值是对象,保存在堆内存中。
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针
  • 理解队列数据结构的目的主要是为了清晰的明白事件循环(Event Loop)的机制到底是怎么回事。

jquery $(document).ready() 与window.onload的区别

1.执行时间

window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行。
$(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕。

2.编写个数不同

window.onload不能同时编写多个,如果有多个window.onload方法,只会执行一个
$(document).ready()可以同时编写多个,并且都可以得到执行

3.简化写法

window.onload没有简化写法
$(document).ready(function(){})可以简写成$(function(){});

一个是数组中所有数都出现了两次,只有一个元素只出现了一次,找出这个数

let arr = [1,1,3,4,3,5,6,8,6,5,8]
function get() {
    let num = 0;
    arr.forEach(item => {
        num = num^item // 异或运算
    })
    console.log(num)
}
get()

js异或运算符^小技巧


Vue

Vue 生命周期

1.beforcreate
2.created
3.beformount
4.mounted
5.beforeUpdate
6.updated
7.actived
8.deatived
9.beforeDestroy
10.destroyed

vue里面的虚拟dom是怎么回事,虚拟DOM主要做了什么

JavaScript从初级往高级走系列————Virtual Dom

vue双向绑定讲一讲

Vue3基于Proxy 的新数据监听系统,全语音特性支持 + 更好的性能

Vue2.x用的是基于ES5的getter/setter,也就是Object.defineProperty这个API。

每个vue 组件都会代理它所包含的 data、props、computed,这些代理都是通过Object.defineProperty实现的,大量的Object.defineProperty是很大的性能消耗

利用Proxy减少组件实例初始化开销,暴露给用户的这个this,其实是一个真正的组件实例的一个Proxy

基于Proxy的监听是所谓的Lazy by default,只有当一个数据被用到的时候才会监听

讲vue-lazyloader的原理,手写伪代码

原理简述:

  1. vue-lazyload是通过指令的方式实现的,定义的指令是v-lazy指令
  2. 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件)
  3. 上面的dom事件回调中,会遍历 listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源
  4. 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容

参考链接:
Vue-lazyload原理详解之源码解析

讲vue的响应式原理、依赖收集、监听数组、虚拟dom

Vue.js 技术揭秘

手写vue双向绑定

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>双向绑定</title>
</head>
<body>
  手写一个简单双向绑定<br/>
  <input type="text" id="model"><br/>
  <div id="modelText"></div>
</body>
<script>
  var model = document.querySelector("#model");
  var modelText = document.querySelector("#modelText");
  var defaultName = "defaultName";
  var userInfo = {}
  model.value = defaultName;
  Object.defineProperty(userInfo, "name", {
    get: function () {
      return defaultName;
    },
    set: function (value) {
      defaultName = value;
      model.value = value;
      console.log("-----value");
      console.log(value);
      modelText.textContent = value;
    }
  })

  userInfo.name = "new value";
  var isEnd = true;

  model.addEventListener("keyup", function () {
    if (isEnd) {
      userInfo.name = this.value;
    }
  }, false)
  //加入监听中文输入事件
  model.addEventListener("compositionstart", function () {
    console.log("开始输入中文");
    isEnd = false;
  })
  model.addEventListener("compositionend", function () {
    isEnd = true;
    console.log("结束输入中文");
  })
</script>
</html>

vue-router的原理

参考链接:
前端路由简介以及vue-router实现原理
【源码拾遗】从vue-router看前端路由的两种实现
浅谈vue-router原理

router-link 与 a 标签的区别

<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 <a> 标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

<router-link> 比起写死的 <a href="..."> 会好一些,理由如下:

  • 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
  • 在 HTML5 history 模式下,router-link会守卫点击事件,让浏览器不再重新加载页面。
  • 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to属性都不需要写 (基路径) 了。

手写vue的mixin方法

参考链接:
react-router从Link组件和a标签的区别说起

vue里面哪儿不会用到双向绑定

对于非UI控件来说,不存在双向,只有单向。只有UI控件才有双向的问题。


React

生命周期

  • 实例化

getDefaultProps
getInitialState
componentWillMount
render
componentDidMount

  • 存在期

componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
componentDidUpdate

  • 销毁时

componentWillUnmount

state是怎么注入到组件的,从reducer到组件经历了什么样的过程

调用 setState 之后发生了什么?

在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

React 中 Element 与 Component 的区别是?

简单而言,React Element 是描述屏幕上所见内容的数据结构,是对于 UI 的对象表述。典型的 React Element 就是利用 JSX 构建的声明式代码片然后被转化为createElement的调用组合。而 React Component 则是可以接收参数输入并且返回某个React Element的函数或者类。更多介绍可以参考React Elements vs React Components。

在什么情况下你会优先选择使用 Class Component 而不是 Functional Component?

在组件需要包含内部状态或者使用到生命周期函数的时候使用 Class Component ,否则使用函数式组件。

React 中 refs 的作用是什么?

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。我们可以为元素添加ref属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:

class CustomForm extends Component {
  handleSubmit = () => {
    console.log("Input Value: ", this.input.value)
  }
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type='text'
          ref={(input) => this.input = input} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

上述代码中的input域包含了一个ref属性,该属性声明的回调函数会接收input对应的 DOM 元素,我们将其绑定到this指针以便在其他的类函数中使用。另外值得一提的是,refs 并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:

function CustomForm ({handleSubmit}) {
  let inputElement
  return (
    <form onSubmit={() => handleSubmit(inputElement.value)}>
      <input
        type='text'
        ref={(input) => inputElement = input} />
      <button type='submit'>Submit</button>
    </form>
  )
}

React 中 keys 的作用是什么?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。

render () {
  return (
    <ul>
      {this.state.todoItems.map(({task, uid}) => {
        return <li key={uid}>{task}</li>
      })}
    </ul>
  )
}

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系,因此我们绝不可忽视转换函数中 Key 的重要性。

Controlled Component 与 Uncontrolled Component 之间的区别是什么?

React 的核心组成之一就是能够维持内部状态的自治组件,不过当我们引入原生的HTML表单元素时(input,select,textarea 等),我们是否应该将所有的数据托管到 React 组件中还是将其仍然保留在 DOM 元素中呢?这个问题的答案就是受控组件与非受控组件的定义分割。受控组件(Controlled Component)代指那些交由 React 控制并且所有的表单数据统一存放的组件。譬如下面这段代码中username变量值并没有存放到DOM元素中,而是存放在组件状态数据中。任何时候我们需要改变username变量值时,我们应当调用setState函数进行修改。

class ControlledForm extends Component {
  state = {
    username: ''
  }
  updateUsername = (e) => {
    this.setState({
      username: e.target.value,
    })
  }
  handleSubmit = () => {}
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type='text'
          value={this.state.username}
          onChange={this.updateUsername} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

而非受控组件(Uncontrolled Component)则是由DOM存放表单数据,并非存放在 React 组件中。我们可以使用 refs 来操控DOM元素:

class UnControlledForm extends Component {
  handleSubmit = () => {
    console.log("Input Value: ", this.input.value)
  }
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type='text'
          ref={(input) => this.input = input} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

竟然非受控组件看上去更好实现,我们可以直接从 DOM 中抓取数据,而不需要添加额外的代码。不过实际开发中我们并不提倡使用非受控组件,因为实际情况下我们需要更多的考虑表单验证、选择性的开启或者关闭按钮点击、强制输入格式等功能支持,而此时我们将数据托管到 React 中有助于我们更好地以声明式的方式完成这些功能。引入 React 或者其他 MVVM 框架最初的原因就是为了将我们从繁重的直接操作 DOM 中解放出来。

在生命周期中的哪一步你应该发起 AJAX 请求?

我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下:

  • React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。
  • 如果我们将 AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。

shouldComponentUpdate 的作用是啥以及为何它这么重要?

shouldComponentUpdate允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新。

如何告诉 React 它应该编译生产环境版本?

通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。编译版本中 React 会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小,React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息。

为什么我们需要使用 React 提供的 Children API 而不是 JavaScript 的 map?

props.children并不一定是数组类型,譬如下面这个元素:

<Parent>
  <h1>Welcome.</h1>
</Parent>

概述下 React 中的事件处理逻辑

为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。

createElement 与 cloneElement 的区别是什么?

createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props。

传入 setState 函数的第二个参数的作用是什么?

该函数会在setState函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成:

this.setState(
  { username: 'tylermcginnis33' },
  () => console.log('setState has finished and the component has re-rendered.')
)

setState为什么默认是异步,什么时候是同步的

下述代码有错吗?

this.setState((prevState, props) => {
  return {
    streak: prevState.streak + props.count
  }
})

这段代码没啥问题,不过只是不太常用罢了,详细可以参考React中setState同步更新策略

React组件中怎么做事件代理

  1. 区别于浏览器事件处理方式,React并未将事件处理函数与对应的DOM节点直接关联,而是在顶层使用了一个全局事件监听器监听所有的事件;
  2. React会在内部维护一个映射表记录事件与组件事件处理函数的对应关系;
  3. 当某个事件触发时,React根据这个内部映射表将事件分派给指定的事件处理函数;
  4. 当映射表中没有事件处理函数时,React不做任何操作;
  5. 当一个组件安装或者卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。

参考链接:

深入浅出React(五)(React组件事件详解)


Nodejs

express框架的设计思想

参考链接:
Express框架详解
深入理解express框架
express框架的简单实现

浏览器的事件循环和nodejs事件循环的区别

阮一峰:JavaScript 运行机制详解:再谈Event Loop

其他

怎么配webpack

深入浅出 Webpack

npm2和npm3+有什么区别

npm3 与 npm2 相比有什么改进?

打包时Hash码是怎么生成的

Webpack中hash与chunkhash的区别

推荐链接

  1. 前端工程师,揭开HTTP的神秘面纱
  2. 前端工程师,必备知识
  3. 深入浅出 Webpack
  4. js函数式编程指南
  5. Vue.js技术揭秘
  6. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
  7. 前端基础进阶
  8. 八段代码彻底掌握Promise
  9. 通俗大白话来理解TCP协议的三次握手和四次分手
  10. js深入底层系列

FinGet
1.1k 声望110 粉丝

前端 —— 我一直在路上!