# HTML
# 1】语义化
- 标题就使用h1~h5
- 文章就写article标签
- 时间就是用time标签
- 画板就是用canvas标签
后台写html阶段:table标签布局;美工阶段:div+css布局不够语义化;前端阶段:选择正确的标签描述正确的内容。
尽可能少的使用无语义的标签div和span
在语义化不明显时,尽量使用p标签,因为p在默认情况下有上下间距
需要强调的文本,可以包含在strong或em标签中,strong默认样式是加粗(不要用b),em是斜体(不要用i)
什么是语义化?就是用合理、正确的标签来展示内容,比如h1~h6定义标题。
<header>
、<nav>
、<main>
、<article>
、<aside>
、<footer>
、<section>
、<video>
、<audio>
、<canvas>
:获取canvas、设置2d上下文、设置画笔颜色、设置画笔范围
# 2】响应式布局(媒体查询)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>响应式布局</title>
<style>
@media screen and (min-width: 992px){
/*pc端*/
}
@media screen and (min-width: 768px) and (max-width: 992px){
/*pad端*/
}
@media screen and (max-width: 768px){
/*移动端*/
}
</style>
</head>
<body>
</body>
</html>
使用viewport meta设置视口宽度和默认缩放比(针对移动端浏览器)
- device-width为设备分辨率宽度
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
name="viewport"说明此meta标签定义视口的属性
width=device-width宽度等于设备宽度
initial-scale=1.0初始化缩放比为1
maximum-scale=3允许用户放大至3倍
minimum-scale=0.5允许用户缩小至0.5倍
user-scalable=no禁止用户缩放
# 3】像素单位
- px逻辑像素
- dp设备像素、物理像素
- ppi屏幕每英寸像素数量,手机电脑的尺寸都是指对角线的长度为X英寸。
- ppi=对角线的物理像素/对角线长度
- dpr设备像素比:是默认缩放为100%的情况下,设备像素和CSS像素的比值
- dpr=dp/px
- 一般这个值是设备固定的,厂商设置。
- 每一个手机都有一个逻辑像素分辨率比如:iPhone 5分辨率为320x568 px
- device-width为设备分辨率宽度
<meta name='viewport' content='width=device-width,initial-sacle=1,user-scalable=no'>
让移动端视口是宽度等于设备分辨率宽度
# 4】!DOCTYPE作用
<!DOCTYPE html>
文档声明:告诉浏览器我们使用的版本号为HTML5的版本,它不是HTML标签。
声明文档的解析类型(document.compatMode),避免浏览器的怪异模式。
document.compatMode
:
BackCompat
:怪异模式,浏览器使用自己的怪异模式解析渲染页面。CSS1Compat
:标准模式,浏览器使用W3C的标准解析渲染页面。
这个属性会被浏览器识别并使用,但是如果你的页面没有DOCTYPE
的声明,那么compatMode
默认就是BackCompat
,如果你的页面添加了<!DOCTYPE html>
那么,那么就等同于开启了标准模式,那么浏览器就得老老实实的按照W3C的标准解析渲染页面,这样一来,你的页面在所有的浏览器里显示的就都是一个样子了。
# 5】script标签(defer、async)
# 6】浏览器运行原理
常见的面试题:从用户在浏览器地址栏输入网址,到看到页面,中间都发生了什么?
涉及知识点:TCP协议的三次握手四次挥手、HTTP报文、状态码、304缓存、DNS缓存、性能优化、重绘回流、AST语法解析树、DOM树、Layout树、Layer树、requestAnimationFrame、同步异步编程、闭包作用域、堆栈内存、面向对象、微任务和宏任务、事件循环、AJAX同源请求和跨域请求解决方案、Cookie和Session以及Token
- HTTP的请求阶段:DNS解析,TCP三次握手四次挥手、HTTPS和HTTP的区别等
- HTTP的响应阶段:HTTP状态码(304缓存),响应报文等
- 浏览器渲染阶段:重绘回流等
# 1】进程和线程
进程Process:进程通俗讲就是我们开的每一个程序
线程Thread:每个程序(进程)可能会做很多事情(子任务),就是线程
栈内存Stack:运行代码的环境(存放指针,这些指针指向堆内存)
# 2】parseHTML中dom,css,js的顺序
CSS样式表
浏览器在加载网页的过程中,先下载HTML并开始解析。如果发现外部CSS资源链接,就发送下载请求。浏览器在下载CSS资源的同时,解析HTML文件。
在应用样式的时候,不会更改DOM树,而是生成CSS树,因此解析样式表的时候也不会停止文档解析。
JS脚本
解析器遇到<script>
标记时,立即解析并执行脚本。一旦发现有脚本文件的引用,就必须等待脚本文件下载完。这时,文档的解析会暂停,并且等到脚本执行完再继续。所以,脚本文件的下载和执行,会阻断其他资源文件的下载。JS会阻塞解析HTML。所以建议<script>
一般放最后面,或者使用defer或者async属性异步加载。
# 3】图解
浏览器地址栏输入字符,浏览器进程的UI线程会捕捉输入内容,如果访问的是一个网址,则UI线程会启动一个网络线程,来请求DNS进行域名解析,接着使用IP地址进行TCP连接服务器,再使用HTTP请求数据和响应数据,浏览器主线程开辟执行上下栈,解析html上下文。
主线程:dom tree&&css tree---结合---》layout tree(layout过程)---》update layertree(形成层叠上下文的过程)--》paint(计算层叠上下文,形成表记录在哪绘制,啥时绘制)--》composite(绘制我们记录的表)
合成器线程----》整个过程栅格化数据,把画面分块,(可视化区域)再组合形成Frame---交给---》GPU线程去渲染。
之后的话,重排和重绘以及js执行或者用户行为都会占用主线程。
# 4】回流和重绘
- 布局是页面首次加载时进行的操作,重新布局即为回流。 - 绘制是页面首次加载时进行的操作,重新绘制即为重绘。
使用transform属性不会导致重绘和重排,因为是运行在合成器线程和栅格化线程中,硬件加速
JS同步任务时间执行过长,会抢占主线程,变卡顿。
# 5】requestAnimationFrame优化
当页面或者动画以每秒60帧的刷新率时,才不会让用户感觉页面卡顿。
如果每一帧在运行动画的时候,还有大量JS任务需要执行。(布局、绘制、JS执行都是在主线程中运行的)当在一帧的时间内布局和绘制结束后,还有剩余的时间JS就会拿到主线程的使用权,如果JS的执行时间过长,就会导致下一帧开始时JS依然没有执行结束(依然占用这主线程,也就是说JS执行可能会阻塞主线程),导致下一帧的动画没有按时渲染,就会出现页面的卡顿。
此时我们就可以使用requestAnimationFrame
这个API来优化。
requestAnimationFrame
这个方法会在每一帧开始时调用执行,在每一帧结束时回调(requestIdleCallback):帮我们把JS运行任务分成一些更小的任务块(分到每一帧结束的位置),在每一帧时间用完前暂停JS运行任务,归还主线程,按时渲染下一帧
# 7】事件流(事件冒泡、处于目标、捕获)
DOM标准规定事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
- 当事件触发在目标阶段时,会根据事件注册(代码顺序)的先后顺序执行,在其他两个阶段(事件捕获阶段和事件冒泡阶段)注册顺序(代码顺序)不影响事件执行顺序。
- 同时用addEventListener绑定的冒泡和捕获事件的元素, 哪个代码写在前面就先执行哪一个。其他元素通过冒泡或者捕获“感知”的事件,按照W3C的标准,先发生捕获事件,后发生冒泡事件。
addEventListener是DOM2级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名,作为事件处理程序的函数和一个布尔值,最后这个布尔值如果是true,表示在捕获阶段调用事件处理程序;默认是false,表示在冒泡阶段调用事件处理程序。
element.addEventListener(event, callback, useCapture)
通过e.stopPropagation()
阻止事件继续传递。
通过e.preventDefault()
阻止标签的默认行为。
e.target
的值是用户行为触发事件的准确target目标
e.currentTarget
的值是绑定事件监听函数的目标
不是所有的事件都能冒泡,例如:blur、focus、load、unload
<body>
<div>
<button>按钮</button>
</div>
</body>
<script type="text/javascript">
document.querySelector('div').addEventListener('click', (e) => console.log('冒泡div',e.target,e.currentTarget), false)
document.querySelector('div').addEventListener('click', (e) => console.log('捕获div',e.target,e.currentTarget), true)
document.querySelector('button').addEventListener('click', (e) => console.log('冒泡button',e.target,e.currentTarget),false)
document.querySelector('button').addEventListener('click', (e) => console.log('捕获button',e.target,e.currentTarget), true)
document.addEventListener('click', (e) => console.log('冒泡domcument',e.target,e.currentTarget), false)
document.addEventListener('click', (e) => console.log('捕获domcument',e.target,e.currentTarget), true)
</script>
此时点击按钮,打印结果如下:
捕获domcument <button>按钮</button> #document
捕获div <button>按钮</button> <div>…</div>
冒泡button <button>按钮</button> <button>按钮</button>
捕获button <button>按钮</button> <button>按钮</button>
冒泡div <button>按钮</button> <div>…</div>
冒泡domcument <button>按钮</button> #document
# 8】事件委托
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
- 父元素监听点击事件,子元素触发点击,父元素使用
e.target
判断。
<body>
<ul>
<li>🍐</li>
<li>🍌</li>
<li>🍊</li>
<li>🥝</li>
</ul>
</body>
<script>
const ul = document.querySelector('ul')
ul.addEventListener('click', (e) => {
console.log(e.target)
}, false)
//就不需要每一个子元素都获取然后去指定事件监听
</script>
# 9】JS事件绑定方式
1】直接在标签属性绑定事件
- 需要在标签属性中立即执行绑定的函数
- 可以直接
return false;
进行阻止默认事件。 - 没有event对象
<div id="btn" onclick="clickone();return false;"></div> //直接在DOM里绑定事件,需要写成立即执行的格式
<script>
function clickone(){ alert("hello"); }
</script>
2】通过获取DOM绑定事件
- 在脚本中通过匿名函数的方式绑定的只会执行最后一个事件。
- 也可以使用
return false;
进行阻止默认事件。也可以使用e.preventDefault()
<div id="btn"></div>
<script>
document.getElementById("btn").onclick = function (){
alert("hello");
return false
}
</script>
3】添加事件监听函数
- 用 "addEventListener" 可以绑定多次同一个事件,且都会执行,
- 而在DOM结构如果绑定两个 "onclick" 事件,只会执行第一个;
- 只能通过
e.preventDefault()
来阻止默认事件。
<div id="btn"></div>
<script>
document.getElementById("btn").addeventlistener("click",(e)=>{
e.preventDefault()//阻止默认事件
alert("hello")
},false);
</script>
# 10】base64
一、图片转换成base64格式的优缺点
- 优点
(1)base64格式的图片是文本格式,占用内存小,转换后的大小比例大概为1/3,降低了资源服务器的消耗;
(2)网页中使用base64格式的图片时,不用再请求服务器调用图片资源,减少了服务器访问次数。
- 缺点
(1)base64格式的文本内容较多,存储在数据库中增大了数据库服务器的压力;
(2)网页加载图片虽然不用访问服务器了,但因为base64格式的内容太多,所以加载网页的速度会降低,可能会影响用户的体验。
(3)base64无法缓存,要缓存只能缓存包含base64的文件,比如js或者css,这比直接缓存图片要差很多,而且一般HTML改动比较频繁,所以等同于得不到缓存效益。
# 11】对iframe的了解
- iframe是用来在网页中插入第三方页面,早期的页面使用iframe主要是用于导航栏这种很多页面都相同的部分,这样在切换页面的时候避免重复下载
- iframe的创建比一般的DOM元素慢了1-2个数量级
- iframe标签会阻塞页面的的加载
# 局限:
# 1、创建比一般的 DOM 元素慢了 1-2 个数量级
iframe 的创建比其它包括 scripts 和 css 的 DOM 元素的创建慢了 1-2 个数量级,使用 iframe 的页面一般不会包含太多 iframe,所以创建 DOM 节点所花费的时间不会占很大的比重。但带来一些其它的问题:onload 事件以及连接池(connection pool)
# 2、阻塞页面加载
及时触发 window 的 onload 事件是非常重要的。onload 事件触发使浏览器的 “忙” 指示器停止,告诉用户当前网页已经加载完毕。当 onload 事件加载延迟后,它给用户的感觉就是这个网页非常慢。
window 的 onload 事件需要在所有 iframe 加载完毕后(包含里面的元素)才会触发。在 Safari 和 Chrome 里,通过 JavaScript 动态设置 iframe 的 SRC 可以避免这种阻塞情况
# 3、唯一的连接池
浏览器只能开少量的连接到 web 服务器。比较老的浏览器,包含 Internet Explorer 6 & 7 和 Firefox 2,只能对一个域名(hostname)同时打开两个连接。这个数量的限制在新版本的浏览器中有所提高。Safari 3+ 和 Opera 9+ 可同时对一个域名打开 4 个连接,Chrome 1+, IE 8 以及 Firefox 3 可以同时打开 6 个
绝大部分浏览器,主页面和其中的 iframe 是共享这些连接的。这意味着 iframe 在加载资源时可能用光了所有的可用连接,从而阻塞了主页面资源的加载。如果 iframe 中的内容比主页面的内容更重要,这当然是很好的。但通常情况下,iframe 里的内容是没有主页面的内容重要的。这时 iframe 中用光了可用的连接就是不值得的了。一种解决办法是,在主页面上重要的元素加载完毕后,再动态设置 iframe 的 SRC。
# 4、不利于 SEO
搜索引擎的检索程序无法解读 iframe。另外,iframe 本身不是动态语言,样式和脚本都需要额外导入。综上,iframe 应谨慎使用。
# 12】table标签
可以在table标签中使用<tr></tr>
(行)、<td></td>
(列)
table标签的属性有:
- width:整个表格的宽度
- height:整个表格的高度
- border:整个表格边框的宽度
- align:(不赞成使用)规定表格相对周围元素的对齐方式
- cellpadding:单元边沿于内容之间的口间距(内边距)
- cellspacing:单元格之间的间距(外边距)
<table border="1">
<tr><!-- 行 -->
<th>Month</th><!-- 表头 -->
<th>Savings</th>
</tr>
<tr>
<td>January</td><!-- 普通列 -->
<td>$100</td>
</tr>
</table>
td标签的属性有:
- char:规定根据哪个字符来进行文本对齐。(character)
- align:单元格中内容的水平对齐方式(left、right、center、justify、char)
- valign:单元格内容的垂直对齐方式(top、middle、bottom、baseline)
- axis:对单元进行分类
- scope:定义将表头数据与单元数据相关联的方法(col、row、colgroup、rowgroup)
# 13】a标签的target属性
href属性:去往的超链接路径(必填属性)
target属性:
_self
:它是a标签的默认值,它使得目标文档载入并显示在相同的框架或者窗口中作为源文档。这个目标是多余且不必要的,除非和文档标题标签中的 target 属性一起使用。 _blank
:浏览器总在一个新打开、未命名的窗口中载入目标文档。_parent
:这个目标使得文档载入父窗口或者包含来超链接引用的框架的框架集。如果这个引用是在窗口或者在顶级框架中,那么它与目标 _self 等效。_top
:(一般也是在iframe页面中使用_top
)这个目标使得文档载入包含这个超链接的窗口,用 _top 目标将会清除所有被包含的框架并将文档载入整个浏览器窗口。- framename:在对应iframename的iframe框架中打开href超链接地址
# CSS
# 1】Flex布局
# 容器的属性:
display:flex
flex-direction
:决定主轴的方向(即项目的排列方向)。默认值row- 值:
row | row-reverse | column | column-reverse;
- 值:
flex-wrap
:如果一条轴线排不下,如何换行。默认值nowrap- 值:
nowrap | wrap | wrap-reverse;
- 值:
flex-flow
:flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrapjustify-content
:定义项目在主轴上的对齐方式。默认值flex-start左对齐- 值:
flex-start | flex-end | center | space-between | space-around | space-evenly;
- 值:
align-items
:定义项目在交叉轴上如何对齐。默认值stretch(如果项目未设置高度或设为auto,将占满整个容器的高度。)- 值:
flex-start | flex-end | center | baseline | stretch;
- 值:
align-content
:定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。默认值stretch- 值:
flex-start | flex-end | center | space-between | space-around | stretch;
- 值:
# 项目的属性:
flex
:flex-grow
,flex-shrink
和flex-basis
的简写,默认值为0 1 auto。后两个属性可选。该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。- flex: 1; === flex: 1 1 任意数字+任意长度单位;
- flex: 1; !== flex:1 1 auto;
flex-grow
:定义项目的放大比例,默认为0(即如果存在剩余空间,也不放大)- 值:
<number>
- 如果所有项目的
flex-grow
属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow
属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
- 如果所有项目的
- 值:
flex-shrink
:定义了项目的缩小比例,默认为1(即如果空间不足,该项目将缩小)- 值:
<number>
- 如果所有项目的
flex-shrink
属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink
属性为0,其他项目都为1,则空间不足时,前者不缩小。
- 如果所有项目的
- 值:
flex-basis
:定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
# 2】BFC
BFC
:Block Formatting Context
(块级格式化上下文)
只需要记住,BFC
的目的就是:形成一个完全独立的空间,让空间中的子元素不会影响到外面的布局。
我们通过为元素设置一些CSS
属性,就能触发这一空间。以下是四种最为常见的触发规则:
float
不为none
overflow
的值不为visible
position
不为relative
和static
display
的值为table-cell
或inline-block
BFC的布局规则:
- 内部的Box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。
- 每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
- BFC的区域不会与float box重叠。
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
- 计算BFC的高度时,浮动元素也参与计算。
普通文档流布局规则
1.浮动的元素是不会被父级计算高度
2.非浮动元素会覆盖浮动元素的位置
3.margin会传递给父级
4.两个相邻元素上下margin会重叠
BFC布局规则
1.浮动的元素会被父级计算高度(父级触发了BFC)
2.非浮动元素不会覆盖浮动元素位置(非浮动元素触发了BFC)
3.margin不会传递给父级(父级触发了BFC)
4.两个相邻元素上下margin会重叠(给其中一个元素增加一个父级,然后让他的父级触发BFC)
# 3】清除浮动
浮动的元素脱离文档流,之前占用空间会释放,影响正常文档流的元素位置。在受到浮动影响其位置的元素上面使用clear:both
来使浮动元素回归文档流。
①在浮动元素所在容器内的最后添加空标签clear:both
②在浮动元素所在容器上面使用::after
伪元素content:'';display:block;clear:both
③在浮动元素所在容器上使用overflow:auto/hidden/scroll
,创建BFC包住浮动的元素。(因为容器没有设置高度,BFC布局中,容器会包含住所有内部元素)
# 4】::before&::after
CSS中,::before
创建一个伪元素,其将成为匹配选中的元素的第一个子元素。常通过content
属性来为一个元素添加修饰性的内容。此元素默认为行内元素。由::before
和::after
生成的伪元素包含在元素格式框内(相当于就是此元素的子元素)。
# 5】IFC
Inline Formatting Contexts,也就是“内联格式化上下文”。
# 符合以下任一条件即会生成一个IFC:
- 块级元素中仅包含内联级别元素
# IFC布局规则:
- 子元素水平方向横向排列,并且垂直方向起点为元素顶部。
- 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。
- 在垂直方向上,子元素会以不同形式来对齐(vertical-align)、水平方向(text-align)
- IFC中的“line box”一般左右边贴紧其包含块,但float元素会优先排列。
# 6】层叠上下文
某些元素的渲染顺序是由其 z-index
的值影响的。这是因为这些元素具有能够使他们形成一个层叠上下文的特殊属性。
形成层叠上下文的条件:
- 文档根元素(
<html>
) position
值为absolute
或relative
且z-index
值不为auto
的元素position
值为fixed
或sticky
(粘滞定位)的元素- flex(
flexbox
)容器的子元素,且z-index
值不为auto
- grid(
grid
)容器的子元素,且z-index
值不为auto
opacity
属性值小于1
的元素(在最下面的层叠上下文)- 以下任意属性值不为
none
的元素:(在最下面的层叠上下文)transform
filter
两大原则:谁大谁上,后来居上
bottom装饰---->布局--->内容top
# 7】盒子模型
①标准盒子模型
box-sizing: content-box;
默认
标准盒模型中width指的是内容区域content的宽度;height指的是内容区域content的高度。
标准盒模型下盒子的大小 = content + border + padding + margin
②怪异盒子模型(更方便)
box-sizing: border-box;
怪异盒模型中的width指的是内容、边框、内边距总的宽度(content + border + padding);height指的是内容、边框、内边距总的高度。
怪异盒模型下盒子的大小=width(content + border + padding) + margin
# 拓展延伸:javascript中的offsetWidth、clientWidth
以下都是基于标准盒子模型
clientWidth = width + 左右padding
offsetWidth = width + 左右padding + 左右boder
# 8】瀑布流布局
# ①column多列属性
一个是 column-count 属性,是分为多少列。
一个是 column-gap 属性,是设置列与列之间的距离
# ②JS实现瀑布流
瀑布流布局的特点是等宽不等高。(使用一个定长度数组来存)
父元素设置为相对定位,图片所在元素设置为绝对定位。然后通过设置top
值和left
值定位每个元素。
img{
position:absolute;
left:上一排中最小索引值*图片固定宽度 px;
top:最小图片的高度,叠加;
}
用一个数组来存放每一排的图片高度,假如是一排四列图片[100,80,120,150],然后不断叠加
用获取屏幕的宽度/每张图片宽度=列数。
# ③flex弹性布局
flex-flow:column wrap
纵向排列,换行
缺点:需要固定外部容器的高度。
# 9】css选择器优先级
越具体优先级越高
一样选择器,代码顺序写在后面的覆盖前面的
important!
最高、少用(优先级比行内样式还要高)行内样式*1000
ID选择器数量 * 100
类、属性、伪类选择器数量 * 10
元素、伪元素选择器数量 * 1
# 10】css属性继承
- 字体样式可继承
- 盒子样式不可继承
# 11】css reset 和 normalize.css 有什么区别?
- 考英文:
- reset 重置,之前的样式我不要,a{color: red;},抛弃默认样式
- normalize 让所有浏览器的标签都跟标准规定的默认样式一致,各浏览器上的标签默认样式基本统一。(保留有价值的默认样式)
# 12】position:sticky
position:sticky
是一个新的css3属性,它的表现类似position:relative
和position:fixed
的合体,当目标区域在屏幕中可见时,它的行为就像position:relative
;当页面滚动超出目标区域时,它的表现就像position:fixed
,它会固定在目标位置。(注意:top和left优先级分别大于bottom和right)
原生实现:
<style>
.header {
width: 100%;
background: #f6d565;
padding: 25px 0;
}
.sticky {
position: fixed;
top: 0;
}
</style>
<body>
<div style='height: 200px;'></div>
<div class="header"></div>
<div style='height: 1000px;'></div>
</body>
<script>
var header = document.querySelector('.header');
var originOffsetY = header.offsetTop;//获取该元素初始的距父元素顶部的高度
function onScroll(e) {
window.scrollY >= originOffsetY//屏幕顶部到页面顶部的距离大于获取的原始值
? header.classList.add('sticky')
: header.classList.remove('sticky')
}
document.addEventListener('scroll', onScroll)
</script>
# JAVASCRIPT
# 1】AJAX和xhr和fetch
Asynchronous JavaScript and XML(异步js和xml)
(严格依赖于XMLHttpRequest对象)
xhr对象的属性:
- readyState:请求状态
- 只有状态4是请求完毕,记住
- status:响应状态
- status表示HTTP响应中的状态码,200才是响应成功
xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open(提交方式get/post,服务器地址url,true);
xmlHttpRequest.setRequestHeader("Content-Type","application/x-www-form-urlencoded");//设置请求头
xmlHttpRequest.send("mobile="+mobile);//post请求key=value的形式,get请求传null值
xmlHttpRequest.onreadystatechange=function(){
if(xmlHttpRequest.readyState ==4 && xmlHttpRequest.status ==200){
var data = xmlHttpRequest.responseText;
alert(data)
}
}
注意:content-Type:application/x-www-form-urlencoded
和content-Type:multipart/form-data
只是post请求在请求体body中数据的两种编码方式。一种支持上传文件,一种不支持。
post参数格式:
- form-data(multipart/form-data):可传文件
- x-www-form-urlencoded(application/x-www-form-urlencoded):"key1=val1&key2=val2"
- raw(application/json):json字符串
- text/plain:普通字符串
# Fetch特点
- 不是基于xhr对象的,而是原生js。
- 并且基于标准Promise来实现,支持 async/await。
- 比xhr的语法简洁,更加语义化
- Fetch API 提供了访问数据的文件流实际字节的方法,而 XHR 的 responseText 只有文本形式。
- fetch需要自己封装处理状态码,它内部只能判断服务器是否响应
# 2】Axios、umi-request
Axios的优点:
- 在浏览器中发送XMLHTTPRequests请求
- 在node.js中发送http请求
- 支持Promise API
- 拦截请求和响应
- 请求拦截:带上token、加载图标显示
- 响应拦截:判断状态码、未登录、token过期、加载图标关闭
- 转换请求和响应数据
- 等等
umi-request:
umi-request 是基于 fetch 的封装,兼具 fetch 和 axios 的特点。
# 3】Promise
基于回调,解决回调地狱
//Promise基本使用
new Promise((resolve, reject) => {
setTimeout(() => {
//成功的时候调用resolve,传入结果
//resolve('hello')
//失败的时候调用reject,传入结果
reject('error message')
}, 1000)
}).then((data) => {
//then里面处理成功,此时打印hello
console.log(data)
}).catch((err)=>{
//catch里面处理失败,此时打印error message
console.log(err)
})
三种状态:pending(等待态)、fulfilled(满足态)、rejected(拒绝态)
链式调用:.then()
中返回一个新promise对象来形成链式调用。
- 如果
.then()
不返回值,即默认return undefined
,作为回调参数传递给下一个.then(d=>d)
- 如果
.then()
返回普通值,将会把这个普通值作为回调参数传递给下一次.then(d=>d)
触发执行.catch()失败:①返回一个reject()
的Promise对象,②抛出异常错误
触发执行.then()成功:①返回一个resolve()
的Promise对象,②返回一个普通值,这个普通值直接传递给下一个then()
,③不显式返回值,将undefined
传给下一个then()
常用API:
Promise.resolve()
- 传入的参数是一个普通值,就可以把普通对象转换成Promise对象。
- 传入的参数是一个Promise对象,则会等待这个Promise对象执行完后再继续执行。
Promise.reject()
- 同上
Promise.all()
- 并发执行,但是有序地得到结果,返回一个promise对象。
- 传入的数组中的参数
[p1,p2,...,pn]
,都会调用Promise.resolve()
来转成Promise对象类型。 - 而且
Promise.race()
自身返回的也是Promise对象。 - 该方法的参数不一定是数组,但必须具有 Iterator 接口,且执行返回的每个成员都是 Promise 实例。
- 全真才真,一假全假。
- 必须使用计数器来底层实现。(因为是异步的,加入微任务队列)
Promise.all = function (arr) {
if(!Array.isArray(promises)){
throw new TypeError('You must pass array')
}
let res = []
return new Promise((resolve, reject) => {
let index = 0
for (let i in arr) {
Promise.resolve(arr[i]).then(data => {
res[i] = data
index++
if (index == arr.length) {//必须在Promise异步中判断
resolve(res)
}
})
}
})
}
Promise.all([1, 2, new Promise((r) => { setTimeout(() => { r(3) }) }), 4]).then(data => { console.log(data) })
//[ 1, 2, 3, 4 ]
Promise.race()
- 传入的参数和上面
Promise.all()
一致 - 依然会把数组中的中的所有对象,使用
Promise.resolve()
转为Promise对象。 - 而且
Promise.race()
自身返回的也是Promise对象。 - 下面代码中,只要
p1
、p2
、...、pn
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数(then
orcatch
)。 - 一变就跳出
- 传入的参数和上面
const p = Promise.race([p1, p2,..., pn]);
p.then(data=>data)//data为率先改变状态的返回值。
Promise.race = function (arr) {
if (!Array.isArray(arr)) {
throw new TypeError('You must pass array')
}
return new Promise((resolve, reject) => {
for (let i in arr) {
Promise.resolve(arr[i])
.then(data => {
resolve(data)
})
.catch(err => {
reject(err)
})
}
})
}
Promise.race([
new Promise((r, j) => { setTimeout(() => { r(1) }, 2000) }),
new Promise((r, j) => { setTimeout(() => { j(2) }, 1000) }),
new Promise((r) => { setTimeout(() => { r(3) }, 1500) })
]).then(data => console.log(data)).catch(err => console.log(err))
Promise.prototype.finally()
- 该方法的回调函数不接受任何参数
- 该方法写出来就必然会执行,与状态无关,不依赖于Promise的执行结果
Promise.finally()
可以看似于特殊的Promise.then()
方法,也会返回一个 Promise 对象用于传递上一个Promise的结果。- 一般用于拦截
# 4】Iterator
比如Map、Set、Array数组、String、generator函数、arrayLike类似数组的对象(但是必须要添加迭代的功能)、TypedArray等(需要非常注意的是对象是没有迭代器接口,需要手动添加迭代器功能,或者转换成数组来进行迭代)
- 只要具有迭代器接口的类型都可以使用
[...It]
或Array.from(It)
强转成数组类型。 Array.prototype.slice.call(arrayLike)
只能把类数组对象转为数组,不能把迭代器对象转成数组
上面提到的数组、Map、Set等都是这样使用[Symbol.iterator]
包装的迭代器
let string = '我是雷浩'
let it1= string[Symbol.iterator]()
let it2= string[Symbol.iterator]()
for (let i of string) {//实际上也是调用了string中的Symbol.iterator方法
console.log(i)//依次打印:我 是 雷 浩
}
for (let i of it1) {
console.log(i)//依次打印:我 是 雷 浩
}
console.log(it2.next().value)//我
console.log(it2.next().value)//是
console.log(it2.next().value)//雷
console.log(it2.next().value)//浩
实现一个对象迭代器:(使用计数器)
var obj = {
name: '雷浩',
age: 21,
address: '四川成都',
[Symbol.iterator]: function () {
let self = this;
let key = Object.keys(self)
let index = 0
return {
next() {
return {
value: obj[key[index]] ? obj[key[index]] : undefined,
done: index++ < key.length ? false : true
}
}
}
}
};
let it = obj[Symbol.iterator]()
console.log(it.next())//{ value: '雷浩', done: false }
console.log(it.next())//{ value: 21, done: false }
console.log(it.next())//{ value: '四川成都', done: false }
console.log(it.next())//{ value: undefined, done: true }
for (let i of obj) {
console.log(i)//依次打印:雷浩 21 四川成都
}
Generator包装一个迭代器:
let obj = {
name: "XX",
age: 20,
job: 'teacher',
* [Symbol.iterator] () {
const self = this;
const keys = Object.keys(self);
for (let index = 0;index < keys.length; index++) {
yield self[keys[index]];
}
}
};
# 5】Generator
generator(生成器)执行之后返回生产一个 Iterator(迭代器)
yield
关键字必须放在带*
指针的生成器函数里面用。Iterator.next(param)
接收一个参数,传入的实参会传递给上一次yield的返回值。在generator生产器函数中支持
yield* Iterator
的用法。function* generator() { yield 1 yield* 'LH'//yield后面带*号,该值会被继续迭代 yield 2 yield* 'LLY'//字符串是具备迭代器接口的 } for (let i of generator()) {//执行generator函数返回一个迭代器 console.log(i) } //依次打印:1 L H 2 L L Y
//一遇到yeild然后执行yeild后面的值,执行完毕就暂停
function* read(){
let a = yield 'hello';//先执行yield之后立即暂停,所以并没有给a赋值
console.log(a);//undefined
let b = yield 'world';
console.log(b);//undefined
}
let it = read()
console.log(it.next())//{ value: 'hello', done: false } undefined
console.log(it.next())//{ value: 'world', done: false } undefined
console.log(it.next())//{ value: undefined, done: true }
使用Generator yield实现async await:
async await ==语法糖=> generator yield +co(大神tj写的一个nodejs库)
function* event() {
const one = yield new Promise((resolve, reject) => setTimeout(() => { resolve(1) }))
const two = yield one+1
const three = yield new Promise((resolve, reject) => setTimeout(() => { resolve(two+1) }))
}
let it = event()
let { value, done } = it.next()
Promise.resolve(value).then(data => {
console.log(data)//1
let { value, done } = it.next(data)
Promise.resolve(value).then(data => {
console.log(data)//2
let { value, done } = it.next(data)
Promise.resolve(value).then(data => {
console.log(data)//3
})
})
})
# 6】AsyncGenerator
Async Generator Functions in JavaScript
总结:记住使用异步迭代器调用next函数之后获得的结果是以Promise的形式进行返回。
异步生成器函数是特别的,因为你可以在函数中同时使用await 和 yield, 异步生成器函数不同于异步函数和生成器函数,因为它不会返回promise或者iterator,而是返回异步迭代器,你可以把异步迭代器作为一个**next()
函数总是返回一个promise的迭代器**。
例子:
异步迭代器函数行为与迭代器函数相似,迭代器函数返回一个有next函数的对象,调用next()
将执行generator函数,直到下一个yield。不同的是,异步迭代器的next()
返回一个promise
async function* run() {
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
// `run()` returns an async iterator.
// 执行异步生成器生成异步迭代器
const asyncIterator = run();
// The function doesn't start running until you call `next()`
// 异步迭代器调用next函数,得到第一个yield后面跟的结果,并以Promise形式返回。
asyncIterator.next().
then(obj => console.log(obj.value)). // Prints "Hello"
then(() => asyncIterator.next()); // Prints "World"
最干净的方法是使用for/await/of
循环整个异步迭代器函数:
async function* run() {
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
const asyncIterator = run();//生成异步迭代器
// Prints "Hello\nWorld"
//使用async、await、for:循环一步等待一步
(async () => {
for await (const val of asyncIterator) {
console.log(val); // Prints "Hello"(val是yield后的结果以Promise形式返回,所以需要await去获取它的值)
}
})();
# 7】async await
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。await
后面的值会被强转成Promise对象
async function event() {
const one = await new Promise((resolve, reject) => setTimeout(() => { resolve(1) })); //1
const two = await one + 1 //2
const three = await new Promise((resolve, reject) => setTimeout(() => { resolve(two + 1) })) //3
return three
}
event().then(data => console.log(data))//3
console.log(event())//Promise { <pending> }
原生async await=co+generator yield
function* read() {
let content = yield 1
let r = yield 2
return r;
}
function co(it) {
return new Promise((resolve,reject)=>{
function exec(data){
let {value,done} = it.next(data)
if(!done){//如果done一直是flase
Promise.resolve(value).then(res=>{//yield的后面可能是普通值,需要强转
exec(res)//递归
},reject)//出错就跳出
}else{
resolve(data)//停止执行,返回的promse的对象调用.then()获取值
}
}
exec()//第一次执行
})
}
co(read()).then(res=>{
console.log(res)//2
})
# 8】Map
键的类型:
- JS对象中的键只能是字符串或者Symbol的形式
- Map中的键可以是任意值。
键的顺序:
- JS对象中的键无序的。
- Map中的键是有序的,顺序是按照插入的顺序来记录。因此,当迭代的时候,一个
Map
对象以插入的顺序返回键值。
Size:
- JS对象中的键值对个数只能手动计算
- Map中的键值对个数可以轻易地通过size属性获取
迭代:
- JS对象需要以某种方式获取它的键,或者手动添加迭代器接口然后才能迭代。
- Map默认具有迭代器接口,所以可以直接被迭代。
Map的方法简单使用:
- 先new的时候,可以向构造函数中传入
[[k,v],[k,v]]
的数组类型参数
const m = new Map([['one', 1], ['two', 2]]);
const n=[1,2]
let a=m.set(n, 'content')//执行并返回Map
let b=m.get(n)//返回传入键所对应的值
console.log(a)//Map { 'one' => 1, 'two' => 2, [ 1, 2 ] => 'content' }
console.log(b)//content
m.size//3
m.has(n)//true
m.delete(n)//true
m.has(n)//false
m.clear()//清除所有成员,没有返回值。
Map 结构的实例有三个迭代器生成函数和一个遍历方法
Map.prototype.keys()
:返回键名的迭代器。- JS对象
Object.keys(obj)
:直接返回一个数组存放所有key。
- JS对象
Map.prototype.values()
:返回键值的迭代器。- JS对象
Object.values(obj)
:直接返回一个数组存放所有value。
- JS对象
Map.prototype.entries()
:返回所有成员的迭代器。- 等同于使用
map[Symbol.iterator]
- JS对象并没有迭代功能
- 等同于使用
Map.prototype.forEach()
:遍历 Map 的所有成员。map.forEach((value,key,self)=>{...})
注意:以下三种迭代方式一样的
Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries
// true
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
# WeakMap:
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
WeakMap中的键名是对象的弱引用(注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用)。
只要WeakMap中键名引用的对象,虽然是被WeakMap引用了,但是依然会被垃圾回收掉,因为这是弱引用。(并不影响引用的那个对象被垃圾回收掉)
无法遍历,不支持keys()
、values()
和entries()
方法,也没有size
属性。
# 9】Set
Set集合类似于数组,但是成员的值都是唯一的,没有重复。
Set的基本使用:
Set
的构造函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。let set = new Set([1,2,3,4]);
- 把
Set
强转成数组:let arr=[...set]
let arr=Array.from(set)
let set = new Set();
set.add(1).add(2).add(3)//执行并返回修改后的结果
// Set { 1, 2, 3 }
set.size // 3
set.has(3) // true
set.delete(3) // true
set.has(3) // false
set.clear() // 清除所有成员,没有返回值。
Set 结构的实例有三个迭代器生成函数和一个遍历方法,可以用于遍历成员。
Set.prototype.keys()
:返回键名的迭代器Set.prototype.values()
:返回键值的迭代器Set.prototype.entries()
:返回键值对的迭代器Set.prototype.forEach()
:使用回调函数遍历每个成员set.forEach((value,key,self)=>{...})
数组Array、集合Set、表Map都具有迭代器生产方法和遍历方法:
instance.keys()
:返回键名的迭代器instance.values()
:返回键值的迭代器instance.entries()
:返回键值对的迭代器instance.forEach()
:使用回调函数遍历每个成员instance.forEach((value,key,self)=>{...})
# WeakSet:
WeakSet 的成员只能是对象,而不能是其他类型的值。
也是弱引用。不容易出现内存泄漏。
无法遍历,不支持keys()
、values()
和entries()
方法,也没有size
属性。
# 10】Array
Array构造函数中的私有方法:
Array.isArray()
:判断数组类型Array.from()
:一个类数组或可迭代对象强转成一个数组Array.prototype.slice.call()
都可以转成数组
Array.of()
:创建具有所有参数新数组,而不考虑参数的数量或类型。
Array原型上的方法:(Array.prototype.method
)
array.push()
入栈、array.pop()
出栈array.unshift()
头增、array.shift()
头删array.indexOf()
、array.includes()
- 返回从索引0开始的第一个找到的数,找不到返回-1
array.keys()
、array.values()
、array.entries()
- 都会生成一个迭代器,需要在再次
for of
来执行迭代器
- 都会生成一个迭代器,需要在再次
array.splice(index,count,item..)
:新增、删除、修改元素,返回包含被删除项目的新数组;会改变原数组- 必须至少传入前两个参数
array.slice(start,end)
:裁剪数组,返回裁剪后的数组,并不改变原数组- 必须至少传入第一个参数
- 裁剪的数组包含start不包含end。
- 如果start或者end为负数,则按照
-1
最后一个元素,-2
倒数第二个元素的顺序来指定元素,依然是遵循包含start不包含end。
array.join()
:拼接成字符串,并不改变原数组array.concat()
:合并数组,并不改变原数组array.reverse()
:翻转数组,返回翻转后的数组,会改变原数组array.sort()
:排序数组,返回排序后的数组,会改变原数组a.sort((pre, back) => pre - back)
升序从小到大a.sort((pre, back) => back - pre)
降序从大到小pre-back>0
前后数字交换;pre-back<0
前后数字不交换
array.every()
:检测所有元素是否满足一定条件,满足的话就返回 true,否则就返回 falsearray.every((item,index,self)=> item>0 )
array.some()
:如果有一个元素,满足条件就返回 true,否则的话返回 falsearray.some((item,index,self)=> item>0 )
array.find()
:方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefinedarray.find((item,index,self)=> item>0 )
array.filter()
:数组过滤,返回过滤之后的新数组。不会改变原数组。array.filter((item,index,self)=> item>0 )
array.forEach()
:遍历一次数组并执行回调,回调中 return 返回的值并没有意义,所以它没有返回值,(只是单纯的遍历数组同时执行一次回调函数)不会改变原数组。array.forEach((item,index,self)=> {...})
array.map()
:遍历一次数组并执行回调,回调中 return 返回的值会组成一个新数组作为返回值。不会改变原数组。array=[1,2,3]
array.map((item,index,self)=> {return item*2 })
- 最终返回的结果是一个新数组:
[2,4,6]
array.reduce()
:接收两个参数,第一个参数是回调,第二个参数是初始值array.reduce(callback,initialValue(可选))
callback=(pre,cur,index,self)=>{...return newValue}
- 首次进入如果有
initialValue
,则将initialValue
赋给pre
- 如果没有
initialValue
,则取数组的第一个数作为pre
- 然后执行回调时,都会将
return
后面的newValue
赋给pre
- 首次进入如果有
# 11】Object
Object构造函数中的私有方法:
Object.assign(target,...sources)
:方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。- 可用于深拷贝对象,合并数组。
Object.create(obj)
:方法创建一个新对象,使这个新对象的__proto__
指向obj
Object.freeze(obj)
:Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。(改变了传入对象,并返回修改后的对象)Object.defineProperty(obj, prop, descriptor)
:方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。const obj = {}; Object.defineProperty(obj, 'property', { get: function () { console.log('get!!'); return property }, set: function (value) { property = value; console.log('set!!') } }); obj.property = 20 // set!! let i=obj.property // get!! console.log(i) // 20 console.log(obj) // {} //注意:此时默认property是不可枚举属性,所以打印obj是{} //我们在浏览器上面打印对象,浅色的属性都是不可枚举属性,深色的属性才是可枚举属性。
Object.is(value1, value2)
:方法判断两个值是否为同一个值。console.log(+0 == -0)//true console.log(Object.is(+0, -0))//false console.log(Number.NaN == NaN)//false console.log(Object.is(Number.NaN, NaN))//true console.log(NaN==NaN)//false
Object.getPrototypeOf(obj)
:方法返回指定对象的原型(内部[[Prototype]]
属性的值Object.keys(obj)
、Object.values(obj)
、Object.entries(obj)
- 返回一个键名组成的数组、键值组成的数组、键值对组成的数组。
- 只能迭代出可枚举属性
enumerable:true
...
Object原型上的方法:(Object.prototype.method
)
obj.hasOwnProperty(prop)
:方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)obj.isPrototypeOf(object)
:方法用于测试一个对象是否存在于另一个对象的原型链上isPrototypeOf()
与instanceof运算符不同。在表达式object instanceof AFunction
中,object
的原型链是针对AFunction.prototype
进行检查的,而不是针对AFunction
本身。
obj.propertyIsEnumerable(prop)
:方法返回一个布尔值,表示指定的属性是否可枚举。obj.toString()
:方法返回一个表示该对象的字符串。- 像Array数组、Set集合、Map表、Number数字、String字符串等内置对象都是在该对象上面重写了
toString()
方法。
- 像Array数组、Set集合、Map表、Number数字、String字符串等内置对象都是在该对象上面重写了
# 12】String
String原型上的方法:(String.prototype.method
)
str.charAt(index)
:返回指定位置的字符。index
为必须参数,类型为number
(0
到str.length-1
之间,否则该方法返回 空串)- 另外:
str.charAt()
即不带参数和str.charAt(NaN)
均返回字符串的第一个字符
str.concat(str1,...,strn)
:用于拼接两个或多个字符串。功能和 “+” 拼接没啥两样。str.indexOf(searchStr,startIndex)
:和数组差不多。str.toUpperCase() / str.toLowerCase()
:用于字符串转换大小写。str.trim()
:用于删除字符串的头尾空格- 不会改变原始字符串。
str.split('')
:用于把一个字符串分割成字符串数组。str.slice()
:和数组一样。str.substring(index1,index2)
:方法用于提取字符串中介于两个指定下标之间的字符。- 参数都不支持负数(负数会被转成0)
index1=负数
和index2=负数
,则都会被转成0
str.substr(start,length)
:方法可在字符串中抽取从start
下标开始的指定数目的字符。lentgh=负数
,则直接返回空字符串start=负数
,则以最后一位为-1
,倒数第二位为-2
的顺序计算
# 13】Number
Number构造函数中的私有方法:
Number.parseInt(string[, radix])
: 依据指定基数 [ 参数 radix 的值],把字符串 [ 参数 string 的值] 解析成整数。Number.parseFloat(string)
:可以把一个字符串解析成浮点数。该方法与全局的parseFloat()
函数相同Number.isInteger(value)
:用来判断一个值是否为整数。- 注意
3.0
和3
是同一个数字,都是整型。
- 注意
Number原型上的方法:(Number.prototype.method
)
num.toString([radix])
:数字转换成字符串radix
参数(可选):指定要用于数字到字符串的转换的基数(从2到36)。如果未指定 radix 参数,则默认值为 10。
num.toFixed(number)
:方法使用定点表示法来格式化一个数值。- 参数:规定小数的位数,是 0 ~ 20 之间的值,包括 0 和 20,有些实现可以支持更大的数值范围。如果省略了该参数,将用 0 代替。
# 14】闭包
- 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包。
- 闭包可以让你从内部函数访问外部函数作用域。
- 每当函数被创建,就会在函数生成时生成闭包。
ECStack
执行环境栈:当我们浏览器加载页面的时候,它会形成一个执行环境,称为:ECStack
执行环境栈;目的是:让代码放进ECStack
中执行。它属于是栈内存(Stack)
EC执行上下文:在JS代码在浏览器跑起来(执行时)的时候,就会产生一个EC(GLOBAL)
全局执行上下文,此时会将这个全局执行上下文EC(GLOBAL)
,放到执行环境栈ECStack
中执行。
VO:Variable object
变量对象;作用:把执行上下文EC中创建的变量和变量的值,存储到[[Environment]]
中。
AO:activation object
活动对象;在函数上下文中,我们用活动对象(activation object, AO
)来表示变量对象;活动对象和变量对象其实是一个东西,区别在于活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化
function fn() {
let a = 1;
return function fo() {
console.log(a++)
}
}
let x = fn()//fn()执行后的返回值fo被引用到了全局变量x中
x();//1
x();//2
x();//3
//下面两种情况相当于创建了两个实例,
//当前面的函数执行完之后里面的变量和函数就被垃圾回收掉,
//所以不影响后面函数的执行依然打印的是a=1
fn()()//1
fn()()//1
fn()()//1
# 垃圾回收:
- 全局变量: 在页面关闭后结束
- 局部变量: 在执行的作用域块执行完成后结束
- 综上, 局部变量会在其函数块执行之后自动解除,对于引用类型的局部作用域其引用关系会自动解除,大多数的引用类型的全局变量需要手动解除引用关系
两种机制:标记清楚法(Chrome)、引用计数法(IE)
函数执行完,形成的执行上下文中,没有变量被上下文以外的内容占用,此上下文就会从执行环境栈中移除(释放),如果有被占用,则该上下文就被继续保留,压缩到执行环境栈底部。(闭包可能会导致内存泄漏,需要手动回收不用的变量)
# 常见面试题
var data = [];
for (var i = 0; i < 3; i++) {
//此时的for循环是全局的,i也是全局变量,执行完成后,全局变量i就变成了3
data[i] = function () {//当data[0]()执行时,此时函数的上下文中只有全局变量i=3
console.log(i);
};
}
data[0]();//3
data[1]();//3
data[2]();//3
var data = [];
for (var i = 0; i < 3; i++) {//此时这里i也是全局变量,执行完毕后也会变成3
data[i] = (function (i) {
return function(){//闭包,保存了一个匿名函数上下文(其中有一个变量i,相当于是一个快照)
console.log(i);//当data[0]()执行时,就会去找函数上下文作用域链上找变量i(i被一直引用,所以不会被垃圾回收掉)
}
})(i);
}
data[0]();//0
data[1]();//1
data[2]();//2
# 15】防抖
防抖:一定时间内重复触发会一直被推迟,直到过了规定延时时间,执行最后一次。
function debounce(fn, delay) {
var timer;
return function() {
clearTimeout(timer);//执行取消定时器,来实现
timer = setTimeout(function() {
fn();
}, delay);
}
}
clearTimeout(TimerId)
方法可取消由setTimeout()
方法设置的定时操作。
- 参数必须是由
setTimeout()
返回的 ID 值。
# 16】节流
节流:触发开始到规定延时时间结束,只执行一次。
function throttle(fun, delay) {
var timer = null;//通过timer去判断是否执行,并没有取消过定时器
return function() {
if (!timer) {
timer = setTimeout(function() {
fun();
timer = null;
}, delay);
}
}
}
# 17】创建对象方式
①new Object()
重复代码多
var person = new Object()
person.name = 'zhangsan'
person.say = function(){
alert(this.name)
}
②对象字面量
重复代码多
var person = {
name:'zhangsan',
say:function(){
alert(this.name)
}
}
③工厂模式
创建的对象原型链上只有有Object
function createPerson(name,say){
var o = new Object();
o.name=name;
o.say=say
return o
}
var person = createPerson('zhangsan',function(){
alert(this.name)
})
④构造函数模式
构造函数的作用是给实例添加各种属性,形成具有自己独有属性的实例。
function Person(name,say){
this.name=name;
this.say=say
}
var person = new Person('zhangsan',function(){
alert(this.name)
})
//Person原型对象上有个constructor属性
//Person构造函数里面又有一个prototype属性指向Person的原型对象
//此时Person构造函数和Person原型对象就形成了一个环状结构
⑤原型模式
所有创造出来的实例,都共享原型上的属性和方法(公有属性)
function Person(){
}
Person.prototype.name='zhangsan';
Person.prototype.say=function(){
alert(this.name)
}
⑥混合模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性
规定:
- 构造函数里面都只定义私有的属性和方法;
- 原型对象上面定义共享的属性和方法;
- 构造函数外直接给对象赋值和方法就是静态属性和方法
new关键字调用构造函数步骤:(new的过程)
- 创建一个新对象
- 将构造函数的作用域赋给新对象(将this指向这个新对象)
- 执行构造函数代码(为这个新对象添加属性)
- 返回新对象 (Person原型对象的指针赋给实例对象person)
function Person(name){
this.name=name
}
Person.prototype.say=function(){
alert(this.name)
}
# 18】原型链
JS中每个对象都是基于Object对象创建。
- 当原型链由下向上找属性的过程中,如果不同原型对象上具有相同的属性名的属性,此时如果我们需要用原型对象处于原型链更上层的那个属性,我们就不能省略
__proto__
,因为在原型链由下至上找的过程中,找的第一个对应的属性名就停止了。 - 实例对象访问一个属性的全过程(原型链):
- 1.先找自己本身对象空间中的属性,找到了停止;
- 2.如果没找到,使用
__proto__
找上一个原型对象,找到了停止; - 3.如果没有找到继续,继续沿
__proto__
,由下至上地找,直到找到为止; - 4.如果都没有对应的属性名,则返回
undefined
; - 注意:此过程
__proto__
可以省略。
# 19】继承
①原型继承
- 父类中私有和公有的属性方法,最后都会变为子类中公有的属性方法。
- 引用类型的属性被所有实例共享。
CHILD.prototype = new PARENT();
CHILD.prototype.constructor = CHILD;
缺陷:实例共享父类的私有属性
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
②call继承
- 父类私有的属性或方法 变为 子类私有的属性或方法;
- 只能继承父类私有的属性或方法;
CHILD方法中把PARENT当做普通函数来执行,
让PARENT中的this指向CHILD的实例,
相当于给CHILD的实例设置了很多PARENT的私有属性和方法。
避免了实例共享父类私有属性
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);//执行时,names属性被加在了this实例上面
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
③寄生组合继承
- 父类私有和公有的属性和方法分别是子类实例私有和公有的属性和方法。
function PARENT(x) {
this.x = x
}
PARENT.prototype.getX = function () {
console.log(this.x)
}
function CHILD(y) {
PARENT.call(this, 200)//保留call:继承构造函数中的私有属性或方法
this.y = y
}
//用于继承公用的属性或方法
CHILD.prototype = Object.create(PARENT.prototype)
//又使用原型链继承,此时是新创建一个Object实例,但是这个实例原型指向PARENT的原型
CHILD.prototype.constructor =CHILD
//再把重定向的到的constructor指回B
CHILD.prototype.getY = function () {
console.log(this.y)
}
④ES6的extends继承
- 原理就是寄生组合继承的语法糖。
//实现 父类公有的属性和方法 成为 子类公有的属性和方法
class CHILD extends PARENT {
//=>B.prototype.__proto__=A.prototype
constructor(){
super()//实现 父类私有的属性和方法 成为 子类私有的属性和方法
//=>A.call(this) 把父类当做普通方法执行,给方法传递参数,让方法中的this是子类的实例
}
//如果不写constructor(),浏览器会默认创建
//constructor(...args){ super(...args) }
}
# 20】this指向
其实所有的函数在调用执行的时候都是使用了.call
或者.bind
方法进行this
的绑定,只是分为了显式绑定和隐式绑定(默认绑定this)而已。
- 显示绑定
fn.call(asThis,1,2)
fn.bind(asThis,1,2)()
obj.method.call(obj,'hi')
- 隐式绑定:我们现在代码都是省略的写法,本来很久以前创建函数都是需要绑定this的,但是js设计者为了让代码看起来跟简约,就约定了简便的写法
- 对于普通函数
fn(1,2)//等同于fn.call(undefined,1,2)
- 对于对象中的方法
obj.method('hi')//等同于obj.method.call(obj,'hi')
- 对于数组
array[0]('hi')//等同于array[0].call(array,'hi')
- 对于普通函数
# 记住以下规则:
this
是call
的第一个参数new
重新设计了this
- 箭头函数不接受
this
# 21】深拷贝
①通过JSON的方式实现
- 它是不可以拷贝
undefined
,function
,RegExp
等等类型的
function deepClone(value){
return JSON.parse(JSON.stringify(value))
}
②通过Object.assign(target, source)
的方式实现
- 它不可以深拷贝嵌套对象。
function deepClone(value){
return Object.assign({},value)
}
③使用递归的方式实现(推荐)
- 完美深拷贝
// 定义一个深拷贝函数 接收目标value参数
function deepClone(value) {
// 定义一个变量
let result;
if (typeof value === 'object') { // 如果当前需要深拷贝的是一个对象的话
if (Object.prototype.toString.call(value) === '[object Array]') { // 如果是一个数组的话
result = [];
for (let i in value) {
result.push(deepClone(value[i]));
}
} else if (value === null) {// 判断如果当前的值是null的话;直接赋值为null
result = null;
}
else if (value.constructor === RegExp) {// 判断如果当前的值是一个RegExp对象的话,直接赋值
result = value;
} else {// 否则是普通对象,直接for in循环,递归赋值对象的所有值
result = {};
for (let i in value) {
result[i] = deepClone(value[i]);
}
}
} else {// 如果不是对象的话,就是基本数据类型,那么直接赋值
result = value;
}
return result;// 返回最终结果
}
# 22】aop编程
# 把业务逻辑抽离出来:
function perform(anyMethod, wrappers) {
return function () {
wrappers.forEach(wrapper => wrapper.initialize());
anyMethod();
wrappers.forEach(wrapper => wrapper.close());
}
}
let Func = perform(function () {
console.log('say');
}, [{//wrapper1
initialize: function () {
console.log('wrapper1 beforeSay');
},
close: function () {
console.log('wrapper1 close');
}
}, {//wrapper2
initialize: function () {
console.log('wrapper2 beforeSay');
},
close: function () {
console.log('wrapper2 close');
}
}])
Func()
# 使用生成器函数:
let arr1 = [1, 2]
let arr2 = [10, 20]
function* chain(params) {
yield 'before'
for (let arr of params) {
yield* arr
}
yield 'after'
}
for (let num of chain([arr1, arr2])) {
console.log(num)
}
//before
//1
//2
//10
//20
//after
# before:修改原型实现
function say(who,sth){
console.log(who + '说话时' + sth)
}
Function.prototype.before=function (beforeFunc){
let that=this//也可以使用下面箭头函数,因为箭头函数没有this,没有arguments,没有原型;这里使用that变量不被垃圾回收掉。
return function(){
beforeFunc()
that(...arguments)//如果是this的话,就需要看调用时的上下文,此时使用闭包,保留that变量在函数外也可以使用。
}
}
let newFn=say.before(function (){
console.log('说话前')
})
newFn('我','大笑');//如果是this的话,需要看调用时的上下文。
//使用ES6箭头函数:
Function.prototype.before=function (beforeFunc){
return (...args)=>{
beforeFunc()
this(...args)//箭头函数不支持arguments,而且不支持this,此时这个this是上一个函数的this传进箭头函数中
}
}
# after:计数器控制执行
function after(times, callback) {//闭包
let index=0;
return function () {//闭包将times变量一直存在栈中
index++;
if (times == index) {
callback();
}
}
}
let fn = after(3, function () {
console.log('函数已执行');
})
fn()
fn()
fn()
//函数已执行
# 23】函数柯里化
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
//var _args = Array.prototype.slice.call(arguments);
var _args = Array.from(arguments);
return function cur() {
//将下一个函数的参数加入数组中
_args.push(...arguments);
cur.toString = function () {//写一个静态方法,在最后的时候调用执行逻辑
return _args.reduce(function (a, b) {
return a + b;
});
}
return cur//递归返回当前函数
};
}
console.log(add(1)(2)(3).toString());//6
console.log(add(1)(2)(3)(4)(5).toString())//15
console.log(add(2, 6)(1).toString())//9
console.log(add(1)(2, 3)(4).toString())//10
# 24】数组去重
# ①集合Set
Array.from(new Set([1,3,3]))//[1,3]
[...new Set([1,3,3])]//[1,3]
Array.prototype.slice.call(new Set([1,3,3]))//直接报错,不能转换Set和Map
# ②reduce去重includes或者indexOf
function unique(arr) {
return arr.reduce((pre, cur) => {
//if (pre.indexOf(cur) === -1) {
if (!pre.includes(cur)) {
pre.push(cur)
}
return pre
}, [])
}
# ③filter和indexOf
function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item) === index;
});
}
# ④哈希字典obj
function unique(arr) {
var arrry = [];
var obj = {};
for (var i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
obj[arr[i]] = '已有值'
arrry.push(arr[i])
}
}
return arrry;
}
# 25】发布订阅
- 首先要想好谁是发布者(比如上面的卖家)。
- 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如上面的买家收藏了卖家的店铺,卖家通过收藏了该店铺的一个列表名单)。
- 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
let event = {
_arr:[],
subscribe(fn){//订阅者订阅消息
this._arr.push(fn)
},
publish(){//发布者发布消息发布
this._arr.forEach(fn=>fn())
}
}
subscriber---订阅push--->EventArr---发布遍历--->publisher
# 26】拖拽功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<body style="margin: 0;">
<img id='dv' src="https://picsum.photos/200/300" />
</body>
<script>
//获取元素
var dv = document.getElementById('dv');
var x = 0, y = 0, l = 0, t = 0;
var isDown = false;
//鼠标按下事件
dv.onmousedown = function (e) {
//获取x坐标和y坐标
x = e.clientX;
y = e.clientY;
//获取左部和顶部的偏移量
l = dv.offsetLeft;
t = dv.offsetTop;
//开关打开
isDown = true;
//设置鼠标样式
dv.style.cursor = 'move';
}
//鼠标移动
window.onmousemove = function (e) {
e.preventDefault()
if (isDown == false) {
return;
}
//获取x和y
var nx = e.clientX;
var ny = e.clientY;
//计算移动后的左偏移量和顶部的偏移量
var nl = nx - (x - l);
var nt = ny - (y - t);
dv.style.left = nl + 'px';
dv.style.top = nt + 'px';
}
//鼠标抬起事件
dv.onmouseup = function () {
//开关关闭
isDown = false;
//还原鼠标样式
dv.style.cursor = 'default';
}
</script>
</html>
# 27】无缝轮播
在第一个张图前面插入最后一张图,最后一张图后面插入第一张图来实现。
如图所示,当鼠标点击轮播视口Box的左按钮,即向左移动画,动画执行完毕,立即向将定位切为最后一张图的位置。(轮播动画是由装全部图片的slider进行绝对定位来控制)
# 28】解构赋值
# 29】commonJS和ESmodules
- commonJS模块输入的是一个值的拷贝,ES6模块输出的是值的引用(所以重新对其赋值)。
- commonJS模块是运行时加载(同步),ES6模块是编译时输出接口(异步)。
- 涉及相关问题:
- webpack中的webpack_require对他们处理方式不同
- webpack的按需加载实现
//commonJS导出一个对象
module.exports={ name:name,age:age }
let xx=require('./index')
//ES modules导出一个对象
export { name:name,age:age }
import { name,age } from './index'//不能任意命名,只能解构赋值
export default { name:name,age:age }
import xx from './index'//导入的对象可以任意取名
commonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化不会影响到这个值。
// common.js
var count = 1;
var printCount = () =>{
return ++count;
}
module.exports = {
printCount: printCount,
count: count
};
// index.js
let v = require('./common');
//commonJS是值的拷贝,可以进行修改
console.log(v.count); // 1
console.log(v.printCount()); // 2
console.log(v.count); // 1
//改写
module.exports = {
printCount: printCount,
get count(){
return count
}
};
//依次打印1 2 2
在ES modules中使用export和import就可以直接实现。ES6模块是动态引用,并且不会缓存,模块里面的便令绑定其所在的模块,而是动态地去加载值,并且不能对其重新赋值
// es6.js
export let count = 1;
export function printCount() {
++count;
}
// main1.js
import { count, printCount } from './es6';
console.log(count)//1
console.log(printCount());//2
console.log(count)//2
总结:
- commonjs:导出的基本数据类型是值的拷贝,对象还是引用。同步加载(需要把require的执行加载之后才能继续执行)「运行时」
- esmodule:导出的全是引用。异步加载「编译时」
# 30】懒加载和预加载
懒加载(延迟加载)通过img标签的src属性和监听屏幕滚动事件来实现
预加载(提前加载)通过image对象的设置url提前加载图片,再通过onload设置回调将其挂载到dom上。
# 31】正则匹配URL
/(http|https)\:\/\/www\.(.*?)\.(com|cn|org)/
'xxxxxxxxxhttp://www.baidu.comxxxxxx'.match(/(http|https)\:\/\/www\.(.*?)\.(com|cn|org)/)
//http://www.baidu.com
# 32】数组扁平化
数组扁平化是指将一个多维数组变为一维数组。
[1, [2, 3, [4, 5]]] ------> [1, 2, 3, 4, 5]
①es6中的flat函数也可以实现数组的扁平化
let arr1 = [1,2,['a','b',['中','文',[1,2,3,[11,21,31]]]],3];
console.log( arr1.flat( Infinity ) );
//[ 1, 2, 3, 4, 5 ]
②toString & split
function flatten(arr) {
return arr.toString().split(',').map(function(item) {
return Number(item);
})
}
③join & split
function flatten(arr) {
return arr.join(',').split(',').map(function(item) {
return parseInt(item);
})
}
④reduce & 递归
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
⑤扩展运算符
[].concat(...[1, 2, 3, [4, 5]]); // [1, 2, 3, 4, 5]
⑥递归
function flatten(arr) {
var res = [];
arr.map(item => {
if(Array.isArray(item)) {
res = res.concat(flatten(item));
} else {
res.push(item);
}
});
return res;
}
⑦yield* & 递归
function* iterTree(tree) {
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
let arr1 = [1, 2, ['a', 'b', ['中', '文', [1, 2, 3, [11, 21, 31]]]], 3];
let arr =[]
for (const x of iterTree(arr1)) {
arr.push(x)
}
console.log(arr);
//[1,2,'a','b','中','文',1,2,3,11,21,31,3]
⑧arr.some()
& 循环
function iterTree2(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(iterTree2(arr1));
//[1,2,'a','b','中','文',1,2,3,11,21,31,3]
# 33】IntersectionObserver API
判断某个元素是否进入了"视口",即用户能不能看到它。(顾名思义:元素与视口交叉区观察)
传统实现方法:监听scroll事件后,调用目标元素的getBoundingClientRect()方法,获得目标元素对于视口左上角的坐标,再去判断是否在视口之内。这种方法的缺点是,由于scroll
事件密集发生,计算量很大,容易造成性能问题,也可以使用防抖节流的方式去优化。
用法:
- 接受两个参数:
callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。- Option参数里面的
threshold
属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0]
,即交叉比例(intersectionRatio
)达到0
时触发回调函数。用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
- Option参数里面的
var io = new IntersectionObserver(callback, option);
//callback参数返回IntersectionObserverEntry对象来描述观察
- 构造函数的返回值是一个观察器实例。实例的
observe
方法可以指定观察哪个 DOM 节点。
var io = new IntersectionObserver(entries=>console.log(entries));
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
# 实现懒加载:
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var observer = new IntersectionObserver(
changes => {
changes.forEach(item => {
var container = item.target;
var content = container.querySelector('template').content;
container.appendChild(content);//添加dom
observer.unobserve(container);//停止观察此项目,执行加载完毕
});
}
);
//观察懒加载的所有项目
query('.lazy-loaded').forEach(item => {observer.observe(item)});
# 实现无限滚动:
var io = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
});
// 开始观察
io.observe(
document.querySelector('.scrollerFooter')
);
# 34】for in 循环bug
for in循环只会迭代可枚举属性,会遍历原型链。
var obj = {
a:1,
b:2,
c:3
}
obj.__proto__.d=4
for (let key in obj){
//if(obj.hasOwnProperty(key))//阻止遍历原型属性
console.log(key)//a,b,c,d
}
console.log(Object.keys(obj))//a,b,c
# 35】可选链操作符"?."
在实际开发中,常常出现下面几种报错情况:
// 1. 对象中不存在指定属性
const leo = {};
console.log(leo.name.toString());
// Uncaught TypeError: Cannot read property 'toString' of undefined
// 2. 使用不存在的 DOM 节点属性
const dom = document.getElementById("dom").innerHTML;
// Uncaught TypeError: Cannot read property 'innerHTML' of null
在可选链 ?.
出现之前,我们会使用短路操作 &&
运算符来解决该问题:
const leo = {};
console.log(leo && leo.name && leo.name.toString()); // undefined
# 可选链?.
是指即使中间的属性不存在,也不会出现错误。如果可选链 ?.
前面部分是 undefined
或者 null
,它会停止运算并返回 undefined
。
语法:
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
# 注意:
- 不能过度使用可选链(易导致bug)
- 可选链
?.
之前的变量必须已声明 - 可选链不能用于赋值
- 可选链访问数组元素的方法(
arr?.[0]
) - 可选链可以访问对象的方法(
obj.fun?.()
)
// 1. 对象中不存在指定属性
const leo = {};
console.log(leo?.name?.toString());
// undefined
console.log(leo.name?.toString()); //leo必须存在
// undefined
// 2. 使用不存在的 DOM 节点属性
const dom = document?.getElementById("dom")?.innerHTML;
// undefined
const dom = document.getElementById("dom")?.innerHTML; //document必须存在
// undefined
# 36】手写call、apply、bind
# 通过call实现bind
Function.prototype.newBind = function (...arr) {
let that = this
return function () {
that.call(...arr)
}
}
obj = {
a: 1
}
function say(a, b, c) {
console.log(this)
console.log(a, b, c)
}
say.newBind(obj, 1, 2, 3)()
//{ a: 1 }
//1 2 3
# 手写call
//通过对象调用方法来修改this指向
Function.prototype.call2 = function (context){
const ctx = context || window
ctx.func = this //调用call2时,this是调用call2的方法(call2是在函数对象上)
//测试用例中这个this打印出来是:[function a]
const args = Array.from(arguments).slice(1)//保存参数
//通过在ctx中新建一个 函数对象等于调用时的对象 来调用执行来修改this指向
const res = arguments.length > 1 ? ctx.func(...args) : ctx.func()
delete ctx.func//避免造成全局污染
return res
}
//测试用例:
obj={c:2}
function a(x,y){console.log(this,x,y)}
a.call(obj,1,2)//{c:2} 1 2
a.call2(obj,1,2)//{c:2,func:[function a]} 1 2
# 手写apply
Function.prototype.apply2 = function (context){
const ctx = context || window
ctx.func = this
//测试用例中这个this打印出来是:[function a]
//通过在ctx中新建一个 函数对象等于调用时的对象 来调用执行来修改this指向
const res = arguments[1] ? ctx.func(...arguments[1]) : ctx.func()
delete ctx.func//避免造成全局污染
return res
}
//测试用例:
obj={c:2}
function a(x,y){console.log(this,x,y)}
a.call(obj,[1,2])//{c:2} 1 2
a.call2(obj,[1,2])//{c:2,func:[function a]} 1 2
# 手写bind
Function.prototype.bind2 = function (context) {
//对context进行深拷贝,防止bind执行后返回函数未执行期间,context被修改
const ctx = JSON.parse(JSON.stringify(context)) || window
ctx.func = this
const args = Array.from(arguments).slice(1)
return function () {
//注意bind方法需要合并两次函数执行的参数
const Allargs = args.concat(Array.from(arguments))
return Allargs.length > 0 ? ctx.func(...Allargs) : ctx.func()
}
}
//测试
obj = { c: 2 }
function a(x,y,z) { console.log(this, x, y, z) }
a.bind(obj,1,2)(3)//{c:2} 1 2 3
a.bind2(obj,1,2)(3)//{c:2,func:[function a]} 1 2 3
# 37】babel
babel:需要脚手架工具转行代码(类似于打包)。.babelrl文件进行配置需要用到的presets和plugin
「Plugin 会运行在 Preset 之前。
Plugin 会从前到后顺序执行。
Preset 的顺序则 刚好相反(从后向前)。」
# 计算机网络
# 1】OSI七层模型与TCP/IP五层模型
OSI七层模型:
- 应用层
- 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
TCP/IP五层模型:
- 应用层
- 传输层
- 网络层
- 数据链路层
- 物理层
# 2】HTTP
注意:http是一种无状态连接。
http1.0是非持久连接:一次HTTP请求和响应就创建和关闭一次TCP连接,严重影响性能。所以只能串行执行,上一次请求发出并收到响应之后,才能继续发送下一次请求。纯文本形式传输。
http1.1是持久连接:在一个TCP连接上可以传输多个HTTP请求和响应。创建多个TCP连接可以并行发送很多过请求,收到响应是有序的。---和all方法差不多(最多并行6个TCP连接,因为容易造成DDoS攻击,给服务器压力)纯文本形式传输。
- 请求发送时请求头加上
Connection:keep-alive
来保持tcp一直连接,不要关闭。Connection:close
客户端通知服务器关闭tcp连接。 - 相较于1.0版本,http1.1请求头多了host字段,可以在同一个IP地址和端口上使用不同的主机名创建多个虚拟web站点。
- 一个页面的请求与响应就只建立一次tcp连接,但不能并行传输http请求和响应。(也可以最多建立6个tcp连接)
http2.0更快、额外的流量花销更少
- header compression(头部压缩)初次请求后将后面请求的头部信息进行压缩。
- binary framing layer(二进制分帧层)将多个请求并行发送给服务器,并且返回的结果根据streamId进行组合形成响应一次性返回给浏览器。通过二进制流的方式实现无差错。
- 位于应用层和表示层之间
- 一次TCP连接就可以实现并行传输HTTP请求和响应
http1.0:每个tcp中只能串行http请求,且每次请求开辟一个tcp连接
http1.1:每个tcp中也是只能串行http请求,但是可以keep-alive,同时最多允许开辟6个tcp连接,来实现并行请求
http2.0:每个tcp中就可以通过二进制分帧层中streamID组合来实现并行http请求
# 状态码:
2xx表示请求成功
200:成功返回,正常抓取
204:成功处理了请求,但没有返回任何内容
206:部分请求成功,
例子:①浏览器先不下载要下载的文件,而是弹窗提醒用户。
②一些流媒体技术比如在线视频,可以边看边下载。就是使用206来实现的。
③使用206状态码来实现断点续传
3xx表示重定向
301:请求的网站*永久*转移到新的URL,搜索引擎访问时,会收录新的URL
302:请求的网页*临时*转移到新的URL,搜索引擎会保留原来的URL
304:被访问网页没有更新,不用再抓取,节省搜索引擎带宽(缓存)。浏览器上的缓存是最新的资源,需要浏览器使用缓存,不需要进一步抓取。
客户端有缓冲的文档并发出了一个条件性的请求。服务器告诉客户端,原来缓冲的文档还可以继续使用。
4xx表示请求出错
400:请求语法错误
401:用户没有访问权限,需要进行身份认证
403:请求被服务器拒绝
404:未找到资源
410:请求资源永久删除,和404类似,这种页面应当用301永久重定向到新位置
5xx表示服务器内部错误(一般是后端程序的问题)
500:内部服务器错误(后端程序报错)
503:服务器无法使用,并发访问高,通常只是暂时的
505:表示Web服务器不支持此HTTP协议的版本
# 3】TCP三次握手四次挥手
TCP/IP协议栈从下至上分层结构:物理层,网络层(IP),传输层(TCP、UDP),应用层
TCP特点:面向连接,可靠,保证数据是正确稳定可靠无差错的传输。
UDP特点:无连接,尽可能交付,效率高。
TCP三次握手:标志位ACK,SYN / 确认号ack / 序列号seq
四次分手:标志位FIN,ACK / 确认号ack / 序列号seq
Client Server
①---SYN=1---seq=x--------------------->
<---SYN=1---ACK=1---seq=y---ack=x+1---②
③---ACK=1---ack=y+1---seq=x+1--------->
(客户端和服务端相互确认通信没问题,同时*开启资源*)
开启资源<----数据传输---资源交换---->开启资源
①---FIN=1---seq=u--------------------->
<---ACK=1---seq=v---ack=u+1-----------②
.....期间还是可以数据传输.....
<---FIN=1---ACK=1---seq=w---ack=u+1---③
④---ACK=1---seq=u+1---ack=w+1--------->
(客户端先通知服务端关闭连接,服务端收到即回复)
(服务端准备好了才通知客户端关闭连接,服务端收到即回复)
四次挥手的目的是:让对方的*资源不能随意释放*,可靠传输。
(挥手可以看成离婚,②~③之间可以看成冷静期)
SOCKET套接字:ip:port(local)---ip:prot(foreign)
必须是四元组的唯一标识。
可靠传输的实现:以字节为单位的滑动窗口
- 接收窗口和发送窗口的大小不一样大
- 缓存处理不按序到达的数据
- 接收方必须有累积确认和捎带确认机制(接收方不应过分推迟发送确定,会导致超时重传)
- TCP是全双工通信,双方可同时接收发送数据
# 4】HTTPS
明文传输:在HTTP中我们使用的是明文传输,黑客可以随时截取传输的数据,显然这并不安全。
对称加密:客户端和服务器使用同种方式对数据进行加密和解密,黑客截取数据依然可以使用这种的方式进行解密。(因为服务器不可能对每个客户端都储存一种加密的方式)
非对称加密:(无法实现服务器对客户端返回数据的加密和解密)
- 私钥只有客户端才有,公钥都可以拿到;
- (公钥加密,私钥解密)公钥对传输数据进行加密,只有对应的私钥才能将其解密;
- (私钥签名,公钥验证)使用私钥加密过的数据,可以用公钥进行验证是否被对应私钥加密过。
非对称加密来加密密钥,对称加密来加密通信数据。
- 可以解决上面两种加密的弊端
- 先利用非对称加密的方式浏览器向服务器来协商传输数据时的需要用的密钥,然后再用这个密钥来加密之后要传输的数据。
- 但是依然有安全问题(中间人代理拦截攻击问题)
第三方权威机构认证服务器安全(解决中间人拦截攻击问题)
- CA的公钥内置在浏览器中
- 服务器向CA付费,用CA的私钥加密服务器的公钥(进行签发证书),浏览器向服务器请求后,服务器返回给浏览器签发的证书,浏览器使用内置的CA的公钥进行验证(证书是不是CA签发的)(原理是:非对称加密中私钥加密,公钥验证)
- 之后再进行非对称加密配合对称加密进行传输数据,完美了
通过非对称加密+对称加密+GlobalSign CA认证才是最安全的。
打比分:有两个盒子及对应的锁,一个盒子锁着通信数据(对称加密),另一个盒子锁着上个盒子的钥匙(非对称哈希加密)。浏览器来创建对称加密密钥。
# 5】Cookie、Session、Token
Cookie:cookie数据存放在客户的浏览器上,由服务器端来创建(服务器把“JSESSIONID”作为cookie的name,SessionId的值作为cookie的value),每次浏览器请求时都把这个cookie放在请求头里面
- HTTP响应通过 Set-Cookie 设置 Cookie
- 浏览器访问指定域名是必须带上 Cookie 作为 Request Header
- Cookie 一般用来记录用户信息
Session:session数据存放在服务器上,服务器对Session列表是以Map的形式来管理,Map的key是SessionId,value是Session对象的引用。
- Session 是服务器端的内存(数据)
- Session 一般通过在 Cookie 里记录 SessionID 实现
- SessionID 一般是随机数
Token:不用像SessionId一样保存在服务器上,是通过哈希算法程序计算得到(用时间换取空间),使用过期时间来解决防止token被盗带来的隐患,设置token较短的过期时间,然后重新进行生产token,使其变得动态。userId+哈希算法+密钥=签名token
LocalStorage 和 Cookie 的区别是什么?
- Cookie 会随请求被发到服务器上,而 LocalStorage 不会
- Cookie 大小一般4k以下,LocalStorage 一般5Mb 左右
# 6】CORS跨域问题
浏览器同源策略:协议,端口,主机都相同才同源。
- JSONP:script和img标签中的src属性不限制跨域请求,JSONP只能发GET请求。
- Form:标签提交不限制跨域请求。
- CORS:(
Cross-origin resource sharing
)标准解决方案跨域资源共享,分为简单请求和非简单请求。 - 反向代理proxy:使用代理请求转发实现跨域请求。
- iframe:页面嵌套,使用ifrmae跨域要满足一个基本条件,父页面和子页面都是自己可以控制的,如果随便把iframe指向一个其他网站,想通过跨域手段操作它基本上是不可能的。
- document.domain:实现最简单但只能用于同一个主域下不同子域之间的跨域请求
- 父页面通过
ifr.contentWindow
就可以访问子页面的window,子页面通过parent.window
或parent
访问父页面的window,接下来可以进一步获取dom和js
- 父页面通过
- postMessage:HTML5新增方法
postMessage(message, targetOrigin)
(在父页面中使用)- message: 是要发送的消息,类型为 String、Object (IE8、9 不支持) targetOrigin: 是限定消息接收范围,不限制请使用 '*'
window.addEventListener('message', function(e))
(子页面中监听message事件)- 回调函数第一个参数接收 Event 对象,有三个常用属性:(在子页面中传入回调)
- data: 消息 origin: 消息来源地址 source: 源 DOMWindow 对象
- document.domain:实现最简单但只能用于同一个主域下不同子域之间的跨域请求
CORS跨域资源共享:
完全依靠服务器端来操作,浏览器端只需要设置withCredentials
属性携带cookie限制。(注意cookie在CORS中依然是同源的)
简单请求:这是为了兼容Form表单发出跨域请求,触发条件:HEAD
、GET
、POST
请求且头信息不超过Accept
、Accept-Language
、Content-Language
、Last-Event-ID
、且Content-Type
的值只限于以下三个值application/x-www-form-urlencoded
(不包含文件上传的post请求的两种编码格式)、multipart/form-data
(包含文件上传的post请求的两种编码格式)、text/plain
(纯文本)
# cors会有两次请求:
简单请求不会触发 CORS 预检请求。
“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
# 总结:
- 发现是跨域请求,浏览器自动请求头添加
Origin
字段,用于后端服务器验证。 - 服务器都会正常返回响应,状态码无法判断是否跨域请求成功,需要浏览器自动判断服务器返回的响应头信息中,是否有
Access-Control-Allow
字段。 - 如果需要CORS请求中使用Cookie,需要前端和后端同时设置
Credentials
,前端才会正常被设置cookie和发送cookie,后端才会正常给前端设置cookie和接收cookie。 - 如果要发送Cookie,
Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。 - Cookie依然遵循同源策略,只有用本服务器域名设置的Cookie才会被浏览器发送。
# 7】service worker
一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。
# 8】HTTP缓存
强缓存:Expires和Pragma、Cache-control:max-age=xxx
协商缓存:Last-Modified/If-Modified-Since、Etag/If-None-Match
# 强缓存:
Expires
策略设置过期时间,是http1.0老版本的,
Pragma
:设置页面是否缓存,为Pragma
则缓存,no-cache
则不缓存,当该字段值为no-cache
的时候,会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。(是http1.0老版本推出)
Cache-control
策略是http1.1最新的。
- 相较于
Expires
选择更多,设置更细致; - 如果同时设置优先级高于
Expires;
# Cache-control
Cache-directive | 说明 |
---|---|
public | 所有内容都将被缓存(客户端和代理服务器都可缓存) |
private | 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存) |
no-cache | 必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。 |
no-store | 所有内容都不会被缓存到缓存或 Internet 临时文件中 |
must-revalidation/proxy-revalidation | 如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证 |
max-age=xxx (xxx is numeric) | 缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高 |
# max-age最为重要设置过期时间
var maxAgeTime = 60 //过期时间
res.writeHead(200, {
"Cache-Control": 'max-age=' + maxAgeTime
})
那么在60s内,如果再去请求这个文件的话,是不会发起请求的。因为还没有过期呢!唯一例外是如果这个文件是你在浏览器地址栏输入的地址来请求的(比如你请求localhost:3030/static/style.css),当你刷新的时候就会让当前的这个文件所设定的过期时间失效,直接去请求服务器来看是返回个304还是返回新文件。一般这么请求的都是我们常说的入口文件,入口文件一刷新就会重新向服务器请求,但是入口文件里面所引入的文件如js,css等不会随着刷新而另过期时间失效。除非你单找出来那个引入链接,通过浏览器地址栏去查询它并刷新 :)。
# Cache-Control
的值有很多:
常用的有max-age
,no-cache
和no-store
。
max-age
是资源从响应开始计时的最大新鲜时间,一般响应中还会出现age
标明这个资源当前的新鲜程度。
no-cache
会让浏览器缓存这个文件到本地但是不用,Network中disable-cache
勾中的话就会在请求时带上这个header,会在下一次新鲜度验证通过后使用这个缓存。
no-store
会完全放弃缓存这个文件。
服务器响应时的Cache-Control
略有不同,其中有两个需要注意下:
- public, public 表明这个请求可以被任何对象缓存,代理/CDN等中间商。
- private,private 表明这个请求只能被终端缓存,不允许代理或者CDN等中间商缓存。
Expires
是一个具体的日期,到了那个日期就会让这个缓存失活,优先级较低,存在max-age
的情况下会被忽略,和本地时间绑定,修改本地时间可以绕过。
另外,如果你的服务器的返回内容中不存在Expires
,Cache-Control: max-age
,或 Cache-Control:s-maxage
但是存在Last-Modified
时,那么浏览器默认会采用一个启发式的算法,即启发式缓存。通常会取响应头的Date_value \- Last-Modified_value
值的10%作为缓存时间,之后浏览器仍然会按强缓存来对待这个资源一段时间,如果你不想要缓存的话务必确保有no-cache
或no-store
在响应头中。
# 协商缓存:
常用的方式为Etag和Last-Modified
# ①Last-Modified/If-Modified-Since
Last-Modified方式需要用到两个字段:Last-Modified & if-modified-since。 先来看下这两个字段的形式:
- Last-Modified : Fri , 12 May 2006 18:53:33 GMT
- If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT
当第一次请求某一个文件的时候,就会传递回来一个 Last-Modified 字段,其内容是这个文件的修改时间。当这个文件缓存过期,浏览器又向服务器请求这个文件的时候,会自动带一个请求头字段 If-Modified-Since ,其值是上一次传递过来的 Last-Modified 的值,拿这个值去和服务器中现在这个文件的最后修改时间做对比,如果相等,那么就不会重新拉取这个文件了,返回 304 让浏览器读过期缓存。如果不相等就重新拉取。
浏览器--------------------第一次请求---------------------->服务器
<---响应Cache-Control:max-age=60;last-modified:[文件修改时间]---
---max-age时间过了;If-Modified-Since:[浏览器缓存中的文件修改时间]--->
<------------服务器去对比'文件修改时间'决定是否返304-----------------
# ②Etag/If-none-match
这两个值是由服务器生成的每个资源的唯一标识符,只有资源变化的时候这个值就会改变,与last-Modified区别是,当服务器返回304 Not-Modified的时候由于Etag重新生成过,response还是会把Etag返回,即使Etag和之前的相比没有变化。
# Etag和last-Modified比较
Etag的出现就是为了解决last-Modified不能解决的问题:
1、有的文件修改过于频繁,而last-Modified只能精确到秒。
2、一些文件周期性改变的文件内容并没有改变,只是时间改变了,此时我们并不希望服务器认为内容变化了,重新去get。
3、(服务器的问题)有些服务器不能精确到文件的具体最后的修改时间。(比如一串字符串不能正确识别,因为last-modified是绝对时间格式)
# 9】XSS跨站脚本攻击
XSS(cross site Scripting)存在的原因?
- 对get请求url中的参数或者用户提交输入的地方没有做充分的过滤。
- 攻击者将脚本代码提交到服务器,当其他用户访问web网页的时候,被执行攻击者的脚本代码。
防范:
- 对输入框、url参数进行过滤。
- 前端输入框进行正则表达式拦截,后端也进行过滤拦截
- 对网页脚本脚本进行编码。
- 对需要展示的html标签进行转义处理
- Cookie设置http-only。
- 如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。
# 10】CSRF攻击
CSRF(Cross-site request forgery)跨站请求伪造?
用户未退出关闭正常站点,同时打开黑客的站点,黑客站点向正常站点请求,此时会携带正常站点的cookie一起发送,造成csrf。
- src属性的get请求可以跨域携带cookie,实施攻击
- from表单中的post请求也可以跨域携带cookie,实施攻击
防范:
- 输入验证码(确保是用户行为,非黑客行为)
- 尽量使用post
- 同源检测 HTTP referer header(会记录当前请求的来源地址,后端进行判断)
- Anti CSRF Token
- token一直都在变化,通过计算生成。
- 双重cookie验证
# 11】token储存在哪
关于 token 的存储问题
JWT:
- csrf 攻击无法获取第三方的 cookie,而是直接使用cookie进行查询的时候会自动携带 cookie。(跨不跨域都可以进行csrf攻击,因为src或者form可以跨域发出请求)
- xss攻击通过代码注入可以获取 cookie。需要设置转义。
方式一、客户端使用cookie直接认证,需要设置cookie为httpOnly,可以防止 xss攻击。但是无法防止csrf攻击。需要设置伪随机数X-CSRF-TOKEN。(推荐!不需要处理xss,并且csrf随机数有完善的应用机制)
方式二、 客户端使用 auth授权头认证,token存储在 cookie中,需要防止xss攻击。可以防止 csrf攻击,因为 csrf只能在请求中携带 cookie,而这里必须从 cookie中拿出相应的值并放到 authorization 头中。实际上cookie不能跨站(同源政策)被取出,因此可以避免 csrf 攻击。(适用于 ajax请求或者 api请求,可以方便的设置 auth头)
方式三、可以将token存储在localstorage里面,需要防止xss攻击。实现方式可以在一个统一的地方复写请求头,让每次请求都在header中带上这个token, 当token失效的时候,后端肯定会返回401,这个时候在你可以在前端代码中操作返回登陆页面,清除localstorage中的token。(适用于 ajax请求或者 api请求,可以方便的存入 localstorage)(小心cors非简单请求预检请求,设置)
设置HTTPS,可以防止提交时的用户名或者密码被拦截或读取。
# 12】cookie跨域访问怎么解决?
①Nginx进行代理解决cookie跨域访问
- 正向代理:正向代理隐藏真实客户端(是客户端域名=服务端域名)
- 反向代理:反向代理隐藏真实服务端(使服务端域名=客户端域名)
因为cookie存在跨域问题,其中一个解决方法是,设置Nginx代理服务器,将两个服务器域名统一到一个反向代理服务器。
②解决顶级域名与二级域名之间的cookie跨域问题:设置domain
#通过设置domain
#顶级域名服务器与二级域名服务器之间哪个设置都能生效
#设置完毕后写回到客户端,用另一个服务器即可访问此Cookie
cookie.setDomain("test.com");
# 13】JSONP跨域
前端代码
<script src='http://localhost:3000/jsonp?name=leihao&age=20&callback=((res) => {
console.log(res)
})'></script>
后端服务器
router.get('/jsonp', async (ctx, next) => {
const { name, age, callback } = ctx.query
ctx.body = `${callback}(${JSON.stringify({ name, age })})`
})
启动服务,访问html页面,前端控制台会打印{name: "leihao", age: "20"}
这是抓到的接口结果:((res) => { console.log(res)})({"name":"leihao","age":"20"})
,因为是script中的src去请求的字符串,请求到资源之后相当于会自动去执行eval(((res) => { console.log(res)})({"name":"leihao","age":"20"}))
,差不多就是这意思
# VUE
# 1】生命周期
beforeCreate
:初始化事件&生命周期
created
:初始化依赖注入&校验
beforeMount
:el
选项template
转换成render()
函数,diff算法生产VDOM
mounted
:将VDOM渲染挂载到真实DOM上
beforeUpdate
:监听到数据data(state)被修改,diff算法生成vdom
updated
:虚拟dom重新渲染并应用更新
beforeDestroy
:解除绑定销毁组件以及事件监听器
destroyed
:销毁完毕
# 2】computed、watch、methods
coumputed 是计算属性,watch是侦听属性,methods是方法
computed 和 methods 相比,最大区别是 computed 有缓存:如果 computed 属性依赖的属性没有变化,那么 computed 属性就不会重新计算。methods 则是看到一次计算一次。
watch 和 computed 相比,computed 是计算出一个属性(废话),而 watch 则可能是做别的事情(如上报数据)。如果在data中没有相应的属性的话,是不能watch的,这点和computed不一样。
- 如果一个数据依赖于其他数据,那么把这个数据设计为computed的
- 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
# 3】vue组件间通信
方式一:父传子props
、子传父$emit + v-on
、兄弟传值eventBus
(eventBus.$emit + eventBus.$on
)
方式二:直接访问dom实例 $refs + ref
属性
方式三:vuex单一状态树
方式四:localStorage
、sessionStorage
进行传值
# 4】Vue 数据响应式
Vue 的响应式,核心机制是 观察者模式来实现数据驱动。(只是实现了找到需要更新的组件。)
vue组件在初始化时,首先会递归遍历data选项,使用Object.defineProperty
对里面的属性进行封装(数据劫持),每一个组件生成一个watcher对象,然后组件template进行模板编译解析生成AST,此时模板中的数据触发getter,对应的watcher进行依赖收集,render生成vdom,随后mount,渲染成真实dom。
组件数据更新时,触发setter,通知wacher(一个wacher对应一个组件)需要视图更新,重新render生成新的vdom,调用patch方法进行新旧vdom的diff算法比较,打补丁渲染到真实dom上。
# 5】Vue和React的视图更新机制对比
Vue和React的其中一个最重要的区别是它们对于数据更新的管理方式不同,Vue基本上是一套基于getter/setter实现的依赖收集/依赖更新的订阅式机制,而React则是通过显式的触发函数调用来更新数据,比如setState。相比来说Vue的实现方式更细粒度一些, 通过依赖收集,Vue是能够知道一些数据的更新导致了哪些地方需要重新计算的,通过这种机制,Vue能够优雅地实现计算属性、watch,包括视图渲染。而React由于缺少这种细粒度的机制,则更多时候需要一些其它方案来提高性能,于是产生了如PureComponent(区别于普通Component,浅比较state和props)、ImmutableJS、shouldComponentUpdate钩子等等。
# Vue和React如何更新视图?
- Vue:赋值data,如
this.value = 3
- React:
this.setState({value: 3})
其中的区别在于,Vue知道是组件数据中的value字段发生更新了, 而React只知道是组件的State发生了变化,并不知道是什么数据发生了变化。
- react会setState之后会把该组件和所有内嵌的子组件一起重新render成vdom来diff
- vue只会对一个watcher对应的一个组件来render生产vdom来diff
# 6】Vue Router
# 基本使用:
- 创建路由实例对象
- 配置路由映射关系
- 使用路由标签(通过
<router-link>
和<router-view>
) - 嵌套路由使用
children
- $router就是
new VueRouter()
整个实例对象,$route
是指当前页面路由对象
- $router就是
# hash模式:(location API)
- Location 对象包含有关当前 URL 的信息
location.hash='foo'
设置或返回从井号 (#) 开始的 URL(锚)。(并不会请求服务器)location.href='xxx'
设置或返回完整的 URL。(会请求服务器)
# history模式:(history API)
- History 对象包含用户(在浏览器窗口中)访问过的 URL
history.pushState()
加入url到浏览器历史列表(类似入栈)- 此时修改的url是在#的前面部分,#后面的部分是hash来控制。
- 强制刷新会向服务器请求,需要服务器设置中间件来解决
使用vue-router来实现的单页应用,访问http://cnode.lsqy.tech,进入首页,点击按钮跳入另一个路由,一切都是很正常的,但当这时刷新页面,发现就会出现404了。出现这样的错误Cannot GET /message/,因为默认浏览器会认为你是在请求服务端的路由,服务端那边没有对应的处理,所以自然就会出错了。
可以引入connect-history-api-fallback中间件来解决
# 9】路由懒加载
当打包构建应用,JavaScript包会变得非常大,影响页面加载。我们把不同路由对应的组件分割成不同的代码块,就可以实现按需加载。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
const Foo = () => import(/* webpackChunkName: "name" */ './Foo.vue')//懒加载组件Foo
上代码是有下面两部构成:首先,可以将异步组件定义为返回一个 Promise 的工厂函数:
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
第二,在 Webpack 2 中,我们可以使用动态import语法来定义代码分块点:
import(/* webpackChunkName: "name" */ './Foo.vue')
// 返回 Promise(加上注释来设置异步加载打包后的文件名)
# 8】路由守卫
导航守卫就是路由跳转过程中的一些钩子函数。
导航守卫:beforeEach、beforResolve、afterEach
router.beforeEach((to, from, next) => {//前置钩子
// ...
console.log(to,from)
next();//一定要调用该方法来 resolve 这个钩子
})
router.afterEach((to,from)=>{//后置钩子
console.log('----');
})//会发现beforeEach先打印,然后afterEach后打印
每个守卫方法接收三个参数:
to: Route
: 即将要进入的目标路由对象from: Route
: 当前导航正要离开的路由next: Function
: 调用该方法后,才能进入下一个钩子
使用白名单进行整个路由登录拦截。
# 11】keep-alive组件缓存
keepalive是一个抽象组件,缓存vnode,缓存的组件没有mounted中的diff步骤,为此提供 activated 和 deactivated 生命周期函数, 使用include、exclude、max 属性值可以控制缓存匹配对应的组件以及组件个数。
配合vue-router使用可通过routes 配置中的meta属性来设置组件是否被keepAlive所缓存。
meta: {
keepAlive: true // 需要被缓存
}
meta: {
keepAlive: false // 不需要被缓存
}
# 10】Vuex
集中式状态管理,单一状态树(唯一的store来管理),是响应式的。
state、getters(计算属性)、mutations(同步)、actions(异步)、modules
# 11】Vue.nextTick
- 用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
用法和react中的setState({},callback)
第二个参数callback差不多,都是视图更新后进行回调。
# 12】mixins
- 组件有mixins选项,是数组类型,里面可以接收mixin对象,
mixins:[mixinObject]
- 全局Vue对象可以调用
mixin()
方法,里面接收mixin对象,Vue.mixin(mixinObject)
- mixin对象与组件选项会进行合并;如果有冲突,组件优先级高于mixin
# react
# 1】渲染原理
dom diff是通过JS层面的计算,返回一个patch对象,即补丁对象(描述两个虚拟dom差异),在通过特定的操作解析patch对象,完成页面的重新渲染。作用是比较两个虚拟DOM区别,根据两个虚拟dom创建出补丁(描述差异),将这个补丁(差异对象)用来更新真实dom
react的优化:
- 同层节点之间相互比较,不会垮节点比较;
- 发现不同直接跳出比较。
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定(节点复用);
- 不同类型的节点,产生不同的树结构;
# 2】生命周期
# 通过shouldComponentUpdate优化性能:
父组件中的state或props改变,会导致父组件中的所有子组件都触发一次render,子组件触发render都是没必要的,可以子组件使用以下生命周期就是拦截。
- 在子组件中的shouldComponentUpdate生命周期中进行判断
shouldComponentUpdate(nextProps,nextState)
class Child extends React.Component{
shouldComponentUpdate(nextProps,nextState){
if(nextState.open!==this.state.open){
return true
}
if(nextProps.color!==this.props.color){
return true
}
return false
}
state={
open:'opened'
}
}
# 3】PureComponent
React.PureComponent 与 React.Component 几乎完全相同,但React.PureComponent通过props和state的浅比较来实现了shouldComponentUpate()
- 如果对象包含复杂的数据结构,它可能会因深层的数据不一致而产生错误的否定判断(表现为对象深层的数据已改变,视图却没有更新)
- 浅比较 只会比较到两个对象的 ownProperty 是否符合
Object.js()
判等,不会递归地去深层比较---源码
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
- 通过is函数对两个参数进行比较,判断是否相同,相同直接返回true:基本数据类型值相同,同一个引用对象都表示相同
- 如果两个参数不相同,判断两个参数是否至少有一个不是引用类型,存在即返回false,如果两个都是引用类型对象,则继续下面的比较;
- 判断两个不同引用类型对象是否相同 3.1 先通过Object.keys获取到两个对象的所有属性,具有相同属性,且每个属性值相同即两个对相同(相同也通过is函数完成)
# 4】Immutable.js不可变数据
Immutable Data 就是一旦创建,就不能再被跟更改的数据。对 Immutable对象 的任何的修改或添加删除操作都会返回一个新的 Immutable对象 。
为什么需要数据不能被改变?
redux等相关应用。redux中的数据是只读的,在使用redux操作的时候,第一步是对数据进行拷贝操作。
对于全局数据来讲,不能每个函数都可以进行修改它,会导致项目不稳定出bug。
所以往往我们对数据进行修改的时候都需要去进行拷贝数据。
优点:
- 降低Mutable带来的复杂度
- 节省内存空间(只把修改的节点拷贝了一份)
- 方便开发Undo/Redo(撤销),Copy/Paste(拷贝)
- 拥抱函数式编程(高阶函数)
缺点:
- 容易与原生对象混用
Immutable基本使用:(只把修改的属性进行拷贝,不修改的属性进行共享)
const { Map } = require('immutable');
let a = Map({
name:'zhangsan',
accont:Map({
QQ:'123456'
})
})
let b = a.set('name','lisi');//只修改了name,并没有修改accont
//此时的变量b就是:先把要修改的属性拷贝一份,然后再去修改它,没有修改的属性就是共享。
//此时我们可以看到a和b地址是不同的
console.log(a===b)//false
//但是a和b中的account又是共享的。
console.log(a.get('account')===b.get('account'))//true
{...obj}=>只浅拷贝了一层
# 解决什么问题?
- 引用类型变量会共享同一个地址。
- 如果我们希望的是:引用类型变量赋值之后是拷贝的新对象的话就需要使用深拷贝,但是深拷贝的开销太大,此时就需要使用immutable。
- 所以说immutable解决的是深拷贝开销大的问题。
var obj = {
a:1,
b:2
}
var obj1=obj
obj1.a=999
console.log(obj.a)//999
再比如:在react中shouldComponentUpdate(nextProps,nextState)
,当props或state对象中的值是一个引用,我们对引用中的具体数据进行修改,nextState和this.state依然会是相等的。就不能正确判断数据改变了。
我们来看看区别:下面这个问题就可以使用immutable来解决
例子:比较nextProps和this.props的值,会发现尽管两个打印出来的值是一样的,但是使用===比较始终不相等。如果我们使用immutable的话,nextProps和this.props就会共享块地址,当修改属性的时候,就会拷贝一份修改时候的属性,然后返回两个不同的immutable对象,可以对比出差异。
//父组件
export default class HHH extends Component {
state = {
name: 'leihao'
}
render() {
return (
<div>
<XXX name={this.state.name} />
<button onClick={() => {
this.setState({ name: 'zhangsan' })
}}>改变名字</button>
</div>
)
}
}
//子组件
export default class XXX extends Component {
shouldComponentUpdate(nextProps) {
return nextProps !== this.props
}
render() {
console.log('render')
return (
<div>
{this.props.name}
</div>
)
}
}
如果我们此时再来比较nextProps.name和this.props.name,会发现点击修改之后,他们使用===来比较之后就相等了,就可以阻止渲染
# 5】setState进阶
setState
方法和state
、props
一样的都是从React
的Component
中继承过来的。
使用格式如下:setState(partialState, callback)
setState
接受两个参数:- 第一个参数是:需要更新到的状态,一般是个对象
{ }
- 第二个参数是:一个回调函数,这个回调函数会在更新后会执行;
- 第一个参数是:需要更新到的状态,一般是个对象
setState是异步的,我们在setState之后,并不能立即打印出更改后的状态。而且setState
的异步还不是一般的异步操作,是react专门通过算法实现的,使用一段代码中setState
多次更新同一项属性,只会被执行一次。
- 提高了性能,多次setState只会render一次。
# 那么如何可以获取到更新后的值呢?
1】方式一:
使用setState()
的第二个参数(回调函数中获取)
changeText() {
this.setState({
message: "你好啊,李银河"
}, () => {//此回调函数会在更新state的时候被调用执行
console.log(this.state.message); // 你好啊,李银河
});
}
2】方式二:
在组件生命周期中进行截获
componentDidUpdate(prevProps, provState, snapshot) {
//渲染更新之后进行获取。
console.log(this.state.message);
}
# 其实分成两种情况:
- 在组件生命周期或
React
合成事件中,setState
是异步;(异步操作,但特定情况下只会执行一次,性能被优化) - 在
setTimeout
或者原生dom
事件中,setState
是同步;(完全就是同步函数)
# 6】react合成事件
React合成事件一套机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给SyntheticEvent函数处理、运行和处理。
React为了避免DOM事件滥用,同时屏蔽底层不同浏览器之间的事件系统差异
- 冒泡到document顶层
- 当用户在为onClick添加函数时,React并没有将Click事件绑定在DOM上面。
- 封装所有事件给SyntheticEvent事件合成(负责所有事件合成)
- 而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)
- 函数dispatchEvent统一分发
- 所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。
# 原生事件和react合成事件混合使用注意:
- 响应顺序(原生事件一般情况执行顺序先于react合成事件)
- 如果我们在原生事件中使用
e.stopPropagation()
进行阻止冒泡事件,可能会影响react合成事件的先外层冒泡,而导致错误
# 7】redux原理
三大原则:
- 单一数据源
- state是只读的
- 使用纯函数
- 一个函数在程序执行的过程中只会根据输入参数给出运算结果,我们把这类函数称为“纯函数”
control层:store=createStore(reducer(state,action))
view层:store.dispatch(action.fun(..))
model层:state={xxx:store.getState()}
# context通信:
provider、consumer
# webpack
# 1】基本配置、loader、plugin
webpack本身只支持加载JS和JSON文件,在编写JS代码时可以导入任何类型的文件(比如css、png、less等),在webpack打包过程中就必须使用loader对除JS以外的类型文件进行加载,分析出JS代码中对这些文件的依赖关系。最终将这些依赖关系打包到一个bundle.js文件中,并且将这些所有静态资源文件一同拷贝到dist文件夹下。期间可以使用plugin机制对整个打包的过程进行处理,实现很强大的打包功能。(注意:css-loader是让webpack读懂css代码,然后去找有没有@import,将依赖关系打包起来,最终这些css文件是会被拷贝到dist文件夹下)
开发阶段需要模块化组织ES6代码和css等文件,生产阶段js转换成兼容性更高的ES5代码并且将文件内容按照依赖关系打包到一起,线上版本不需要模块化。
以下是webpack.config.js的基本配置:
const path = require('path')
const { cleanWebpackPlugin }=require('clean-webpack-plugin')
const uglifyjsPlugin=require('uglifyjs-webpack-plugin')
const htmlWebpackPlugin=require('html-webpack-plugin')
const copyWebpackPlugin=require('copy-webpack-plugin')
module.exports={
//entry可以接受一个对象或者字符串
entry:{
app:'./src/app.js',
vendors:'./src/vendors.js'
},
output:{
path:path.resolve(__dirname,'dist'),//设置出口路径
filename:'[name].js'//设置打包好的文件的名字,[name]是指定出口文件名对应的是入口的别名
},
modules:{
rules:[{
test:/\.css$/,//根据打包过程中所遇到文件路径匹配是否使用这个loader
use:['style-loader','css-loader']//指定具体使用的loader
////*重要*:使用多个loader时,是从后往前读取,这里就是先使用css-loader,再使用style-loader(通过脚本创建<style>标签里面包含css样式)
}]
},
plugins:[
new cleanWebpackPlugin(),//每次打包时清除dist文件夹
new uglifyjsPlugin(),//压缩代码
new htmlWebpackPlugin({//生成html入口文件
title:'Webpack Example',
meta:{
viewport:'width=device-width'
}
}),
new copyWebpackPlugin([//将不需要参与打包的文件拷贝到dist文件夹
'public'//需要拷贝的目录或者路径
])
]
}
注意:NodeJS中path.resolve
的用法:(看的出区别吗)
console.log(__dirname);
// /Users/mac/Desktop/测试代码
const path= require('path')
console.log(path.resolve(__dirname,'dist'));//相对路径
// /Users/mac/Desktop/测试代码/dist
console.log(path.resolve(__dirname,'./dist'));//相对路径
// /Users/mac/Desktop/测试代码/dist
console.log(path.resolve(__dirname,'/dist'));//绝对路径
// /dist
# 2】按需加载
# 为什么要按需加载?
在一个前端应用中,将所有的代码都打包进一个或几个文件中,加载的时候,把所有文件都加载进来,然后执行我们的前端代码。只要我们的应用稍微的复杂一点点,包括依赖后,打包后的文件都是挺大的。而我们加载的时候,不管那些代码有没有执行到,都会下载下来。如果说,我们 只下载我们需要执行的代码的 话,那么可以节省相当大的流量。也就是我们所说的 按需加载。(比如:vue中的路由懒加载,就是借助webpack的动态import语法来定义代码分割点实现的。)
实现按需加载使用到的是require.ensure(dependencies, callback, chunkName)
,但是require.ensure()
已被 import()
取代。
这个方法可以实现js的按需加载,分开打包,webpack 管包叫 chunk
,为了打包能正常输出,我们先给webpack配置文件配置一下chunk文件输出路径:
// webpack.config.js
module.exports = {
...
output: {
...
chunkFilename: '[name].[chunkhash:5].chunk.js',
publicPath: '/dist/'
}
...
}
# 按需加载原理:
// a.js
console.log('a');
// b.js
console.log('b');
// c.js
console.log('c');
// entry.js
require.ensure([], () => {
require('./a');
require('./b');
}, 'chunk1');
if(false){
require.ensure([], () => {
require('./c');
}, 'chunk2');
}
将会打包出 3 个文件,基础包、chunk1 和 chunk2,但是chunk2在if判断中,而且永远为false,所以 chunk2 虽然打包了但永远不会被加载。
# Git
# 1】git rebase
git reset --hard HEAD^
(回退到的版本库地址,这里是上一次提交)
:重置缓存区和工作目录
# Linux操作系统
# 1】系统目录结构
登录linux系统之后你会看到以下文件夹
树状目录结构:
- /bin:bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令。
- /boot:这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。
- /etc:etc 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件和子目录。
- /usr:usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 program files 目录。
- /home:用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve。
- /lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。
- /root:该目录为系统管理员,也称作超级权限者的用户主目录。
# 职业规划
分时间段:
短期:继续夯实基础,自己封装一个属于自己的组件库,学再研究一门框架
中长期:再研究一门后端语言java为我们公司更好的服务。
长期:我也相信咱们公司有合理晋升机制,争取公司的机会
科来官网bug、我们公司的发展行业发展前景。
遇到的最难的问题:保持质疑的态度,找资源去验证。