秋招流年不利,于是发奋刷算法题,春招时来运转,拿到了字节跳动的前端Offer。准备面试阶段,将去年所有前端面经的题目所涉及到的知识点作了一遍梳理。实践证明,面试问题确实没有超出相关知识点范围,权作参考吧!
计算机基础
进程与线程
进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元。
组成部分: 文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须拥有一个父进程,线程不拥有系统资源。只有运行必须的一些数据结构,他与父进行的其他线程共享该进程的其他全部资源,线程可以创建和撤销线程
线程池
开始之前,需要明确几个概念,方便后面理解线程池的运行原理。
核心线程(corePool):线程池最终执行任务的角色肯定还是线程,同时我们也会限制线程的数量,所以我们可以这样理解核心线程,有新任务提交时,首先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
等待队列 (workQueue):等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,也会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可以由开发者根据业务来定制。
非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
线程活动保持时间 (keepAliveTime):线程空闲下来之后,保持存活的持续时间,超过这个时间还没有任务执行,该工作线程结束。
饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
那么,线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
TCP / UDP
| OSI七层模型 | TCP/IP概念层模型 | 功能 | TCP/IP协议族 |
|---|---|---|---|
| 应用层 | 应用层 | 文件传输、电子邮件、文件服务等等 | TFTP、HTTP、SMTP |
| 表示层 | 应用层 | 数据格式化、数据加密 | |
| 会话层 | 应用层 | 建立或接触与别的点的联系 | |
| 传输层 | 传输层 | 提供端对端的借口 | TCP、UDP |
| 网络层 | 网络层 | 为数据包选择路由 | IP |
| 数据链路层 | 链路层 | 传输有地址的帧以及错误检测功能 | ARP |
| 物理层 | 链路层 | 以二进制形式在物理媒体上传输数据 | IEEE802 |
UDP
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
面向无连接
具体来说就是:
- 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识,然后就传递给网络层了
- 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
面向报文
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文。
不可靠
有单播,多播,广播的功能
TCP
TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。
可靠传输
交互数据流传输
- 停止等待协议
- nagle 算法
成块数据流传输
滑动窗口协议
滑动窗户协议是指数据发送方有一个发送窗口,发送窗口范围内的数据允许发送,随着接收方传来的确认信息以及通知的接收窗口的大小来动态调整发送窗口的起始位置以及大小。
如图所示:序号1-4的数据是已经发送过并且被确认的数据,TCP可以将这些数据清出缓存;序号5-11是在窗口范围内的数据,允许发送;序号12-16的数据在缓存中不允许发送。
窗口后沿是由接收方发送的确认序号决定的,窗口前沿是由确认序号与发送窗口大小共同决定的。而发送窗口大小并不一定等于接收方提供的接收窗口的大小,发送方会根据网络拥塞情况来动态调整发送窗口大小,前提是发送方发送窗口大小一定不大于接收方接收窗口大小。
流量控制
拥塞控制
拥塞控制是指防止过多的数据注入网络中,这样可以使网络中路由器或者链路不致过载。现在通信线路的传输质量一般都很好,因传输出现差错丢弃分组的概率很小。因此,判断网络拥塞的依据就是出现了超时。
TCP进行拥塞控制常用的算法有四种:慢启动、拥塞避免、快重传、快恢复。
慢启动
拥塞避免
快重传
快恢复
HTTP 状态码
区分状态码
1××开头 - 信息提示
2××开头 - 请求成功
3××开头 - 请求被重定向
4××开头 - 请求错误
5××开头 - 服务器错误
常见状态码
100 - Continue
101 - Switching protocol
102 - Processing
103 - Early Hints
200 - 请求成功,Ajax 接受到信息了
300 - 重定向
301 - Moved Permanently
304 - Not Modifoed
307 - Temporary Redirect
308 - Permanent Redirect
400 - 服务器不理解请求
401 - 验证有误
403 - 服务器拒绝请求
404 - 请求页面错误
405 - Method Not Allowed
408 - Request Timeout
500 - 服务器内部错误,无法完成请求
501 - 此请求方法不被服务器支持且无法被处理
502 - Bad Gateway
503 - Service Unavaliable
504 - Gateway Timeout
HTTP 跨域
浏览器的同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
举个例子,我们的网站可以从不同的源加载脚本文件、样式文件、图片、字体等等,但假若在我们自身的脚本文件中,我们仅允许向同一源发起资源请求,向不同的源请求或发送资源时,会形成跨域限制。
如何解决跨院带来的困扰
CORS
答案有很多,一个比较好的方案是跨域资源共享(CORS),我们可以通过设置相关HTTP请求头的参数来完成跨域请求。对于简单请求,包括GET、HEAD和POST,可以通过在服务端设置Access-Control-Allow-Origin对源进行处理;对于预检请求,则有必要设置更多请求头,并在服务端对Access-Control-Allow-Headers 进行响应的设置;对于附带身份的请求,则需在服务端对Access-Control-Allow-Credentials进行响应的设置。
JSONP
由于AJAX 无法跨域,而<script>标签的src属性是可以跨域的,也就是说,令跨域服务器调用本地数据,回调数据即可。
1 | <script type="text/javascript"> |
1 | flightHandler({ |
1 | // 数据 |
总结
jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本。
HTTPS(HTTP over TLS)
1 | Client->Server: 消息(TLS版本)+ 加密算法 + 压缩算法 |
我们以 Github 网站使用的 TLS 为例,使用浏览器可以看到它使用的加密为 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。其中密钥交互算法是 ECDHE_RSA,对称加密算法是 AES_128_GCM,消息认证(MAC)算法为 SHA256。
浏览器页面加载机制
STEP 1:浏览器客户端借助URL向目标服务器发起HTTP请求:
- 借助DNS服务器解析得到URL所指向的目标服务器的IP地址
- 通过TCP/IP协议,客户端与服务端进行三次握手,建立连接。
- 客户端向服务端发送HTTP请求
STEP 2:服务器收到HTTP请求,并向客户端发送响应
STEP 3:客户端收到响应,开始下载HTML代码(开源服务器以8k / chunk的速率)
- 解析HTML代码,生成DOM树
- 遇到
<style>或<script>标签,下载响应的CSS文件和JS文件,而后继续构建DOM树 - 解析CSS代码,生成CSS规则树,并与DOM树结合,构建渲染树
- 解析JS代码,借助DOM API修改DOM树,借助CSSOM修改渲染树
- 将渲染树节点由上至下、由左至右压入文档流,并对页面进行布局
- 最后绘制页面
DNS
DNS (Domain Name System), 也叫网域名称系统,是互联网的一项服务。它实质上是一个 域名 和 IP 相互映射的分布式数据库,有了它,我们就可以通过域名更方便的访问互联网。
特点
- 分布式
- 协议支持TCP 和 UDP, 常用端口是53
- 每一级域名的长度限制是63
- 域名总长度限制是253
解析流程
- 客户端向本地DNS服务器(递归解析服务器) 发出解析tool.chinaz.com域名的请求
- 本地dns服务器查看缓存,是否有缓存过tool.chinaz.com域名,如果有直接返回给客户端;如果没有执行下一步
- 本地dns服务器向根域名服务器发送请求,查询com顶级域的nameserver 地址
- 拿到com域名的IP后,再向com nameserver发送请求,获取chinaz域名的nameserver地址
- 继续请求chinaz的nameserver, 获取tool域名的地址,最终得到了tool.chinaz.com的IP,本地dns服务器把这个结果缓存起来,以供下次查询快速返回
- 本地dns服务器把把结果返回给客户端
资源加载
- 首先会将所有需要加载的资源进行分类。
- 然后根据浏览器相关的安全策略,来决定资源的加载权限。
- 接着对各个资源的加载优先级进行计算和排序。
- 最后一步,根据加载优先级顺序来加载资源。
回流(Reflow) & 重绘(Repaint)
当
Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
| 页面首次渲染 | |
|---|---|
| 浏览器窗口大小发生改变 | clientWidth、clientHeight、clientTop、clientLeft |
| 元素尺寸或位置发生改变 | offsetWidth、offsetHeight、offsetTop、offsetLeft |
| 元素尺寸或位置发生改变 | scrollWidth、scrollHeight、scrollTop、scrollLeft |
| 元素尺寸或位置发生改变 | scrollIntoView()、scrollIntoViewIfNeeded() |
| 元素尺寸或位置发生改变 | scrollTo() |
| 元素内容变化 | |
| 元素字体大小变化 | |
添加或者删除可见的DOM元素 |
|
激活CSS伪类 |
|
| 查询某些属性或调用某些方法 | getComputedStyle()、getBoundingClientRect() |
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:
color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
延伸
针对页面性能优化,为什么transform优于top ?
前文提到,元素位置发生改变,会引发回流,而transform通过创建一个RenderLayers合成层,拥有独立的GraphicsLayers,并不会引起整个页面回流。
性能优化
CSS
- 避免使用
table布局。 - 尽可能在
DOM树的最末端改变class。 - 避免设置多层内联样式。
- 将动画效果应用到
position属性为absolute或fixed的元素上。 - 避免使用
CSS表达式(例如:calc())。
JavaScript
避免频繁操作样式,最好一次性重写
style属性,或者将样式列表定义为class并一次性更改class属性。避免频繁操作
DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。也可以先为元素设置
display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
浏览器的缓存机制
浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的。
缓存过程分析
浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
强制缓存
强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程
强制缓存的情况主要有三种:
- 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求
- 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存
- 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
问题的关键在于:强制缓存的缓存规则是什么?
当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。
Expires
Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。
到了HTTP/1.1,Expire已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义,那么Cache-Control又是如何控制的呢?
Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:
public:所有内容都将被缓存(客户端和代理服务器都可缓存)private:所有内容只有客户端可以缓存,Cache-Control的默认取值no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效no-transform: 告知服务端希望获取的实体数据没有被转换only-if-cached:告知服务端获取缓存的资源,不向原服务器请求
在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
协商缓存主要有两种情况:
- 协商缓存生效,返回304
- 协商缓存失效,返回200和请求结果结果
拓展
与缓存相关的HTTP首部字段大致如下
通用首部字段
| 名称 | 说明 |
| ——————- | —————————————————————— |
| Cache-Control | 控制缓存行为 |
| Pragma | HTTP 1.0遗留字段,取值为no-cache时禁用缓存 |Pragma的优先级高于Cache-Control。请求首部字段
| 名称 | 说明 |
| —————————- | ——————————————— |
| If-Match | 比较E-tag是否一致 |
| If-None-Match | 比较E-tag是否不一致 |
| If-Modified-Since | 比较资源最后更新时间是否一致 |
| If-Unmodified-Since | 比较资源最后更新时间是否不一致 |响应首部字段
| 名称 | 说明 |
| ——- | ——————— |
| E-tag | 资源的匹配信息 |实体首部字段
| 名称 | 说明 |
| ——————- | ————————————————— |
| Expires | HTTP 1.0遗留字段,实体主体过期时间 |
| Last-Modified | 资源最后一次修改时间 |相对于
pragma字段禁用缓存,Expires字段用于启用和定义缓存,即规定资源过期时间,该时间是相对于服务器时间而言的,因此,客户端与服务端时间不一致,会出现问题(为了解决这一问题,HTTP1.1提出了字段Cache-Control)。此外,pragma的优先级高于Expires。
本地缓存
cookie
cookie存在于浏览器端,是客户端保存用户信息的一种机制,用来记录用户的一些信息。是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头加上一行特殊的指示(set-cookie)以提示浏览器按照指示生成对应的cookie。主要用于以下三个方面。
- 1、会话状态管理(保存用户登录状态等需要记录的信息)
- 2、个性化设置(用户自定义主题、设置等)
- 3、浏览器行为跟踪(跟踪分析用户行为)
cookie的内容包括:名字、值、过期时间、路径和域。路径和域一起构成cookie的作用范围。如果不设置过期时间,则表示该cookie的生命周期为浏览器会话期间,关闭浏览器窗口,cookie就消失,一般保存在内存中(会话cookie),如果设置了过期时间,该cookie则会被保存到硬盘里(硬盘cookie)。
sessionStorage
客户端的存储,基于html5,本地存储。可以将数据在当前会话中保存下来。刷新页面数据依然存在,但是关闭页面后,sessionStorage中的数据就会被清空。
localStorage
是HTML5标准中新添加的技术。本地存储,存储在本地硬盘。除非手动清除,否则永久保存。
HTTP/2
简单来说,HTTP/2,是HTTP协议的第二个主要版本。HTTP/2是HTTP协议自1999年HTTP1.1发布后的首个更新,主要基于google的SPDY协议。 HTTP/2的特点/目的是:在不改动HTTP语义、方法、状态码、URI及首部字段的情况下,大幅度提高web性能。
HTTP/1 存在的性能问题
TCP 连接数限制(6~8)
每一个TCP连接创建都需要经历DNS解析、三次握手、慢启动等过程,对于客户端而言,占用了额外的CPU和内存;对于服务器而言,过多连接容易造成网络拥堵
线头阻塞问题(Head of Line-blocking)
- 每一个TCP连接一次只能处理一个请求-响应,浏览器按照先进先出原则,如果上一个请求迟迟没有响应,后续的请求-响应都会收到影响。
- 为了解决上述问题,出现了管线化(HTTP Pipelining)技术。HTTP Pipelining(管线化)是将多个 HTTP 请求整批提交的技术,在传送过程中不需等待服务端的回应。但如果第一个响应慢还是会阻塞后续响应,服务器为了按序返回相应需要缓存多个响应占用更多资源等等问题亦没有从根本上解决线头阻塞问题。
Header 内容多,而且每次请求 Header 不会变化太多,没有相应的压缩传输优化方案
文件合并等优化工作带来单个请求的延迟
明文传输不安全
总而言之,性能不高和安全不足,是HTTP/1的主要缺陷。
HTTP/2 的改进
二进制传输
HTTP/2 采用二进制格式传输数据,而非HTTP/1.x 里纯文本形式的报文。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。它把TCP协议的部分特性挪到了应用层,把原来的”Header+Body”的消息”打散”为数个小片的二进制”帧”(Frame),用”HEADERS”帧存放头数据、”DATA”帧存放实体数据。HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。
Header 压缩
- 客户端和服务端建立连接后,双方共同建立和维护一个首部表,用于存储和更新头部字段
- 第一次请求-响应发送完整的头部字段,存储到首部表,后续连接只需要发送差异字段,从而达到减少冗余数据,节省开销的目的
- 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值
多路复用
- 同一域名开启一个TCP连接(不再受限于TCP连接数量),每个HTTP请求以流的方式传输(解决线头阻塞)
- 二进制传输方式令客户端与服务端之间的数据以帧的形式乱序传输,借助帧上的标识符将同一流的数据解析成可用数据
- 客户端通过对象的依赖关系告诉服务器哪些资源应该优先传输,服务端根据依赖关系和权重进行带宽分配,优化传输
服务端推送
HTTP2还在一定程度上改变了传统的“请求-应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为”服务器推送”。
另外需要补充的是,服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。
提高安全性
HTTPS加密
Content-Type
Content-Type实体头部用于指示资源的MIME类型 media type 。
案例#1 - Form
在通过HTML form提交生成的POST请求中,请求头的Content-Type由``元素上的enctype属性指定,即multipart/form-data
案例#2 - JSON
application/json
JavaScript
JS的几条基本规范
1 | 1、不要在同一行声明多个变量 |
JS 引用方法
行内引入
1 | <body> |
内部引入
1 | <script> |
外部引入
1 | <body> |
注意
1 | 1,不推荐写行内或者HTML中插入<script>,因为浏览器解析顺序缘故,如果解析到死循环之类的JS代码,会卡住页面 |
JS 的基本数据类型
Undefined、Null、Boolean、Number、String、新增:Symbol
类型判断
typeof
效果有限
最佳方式:Object.prototype.toString.call(obj).slice(8, -1)
1 | function is(type, obj) { |
那么,typeof·有用的地方是什么?
1 | typeof foo !== 'undefined' |
上面代码会检测 foo 是否已经定义;如果没有定义而直接使用会导致 ReferenceError 的异常。 这是 typeof 唯一有用的地方。
JS有哪些内置对象
1 | Object是JavaScript中所有对象的父对象 |
Get请求传参长度的误区
对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。为了明确这个概念,我们必须再次强调下面几点:
- HTTP 协议 未规定 GET 和POST的长度限制
- GET的最大长度显示是因为 浏览器和 web服务器限制了 URI的长度
- 不同的浏览器和WEB服务器,限制的最大长度不一样
- 要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte
get和post请求在缓存方面的区别
- get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
- post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存。
原型和原型链
每个对象都会在其内部初始化一个属性,就是prototype(原型),当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去。
其中,对象的原型和原型链是相同的,即
1 | instance.constructor.prototype === instance.__proto__ |
JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的话,就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象。
作用域
所有的编程语言都可以存储,访问,修改变量。但是这些变量如何存储,程序如何找到并且能够使用它们?这些问题需要设计一套规则,这套规则就被称为我们所熟知的
作用域。简单来说,
作用域指的是变量的生命周期的所在范围。
JavaScript 作用域
- 全局作用域
- 词法作用域
- 函数作用域
- 块级作用域
- 动态作用域
全局作用域
全局变量的生命周期在整个程序之内,能被任意函数或方法访问,并默认可修改;整个程序就是全局变量的作用域。在浏览器中,全局作用域为window
词法作用域
词法作用域是指一个变量的可见性,及其文本表述的模拟值
当我们使用声明的变量时,总是从最近的一个域开始,向外域查找。
1 | var testValue = 'outer'; |
函数作用域
每声明一个函数都会为其自身创建一个作用域,外部函数或方法无法访问或修改函数内部变量。
如何访问函数内部变量?
return- 闭包
- 立即执行函数
块级作用域
ES6之前,JavaScript 语言中并不存在块级作用域。在C++等语言中,花括号{}能够创建一个块级作用域,在JavaScript中,通常情况下,即使用var声明变量,并不能在循环语句或条件语句中创建块级作用域;而使用let和const即可创建一个块级作用域。
动态作用域
动态作用域,作用域是基于调用栈的,而不是代码中的作用域嵌套;
作用域嵌套,有词法作用域一样的特性,查找变量时,总是寻找最近的作用域;
JavaScript 中仅存的动态作用域,在于this 的引用
举例
如何处理for循环语句中的setTimeout函数?
思路:创建作用域
- 使用
let或const创建块级作用域 - 使用外部函数或者立即执行函数创建函数作用域
继承
原型链继承
1 | function Foo() { |
构造函数继承
1 | function Product(name, price) { |
缺陷:构造函数式继承并没有继承父类原型上的方法。
组合式继承
1 | function Product(name, price) { |
extends
1 | class ColorPoint extends Point { |
闭包
闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。
1 | function Counter(start) { |
闭包的特征
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收制回收
闭包的好处
能够实现封装和缓存等
闭包的坏处
就是消耗内存、不正当使用会造成内存溢出的问题
作用域链
一般情况下,变量取值到 创建 这个变量 的函数的作用域中取值。
但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链
组件化和模块化
组件化
为什么要组件化开发
有时候页面代码量太大,逻辑太多或者同一个功能组件在许多页面均有使用,维护起来相当复杂,这个时候,就需要组件化开发来进行功能拆分、组件封装,已达到组件通用性,增强代码可读性,维护成本也能大大降低
组件化开发的优点
很大程度上降低系统各个功能的耦合性,并且提高了功能内部的聚合性。这对前端工程化及降低代码的维护来说,是有很大的好处的,耦合性的降低,提高了系统的伸展性,降低了开发的复杂度,提升开发效率,降低开发成本
组件化开发的原则
- 专一
- 可配置性
- 标准性
- 复用性
- 可维护性
模块化
为什么要模块化
早期的javascript版本没有块级作用域、没有类、没有包、也没有模块,这样会带来一些问题,如复用、依赖、冲突、代码组织混乱等,随着前端的膨胀,模块化显得非常迫切
模块化的好处
- 避免变量污染,命名冲突
- 提高代码复用率
- 提高了可维护性
- 方便依赖关系管理
模块化的几种方法
函数封装
1
2
3
4
5
6
7
8
9
10
11
12
13var myModule = {
var1: 1,
var2: 2,
fn1: function(){
},
fn2: function(){
}
}立即执行函数表达式(IIFE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var myModule = (function(){
var var1 = 1;
var var2 = 2;
function fn1(){
}
function fn2(){
}
return {
fn1: fn1,
fn2: fn2
};
})();
CommonJS和ES6模块循环加载处理的区别
CommonJS模块规范使用require语句导入模块,module.exports导出模块,输出的是值的拷贝,模块导入的也是输出值的拷贝,也就是说,一旦输出这个值,这个值在模块内部的变化是监听不到的。
ES6模块的规范是使用import语句导入模块,export语句导出模块,输出的是对值的引用。ES6模块的运行机制和CommonJS不一样,遇到模块加载命令import时不去执行这个模块,只会生成一个动态的只读引用,等真的需要用到这个值时,再到模块中取值,也就是说原始值变了,那输入值也会发生变化。
图片的预加载和懒加载
- 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染
- 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。预加载则会增加服务器前端压力,懒加载对服务器有一定的缓解压力作用。
mouseover和mouseenter的区别
mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
mouseenter:当鼠标移入元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
堆和栈
堆和栈都是内存中划分出来用来存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
由此,深拷贝和浅拷贝的主要区别在于其在内存中的存储类型不同。
对于JavaScript语言,其基本类型的数据存储在栈内存中,不可改变,而引用类型的数据存储在堆内存中,可以改变。前者的赋值为传值,即开辟新的内存,并存储数值,后者为传址,即将指针指向堆内存的地址。
浅拷贝和深拷贝
由针对不同类型数据赋值的表现(传值和传址),可知,与赋值相比,不管是浅拷贝还是深拷贝,都是重新创建了对象,而就后二者比较,浅拷贝仅复制对象一层属性,并不包含其子对象,而深拷贝则是完全复制。
1 | /** |
1 | /** |
事件机制
DOM事件流(event flow )存在三个阶段(事件传播):
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
事件捕获
当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
事件冒泡
与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
DOM 标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发DOM事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
因此,我们需要了解事件绑定和事件监听,对于前者,可以使用 onclick、on change等属性进行事件绑定,后者即addEventListener:
1 | /** |
参数useCapture的取值决定了事件监听器以何种方式(事件捕捉 vs. 事件冒泡)触发,那么如果同时为父元素和子元素注册了事件监听器,在某些时候,只希望触发子元素绑定的事件监听器,则可以使用event.stopPropagation()停止事件传播。
事件委托
事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素;
举个例子:
1 | <ul id="list"> |
1 | document.getElementById('list').addEventListener('click', function (e) { |
事件循环
浏览器下的事件循环
事件循环中的事件与上文中的事件机制所涉及的事件是有区别的。首先,我们要了解,JavaScript是如何执行函数的:
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。
那么,假如JavaScript需要处理一个异步任务,其并不会等待异步任务返回结果,而是先将事件挂起,继续处理执行栈中的其他任务。当异步任务返回结果之后,JavaScript亦不会立刻处理其回调,而是将其加入事件队列,等待执行栈中的所有任务完成,再去处理事件队列中的任务回调,即将其加入执行栈。由此循环往复,构成事件循环。
宏任务 & 微任务
以下属于宏任务:
setIntervalsetTimeout
以下属于微任务
new Promise()new MutationObserver()
当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
Node 环境下的事件循环
外部输入数据—>轮询阶段(poll)—>检查阶段(check)—>关闭事件回调阶段(close callback)—>定时器检测阶段(timer)—>I/O事件回调阶段(I/O callbacks)—>闲置阶段(idle, prepare)—>轮询阶段……
轮询阶段
检查poll queue中是否有事件
- 有,按先进先出的顺序依次执行回调
- 无,检查是否存在setImmediate()的回调;检查是否存在到期的timer事件
上述二者顺序不固定,若同时为空,则暂时停止,知道接收I/O事件并进入I/O事件回调阶段
检查阶段
执行setImmediate()的回调
关闭事件回调
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。
定时器检测阶段
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。
I/O 回调阶段
这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
process.nextTick,setTimeout与setImmediate的区别与使用场景
在node中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval与之相同)与setImmediate
setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行你所设定的任务。
setImmediate()方法从意义上讲是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—-因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。
let、const和var的区别
| 区别 | var |
let |
const |
|---|---|---|---|
| 是否可以重复声明 | :white_check_mark: | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
| 是否受限于块级 | :negative_squared_cross_mark: | :white_check_mark: | :white_check_mark: |
是否与window 相映射 |
:white_check_mark: | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
| 是否存在暂存死区(先声明,再访问) | :negative_squared_cross_mark: | :white_check_mark: | :white_check_mark: |
| 声明之后是否必须赋值 | :negative_squared_cross_mark: | :negative_squared_cross_mark: | :white_check_mark: |
| 是否不可变 | :negative_squared_cross_mark: | :negative_squared_cross_mark: | :white_check_mark: |
Array.prototype.map / Array.prototype.filter / Array.prototype.reduce
Array.prototype.map
根据传递的转换函数,更新给定数组中的每个值,并返回一个相同长度的新数组。它接受一个回调函数作为参数,用以执行转换过程。
1 | [1,4,6,14,32,78].map(val =>val *10) |
Array.prototype.filter
当我们想要过滤数组的值到另一个数组,新数组中的每个值都通过一个特定检查。
1 | [1,4,6,14,32,78].filter(val =>val >10) |
Array.prototype.reduce
接受一个数组作为输入值并返回一个值。reduce() 接受一个回调函数,回调函数参数包括一个累计器(数组每一段的累加值,它会像雪球一样增长),当前值,和索引。reduce() 也接受一个初始值作为第二个参数。
1 | let finalVal = oldArray.reduce((accumulator,currentValue,currentIndx |
原生JS的实现
1 | Array.prototype.reduce = function (reducer,initVal) { |
严格模式
严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显式地脱离“马虎模式/稀松模式/懒散模式“(sloppy)模式,同时,其改变了语法及其运行行为。
特征
- 严格模式通过抛出错误来消除了一些原有静默错误。
- 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。
- 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。
应用场景
整个脚本
1
2
3// 整个脚本开启严格模式
;
/* Code ... */推荐指数::star:
为了减少因合并严格模式代码和非严格模式代码而产生的问题,不建议为整个脚本开启严格模式。
个别函数
1
2
3
4
5// 个别函数开启严格模式
const strict = () => {
;
return 'Hi! I am a strict mode function';
}推荐指数::star::star::star::star::star:
严格模式的变化
将过失错误转化为异常
变量创建需先声明,无法再意外创建全局变量 =>
Reference Error会使引起静默失败的赋值操作抛出异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15;
// 给不可写属性赋值
var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // 抛出TypeError错误
// 给只读属性赋值
var obj2 = { get x() { return 17; } };
obj2.x = 5; // 抛出TypeError错误
// 给不可扩展对象的新属性赋值
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // 抛出TypeError错误试图删除不可删除的属性时会抛出异
1
2;
delete Object.prototype; // 抛出TypeError错误函数的参数不允许重名
1
2
3
4function sum(a, a, c) { // !!! 语法错误
;
return a + a + c; // 代码运行到这里会出错
}不允许使用八进制数字语法
1
2
3
4;
var sum = 015 + // !!! 语法错误
197 +
142;禁止设置primitive值的属性
1
2
3
4
5
6
7(function() {
;
false.true = ""; //TypeError
(14).sailing = "home"; //TypeError
"with".you = "far away"; //TypeError
})();
简化变量的使用
- 严格模式禁用
with - 严格模式下的 eval 不再为上层范围引入新变量
- 禁止删除声明变量,
delete name在严格模式下会引起语法错误
- 严格模式禁用
令
eval和arguments变得更简单- 名称
eval和arguments不能通过程序语法被绑定(be bound)或赋值 - 参数的值不会随 arguments 对象的值的改变而变化
- 不再支持
arguments.callee和arguments.callee.caller
- 名称
安全性
- 严格模式禁止了不在脚本或者函数层面上的函数声明
- 在严格模式中一部分字符变成了保留的关键字
运算符new的机制
运算符new用于创建对象实例,包括:
用户定义的对象
1
2
3
4
5
6
7
8
9class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
}
const car = new Car('Eagle', 'Talon Tsi', 1993);:warning: 注意:箭头函数不能用作构造器,和
new一起用会抛出错误。1
2var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor具有构造函数的内置对象,比如内置对象
Array、Map、Date、Object、Number、Set·、String、RegExp等
问:运算符new 创建对象实例car时发生了什么?
STEP 0: 创建一个空对象,用__proto__属性连接Car对象的原型
STEP 1: 一个继承自Car.prototype的新对象被创建
STEP 2: 使用指定的参数调用构造函数 Car,并将 this 绑定到新创建的对象
STEP 3: 由构造函数返回的对象就是 new 表达式的结果
异步
异步编程就是在执行一个指令之后不是马上得到结果,而是继续执行后面的指令,等到特定的事件触发后,才得到结果。
异步实现
- 回调
- Promise
- Generator
- await/async
回调
1 | setTimeout(function () { |
Promise
参见下文
Generator
在 ES 2015 中,出现了 Generator 的语法,熟悉 Python 的同学肯定对这种语法有点了解。
简单来说,Generator 可以理解为一个可以遍历的状态机,调用 next 就可以切换到下一个状态。
在 JavaScript 中,Generator 的 function 与 函数名之间有一个 *, 函数内部使用 yield 关键词,定义不同的状态。
1 | function a() { |
await/async
这是在 ES 2016 中引入的新关键词,这将在语言层面彻底解决 JavaScript 的异步回调问题,目前可以借助 babel 在生产环境中使用。使用 await/async 可以让异步的操作以同步的方式来写。
1 | function a() { |
async/await 实现原理
async的本质是Generator的自动执行
首先看Generator:
1 | var x = 1; |
1 | function readFile(a){ |
defer/async
defer的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在`元素中设置defer`属性,相当于告诉浏览器立即下载,但延迟执行。指定
async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。
回调地狱
函数作为参数层层嵌套
如何解决:
- 保持你的代码简短
- 模块化(函数封装,打包,每个功能独立)
- 处理每一个错误
- 异步(Promise/Generator/async/await)
Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败),及其结果值。
Demo
1 | const promise = new Promise(function(resolve, reject) { |
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
当其中任一种情况(fulfilled or rejected)出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。
实现
1 | var PENDING = 0; |
Promise.all() 的实现
1 | function promiseAll(promises) { |
实例
1 | const promise = new Promise((resolve, reject) => { |
使用Promise封装Ajax
1 | let attributes = { |
高阶函数
一个函数接收另一个函数作为参数,称之为高阶函数
1 | function add(x, y, fn) { |
对This对象的理解
全局范围内
1
this;
当在全部范围内使用
this,它将会指向全局对象。函数调用
1
2
3
4
5function foo() {
console.log(this);
}
foo(); // console.log: window;这里
this也会指向全局对象。方法调用
1
2
3
4
5
6
7const test = {
foo: function () {
console.log(this);
}
}
test.foo(); // console.log: {foo: f}这个例子中,
this指向test对象。调用构造函数
1
new foo();
如果函数倾向于和
new关键词一块使用,则我们称这个函数是 构造函数。 在函数内部,this指向新创建的对象。显式设置
this1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const module = {
x: 42,
getX: function() {
return this.x;
}
}
const unboundGetX = module.getX;
console.log(unboundGetX());
// The function gets invoked at the global scope
// expected output: undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42当使用
Function.prototype上的call或者apply方法时,函数内的this将会被 显式设置为函数调用的第一个参数。因此函数调用的规则在上例中已经不适用了,在
foo函数内this被设置成了bar。
Function.prototype.bind() / Function.prototype.apply() / Function.prototype.call()
bind()方法创建一个新函数,在调用时,将其this关键字设置为所需的值。
1 | var pokemon = { |
call()方法调用一个给定this值的函数,并单独提供参数。
call()和apply()使用于完全相同的目的。 它们工作方式之间的唯一区别是call()期望所有参数都单独传递,而apply()需要所有参数的数组。
1 | var pokemon = { |
call() 实现
1 | function myCall(context) { |
apply()
1 | function myApply(context) { |
bind()
1 | function myBind(context) { |
条件语句嵌套优化
- 假如条件测试结果相同,可以选择合并条件
- 理清不同分支之间关联性,减少
if ... else ...嵌套 - 异常条件先退出,保证主干流程是核心流程
- 将类似流程封装为类似的公共函数
- 使用多态取代条件表达式
EventEmitter
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。
1 | let events = require('events'); |
module.export exports export的区别
总览
require: node 支持的引入
export / import : 只有es6 支持的导出引入
module.exports / exports: 只有 node 支持的导出
node 模块
Node里面的模块系统遵循的是CommonJS规范。CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)
在一个node执行一个文件时,会给这个文件内生成一个 exports和module对象, 而module又有一个exports属性。三者关系如下:
1 | exports = module.exports = {}; |
其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。 简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。
为了避免糊涂,尽量都用 module.exports 导出,然后用require导入。
ES 模块
对于 export 和export default:
- 二者可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,
export、import可以有多个,export default仅有一个 - 通过
export方式导出,在导入时要加{ },export default则不需要 export能直接导出变量表达式,export default不行
1 |
|
虚拟DOM
用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。
步骤
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存
ES 6
解构赋值
数组解构
1 | let [a, b, c] = [1, 2, 3] //a=1, b=2, c=3 |
对象解构
1 | let {a, b} = {a: 'aaaa', b: 'bbbb'} //a='aaaa' b='bbbb' |
函数参数的定义
1 | function personInfo({name, age, address, gender}) { |
交换变量的值
1 | let a=1, b=2; |
函数默认参数
1 | function saveInfo({name= 'william', age= 18, address= 'changsha', gender= 'man'} = {}) { |
使用箭头函数应注意什么?
1、用了箭头函数,this就不是指向window,而是父级(指向是可变的)
2、不能够使用arguments对象
3、不能用作构造函数,这就是说不能够使用new命令,否则会抛出一个错误
4、不可以使用yield命令,因此箭头函数不能用作 Generator 函数
Ajax
同步和异步的区别
同步:
浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作
异步:
浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容
get和post的区别
1、get和post在HTTP中都代表着请求数据,其中get请求相对来说更简单、快速,效率高些 2、get相对post安全性低
3、get有缓存,post没有
4、get体积小,post可以无限大
5、get的url参数可见,post不可见
6、get只接受ASCII字符的参数数据类型,post没有限制
7、get请求参数会保留历史记录,post中参数不会保留
8、get会被浏览器主动catch,post不会,需要手动设置
9、get在浏览器回退时无害,post会再次提交请求
什么时候使用post?
post一般用于修改服务器上的资源,对所发送的信息没有限制。比如
1、无法使用缓存文件(更新服务器上的文件或数据库)
2、向服务器发送大量数据(POST 没有数据量限制)
3、发送包含未知字符的用户输入时,POST 比 GET 更稳定也更可靠
Webpack
webpack只是一个打包模块的机制,只是把依赖的模块转化成可以代表这些包的静态文件。webpack就是识别你的 入口文件。识别你的模块依赖,来打包你的代码。至于你的代码使用的是commonjs还是amd或者es6的import。webpack都会对其进行分析。来获取代码的依赖。webpack做的就是分析代码。转换代码,编译代码,输出代码。webpack本身是一个node的模块,所以webpack.config.js是以commonjs形式书写的(node中的模块化是commonjs规范的)
模块热更新
模块热更新是webpack的一个功能,他可以使代码修改过后不用刷新就可以更新,是高级版的自动刷新浏览器
devServer中通过hot属性可以控制模块的热替换
1 | const webpack = require('webpack'); |
打包流程
webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和
Shell语句中读取与合并参数,得出最终的参数; - 开始编译:用上一步得到的参数初始化
Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译; - 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; - 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,
webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的 API 改变webpack的运行结果。
依照上述流程,我们来学习webpack.config.js
1 | // webpack.config.js |
XHR / Ajax / Fetch / Axios
前端异步请求
1 | // 原生XHR |
注意:
当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ), 仅当网络故障时或请求被阻止时,才会标记为 reject。 默认情况下, fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)。
底层封装
1 | // jquery ajax |
axios
同样是对XHR的封装
1 | axios({ |
并发
1 | function getUserAccount() { |
柯里化 / Currying
Currying 为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数
1 | function add(x) { |
通用柯里化
1 | // 柯里化通用式 ES6 |
防抖和节流
防抖(debounce)和节流(throttle)的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。
防抖
1 | const debounce = (fn, wait, immediate) => { |
节流
1 | const throttle = (fn, wait) => { |
实例
Array.prototype.flat 的实现
1 | const flatten = (arr) => { |
高精度计时器
1 | var start = new Date().getTime(), |
CSS
盒子模型
当对一个文档进行布局(lay out)的时候,浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型(CSS basic box model),将所有元素表示为一个个矩形的盒子(box)。
每个盒子由四个部分(或称区域)组成,其效用由它们各自的边界(Edge)所定义,包括:
内容边界 / Content Edge
1
box-sizing: content-box; /* default */
内容区域的大小明确可以通过以下属性控制:
|
width|min-width|max-width|height|min-height|max-height|
| ———- | —————- | —————- | ———— | —————— | —————— |内边距边界 / Padding Edge
边框边界 / Border Edge
1
box-sizing: border-box;
边框区域的大小明确可以通过以下属性控制:
|
width|min-width|max-width|height|min-height|max-height|
| ———- | —————- | —————- | ———— | —————— | —————— |外边框边界 / Margin Edge
拓展
box-sizing属性可以被用来调整这些表现:
content-box是默认值。如果你设置一个元素的宽为100px,那么这个元素的内容区会有100px 宽,并且任何边框和内边距的宽度都会被增加到最后绘制出来的元素宽度中。border-box告诉浏览器:你想要设置的边框和内边距的值是包含在width内的。也就是说,如果你将一个元素的width设为100px,那么这100px会包含它的border和padding,内容区的实际宽度是width减去(border + padding)的值。大多数情况下,这使得我们更容易地设定一个元素的宽高。
由此,可以对盒子模型进行分类:标注盒模型和怪异盒模型,前者的box-sizing属性取值为content-box,而后者的box-sizing属性取值为border-box。也就是说,对于标注盒模型,元素的宽度等于style里的width+border+padding宽度,而对于怪异盒模型,元素宽度等于style里的width宽度。
rem与em的区别
rem是根据根的font-size变化,而em是根据父级的font-size变化
CSS新特性
1 | transition:过渡 |
行内元素(display: inline)
宽度和高度是由内容决定,与其他元素共占一行的元素,我们将其叫行内元素,例如:<span>、 <i> 、<a>等
块级元素(display: block)
默认宽度由父容器决定,默认高度由内容决定,独占一行并且可以设置宽高的元素,我们将其叫做块级元素,例如:<p> 、<div> 、<ul>等
行内块级元素(display: inline-block)
结合了行内元素和块级元素的特性,与其他行内元素共享一行,可以修改width、height属性padding、margin四个方向的值设置均有效。
缺点:
- 在不等高的情况下视需要设置
vertical-align - 需要解决换行符空格引起的间隙问题:常用
font-size: 0; - 在IE6/7下残留1像素间隙。令人讨厌的内部元素的
font-size需要另外设置
CSS-Module
目的:为CSS规则加入局部作用域和模块依赖
局部作用域
CSS的规则都是全局的,生成局部作用域的唯一方法是使用独一无二的类名。通过生成独一无二的类名,使其仅对某一组间有效
模块依赖
CSS-Module允许通过composes 字段使一个选择器继承另一个选择器的规则,或者继承其他CSS文件中的规则
输入变量
1 | /* colors.css */ |
Flex
Flex是Flexible Box的缩写,意味”弹性布局”,任何一个容器都可以指定为Flex布局
- Flex布局元素,称为Flex容器,简称”容器”。它的所有子元素自动成为容器元素,简称”项目”。
- 容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的排列方式:从左到右;交叉轴的排列方式:从上到下;
容器属性
1 | display: flex; |
flex-direction :属性决定主轴的方向 (即项目的排列方式)
横向排列块级元素
1
flex-direction: row; /* 主轴水平方向,起点在左端 */
1
flex-direction: row-reverse; /* 主轴水平方向,起点在右端 */
纵向排列块级元素
1
flex-direction: column; /* 主轴垂直方向,起点在上沿 */
1
flex-direction: column-reverse; /* 主轴在垂直方向,起点在下沿 */
flex-wrap
1
2
3flex-wrap:nowarp (不换行,默认的)
flex-wrap:wrap (换行,第一行在上面)
flex-wrap:wrap-reverse (换行,第一行在下面)flex-flow:是flex-direction 属性和flex-wrap属性的简写,默认值row、nowrap
justify-content:属性定义了项目在主轴上的对齐方式
1
2
3
4
5justify-content:flex-start (左对齐,默认值)
justify-content:flex-end(右对齐)
justify-content:center (居中)
justify-content:space-between (两端对齐,项目之间的间隔相等)
justify-content:space-around (每个项目两侧的间距相等)align-items :定义项目交叉轴上如何对齐(单行)
1
2
3
4
5align-items:flex-start (交叉轴起点对齐)
align-items: flex-end (交叉轴终点对齐)
align-items:center (垂直方向,中间开始)
align-items:baseline (项目第一行文字的基线对齐)
align-items:stretch (默认值,如果项目未设置高度或设为auto,将占满整个容器的高度)align-content :多行轴线对齐(用法同align-items )
flex 项目属性
order 定义项目排列顺序
1
order:number (数值越小越靠前,默认为0)
flex-grow 定义项目放大比例
1
flex-grow :number(默认0,如果有剩余空间也不放大,值为1放大,2是1的双倍大小,此类推)
flex-shrink 定义项目缩小比例
flex-basis 定义项目自身大小
flex:属性是flex-grow,flex-shrink ,flex-basis的简写,默认值为0、1、auto
align-self 项目自身对齐
选择器优先级
浏览器通过优先级来判断哪一些属性值与一个元素最为相关,从而在该元素上应用这些属性值。优先级是基于不同种类选择器组成的匹配规则。
同一块内容,我们同时用了 ID选择器 和 类选择器, ID选择器 优先级大于 类选择器
优先级的计算规则
内联 > ID选择器 > 类选择器 > 标签选择器
优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下:
- 如果存在内联样式,那么
A = 1, 否则A = 0; B的值等于ID选择器出现的次数;C的值等于类选择器和属性选择器和伪类出现的总次数;D的值等于标签选择器和伪元素出现的总次数 。
优先级的特殊情况
!important 如果出现在外部样式,则优先级高于内联样式;若出现在内联样式,则优先级至高。
注意 :优先级的比较是基于同一元素的同一属性
浮动溢出
在非IE浏览器(如Firefox)下,当容器的高度为auto,且容器的内容中有浮动(float为left或right)的元素,在这种情况下,容器的高度不能自动伸长以适应内容的高度,使得内容溢出到容器外面而影响(甚至破坏)布局的现象。
为了解决浮动溢出的问题,需要进行的一系列处理方式,即清除浮动;
使用带
clear属性的空元素或邻接元素在浮动元素后使用一个空元素如
`<div class="clear"></div>,或针对既有的邻接元素,在CSS中赋予.clear{clear:both;}属性即可清理浮动。使用
overflow属性给浮动元素的容器添加
overflow:hidden;或overflow:auto;可以清除浮动。给浮动元素的容器添加浮动属性,即可清除内部浮动。
使用
:after伪元素 :star::star::star::star::star:给浮动元素的容器添加一个clearfix的class,然后给这个class添加一个:after伪元素实现元素末尾添加一个看不见的块元素(Block element)清理浮动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<style>
.news {
background-color: gray;
border: solid 1px black;
}
.news img {
float: left;
}
.news p {
float: right;
}
.clearfix:after{
content: "020";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix {
/* 触发 hasLayout */
zoom: 1;
}
</style>
<div class="news clearfix">
<img src="news-pic.jpg" />
<p>some text</p>
</div>
块格式化上下文(Block Formatting Context, BFC)
块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。它是一个独立的渲染区域,让处于 BFC 内部的元素和外部的元素相互隔离,使内外元素的定位不会相互影响。
下列方式会创建块格式化上下文:
- 根元素(
html) - 浮动元素(元素的
float不是none) - 绝对定位元素(元素的
position) 为absolute或fixed) - 行内块元素(元素的
display为inline-block) - 表格单元格(元素的
display为table-cell,HTML表格单元格默认为该值) - 表格标题(元素的
display为table-caption,HTML表格标题默认为该值) - 匿名表格单元格元素(元素的
display为table、table-row、table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或inline-table) overflow值不为visible的块元素display值为flow-root的元素contain值为layout、content或paint的元素- 弹性元素(
display为flex或inline-flex元素的直接子元素) - 网格元素(
display为grid或inline-grid元素的直接子元素) - 多列容器(元素的
column-count或column-width不为auto,包括column-count为1) column-span为all的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)
BFC 的作用
BFC最大的一个作用就是:在页面上有一个独立隔离容器,容器内的元素和容器外的元素布局不会相互影响。
1 | 解决上外边距重叠: 重叠的两个box都开启bfc; |
position的取值absolute和relative的区别
position: absolute
绝对定位:是相对于元素最近的已定位的祖先元素
position: relative
相对定位:相对定位是相对于元素在文档中的初始位置
水平垂直居中
1 | /** Flex Layput */ |
多行元素的文本省略号
1 | overflow : hidden; |
animation、transition、transform的区别
简述
1 | 1.transform用于元素旋转、缩放、移动、倾斜等效果 |
实例
环形进度条
HTML
1 | <div class="circle-bar"> |
CSS
1 | div.circle-bar { |
JavaScript
1 | window.onload = function() { |
clip-path与形状
三角形
1 | div { |
梯形
1 | div { |
圆形
1 | div { |
椭圆形
1 | div { |
矩形裁切
1 | div { |
Vue
Vue 的生命周期
Vue-router 的定义及其实现
1 | import VueRouter from 'vue-router' |
服务端
Apache 与 Nginx 的异同
Apache
Apache是基于模块化设计的,它的核心代码并不多,大多数的功能都被分散到各个模块中,各个模块在系统启动的时候按需载入。
MPM(Multi-Processing Modules,多重处理模块)是Apache的核心组件之一,Apache通过MPM来使用操作系统的资源,对进程和线程池进行管理,包括:
mpm-prefork预派生子进程:由模块产生众多子进程,每个子进程是单线程的,每个线程链接一个请求mpm-worker由模块产生线程数量固定的子进程,子进程数量由服务器根据负载情况决定。每个子进程建立一定数量的监听线程和一个服务线程mpm-event与mpm-worker类似,不过它解决了keep-alive长连接的时候占用线程资源被浪费的问题,在event工作模式中,会有一些专门的线程用来管理这些keep-alive类型的线程,当有真实请求过来的时候,将请求传递给服务器的线程,执行完毕后,又允许它释放。
Apache的运行
启动阶段
- 配置文件解析
- 模块加载
- 系统资源初始化 / root 权限
运行阶段
处理用户的服务请求 / 普通权限
Request => Post-Read-Request => URI Translation => Header Parsing => Access Control => Authorization => MIME Type Checking => FixUp => Response => Logging => CleanUp
URI Translation
将请求的URL映射到本地文件系统。
MIME Type Checking
根据请求资源的MIME类型的相关规则,判定将要使用的内容处理函数。
FixUp
这是一个通用的阶段,允许模块在内容生成器之前,运行任何必要的处理流程。
Nginx
Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。
模块
Nginx的模块从结构上分为核心模块、基础模块和第三方模块:
- 核心模块:HTTP模块、EVENT模块和MAIL模块
- 基础模块:HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块
- 第三方模块:HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块
Nginx的模块从功能上分为如下三类:
- Handlers(处理器模块)。此类模块直接处理请求,并进行输出内容和修改headers信息等操作。Handlers处理器模块一般只能有一个。
- Filters (过滤器模块)。此类模块主要对其他处理器模块输出的内容进行修改操作,最后由Nginx输出。
- Proxies (代理类模块)。此类模块是Nginx的HTTP Upstream之类的模块,这些模块主要与后端一些服务比如FastCGI等进行交互,实现服务代理和负载均衡等功能。
Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作。Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作。
从进程和线程的角度看,所有实际上的业务处理逻辑都在worker进程。worker进程中有一个函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个nginx服务被停止。Worker中这个函数执行内容如下:
- 操作系统提供的机制(例如epoll, kqueue等)产生相关的事件。
- 接收和处理这些事件,如是接受到数据,则产生更高层的request对象。
- 处理request的header和body
- 产生响应,并发送回客户端
- 完成request的处理
- 重新初始化定时器及其他事件
对比
Nginx和Apache一样,都是HTTP服务器软件,在功能实现上都采用模块化结构设计,都支持通用的语言接口,如PHP、Perl、Python等,同时还支持正向和反向代理、虚拟主机、URL重写、压缩传输、SSL加密传输等。
- 在功能实现上,Apache的所有模块都支持动、静态编译,而Nginx模块都是静态编译的,
- 对FastCGI的支持,Apache对Fcgi的支持不好,而Nginx对Fcgi的支持非常好;
- 在处理连接方式上,Nginx支持epoll,而Apache却不支持;
- 在空间使用上,Nginx安装包仅仅只有几百K,和Nginx比起来Apache绝对是庞然大物。
Nginx相对Apache的优点
- 轻量级,同样起web 服务,比apache 占用更少的内存及资源
- 静态处理,Nginx 静态处理性能比 Apache 高 3倍以上
- 抗并发,nginx 处理请求是异步非阻塞的,而apache则是阻塞型的,在高并发下nginx 能保持低资源低消耗高性能。在- - Apache+PHP(prefork)模式下,如果PHP处理慢或者前端压力很大的情况下,很容易出现Apache进程数飙升,从而拒绝服务的现象。
- 高度模块化的设计,编写模块相对简单
- 社区活跃,各种高性能模块出品迅速啊
Apache相对Nginx的优点
- rewrite,比nginx 的rewrite 强大
- 模块超多,基本想到的都可以找到
- 少bug,nginx的bug相对较多
- 超稳定
- Apache对PHP支持比较简单,Nginx需要配合其他后端用
优化
性能优化
HTML优化
1 | 1、避免 HTML 中书写 CSS 代码,因为这样难以维护。 |
CSS优化
1 | 1、优化选择器路径:使用 .c {} 而不是 .a .b .c {}。 |
JavaScript优化
1 | 1、尽可能把 <script> 标签放在 body 之后,避免 JS 的执行卡住 DOM 的渲染,最大程度保证页面尽快地展示出来 |