共计 35217 个字符,预计需要花费 89 分钟才能阅读完成。
网上找到的各种面试题整理,长期更新。大部分答案整理来自网络,有问题的地方,希望大家能指出,及时修改; 技术更新迭代,也会及时更新
博客原地址: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
压缩技术可以从文件中去掉多余的字符。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。
使用预先获取
预先获取可以在真正需要之前通过取得必需的资源和相关数据来改善访问用户的浏览体验,主要有 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.com 到 http://www.example.com)5. 浏览器跟踪重定向地址 6. 服务器处理请求 7. 服务器返回一个 HTTP 响应 8. 浏览器显示 HTML9. 浏览器发送请求获取嵌入在 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.cnport – 定义主机上的端口号(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_SENDB:“我听得到呀,你听得到我吗?”应答与请求同时发出 B->SYN_RCVD | A->ESTABLISHEDA:“我能听到你,今天 balabala……”B->ESTABLISHED
四次挥手:A:“喂,我不说了。”A->FIN_WAIT1B:“我知道了。等下,上一句还没说完。Balabala…..”B->CLOSE_WAIT | A->FIN_WAIT2B:”好了,说完了,我也不说了。”B->LAST_ACKA:”我知道了。”A->TIME_WAIT | B->CLOSEDA 等待 2MSL, 保证 B 收到了消息, 否则重说一次”我知道了”,A->CLOSE
iframe 有那些缺点?
iframe 会阻塞主页面的 Onload 事件;
搜索引擎的检索程序无法解读这种页面,不利于 SEO;
iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
使用 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 的高度时,浮动元素也参与计算。因此,父元素在计算其高度时,加入了浮动元素的高度,“顺便”达成了清除浮动的目标,所以父元素就包裹住了子元素。
BFC 是什么
BFC(Block Formatting Context), 块级格式化上下文, 是 Web 页面中盒模型布局的 CSS 渲染模式。它的定位体系属于常规文档流。
浮动,绝对定位元素,inline-blocks, table-cells, table-captions, 和 overflow 的值不为 visible 的元素,(除了这个值已经被传到了视口的时候)将创建一个新的块级格式化上下文。
上面的引述几乎总结了一个 BFC 是怎样形成的。但是让我们以另一种方式来重新定义以便能更好的去理解。一个 BFC 是一个 HTML 盒子并且至少满足下列条件中的任何一个:
float 的值不为 none
position 的值不为 static 或者 relative
display 的值为 table-cell, table-caption, inline-block, flex, 或者 inline-flex 中的其中一个
overflow 的值不为 visible
参考链接:理解 CSS 中 BFC
讲 flex,手写出 flex 常用的属性,并且讲出作用
这个直接看 阮一峰:Flex 布局教程
介绍 css3 中 position:sticky
单词 sticky 的中文意思是“粘性的”,position:sticky 表现也符合这个粘性的表现。基本上,可以看出是 position:relative 和 position:fixed 的结合体——当元素在屏幕内,表现为 relative,就要滚出显示器屏幕的时候,表现为 fixed。
详细讲解的还是看大神的吧,张鑫旭:position:sticky
JavaScript
js 三座大山
原型与原型链,作用域及闭包,异步和单线程。三座大山,真不是一两句可以说清楚的,只有靠大家多看,多用,多理解,放点链接吧。
原型,原型链,call/applyJavaScript 从初级往高级走系列————prototypeJavaScript 从初级往高级走系列————异步 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 的过程
新生成了一个对象
链接到原型
绑定 this
返回新对象
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 会让出线程,阻塞后面的代码,那么上面例子中,async2 和 script 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 实现原理
Promise.all = arr => {
let aResult = []; // 用于存放每次执行后返回结果
return new _Promise(function (resolve, reject) {
let i = 0;
next(); // 开始逐次执行数组中的函数
function next() {
arr[i].then(function (res) {
aResult.push(res); // 执行后返回的结果放入数组中
i++;
if (i == arr.length) {// 如果函数数组中的函数都执行完,便把结果数组传给 then
resolve(aResult);
} else {
next();
}
})
}
})
};
手写函数防抖和函数节流
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。
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
}
预加载 提前加载图片,当用户需要查看时可直接从本地缓存中渲染
实现预加载的三种方法:
用 CSS 和 JavaScript 实现预加载
仅使用 JavaScript 实现预加载
使用 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;
}();
关于对象的 enumerable、writable、configurable, 可以看看 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 的区别
推荐在循环对象属性的时候,使用 for…in, 在遍历数组的时候的时候使用 for…of。
for…in…遍历对象会遍历出对象的所有可枚举的属性
for…in 循环出的是 key,for…of 循环出的是 value
注意,for…of 是 ES6 新引入的特性。修复了 ES5 引入的 for…in 的不足
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)的机制到底是怎么回事。
Vue
Vue 生命周期
1.beforcreate2.created3.beformount4.mounted5.beforeUpdate6.updated7.actived8.deatived9.beforeDestroy10.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 的原理,手写伪代码
原理简述:
vue-lazyload 是通过指令的方式实现的,定义的指令是 v -lazy 指令
指令被 bind 时会创建一个 listener,并将其添加到 listener queue 里面,并且搜索 target dom 节点,为其注册 dom 事件(如 scroll 事件)
上面的 dom 事件回调中,会遍历 listener queue 里的 listener,判断此 listener 绑定的 dom 是否处于页面中 perload 的位置,如果处于则加载异步加载当前图片的资源
同时 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
生命周期
实例化
getDefaultPropsgetInitialStatecomponentWillMountrendercomponentDidMount
存在期
componentWillReceivePropsshouldComponentUpdatecomponentWillUpdatecomponentDidUpdate
销毁时
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 组件中怎么做事件代理
区别于浏览器事件处理方式,React 并未将事件处理函数与对应的 DOM 节点直接关联,而是在顶层使用了一个全局事件监听器监听所有的事件;
React 会在内部维护一个映射表记录事件与组件事件处理函数的对应关系;
当某个事件触发时,React 根据这个内部映射表将事件分派给指定的事件处理函数;
当映射表中没有事件处理函数时,React 不做任何操作;
当一个组件安装或者卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。
参考链接:
深入浅出 React(五)(React 组件事件详解)
Nodejs
express 框架的设计思想
参考链接:Express 框架详解深入理解 express 框架 express 框架的简单实现
浏览器的事件循环和 nodejs 事件循环的区别
阮一峰:JavaScript 运行机制详解:再谈 Event Loop
其他
怎么配 webpack
深入浅出 Webpack
npm2 和 npm3+ 有什么区别
npm3 与 npm2 相比有什么改进?
打包时 Hash 码是怎么生成的
Webpack 中 hash 与 chunkhash 的区别
推荐链接
前端工程师,揭开 HTTP 的神秘面纱
前端工程师,必备知识
深入浅出 Webpack
js 函数式编程指南
Vue.js 技术揭秘
从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理
前端基础进阶
八段代码彻底掌握 Promise
通俗大白话来理解 TCP 协议的三次握手和四次分手
js 深入底层系列
最后
创建了一个前端学习交流群,感兴趣的朋友,一起来嗨呀!