前端面试HTML5+CSS3+JS+TS4+Vue3+React18+八股文+手写+项目+笔试
变量声明不开辟内存,只是告诉编译器,要声明的部分存在,要预留部分的空间。var i;变量定义开辟内存。var i=123;props公开,单向数据流值,父子组件间的唯一通信不可改1.每个组件对象都会有props(properties的简写)属性2.组件标签的所有属性都保存在props中3.内部读取某个属性值:this.props.propertyNamestate私有(通过Ajax获取回来的数据,
高频面试考点分集,适用于基础面和临时抱佛脚复习
前端面试高频考点之 通信(渲染、http、缓存、异步、跨域)
分集方便专题学习(合集因码字过多,太卡,优先更新分集,合集与分集同步率最低98%)
前端面试题考点之 通信(渲染、http、缓存、异步、跨域、安全)
高频前端面试项目考点(npm,git,webpack,TS4,sass,瀑布流,懒加载)
前端笔试常考设计模式,操作系统,数据结构,ACM模板,经典算法,正则表达式,常用方法
⭐表示手写 和 重要程度,*表示了解即可
为了简洁,相关文章参考链接在标题里
常考考点也写的很详细,因为面试官会顺着深挖,所以导致篇幅全又长,见谅
长文不易,码字卡死,如果有帮助到你,可以点个赞吗?Thanks♪(・ω・)ノ
一开始就是在CSDN博客上写的,现在应部分同学的要求,我直接将博客内容复制成pdf,暂时没空整理格式,有的代码缺失还请见原文,见谅。
pdf版CSDN免费下载链接
合集(方便检索,手机端有时文章会不显示目录,可以点下方的目录按钮)
目录
系统屏幕(window.screen.availHeight,height)
浏览器(window.outerHeight,innerHeight)
元素(clientHeight,offsetHeight,scrollHeight)
相对距离或位置(offsetTop,scrollTop,getBoundingClientRect)
content-box 内容盒模型(W3C盒) 和 border-box 边框盒模型(IE 盒)
align-items和justify-content的区别
判断数据类型:typeof运算符,instance of运算符,isPrototypeOf() 方法,constructor,Object prototype
Iterator,for in,for of,forEach,map循环遍历
正则表达式Regular Expression(RegExp)
Cookie、localStorage和sessionStorage
Promise.all()哪怕一个请求失败了也能得到其余正确的请求结果的解决方案⭐⭐
set(target,propKey,value,receiver)
deleteProperty(target,propKey)
取消代理Proxy.revocable(target, handler)
二叉排序树/二叉查找树BST(Binary Search/Sort Tree)
正则表达式Regular Expression(RegExp)
HTML5
HTML5的设计目的是为了在移动设备上支持多媒体。
在HTML5出来之前,我们习惯于用没有语义的div来表示不同模块。
在HTML5中加入了一些语义化标签,来更清晰的表达文档结构。
语义化标签的好处⭐⭐
- 用户:提高体验,比如:title,alt用于解释名词和图片信息
- 非技术人员:能看懂代码,很好的呈现内容结构、代码结构
- 技术人员:便于团队开发与维护,语义化更具有可读性
- 搜索引擎:利于SEO。语义化能和搜索引擎建立更好的联系,优化搜索
Web标准和W3C标准⭐
网页组成
web标准
- 结构(骨架):HTML用于描述页面的结构
- 表现(皮肤):CSS用于控制页面中元素的样式
- 行为(交互):JavaScript用于响应用户操作
- W3C:World Wide Web(万维网) Consortium,对web标准提出了代码规范的要求
-
对结构的要求
1、标签字母要小写
2、标签要闭合
-
对行为的要求
1、建议使用外链CSS和js脚本,实现结构与表现分离、结构与行为分离,能提高页面的渲染效率,更快地显示网页内容
浏览器的渲染过程⭐⭐⭐
1.解析HTML的所有标签,深度遍历生成DOM Tree
2.解析CSS,构建层叠样式表模型CSSOM(CSS Object Model)
2.5.JS脚本加载
a. 普通js/sync
文档解析的过程中,如果遇到script脚本,就会停止页面的解析进行下载,当脚本都执行完毕后,才会继续解析页面。
(因为JS可以操作DOM和CSS,可能会改动DOM和CSS,所以继续解析会造成浪费)。
如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。
所以常见的做法是将js放到页脚部分。
b. async(异步:HTML加载和解析,js加载)
async脚本会在加载完毕后执行。
<script type="text/javascript" src="x.min.js" async="async"></script>
async脚本的加载不计入DOMContentLoaded事件统计,也就是说下图两种情况都是有可能发生的:
HTML 还没有被解析完的时候,async脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件。
HTML 解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件
c. defer(推迟)
文档解析时,遇到设置了defer的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析和渲染完毕后,会等到所有的defer脚本加载完毕并按照顺序执行完毕才会触发
<script type="text/javascript" src="x.min.js" defer="defer"></script>
DOMContentLoaded事件,也就是说下图两种情况都是有可能发生的:
HTML 还没有被解析完的时候,defer脚本已经加载完了,那么 等待HTML 解析完成后执行脚本,脚本执行完毕后触发DOMContentLoaded事件。
HTML 解析完了之后,defer脚本才加载完,然后再执行脚本,脚本执行完毕后触发DOMContentLoaded事件。
defer是“渲染完再执行”:依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖
async是“下载完就执行”:并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据。
3.构建Render Tree(渲染树)
DOM和CSSOM根据一定的规则组合起来生成了Render Tree
4.布局(Layout)
确定各个元素的位置,以及大小。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素。
5.绘制(Painting)
浏览器会遍历Render Tree渲染树,调用“paint”方法,将渲染树的各个节点绘制到屏幕上。
回流(重排)和重绘⭐⭐
回流(重排)
元素改变 尺寸,宽高,边框,内容,位置 都会引起重排,导致需要重新构建页面的时候
- 增删可见的 DOM 元素的时候
- 元素的位置发生改变
- 元素的尺寸发生改变
- 内容改变
- 页面第一次渲染的时候
重绘
外观发生改变,但没有改变布局
列举一些相关的 CSS 样式:color、background、background-size、visibility、box-shadow
常用手写⭐
获取标签
<html>
<head>
<meta charset=utf-8>
<style type="text/css">
div{
color:#ff0000;
font-size:20px;
}
.green{
color:#008000;
}
#black{
color:#000000;
}
</style>
</head>
<body>
<div>红色</div>
<div class='green'>绿色</div>
<div id='black'>黑色</div>
</body>
</html>
document.getElementById('id')//全局唯一
document.getElementByClassName('className')//获得数组
document.getElementByTagName('div')//获得数组
js中插入标签
let head = document.head;
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = "p {color: rgb(255,0,0);}";
head.appendChild(style);
获取第n个标签
let pArr = document.getElementsByTagName('p')
for(let i = 0; i < pArr.length; i++){
if(i===n-1){
...
}
}
<html>
<head>
<meta charset=utf-8>
<style type="text/css">
ul li:nth-child(2) {
background-color: rgb(255,0,0);
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</body>
</html>
常用触发事件
<input type="text" onkeyup="myFunction()">
//不用 "on" 前缀。例如,使用 "click" 来取代 "onclick"。
//true - 事件在捕获阶段执行
//false- 默认。事件在冒泡阶段执行
document.addEventListener(event, function[, useCapture])
鼠标事件
属性 | 描述 | DOM |
---|---|---|
onclick | 当用户点击某个对象时调用的事件句柄。 | 2 |
ondblclick | 当用户双击某个对象时调用的事件句柄。 | 2 |
onmousedown | 鼠标按钮被按下。 | 2 |
onmousemove | 鼠标被移动。 | 2 |
onmouseover | 鼠标移到某元素之上。 | 2 |
onmouseout | 鼠标从某元素移开。 | 2 |
onmouseup | 鼠标按键被松开。 | 2 |
键盘事件
属性 | 描述 | DOM |
---|---|---|
onkeydown | 某个键盘按键被按下。 | 2 |
onkeypress | 某个键盘按键被按下并松开。 | 2 |
onkeyup | 某个键盘按键被松开。 |
表单事件
属性 | 描述 | DOM |
---|---|---|
onchange | 该事件在表单元素的内容改变时触发( <input>, <keygen>, <select>, 和 <textarea>) | 2 |
onfocus | 元素获取焦点时触发 | 2 |
onfocusin | 元素即将获取焦点时触发 | 2 |
onfocusout | 元素即将失去焦点时触发 | 2 |
oninput | 元素获取用户输入时触发 | 3 |
onreset | 表单重置时触发 | 2 |
onsearch | 用户向搜索域输入文本时触发 ( <input="search">) | |
onselect | 用户选取文本时触发 ( <input> 和 <textarea>) | 2 |
onsubmit | 表单提交时触发 | 2 |
获取宽高
系统屏幕(window.screen.availHeight,height)
- window.screen.height
这个是设备显示屏的高度,各个机型的显示屏高度都不一样,可以在系统设置中看
- window.screen.availHeight
屏幕的可用高度,一般是显示屏高度减去显示屏任务栏的高度
screen.availHeight = screen.height - 任务栏高度
注; 更改显示器的缩放倍数,会影响到获取的值,比如屏幕放大125%,则原本1080高度的值,读取后为864。即1080 / 1.25 = 864
浏览器(window.outerHeight,innerHeight)
-
window.outerHeight
浏览器的高度,高度改变,会改变值的大小
-
window.innerHeight
浏览器的可用高度 = 浏览器高度 - 顶部工具栏
若有调试面板还会再减去调度面板的高度,最后得出的才是可用高度
元素(clientHeight,offsetHeight,scrollHeight
)
获取body的高(不含边框)
element.clientHeight
:body的高度
clientHeight = padding + height
获取body的高(含边框)
element.offsetHeight
:body的高度(包含border)
offsetHeight = padding + height + border
element.scrollHeight
,为可见高度加上未显示的高度(滚动条未显示部分)。
相对距离或位置(offsetTop,scrollTop,getBoundingClientRect
)
- 获取到顶部或左部的距离
element.offsetTop
,element.offsetLeft
offsetTop:元素到offsetParent顶部的距离
offsetParent:距离元素最近的一个具有定位的父元素(relative,absolute,fixed),
若父元素都不符合条件,offsetParent为body。
- 注意:只有元素show(渲染完成)才会计算入offsetTop,若是中间有元素数据需要异步获取,会导致最终获取的offsetTop值偏小
- 获取滚动条到top,left的距离:
element.scrollTop,element.scrollLeft
- 获取相对于视窗的位置集合
element.getBoundingClientRect()
集合中有top, right, bottom, left等属性。
- rectObject.top:元素上边到视窗上边的距离;
- rectObject.right:元素右边到视窗左边的距离;
- rectObject.bottom:元素下边到视窗上边的距离;
- rectObject.left:元素左边到视窗左边的距离;
CSS3
盒模型⭐⭐⭐
内容(content)、内边距/填充(padding)、外边距/边界(margin)、 边框(border);
content-box 内容盒模型(W3C盒) 和 border-box 边框盒模型(IE 盒)
width = content宽度
width = content宽度 + padding + border
<div class="content-box"></div>
<div class="border-box"></div>
实现梯形,三角形,扇形,圆形,半圆(⭐手写)
/* HTML CODE:
<div class="square">正方形</div>
*/
/* CSS CODE */
.square {
width: 100px;
height: 100px;
border-top: 50px solid red;<!--solid: 定义实线边框-->
border-right: 50px solid green;
border-bottom: 50px solid orangered;
border-left: 50px solid blue;
}
关键:
border: 50px solid transparent; border-color设置为【透明】
border-radius: 50%;
border-top-left-radius: 50px;
详情:
CSS实现各种图形 -- 梯形,三角形,扇形,圆形,半圆 - 掘金
盒子充满屏幕 (⭐手写)
相对当前屏幕高度
div.test
{
background-color:red;
width:100vw;
height:100vh;
}
选择器
ID选择器、类选择器、标签选择器(按优先级高到低排序)⭐⭐
<html>
<head>
<meta charset=utf-8>
<style type="text/css">
div{
color:#ff0000;
font-size:20px;
}
.green{
color:#008000;
}
#black{
color:#000000;
}
</style>
</head>
<body>
<div>红色</div>
<div class='green'>绿色</div>
<div id='black'>黑色</div>
</body>
</html>
属性选择器
属性选择元素
[title]
{
color:blue;
}
伪类和伪元素选择器(⭐手写)伪类和伪元素选择器
伪类选择器:逻辑选择元素
selector:pseudo-class {property:value;}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<style>
a:link {color:#000000;} /* 未访问链接*/
a:visited {color:#00FF00;} /* 已访问链接 */
a:hover {color:#FF00FF;} /* 鼠标移动到链接上 */
a:active {color:#0000FF;} /* 鼠标点击时 */
</style>
</head>
<body>
<p><b><a href="/css/" target="_blank">这是一个链接</a></b></p>
<p><b>注意:</b> a:hover 必须在 a:link 和 a:visited 之后,需要严格按顺序才能看到效果。</p>
<p><b>注意:</b> a:active 必须在 a:hover 之后。</p>
</body>
</html>
- nth-child(n)
nth-child(n)匹配属于其父元素的第n个子元素,不论元素类型,n可以是数字、关键词、或公式。关键词odd和even是可用于匹配下标是奇数或偶数的子元素的关键词(第一个子元素的下标是 1)
<html>
<head>
<meta charset=utf-8>
<style type="text/css">
ul li:nth-child(even) {
background-color: rgb(255,0,0);
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</body>
</html>
- 伪元素
<head>
<meta charset=utf-8>
<style type="text/css">
div::after{
content:"";
width: 20px;
height: 20px;
background-color: rgb(255,0,0);
display: block;
}
</style>
</head>
<body>
<div></div>
</body>
</html>
html模块的div元素加一个后伪元素
- 伪类只能使用“:”,伪元素既可以使用“:”,也可以使用“::”
- 伪元素其实相当于伪造了一个元素,伪类没有伪造元素,例如first-child只是给子元素添加样式而已。(本质区别就是是否抽象创造了新元素)
优先级⭐⭐⭐
- 在同一层级下:权值由高到低
- !important (会覆盖CSS的任何声明,其实与优先级毫无关系) 权值
- 内联样式(style=“ ”) 1000
- ID选择器(id=" “) 100
- 伪类选择器(如:hover)
- 属性选择器[title]{color:blue;})
- Class类选择器(class=” ") 10
- HTML标签选择器 (p{}) 1
- 通用选择器(*) 0
- 不同层级下:
正常来说权重值越高的优先级越高,但是一直以来没有具体的权重值划分,所以目前大多数开发中层级越深的优先级越高
样式方式(按优先级高到低排序)⭐⭐
内联样式表(在标签内设置元素的样式)
写一次只能设置一个
<p style="background:red"></p>
嵌入样式表(在head标签内)
<head>
<title></title>
<style type="text/css">
p{
background-color:yellow;
}
</style>
</head>
外部样式表(在head标签内)
rel=relationship
href=hypertext Reference
<head>
<title></title>
<link href="xxx.css" rel="stylesheet" type="text/css"/>
</head>
通过 link 进行对外部CSS样式文件的引用,也可以引用网上别人写好的样式
transform旋转,缩放,平移⭐⭐
修改 CSS 坐标空间,实现旋转,缩放,倾斜或平移
默认相对元素的中心点
position关键字⭐⭐⭐
static(默认)
该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top
, right
, bottom
, left
和 z-index
属性无效。
z-index 属性指定一个元素的堆叠顺序。
拥有更高堆叠顺序的元素总是会处于堆叠顺序较低的元素的前面。
- inherit
从父元素继承 position 属性的值。
relative
相对于其正常位置进行定位。
元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。
absolute
相对于 static 定位以外的第一个父元素进行定位。
元素会被移出正常文档流,并不为元素预留空间。绝对定位的元素可以设置外边距(margins),且不会与其他边距合并。
- fixed:相对于浏览器窗口进行定位。在屏幕滚动时不会改变
- sticky(CSS3新增) :基于用户滚动的位置,屏幕滚出时会粘住
水平 & 垂直对齐 (⭐手写)
水平居中
指在水平方向上处于中间的位置。
- 元素/图片:
margin: auto;
行内元素会占整行,看不出来水平居中,所以需要:width: 50%;
- 文本:
文本标签除了<p>都是行内元素,text-align=center
垂直居中
- 单行文本:
line-height = height
- 图片:
vertical-align: middle;
水平垂直居中
transform:translate
top: 50%;left: 50%;, 是以元素左上角为原点,故不处于中心位置,
加上transform:translate(-50%,-50%) ,元素原点(中心点)往上(x轴),左(y轴)移动自身长宽的 50%,
flex(强烈推荐)
只需要设置 align-items:center; 属性
.wrap {
width: 300px;
height: 300px;
border: 1px solid red;
display:flex;
justify-content:center;
align-items:center;
}
.box {
height: 100px;
width: 100px;
border: 1px solid blue;
}
flex布局⭐⭐⭐
布局的传统解决方案,基于盒状模型,依赖 display属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
关键是flex布局能触发BFC规范
Flexible box设置或检索弹性盒模型对象的子元素如何分配空间
align-items和justify-content的区别
水平的主轴(main axis)和垂直的交叉轴(cross axis)
flex-direction 属性决定主轴的方向(也就是排列方向)。有4个属性值可以设置。
- column:主轴为垂直方向,起点在上沿。
- column-reverse:主轴为垂直方向,起点在下沿。
- row(默认值):主轴为水平方向,起点在左端。
- row-reverse:主轴为水平方向,起点在右端。
- align-items 属性定义项目在交叉轴上如何对齐。
baseline | 元素位于容器的基线上。 如弹性盒子元素的行内轴与侧轴为同一条,则该值与'flex-start'等效。其它情况下,该值将参与基线对齐。 |
stretch | 默认值。元素被拉伸以适应容器。 如果指定侧轴大小的属性值为'auto',则其值会使项目的边距盒的尺寸尽可能接近所在行的尺寸,但同时会遵照'min/max-width/height'属性的限制。 |
- justify-content 属性定义了项目在主轴上的对齐方式。
space-between | 均匀排列每个元素,首个元素放置于起点,末尾元素放置于终点。 |
space-evenly | 均匀排列每个元素,每个元素之间的间隔相等。 |
space-around | 均匀排列每个元素,每个元素周围分配相同的空间。 |
flex:1
flex 属性是 flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto。后两个属性可选。
flex 属性属性有两个快捷值:auto
(1 1 auto
) 和 none (0 0 auto
)。
建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。
/* 一个值,width/height: flex-basis */
flex: 10em;
flex: 30px;
flex: min-content;
/* 两个值:flex-grow | flex-basis */
flex: 1 30px;
/* 两个值:flex-grow | flex-shrink */
flex: 2 2;
/* 三个值:flex-grow | flex-shrink | flex-basis */
flex: 2 2 10%;
flex-grow 属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
负值对该属性无效。
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小
BFC规范⭐⭐⭐
问题
- 外边距重叠:
块的上外边距margin-top和下外边距margin-bottom会合并为单个边距(为单个边距的最大值)
- 浮动导致父高度塌陷:
- 不浮动的元素被浮动元素覆盖:
BFC块级格式化上下文(Block Fromatting Context)
决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用
独立布局,盒子内子元素样式不会影响到外面的元素。
常见触发条件
- overflow: hidden
- display: flex | inline-block | table-cell
- position: absolute | fixed
规则
BFC
就是一个块级元素,块级元素会在垂直方向一个接一个的排列BFC
就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签- 垂直方向的距离由margin决定, 属于同一个
BFC
的两个相邻的标签外边距会发生重叠 - 计算
BFC
的高度时,浮动元素也参与计算
overflow: hidden示例
overflow属性指定如果内容溢出一个元素的框,会发生什么
值 | 描述 |
---|---|
visible | 默认值。内容不会被修剪,会呈现在元素框之外。 |
hidden | 内容会被修剪,并且其余内容是不可见的。 |
scroll | 内容会被修剪,但是浏览器会显示滚动条以便查看其余的内容。 |
auto | 如果内容被修剪,则浏览器会显示滚动条以便查看其余的内容。 |
inherit | 规定应该从父元素继承 overflow 属性的值。 |
overflow:hidden
- 避免外边距重叠
- 清除浮动
- 阻止元素被浮动元素覆盖:
*浮动
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>float实现浮动</title>
</head>
<style>
.z1{
height: 200px;
width: 200px;
float: left;
text-align: center;
line-height: 200px;
background: skyblue;
}
.fu {
width: 400px;
}
</style>
<body>
<div class="fu clearfix">
<div class="z1">设置了float为left的图片</div>
<div class="z2">你看,我没有被浮动哥哥挡住哦,这是一段神奇旅行,一天我遇上了白雪公主</div>
</div>
</body>
</html>
即使图片浮动了,破坏了文档流,也覆盖在没有浮动的元素上了,但是其并没有将文本内容也覆盖掉 ,证明了被设计出来的主要目的:实现文字环绕图片排版功能
absolute
的容器,才是意义上的完全脱离文档流。覆盖在当前位置上的所有容器和文本内容之上。
absolute
和float
都不会去覆盖掉在他们之前的正常文档流,这应该和浏览器渲染机制有关系,会从上到下依次渲染内容,渲染成功后,就不会因为后续元素浮动而使其被覆盖住(不考虑使用fix等强行覆盖的情况)。
三栏布局 :左右固定,中间自适应(⭐手写)
flex布局(强烈推荐)
- 基础巩固
flex 属性用于设置或检索弹性盒模型对象的子元素如何分配空间。
flex 属性是 flex-grow、flex-shrink 和 flex-basis 属性的简写属性。
注意:如果元素不是弹性盒模型对象的子元素,则 flex 属性不起作用。
- 实现方法
左右两栏设置宽度,中间栏设置 flex:1,占满余下部分
<!DOCTYPE html>
<html lang="en">
<head>
<title>flex布局</title>
<style>
.main{
height: 60px;
display: flex;
}
.left,
.right{
height: 100%;
width: 200px;
background-color: #ccc;
}
.content{
flex: 1;
background-color: #eee;
}
</style>
</head>
<body>
<div class="main">
<div class="left"></div>
<div class="content"></div>
<div class="right"></div>
</div>
</body>
</html>
grid布局
- 基础巩固
grid:CSS 所有网格容器的简写属性
grid-template-rows / grid-template-columns :设置列和行的尺寸。
- 实现方法
左右两栏设置宽度,中间栏宽度auto
<!DOCTYPE html>
<html lang="en">
<head>
<title>grid布局</title>
<style>
body {
display: grid;
grid-template-columns: 200px auto 200px;
grid-template-rows: 60px;
}
.left,
.right {
background-color: #ccc;
}
.content {
background-color: #eee;
}
</style>
</head>
<body>
<div class="left"></div>
<div class="content"></div>
<div class="right"></div>
</body>
</html>
margin负值法
- 原理解释
- 实现方法:
左右两栏均左浮动,外层盒子左浮动,
中间栏设置左右两栏宽度的margin值,
左栏设置margin -100%(向左移动整个屏幕的距离),
右栏设置 margin值为负的盒子宽度。
<!DOCTYPE html>
<html lang="en">
<head>
<title>margin负值</title>
<style>
.left,
.right {
float: left;
width: 200px;
height: 60px;
background-color: #eee;
}
.left {
margin-left: -100%;
}
.right {
margin-left: -200px;
}
.main {
width: 100%;
float: left;
height: 60px;
}
.content {
height: 60px;
margin: 0 200px;
background-color: #ccc;
}
</style>
</head>
<body>
<div class="main">
<div class="content"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</body>
</html>
自身浮动
<!DOCTYPE html>
<html lang="en">
<head>
<title>自身浮动法</title>
<style>
.left,
.right {
height: 60px;
width: 200px;
background-color: #eee;
}
.left {
float: left;
}
.right {
float: right;
}
.content{
height: 60px;
background-color: #ccc;
margin: 0 200px;
}
</style>
</head>
<body>
<div class="left"></div>
<div class="right"></div>
<div class="content"></div>
</body>
</html>
绝对定位
左右两栏绝对定位,分别定位到盒子的两侧,中间栏采用margin值撑开盒子
注意:采用定位时,浏览器默认的padding或margin值会影响布局,需要初始化样式 margin:0;padding:0;
圣杯布局 (⭐手写)
两边固定,中间自适应,且中间栏放在文档流的前面,率先渲染
基本的dom结构(注意center需要排在第一个位置)
<div class="header">header</div>
<div class="container">
<div class="center column">center</div>
<div class="left column" >left</div>
<div class="right column" >right</div>
</div>
<div class="footer">footer</div>
或者
<section class="container">
<article class="center"><br /><br /><br /></article>
<article class="left"><br /><br /><br /></article>
<article class="right"><br /><br /><br /></article>
</section>
<br> 标签插入一个简单的换行符
- 定位+浮动
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.container {
border: 1px solid black;
/* 防止容器盒子高度塌陷和给之后的左、右浮动元素预留位置 */
overflow: hidden;
padding: 0px 100px;
min-width: 100px;
}
.left {
background-color: greenyellow;
/* 保证之后的"margin-left"属性可以将自身拉到上一行 */
float: left;
/* 固定宽度 */
width: 100px;
/* 将元素向左移动属性值的单位,100%相对于父容器计算 */
margin-left: -100%;
/* 相对定位,需要将自身再向左移动自身的宽度,进入容器的"padding-left"区域 */
position: relative;
/* 自身的宽度,刚好进入容器的"padding-left"区域 */
left: -100px;
}
.center {
background-color: darkorange;
float: left;
width: 100%;
}
.right {
background-color: darkgreen;
float: left;
width: 100px;
margin-left: -100px;
position: relative;
left: 100px;
}
</style>
</head>
<body>
<section class="container">
<article class="center"><br /><br /><br /></article>
<article class="left"><br /><br /><br /></article>
<article class="right"><br /><br /><br /></article>
</section>
</body>
</html>
magin-left:-100%
这个百分比是以父元素内容长度的百分比,该父元素内容长度需要去除padding magin border。由于长度设置为了100%,需要一整行的宽度补偿margin,则移到最左边。
magin-left:-100px
margin负值会改变元素占据的空间,及移到父元素的最左边,并且该子元素width即为100px
单位⭐⭐⭐
- 绝对长度单位:px 像素
- 百分比: %
- 相对父元素字体大小单位: em
- 相对于根元素字体大小的单位: rem(默认16px)
- 相对于视口*宽度的百分比(100vw即视窗宽度的100%): vw
- 相对于视口*高度的百分比(100vh即视窗高度的100%): vh
px转rem
相对于根元素字体大小的单位: rem(默认16px)
PostCss是一个用JavaScript工具和插件转换CSS代码的工具。postcss-pxtorem
px转vw
网页宽度=1920px 网页高度=1080px
1920px = 100vw
1080px = 100vh
宽 300px 和 200px 的 div ,其所占的宽高,以 vw 和 vh 为单位
vwDiv = (300px / 1920px ) * 100vw
vhDiv = (200px / 1080px ) * 100vh
当屏幕放大或者缩小时,div 还是以 vw 和 vh 作为宽高的,就会自动适应不同分辨率的屏幕
可自定义scss函数。
$vm_base: 1920;
$vh_base: 1080;
@function vw($px) {
@return ($px / $vm_base) * 100vw;
}
@function vh($px) {
@return ($px / $vh_base) * 100vh;
}
.head{
font-size:vh(100);
}
opacity: 0 、visibility: hidden、display: none⭐⭐⭐
区别 | opacity: 0 | visibility: hidden | display: none |
页面布局 | 不改变 | 不改变 | 改变 |
触发事件 | 能触发 | 不能触发 | 不能触发 |
img的 title 和 alt 有什么区别⭐
- 通常当鼠标滑动到元素上的时候显示
alt(alternative)
是<img>
的特有属性,是图片内容的等价描述,用于图片无法加载显示、读屏器阅读图片。可提图片高可访问性,除了纯装饰图片外都必须设置有意义的值,搜索引擎会重点分析。
行内素、块级元素和行内块元素⭐⭐⭐
display:inline;// 转换为行内元素
display:block;// 转换为块级元素
display:inline-block// 转换为行内块元素
从 HTML 的角度来讲,标签分为:
- 文本级标签:p、span、a、b、i、u、em
- 容器级标签:div、h系列、li、dt、dd
行内元素:除了p之外,所有的文本级标签,都是行内元素,p是个文本级,但是是个块级元素
块级元素:所有的容器级标签都是块级元素,还有p标签
块标签:div、h1~h6、ul、li、table、p、br、form。
特征:独占一行,换行显示,可以设置宽高,可以嵌套块和行
行标签:span、a、img、textarea、select、option、input。
特征:只有在行内显示,不会自动进行换行,内容撑开宽、高,不可以设置宽、高(img、input、textarea等除外)。(设置float后可以设置宽、高)
对 margin 仅设置左右方向有效,上下无效,padding 设置上下左右都有效
溢出转省略 (⭐手写)⭐⭐
单行多行,都要overflow: hidden;
单行
定元素内的空白处理:white-space:nowrap; 文本不进行换行;默认值normal
overflow: hidden;
text-overflow:ellipsis; //ellipsis;省略
white-space: nowrap; //nowrap 不换行
多行
1.-webkit-line-clamp用来限制在一个块元素显示的文本的行数。 为了实现该效果,它需要组合其他的WebKit属性。常见结合属性:
2.display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 。
3.-webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。
IE不兼容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<style>
.text2{
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
</style>
</head>
<body>
<div class="text2">
这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话
这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话
这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话这是一句话
</div>
</body>
</html>
布局
静态布局
- 描述:就是设定好的长和宽,大小不会改变,不管你多大的屏幕它都是那么大,分辨率是不会改变的
- 优点:这个对于开发者来说是最简单的一种布局方式,没有什么兼容性的问题
- 缺点:窗口的大小比显示的内容小。,会展现出滚动条
- 场景:传统web网站
响应式布局⭐⭐
- 使用媒体查询(@media)
- 使用flex弹性布局
- 使用百分比单位:rem单位,VH、HW单位
自适应和响应式区别
pc端(>1024)一套适配,平板(768-1024)一套适配,手机端(<768)
自适应设计(AWD):
- 需要开发多套界面;
- 通过检测视口分辨率,来判断当前访问的设备是:pc端、平板、手机,
- 从而请求服务层,返回不同的页面;
响应式设计(RWD):
- 只需开发一套界面。
- 通过检测视口分辨率,
- 针对不同客户端在客户端做代码处理,通过CSS Media Query,Content - Based Breakpoint等技术展现不同的布局和内容。
弹性布局(flex布局)
- 描述:目前比较流行的一种布局,使用传统布局难以实现一些复杂的布局,使用flex布局实现就变得非常容易
- 优点:简便、完整、响应式地实现各种页面布局
- 缺点:只兼容IE10+的浏览器
- 场景:三栏式布局、垂直水平居中布局
流式布局
- 描述:页面元素的宽度按照屏幕分辨率进行适配调整,但整体布局不变。主要特征是像瀑布一样往下流,有规律的无限遍历模块。
- 优点:灵活,充分利用浏览器的空间
- 缺点:宽度按照屏幕进行适配调整,对于大屏幕来说用户体验并不是特别好,有些布局元素会显得很长
- 场景:类似抖音视频、微博消息、微信朋友圈等布局
媒体查询@media(css3)
@media可以针对不同的屏幕尺寸设置不同的样式。 例如,可以缩小小型设备上的字体大小
@media
规则可置于代码的顶层或位于其它任何@条件规则组内
/* 在 screen 类型 大于560px 小于 700px 加载 */
@media screen and (min-width: 560px) and (max-width: 700px) {
.box1 {
background-color: burlywood;
}
}
媒体类型
① all,适用于所有设备。默认使用该值。
② print,适用于在打印预览模式下在屏幕上查看的分页材料和文档。
③ screen,主要屏幕设备,例如电脑屏幕,平板电脑,智能手机等。
④ speech,主要用于语音合成器。
逻辑运算符(logical operators)
not, and, 似于逻辑或or运算符),only
逗号分隔多个媒体查询来将它们合并为一个规则
/* 在 screen 类型 大于560px 或 小于240px 加载 */
@media screen and (min-width: 560px), (max-width: 240px) {
.box {
background-color: red;
}
}
/* 在 screen 类型 小于 240px 或 大于360px 小于 700px 加载 */
@media screen and (max-width: 240px), (min-width: 360px) and (max-width: 700px) {
.box1 {
background-color: burlywood;
}
}
媒体功能
height
输出设备中的页面可见区域高度。width
输出设备中的页面可见区域宽度。
max-aspect-ratio
输出设备的页面可见宽度与高度的最大比率。max-device-aspect-ratio
输出设备的屏幕可见宽度与高度的最大比率。
max-device-height
输出设备的屏幕可见的最大高度。max-device-width
输出设备的屏幕最大可见宽度。
max-height
输出设备中的页面最大可见区域高度。max-width
输出设备中的页面最大可见区域宽度。min-height
输出设备中的页面最小可见区域高度。min-width
输出设备中的页面最小可见区域宽度。
其他加载方式
style
标签上加载
<style media="(min-width: 500px)">
.box {
background-color: red;
}
</style>
<style media="(max-width: 500px">
.box {
background-color: burlywood;
}
</style>
- 根据
media属性
定义的媒体查询判断加载那个样式。
@import 使用时加载
@import url(./index.css) (min-width:350px);
@import url(./home.css) (max-width:750px);
- 在加载最后添加
()
定义媒体查询判断加载那个样式。
<picture>
标签
<picture>
<source media="(min-width: 650px)" srcset="demo1.jpg">
<source media="(min-width: 465px)" srcset="demo2.jpg">
<img src="img_girl.jpg">
</picture>
- 根据屏幕匹配的不同尺寸显示不同图片,如果没有匹配到或浏览器不支持
picture
属性则使用img
元素
JavaScript
语言区别
- 面向过程:通过函数一步一步实现这些步骤,接着依次调用即可
优点:性能上它是优于面向对象的,因为类在调用的时候需要实例化,开销过大。
缺点:不易维护、复用、扩展
用途:单片机、嵌入式开发、Linux/Unix等对性能要求较高的地方
- 面向对象:将数据与函数绑定到一起,进行封装减少了重复代码的重写过程
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 。
缺点:性能比面向过程低
(多态:同类不同对象)
- 基于原型的面向对象:js
- 比喻:原型(原始吸血鬼),被传染的吸血鬼
前端,追求灵活性,
- 基于类的面向对象:c++,Java,Python
类是模具,对象是实体
多用于服务端,更追求稳定性
比喻:量产的机器人
BOM,DOM,文档,对象,模型
BOM,
Browser Object Model浏览器对象模型,是JavaScript的组成之一,它提供了独立于内容与浏览器窗口进行交互的对象,使用浏览器对象模型可以实现与HTML的交互。它的作用是将相关的元素组织包装起来,提供给程序设计人员使用,从而降低开发人员的劳动量,提高设计Web页面的能力。
window : alert() , prompt() , confirm() , setInterval() , clearInterval() , setTimeout() , clearTimeout() ;
history : go(参数) , back() , foward() ;
location : herf属性.
1、window.location.href = '你所要跳转到的页面'; 2、window.open('你所要跳转到的页面’); 2、window.history.back(-1):返回上一页 4、window.history.go(-1/1):返回上一页或下一页五、 3、history.go("baidu.com");
4、window.print() 直接掉用打印窗口可以用来拔面试题。
DOM,全称Document Object Model 文档对象模型。JS中通过DOM来对HTML文档进行操作
文档是整个的HTML网页文档
将网页中的每一个部分都转换为了一个对象
使用模型来表示对象之间的关系,方便获取对象
ES6新增
- 数据类型:基本数据类型Symbol,引用数据类型Set ,Map
- 运算符:变量的解构赋值,对象和数组新增了扩展运算符
- 字符串方法:${ },需要配合单反引号好完成字符串拼接的功能,eg:`http://localhost:3000/search/users?q=${keyWord}`
- 块级作用域:let,const
- 原生提供 Proxy 构造函数,用来生成 Proxy 实例
- 定义类的语法糖(class)
- 模块化import/export
- 生成器(Generator)和遍历器(Iterator)
数据类型
基本数据类型
ES5:Null,Undefined,Number,String,Boolean
ES6新增:Symbol(仅有目的:作为对象属性的标识符,表示唯一的值)
var obj = {};
obj[Symbol("a")] = "a";
//会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,
//如果找到了,则返回它,
//否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
Symbol.for(key);
Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol
ES10新增:BigInt(表示任意的大整数)
let bnum=1684424684321231561n //方式1:数组后加n
bunm=BigInt("1684424684321231561")//方式2:调用BigInt
存储在栈(大小固定,先进后出)
引用数据类型
Object,function,Array,Date,RegExp,ES6新增:Set,MAP
地址存储在栈,内容存储在堆(树形结构,队列,先进先出)
声明和定义
变量声明不开辟内存,只是告诉编译器,要声明的部分存在,要预留部分的空间。var i;
变量定义开辟内存。 var i=123;
Null,NaN,Undefined
- null:空对象,一般作为对象的初值
- Nan:浮点数中表示未定义或不可表示的值,例如0/0、∞/∞、∞/−∞、−∞/∞、−∞/−∞
- undefined:未定义,声明但未定义,例如,形参未传参,获取return的函数返回,对象属性名不存在
toString,valueOf
javascript中所有数据类型都拥有valueOf和toString这两个方法,null和undefined除外
- valueOf偏向于运算,toString偏向于显示
对象字面量表达式是加 ():({}).toString()
- valueOf:除了Date其他的都是返回数据本身
==,===,Object.is()
- ==:自动数据类型转换
强制转换规则
- string和number,string->number,
- 其他类型和boolean,bool->number
- 对象和非对象,对象先调用 ToPrimitive 抽象操作(调用
valueOf()或
toString()
) - null==undefined值转为Boolean值false
- NaN!=NaN
- ===:严格模式,不进行自动数据类型转换,比较的是栈中值(即基本数据类型的值,或者引用数据类型的地址)
- Object.is():在===基础上特别处理了NaN,-0,+0,保证-0与+0不相等,但NaN与NaN相等
Object.is(+0,-0) //false
Object.is(NaN,NaN) //true
判断数据类型:typeof运算符,instance of运算符,isPrototypeOf()
方法,constructor,Object prototype
- typeof:判断 基本数据类型
- instance of:判断 引用数据类型,在其原型链中能否找到该类型的原型
isPrototypeOf()
:在表达式 "object instanceof AFunction
"中,object
的原型链是针对AFunction.prototype
进行检查的,而不是针对AFunction
本身。
Foo.prototype.isPrototypeOf(baz)
- constructor:判断 所有数据类型(不包含继承引用数据类型的自定义类型)
(数据).constructor === 数据类型
- Object.prototype.toString.call():Object 对象的原型方法 toString 来判断数据类型:
instance of (⭐手写)
第一个实例参数是否在第二个函数参数的原型链上
- 获取首个对象参数的原型对象
- 获取Fn函数的原型对象
- 进入死循环,当两个参数的原型对象相等时返回true
- 当两个参数的原型对象不相等时获取首个对象参数原型的原型并且循环该步骤直到null时返回false
const _instanceof = (target, Fn) => {
let proto = target.__proto__
let prototype = Fn.prototype
while(true) {
if(proto === Fn.prototype) return true
if(proto === null) return false
proto = proto.__proto__
}
}
const _instanceof = (target, Fn) => {
return Fn.prototype.isPrototypeOf(target);
}
new (⭐手写)
"_new"函数,该函数会返回一个对象,
该对象的构造函数为函数参数、原型对象为函数参数的原型,核心步骤有:
- 创建一个新对象
- 获取函数参数
- 将新对象的原型对象和函数参数的原型连接起来
- 将新对象和参数传给构造器执行
- 如果构造器返回的不是对象,那么就返回第一个新对象
const _new = function() {
const object1 = {}
const Fn = [...arguments].shift()
object1.__proto__ = Fn.prototype
const object2 = Fn.apply(object1, arguments)
return object2 instanceof Object ? object2 : object1
}
类型转换
- 转换为数字:
Number():可以把任意值转换成数字,如果要转换的字符串中有不是数字的值,则会返回NaN
parseInt(string,radix):解析一个字符串并返回指定基数的十进制整数,radix是2-36之间的整数,表示被解析字符串的基数。
parseFloat(string):解析一个参数并返回一个浮点数
隐式转换:
-
let str = '123'
-
let res = str - 1 //122
-
str+1 // '1231'
-
+str+1 // 124
- 转换为字符串
.toString() ⚠️注意:null,undefined不能调用
String() 都能转
隐式转换:当+两边有一个是字符串,另一个是其它类型时,会先把其它类型转换为字符串再进行字符串拼接,返回字符串
转换为布尔值
Boolean():0, ''(空字符串), null, undefined, NaN会转成false,其它都是true
隐式转换 !!
type of null
typeof null 的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits)
000: object - 当前存储的数据指向一个对象。
null 的值是机器码 NULL 指针(null 指针的值全是 0)
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
事件
文档和浏览器窗口中发生的特定交互
DOM事件流(event flow )
先捕获再冒泡。存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
- 事件捕获:由外往内,从事件发生的根节点开始,逐级往下查找,一直到目标元素。
- 事件冒泡:由内往外,从具体的目标元素触发,逐级向上传递,直到根节点。
element.addEventListener(event, function[, useCapture]);
//useCapture 默认为false,即冒泡阶段调用事件处理函数,
//为ture时,在事件捕获阶段调用处理函数
事件委托/代理(⭐手写)
事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。
应用:
1000
个button
注册点击事件。如果循环给每个按钮添加点击事件,那么会增加内存损耗,影响性能
好处:
-
替代循环绑定事件的操作,减少内存消耗,提高性能。比如:
ul
上代理所有li
的click
事件。 -
简化了
dom
节点更新时,相应事件的更新。比如:- 不用在新添加的
li
上绑定click
事件。 - 当删除某个
li
时,不用解绑上面的click
事件。
- 不用在新添加的
缺点:
- 事件委托基于冒泡,对于不冒泡的事件不支持。
- 层级过多,冒泡过程中,可能会被某层阻止掉。
- 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在
table
上代理td
,而不是在document
上代理td
。
阻止事件冒泡:event.stopPropagation() .stop修饰符
1. 给"ul"标签添加点击事件
2. 当点击某"li"标签时,该标签内容拼接"."符号。如:某"li"标签被点击时,该标签内容为".."
注意:
1. 必须使用DOM0级标准事件(onclick)
target表示当前触发事件的元素
currentTarget是绑定处理函数的元素
只有当事件处理函数绑定在自身的时候,target才会和currentTarget一样
<ul>
<li>.</li>
<li>.</li>
<li>.</li>
</ul>
<script type="text/javascript">
document.querySelector('ul').onclick=event=>{
event.target.innerText+='.'
}
</script>
发布订阅模式(⭐手写)
完成"EventEmitter"类实现发布订阅模式。
1. 同一名称事件可能有多个不同的执行函数:构造函数中创建”events“对象变量存放所有的事件
2. 通过"on"函数添加事件:订阅事件。当总事件中不存在此事件时创建新的事件数组,当存在时将”fn“函数添加在该事件对应数组中
3. 通过"emit"函数触发事件:发布事件,遍历该事件下的函数数组并全部执行
class EventEmitter {
constructor() {
this.events = {}//二维,events' funcs
}
//添加事件:订阅事件
on(event, fn) {
if(!this.events[event]) {//当总事件中不存在此事件时创建新的事件数组
this.events[event] = [fn]
} else { //当存在时将”fn“函数添加在该事件对应数组中
this.events[event].push(fn)
}
}
//触发事件:发布事件
emit(event) {
if(this.events[event]) {//遍历该事件下的函数数组并全部执行
this.events[event].forEach(callback => callback())
}
}
}
观察者模式(⭐手写)
"Observerd"类实现观察者模式。要求如下:
"Observer"为观察者,"Observerd"为被观察者
- 被观察者构造函数声明三个属性分别为"name"用于保存被观察者姓名、"state"用于保存被观察者状态、"observers"用于保存观察者们
- 被观察者创建"setObserver"函数,用于保存观察者们,该函数通过数组的push函数将观察者参数传入"observers"数组中
- 被观察者创建"setState"函数,设置该观察者"state"并且通知所有观察者,该函数首先通过参数修改被观察者的"state"属性,然后通过遍历"observers"数组分别调用各个观察者的"update"函数并且将该被观察者作为参数传入
- 观察者创建"update"函数,用于被观察者进行消息通知,该函数需要打印(console.log)数据,数据格式为:小明正在走路。其中"小明"为被观察者的"name"属性,"走路"为被观察者的"state"属性
//被观察者
class Observerd {
constructor(name) {
this.name = name
this.state = '走路'
this.observers = []
}
setObserver(observer) {
this.observers.push(observer)
}
setState(state) {
this.state = state
this.observers.forEach(observer => observer.update(this))
}
}
//观察者
class Observer {
constructor() {
}
update(observerd) {
console.log(observerd.name + '正在' + observerd.state)
}
}
封装事件绑定
绑定事件的元素.addEventListener(事件类型,执行函数,true/false) 默认值为false(即 使用事件冒泡)true 事件捕获
document.addEventListener("click", function(){
document.getElementById("demo").innerHTML = "Hello World";
});
执行上下文/作用域和作用链
作用域就是一个变量可以使用的范围
C/C++中有块级作用域,变量在声明它们的代码段之外是不可见的
javascript的作用域是相对函数而言的,可以称为函数作用域
全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
js为每一个执行环境关联了一个变量对象。环境中 定义的 所有变量和函数 都保存在这个对象中。
全局执行环境被认为是window对象
这样由多个执行上下文的变量对象构成的链表就叫做作用域链,从某种意义上很类似原型和原型链。
当前作用域外的变量都是自由变量,一个变量在当前作用域没有定义,但是被使用了,就会向上级作用域
作用域链和原型继承查找时的区别:
查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined
查找的属性在作用域链中不存在的话就会抛出ReferenceError。
this
若是在全局环境(例如,普通函数,匿名函数)中,则this指向window;( 严格模式下this会指向 undefined)
在对象里调用的this,指向调用函数的那个对象,
JS预解析(变量提升)
预编译/解析:JS代码在执行前,浏览器会对js代码进行扫描,默认的把所有带var和function声明的变量进行定义,创建执行上下文,初始化一些代码执行时需要用到的对象。
var,let 和 const关键字
在 ES6 之前,JavaScript 只有两种作用域: 全局变量 与 函数内的局部变量。
ES6新增,块级作用域(由大括号包裹,比如:if(){},for(){}等)
- var:可以跨块访问, 不能跨函数访问,允许重复声明,变量提升
- let、const:只能在块作用域里访问,不允许在相同作用域中重复声明,不存在变量提升
- const :声明一个只读的常量,使用时必须初始化(即必须赋值),一旦声明,常量的值就不能改变,(即,栈中的值不能变,引用类型,内存地址不能修改,可以修改里面的值。)。
原型链
console.log(Person.prototype);
// {constructor: ƒ}
// constructor: ƒ Person()
// arguments: null
// caller: null
// length: 0
// name: "Person"
// prototype: {constructor: ƒ}
// __proto__: ƒ ()
// [[FunctionLocation]]: 09-原型对象.html:8
// [[Scopes]]: Scopes[1]
// __proto__: Object
(引用类型统称为object类型)
所有引用类型
都有一个__proto__(隐式原型)
属性,属性值是一个普通的对象
所有函数
都有一个prototype(原型)
属性,属性值是一个普通的对象
构造函数和类同名
_ _ proto _ _
person.prototype.isPrototypeOf(stu)
只要调用对象在传入对象的原型链上都会返回true
首先,fn的构造函数是Foo()。所以:
fn._ _ proto _ _=== Foo.prototype
又因为Foo.prototype是一个普通的对象,它的构造函数是Object,
Foo.prototype=object
所以:
Foo.prototype._ _ proto _ _=== Object.prototype
【原型和原型链】什么是原型和原型链_TowYingWang的博客-CSDN博客_原型和原型链
寄生组合式继承(⭐手写)
通过寄生组合式继承使"Chinese"构造函数继承于"Human"构造函数。要求如下:
1. 给"Human"构造函数的原型上添加"getName"函数,该函数返回调用该函数对象的"name"属性
2. 给"Chinese"构造函数的原型上添加"getAge"函数,该函数返回调用该函数对象的"age"属性
- 在"Human"构造函数的原型上添加"getName"函数
- 在”Chinese“构造函数中通过call函数借助”Human“的构造器来获得通用属性
- Object.create函数返回一个对象,该对象的__proto__属性为对象参数的原型。此时将”Chinese“构造函数的原型和通过Object.create返回的实例对象联系起来
- 最后修复"Chinese"构造函数的原型链,即自身的"constructor"属性需要指向自身
- 在”Chinese“构造函数的原型上添加”getAge“函数
function Human(name) {
this.name = name
this.kingdom = 'animal'
this.color = ['yellow', 'white', 'brown', 'black']
}
Human.prototype.getName = function() {
return this.name
}
function Chinese(name,age) {
Human.call(this,name)//call函数借助”Human“的构造器来获得通用属性
this.age = age
this.color = 'yellow'
}
//返回的对象__proto__属性为对象参数的原型
Chinese.prototype = Object.create(Human.prototype)//使用现有的对象来作为新创建对象的原型
//修复"Chinese"构造函数的原型链,即自身的"constructor"属性需要指向自身
Chinese.prototype.constructor = Chinese
Chinese.prototype.getAge = function() {
return this.age
}
【原型和原型链】什么是原型和原型链_TowYingWang的博客-CSDN博客_原型和原型链
Iterator,for in,for of,forEach,map循环遍历
Iterator
一种接口,为各种不同的数据结构提供统一的访问机制
例如Array.prototype[@@iterator]()
Array
对象的 @@iterator
方法实现了迭代协议,并允许数组被大多数期望可迭代的语法所使用,例如展开语法和 for...of 循环。它返回一个迭代器,生成数组中每个索引的值。
for of
["a", "b", "c", "d"];for…of 循环读取键值// a b c d
支持迭代协议的数据结构(数组、字符串、Set、Map 等),不包括对象。
对于字符串,类数组,类型数组的迭代,循环内部调用的是数据结构的Symbol.iterator
方法。
for...of 不能循环普通的对象,需要通过和 Object.keys()搭配使用
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
for in
["a", "b", "c", "d"];for…in 循环读取键名 // 0 1 2 3
适用于遍历对象的 公有 可枚举属性,无法遍历 symbol 属性
hasOwnProperty() 方法来判断属性是否来自对象本身,并避免遍历原型链上的属性。
var triangle = {a: 1, b: 2, c: 3};
function ColoredTriangle() {
this.color = 'red';
}
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log(`obj.${prop} = ${obj[prop]}`);
}
}
// Output:
// "obj.color = red"
forEach
arr.forEach(value[,index,默认隐藏参数arr])
适用于需要知道索引值的数组遍历,但是不能中断( break 和 return )
如果需要跳出循环可以使用 some() 或 every() 方法
const isBelowThreshold = (currentValue) => currentValue < 30;
const array1 = [1, 30, 39, 29, 10, 13];
array1.forEach(element => console.log(element));
console.log(array1.every(isBelowThreshold));
// Expected output: false
//是不是至少有 1 个元素
console.log(array1.some(isBelowThreshold));//空数组,则返回false。
// Expected output: true
map
map 方法,基本用法与 forEach 一致
- forEach()方法不会返回执行结果,而是undefined
- map()方法会得到一个新的数组并返回
- 同样的一组数组,map()的执行速度优于 forEach()(map() 底层做了深度优化)
匿名函数、箭头函数、构造函数
匿名函数
有关键词 function,没有函数名。
//声明匿名函数
let myFun = function( a,b ){
console.info( a+b);
};
//执行
myFun( 10,30 );
//等同于 立即执行匿名函数
(function(a,b){
console.info( a+b );
})(10,30);
箭头函数
连function都没有的匿名函数,箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
箭头函数不绑定arguments,取而代之用rest参数解决
不可以使用yield,因此箭头函数不能用作Generator函数。
没有原型prototype,没有super用于访问原型属性。
//传递给getVal函数内的this并不是调用者自身,而是外部的this,即window
this.val = 2;
var obj = {
val: 1,
getVal: () => {
console.log(this.val);
}
}
obj.getVal(); // 2
构造函数
function Person(name,id)
{
this.name=name;
this.id=id;
this.sayHi=function() {
alert("Hi")
}
}
var p= new Person('参宿','7');
- 习惯上首字母大写
- 使用new关键字进行调用
this
普通函数的this在运行时创建,匿名函数this指向window,箭头函数的this是定义时确定。
箭头函数不能用作构造函数:
因为构造函数的this指向实例对象,但是箭头函数无法对创建出来的实例进行this绑定
也不能使用call()、apply()、bind() 去改变this的指向。
call、apply、bind改变this
call()和apply()唯一区别:
call()
接受的是一个参数列表
apply()
方法接受的是一个包含多个参数的数组。
var obj1 = {
name: 1,
getName: function (num = '') {
return this.name + num;
}
};
var obj2 = {
name: 2,
};
// 可以理解成在 obj2的作用域下调用了 obj1.getName()函数
console.log(obj1.getName()); // 1
console.log(obj1.getName.call(obj2, 2)); // 2 + 2 = 4
console.log(obj1.getName.apply(obj2, [2])); // 2 + 2 = 4
bind:语法和call一样,区别在于call立即执行,bind等待执行,bind不兼容IE6~8
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用
call (⭐手写)
// 给function的原型上面添加一个 _call 方法
Function.prototype._call = function (context) {
// 判断调用者是否是一个函数 this 就是调用者
if (typeof this !== 'function') {
throw new TypeError('what is to be a function')
}
// 如果有 context 传参就是传参者 没有就是window
context = context || window
// 保存当前调用的函数
context._this = this
// 截取传过来的参数
/*
arguments
a: 1
fn: ƒ fns()
*/
// 通过 slice 来截取传过来的参数
const local = [...arguments].slice(1)
// 传入参数调用函数
let result = context._this(...local)
// 删属性
delete context._this
return result
}
let obj = { a: 1 }
function fns(a, b) {
console.log(a, b);
console.log(this)
}
fns._call(obj, 23, 555)
apply(⭐手写)
// 给function的原型上面添加一个 _apply 方法
Function.prototype._apply= function (context) {
// 判断调用者是否是一个函数 this 就是调用者
if (typeof this !== 'function') {
throw new TypeError('what is to be a function')
}
// 如果有 context 传参就是传参者 没有就是window
context = context || window
// 保存当前调用的函数
context._this = this
// 截取传过来的参数
/*
arguments
a: 1
fn: ƒ fns()
*/
//!!!!!!!!!!!!!!与call的唯一区别!!!!!!!!!!!
// 这里开始判断传入的参数是否存在,此时参数是一个数组形式[thisArg,[传参]]
// 那么如果arguments[1]即传参存在的时候,就是需要传参调用保存的函数
// 如果不存在就直接调用函数
if (arguments[1]) {
result = context._this(...arguments[1])//!!!!将数组展开!!!!
} else {
result = context._this()
}
//!!!!!!!!!!!!!!与call的唯一区别!!!!!!!!!!!
// 删属性
delete context._this
return result
}
let obj = { a: 1 }
function fns(a, b) {
console.log(a, b);
console.log(this)
}
fns._call(obj, 23, 555)
bind(⭐手写)
Function.prototype._bind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('what is to be a function')
}
var _this = this; // 保存调用bind的函数
var context = context || window; // 确定被指向的this,如果context为空,执行作用域的this就需要顶上喽
return function(){
return _this.apply(context, [...arguments].slice(1)); // 如果只传context,则[...arguments].slice(1)为空数组
}
};
var obj = {
name: 1,
getName: function(){
console.log(this.name)
}
};
var func = function(){
console.log(this.name);
}._bind(obj);
func(); // 1
闭包(closure)
类比背包,当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。
var name = '余光';
function foo() {
console.log(name); // 余光
}
(function (func) {
var name = '老王';
func()
})(foo); // 余光
因为js作用域生命周期在于内部脚本是否全部执行完毕才会销毁,并且不会带到父级作用域;
当函数内部返回一个函数,子函数没在父级作用域内完成整个生命周期的话,父级函数是没办法完成一整个生命周期的,闭包正是利用这一点卡住了父级函数的作用域。
因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
面试的时候,直接回答函数嵌套函数,且内部函数调用父级作用域的变量就可以称之为闭包了。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
- 闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
- 滥用闭包容易内存泄漏。
- 使用场景 : 防抖、节流、函数套函数避免全局污染
正则表达式Regular Expression(RegExp)
字符串搜索模式。
/正则表达式主体/修饰符(可选)
RegExp 对象是一个预定义了属性和方法的正则表达式对象
regexp.test(str)返回Bool
regexp.exec(str)返回匹配的子串 或者 null
常用修饰符
i | ignoreCase 执行对大小写不敏感的匹配。 |
g | global 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。 |
常用字符
\标记下一个字符是特殊字符或文字。例如,"n”和字符"n”匹配。"\n"则和换行字符匹配。
^匹配输入的开头.
$匹配输入的末尾
·匹配除换行字符外的任何单个字符
*匹配前一个字符零或多次。例如,"zo*”与"z”或"zoo”匹配。
+匹配前一个字符一次或多次。例如,"zo+"与"zoo”匹配,但和"z”不匹配。
?匹配前一个字符零或一次。例如,"a?ve?”和"never"中的“"ve”匹配。
x|y 匹配x或y
{n}匹配n次。n是非负整数
{n,} n是一个非负整数。至少匹配n次。例如,"o{2,)"和"Bob”中的"o”不匹配,但和"foooood"中的所有o匹配。"o{1}”与"o+”等效。"o{0,}”和"o*”等效。
{n,m}m和n是非负整数。至少匹配n次而至多匹配 m次。例如,"o{1,3]"和"fooooood”中的前三个o匹配。"o{0,1}”和“o?”等效。
[xyz]匹配括号内的任一字符。例如,"[abc]"和"plain”中的"a”匹配。
[^xyz]匹配非括号内的任何字符。例如,"[^abc]"和“plain”中的"p”匹配。
[a-z]字符范围。和指定范围内的任一字符匹配。例如,"[a-z]”匹配"a"到"z"范围内的任一小写的字母表字符。
[^m-z]否定字符范围。匹配不在指定范围内的任何字符。例如,"[m-z]”匹配不在"m"到"z"范围内的任何字符。
助记:digital
\d匹配数字字符。等价于[0-9]。
\D匹配非数字字符。等价于[^0-9]。
助记:space
\s匹配任何空白,包括空格、制表、换页等。与"[ \fn\rlt\v]”等效。
\S匹配任何非空白字符。与"[^ \fn\rlt\v]”等效。
\w匹配包括下划线在内的任何字字符。与"[A-Za-z0-9_]”等效。
\W匹配任何非字字符。与"[^A-Za-z0-9_]”等效。
合法的URL (⭐手写)
URL结构一般包括协议、主机名、主机端口、路径、请求信息、哈希
- 首先必须是以http(s)开头并且可以不包含协议头部信息
- 主机名可以使用"-"符号,所以两种情况都要判断,包含"-"或不包含"-"
- 顶级域名很多,直接判断"."之后是否为字母即可
- 最后判断端口、路径和哈希,这些参数可有可无
域名中只能包含以下字符
1. 26个英文字母
2. "0,1,2,3,4,5,6,7,8,9"十个数字
3. "-"(英文中的连词号,但不能是第一个字符)
https://www.bilibili.com/video/BV1F54y1N74E/?spm_id_from=333.337.search-card.all.click&vd_source=6fd32175adc98c97cd87300d3aed81ea
//开始: ^
//协议: http(s)?:/\/\
//域名: [A-z0-9]+-[A-z0-9]+|[A-z0-9]+
//顶级域名 如com cn,2-6位: [A-z]{2,6}
//端口 数字: (\d+)?
//路径 任意字符 如 /login: (\/.+)?
//哈希 ? 和 # ,如?age=1: (\?.+)?(#.+)?
//结束: $
// https:// www.bilibili com /video/BV1F54y1N74E ?spm..
/^(http(s)?:\/\/)?(([a-zA-Z0-9]+-[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)+([a-zA-Z]{2,6})(:\d+)?(\/.+)?(\?.+)?(#.+)?$/.test(url)
函数
函数的length
属性
将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
function fun(a = 1, b, c, d) { }
console.log(fun.length) // 0
函数声明与函数表达式的区别
函数声明会将那个函数提升到最前面(即使你写代码的时候在代码块最后才写这个函数),成为全局函数。
函数声明要指定函数名,而函数表达式不用,可以用作匿名函数。
立即执行函数(iife)
( function( ){ })( )
原理:括号内部不能包含语句,当解析器对代码进行解释的时候,先碰到了(), 然后碰到function关键字
就会自动将()里面的代码识别为函数表达式而不是函数声明。
作用:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量。
常用方法
异或运算^
按位异或,相同为0,不同为1
运算法则:
1.交换律(随便换像乘一样):a ^ b ^ c === a ^ c ^ b
2.任何数于0异或为任何数 0 ^ n === n
3.相同的数异或为0: n ^ n === 0
Math
//e=2.718281828459045
Math.E;
//绝对值
Math.abs()
//基数(base)的指数(exponent)次幂,即 base^exponent。
Math.pow(base, exponent)
//max,min不支持传递数组
Math.max(value0, value1, /* … ,*/ valueN)
Math.max.apply(null,array)
apply会将一个数组装换为一个参数接一个参数
null是因为没有对象去调用这个方法,只需要用这个方法运算
//取整
Math.floor() 向下取一个整数(floor地板)
Math.ceil(x) 向上取一个整数(ceil天花板)
Math.round() 返回一个四舍五入的值
Math.trunc() 直接去除小数点后面的值
Number
0B,0O为ES6新增
- 二进制:有前缀0b(或
0B
)的数值,出现0,1以外的数字会报错(b:binary) - 八进制:有前缀0o(或
0O
)的数值,或者是以0后面再跟一个数字(0-7)。如果超出了前面所述的数值范围,则会忽略第一个数字0,视为十进制数(o:octonary) - 注意:八进制字面量在严格模式下是无效的,会导致支持该模式的JavaScript引擎抛出错误
- 十六进制:有前缀0x,后跟任何十六进制数字(0~9及A~F),字母大小写都可以,超出范围会报错
特殊值
- Number.MIN_VALUE:5e-324
- Number.MAX_VALUE:1.7976931348623157e+308
- Infinity ,代表无穷大,如果数字超过最大值,js会返回Infinity,这称为正向溢出(overflow);
- -Infinity ,代表无穷小,小于任何数值,如果等于或超过最小负值-1023(即非常接近0),js会直接把这个数转为0,这称为负向溢出(underflow)
- NaN ,Not a number,代表一个非数值
- isNaN():用来判断一个变量是否为非数字的类型,如果是数字返回false;如果不是数字返回true。
- isFinite():数值是不是有穷的
var result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); //false
typeof NaN // 'number' ---
NaN
不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
NaN === NaN // false ---
NaN
不等于任何值,包括它本身(1 / +0) === (1 / -0) // false ---除以正零得到
+Infinity
,除以负零得到-Infinity
,这两者是不相等的
科学计数法
对于那些极大极小的数值,可以用e表示法(即科学计数法)表示的浮点数值表示。
等于e前面的数值乘以10的指数次幂
numObj.toFixed(digits)//用定点表示法来格式化一个数值
function financial(x) {
return Number.parseFloat(x).toFixed(2);
}
console.log(financial(123.456));
// Expected output: "123.46"
console.log(financial(0.004));
// Expected output: "0.00"
console.log(financial('1.23e+5'));
// Expected output: "123000.00"
取余是数学中的概念,
取模是计算机中的概念,
两者都是求两数相除的余数
1.当两数符号相同时,结果相同,比如:7%4 与 7 Mod 4 结果都是3
2.当两数符号不同时,结果不同,比如 (-7)%4=-3和(-7)Mod4=1
取余运算,求商采用fix 函数,向0方向舍入,取 -1。因此 (-7) % 4 商 -1 余数为 -3
取模运算,求商采用 floor 函数,向无穷小方向舍入,取 -2。因此 (-7) Mod 4 商 -2 余数为 1
key:((n % m) + m) % m;
Number.prototype.mod = function(n) {
return ((this % n) + n) % n;
}
// 或
function mod(n, m) {
return ((n % m) + m) % m;
}
Map
保存键值对,任何值(对象或者基本类型)都可以作为一个键或一个值。
Map的键可以是任意值,包括函数、对象或任意基本类型。
object的键必须是一个String或是Symbol 。
const contacts = new Map()
contacts.set('Jessie', {phone: "213-555-1234", address: "123 N 1st Ave"})
contacts.has('Jessie') // true
contacts.get('Hilary') // undefined
contacts.delete('Jessie') // true
console.log(contacts.size) // 1
function logMapElements(value, key, map) {
console.log(`m[${key}] = ${value}`);
}
new Map([['foo', 3], ['bar', {}], ['baz', undefined]])
.forEach(logMapElements);
// Expected output: "m[foo] = 3"
// Expected output: "m[bar] = [object Object]"
// Expected output: "m[baz] = undefined"
Set
值的集合,且值唯一
虽然NaN !== NaN,但set中NaN 被认为是相同的
let setPos = new Set();
setPos.add(value);//Boolean
setPos.has(value);
setPos.delete(value);
function logSetElements(value1, value2, set) {
console.log(`s[${value1}] = ${value2}`);
}
new Set(['foo', 'bar', undefined]).forEach(logSetElements);
// Expected output: "s[foo] = foo"
// Expected output: "s[bar] = bar"
// Expected output: "s[undefined] = undefined"
set判断值相等的机制
//Set用===判断是否相等
const set= new Set();
const obj1={ x: 10, y: 20 },obj2={ x: 10, y: 20 }
set.add(obj1).add(obj2);
console.log(obj1===obj2);//false
console.log(set.size);// 2
set.add(obj1);
console.log(obj1===obj1);//true
console.log(set.size);//2
数组去重 (⭐手写)
// Use to remove duplicate elements from the array
const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)])
// [2, 3, 4, 5, 6, 7, 32]
Array
//创建字符串
//join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串
//如果数组只有一个元素,那么将返回该元素而不使用分隔符。
Array.join()
Array.join(separator)
//################创建数组:
//伪数组转成数组
Array.from(arrayLike, mapFn)
console.log(Array.from('foo'));
// Expected output: Array ["f", "o", "o"]
console.log(Array.from([1, 2, 3], x => x + x));
// Expected output: Array [2, 4, 6]
console.log( Array.from({length:3},(item, index)=> index) );// 列的位置
// Expected output:Array [0, 1, 2]
//################原数组会改变:
arr.reverse()//返回翻转后的数组
// 无函数
arr.sort()//默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16
// 比较函数
arr.sort(compareFn)
function compareFn(a, b) {
if (在某些排序规则中,a 小于 b) {
return -1;
}
if (在这一排序规则下,a 大于 b) {
return 1;
}
// a 一定等于 b
return 0;
}
//升序
function compareNumbers(a, b) {
return a - b;
}
//固定值填充
arr.fill(value)
arr.fill(value, start)
arr.fill(value, start, end)
//去除
array.shift() //从数组中删除第一个元素,并返回该元素的值。
array.pop() //从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度。
array.push() //将一个或多个元素添加到数组的末尾,并返回该数组的新长度
//unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度
array.unshift(element0, element1, /* … ,*/ elementN)
//粘接,通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
array.splice(start)
array.splice(start, deleteCount)
array.splice(start, deleteCount, item1)
array.splice(start, deleteCount, item1, item2...itemN)
//################原数组不会改变:
//切片,浅拷贝(包括 begin,不包括end)。
array.slice()
array.slice(start)
array.slice(start, end)
//展平,按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
array.flat()//不写参数默认一维
array.flat(depth)
//过滤器,函数体 为 条件语句
// 箭头函数
filter((element) => { /* … */ } )
filter((element, index) => { /* … */ } )
filter((element, index, array) => { /* … */ } )
array.filter(str => str .length > 6)
//遍历数组处理
// 箭头函数
map((element) => { /* … */ })
map((element, index) => { /* … */ })
map((element, index, array) => { /* … */ })
array.map(el => Math.pow(el,2))
//map和filter同参
//接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
// 箭头函数
reduce((previousValue, currentValue) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex, array) => { /* … */ } )
reduce((previousValue, currentValue) => { /* … */ } , initialValue)
reduce((previousValue, currentValue, currentIndex) => { /* … */ } , initialValue)
array.reduce((previousValue, currentValue, currentIndex, array) => { /* … */ }, initialValue)
//一个“reducer”函数,包含四个参数:
//previousValue:上一次调用 callbackFn 时的返回值。
//在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,
//否则为数组索引为 0 的元素 array[0]。
//currentValue:数组中正在处理的元素。
//在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],
//否则为 array[1]。
//currentIndex:数组中正在处理的元素的索引。
//若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。
//array:用于遍历的数组。
//initialValue 可选
//作为第一次调用 callback 函数时参数 previousValue 的值。
//若指定了初始值 initialValue,则 currentValue 则将使用数组第一个元素;
//否则 previousValue 将使用数组第一个元素,而 currentValue 将使用数组第二个元素。
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
Array.filter(⭐手写)
Array.prototype._filter = function(Fn) {
if (typeof Fn !== 'function') return
const array = this
const newArray = []
for (let i=0; i<array.length; i++) {
const result = Fn.call(null, array[i], i, array)
result && newArray.push(array[i])
}
return newArray
}
Array.map(⭐手写)
Array.prototype._map = function(Fn) {
if (typeof Fn !== 'function') return
const array = this
const newArray = []
for (let i=0; i<array.length; i++) {
const result = Fn.call(null, array[i], i, array)
//##########与filter的唯一不同
newArray.push(result)
}
return newArray
}
Array.reduce(⭐手写)
Array.prototype._reduce = function(fn,initialValue = 0){
if(typeof fn !== 'function') return;
let res = initialValue
this.forEach((value,index,arr)=>{
res = fn(res,value,index,arr)
})
return res
}
String
str.charAt(index)//获取第n位字符
str.charCodeAt(n)//获取第n位UTF-16字符编码 (Unicode)A是65,a是97
String.fromCharCode(num1[, ...[, numN]])//根据UTF编码创建字符串
String.fromCharCode('a'.charCodeAt(0))='a'
str.trim()//返回去掉首尾的空白字符后的新字符串
str.split(separator)//返回一个以指定分隔符出现位置分隔而成的一个数组,数组元素不包含分隔符
const str = 'The quick brown fox jumps over the lazy dog.';
const words = str.split(' ');
console.log(words[3]);
// Expected output: "fox"
str.toLowerCase( )//字符串转小写;
str.toUpperCase( )//字符串转大写;
str.concat(str2, [, ...strN])
str.substring(indexStart[, indexEnd]) //提取从 indexStart 到 indexEnd(不包括)之间的字符。
str.substr(start[, length]) //没有严格被废弃 (as in "removed from the Web standards"), 但它被认作是遗留的函数并且可以的话应该避免使用。它并非 JavaScript 核心语言的一部分,未来将可能会被移除掉。
str.indexOf(searchString[, position]) //在大于或等于position索引处的第一次出现。
str.match(regexp)//找到一个或多个正则表达式的匹配。
const paragraph = 'The quick brown fox jumps over the lazy dog. It barked.';
let regex = /[A-Z]/g;
let found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T", "I"]
regex = /[A-Z]/;
found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T"]
//match类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
var str = '123123000'
str.match(/\w{3}/g).join(',') // 123,123,000
str.search(regexp)//如果匹配成功,则 search() 返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1
const paragraph = '? The quick';
// Any character that is not a word character or whitespace
const regex = /[^\w\s]/g;
console.log(paragraph.search(regex));
// Expected output: 0
str.repeat(count)//返回副本
str.replace(regexp|substr, newSubStr|function)//返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。
const p = 'lazy dog.Dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replace('dog', 'monkey'));
// "lazy monkey.Dog lazy"
let regex = /dog/i;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replace(regex, 'ferret'));
//"lazy ferret.Dog lazy"
regex = /d|Dog/g;
console.log(p.replace(regex, 'ferret'));
//"lazy ferretog.ferret lazy"
//当使用一个 regex 时,您必须设置全局(“g”)标志, 否则,它将引发 TypeError:“必须使用全局 RegExp 调用 replaceAll”。
const p = 'lazy dog.dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replaceAll('dog', 'monkey'));
// "lazy monkey.monkey lazy"
let regex = /dog/g;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replaceAll(regex, 'ferret'));
//"lazy ferret.ferret lazy"
Class
类声明
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
类表达式
// 未命名/匿名类
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// output: "Rectangle"
// 命名类
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 输出:"Rectangle2"
使用 super 调用超类
super 关键字用于调用对象的父对象上的函数。
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Lion extends Cat {
constructor(name,id) {
super(name);
this.id = id;
}
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}
private
类属性在默认情况下是公有的,但可以使用增加哈希前缀 #
的方法来定义私有类字段
从类外部引用私有字段是错误的。它们只能在类里面中读取或写入。
class ClassWithPrivateField {
#privateField;
}
class ClassWithPrivateMethod {
#privateMethod() {
return 'hello world';
}
}
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD;
}
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 'hello world';
}
}
static 关键字
定义静态方法和值。不能在类的实例上调用静态方法,而应该通过类本身调用
静态方法调用同一个类中的其他静态方法,可使用 this
关键字
class StaticMethodCall {
static staticMethod() {
return 'Static method has been called';
}
static anotherStaticMethod() {
return this.staticMethod() + ' from another static method';
}
}
StaticMethodCall.staticMethod();
// 'Static method has been called'
StaticMethodCall.anotherStaticMethod();
// 'Static method has been called from another static method'
非静态方法中,不能直接使用 this 关键字来访问静态方法。
而是要用类名来调用:CLASSNAME.STATIC_METHOD_NAME()
,
或者用构造函数的属性来调用该方法: this.constructor.STATIC_METHOD_NAME()
.
class StaticMethodCall {
constructor() {
console.log(StaticMethodCall.staticMethod());
// 'static method has been called.'
console.log(this.constructor.staticMethod());
// 'static method has been called.'
}
static staticMethod() {
return 'static method has been called.';
}
}
创建对象的方式
1、{}
2、new Object()/** 使用{}创建对象,等同于 new Object(); **/ var o = {};
3、使用字面量
var person = {name: 'zhang', age:20}
4、工厂模式
'use strict'; // 使用工厂模式创建对象 // 定义一个工厂方法 function createObject(name){ var o = new Object(); o.name = name; o.sayName = function(){ alert(this.name); }; return o; } var o1 = createObject('zhang'); var o2 = createObject('li'); //缺点:调用的还是不同的方法 //优点:解决了前面的代码重复的问题 alert(o1.sayName===o2.sayName);//false
5、构造函数模式(constructor)
<script> 'use strict'; /** * 构造函数模式创建对象 **/ function Person(name){ this.name = name; this.sayName = function(){ alert(this.name); }; } var p1 = new Person('zhang'); var p2 = new Person('li'); p1.sayName(); p2.sayName(); alert(p1.constructor === p2.constructor);//true alert(p1.constructor === Person);//true alert(typeof(p1));//object alert(p1 instanceof Object); //true alert(p2 instanceof Object); //trueb alert(p1.sayName===p2.sayName);//false </script>
6、原型模式(prototype)<script> 'use strict'; /* * 原型模式创建对象 */ function Animal() { } Animal.prototype.name = 'animal'; Animal.prototype.sayName = function () { alert(this.name); }; var a1 = new Animal(); var a2 = new Animal(); a1.sayName(); alert(a1.sayName === a2.sayName);//true alert(Animal.prototype.constructor);//function Animal(){} alert(Animal.prototype.constructor==Animal);//true </script>
如果往新建的对象中加入属性,那么这个属性是放在对象中,如果存在与原型同名的属性,也不会改变原型的值。但是访问这个属性,拿到的是对象的值。
访问的顺序:对象本身>构造函数的prototype
如果对象中没有该属性,则去访问prototype,如果prototype中没有,继续访问父类,直到Object,如果都没有找到,返回undefined
<script> 'use strict'; /* * 原型模式创建对象 */ function Animal() { } Animal.prototype.name = 'animal'; Animal.prototype.sayName = function () { alert(this.name); }; var a1 = new Animal(); var a2 = new Animal(); a1.sayName(); alert(a1.sayName === a2.sayName);//true alert(Animal.prototype.constructor);//function Animal(){} //修改a2.name,a1的name不会变 a2.name = 'dog'; a2.sayName();//dog a1.sayName();//animal </script>
假如原型中包含有引用类型的属性,那么如果某个对象修改了该属性的值,所有的该原型创建的对象访问的值都会改变。
<script> 'use strict'; //原型模式2 //存在的问题:如果原型中含有引用类型 function Animal (){} Animal.prototype = { name: 'animal', friends: ['dog','cat'], sayName: function(){ alert(this.name); } }; var a1 = new Animal(); var a2 = new Animal(); a2.friends.push('snake'); alert(a2.friends);//[dog,cat,snake] alert(a1.friends);//[dog,cat,snake] </script>
7、构造函数+原型模式<script> 'use strict'; function Animal(name){ this.name = name; this.friends = ['dog','cat']; } Animal.prototype.sayName = function(){ alert(this.name); }; var a1 = new Animal('d'); var a2 = new Animal('c'); a1.friends.push('snake'); alert(a1.friends);//[dog,cat,snake] alert(a2.friends);//[dog,cat] </script>
Object
//创建的新对象.prototype=proto(参数对象)
Object.create(proto[, propertiesObject])
propertiesObject 可选
如果该参数被指定且不为 undefined,则该传入对象的自有 可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。这些属性对应于 Object.defineProperties() 的第二个参数。
描述符
- 拥有布尔值的键
configurable
、enumerable
和writable
的默认值都是false
。- 属性值和函数的键
value
、get
和set
字段的默认值为undefined
。数据描述符:
configurable
true
时,描述符才能够被改变,同时该属性也能从对应的对象上被删除。enumerable 属性
定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
writable
当且仅当该属性的
writable
键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符 (en-US)改变。存取描述符:
get
属性的 getter 函数,如果没有 getter,则为
undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
set
属性的 setter 函数,如果没有 setter,则为
undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。
描述符可拥有的键值
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
如果一个描述符不具有 value
、writable
、get
和 set
中的任意一个键,那么它将被认为是一个数据描述符。
如果一个描述符同时拥有 value
或 writable
和 get
或 set
键,则会产生一个异常。
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
//在一个对象上定义一个新的属性或修改现有属性,并返回该对象
Object.defineProperty(obj, prop, descriptor)
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
//用 Symbol 类型的值来做对象的 key 与常规的定义或修改不同
//Object.defineProperty 是定义 key 为 Symbol 的属性的方法之一。
Object.defineProperty(o, Symbol.for('e'), {
value: 5,
enumerable: true
});
//在一个对象上定义新的属性或修改现有属性,并返回该对象
Object.defineProperties(obj, props)
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// etc. etc.
});
//Object.prototype.hasOwnProperty()指示对象自身属性中是否具有指定的属性
const object1 = {};
object1.property1 = 42;
console.log(object1.hasOwnProperty('property1'));
// Expected output: true
console.log(object1.hasOwnProperty('hasOwnProperty'));
// Expected output: false
//Object.getPrototypeOf(object)返回对象原型
const prototype1 = {};
const object1 = Object.create(prototype1);
console.log(Object.getPrototypeOf(object1) === prototype1);
// Expected output: true
//Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
//Object.values() 方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用 for...in 循环的顺序相同(区别在于 for-in 循环枚举原型链中的属性)。
var obj = { foo: 'bar', baz: 42 };
console.log(Object.values(obj)); // ['bar', 42]
// non-object argument will be coerced to an object
console.log(Object.values('foo')); // ['f', 'o', 'o']
Object.create (⭐手写)
该函数创建一个新对象,使用现有的对象来提供新创建的对象的proto,核心步骤有:
- 创建一个临时函数
- 将该临时函数的原型指向对象参数
- 返回该临时对象的实例
Object.create法创建一个新对象,使用现有的对象来提供新创建的对象的proto。
const _objectCreate = proto => {
if(typeof proto !== 'object' || proto === null) return
const fn = function() {}
fn.prototype = proto
return new fn()
}
Object.freeze (⭐手写)
Object.freeze = writable: false + Object.seal = writable: false + Object.preventExtensions + configable: false
- Symbol 类型作为 key 值的情况,也要冻结
- 只冻结对象自有的属性(使用 for ... in 会把原型链上的可枚举属性遍历出来)。
- 注意不可扩展性(不能添加新属性,使用 Object.preventExtensions() 或 Object.seal() 实现,同时也相当于把原型链冻结)。
key:
- Object.getOwnPropertyNames/Symbol
- forEach
- Object.defineProperty:configurable,writable
- Object.preventExtensions(object)
const _objectFreeze = object => {
if(typeof object !== 'object' || object === null) {
throw new TypeError(`the ${object} is not a object`)
}
const keys = Object.getOwnPropertyNames(object);
const symbols = Object.getOwnPropertySymbols(object);
[...keys, ...symbols].forEach(key => {
Object.defineProperty(object, key, {
configurable: false,
writable: false,
})
})
Object.preventExtensions(object)
}
indexOf (⭐手写)
在函数前加上波浪号,其作用是把函数声明转换为表达式,这样就可以直接运行
~function sayHello(){
console.log('hello');
}()
//Expected output: hello
~ function () {
function myIndexOf(searchStr) {
// 这个也可以正则实现 下面代码
// let reg = new RegExp(searchStr)
// res = reg.exec(this)
// return res === null ? -1 : res.index
let len = this.length
let searchLen=searchStr.length
if (searchLen > len) return -1
// 如果输入的字符串大于要检测的字符串直接 -1
for (var i = 0; i <= len-searchLen; i++) {
if (this.substring(i,searchLen+i) === searchStr) {
return i
}
}
return -1
}
String.prototype.myIndexOf = myIndexOf
}()
let str = 'dwanlghMappaw'
let searchStr= 'hM'
console.log(str.myIndexOf(searchStr));
高阶函数和函数的珂里化Currying
高阶函数:参数 或者 返回值为函数
函数柯里化:返回值为函数,实现多次接收参数最后统一处理的函数编码
作用:能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参。
用途:延迟计算、参数复用、动态生成函数(都是闭包的用途)。
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
Arguments
对象
是所有(非箭头)函数中都可用的局部变量。类似于Array
,但除了 length 属性和索引元素之外没有任何Array
属性。
function add() {
var sum =0,
len = arguments.length;
for(var i=0; i<len; i++){
sum += arguments[i];
}
return sum;
}
add() // 0
add(1) // 1
add(1,2,3,4); // 10
深浅拷贝
基本类型:内存区域存储的是值,不存在深拷贝和浅拷贝
引用类型:内存区域存储的是地址,浅拷贝只拷贝一层(内存地址),而深拷贝是层层拷贝(拷贝内容,新开辟内存)。
深拷贝(⭐手写)
function cloneDeep(arr = {}) {
// 终止递归 判断如果传进来的数据不是 object 或者 传进来的是一个 null 直接返回
if (!arr || typeof arr != 'object' || arr == null) return arr
// 用 instanceof 判断原型链上是否有该类型的原型 是 Array => [] ! Arrays =>{}
let result=arr instanceof Array ? [] : {}
// forin 循环对象的key值
for (const key in arr) {
// 对象 key 赋值 result
result[key] = cloneDeep(arr[key])
}
return result
}
严格模式
严格模式通过抛出错误来消除了一些原有静默错误。
- 严格模式下,不允许给未声明的变量赋值
严格模式修复了一些导致 JavaScript 引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。
严格模式禁用了在 ECMAScript 的未来版本中可能会定义的一些语法。
防抖 (⭐手写)
触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,会重计算函数执行时间。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
防抖: <input id="input" type="text">
</body>
<script>
// 防抖的核心代码
function debounce(fun,time) {
let flag // 定义状态
return function () {
clearTimeout(flag)// 在执行之前 清除 定时器的 flag 不让他执行
flag = setTimeout(() => {
fun.call(this,arguments)//拿到正确的this对象,即事件发生的dom
}, time)
}
}
let val = debounce(function (val) {
console.log(val)
},1000)
// 监听拿到input输入的值
input.addEventListener('input', function (e) {
val(e.target.value)
})
</script>
</html>
节流(⭐手写)
连续触发事件但是在 n 秒中只执行一次函数。两种方式可以实现,分别是时间戳版和定时器版。
<body>
<button id="button">1秒执行一次</button>
</body>
<script>
/*
定时器版本的
fns 回调函数
time 间隔时间
function throttle(fun, time) {
let flag // 定义一个空状态
return function () { // 内部函数访问外部函数形成闭包
if (!flag) { // 状态为空执行
flag = setTimeout(() => {
fns.apply(this, arguments) // 改变this指向 把 event 事件对象传出去
flag = null
}, time)
}
}
}
*/
function throttle(fun, time) {
let last = 0
return function () {
let now = Date.now()
// 当前的值 减去上一次的值 >= 传过来的事件 执行
if (now - last >= time) {
fun.apply(this, arguments)
last = now
}
}
}
button.onclick = throttle((e) => {
console.log(e)
}, 1000)
</script>
防抖、节流应用
防止某一时间频繁触发
- 防抖debounce:time内只执行一次
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
- 节流throttle: 间隔time执行
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
垃圾回收(GC)
GC
即 Garbage Collection
浏览器的js具有自动垃圾回收机制,垃圾回收机制也就是自动内存管理机制,垃圾收集器会定期的找出不可访问的值,然后释放内存,所以将不需要的对象设为null即可。
内存分配
-
First-fit
,找到第一个的大于等于size
的块立即返回 -
Best-fit
,遍历整个空闲列表,返回大于等于size
的最小分块 -
Worst-fit
,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size
大小,并将该部分返回
Worst-fit
的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,
First-fit
和 Best-fit
来说,考虑到分配的速度和效率 First-fit
是更为明智的选择
回收策略
标记清除(Mark-Sweep):最常用
(根对象,在浏览器环境中包括
全局Window对象
、文档DOM树
等)
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点
简单
缺点
- 内存碎片化,清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了
内存碎片
,存在内存分配的问题- 分配速度慢,因为即便是使用
First-fit
策略,其操作仍是一个O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
标记整理(Mark-Compact)
改善标记清除清除之后剩余的对象位置不变而导致的空闲内存不连续
标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
引用计数(Reference Counting),早的一种垃圾回收算法
它把
对象是否不再需要
简化定义为 没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了,因为它的问题很多跟踪记录每个变量值被使用的次数
当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
如果同一个值又被赋给另一个变量,那么引用数加 1
如果该变量的值被其他的值覆盖了,则引用次数减 1
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
优点
引用值为 0 时,可以立即回收垃圾
缺点
计数器需要占内存
不知道被引用数量的上限
无法解决循环引用无法回收的问题,这也是最严重的
内存泄漏
如果 那些不再使用的变量,它们所占用的内存 不去清除的话就会造成内存泄漏
造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果。
比如说:
1、闭包:在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。
2、DOM:当原有的DOM被移除时,子结点引用没有被移除则无法回收
JS中拥有自动的垃圾回收机制,
宏任务、微任务、Event-Loop
js引擎会优先执行微任务,例如:网页加载完毕,但是图片没加载出来
- 微任务microtask(异步):可以理解为task执行完后立刻执行,Promise async/await。
- 宏任务macrotask: setTimeout,setInterval一类的定时事件,Ajax,DOM事件,script 脚本的执行、 I/O 操作、UI 渲染等。
例如:new Promise
实例化是同步,而then
中注册的回调才是异步执行的。
例如:等待的客户为宏任务,他的每个业务为微任务
每办理完一个业务,柜员就会问当前的客户,是否还有其他需要办理的业务。(检查还有没有微任务需要处理)
而客户明确告知说没有事情以后,柜员就去查看后边还有没有等着办理业务的人。(结束本次宏任务、检查还有没有宏任务需要处理)
这个检查的过程是持续进行的,每完成一个任务都会进行一次,而这样的操作就被称为Event Loop
setImmediate与setTimeout的区别
setImmediate
为一次Event Loop
执行完毕后调用。setTimeout
则是通过计算一个延迟时间后进行执行。
如果在主进程中直接执行这两个操作,很难保证哪个会先触发。
当注册这两个任务耗时超过delay(s)
,定时器处于可执行回调的状态,会先执行定时器,
执行完定时器以后才是结束了一次Event Loop
,这时才会执行setImmediate
。
JS延迟加载的方式
JavaScript 是单线程(js不走完下面不会走是因为同步)会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
1.把JS放在页面的最底部(css放顶部,js放底部是框架常见优化)
2.script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
3.是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行, 该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
4.动态创建script标签,监听dom加载完毕再引入js文件
服务器端渲染
基本概念
SSR (server side render)服务端渲染,是指由服务侧(server side)完成页面的DOM结构拼接,然后发送到浏览器,为其绑定状态与事件,成为完全可交互页面的过程。
CSR(client side render)客户端渲染,是指由客户端(client side)JS完成页面和数据的拼接,生成DOM结构再交由浏览器渲染成页面的过程。
SPA(single page application)单页面应用,只是局部更新内容。SPA实现的原理就采用了CSR,页面中所有内容由JS控制,需要浏览器进行JS解析才能显示出来。
SEO(search engine optimization)搜索引擎优化,利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。
服务器端渲染SSR
前端耗时少。因为后端拼接了html,浏览器只需直接渲染出来。
不利于前后端分离,开发效率低。
有利于SEO。因为在后端有完整的html页面,所以爬虫更容易爬取获得信息,更有利于seo。
后端生成静态化文件。即生成缓存片段,这样就可以减少数据库查询浪费的时间了,且对于数据变化不大的页面非常高效 。
占用服务器端资源。无需占用客户端资源。即解析模板的工作完全交由后端来做。
vue,react都是推荐通过服务端渲染来实现路由的。
客户端渲染
浏览器从输入url到渲染页面 过程⭐⭐⭐
查找缓存
- 合成 URL:
浏览区会判断用户输入是合法 URL(Uniform Resource Locator,统一资源定位器),比如用户输入的是搜索的关键词,默认的搜索引擎会合成新的,
如果符合url规则会根据url协议,在这段内容加上协议合成合法的url
- 查找缓存:
网络进程获取到 URL,
先去本地缓存中查找是否有缓存资源,如果有则拦截请求,直接将缓存资源返回给浏览器进程;
否则,进入网络请请求阶段;
- DNS 解析:(域名系统Domain Name System)
DNS 查找数据缓存服务中是否缓存过当前域名信息,有则直接返回;
否则,会进行 DNS 解析返回域名对应的 IP 和端口号,
如果没有指定端口号,http 默认 80 端口,https 默认 443。
如果是 https 请求,还需要建立 TLS 连接;(传输层安全性协议Transport Layer Security)
TCP连接
- 建立 TCP 连接:
TCP 三次握手与服务器建立连接,然后进行数据的传输;
- 发送 HTTP 请求:
浏览器首先会向服务器发送请求行,它包含了请求方法、请求 URI (统一资源标识符Uniform Resource Identifier)和 HTTP 协议的版本;
还会发送请求头,告诉服务器一些浏览器的相关信息,比如浏览器内核,请求域名;
- 服务器处理请求:
服务器首先返回响应头+响应行,响应行包括协议版本和状态码
- 页面渲染:
查看响应头的信息,做不同的处理,比如重定向,存储cookie 看看content-type的值,根据不同的资源类型来用不同的解析方式
渲染详情可见2023年最全前端面试题考点HTML5+CSS3+JS_参宿7的博客-CSDN博客
- 断开 TCP 连接:
数据传输完成,正常情况下 TCP 将四次挥手断开连接。
DNS
因特网使用的命名系统,用来把人们方便记忆的主机名转换为机器方便处理的IP地址。
DNS协议属于应用层协议,一般是运行在UDP协议之上,使用53端口。
解析过程⭐⭐
1.当客户端需要域名解析时,通过本机的DNS客户端构造一个DNS请求报文,以UDP数据报的形式发往本地域名服务器。
2.域名解析有两种方式:递归查询和迭代查询相结合的查询。
由于递归查询给根域名服务器的负载过大,所以一般不使用。
OSI模型和TCP/IP协议⭐
HTTP协议
HTTP:基于TCP/IP的关于数据如何在万维网中如何通信的协议。
无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息)
Http和Https区别⭐⭐⭐
1.`HTTP` 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
2.``HTTP` 无法加密,而HTTPS 对传输的数据进行加密,安全
3.`HTTP` 标准端口是80 ,而 HTTPS 的标准端口是443
4.`在OSI` 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
常见的请求方式
-
POST:用于传输信息给服务器,功能与 GET 类似,但一般推荐使用 POST 方式;
-
GET: 用于请求访问已经被 URI(统一资源标识符)识别的资源,可以通过 URL 传参给服务器;
-
HEAD:,类似 GET 获得报文首部 ,只是不返回报文主体,一般用于验证 URI 是否有效;
-
PUT: 传输文件,报文主体中包含文件内容,保存到对应 URI 位置;
-
DELETE:与 PUT 相反,删除文件,删除对应 URI 位置的文件;
-
OPTIONS:查询相应 URI 支持的 HTTP 方法。
GET和POST发送请求⭐⭐⭐
HTTP协议中的两种发送请求的方法。
异同
同:GET和POST本质上就是TCP链接
异:
数据包数量:GET产生一个TCP数据包;POST产生两个TCP数据包。
(并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。)
过程:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
应用:
在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。
在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
因为GET一般用于查询信息,POST一般用于提交某种信息进行某些修改操作(私密性的信息如注册、登陆)
所以GET在浏览器回退不会再次请求,POST会再次提交请求
因为GET在浏览器回退不会再次请求,POST会再次提交请求
所以GET请求会被浏览器主动缓存,POST不会,要手动设置
GET请求参数会被完整保留在浏览器历史记录里,POST中的参数不会
因为 GET请求参数会被完整保留在浏览器历史记录里
所以GET请求在URL中传送的参数是有长度限制的,而POST没有限制
因为GET参数通过URL传递,POST放在Request body中
所以GET参数暴露在地址栏不安全,POST放在报文内部更安全
POST的content-type数据编码
Content-Type(MediaType),即是Internet Media Type,互联网媒体类型,也叫做MIME类型。(最初MIME是用于电子邮件系统的)
在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息。
它用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。
Content-Type的格式:type/subtype ;parameter
- type:主类型,任意的字符串,如text,如果是*号代表所有;
- subtype:子类型,任意的字符串,如html,如果是*号代表所有,用“/”与主类型隔开;
- parameter:可选参数,如charset,boundary等。
POST 方法中对发送数据编码的方式,也就是 Content-Type 有四种方式,
application/x-www-form-urlencoded (URL encoded)(默认)
multipart/form-data (键值对型数据)
application/json (Json 类型数据)(最方便)
text/xml (xml)(HTML文档标记)
传统的ajax请求时候,Content-Type默认为"文本"类型。
传统的form提交的时候,Content-Type默认为"Form"类型。
axios传递字符串的时候,Content-Type默认为"Form"类型。
axios传递对象的时候,Content-Type默认为"JSON"类型
http报文
响应报头
常见的响应报头字段有: Server, Connection...
响应报文
你从服务器请求的HTML,CSS,JS文件就放在这里面
HTTP请求(Request)报文
报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体)
HTTP响应(Response)报文
报文格式为:状态行 – HTTP头(通用信息头,响应头,实体头) – 响应报文主体
http版本⭐⭐⭐
1.0->1.1(一次传输多个文件,默认Connection: keep-alive)
http1.x解析基于文本,
http2.0采用二进制格式,新增特性 多路复用、header压缩、服务端推送(静态html资源)
http状态码⭐⭐⭐
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
1xx Informational(信息状态码) 接受请求正在处理
2xx Success(成功状态码) 请求正常处理完毕
3xx Redirection(重定向状态码) 需要附加操作已完成请求
4xx Client Error(客户端错误状态码) 服务器无法处理请求
5xx Server Error(服务器错误状态码) 服务器处理请求出错
常见状态码:
200 响应成功
204 返回无内容
301永久重定向 (请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。)
302临时重定向(服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。)
304资源缓存(自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。)
400 错误请求(请求格式错误,服务器不理解请求的语法。)
422 无法处理(请求格式正确,但是由于含有语义错误,无法响应)
401 未授权(请求要求身份验证。)
403服务器禁止访问
404服务器找不到请求的网页
500 502服务器内部错误
504 服务器繁忙
UDP⭐
应用层协议DNS基于UDP
- `TCP`向上层提供面向连接的可靠服务 ,`UDP`向上层提供无连接不可靠服务。
- `TCP`准确(文件传输),`UDP`实时(视频会议、直播)
- `TCP`仅支持一对一,`UDP`支持一对一,一对多,多对一和多对多交互通信
- `TCP`面向字节流传输,`UDP`面向报文传输
TCP⭐⭐⭐
三次握手
四次挥手
TIME-WAIT:2 MSL (Maximum segment lifetime) 最长报文最大生存时间
流量控制(滑动窗口机制)
让发送方的发送速率不要太快,要让接收方来得及接收。
还可接收的窗口是 rwnd = 400 ”(receiver window) 。
发送方的发送窗口不能超过接收方给出的接收窗口的数值。TCP的窗口单位是字节,不是报文段。
拥塞控制
拥塞:资源供不应求
- 慢开始、拥塞避免
- 快重传、快恢复
keep-alive持久连接
为了能够提升效率,在 HTTP 1.1 规范中把 Connection 头写入了标准,并且默认启用。
浏览器 或 服务器在HTTP头部加上 Connection: keep-alive,TCP 就会一直保持连接。
它会在隔开一段时间之后发送几次没有数据内容的网络请求来判断当前连接是不是应该继续保留。
可能会造成大量的无用途连接,白白占用系统资源
*粘包
在流传输中出现,UDP不会出现粘包,因为它有消息边界
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
情况
粘包情况有两种,一种是粘在一起的包都是完整的数据包
,另一种情况是粘在一起的包有不完整的包
。
措施
(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push
,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施
,使其及时接收数据,从而尽量避免出现粘包现象;
(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。分包多发
。
问题
(1)设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
(2)只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。
(3)避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。
总结
接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。实验证明这种方法是高效可行的。
缓存⭐⭐⭐
存储方式
白话:
强制缓存就是根据headers中的信息(expires,cache-control)强行从本地拿缓存,
拿不到再和服务器协商拿缓存,
如果服务器返回304(缓存无更新),就又从本地拿缓存。
否则,将从服务器那拿到的新资源存入浏览器
强制缓存
浏览器在加载资源的时候,会根据本地缓存中的headers中的信息(expires,cache-control)是否要强缓存,如果命中的话,则会使用缓存中的资源,否则继续发送请求。
其中Cache-Control优先级比Expires高。
Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。
但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
- 协商缓存生效,返回304(缓存无更新),如下
- 协商缓存失效,返回200和请求结果结果,如下
强制缓存优先于协商缓存进行
本地存储
cookie
浏览器本地保存数据的一种方案,且会在每次请求中带上该字段。
cookie 最常见的用法是作为用户登录凭证,赋予原本无状态HTTP 协议以一种状态
必选属性:Name Value
可选属性:有效期、使用范围、安全性的可选属性组成
Cookie 的作用域是和 domain(域名或 ip)绑定的,端口无关。
cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。
- Expires/Max-Age属性:有两种存储类型的Cookie:会话性(默认)与持久性。
会话性cookie仅保存在客户端内存中,并在用户关闭 整个 浏览器时失效;
持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效
Max-Age:失效时间(单位秒)
为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,
如果为 0,表示删除该 cookie 。默认为 -1。
如果 Max-Age 和 Expires 同时存在,以 Max-Age 为准。
- Path属性:指定 cookie 在哪个路径(路由)下生效,默认是 '/'。
如果设置为/abc
,则只有/abc
下的路由可以访问到该 cookie,如:/abc/read
- Domain属性:指定了可以访问该 Cookie 的 Web 域,默认是当前域名
- Secure属性:指定是否使用HTTPS安全协议发送Cookie。默认为false。(安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。)
- httpOnly属性:无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全
HTTP 响应报文通过 Set-Cookie 头字段给浏览器给当前网站设置 cookie:
HTTP/1.1 200 OK
Set-Cookie: token=abcd; Max-Age=6000; Path=/; Expires=Thu, 28 Apr 2022 16:31:36 GMT; HttpOnly
name=value; (s); ; <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
Cookie的安全规范:
- 不放重要数据,重要数据放Session。
- Cookie 数据加签名。对 Cookie 数据添加签名,这样 Cookie 如果被篡改了,服务端使用的时候通过校验签名就能发现了。
- Cookie数据加密。加密后数据就很难篡改了,但是加解密过程会带来性能损耗,
session
浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID)(JSESSIONID:j session id)
服务器端根据该Cookie查询Session对象,从而区分不同用户。
session 是基于 cookie 实现的,
session 存储在服务器端,
sessionId 会被存储到客户端的cookie 中,是连接 Cookie 和 Session 的一道桥梁
共同点
cookie和session都是用来跟踪浏览器用户身份的会话方式。
用于浏览器中存储数据的
浏览器的本地存储主要分为Cookie、WebStorage和IndexDB
(运行在浏览器的非关系型数据库)
其中WebStorage
又可以分为localStorage和sessionStorage
。
Cookie、
localStorage和sessionStorage
同:都是保存在浏览器端、且同源的
异:
cookie
在浏览器和服务器间来回传递
存储地
- cookie数据保存在客户端,session数据保存在服务端,session更安全
- sessionStorage和localStorage不会把数据发给服务器,仅在本地保存
存取值的类型
- Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,
- Session 可以存任意数据类型。
存储大小
- 单个 Cookie 保存的数据不能超过 4K,
- Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
有效期
-
localStorage
:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据; -
cookie的有效期是可以设置的,默认情况下是关闭浏览器后失效。
-
sessionStorage的有效期是仅存在于当前会话,关闭当前会话或者浏览器后就会失效。
由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,
当距离客户端上一次使用 session 的时间超过这个失效时间时,
服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间
作用域不同
-
sessionStorage
:不在不同的浏览器窗口中共享,即使是同一个页面; -
cookie,localstorage
:在所有同源窗口中都共享;也就是说只要浏览器不关闭,数据仍在
存储地
状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置,分别为from memory cache 和 from disk cache。
- 浏览器读取缓存的顺序为memory –> disk。
- 访问URL–> 200 –> 关闭标签页 –> 重新打开URL–> 200(from disk cache) –> 刷新 –> 200(from memory cache)
内存缓存(from memory cache)
- 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
- 时效性:一旦该进程关闭,则该进程的内存则会清空。
硬盘缓存(from disk cache)
重新解析该缓存内容,读取复杂,速度比内存缓存慢。
应用
js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取;
css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。
前端基础-浏览器缓存/HTTP缓存机制(面试常考)_浏览器缓存机制 面试_LYFlied的博客-CSDN博客
Token、JWT
认证(Authentication)
针对用户,验证当前用户的身份
互联网中的认证:
- 用户名密码登录
- 手机号接收验证码
授权(Authorization)
针对第三方应用,用户授予第三方应用访问该用户某些资源的权限
- 你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限)
- 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)
实现授权的方式有:cookie、session、token、OAuth
凭证(Credentials)
实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌
Token(令牌)
Acesss Token
- 访问资源接口(API)时所需要的资源凭证
- 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
- 特点:
- 服务端无状态化、可扩展性好
- 支持移动端设备
- 安全
- 支持跨程序调用
- token 的身份验证流程:
基于token的登录流程⭐⭐
1. 客户端使用用户名跟密码请求登录
2. 服务端收到请求,去验证用户名与密码
3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
- 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
- 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
- token 完全由应用管理,所以它可以避开同源策略
Refresh Token
refresh token 是专用于刷新 access token 的 token。
如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。
有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。
- Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
- Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。
Token 和 Session 的区别
- Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。
- Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
- 作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,
- Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
- Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。
- Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。
- Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token
JSON Web Token(JWT)⭐
跨域认证解决方案。是一种认证授权机制。
因为 JWT 通常使用 localstorage (也可以使用 Cookie),所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
内容
1.Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
方式
当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,
所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
GET /calendar/v1/events
Host: api.example.com
Authorization: Bearer <token>
服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
由于 JWT 是自包含的,因此减少了需要查询数据库的需要
JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。
- 跨域方式
JWT 放在 POST 请求的数据体里。
- 通过 URL 传输方式
http://www.example.com/user?token=xxx
优缺点
- 优点:
基于JSON,方便解析,可以在令牌中自定义丰富内容,易扩展。
通过非对称加密及数字签名技术,可以防止篡改、安全性高。
可以不依赖认证服务就可以完成授权。
- 缺点:
JWT令牌较长,占存储空间比较大。
由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。
Token 和 JWT 的区别
相同:
- 都是访问资源的令牌
- 都可以记录用户的信息
- 都是使服务端无状态化
- 都是只有验证成功后,客户端才能访问服务端上受保护的资源
区别:
- Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
- JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
常见的加密算法
哈希算法(Hash Algorithm)又称散列算法
数据交换格式
服务器端与客户端之间进行数据传输与交换的格式。
XML
HTML和XML
HTML(HyperText Markup Language超文本标记语言)
XML(Extensiable Markup Language可扩展标记语言)
发展史:HTML->XHTML->XML
HTML缺点:
- 不能自定义标签
- 本身缺少含义
应用
- 程序间的数据通信(例:QQ之间传输文件)
- 配置文件(例:structs-config.xml)
- 小型数据库(例:msn中保存用户聊天记录就是用XML文件)直接读取文件比读数据库快。
JSX
JSX =JavaScript + XML。是一个 JavaScript 的语法扩展。
let element = React.createElement('h2', {title: 'hi'}, [
'hello world',
React.createElement('span', null, '!!!!!!')
]);
JSX写起来就方便很多了,在内部会转换成React.createElement(),然后再转换成对应的虚拟DOM,但是JSX语法浏览器不认识,所以需要利用babel插件进行转义处理。
Babel⭐
JavaScript 编译器
将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法,即浏览器兼容的语法,比如将箭头函数转换为普通函数
将jsx转换成浏览器认的js
JSON
JSON,全称是 JavaScript Object Notation,即 JavaScript对象标记法。
是一种轻量级(Light-Meight)、基于文本的(Text-Based)、可读的(Human-Readable)格式。
名称中虽然带有JavaScript,但是指其语法规则是参考JavaScript对象的,而不是指只能用于JavaScript 语言。
相比 XML(另一种常见的数据交换格式),文件更小
//JSON
{
"name":"参宿", //"key":value,value可以str、num、bool、null、obj,arr。
"id":7, //并列的数据之间用逗号(“,”)分隔
"fruits":["apple","pear","grape"] //数组用[],对象用{}
}
//XML
<root>
<name>"参宿"</name>
<id>7</id>
<fruits>apple</fruits>
<fruits>pear</fruits>
<fruits>grape</fruits>
</root>
JSON解析和生成
var str = '{"name": "参宿","id":7}'; //'JSON字符串'
var obj = JSON.parse(str); //JSON.parse(str)
console.log(obj); //JSON的解析(JSON字符串转换为JS对象)
//Object { name: "参宿", id: 7 }
var jsonstr = JSON.stringify(obj); //JSON.stringify(obj)
console.log(jsonstr); //JSON的生成(JS对象转换为JSON字符串)
JSON.parse(text[, reviver])//reviver函数参数,修改解析生成的原始值,调用时机在 parse 函数返回之前。
//k:key,v:value
JSON.parse('{"p": 5}', function (k, v) {
if(k === '') return v; // 如果到了最顶层,则直接返回属性值,
return v * 2; // 否则将属性值变为原来的 2 倍。
}); // { p: 10 }
//从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身
JSON.parse('{"1": 1, "2": 2,"3": {"4": 4, "5": {"6": 6}}}', function (k, v) {
console.log(k); // 输出当前的属性名,从而得知遍历顺序是从内向外的,
// 最后一个属性名会是个空字符串。
return v; // 返回原始属性值,相当于没有传递 reviver 参数。
});
// 1
// 2
// 4
// 6
// 5
// 3
// ""
异步⭐⭐⭐
- 异步是通过一次次的循环事件队列来实现的.例如 setTimeout setInterval xmlHttprequest 等
- 同步是阻塞式的IO,在高并发环境会是一个很大的性能问题,
所以同步一般只在基础框架的启动时使用,用来加载配置文件,初始化程序什么的.
node是单线程的,网络请求,浏览器事件等操作都需要使用异步的方法。
webworker(创建分线程)⭐
当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。
web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。
index.js为加载到html页面中的主线程(js文件)
work.js为在index中创建的分线程
werbwork的实现方法
在index.js中 创建分线程 var w =new webwork('work.js')//创建 Web Worker 对象
在index.js中 通过 w.postmesage('数据') 向子线程发送数据
在work.js中 通过
onmessage = function(ev) {
console.log(ev);//接受主线程发送过来的ev.data数据
this.postMessage('数据')//通过 postmesage('数据') 向主线程发送数据
}
在index.js中 通过w.onmessage=function(ev){ev.data} ev.data 接受 a 的值.
在work.js中 w.terminate();//终止 Web Worker
AJAX
-
ajax 全名 async javascript and XML(异步JavaScript和XML)
前后端交互的重要工具
在无需重新加载整个网页的情况下,能够更新部分网页
原生AJAX创建⭐
//1.创建xhr 核心对象
var xhr=new XMLHttpRequest();
//2.调用open 准备
xhrReq.open(method, url, [async, user, password]);//请求方式,请求地址,true异步,false 同步
xhr.open('post','http://www.baidu.com/api/search',true)
//3.如果是post请求,必须设置请求头。
//xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
//4.调用send 发送请求 (如果不需要参数,就写null)
xhr.send('user=tom&age=10&sex=女')
//5.监听异步回调 onreadystatechange
xhr.onreadystatechange=function(){
if(xhr.readyState==4){ //表示请求完成
if(xhr.status==200){ //状态码 为 200 表示接口请求成功
console.log(xhr.responseText); //responeseText 为相应数据。字符串类型。
var res=JSON.parse(xhr.responseText);
console.log(res);
if(res.code==1){
modal.modal('hide');
location.reload();
}
}
*jQuery ajax
$(function(){
// jQuery里使用ajax
$.ajax({
url:"/js/data.json",
type:'GET',
dataType:'json',
data:{'aa':1},
})
//设置请求成功后的回调函数
.done(function(dat){
console.log(dat);
})
//设置请求失败后的回调函数和
.fail(function(dat){
console.log('no!');
})
})
jQuery是一个 JavaScript 工具库
Axios⭐⭐
axios 是一个基于Promise 的ajax的封装,用于浏览器和 nodejs 的 HTTP 客户端。
特征:
- 从浏览器中创建 XMLHttpRequest
- 从 node.js 发出 http 请求
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防止 CSRF
import axios from "axios";
axios.get('/users')
.then(res => {
console.log(res.data);
});
// 向给定ID的用户发起请求
axios.get('/user?ID=12345')
.then(function (response) {
// 处理成功情况
console.log(response);
})
.catch(function (error) {
// 处理错误情况
console.log(error);
})
.then(function () {
// 总是会执行
});
axios.get('/user?ID=12345')===
axios.get('/user', {
params: {
ID: 12345
}
})
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Axios API
1.axios(config)
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
2.axios(url[, config])
// 发送 GET 请求(默认的方法)
axios('/user/12345');
// 在 node.js 用GET请求获取远程图片
axios({
method: 'get',
url: 'http://bit.ly/2mTM3nY',
responseType: 'stream'
})
.then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
axios.get(`http://localhost:3000/search/users?q=${keyWord}`)
get('') (自动默认) 和get(``)(反单引号)均可
axios基本用法_面条请不要欺负汉堡的博客-CSDN博客_axios
setTimeout()(倒)计时(手写)⭐
setInterval()
方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。
//返回值timeoutID是一个正整数,表示定时器的编号。
let timeoutID = scope.setTimeout(function[, delay]),//delay表示最小等待时间,真正等待时间取决于前面排队的消息
clearTimeout(timeoutID) //取消该定时器。
<body>
<button onclick="startCount()">开始计数!</button> <input type="text" id="txt">
<button onclick="stopCount()">停止计数!</button>
</body>
<script type="text/javascript">
var c = 0;
var t;
var timer = 0;
function timedCount() {
document.getElementById("txt").value = c;
c = c + 1;
t = setTimeout(function() {
timedCount()
}, 1000);
}
function startCount() {
if (!timer) {
timer = 1;
timedCount();
}
}
function stopCount() {
clearTimeout(t);
timer = 0;
}
</script>
<body>
<button onclick="startCount()">开始倒计时</button> <input type="text" id="txt">
</body>
<script type="text/javascript">
var c = 0;
var t;
function timedCount() {
c -=1;
document.getElementById("txt").value=c;
if(c===0){
clearTimeout(t);
return;
}
t = setTimeout(function() {
timedCount()
}, 1000);
}
function startCount() {
c=document.getElementById("txt").value;
timedCount();
}
</script>
setTimeout()和setInterval()⭐
- 在同一个对象上(一个window或者worker),
setTimeout()
或者setInterval()
在后续的调用不会重用同一个定时器编号。- 如果省略delay, 取默认值 0,意味着“马上”执行,或者尽快执行。
- 不同的对象使用独立的编号池
- 浏览器会控制最小的超时时间固定在4ms到15ms之间。
setTimeout
:延时delay毫秒之后,直接将回调函数加入事件队列。
setInterval
:延时delay毫秒之后,先看看事件队列中是否存在还没有执行setInterval的回调函数,如果存在,就不要再往事件队列里加入回调函数了。
setTimeout
保证调用的时间间隔是一致的,
setInterval
的设定的间隔时间包括了执行回调的时间。
setInterval的回调函数不报错
Promise(ES6 解决地狱回调)⭐⭐⭐
回调函数:
当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数,例如setTimeout
回调地狱:回调函数套回调函数,回调的多重嵌套,会导致代码可读低、编写费劲、容易出错,故而被称为 callback hell
Promise:ES6异步操作的一种解决方案
Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,称为起始函数。
起始函数包含两个函数参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。
起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。
起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。
promise 有三个状态:
pending[待定]初始状态,fulfilled[实现]操作成功,rejected[被否决]操作失败
如果不使用 resove 和 reject 两个函数 状态为pendding
Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:
- then:用于处理 Promise 成功状态的回调函数,参数为resolve传递的参数。
- catch:用于处理 Promise 失败状态的回调函数,参数为reject传递的参数。
- finally:无论 Promise 是成功还是失败,都会执行的回调函数。
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
if (Math.random() < 0.5) {
resolve('success');
} else {
reject('error');
}
}, 1000);
});
promise.then(result => {
console.log(result);
}).catch(error => {
console.log(error);
});
new Promise(function (resolve, reject) {
var a = 0;
var b = 1;
if (b == 0) reject("Divide zero");
else resolve(a / b);
}).then(function (value) {
console.log("a / b = " + value);
}).catch(function (err) {
console.log(err);
}).finally(function () {
console.log("End");
});
promise调用then,
如果非promise,会将值包装成promise
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});
new Promise(function (resolve, reject) {
console.log(1111);
resolve(2222);
}).then(function (value) {
console.log(value);
return 3333;
}).then(function (value) {
console.log(value);
throw "An error";
}).catch(function (err) {
console.log(err);
});
// then方法
then(resolveFn, rejectFn) {
//return一个新的promise
return new MyPromise((resolve, reject) => {
//把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
//执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
//分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
//这里resolve之后,就能被下一个.then()的回调获取到返回值,从而实现链式调用
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
//把后续then收集的依赖都push进当前Promise的成功回调队列中(_resolveQueue), 这是为了保证顺序调用
this._resolveQueue.push(fulfilledFn)
//reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
this._rejectQueue.push(rejectedFn)
})
}
- 值穿透:根据规范,如果 then() 接收的参数不是function,那么我们应该忽略它。
如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断
- 处理状态为resolve/reject的情况:其实我们上边 then() 的写法是对应状态为pending的情况,但是有些时候,resolve/reject 在 then() 之前就被执行(比如
Promise.resolve().then()
),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为fulfilled
或rejected
的情况,我们直接执行then回调
// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
// 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
// return一个新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
// 执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
兼容同步任务
executor是异步任务:
Promise的执行顺序是new Promise -> then()收集回调 -> resolve/reject执行回调
executor是同步任务:new Promise -> resolve/reject执行回调 -> then()收集回调
,resolve的执行跑到then之前去了,
为了兼容这种情况,给resolve/reject
执行回调的操作包一个setTimeout,让它异步执行。
//Promise/A+规定的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._value = undefined // 储存then回调return的值
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发
// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以兼容executor是同步代码的情况
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态
this._value = val // 储存当前value
// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// 实现同resolve
let _reject = (val) => {
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
this._value = val // 储存当前value
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
// 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
// return一个新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
// 执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
}
Promise.prototype.catch()
catch()方法
返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。
//catch方法其实就是执行一下then的第二个回调
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
Promise.prototype.finally()
finally()方法
返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then
//finally方法
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value), // MyPromise.resolve执行回调,并在then中return结果传递给后面的Promise
reason => MyPromise.resolve(callback()).then(() => { throw reason }) // reject同理
)
}
复制代码
MyPromise.resolve(callback())
的意义:这个写法其实涉及到一个finally()
的使用细节,finally()如果return了一个reject状态的Promise,将会改变当前Promise的状态,这个MyPromise.resolve
就用于改变Promise状态,在finally()没有返回reject态Promise或throw错误的情况下,去掉MyPromise.resolve
也是一样的
Promise.resolve()
Promise.resolve(value)
方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有"then" 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
//静态的resolve方法
static resolve(value) {
if(value instanceof MyPromise) return value // 根据规范, 如果参数是Promise实例, 直接return这个实例
return new MyPromise(resolve => resolve(value))
}
复制代码
Promise.reject()
Promise.reject()
方法返回一个带有拒绝原因的Promise对象。
//静态的reject方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}
Promise.all()
Promise.all(iterable)
方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
//静态的all方法
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于处理传入值不为Promise的情况
MyPromise.resolve(p).then(
val => {
index++
result[i] = val
//所有then执行后, resolve结果
if(index === promiseArr.length) {
resolve(result)
}
},
err => {
//有一个Promise被reject时,MyPromise的状态变为reject
reject(err)
}
)
})
})
}
复制代码
Promise.race()
Promise.race(iterable)
方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
for (let p of promiseArr) {
MyPromise.resolve(p).then( //Promise.resolve(p)用于处理传入值不为Promise的情况
value => {
resolve(value) //注意这个resolve是上边new MyPromise的
},
err => {
reject(err)
}
)
}
})
}
Promise.all()哪怕一个请求失败了也能得到其余正确的请求结果的解决方案⭐⭐
await(串行):如果在一个async的方法中,有多个await操作的时候,程序会变成完全的串行操作,一个完事等另一个但是为了发挥node的异步优势,
当异步操作之间不存在结果的依赖关系时,可以使用promise.all来实现并行,all中的所有方法是一同执行的。
执行后的结果:
async函数中,如果有多个await关键字时,如果有一个await的状态变成了rejected,那么后面的操作都不会继续执行,promise也是同理
await的返回结果就是后面promise执行的结果,可能是resolves或者rejected的值使用场景循环遍历方便了代码需要同步的操作(文件读取,数据库操作等)
promise.all并行(同时)执行promise,当其中任何一个promise 出现错误的时候都会执行reject,导致其它正常返回的数据也无法使用
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');//setTimeout(function[, delay, arg1, arg2, ...]);
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// Expected output: Array [3, 42, "foo"]
map的每一项都是promise,catch方法返回值会被promise.reslove()包裹,这样传进promise.all的数据都是resolved状态的。
let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = Promise.resolve(3)
let p4 = Promise.resolve(4)
let p5 = Promise.reject("error")
let arr = [p1,p2,p3,p4,p5];
let all = Promise.all(arr.map((promise)=>promise.catch((e)=>{console.log("错误信息"+e)})))
all.then(res=>{console.log(res)}).catch(err=>console.log(err));
Mypromise
//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发
// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态
// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}
fetch(ES6 拉取网络资源)
以前发送请求,使用ajax或者axios,现在还可以使用fetch。这个是window自带的方法,和xhr是一个级别的。(xhr=new XMLHttpRequest())
fetch(input[, init]);
// url (必须), options (可选)
fetch('/some/url', {method: 'get'})
.then(function(response) {
})
.catch(function(err) {
// 出错了;等价于 then 的第二个参数,但这样更好用更直观 :(
});
第二个then接收的才是后台传过来的真正的数据
window.fetch(url, { method: 'get'}) //fetch() 返回响应的promise
// 第一个then 设置请求的格式
.then(res => return res.json()) // 第一层then返回的是:响应的报文对象
.then((data) => { //第二层:如果想要使用第一层的数据,必须把数据序列化
res.json() 返回的对象是一个 Promise对象再进行then()
<!-- data为真正数据 -->
}).catch(e => console.log("Oops, error", e))
Fetch()方法介绍_小钢炮vv的博客-CSDN博客_fetch
*Generator
ES6 引入Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。yield
表达式就是暂停标志,把函数的执行流挂起,通过next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。
function* myGenerator() {
yield '1'
yield '2'
return '3'
}
const gen = myGenerator(); // 获取迭代器
gen.next() //{value: "1", done: false}
gen.next() //{value: "2", done: false}
gen.next() //{value: "3", done: true}
-
形式上,Generator是一个普通函数。
区别一是function命令和函数名之间有一个星号*
区别二是函数体内部使用yield定义不同的状态。
-
调用后函数并不执行,返回的是一个指向内部状态的指针对象Iterator。
function* gen(x){
console.log('x='+x)
var y = yield x + 2;
return y;
}
//调用Generator 函数
var g = gen(1);
g.next();
// x=1
// {value: 3, done: false}
async/await函数
异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于阅读。
相当于promise用法的语法糖,async/await实际上是对Generator(生成器)的封装。
ES6 引入了 Generator 函数,被ES7 提出的async/await取代了,
将Generator函数的星号 * 替换成async,将yield替换成await。
相对于Generator函数的改进:自带执行器,会自动执行。
await
规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果
Promise.resolve(a)
.then(b => {
// do something
})
.then(c => {
// do something
})
//等价于
async () => {
const a = await Promise.resolve(a);
const b = await Promise.resolve(b);
const c = await Promise.resolve(c);
}
async
关键字+函数,表明该函数内部有异步操作。
函数返回的是一个状态为fulfilled
的Promise对象,
如果结果是值,会经过Promise包装返回。
如果是promise则会等待promaise 返回结果,否则,就直接返回对应的值,
await 操作符+promise对象,用于组成表达式
awai+值,就会转到一个立即resolve
的Promise对象。
async function asyncFunc() {
let value = await new Promise(
function (resolve, reject) {
resolve("Return value");
}
);
console.log(value);
}
asyncFunc();
await只能在async函数中出现, 普通函数直接使用会报语法错误SyntaxError
await语句后的Promise对象变成reject状态时,那么整个async函数会中断,后面的程序不会继续执行
处理异常的机制将用 try-catch 块实现
async function asyncFunc() {
try {
await new Promise(function (resolve, reject) {
throw "Some error"; // 或者 reject("Some error")
});
} catch (err) {
console.log(err);
// 会输出 Some error
}
}
asyncFunc();
async和await的讲解_傲娇的koala的博客-CSDN博客_async await
SPA和MPA
- 爬虫在爬取的过程中,不会去执行js,所以隐藏在js中的跳转也不会获取到
单页Web应用(single page web application,SPA)。
整个应用只有一个完整的页面。
点击页面中的链接不会刷新页面,只会做页面的局部更新。
数据都需要通过ajax请求获取,并在前端异步展现
URL
字符串 | 说明 |
---|---|
:// | 协议符号 |
/ | 分隔目录和子目录 |
测试 | 代表需要编译处理了的路径 |
? | 分隔实际的URL和参数 |
& | URL中指定的参数间的分隔符 |
= | 左边为参数名、右边参数值 |
搜&索 | 搜索词含有中文,含有保留字段,需要编译 |
#
符号的url就是一个 Fragment URL。#
指定了网页中的一个位置
浏览器就会查询网页中name
属性值匹配print
的<a>
标签。即:<a name="print"></a>
,
或者是 id
属性匹配 print
的 <a>
标签。即<a id="print"></a>
匹配后,浏览器会将该部分滚动到可视区域的顶部。
#
仅仅作用于浏览器,它不会影响服务器端。所以http请求中不会包括#
。
URL和URI
URL (Uniform Resource Locator ),统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源唯一的地址。
URI(Uniform Resource Identifier),统一资源标识符,结构如下
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/ \________/\_________/ \__/
| | | | |
scheme authority path query fragment
URL 是 URI 的一个子集, URL 同时说明访问方式
跨域通信⭐⭐⭐
所谓同源(域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能
如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。(跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。)
同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。
(服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。)
服务器与服务器之间是可以通信的不受同源策略的影响:Nginx反向代理,proxy代理
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
允许跨域加载资源的标签:
- script标签的跨域功能:
<img src=XXX><script src=XXX>
- 规定外部脚本文件的 URL:
<link href=XXX>
跨域的解决方案思路两种,绕过去和cors;
- iframe方式可传递数据,但组织和控制代码逻辑太复杂,鸡肋;
- jsonp适合加载不同域名的js、css,img等静态资源,现在浏览器兼容性高了,以及受限于仅get方式,逐步淘汰了;
- Nginx反向代理和nodejs中间件跨域原理都相似,是绕过去的方式,是从古至今通用的没完解决方案,适合前后端分离的前端项目调后端接口。都是搭建一个服务器,直接在服务器端请求HTTP接口,缺点也许是服务器压力大一点,实际中那点压力根本不是大问题;同时反向代理更适合内部应用间访问和共享;
- cors才是真正的称得上跨域请求解决方案(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;
- websocket都是HTML5新特性,兼容性不是很好,只适用于主流浏览器和IE10+。
JSONP跨域(⭐手写)
服务器与客户端跨源通信的常用方法。
JSONP(JSON With Padding)是利用<script src=XXX>跨域
因为是动态创建script标签,所以它只支持get请求,不支持post请求。
- 优点:
- 简单适用,
- 兼容低版本IE,可以向不支持CORS的网站请求数据( IE<=9, Opera<12, or Firefox<3.5 )
- 不足:
- 只支持get请求,
- 只支持跨域 HTTP 请求不安全,可能遇到XSS攻击,
- 不能解决不同域的两个页面之间如何进行 Javascript 调用
原生实现
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
- src=跨域的API数据接口地址+向服务器传递该函数名(?问号传参
?user=admin&callback=handleCallback)
- 客户端声明一个回调函数,其函数名当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
- 服务器接收到请求后,查找数据库,把返回的data和传递进来的函数名拼接成一个字符串,
- 例如:传递进去的函数名是handleCallback,它准备好的数据是handleCallback
('res')
。 - 服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数handleCallback,对返回的数据进行操作。
封装JSONP
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时需封装一个 JSONP函数。
// index.html
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
//抽出参数,新建对象
params = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`//拼接
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
向http://localhost:3000/say?wd=Iloveyou&callback=show
这个地址请求数据,然后后台返回show('我不爱你')
,最后会运行show()这个函数,打印出'我不爱你'
// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
jQuery实现
JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,
且jQuery默认就会给JSONP的请求清除缓存。
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});
跨域资源共享(CORS)(⭐手写)
Cross-Origin Resource Sharing W3C 标准 CORS(主流的解决方案,推荐使用)
允许浏览器向跨源服务器发送XMLHttpRequest
请求,从而克服了 AJAX 只能同源使用的限制
属于跨源 AJAX 请求的根本解决方法,最常用的一种解决办法
目前,所有浏览器都支持该功能,IE浏览器不能低于IE10
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
CORS分为简单请求,非简单/复杂请求
简单请求
只要同时满足以下两大条件,就属于简单请求
条件1:使用下列方法之一:
- GET
- HEAD
- POST
条件2:Content-Type 的值仅限于下列三者之一:
- text/xxx
- multipart/form-data(键值对型数据)
- application/x-www-form-urlencoded(URL encoded)(默认)
application/xml 、 text/xml、text/html、text/plain的区别
1、text/html是html格式的正文
2、text/plain是无格式正文(可以有效避免XSS漏洞)
3、text/xml忽略xml头所指定编码格式而默认采用us-ascii编码
4、application/xml会根据xml头指定的编码格式来编码:
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
GET /cors? HTTP/1.1
Host: localhost:2333
Connection: keep-alive
Origin: http://localhost:2332
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: */*
Referer: http://localhost:2332/CORS.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
If-None-Match: W/"1-NWoZK3kTsExUV00Ywo1G5jlUKKs"
普通跨域请求
只需服务器端设置Access-Control-Allow-Origin(表示接受那些域名的请求(*为所有))
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
带cookie跨域请求(⭐手写)
前后端都需要进行设置
Access-Control-Allow-Credentials值为true时,Access-Control-Allow-Origin必须有明确的值,不能是通配符(*)
withCredentials 表示跨域请求是否提供凭据信息(cookie、HTTP认证及客户端SSL证明等)
cors中间件
cors是Express的一个第三方中间件
使用的步骤分为如下三步:
1.运行 npm install cors 安装中间件
2.使用const cores = require(‘cors’) 导入中间件
3.在路由之前调用app.use(cors())配置中间件
复杂请求
在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
用PUT
向后台请求时,后台需做如下配置:
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
完整复杂请求的例子,并且介绍下CORS请求相关的字段:
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
上述代码由http://localhost:3000/index.html
向http://localhost:4000/
跨域请求,后端是实现 CORS 通信的关键。
postMessage
HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,
可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message: 将要发送到其他 window的数据。
- targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
- transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
http://localhost:3000/a.html
页面向http://localhost:4000/b.html
传递“我爱你”,然后后者传回"我不爱你"。
// a.html
//内联框架元素 (<iframe>) 表示嵌套的browsing context。将另一个 HTML 页面嵌入到当前页面中。
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
</script>
// b.html
window.onmessage = function(e) {
console.log(e.data) //我爱你
e.source.postMessage('我不爱你', e.origin)
}
代理服务器
前端配置一个代理服务器代替浏览器去发送请求:
因为服务器与服务器之间是可以通信的不受同源策略的影响。
proxy代理服务器(ES6)
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时的代理行为
const p = new Proxy(target, handler)
Reflect(ES6)
ES6
中操作对象而提供的新 API
,若需要在Proxy
内部调用对象的默认行为,建议使用Reflect
- 只要
Proxy
对象具有的代理方法,Reflect
对象全部具有,以静态方法的形式存在 - 修改某些
Object
方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
) - 让
Object
操作都变成函数行为
handler
拦截属性
target,propKey,value,receiver:目标对象、属性名、属性值 proxy
实例本身
以下为关键属性
get(target,propKey,receiver)
拦截对象属性的读取
let person = {
name: "Guest"
};
let proxy = new Proxy(person, {
get: function(target, propKey, receiver) {
return Reflect.get(target, propKey, receiver)
// or
// return target[propKey]
}
});
proxy.name // "Guest"
get
能够对数组增删改查进行拦截,将读取数组负数的索引
function createArray(...elements) {
//handler对象
let handler = {
//get函数属性
get(target, propKey, receiver) {
let index = Number(propKey);
//实现循环索引
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
set(target,propKey,value,receiver)
拦截对象属性的设置
现定义一个对象 规定 年龄输入整数时才被赋值,访问无效属性时控制台提醒
const obj = { name: "张三", age: 18 };
const proxy = new Proxy(obj, {
get(target, prop) {
if (prop in target) {
return Reflect.get(target, prop);
} else {
console.error("字段不存在")
return undefined;
}
},
set(target, propKey, value, receiver) {
if (propKey === "age") {
if (typeof value === "number") {
return Reflect.set(target, propKey, value, receiver);
// or
// target[propKey] = value
// return true
} else {
console.error("年龄只能输入正整数");
return false;
}
} else {
return false;
}
}
});
proxy.age = 20;
console.log(proxy.age); // 20
proxy.age = "22";
console.log(proxy.age); // 20
console.log(proxy.test); // undefined
deleteProperty(target,propKey)
拦截delete proxy[propKey]
的操作,返回一个布尔值
如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`);
}
}
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性
取消代理Proxy.revocable(target, handler)
Proxy.revocable(target, handler);
应用
类似于设计模式中的代理模式,常用功能如下:
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
使用 Proxy
保障数据类型的准确性
let data = { num: 0 };
data = new Proxy(data, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("属性只能是number类型");
}
return Reflect.set(target, key, value, proxy);
}
});
data.num = "foo"
// Error: 属性只能是number类型
data.num = 1
// 赋值成功
声明一个私有的 apiKey
,便于 api
这个对象内部的方法调用
let api = {
_apiKey: 'kafakakafaka',
};
//私有属性名 常量数组
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, receiver) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
}
return Reflect.set(target, key, value, receiver);
}
});
console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误
观察者模式(Observer mode):函数自动观察数据对象,一旦对象有变化,函数就会自动执行
observable
函数返回一个原始对象的 Proxy
代理,拦截赋值操作,触发充当观察者的各个函数
//观察者函数都放进Set集合
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
//当修改obj的值,在会set函数中拦截
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
//自动执行Set所有的观察者
queuedObservers.forEach(observer => observer());
return result;
}
Nginx反向代理
nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin...等字段。
是最简单的跨域方式,只需要修改 nginx 的配置即可解决跨域问题
- node中间件和nginx反向代理,都是搭建一个中转 nginx 服务器,用于转发请求。
- 请求发给代理服务器,静态页面和代理服务器是同源的,
- 代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制。
正向代理和反向代理
正向代理是代理用户客户端,为客户端发送请求,对服务器隐藏真实客户端。
反向代理以代理服务器来接收客户端的请求,然后将请求转发给内部网络上的服务器,将从服务器上得到的结果返回给客户端。
正向代理主要是用来解决访问限制问题;
反向代理则是提供负载均衡、安全防护等作用。
websocket协议
HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信
WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。
WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了
原生WebSocket API使用起来不太方便
Socket.io
,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
本地文件socket.html向localhost:3000
发生数据和接受数据:
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('我不爱你')
});
})
web安全及防护
XSS攻击
跨站脚本攻击Cross-Site Scripting,
代码注入攻击。
当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
- 解决:
url参数使用encodeURIComponent方法转义
尽量不用InnerHtml插入HTML内容
使用特殊符号、标签转义符。
CSRF攻击⭐⭐⭐
跨站请求伪造Cross-site request forgery,在第三方网站中,向被攻击网站发送跨站请求。
利用用户在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
- 解决:添加验证码、使用token
SQL注入攻击
SQL命令插入到Web表单递交或输入域名,最终达到欺骗服务器执行恶意的SQL命令。
解决:表单输入时通过正则表达式将一些特殊字符进行转换
DDoS攻击
分布式拒绝服务,全称 Distributed Denial of Service
,其原理就是利用大量的请求造成资源过载,导致服务不可用。
解决:
-
限制单IP请求频率。
-
防火墙等防护设置禁止
ICMP
包等 -
检查特权端口的开放
考试时允许使用草稿纸,请提前准备纸笔。考试过程中允许上厕所等短暂离开,但请控制离开时间
笔试得分60%一般通过,面试答对80%才能通过
考试范围收录
选择题总集合={前端,计算机基础(数据库,操作系统,数据结构与算法,计算机网络),行测};
编程题总集合={常规算法(到具体情景),js手写,Dom操作}
例如:
- 美团:前端,计算机基础,行测,常规算法(前端:计算机基础=1:1)
- 小红书:前端,计算机基础,常规算法(前端:计算机基础=3:1)
- SHINE:前端,js手写
- 携程,京东:是先行测
- 百度:前端,计算机基础,常规算法,Dom操作
选择题
⭐是常考考点,其他是作为理解原理的补充,原理部分在大厂笔面试中会考到
常用设计模式
原则
- S – Single Responsibility Principle 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
- 例如:Promise每个then中的逻辑只做好一件事
- O – OpenClosed Principle 开放/封闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
- 例如:Promise如果新增需求,扩展then
- L – Liskov Substitution Principle 里氏替换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
- I – Interface Segregation Principle 接口隔离原则
- 保持接口的单一独立
- 类似单一职责原则,这里更关注接口
- D – Dependency Inversion Principle 依赖倒转原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
//checkType('165226226326','mobile')
//result:false
let checkType=function(str, type) {
switch (type) {
case 'email':
return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str)
case 'mobile':
return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
case 'tel':
return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
default:
return true;
}
}
想添加其他规则就得在函数里面增加 case 。违反了开放-封闭原则(对扩展开放,对修改关闭)
给 API 增加一个扩展的接口:
let checkType=(function(){
let rules={
email(str){
return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
},
mobile(str){
return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
}
};
//暴露接口
return {
//校验
check(str, type){
return rules[type]?rules[type](str):false;
},
//添加规则
addRule(type,fn){
rules[type]=fn;
}
}
})();
//调用方式
//使用mobile校验规则
console.log(checkType.check('188170239','mobile'));
//添加金额校验规则
checkType.addRule('money',function (str) {
return /^[0-9]+(.[0-9]{2})?$/.test(str)
});
//使用金额校验规则
console.log(checkType.check('18.36','money'));
创建型
单例模式
一个类只有一个实例,并提供一个访问它的全局访问点。
class LoginForm {
constructor() {
this.state = 'hide'
}
show() {
if (this.state === 'show') {
alert('已经显示')
return
}
this.state = 'show'
console.log('登录框显示成功')
}
hide() {
if (this.state === 'hide') {
alert('已经隐藏')
return
}
this.state = 'hide'
console.log('登录框隐藏成功')
}
}
LoginForm.getInstance = (function () {
let instance
return function () {
if (!instance) {
instance = new LoginForm()
}
return instance
}
})()
let obj1 = LoginForm.getInstance()
obj1.show()
let obj2 = LoginForm.getInstance()
obj2.hide()
console.log(obj1 === obj2)
优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
- 只会实例化一次。简化了代码的调试和维护
缺点:
- 单例模式一般没有接口,扩展困难
- 有可能导致模块间的强耦合 从而不利于单元测试。
应用:登录框
工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。
该模式使一个类的实例化延迟到了子类。
而子类可以重写接口方法以便创建的时候指定自己的对象类型。
class Product1 {
product() {
console.log("生产一线");
}
}
class Product2 {
product() {
console.log("生产二线");
}
}
class Factory {
constructor() {
this.Product1 = Product1;
this.Product2 = Product2;
}
create(name, callBack) {
const product = new this[name]();
product.product();
return callBack("susess");
}
}
let p = new Factory();
p.create("Product1", (res) => {
console.log(res);
});
优点:
- 工厂职责单一化易于维护
- 有利于消除对象间的耦合,提供更大的灵活性
缺点:添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
结构型
- 装饰者模式: 扩展功能,原有功能不变且可直接使用
- 代理模式: 显示原有功能,但是经过限制之后的
代理模式
是为一个对象提供一个代用品或占位符,以便控制对它的访问
应用:
- ES6 的 proxy
- HTML元 素事件代理
1. 给"ul"标签添加点击事件
2. 当点击某"li"标签时,该标签内容拼接"."符号。如:某"li"标签被点击时,该标签内容为".."
注意:
1. 必须使用DOM0级标准事件(onclick)
target表示当前触发事件的元素
currentTarget是绑定处理函数的元素
只有当事件处理函数绑定在自身的时候,target才会和currentTarget一样
<ul>
<li>.</li>
<li>.</li>
<li>.</li>
</ul>
<script type="text/javascript">
document.querySelector('ul').onclick=event=>{
event.target.innerText+='.'
}
</script>
装饰器模式
动态地给某个对象添加一些额外的职责,是一种实现继承的替代方案
class Cellphone {
create() {
console.log('生成一个手机')
}
}
class Decorator {
constructor(cellphone) {
this.cellphone = cellphone
}
create() {
this.cellphone.create()
this.createShell(cellphone)
}
createShell() {
console.log('生成手机壳')
}
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()
console.log('------------')
let dec = new Decorator(cellphone)
dec.create()
优点:
- 方便动态的扩展功能,且提供了比继承更多的灵活性。
缺点:
- 多层装饰比较复杂。
应用:
- ES7 Decorator
- 比如现在有4 种型号的自行车,我们为每种自行车都定义了一个单 独的类。现在要给每种自行车都装上前灯、尾 灯和铃铛这3 种配件。如果使用继承的方式来给 每种自行车创建子类,则需要 4×3 = 12 个子类。 但是如果把前灯、尾灯、铃铛这些对象动态组 合到自行车上面,则只需要额外增加3 个类
行为型
职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
// 请假审批,需要组长审批、经理审批、总监审批
class Action {
constructor(name) {
this.name = name
this.nextAction = null
}
setNextAction(action) {
this.nextAction = action
}
handle() {
console.log( `${this.name} 审批`)
if (this.nextAction != null) {
this.nextAction.handle()
}
}
}
let a1 = new Action("组长")
let a2 = new Action("经理")
let a3 = new Action("总监")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
优点:
- 简化了对象。使得对象不需要知道链的结构
缺点:
- 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
- 使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。
应用:
- JS 中的事件冒泡
- 作用域链
- 原型链
观察者模式
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result')
},
1000);
})
p1.then(res => console.log(res), err => console.log(err))
分析Promise的调用流程:
Promise
的构造方法接收一个executor()
,在new Promise()
时就立刻执行这个executor回调executor()
内部的异步任务被放入宏/微任务队列,等待执行then()
被执行,收集成功/失败回调,放入成功/失败队列executor()
的异步任务被执行,触发resolve/reject
,从成功/失败队列中取出回调依次执行
观察者模式:收集依赖 -> 触发通知 -> 取出依赖执行
在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖
。
class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._resolveQueue = [] // then收集的执行成功的回调队列
this._rejectQueue = [] // then收集的执行失败的回调队列
/*由于resolve/reject是在executor内部被调用,
因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue*/
let _resolve = (val) => {
// 从成功队列里取出回调依次执行
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一个成功的回调和一个失败的回调,并push进对应队列
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}
操作系统
进程
程序、进程、线程⭐
程序:(静态)以 文件形式 存于 硬盘
进程:(传统OS)资源分配 和 独立调度 的 基本单位,进程 实体 的 运行过程
线程:(引入线程的OS)独立调度 的 基本单位
进程 的状态 和 转换
- 运行
- 就绪:仅缺处理机
- 阻塞/等待:等待资源(除处理机)可用 或 输入/输出完成
- 创建:正创建;
- 结束:正消失;
<--时间片/优先级--
新建---创建--->就绪---调度--->运行----退出--->终止
事件发生↖阻塞↙等待事件
死锁
⭐常考类型:进程Pi各需资源S Xi个,则Smin/Nmax不死锁条件:S=∑(Xi-1)+1
- 定义:多进程 ∵资源竞争 而造成 相互等待 的僵局,无外力作用下 都无法 继续推进
- 原因:非剥夺资源的 竞争 + 进程的 非法推进顺序(含 信号量 使用不当)
- 充要:
等待循环+Pi的资源 必须 由 Pi+1 满足(当各类资源=1,则循环等待=死锁)
- 必要条件:b->a、c->d
- 互斥访问
- 非剥夺资源
- 请求和保持
- 循环等待
处理 | 预防 | 避免 | 检测 |
分配 | 严格, 宁愿闲置资源 | 折中, 运行时判断是否可能死锁 | 宽松,并发性最强 只要允许 就分配 |
操作 | 破坏必要条件之一: 一次请求all;剥夺;按资源序分配 | 算法通过 是否安全状态,找 可能的安全序列 | 定期检查 是否死锁 |
优点 | 适用突发处理,不必剥夺 | 不必剥夺,限制条件弱,系统性能较好 | 不延长进程初始化时间, 允许 对死锁 现场处理 |
缺点 | 效率低,进程初始化时间长;饥饿 剥夺次数过多;不便灵活申请资源 | 需知 将来需求资源; 可能 长时阻塞进程 | 剥夺解除死锁,造成损失 |
- 预防:
破坏必要条件:
- 互斥访问:某些场合 必须保证 互斥,∴实现 可能性小:
- 非剥夺资源:释放已获得,造成前段工作失效;反复申请+释放,增加开销,降低吞吐
(常用于 易于保存和恢复的资源,eg:CPU的寄存器+内存资源;而非打印机etc)
- 请求和保持:预先静态分配(一次申请完);饥饿
- 循环等待:资源编号,只能按递增申请,同类资源一次申请完
(需编号稳定,限制了设备增加;可能 用资源和规定顺序不同,造成浪费资源;编程麻烦)
- 避免:(死锁=>不安全状态 )
银行家算法:Max,Need,Allocation,Available
- 检测:
- 死锁定理:S状态的 资源分配图 不可完全简化
- 解除:
- 剥夺(暂停):挂起 某些进程,并抢夺资源(应防止 被挂起的进程 长期得不到资源)
- 撤销(关闭):(按 进程 优先级+撤销代价)撤销 部分甚至全部 进程,并抢夺资源
- 回退(回放):一/多个进程 回退到 足以避免死锁,自愿释放资源(要求 系统 保持进程 的历史信息,设置还原点)
并发和并行
并发:逻辑上的同时发生(simultaneous)一个处理器同时处理多个任务。
并行:物理上的同时发生,是指多个处理器或者是多核的处理器同时处理多个不同的任务。
处理机调度
调度层次
多道批处理系统 大多有作业调度,其他系统则不需要
- 低级/作业调度:外/辅存 后备--->入内存,建进程,分资源,获 竞争处理机权利
- 中级/内存调度:暂不能运行--->外存 挂起(提高内存利用率+系统吞吐量)
- 高级/进程调度:按某种方法/策略 分配
调度基本准则
- CPU利用率
- 系统吞吐量:单位时间内 CPU完成的作业量
- 周转时间=作业完成时间-提交时间=t总;
- 带权周转时间=
- 等待时间=∑等待处理机时间;(判断 效率)
- 响应时间=首次响应时刻-提交时刻;
调度方式
- 非剥夺/抢占式:适用 多数 批处理系统
- 剥夺/抢占式:提高 系统吞吐率+响应率
调度算法⭐
(含作业/进程)
平均等待时间:时间片轮转 较长(上下文切换 费时); 短作业优先 最短
FCFS | 短作业优先 | 高响应比 | RR | 多级反馈队列 | |
抢占 | x | √ | √ | √ | 对内算法? |
非抢占 | √ | √(默认) | √(默认) | x | 对内算法? |
适用 | 无 | 批处理OS | 无 | 分时 | 通用 |
- FCFS先来先服务(First Come First Serve):利于长作业,CPU繁忙型作业
- SJF短作业优先:一个/若干 估计运行时间 最短 作业 入内存
- SPF短进程优先:一个最短 进程 调度,分配处理机
- 优先级:静优先级 取决 进程类型(系统>用户),要求资源(I/O>计算),用户要求
动态priority=nice+k1*cpuTime-k2*waitTime(k1,k2>0调整所占比例)
- 高响应比 优先(主要 作业)=FCFS+SJF;无 饥饿
t总/t实=响应比Rp=
- 时间片轮转RR(主要 分时):长短 取决 系统响应时间,就绪进程数,系统处理能力
- 多级反馈队列=时间片+优先级:
- 特点:
- 级↓的就绪队列,优先级↑,时间片↑
- 新进程入内存,先1级,时间片用完则降级;第n级队列 时间片轮转
- i级队列空,才执行i+1级队列
- 若执行j级队列时,k级队列入进程(k<j),则抢占,当前进程回j队列末尾
- 优势:
- 终端型作业用户:短作业优先(大多交互型,常短小)
- 短批处理作业用户:周转时间短
- 长批处理作业用户:经过前几个队列的部分执行,不会长时间无响应
内存管理
内存空间的扩充:从逻辑上扩充,虚拟存储/自动覆盖技术
- 源程序->可在内存中 执行的程序:
- 编译:编译程序 编译 源代码 成 若干个 目标模块
- 链接:链接程序 链接 目标模块 + 所需库函数,形成 一个 完整 的装入模块(形成逻辑地址)
- 装入:装入程序 将 装入模块 装入 内存(形成绝对地址)
- 相对/逻辑地址:编译后,每个目标模块 都从0号单元开始编址
- 逻辑地址空间:各个目标模块 的 相对地址 构成 的统一 从0号单元开始编址的 集合
- (内存管理的具体机制 完全透明,只有系统编程人员 才涉及,用户程序/程序员只需 知道逻辑地址)
- 物理地址空间:内存 中 物理单元的集合
- 地址重定位:逻辑地址->物理地址
连续空间分配策略算法⭐
分配策略算法 | 首次适应FF | 最佳适应BF | 最坏适应WF | 邻近适用NF |
空闲分区链接 | 地址递增 | 容量递增 | 容量递减 | 循环 首次适应 |
性能 | 最简单、快、好 | 最多 外部碎片 | 很快没大内存块 | 内存末尾 碎片 |
比较 | (∵留下了 高地址的大空闲区,∴更可能满足进程) 优于顺序:FF 可能>BF >WF,FF 通常>NF |
页面置换算法⭐
- 最佳(OPT)置换算法:替换 最长时间内/永久 不再被访问
- ∵最低缺页率,不可实现,∴只拿来评价其他算法
- 先进先出(FIFO)置换算法(队列):Belady异常(分配物理块数↑,页故障数↑)
- 最近最久未使用(LRU)置换算法(堆栈):性能接近OPT,但需寄存器+栈;困难,开销大
- 理论可证明,堆栈类算法 不可能出现 Belady异常
- 时钟(CLOCK)/最近未使用(NRU) 置换算法:循环扫描 缓冲区
- 简单CLOCK 算法:
使用位:每一帧关联一个附加位/访问位
使用位 置1:首次装入/再被访问
候选帧 集合:看做 循环缓冲区,有一个指针与之关联
替换:按装入顺序扫描/上次扫描位置,扫描查找到0的帧,之前的1帧置0
- 改进型CLOCK算法:+ 修改位m(修改过的页,被替换前,需写回外存)
替换 第一个帧(u=0,m=0)
重新扫描,替换 第一个帧(u=0,m=1),跳过的帧u置0
指针回到 最初位置,所有帧 u置0,重复①
数据结构
链表
- 指针:是结点的相对地址,即数组下标,即游标
- 分配:预先 分配 连续 的 内存空间
- 结束标志:next=-1
a | --> | b | --> | c | ^ |
下标 | data | next |
0 | 2 | |
1 | b | 4 |
2 | a | 1 |
3 | ||
4 | c | -1 |
栈
压栈 的 出入序列
(以 入栈 1 2 3 4 5 为例)
(1)出栈p首时,p前的序列A,只能逆序出栈,且插在A中每个元素后面
eg:4****; 4_3_2_1_
(2)p出栈序列的前一个元素p1,可能为p的前面的 或 后一个结点
eg:出栈 p1,3 则p1可能=1,2;4
n个不同元素进栈,出栈序列数
合法的出栈序列的数量=出栈序列的总数-非法序列的数量
卡特兰数Catalan
栈指针
操作 | 初始S.top=-1,即top指向栈顶 | S.top=0 | 共享栈,top指向栈顶 |
栈顶元素 | S.data[S.top] | S.data[S.top-1] | S.data[S.top] |
进栈 | S.data[++top]=x; | S.data[top++]=x; | S.data[--top1]=x; |
出栈 | x=S.data[top--]; | x=S.data[--top]; | x=S.data[top1++]; |
栈空 | S.top==-1; | S.top=0; | top1==MaxSize; top0=0; |
栈满 | S.top==MaxSize-1; | S.top==MaxSize; | top1-top0=1; |
- 缺点:数组上界 约束 入栈,对 最大空间 估计不足时,可能上溢(整个 存储空间满时)
- 共享栈:栈顶向 共享空间 延伸,栈底 在两端,优点:更有效 利用 存储空间
表达式求值
将表达式构建成中序二叉树,然后先序求值
前,中,后缀 指 op在 两 操作数 中的位置
- 中缀表达式:A+Bx(C-D)-E/F依赖 运算符的优先级;处理括号
- 后缀表达式:ABCD-x+EF/- 已考虑 运算符优先级,无括号
后缀表达式
- 组成:只有 操作数 + 运算符;
- 表达:原运算式 对应 的表达式树 的后续遍历
- 计算表达式值:
- 初始设置一个空栈,顺序扫描 后缀表达式
- 若为操作数,则压入栈
- 若为操作符<op>,则连续从 栈中 退出 两个操作数Y和X,形成 运算指令X<op>Y,计算结果 重新 压入栈
- 所有表达式 项 都扫描完, 栈顶 即为 结果
中缀 转换为 前/后缀:(手工)
- 按运算符优先级,对所有运算单位 加()
- 运算符 移到 相应 的()前/后面
- 去掉括号
中缀 转换为 后缀:以a*b+(-c)为例
- 根本:栈存放 暂时 不能确定 运算次序的 操作符
- 算法思想:
- 从左到右 扫描 中缀表达式
- 扫到 数,加入后缀
- 扫到 符:
- ‘(’:入栈
- ‘)’:栈内运算符依次 出栈,直至栈内遇到‘(’,然后 直接删除‘(’
- 其他运算符:优先级 > 栈顶的非‘(’运算符时or栈空or栈顶‘(’,直接入栈
否则,依次弹出 当前处理 符 ≤ 栈内优先级 的 运算符,
直到遇‘(’or 优先级<当前处理 符
待处理序列 | 栈 | 后缀表达式 | 当前扫描元素 | 动作 |
a*b+(-c) | a | a加入后缀表达式 | ||
*b+(-c) | a | * | *入栈 | |
b+(-c) | * | a | b | b加入后缀表达式 |
+(-c) | * | ab | + | +<栈顶*,弹出* |
+(-c) | ab | + | +入栈 | |
(-c) | + | ab* | ( | (入栈 |
-c) | +( | ab* | - | 栈顶为(,-入栈 |
c) | +(- | ab* | c | c加入后缀表达式 |
) | +(- | ab*c | ) | 把栈中(之后的符号入后缀,并删( |
ab*c- | 扫描完毕,运算符依次退栈,入后缀 | |||
ab*c-+ | 完成 |
- 具体转换 过程 (在 中缀表达式后+‘#’表示 表达式结束,题中 不算 操作符)
操作符 | # | ( | *,/ | +,- | ) |
isp栈内优先 | 0 | 1 | 5 | 3 | 6 |
icp栈外优先 | 0 | 6 | 4 | 2 | 1 |
步骤 | 扫描项 | 项类型 | 动作 | 栈内 | 输出 |
0 | ‘#’进栈,读下一符号 | # | |||
1 | a | 操作数 | 直接输出 | # | a |
2 | * | 操作符 | isp(‘#’)<icp(‘*’),进栈 | #* | |
3 | b | 操作数 | 直接输出 | #* | b |
4 | + | 操作符 | isp(‘*’)>icp(‘+’),退栈并输出 | # | * |
5 | isp(‘#’)<icp(‘+’),进栈 | #+ | |||
6 | ( | 操作符 | isp(‘-’)<icp(‘(’),进栈 | #+( | |
7 | - | 操作符 | isp(‘(’)<icp(‘-’),进栈 | #+(- | |
8 | c | 操作数 | 直接输出 | #+(- | c |
9 | ) | 操作符 | isp(‘-’)>icp(‘)’),退栈并输出 | #+( | - |
10 | isp(‘(’)==icp(‘)’),直接退栈 | #+ | |||
11 | # | 操作符 | isp(‘+’)>icp(‘#’),退栈并输出 | # | + |
12 | isp(‘#’)==icp(‘#’),退栈,结束 |
队列
顺序存储
队非空时,
Q.front指向队头元素的上一个元素,Q.rear指向队尾元素
Q.front指向队头元素,Q.rear指向队尾元素的下一个位置
∵假溢出:初始Q.front==Q.rear=0; “队满”Q.front==Q.rear
∴循环队列
- 初始Q.front=Q.rear=0;
- 队长:(Q.rear-Q.front+MaxSize)%MaxSize
- 出队/入队时,Q.front或Q.rear顺时针+1:
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize
链式存储
链队列
- 优点:不存在队满 甚至 溢出
- 适用:数据元素波动大,或者 多个队列
- 队空:
- 无头结点: Q.rear=NULL,Q.front=NULL
- 带头结点:Q.front==Q.rear
(带头结点,增删操作统一)
- 入队:...s->next=NULL...
- 出队:...Q.front->next=p->next,if(Q.rear==p){ Q.rear=Q.front;}...
树
二叉树
N0=1+N2
满二叉树
- 结点数:2^h-1
结点i的
- 父结点:└i/2┘
- 左孩子结点:2i
- 右孩子结点:2i+1
哈夫曼树(最优二叉树)Huffman
目的:找出存放一串字符所需的最少的二进制编码
最小的两个合成组成二叉树。在频率表中删去他俩,并加入新的根结点。重复该步骤
默认是小在左,大在右,,所以哈弗曼编码不唯一
例如:频率表 B:45, C:13 D:69 E:14 F:5 G:3
度m的哈夫曼树只有度为0和m的结点∴Nm=(n-1)/(m-1)
- 固定长度编码:待处理的字符串序列中,每个字符用同样长度的二进制位
- 可变长度编码:频率高的字符短编码;平均编码长度 减短,压缩数据
- 前缀编码:没有一个 编码 是 另外 一个编码的前缀
- 哈夫曼是前缀,可变长度编码
二叉排序树/二叉查找树BST(Binary Search/Sort Tree)
左子树的关键字 <根结点<右子树的关键字
判断是否为BST:中序序列递增=>B为BST,即pre < bt->data
平衡二叉树AVL
任一结点的 左子树 和 右子树 的深度之差≤1
插入:若需调整,则每次 调整对象 必为 最小 不平衡二叉树
查找:Nh表示深度为h最少结点数,则N0=0,N1=1,N2=2,Nh=Nh-1+Nh-2+1
大根堆
左/右子树的关键字 ≤根结点,完全二叉树
最小生成树
- 定义:连通,无向带权 图的生成树,权值之和最小的
- 唯一:当任意环中边的权值相异,则最小生成树唯一
普里姆Prim算法 | 克鲁斯卡Kruskal算法 | |
共同 | 基于贪心算法 | |
特点 | 从顶点开始扩展最小生成树 | 按权递增次序,选择不构成环的边 |
T(n) | O(|V|^2) | O(|E|log2|E|) 堆存放E,每次选最小权值边O(log2|E|) T所有边看成等价类,每次+边,看成求等价类∴并查集描述T, ∴构造T需O(|E|) |
适用 | 稠密图 | 稀疏图 |
森林
对应树 | 森林1次 | 对应二叉树 |
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
先序 确定的 二叉树个数
∵先序+中序 可 唯一 确定 一棵二叉树
其关系 就如 入栈序列+出栈序列 可 唯一 确定 一个 栈
∴先序 确定 二叉树个数,即先序 确定 中序个数,
NLR确定LNR,LN、NL相当于压栈,R相当于进了立即出
∴h(n)=Catalan卡特兰数=
带权路径长度WPL
WPL=∑(weight*路径边数)=(16+21+30)*2+(10+12)*3=200
查找次数=路径上的结点数,路径长度=路径上的边数
图
完全图
- 无向:任意两个顶点间,只有一条边,n(n-1)/2条边
- 有向:任意两个顶点间,只有方向相反的两条弧,n(n-1)条弧
最短路径
Dijkstra算法 | Floyd算法 | |
问题 | 单源最短路径(单起源到各个顶点的最短距离,从源点的临近点开始) | 各个顶点之间的最短路径 |
拓扑排序
- DAG:有向无环图Directed Acycline Graph
- 拓扑排序:DAG中,每个顶点只出现一次,对每个<u,v>,序列中,u在v前
- 唯一:图为线性有序序列时,唯一;若存在顶点 有多个后继则不唯一
- 邻接矩阵:
- 算法:
- 输出并删除 一个 没有前驱 的结点
- 删除 以该结点 为弧头 的边
- 重复(1)(2),直到 DAG为空 或者 不存在 无前驱的 结点(环)
关键路径
- 关键路径:最长路径,工程所需时间
- 关键活动:最长路径上的边
- ve(k):事件k最早发生时间ve(k)=0(源点),ve(k)=Max{ve(j)+Weight(j,k)}
- vl(k):事件k最迟发生时间vl(k)=ve(k)(汇点),vl(k)=Min{vl(j)-Weight(j,k)}
- e(i):活动ai最早开始时间<vk,vj>,e(i)=vl(k)
- l(i):活动ai最迟开始时间<vk,vj>,l(i)=vl(j)-Weight(k,j)
- d(i):l(i)-e(i),为0的即关键活动
- 适度缩短 关键活动,可以缩短工期,过度时,关键活动可能变成非关键活动
- 多关键路径时,缩短 所有 关键路径 才 缩短工期,除非有“桥”---所有关键路径的共有活动
模式匹配
主串S,长n,模式串T,长m。T在S中首次出现的位置
BF模式匹配
最坏T(n)=O(m*n)
KMP模式匹配
- next[j]:T的第j个字符失配于S中的第i个字符,需要用T的第next[j]个字符与S中的第i个字符 比较
abcdeabf(f失配,第next[j]=3个字符c比较)T起点开始,和失配点结束的最大公共前缀
- next[1]=0:i++;
- next[2]=1,next[j]:i不变;
模式匹配过程:
- S中第i个char,T中第j个char
- j指向 失配点/ j=m(全部匹配成功) 为 一趟
虽KMP的T(n)=O(m+n),
但实际中BF的T(n)接近O(m+n),
∴至今采用
只有T中有很多部分匹配,KMP才明显快
内部排序算法
T(n)和S(n)
- 任何 基于 比较 的算法,都可用 二叉树 描述判定过程,∴T(n)至少=O(nlog2n)
操作 | 内部排序 | 思想 | 稳定 | 平均 S(n) | T(n) | ||
平均 | 最坏 | 最好 | |||||
插 入 | 直接 | 查找;elem插入到有序,顺序找位 | √ | 1 | n2 | n2顺序 | n逆序 |
折半 | 查找;直接插入的优化,折半找位 | x | 1 | n2与初始序列无关 | |||
希尔 | 分治;分组直接插入排序d个组L[i,i+d,...,i+kd] | x | 1 | n1.3 | n2 | 依赖f(d) | |
交换 | 冒泡 | 擂台;无序中两两比较,不等则交换,至最值 | √ | 1 | n2 | n2逆序 | n顺序 |
快速 | 分治;取pivot,调整至L[1...k-1]<L(k)≤L[k+1...n] | x | log2n | nlog2n | n2最不平衡 | nlog2n最平衡 | |
选择 | 简单 | 擂台;第i趟L[i...n],初始min=i,swap(L(min),L(i)) | x | 1 | n2 | n2逆序 | n2顺序 |
堆 | 擂台;完全二叉树,根>子结点(大根堆) | x | 1 | nlog2n | nlog2n逆序 | nlog2n顺序 | |
2-路归并 | 分治;分组排序,两两合并 相邻 有序序列 | √ | n | nlog2n | nlog2n逆序 | nlog2n顺序 | |
基数 | 多key;从优先级min的开始入队分配,收集 | √ | r | d(n+r)与初始序列无关 |
- 顺序/链式存储 均可:(与 下标 无关)直接插入,冒泡,快速,简单选择,2-路归并
- 顺序存储:(数组)折半,希尔,堆,基数
- 比较次数与初始状态无关:简单选择,基数
- T(n)与初始序列无关:折半,堆,多路归并,基数
- 过程特征:(每一趟确定一个elem最终位置)
- 第k趟确定第k小/大值:冒泡,堆,简单选择
- 第k趟确定第i小/大值:快速
应用
- 考虑因素:
- n待排序数目
- elem本身信息量大小
- key的结构
- 稳定性要求
- 语言工具条件
- 存储结构
- 辅助空间大小
- 情况:
- n≤50 ? 100:n2
- 链式存储/n≤50:直接插入
- 顺序存储:折半插入
- elem本身信息量大:简单选择(移动少)
- 1000≥n>50?100:希尔
- n>1000:nlog2n
- key随机分布:快速排序
- key位数少且可分解:基数
- T(n)与初始序列无关:
- 辅助空间小:堆
- 稳定:归并
- key基本有序:(比较:直接插入 < 冒泡 ,移动:直接插入>冒泡)
- 基本逆序:直接插入
- 基本顺序:冒泡
- elem本身信息量大:链式存储;避免耗费 大量移动记录
- 总体信息量大:归并排序;内存一次放不下,需要外部介质
平均查找长度ASL
顺序 / 线性查找
ASL | 无序线性表 | 有序表 |
succ | (n+1)/2 | |
fail | n+1 |
折半 / 二分查找
判定树:描述 折半查找过程
ASLsucc≈log2(n+1)-1
分块 / 索引顺序查找
- 优点:动态结构,快速查找
- 基本思想:
- 块间:第i块max key<第i+1块all key
- 块内:无序
- 索引表:含 各块的 max key和各块 第一个元素 地址
- ASLSucc=LI+LS
- LI:ASL索引查找
- LS:ASL块内查找
- 分b块,每块s个记录:(b+1)/2 + (s+1)/2 =
(索引表,顺序表,均顺序查找)当s=时ASLmin=+1
- 若对索引表 折半查找:┌log2(b+1)┐ +(s+1)/2
散列(Hash)表
- 散列表:根据 关键字 直接访问 的 数据结构
- 对比:散列表中key和 存储地址 有直接映射关系,而线性表,树表等无确定映射关系
- 散列函数:Hash(key)=Addr(数组下标,索引,内存地址)
- 冲突:不同关键字 映射到同一地址
- 同义词:映射到同一地址的不同关键字
- T(n):无冲突时,O(1)
散列函数 构造
要求
- 定义域:全部 关键字
- 值域:依赖于 散列表大小/地址范围
- 地址:计算出来的地址 应 等概率,均匀 分布 整个地址空间
- 计算:简单,以在短时间计算出
方法 | Hash(key) | 适用 | 不适/特点 |
直接定址 | a*key+b | key分布连续 | key分布不连续,空位多,则空间浪费 |
除留取余 | key MOD p | 质数p左接近表长m;简单,常用 | |
平方取中 | key2的中间几位 | 每一位取值都不够均匀 /均<Addr所需位数 | Addr与key的每一位都有关系, 使Addr分布均匀 |
数字分析 | 数码分布均匀的几位 | 已知Key的集合 (r进制,r个数码) | 某些位分布不均匀, 只有某几种数码经常出现 |
折叠 | 分割成位数相同的几部分, 取这几部分的叠加和 | key位数多,且每一位 数字上分布大致均匀 | key分成 位数相同的几部分 (最后一部分,可以短些) |
处理冲突
- 定义:同义词 找下一个“空”Hash地址
- 方法:
*Hi表示冲突后的第i次探测的散列地址,m散列表表长,di增量序列,i∈[1,m-1]
- 开放地址:Hi=(Hash(key)+di)%m空闲地址 还对 同义词表项开放
- 线性探测:di=1...di++..di=m-1顺序查看下一单元,直到找到空闲单元/查遍全表
(检测到表尾地址m-1时,下一地址为表首地址0)
可能 大量元素 在相邻 散列地址 堆积,大大降低了查找效率
- 平方/二次探测:di=(k≤m/2,m为4k+3的质数);较好,可避免堆积;
不能 检测所有单元,但至少能检测 一半单元
- 再/双散列:di=Hash2(key);最多m-1次 探测 遍历全表,回到H0位置
- 伪随机序列:di=伪随机序列
- 拉链/链式地址:同义词链 由 散列地址 唯一标识;适用于 经常增删
ps:
∵查找时,碰到空指针 就认为查找失败
∴开放地址 不能物理删除元素,否则会 截断 其他具有相同 散列地址 的 查找地址;
∴只能做删除标记 ∴ 多次删除后,散列表看起来很满,其实许多位置没用
∴要定期维护散列表,把删除标记的 元素 物理删除
性能分析
- 决定因素:散列函数;处理冲突的方法;装满因子α=n/m=记录数/表长
- ASL:与α有关
递归
- 代码简单,容易理解
- 缺点:通常 包含很多重复计算,效率不高
- 精髓:将 原始问题 转换为 属性相似 的 小规模 问题
- 递归工作栈:容量与 递归调用最大深度一致
递归和递推的区别
- 递归:
- 设置递归边界
- 判断已经计算过,直接返回结果
- 返回关系式
- 递推:
- 初始化边界
- 根据初始化边界 开始 递推
- 循环递推
十进制转换为二进制
余数入栈
迷宫求解
- 已走过的0改2,且入栈,
- 坐标周围无0时,出栈直到遇到周围有0
算法(编程题)
场景题千千万,但都是由经典算法变换而来。
优化一般靠牺牲空间换时间
经验
一般过了3道编程,过了1.5就差不多,2就稳了。但是不绝对,有的一道题也会让你面试,有的a了2,也不一定有面试机会
有没有面试机会更多看的是卷的程度,名额多少,简历(例如学历高低)
- 运用示例,摸清规律,弄懂整个逻辑后,再动手
- 10min没有完整思路的先跳过,有时候局限了,回过头可能想得出来
- 随手保存
- 不要追求AC率,后面有空再返回完善,
- 注意题目中说明输入的上限,如下
read_line()//将读取至多1024个字符,一定注意看题目字符上限
gets(n)//将读取至多n个字符,当还未达到n个时如果遇到回车或结束符
常用输出
let a=[1,2,3];
console.log(a.toString()); //1,2,3 arr->str
console.log(a.join(' ')); // 1 2 3 arr->str
console.log(...a); // 1 2 3 展开运算符...
考核方式
ACM模式
自己构造输入格式,控制返回格式,OJ不会给任何代码,不同的语言有不同的输入输出规范。
JavaScript(V8)
readline()获取单行字符串
key:
read_line()//将读取至多1024个字符,一定注意看题目字符上限
gets(n)//将读取至多n个字符,当还未达到n个时如果遇到回车或结束符
printsth(sth, ...)//多个参数时,空格分隔;最后不加回车。
console.log(sth, ...)、print(sth, ...)//多个参数时,空格分隔;最后加回车
line.split(' ').map(e=>Number(e));//str->arr
arr.push([]);//arr[]->arr[][]
//单行输入
while(line=readline()){
//字符数组
var lines = line.split(' ');
//.map(Number)可以直接将字符数组变为数字数组
var lines = line.split(' ').map(Number);
var a = parseInt(lines[0]);//效果等同下面
var b = +lines[1]; //+能将str转换为num
print(a+b);
}
//矩阵的输入
while (line = readline()) {
let nums = line.split(' ');//读取第一行
var row = +nums[0];//第一行的第一个数为行数
var col = +nums[1];//第一行的第二个数为列数
var map = [];//用于存放矩阵
for (let i = 0; i < row; i++) {
map.push([]);
let mapline = readline().split(' ');
for (let j = 0; j < col; j++) {
map[i][j] = +mapline[j];
}
}
}
JavaScript(Node)
华为只可以采用Javascript(Node)
模板1
var readline = require('readline')
// 创建读取行接口对象
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
单行
//监听换行,接受数据
rl.on('line', function(line) {
//line为输入的单行字符串,split函数--通过空格将该行数据转换为数组。
var arr= line.split(' ')
//数组arr的每一项都是字符串格式,如果我们需要整型,则需要parseInt将其转换为数字
console.log(parseInt(arr[0]) + parseInt(arr[1]));
})
多行
const inputArr = [];//存放输入的数据
rl.on('line', function(line){
//line是输入的每一行,为字符串格式
inputArr.push(line.split(' '));//将输入流保存到inputArr中(注意为字符串数组)
}).on('close', function(){
console.log(fun(inputArr))//调用函数并输出
})
//解决函数
function fun() {
xxxxxxxx
return xx
}
模板2
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
void async function () {
// Write your code here
while(line = await readline()){
let tokens = line.split(' ');
let a = parseInt(tokens[0]);
let b = parseInt(tokens[1]);
console.log(a + b);
}
}()
核心代码模式
只需要实现函数核心功能并返回结果,无须处理输入输出
例如力扣上是核心代码模式,就是把要处理的数据都已经放入容器里,可以直接写逻辑
链表
判断链表是否有环
key:遍历链表,判断相邻结点是否相等,若结点为空,则false,若相等,则true
function ListNode(x){
this.val = x;
this.next = null;
}
/**
*
* @param head ListNode类
* @return bool布尔型
*/
function hasCycle( head ) {
// write code here
if(!head || !head.next){return false}
let fast = head.next
let slow = head
while(slow !== fast){
if(!fast || !fast.next){
return false
}
fast = fast.next.next
slow = slow.next
}
return true
}
module.exports = {
hasCycle : hasCycle
};
二叉树
(反)序列化二叉树
序列化二叉树,key:
- let arr = Array.isArray(s) ? s : s.split("");
- let a = arr.shift();
- let node = null;
- if (typeof a === "number")
function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
}
//反序列化二叉树:tree->str 把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串
function Serialize(pRoot, arr = []) {
if (!pRoot) {
arr.push("#");
return arr;
} else {
arr.push(pRoot.val);//注意是val。而不是root
Serialize(pRoot.left, arr);
Serialize(pRoot.right, arr);
}
return arr;
}
//序列化二叉树:str->tree 根据字符串结果str,重构二叉树
function Deserialize(s) {
//转换为数组
let arr = Array.isArray(s) ? s : s.split("");
//取出val
let a = arr.shift();
//构建二叉树结点
let node = null;
if (typeof a === "number") {
//还有可能等于#
node = new TreeNode(a);
node.left = Deserialize(arr);
node.right = Deserialize(arr);
}
return node;
}
module.exports = {
Serialize: Serialize,
Deserialize: Deserialize,
};
前序遍历(迭代)
入栈:中右左
出栈:中左右
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
let stack=[]
let res = []
let cur = null;
if(!root) return res;
root&&stack.push(root)
while(stack.length){
cur = stack.pop()
res.push(cur.val)
cur.right&&stack.push(cur.right)
cur.left&&stack.push(cur.left)
}
return res
};
中序遍历(迭代)
指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
let stack = []
let res = []
let cur = root
while(cur||stack.length){
if(cur){
stack.push(cur)
cur = cur.left
} else {
cur = stack.pop()
res.push(cur.val)
cur = cur.right
}
}
return res
};
后序遍历(迭代)
和前序遍历不同:
入栈:中左右
出栈:中右左
rever出栈:左右中
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
let stack = []
let res = []
let cur = root
if(!root) return res
stack.push(root)
while(stack.length){
cur = stack.pop()
res.push(cur.val)
cur.left&&stack.push(cur.left)
cur.right&&stack.push(cur.right)
}
return res.reverse()
};
层序遍历
树的层序遍历,相似 图的广度优先搜索
- 初始设置一个空队,根结点入队
- 队首结点出队,其左右孩子 依次 入队
- 若队空,说明 所有结点 已处理完,结束遍历;否则(2)
/*
* function TreeNode(x) {
* this.val = x;
* this.left = null;
* this.right = null;
* }
*/
/**
*
* @param root TreeNode类
* @return int整型二维数组
*/
function levelOrder(root) {
// write code here
if (root == null) {
return [];
}
const arr = [];
const queue = [];
queue.push(root);
while (queue.length) {
const preSize = queue.length;
const floor = [];//当前层
for (let i = 0; i < preSize; ++i) {
const v = queue.shift();
floor.push(v.val);
v.left&&queue.push(v.left);
v.right&&queue.push(v.right);
}
arr.push(floor);
}
return arr;//[[1],[2,3]]
}
module.exports = {
levelOrder: levelOrder,
};
判断对称二叉树
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
let flag = true;
function deep(left, right) {
if (!left && !right) return true; //可以两个都为空
if (!right||!left|| left.val !== right.val) {//只有一个为空或者节点值不同,必定不对称
return false;
}
return deep(left.left, right.right) && deep(left.right, right.left); //每层对应的节点进入递归比较
}
function isSymmetrical(pRoot) {
return deep(pRoot, pRoot);
}
module.exports = {
isSymmetrical: isSymmetrical,
};
判断完全二叉树
完全二叉树:叶子节点只能出现在最下层和次下层,且最下层的叶子节点集中在树的左部。
/*
* function TreeNode(x) {
* this.val = x;
* this.left = null;
* this.right = null;
* }
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param root TreeNode类
* @return bool布尔型
*/
function isCompleteTree(root) {
// write code here
if (root == null) return true;
const queue = [];
queue.push(root);
let flag = false; //是否遇到空节点
while (queue.length) {
const node = queue.shift();
if (node == null) {
//如果遇到某个节点为空,进行标记,代表到了完全二叉树的最下层
flag = true;
continue;
}
if (flag == true) {
//若是后续还有访问,则说明提前出现了叶子节点,不符合完全二叉树的性质。
return false;
}
queue.push(node.left);
queue.push(node.right);
}
return true;
}
module.exports = {
isCompleteTree: isCompleteTree,
};
判断平衡二叉树
平衡二叉树是左子树的高度与右子树的高度差的绝对值小于等于1,同样左子树是平衡二叉树,右子树为平衡二叉树。
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
function IsBalanced_Solution(pRoot)
{
if(!pRoot) return true;
// write code here
return (Math.abs(getMaxDepth(pRoot.left) - getMaxDepth(pRoot.right)) <=1) && IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right)
}
function getMaxDepth(root) {
if(!root) return 0;
return Math.max(getMaxDepth(root.left)+1,getMaxDepth(root.right)+1)
}
module.exports = {
IsBalanced_Solution : IsBalanced_Solution
};
二叉树的镜像
先序遍历
/*
* function TreeNode(x) {
* this.val = x;
* this.left = null;
* this.right = null;
* }
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pRoot TreeNode类
* @return TreeNode类
*/
function Mirror( pRoot ) {
function traversal(root){
if(root===null) return ;
//交换左右孩子
let temp = root.left;
root.left = root.right;
root.right = temp;
traversal(root.left);
traversal(root.right);
return root;
}
return traversal(pRoot);
// write code here
}
module.exports = {
Mirror : Mirror
};
最近公共祖先
如果从两个节点往上找,每个节点都往上走,一直走到根节点,那么根节点到这两个节点的连线肯定有相交的地方,
如果从上往下走,那么最后一次相交的节点就是他们的最近公共祖先节点。
/*
* function TreeNode(x) {
* this.val = x;
* this.left = null;
* this.right = null;
* }
*/
/**
*
* @param root TreeNode类
* @param o1 int整型
* @param o2 int整型
* @return int整型
*/
function dfs(root, o1, o2) {
if (root == null || root.val == o1 || root.val == o2) {
return root;
}
//递归遍历左子树
let left = dfs(root.left, o1, o2);
//递归遍历右子树
let right = dfs(root.right, o1, o2);
//如果left、right都不为空,那么代表o1、o2在root的两侧,所以root为他们的公共祖先
if (left && right) return root;
//如果left、right有一个为空,那么就返回不为空的那一个
return left != null ? left : right;
}
数组和树
扁平结构(一维数组)转树
key:
- pid:parent id
- obj[item.id] = { ...item, children: [] }
- pid === 0
- !obj[pid]
- obj[pid].children.push(treeitem)
//pid:parent id
let arr = [
{ id: 1, name: '部门1', pid: 0 },
{ id: 2, name: '部门2', pid: 1 },
{ id: 3, name: '部门3', pid: 1 },
{ id: 4, name: '部门4', pid: 3 },
{ id: 5, name: '部门5', pid: 4 },
]
// // 上面的数据转换为 下面的 tree 数据
// [
// {
// "id": 1,
// "name": "部门1",
// "pid": 0,
// "children": [
// {
// "id": 2,
// "name": "部门2",
// "pid": 1,
// "children": []
// },
// {
// "id": 3,
// "name": "部门3",
// "pid": 1,
// "children": [
// {
// id: 4,
// name: '部门4',
// pid: 3,
// "children": [
// {
// id: 5,
// name: '部门5',
// pid: 4,
// "children": []
// },
// ]
// },
// ]
// }
// ]
// }
// ]
function tree(items) {
// 1、声明一个数组和一个对象 用来存储数据
let arr = []
let obj = {}
// 2、给每条item添加children ,并连带一起放在obj对象里
for (let item of items) {
obj[item.id] = { ...item, children: [] }
}
// 3、for of 逻辑处理
for (let item of items) {
// 4、把数据里面的id 取出来赋值 方便下一步的操作
let id = item.id
let pid = item.pid
// 5、根据 id 将 obj 里面的每一项数据取出来
let treeitem = obj[id]
// 6、如果是第一项的话 吧treeitem 放到 arr 数组当中
if (pid === 0) {
// 把数据放到 arr 数组里面
arr.push(treeitem)
} else {
// 如果没有 pid 找不到 就开一个 obj { }
if (!obj[pid]) {
obj = {
children: []
}
}
// 否则给它的 obj 根基 pid(自己定义的下标) 进行查找 它里面的children属性 然后push
obj[pid].children.push(treeitem)
}
}
// 返回处理好的数据
return arr
}
console.log(tree(arr))
数组扁平化
要求将数组参数中的多维数组扩展为一维数组并返回该数组。
数组参数中仅包含数组类型和数字类型
function flatten(arr){
// toString() + split() 实现
return arr.toString().split(',').map(item => Number(item));
//join() + split() 实现
return arr.join(',').split(',').map(item => Number(item));
//reduce 实现
return arr.reduce((target, item) => {
return target.concat(Array.isArray(item) ? flatten(item) : item);
}, [])
// 递归实现
let res = [];
arr.forEach(item => {
if (Array.isArray(item)) {
res = res.concat(flatten(item))
} else {
res.push(item);
}
});
return res;
// 扩展运算符实现
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
// flat()实现(这里不支持使用)
return arr.flat(Infinity);
}
排序
快速排序
快速排序的基本思想是通过分治来使一部分均比另一部分小(大)再使两部分重复该步骤而实现有序的排列。核心步骤有:
- 选择一个基准值(pivot)
- 以基准值将数组分割为两部分
- 递归分割之后的数组直到数组为空或只有一个元素为止
key:
- pivot = array.splice(pivotIndex, 1)[0]
- _quickSort(left).concat([pivot], _quickSort(right))
const _quickSort = array => {
if(array.length <= 1) return array
var pivotIndex = Math.floor(array.length / 2)
var pivot = array.splice(pivotIndex, 1)[0]
var left = []
var right = []
for (var i=0 ; i<array.length ; i++){
if (array[i] < pivot) {
left.push(array[i])
} else {
right.push(array[i])
}
}
return _quickSort(left).concat([pivot], _quickSort(right))
}
*归并排序
思想:两个/两个以上有序表 合并成 新 有序表
- 2路-归并排序:两两归并
- key:
- left=arr.slice(0,mid)
- mergeLeft=mergeSort(left)
- res.push(leftArr.shift())
- res=res.concat(leftArr)
function mergesort(arr){
if(arr.length<2)return arr
let len=arr.length
let mid=parseInt(len/2)
let l1=arr.slice(0,mid)
let r1=arr.slice(mid,len)
let mergeleft=mergesort(l1)
let mergeright=mergesort(r1)
return merge(mergeleft,mergeright)
function merge(left,right){
let res=[]
while(left.length!=0 &&right.length!=0){
if(left[0]<=right[0]){
res.push(left.shift())
}else{
res.push((right.shift()))
}
}
if(left.length){
res=res.concat(left)
}
if(right.length){
res=res.concat(right)
}
return res
}
}
*堆排序
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
注意:升序用大根堆,降序就用小根堆(默认为升序)
key:
headAdjust:
- for (var i = 2 * start + 1; i <= end; i = i * 2 + 1)
- if (i < end && arr[i] < arr[i-1])
buildHeap://从最后一棵子树开始,从后往前调整
- //最大元素保存于尾部,并且不参与后面的调整
- //进行调整,将最大元素调整至堆顶
- headAdjust(arr, 0, i);
//每次调整,从上往下调整
//调整为大根堆
function headAdjust(arr, start, end){
//将当前节点值进行保存
var tmp = arr[start];
//遍历孩子结点
for (var i = 2 * start + 1; i <= end; i = i * 2 + 1)
{
if (i < end && arr[i] < arr[i-1])//有右孩子并且左孩子小于右孩子
{
i--;//i一定是左右孩子的最大值
}
if (arr[i] > tmp)
{
arr[start] = arr[i];
start = i;
}
else
{
break;
}
}
arr[start] = tmp ;
}
}
//构建堆
function buildHeap(arr){
//从最后一棵子树开始,从后往前调整
for(var i= Math.floor(arr.length/2) ; i>=0; i--){
headAdjust(arr, i, arr.length);
}
}
function heapSort(arr){
//构建堆
buildHeap(arr);
for(var i=arr.length-1; i>0; i--){
//最大元素保存于尾部,并且不参与后面的调整
var swap = arr[i];
arr[i] = arr[0];
arr[0] = swap;
//进行调整,将最大元素调整至堆顶
headAdjust(arr, 0, i);
}
}
回溯
如果不能成功,那么返回的时候我们就还要把这个位置还原。这就是回溯算法,也是试探算法。
全排列
通过回溯剪枝。修剪掉有当前元素的path,最后保留与原字符串长度相等的所有元素。
key:
- path.length == string.length
- path.includes(item)
const _permute = string => {
const res = [];
const backtrace = path => {
if(path.length == string.length){
res.push(path);
return;
}
for(const item of string) {
if(path.includes(item)) continue;
backtrace(path + item);
}
};
backtrace('');
return res;
}
N皇后
N 皇后问题是指在 n * n 的棋盘上要摆 n 个皇后,
要求:任何两个皇后不同行,不同列也不在同一条斜线上,
求给一个整数 n ,返回 n 皇后的摆法数。
要求:空间复杂度 O(1) ,时间复杂度O(n!)
- 要确定皇后的位置,其实就是确定列的位置,因为行已经固定了
- 进一步讲,也就是如何摆放 数组
arr
[0,1,2,3,...,n-1]
- 如果没有【不在同一条斜线上】要求,这题其实只是单纯的全排列问题
- 在全排列的基础上,根据N皇后的问题,去除一些结果
-
arr
n个皇后的列位置 -
res
n皇后排列结果 -
ruler
记录对应的列位置是否已经占用(也是是否有皇后),如果有,那么设为1,没有设为0 -
setPos
哈希集合,标记正斜线(从左上到右下)位置,如果在相同正斜线上,坐标(x,y)满足 y-x 都相同 -
setCon
哈希集合,标记反正斜线(从y右上到左下)位置,如果在相同反斜线上,坐标(x,y)满足 x+y 都相同 -
是否在同一斜线上,其实就是这两个点的所形成的斜线的斜率是否为±1。点P(a,b) ,点Q(c,d)
(1)斜率为1 (d-b)/(c-a) = 1,横纵坐标之差相等
(2)斜率为-1 (d-b)/(c-a) = -1 ,等式两边恒等变形 a+b = c + d ,横纵坐标之和相等
/**
*
* @param n int整型 the n
* @return int整型
*/
function Nqueen(n) {
let res = []; //二维数组,存放每行Q的列坐标
let isQ = new Array(n).fill(0); //记录该列是否有Q
let setPos = new Set(); //标记正对角线
let setCon = new Set(); // 标记反对角线
//给当前row找一个col
const backTrace = (row, path) => {
if (path.length === n) {
res.push(path);
return;
}
for (let col = 0; col < n; col++) {
if (
isQ[col] == 0 &&
!setPos.has(row - col) &&
!setCon.has(row + col)
) {
path.push(col);
isQ[col] = 1;
setPos.add(row - col);
setCon.add(row + col);
backTrace(row + 1, path);
path.pop();
isQ[col] = 0;
setPos.delete(row - col);
setCon.delete(row + col);
}
}
};
backTrace(0, []);
return res.length;
}
module.exports = {
Nqueen: Nqueen,
};
动态规划(Dynamic Programming,DP)
用来解决一类最优化问题的算法思想。考虑最简单的情况,以及下一级情况和它的关系
简单来说,动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样
一般可以使用递归或者递推的写法来实现动态规划,其中递归写法在此处又称作记忆化搜索。
斐波那契(Fibonacci)数列(递归)
function F(n){
if(n= 0||n== 1) return 1;
else return F(n-1)+F(n-2);
}
dp[n]=-1表示F(n)当前还没有被计算过
function F(n) {
if(n == 0lIn-1) return 1;//递归边界
if(dp[n] != -1) return dp[n]; //已经计算过,直接返回结果,不再重复计算else {
else dp[n] = F(n-1) + F(n-2); 1/计算F(n),并保存至dp[n]
return dp [n];//返回F(n)的结果
}
数塔(递推)
第i层有i个数字。现在要从第一层走到第n层,最后将路径上所有数字相加后得到的和最大是多少?
dp[i][j]表示从第i行第j个数字出发到达最底层的所有路径中能得到的最大和
dp[i][i]=max(dp[i-1][j],dp[i-1][j+1])+f[i][j]
最长公共子序列(LCS)
Longest Common Subsequence:子序列可以不连续
“sadstory”与“adminsorry”最长公共子序列为“adsory”
dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS 长度下标从1开始)
最长回文子串
dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串,是则为1,不是为0
最小路径和
mxn矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。
dp[i][j]代表i到j的最短路径
求解子问题时的状态转移方程:从「上一状态」到「下一状态」的递推式。
dp[i, j] = min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j]
JavaScript中没有二维数组的概念,但是可以设置数组元素的值等于数组
key:
- dp[0][i] = dp[0][i - 1] + matrix[0][i];
- dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
function minPathSum(matrix) {
var row = matrix.length,
col = matrix[0].length;
var dp = new Array(row).fill(null).map(() => new Array(col).fill(0));
dp[0][0] = matrix[0][0]; // 初始化左上角元素
// 初始化第一行
for (var i = 1; i < col; i++) dp[0][i] = dp[0][i - 1] + matrix[0][i];
// 初始化第一列
for (var j = 1; j < row; j++) dp[j][0] = dp[j - 1][0] + matrix[j][0];
// 动态规划
for (var i = 1; i < row; i++) {
for (var j = 1; j < col; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
}
}
return dp[row - 1][col - 1]; // 右下角元素结果即为答案
}
背包
01背包
有n件物品,每件物品的重量为w[i],价值为c[j]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。
dp[i][v]表示前i件物品(1≤i≤n, 0≤v≤V)恰好装入容量为v的背包中所能获得的最大价值。
这样修改对应到图中可以这样理解:v的枚举顺序变为从右往左,dp[i][v]右边的部分为刚计算过的需要保存给下一行使用的数据,而dp[i][v]左上角的阴影部分为当前需要使用的部分。将这两者结合一下,即把 dp[i][v]左上角和右边的部分放在一个数组里,每计算出一个dp[i][v],就相当于把 dp[i - 1][v]抹消,因为在后面的运算中 dp[i- 1][v]再也用不到了。我们把这种技巧称为滚动数组。
特别说明:
如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓;
如果使用一维数组存放,则v的枚举必须是逆序!
完全背包
与01背包问题不同的是其中每种物品都有无数件。
写成一维形式之后和01背包完全相同,唯一的区别在于这里v的枚举顺序是正向枚举,而01背包的一维形式中v必须是逆向枚举。
散列/哈希Hash
空间换时间,即当读入的数为x时,就令hashTable[x]=true(说明: hashTable数组需要初始化为false,表示初始状态下所有数都未出现过)。
数字千位分割
const format = (n) => {
let num = n.toString() // 拿到传进来的 number 数字 进行 toString
let len = num.length // 在拿到字符串的长度
// 当传进来的结果小于 3 也就是 千位还把结果返回出去 小于3 不足以分割
if (len < 3) {
return num
} else {
let render = len % 3 //传入 number 的长度 是否能被 3 整除
console.log(render)
if (render > 0) { // 说明不是3的整数倍
return num.slice(0, render) + ',' + num.slice(render, len).match(/\d{3}/g).join(',')
} else {
return num.slice(0, len).match(/\d{3}/g).join(',')
}
}
}
let str = format(298000)
console.log(str)
常用方法
异或运算^
按位异或,相同为0,不同为1
运算法则:
1.交换律(随便换像乘一样):a ^ b ^ c === a ^ c ^ b
2.任何数于0异或为任何数 0 ^ n === n
3.相同的数异或为0: n ^ n === 0
Math
//e=2.718281828459045
Math.E;
//绝对值
Math.abs()
//基数(base)的指数(exponent)次幂,即 base^exponent。
Math.pow(base, exponent)
//max,min不支持传递数组
Math.max(value0, value1, /* … ,*/ valueN)
Math.max.apply(null,array)
apply会将一个数组装换为一个参数接一个参数
null是因为没有对象去调用这个方法,只需要用这个方法运算
//取整
Math.floor() 向下取一个整数(floor地板)
Math.ceil(x) 向上取一个整数(ceil天花板)
Math.round() 返回一个四舍五入的值
Math.trunc() 直接去除小数点后面的值
Number
0B,0O为ES6新增
- 二进制:有前缀0b(或
0B
)的数值,出现0,1以外的数字会报错(b:binary) - 八进制:有前缀0o(或
0O
)的数值,或者是以0后面再跟一个数字(0-7)。如果超出了前面所述的数值范围,则会忽略第一个数字0,视为十进制数(o:octonary) - 注意:八进制字面量在严格模式下是无效的,会导致支持该模式的JavaScript引擎抛出错误
- 十六进制:有前缀0x,后跟任何十六进制数字(0~9及A~F),字母大小写都可以,超出范围会报错
特殊值:
- Number.MIN_VALUE:5e-324
- Number.MAX_VALUE:1.7976931348623157e+308
- Infinity ,代表无穷大,如果数字超过最大值,js会返回Infinity,这称为正向溢出(overflow);
- -Infinity ,代表无穷小,小于任何数值,如果等于或超过最小负值-1023(即非常接近0),js会直接把这个数转为0,这称为负向溢出(underflow)
- NaN ,Not a number,代表一个非数值
- isNaN():用来判断一个变量是否为非数字的类型,如果是数字返回false;如果不是数字返回true。
- isFinite():数值是不是有穷的
var result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); //false
typeof NaN // 'number' ---
NaN
不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
NaN === NaN // false ---
NaN
不等于任何值,包括它本身(1 / +0) === (1 / -0) // false ---除以正零得到
+Infinity
,除以负零得到-Infinity
,这两者是不相等的
科学计数法:
对于那些极大极小的数值,可以用e表示法(即科学计数法)表示的浮点数值表示。
等于e前面的数值乘以10的指数次幂
numObj.toFixed(digits)//用定点表示法来格式化一个数值
function financial(x) {
return Number.parseFloat(x).toFixed(2);
}
console.log(financial(123.456));
// Expected output: "123.46"
console.log(financial(0.004));
// Expected output: "0.00"
console.log(financial('1.23e+5'));
// Expected output: "123000.00"
取余是数学中的概念,
取模是计算机中的概念,
两者都是求两数相除的余数
1.当两数符号相同时,结果相同,比如:7%4 与 7 Mod 4 结果都是3
2.当两数符号不同时,结果不同,比如 (-7)%4=-3和(-7)Mod4=1
取余运算,求商采用fix 函数,向0方向舍入,取 -1。因此 (-7) % 4 商 -1 余数为 -3
取模运算,求商采用 floor 函数,向无穷小方向舍入,取 -2。因此 (-7) Mod 4 商 -2 余数为 1
key:((n % m) + m) % m;
Number.prototype.mod = function(n) {
return ((this % n) + n) % n;
}
// 或
function mod(n, m) {
return ((n % m) + m) % m;
}
Map
保存键值对,任何值(对象或者基本类型)都可以作为一个键或一个值。
Map的键可以是任意值,包括函数、对象或任意基本类型。
object的键必须是一个String或是Symbol 。
const contacts = new Map()
contacts.set('Jessie', {phone: "213-555-1234", address: "123 N 1st Ave"})
contacts.has('Jessie') // true
contacts.get('Hilary') // undefined
contacts.delete('Jessie') // true
console.log(contacts.size) // 1
function logMapElements(value, key, map) {
console.log(`m[${key}] = ${value}`);
}
new Map([['foo', 3], ['bar', {}], ['baz', undefined]])
.forEach(logMapElements);
// Expected output: "m[foo] = 3"
// Expected output: "m[bar] = [object Object]"
// Expected output: "m[baz] = undefined"
Set
值的集合,且值唯一
let setPos = new Set();
setPos.add(value);//Boolean
setPos.has(value);
setPos.delete(value);
function logSetElements(value1, value2, set) {
console.log(`s[${value1}] = ${value2}`);
}
new Set(['foo', 'bar', undefined]).forEach(logSetElements);
// Expected output: "s[foo] = foo"
// Expected output: "s[bar] = bar"
// Expected output: "s[undefined] = undefined"
set判断值相等的机制
//Set用===判断是否相等
const set= new Set();
const obj1={ x: 10, y: 20 },obj2={ x: 10, y: 20 }
set.add(obj1).add(obj2);
console.log(obj1===obj2);//false
console.log(set.size);// 2
set.add(obj1);
console.log(obj1===obj1);//true
console.log(set.size);//2
数组去重 (⭐手写)
// Use to remove duplicate elements from the array
const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)])
// [2, 3, 4, 5, 6, 7, 32]
Array
//创建字符串
//join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串,用逗号
或指定的分隔符字符串分隔。如果数组只有一个元素,那么将返回该元素而不使用分隔符。
Array.join()
Array.join(separator)
//################创建数组:
//伪数组转成数组
Array.from(arrayLike, mapFn)
console.log(Array.from('foo'));
// Expected output: Array ["f", "o", "o"]
console.log(Array.from([1, 2, 3], x => x + x));
// Expected output: Array [2, 4, 6]
console.log( Array.from({length:3},(item, index)=> index) );// 列的位置
// Expected output:Array [0, 1, 2]
//################原数组会改变:
arr.reverse()//返回翻转后的数组
// 无函数
arr.sort()//默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16
// 比较函数
arr.sort(compareFn)
function compareFn(a, b) {
if (在某些排序规则中,a 小于 b) {
return -1;
}
if (在这一排序规则下,a 大于 b) {
return 1;
}
// a 一定等于 b
return 0;
}
//升序
function compareNumbers(a, b) {
return a - b;
}
//固定值填充
arr.fill(value)
arr.fill(value, start)
arr.fill(value, start, end)
//去除
array.shift() //从数组中删除第一个元素,并返回该元素的值。
array.pop() //从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度。
array.push() //将一个或多个元素添加到数组的末尾,并返回该数组的新长度
//unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度
array.unshift(element0, element1, /* … ,*/ elementN)
//粘接,通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
array.splice(start)
array.splice(start, deleteCount)
array.splice(start, deleteCount, item1)
array.splice(start, deleteCount, item1, item2...itemN)
//################原数组不会改变:
//切片,浅拷贝(包括 begin,不包括end)。
array.slice()
array.slice(start)
array.slice(start, end)
//展平,按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
array.flat()//不写参数默认一维
array.flat(depth)
//过滤器,函数体 为 条件语句
// 箭头函数
filter((element) => { /* … */ } )
filter((element, index) => { /* … */ } )
filter((element, index, array) => { /* … */ } )
array.filter(str => str .length > 6)
//遍历数组处理
// 箭头函数
map((element) => { /* … */ })
map((element, index) => { /* … */ })
map((element, index, array) => { /* … */ })
array.map(el => Math.pow(el,2))
//map和filter同参
//接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
// 箭头函数
reduce((previousValue, currentValue) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex) => { /* … */ } )
reduce((previousValue, currentValue, currentIndex, array) => { /* … */ } )
reduce((previousValue, currentValue) => { /* … */ } , initialValue)
reduce((previousValue, currentValue, currentIndex) => { /* … */ } , initialValue)
array.reduce((previousValue, currentValue, currentIndex, array) => { /* … */ }, initialValue)
//一个“reducer”函数,包含四个参数:
//previousValue:上一次调用 callbackFn 时的返回值。
//在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,
//否则为数组索引为 0 的元素 array[0]。
//currentValue:数组中正在处理的元素。
//在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],
//否则为 array[1]。
//currentIndex:数组中正在处理的元素的索引。
//若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。
//array:用于遍历的数组。
//initialValue 可选
//作为第一次调用 callback 函数时参数 previousValue 的值。
//若指定了初始值 initialValue,则 currentValue 则将使用数组第一个元素;
//否则 previousValue 将使用数组第一个元素,而 currentValue 将使用数组第二个元素。
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
String
str.charAt(index)//获取第n位字符
str.charCodeAt(n)//获取第n位UTF-16字符编码 (Unicode)A是65,a是97
String.fromCharCode(num1[, ...[, numN]])//根据UTF编码创建字符串
String.fromCharCode('a'.charCodeAt(0))='a'
str.trim()//返回去掉首尾的空白字符后的新字符串
str.split(separator)//返回一个以指定分隔符出现位置分隔而成的一个数组,数组元素不包含分隔符
const str = 'The quick brown fox jumps over the lazy dog.';
const words = str.split(' ');
console.log(words[3]);
// Expected output: "fox"
str.toLowerCase( )//字符串转小写;
str.toUpperCase( )//字符串转大写;
str.concat(str2, [, ...strN])
str.substring(indexStart[, indexEnd]) //提取从 indexStart 到 indexEnd(不包括)之间的字符。
str.substr(start[, length]) //没有严格被废弃 (as in "removed from the Web standards"), 但它被认作是遗留的函数并且可以的话应该避免使用。它并非 JavaScript 核心语言的一部分,未来将可能会被移除掉。
str.indexOf(searchString[, position]) //在大于或等于position索引处的第一次出现。
str.match(regexp)//找到一个或多个正则表达式的匹配。
const paragraph = 'The quick brown fox jumps over the lazy dog. It barked.';
let regex = /[A-Z]/g;
let found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T", "I"]
regex = /[A-Z]/;
found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T"]
//match类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
var str = '123123000'
str.match(/\w{3}/g).join(',') // 123,123,000
str.search(regexp)//如果匹配成功,则 search() 返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1
const paragraph = '? The quick';
// Any character that is not a word character or whitespace
const regex = /[^\w\s]/g;
console.log(paragraph.search(regex));
// Expected output: 0
str.repeat(count)//返回副本
str.replace(regexp|substr, newSubStr|function)//返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。
const p = 'lazy dog.Dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replace('dog', 'monkey'));
// "lazy monkey.Dog lazy"
let regex = /dog/i;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replace(regex, 'ferret'));
//"lazy ferret.Dog lazy"
regex = /d|Dog/g;
console.log(p.replace(regex, 'ferret'));
//"lazy ferretog.ferret lazy"
//当使用一个 regex 时,您必须设置全局(“g”)标志, 否则,它将引发 TypeError:“必须使用全局 RegExp 调用 replaceAll”。
const p = 'lazy dog.dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replaceAll('dog', 'monkey'));
// "lazy monkey.monkey lazy"
let regex = /dog/g;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replaceAll(regex, 'ferret'));
//"lazy ferret.ferret lazy"
正则表达式Regular Expression(RegExp)
RegExp 对象是一个预定义了属性和方法的正则表达式对象
字面量和字符串
//以下三种表达式都会创建相同的正则表达式:
/ab+c/i; //字面量形式 /正则表达式主体/修饰符(可选)
new RegExp('ab+c', 'i'); // 首个参数为字符串模式的构造函数
new RegExp(/ab+c/, 'i'); // 首个参数为常规字面量的构造函数
//防止在字符串中被解译成一个转义字符
var re = new RegExp("\\w+");//需要常规的字符转义规则(在前面加反斜杠 \)
var re = /\w+/;
当表达式被赋值时,字面量形式提供正则表达式的编译(compilation)状态,
当正则表达式保持为常量时使用字面量。
例如在循环中使用字面量构造一个正则表达式时,正则表达式不会在每一次迭代中都被重新编译(recompiled)。
正则表达式对象的构造函数,如
new RegExp('ab+c')
提供了正则表达式运行时编译(runtime compilation)。如果你知道正则表达式模式为变量,如用户输入,这些情况都可以使用构造函数。
regexp.test和regexp.exec
regexp.test(str)返回Bool
regexp.exec(str)返回匹配的子串 或者 null
常用修饰符
- i ignoreCase 执行对大小写不敏感的匹配。
- g global 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
- y sticky 粘性匹配 从源字符串的RegExp.prototype.lastIndex位置开始匹配,
lastIndex
只有 "g
" 或"y
" 标志时,lastIndex才会起作用。
y
:下一次匹配一定在 lastIndex
位置开始;
g
:下一次匹配可能在 lastIndex
位置开始,也可能在这个位置的后面开始。
lastIndex
>str.length,则匹配失败,
匹配失败,则 lastIndex
被设置为 0。
let str = '#foo#'
let regex = /foo/y
regex.lastIndex = 1
regex.test(str) // true
regex.lastIndex = 5
regex.test(str) // false (lastIndex is taken into account with sticky flag)
regex.lastIndex // 0 (reset after match failure)
分组
‘(
正则表达式)’
每一个分组都是一个子表达式
回溯引用
(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。
回溯引用的语法像\1
,\2
,....,其中\1
表示引用的第一个子表达式,\2
表示引用的第二个子表达式,以此类推。而\0
则表示整个表达式。
匹配两个连续相同的单词:\b(\w+)\s\1
Hello what what is the first thing, and I am am scq000.
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1
,$2
...来引用要被替换的字符
var str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g');
// 得到结果 'abg abg 123'
匹配
选择匹配:(子模式)|(子模式)
多重选择模式:在多个子模式之间加入选择操作符。
为了避免歧义:(子模式)。
var r = /(abc)|(efg)|(123)|(456)/;
惰性匹配:最小化匹配
重复类量词都具有贪婪性,在条件允许的前提下,会匹配尽可能多的字符。
- ?、{n} 和 {n,m} 重复类具有弱贪婪性,表现为贪婪的有限性。
- *、+ 和 {n,} 重复类具有强贪婪性,表现为贪婪的无限性。
越左的重复类量词优先级越高,会在保证右侧重复类量词最低匹配次数基础上,使最左侧的重复类量词尽可能占有所有字符。
var s = "<html><head><title></title></head><body></body></html>";
var r = /(<.*>)(<.*>)/
var a = s.match(r);
console.log(a[0])//整个表达式匹配'<html><head><title></title></head><body></body></html>'
console.log(a[1]);//左侧表达式匹配"<html><head><title></title></head><body></body></html>"
console.log(a[2]);//右侧表达式匹配“</html>”
定义:在满足条件的前提下,尽可能少的匹配字符。
方法:在重复类量词后面添加问号?限制词。
贪婪匹配体现了最大化匹配原则,惰性匹配则体现最小化匹配原则。
var s = "<html><p><title></title></head><body></body></html>";
var r = /<.*?>/
var a = s.match(r); //返回单个元素数组["<html>"]而不是最短的<p>
针对 6 种重复类惰性匹配的简单描述如下:
- {n,m}?:尽量匹配 n 次,但是为了满足限定条件也可能最多重复 m 次。
- {n}?:尽量匹配 n 次。
- {n,}?:尽量匹配 n 次,但是为了满足限定条件也可能匹配任意次。
- ??:尽量匹配,但是为了满足限定条件也可能最多匹配 1 次,相当于 {0,1}?。
- +?:尽量匹配 1 次,但是为了满足限定条件也可能匹配任意次,相当于 {1,}?。
- *? :尽量不匹配,但是为了满足限定条件也可能匹配任意次,相当于 {0,}?。
前/后向查找:匹配括号中的内容(不包含括号)
包括括号:\[\S+?\]
不包括括号:(?<=\[)\S+?(?=\])
后向查找:(?<=exp)是以exp开头的字符串, 但不包含本身.
负后向查找:(?<!exp) ,被指定的子表达式不能被匹配到。
前向查找:(?=exp)就匹配为exp结尾的字符串, 但不包含本身.
负前向查找::(?!exp),被指定的子表达式不能被匹配到。
\S 匹配任何非空白字符。等价于[^\f\n\r\t\v]。
如果不支持后向查找:将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来
技巧
反义字符
可以匹配很多无法直接描述的字符,达到以少应多的目的。
var r = /[^0123456789]/g;
边界量词
边界就是确定匹配模式的位置,如字符串的头部或尾部,具体说明如表所示。
量词 | 说明 |
---|---|
^ | 匹配开头,在多行检测中,会匹配一行的开头 |
$ | 匹配结尾,在多行检测中,会匹配一行的结尾 |
- var s = "how are you"
1) 匹配最后一个单词
var r = /\w+$/;
var a = s.match(r); //返回数组["you"]
2) 匹配第一个单词
var r = /^\w+/;
var a = s.match(r); //返回数组["how"]
3) 匹配每一个单词
var r = /\w+/g;
var a = s.match(r); //返回数组["how","are","you"]
应用
str.split()
//对于不同的平台(Unix,Windows 等等),其默认的行结束符是不一样的。而下面的划分方式适用于所有平台。
let text = 'Some text\nAnd some more\r\nAnd yet\rThis is the end'
let lines = text.split(/\r\n|\r|\n/)
console.log(lines) // logs [ 'Some text', 'And some more', 'And yet', 'This is the end' ]
str.match()
在字符范围内可以混用各种字符模式。
var s = "abcdez"; //字符串直接量
var r = /[abce-z]/g; //字符a、b、c,以及从e~z之间的任意字符
var a = s.match(r); //返回数组["a","b","c","e","z"]
str.match(regexp)//找到一个或多个正则表达式的匹配。
const paragraph = 'The quick brown fox jumps over the lazy dog. It barked.';
let regex = /[A-Z]/g;
let found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T", "I"]
regex = /[A-Z]/;
found = paragraph.match(regex);
console.log(found);
// Expected output: Array ["T"]
//match类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
var str = '123123000'
str.match(/\w{3}/g).join(',') // 123,123,000
str.replace()
let re = /(\w+)\s(\w+)/;//匹配姓名 first last 输出新的格式 last, first。
let str = "John Smith";
let newstr = str.replace(re, "$2, $1");//$1 和 $2 指明括号里先前的匹配
console.log(newstr);//"Smith, John".
str.replace(regexp|substr, newSubStr|function)//返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。
const p = 'lazy dog.Dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replace('dog', 'monkey'));
// "lazy monkey.Dog lazy"
let regex = /dog/i;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replace(regex, 'ferret'));
//"lazy ferret.Dog lazy"
regex = /d|Dog/g;
console.log(p.replace(regex, 'ferret'));
//"lazy ferretog.ferret lazy"
//当使用一个 regex 时,您必须设置全局(“g”)标志, 否则,它将引发 TypeError:“必须使用全局 RegExp 调用 replaceAll”。
const p = 'lazy dog.dog lazy';//如果pattern是字符串,则仅替换第一个匹配项。
console.log(p.replaceAll('dog', 'monkey'));
// "lazy monkey.monkey lazy"
let regex = /dog/g;//如果非全局匹配,则仅替换第一个匹配项
console.log(p.replaceAll(regex, 'ferret'));
//"lazy ferret.ferret lazy"
设计对提交的表单字符串进行敏感词过滤。先设计一个敏感词列表,然后使用竖线把它们连接在一起,定义选择匹配模式,最后使用字符串的 replace() 方法把所有敏感字符替换为可以显示的编码格式。
var s = '<meta charset="utf-8">'; //待过滤的表单提交信息
var r = /\'|\"|\<|\>/gi; //过滤敏感字符的正则表达式
function f() { //替换函数
把敏感字符替换为对应的网页显示的编码格式
return "&#" + arguments[0].charCodeAt(0) + ";";
}
var a =s.replace(r,f); //执行过滤替换
document.write(a); //在网页中显示正常的字符信息
console.log(a);
str.serach()
str.search(regexp)//如果匹配成功,则 search() 返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1
const paragraph = '? The quick';
// Any character that is not a word character or whitespace
const regex = /[^\w\s]/g;
console.log(paragraph.search(regex));
// Expected output: 0
合法的URL
URL结构一般包括协议、主机名、主机端口、路径、请求信息、哈希
- 首先必须是以http(s)开头并且可以不包含协议头部信息
- 主机名可以使用"-"符号,所以两种情况都要判断,包含"-"或不包含"-"
- 顶级域名很多,直接判断"."之后是否为字母即可
- 最后判断端口、路径和哈希,这些参数可有可无
域名中只能包含以下字符
1. 26个英文字母
2. "0,1,2,3,4,5,6,7,8,9"十个数字
3. "-"(英文中的连词号,但不能是第一个字符)
https://www.bilibili.com/video/BV1F54y1N74E/?spm_id_from=333.337.search-card.all.click&vd_source=6fd32175adc98c97cd87300d3aed81ea
//开始: ^
//协议: http(s)?:\/\/
//域名: [A-z0-9]+-[A-z0-9]+|[A-z0-9]+
//顶级域名 如com cn,2-6位: [a-zA-Z]{2,6}
//端口 数字: (\d+)?
//路径 任意字符 如 /login: (\/.+)?
//哈希 ? 和 # ,如?age=1: (\?.+)?(#.+)?
//结束: $
// https:// www.bilibili com /video/BV1F54y1N74E ?spm..
/^(http(s)?:\/\/)?(([a-zA-Z0-9]+-[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)+([a-zA-Z]{2,6})(:\d+)?(\/.+)?(\?.+)?(#.+)?$/.test(url)
常用字符
\标记下一个字符是特殊字符或文字。例如,"n”和字符"n”匹配。"\n"则和换行字符匹配。
^匹配输入的开头.
$匹配输入的末尾
·匹配除换行字符外的任何单个字符
*匹配前一个字符零或多次。例如,"zo*”与"z”或"zoo”匹配。
+匹配前一个字符一次或多次。例如,"zo+"与"zoo”匹配,但和"z”不匹配。
?匹配前一个字符零或一次。例如,"a?ve?”和"never"中的“"ve”匹配。
x|y 匹配x或y
{n}匹配n次。n是非负整数
{n,} n是一个非负整数。至少匹配n次。例如,"o{2,)"和"Bob”中的"o”不匹配,但和"foooood"中的所有o匹配。"o{1}”与"o+”等效。"o{0,}”和"o*”等效。
{n,m}m和n是非负整数。至少匹配n次而至多匹配 m次。例如,"o{1,3]"和"fooooood”中的前三个o匹配。"o{0,1}”和“o?”等效。
[xyz]匹配括号内的任一字符。例如,"[abc]"和"plain”中的"a”匹配。
[^xyz]匹配非括号内的任何字符。例如,"[^abc]"和“plain”中的"p”匹配。
[a-z]字符范围。和指定范围内的任一字符匹配。例如,"[a-z]”匹配"a"到"z"范围内的任一小写的字母表字符。
[^m-z]否定字符范围。匹配不在指定范围内的任何字符。例如,"[m-z]”匹配不在"m"到"z"范围内的任何字符。
助记:digital
\d匹配数字字符。等价于[0-9]。
\D匹配非数字字符。等价于[^0-9]。
助记:space
\s匹配任何空白,包括空格、制表、换页等。与"[ \fn\rlt\v]”等效。
\S匹配任何非空白字符。与"[^ \fn\rlt\v]”等效。
\w匹配包括下划线在内的任何字字符。与"[A-Za-z0-9_]”等效。
\W匹配任何非字字符。与"[^A-Za-z0-9_]”等效。
元字符表
元字符 | 描述 |
---|---|
. | 查找单个字符,除了换行和行结束符 |
\w | 查找单词字符 |
\W | 查找非单词字符 |
\d | 查找数字 |
\D | 查找非数字字符 |
\s | 查找空白字符 |
\S | 查找非空白字符 |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
\0 | 查找 NUL字符 |
\n | 查找换行符 |
\f | 查找换页符 |
\r | 查找回车符 |
\t | 查找制表符 |
\v | 查找垂直制表符 |
\xxx | 查找以八进制数 xxxx 规定的字符 |
\xdd | 查找以十六进制数 dd 规定的字符 |
\uxxxx | 查找以十六进制 xxxx规定的 Unicode 字符 |
[A-z]和[a-zA-Z]
[A-z]
将在范围匹配的ASCII字符从A
到z
,
[a-zA-Z]
将在范围中的范围匹配的ASCII字符从A
到Z
和从a
到z
。
查看ASCII字符的this table,则会看到A-z
包含[
,\
,]
,^
,_,
```
规范
*命名规范
建议养成每句后加;的好习惯
- Pascal Case 大驼峰式命名法:首字母大写。eg:StudentInfo、UserInfo、ProductInfo
- Camel Case 小驼峰式命名法:首字母小写。eg:studentInfo、userInfo、productInfo
常量
命名方法:名词全部大写
命名规范:使用大写字母和下划线来组合命名,下划线用来分割单词
const MAX_COUNT = 10;
变量,函数
命名方法: 小驼峰式命名法
命名规范:前缀为形容词(变量) ,前缀为动词(函数)
let maxCount = 10;
/**
*
* @param n int整型 the n
* @return int整型
*/
function setConut(n){
this.count=n;
return n
}
类
类 & 构造函数
命名方法:大驼峰式命名法,首字母大写。
命名规范:前缀为名称。
- 公共属性和方法:跟变量和函数的命名一样。
- 私有属性和方法:前缀为_(下划线),后面跟公共属性和方法一样的命名方式。
class Person {
private _name: string;
constructor() { }
// 公共方法
getName() {
return this._name;
}
// 公共方法
setName(name) {
this._name = name;
}
}
const person = new Person();
person.setName('mervyn');
person.getName(); // ->mervyn
*注释
HTML
<!-- 注释 -->
CSS
p{
color: #ff7000; /*字体颜色设置*/
height:30px; /*段落高度设置*/
}
/*注释*/
JS
//注释
/*注释*/
项目为二面三面,面试官基本就是照着简历里面的项目技术点切入然后深入展开。
为了简洁,相关文章参考链接在标题里
目录
模块化规范
一个模块是实现一个特定功能的一组方法。
- 由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
- 后面提出了对象,通过将函数作为一个对象的方法来实现,但是这种办法会暴露所 有的所有的模块成员,外部代码可以修改内部属性的值。
- 现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
- ES6 :使用 import 和 export 的形式来导入导出模块。
- CommonJS : require 来引入模块,通过 module.exports 定义模块的输出接口。
这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。
但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
- AMD :Asynchronous Module Definition,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
- CMD :Common Module Definition,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。
AMD是预加载,CMD是懒加载。
AMD是提前执行,CMD是延迟执行。
AMD在对应的加载之前导入,CMD在用的时候导入
懒加载(性能优化)
(Load On Demand)延迟加载、按需加载
可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
scroll版
- 图片的 src 属性设为默认图片
- 图片的真实路径则设置在data-src属性中,
- 绑定 window 的
scroll
事件,对其进行事件监听。 -
//节流 window.addEventListener('scroll', throttle(lazyload, 200))
- 在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,
- 如果图片在可视区内将图片的 src 属性设置为data-src的值
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lazyload</title>
<style>
.image-item {
display: block;
margin-bottom: 50px;
height: 200px;//一定记得设置图片高度
}
</style>
</head>
<body>
<img src="./img/default.png" data-src="./img/1.jpg" />
...
<img src="./img/default.png" data-src="./img/10.jpg" />
<script>
function lazyload() {
let viewHeight = document.body.clientHeight //获取可视区高度
//用属性选择器返回属性名为data-src的img元素列表
let imgs = document.querySelectorAll('img[data-src]')
imgs.forEach((item, index) => {
if (item.dataset.src === '') return
// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
let rect = item.getBoundingClientRect()
if (rect.bottom >= 0 && rect.top < viewHeight) {
item.src = item.dataset.src
item.removeAttribute('data-src')//移除属性,下次不再遍历
}
})
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>
IntersectionObserver
版
IntersectionObserver
"交叉观察器"。可见(visible)本质是,目标元素与视口产生一个交叉区
callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)
//callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)
var io = new IntersectionObserver(callback, option)
// 开始观察
io.observe(document.getElementById('example'))
// 停止观察
io.unobserve(element)
// 关闭观察器
io.disconnect()
callback 函数的参数(entries)
是一个数组,每个成员都是一个 IntersectionObserverEntry
对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
- time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
- target:被观察的目标元素,是一个 DOM 节点对象
- isIntersecting: 目标是否可见
- rootBounds:根元素的矩形区域的信息,
getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null - boundingClientRect:目标元素的矩形区域的信息
- intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
- intersectionRatio:目标元素的可见比例,即
intersectionRect
占boundingClientRect
的比例,完全可见时为 1,完全不可见时小于等于 0
const config = {
rootMargin: '0px',
threshold: 0,
}
let observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let img = entry.target
let src = img.dataset.src
if (src) {
img.src = src
img.removeAttribute('data-src')
}
// 解除观察
self.unobserve(entry.target)
}
})
}, config)
imgs.forEach((image) => {
observer.observe(image)
})
require与import的区别和使用
require/import
// CommonJS 的写法
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;
// ES6 的写法
import { func1, func2 } from 'moduleA';
module.exports/export
// commonJS 的写法
var React = require('react');
var Breadcrumbs = React.createClass({
render() {
return <nav />;
}
});
module.exports = Breadcrumbs;
// ES6 的写法
import React from 'react';
class Breadcrumbs extends React.Component {
render() {
return <nav />;
}
};
export default Breadcrumbs;
- 规范:require是CommonJS,AMD规范的模块化语法,import是ECMAScript 6规范的模块化语法,如果要兼容浏览器的话必须转化成es5的语法;CommonJS模块默认export的是一个对象,即使导出的是基础数据类型。
- 本质:require是赋值过程,其实require 的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量,引入复杂数据类型时,数据浅拷贝该对象。。import是解构过程。
- 加载:require是运行时加载,import是编译时加载;
- 位置:require可以写在代码的任意位置,import只能写在文件的最顶端且不可在条件语句或函数作用域中使用;
- 改变:require通过module.exports导出的值就不能再变,import通过export导出的值可以改变;
js的运行环境
脚本语言需要一个解析器才能运行,每一种解析器都是一个运行环境
JavaScript是脚本语言,在不同的位置有不一样的解析器,
浏览器
写入html的js语言,浏览器是它的解析器角色。
浏览器中的js的用途是操作DOM,浏览器就提供了document之类的内置对象。
Node
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,独立于浏览器的运行环境
nodejs中的js的用途是操作磁盘文件或搭建http服务器,nodejs就相应提供了fs,http等内置对象。
特点
- 简单:javascript,json进行编码,
- 强大:非阻塞IO,可以适应分块传输数据,较慢的网络环境,尤其擅长高并发访问,
- 轻量:node本身既是代码,又是服务器,前后端使用统一语言;
- 可扩展体:轻松应对多实例,多服务器架构,同时有海量的第三方应用组件。
npm
Node Package Manager ,即:node包管理器
是nodeJS的一个程序包管理和分发的管理工具,npm完全用 JavaScript 写成
允许用户从NPM服务器下载安装上传包
项目规范
整个项目不再使用class组件,统一使用函数式组件,并且全面用Hooks;
所有的函数式组件,为了避免不必要的渲染,全部使用memo进行包裹;
组件内部的状态,使用useState、业务数据全部放在redux中管理;
函数组件内部逻辑代码基本按照如下顺序编写代码:
组件内部state管理;
redux的hooks代码;
其他hooks相关代码(比如自定义hooks);
其他逻辑代码;
返回JSX代码;
redux代码规范如下:
redux目前我们学习了两种模式, 根据自己的情况选择普通模式还是rtk模式
每个模块有自己独立的reducer或者slice,之后合并在一起;
redux中会存在共享的状态、从服务器获取到的数据状态;
网络请求采用axios
对axios进行二次封装;
所有的模块请求会放到一个请求文件中单独管理;
项目使用AntDesign或者MUI(Material UI)
其他规范在项目中根据实际情况决定和编写
命令(创建运行)
npm run start 来运行启动项目并打开页面
npx create-react-app my-app //创建项目
cd my-app
npm start
安装命令:
npx create-react-app 项目名称 --template typescript
项目文件结构
主要开发代码在src目录下。App.js为根组件,index.js为入口模块,index.css为全局样式文件。
package.json
配置jsconfig.json:这个文件在Vue通过脚手架创建项目时自动生成, React是没有自动生成
package.json相当于说明书,其重点字段:
{
"name": "react-ts-app",
"version": "0.1.0",
"private": true,//是否私有,设置为 true 时,npm 拒绝发布
"dependencies": {//生产环境下,项目运行所需依赖
"@ant-design/icons": "^4.8.0",
...
"@types/node": "^16.18.6",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"antd": "^5.0.4",
"axios": "^1.2.1",
"classnames": "^2.3.2",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.4",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"sass": "^1.56.1",
"typescript": "^4.9.3",
"web-vitals": "^2.1.4"
},
"scripts": {//执行npm脚本命令简写,比如“start" : "react-scripts start",执行npm start就是运行“react-scripts start"
"start": "SET PORT=8080 && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
....
"devDependencies": {//开发环境下,项目所需依赖
"@types/lodash": "^4.14.191"
}
}
package-lock.json
用一句话来概括很简单,就是锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致。
node_modules
是安装node后用来存放用包管理工具下载安装的包的文件夹。比如webpack、gulp、grunt这些工具。
git代码管理
常用命令
git init 初始化git仓库
git status 查看文件状态
git add 文件列表 追踪文件
git commit -m 提交信息 向仓库中提交代码
git log 查看提交记录
分支
1.分支明细
(1)主分支(master):第一次向 git 仓库中提交更新记录时自动产生的一个分支。
(2)开发分支(develop):作为开发的分支,基于 master 分支创建。
(3)功能分支(feature):作为开发具体功能的分支,基于开发分支创建
2.分支命令
(1)git branch 查看分支
(2)git branch 分支名称 创建分支
(3)git checkout 分支名称 切换分支
(4)git merge 来源分支 合并分支 (备注:必须在master分支上才能合并develop分支)
(5)git branch -d 分支名称 删除分支(分支被合并后才允许删除)(-D 强制删除)
git多人协同merge冲突
是当前修改是左箭头方向,传入的是右箭头的方向,
中间用等于号分割,等号上边是当前修改(本地),下边是传入的修改(线上的代码)。
两人同时提交可能会出现冲突,解决办法是手动修改冲突
暂时保存更改
存储临时改动:git stash(藏匿)
恢复改动:git stash pop
- 应用场景:
对于多人并行开发,维护同一仓库工作场景,经常会出现文件合并冲突的情况
- 作用:
能够将所有未提交的修改(工作区和暂存区)保存至堆栈中,用于后续恢复当前工作目录。
git add
只是把文件加到 git 版本控制里
性能优化
压缩js,css,js分包,优化图片(webp),开启gzip(后端开启),配置缓存(强制缓存协商缓存),使用cdn,
webpack分包,减少重绘回流
webpack打包管理
它将根据模块的依赖关系进行静态分析,然后将这些模块( js、css、less )按照指定的规则生成对应的静态资源,减少了页面的请求。Webpack是以公共JS的形式来书写脚本的,方便旧项目进行代码迁移。
原理
Webpack通过一个给定的主文件(如:index.js)开始找到项目的所有依赖文件,
使用loaders处理它们,plugin可以压缩代码和图片,
把所有依赖打包成一个 或多个bundle.js文件(捆bundle)浏览器可识别的JavaScript文件。
Babel
JavaScript 编译器
将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法,即浏览器兼容的语法,比如将箭头函数转换为普通函数
将jsx转换成浏览器认的js
loader
webpack只认识JS和JSON,所以Loader相当于翻译官,将其他类型资源进行预处理,最终变为js代码。
- less-loader:将less文件编译成css文件(开发中,会使用less预处理器编写css样式,使开发效率提高)
- css-loader:将css文件变成commonjs模块(模块化的规范)加载到js中,模块内容是样式字符串
- style-loader:创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效
- ts-loader:打包编译Typescript文件
plugin
Plugin解决loader 无法实现的事情,比如打包优化和代码压缩等。
- html-webpack-plugin 处理html资源,默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)
- mini-css-extract-plugin 打包过后的css在js文件里,该插件可以把css单独抽出来
- clean-webpack-plugin 每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除
loader和plugin的区别
运行时机
1.loader运行在编译阶段
2.plugins 在整个周期都起作用
热加载原理
浏览器热更新:开发时能够在浏览器页面中实时看到我们代码的变化产生效果的流程。
热加载是通过内置的 HotModuleReplacementPlugin 实现的
- 构建 bundle 的时候,监听文件变化。
- 文件修改会触发 webpack 重新构建,
- 服务器通过向浏览器发送更新消息,
- 浏览器通过 jsonp 拉取更新的模块文件,
- jsonp 回调触发模块热替换逻辑。
TS4 加分项
一般面试官不会考察,但是你可以写在简历项目或者技能中,面试时主动提出使用了ts,解释下为什么要使用ts及ts与js,java(类成员可见性,多的类型(any,void,泛型,断言等))的区别,然后引导面试官提问,回答完其问题再自己拓展一些相关,是不错的加分项。
但既然是加分项,一般不会问太深,毕竟还有更关键的要问你,所以会试探一下你是否真的用过,还是纯背。
面试官一般是看你简历问的,没有问的才会问八股文之类。
面试一定要把握主动权,不要干等着面试官问,尽量展示自己所会的,并且回答不必太急,最后能条理清晰。
区别
为什么推荐使用TypeScript?
- JS的超集,扩展新功能
- 强制类型,防止报错
- 自动类型推断,保证变量的类型稳定
// let a: string -> 类型推断
let a = 'hello'
a = 123; // error
- 强大的类型系统,包括泛型。
- 支持静态类型。(主要改进)更有利于构建大型应用
TypeScript和JavaScript的区别
- TypeScript是JavaScript 的超集,可以被编译成JS。用JS编写的代码,在TS中依然有效。
- TypeScript通过TypeScript编译器或Babel转译为JavaScript代码
- TypeScript 是纯面向对象的编程语言,包含类和接口的概念。
- JavaScript弱类型语言,动态类型
类型强弱是针对类型转换是否显示来区分,静态和动态类型是针对类型检查的时机来区分。
- 在JS中只有变量声明空间,比如
let a = 123
。但在TS中还存在类型声明空间,可通过类型注解结合这两个空间
可以通过type
关键字来定义类型声明空间,type
在TS中叫做类型别名。为了能够更好的区分两个空间,所以人为规定类型空间定义的名字首字母要大写,例如:type A = number
控制类成员可见性的访问关键字有哪些?
- 公共(public),类的所有成员,其子类以及该类的实例都可以访问。
- 受保护(protected),该类及其子类都可以访问它们。 但是该类的实例无法访问。
- 私有(private),只有类的成员可以访问它们。
TS与java不同,TS默认隐式公共的,TS符合 JS 的便利性,java默认private,符合安全性
接口interface与类型别名type
接口是一系列抽象方法的声明,是一些方法特征的集合。
接口跟类型别名类似都是用来定义类型注解的,但接口只能操作对象,且有继承,合并
interface A {
username: string;
age: number;
}
let a: A = {
username: 'xiaoming',
age: 20
}
接口可以进行合并操作。
interface A {
username: string;
}
interface A {
age: number;
}
let a: A = {
username: 'xiaoming',
age: 20
}
接口具备继承能力。
interface A {
username: string
}
interface B extends A {
age: number
}
let b: B = {
username: 'xiaoming',
age: 20
}
类型
never,any,unknown
never类型表示永不存在的值的类型,当一个值不存在的时候就会被自动类型推断成never类型。
在出问题的时候会被自动转成never。
any类型表示任意类型,而unknown类型表示为未知类型,是any类型对应的安全类型。
类型断言as与非空断言!
当 TypeScript 推断出来类型并不满足需求,需要手动指定一个类型,强制说明类型,让TS编译不报错
let a: unknown = 'hello';
a = 123;
(a as []).map(()=>{}) // success
因为b可能是字符串也可能是undefined,所以b.length
的时候就会报错,这样我们可以采用非空断言来告诉TS,这个b肯定不是undefined,所以b只能是字符串,那么b.length
就不会报错了。
let b: string|undefined = undefined;
b!.length // success
函数类型和void
function foo(n: number, m?: string): number{
return 123;
}
foo(123, 'hello');
foo(123); // success
当函数没有return和return undefined的时候返回void类型。
let foo = function(){ // void
}let foo = function(): undefined{ // undefined 不能不写return的
} // error
泛型
泛型是指在定义函数、接口、类时,未指定其参数类型,只有在运行时传入才能确定。泛型简单来说就是对类型进行传参处理。
应用
变量
*类型注解
类型注解:把变量空间与类型空间结合在一起
type关键字+类型别名
let a: string = 'hello'
//也可以通过类型别名进行连接
type A = string
let a: A = 'hello'
数组
let arr: Array<number> = [1, 2, 3];
//自定义MyArray实现
type MyArray<T> = T[];
let arr2: MyArray<number> = [1, 2, 3];
函数
function foo(n: number, m?: string): number{
return 123;
}
foo(123, 'hello');
foo(123); // success
function foo<T>(n: T){
}
foo<string>('hello');
foo(123); // 泛型会自动类型推断
类
class Foo {
//第一种写法
//username: string = 'xiaoming';
//第二种写法
// username: string;
// constructor(){
// this.username = 'xiaoming';
// }
//第三种写法
username: string;
constructor(username: string){
this.username = username;
}
}
interface A {
username: string
age: number
showName(n: string): string
}
class Foo implements A {
username: string = 'xiaoming'
age: number = 20
gender: string = 'male'
showName = (n: string): string => {
return n
}
}
class Foo {
...
showAge = (n: number): number => {
return n;
}
}
class Foo<T> {
username!: T; //不给初始值可通过非空断言
}
let f = new Foo<string>();
f.username = 'hello';
class Foo<T> {
username!: T
}
class Baz extends Foo<string> {}
let f = new Baz()
f.username = 'hello'
类型判断
is
is
让 ts 分辨类型
function toUpperCase(x: unknown) {
if(isString(x)) {
x.toUpperCase(); // ⚡️ x is still of type unknown
}
}
-
但是由于这个检验函数(isString)被包裹在 toUpperCase()函数中,ts 在判断的时候还是抛出了错误提示
-
使用
is
,这里让我们主动明确的告诉 ts ,在 isString() 这个函数的参数是一个 string。
// !!! 使用 is 来确认参数 s 是一个 string 类型
function isString(s): s is string {
return typeof s === 'string';
}
Narrowing down sets
function pipsAreValid(pips: number) {
// we check for every discrete value, as number can
// be something between 1 and 2 as well.
return pips === 1 || pips === 2 || pips === 3 ||
pips === 4 || pips === 5 || pips === 6;
}
function evalThrow(count: number) {
if (pipsAreValid(count)) {
// my types are lying 😢
switch (count) {
case 1:
case 2:
case 3:
case 4:
case 5:
console.log('Not today');
break;
case 6:
console.log('Won!');
break;
case 7:
// TypeScript does not complain here, even though
// it's impossible for count to be 7
console.log('This does not work!');
break;
}
}
}
count
是一个 number
类型,输入的参数是没有问题的。
在我们校验它的时候,count
不再是一个 number
类型,而是变成了一个 1-6 number 的特殊类型。
ts 为了防止类型溢出,使用了联合类型把原来 number
类型变成 1-6的数字(缩小了范围)
改变处
// 主动使用联合类型确保我们的输入是 1-6的数字
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
function pipsAreValid(pips: number): pips is Dice {
改变处
return pips === 1 || pips === 2 || pips === 3 ||
pips === 4 || pips === 5 || pips === 6;
}
function evalThrow(count: number) {
if (pipsAreValid(count)) {
// count is now of type Dice 😎
switch (count) {
case 1:
case 2:
case 3:
case 4:
case 5:
console.log('Not today');
break;
case 6:
console.log('Won!');
break;
case 7:
// TypeScript errors here. 7 is not in the union type of
// Dice
console.log('This does not work!');
break;
}
}
}
样式
Ant Design
Css、less、Sass (SCSS)
less, sass, scss都是css预处理语言(也是对应的文件后缀名)。
CSS 预处理器为 CSS 增加一些编程的特性,无需考虑浏览器的兼容性问题,
可以在 CSS 中使用变量、简单的逻辑程序、函数(如变量$main-color)等等在编程语言中的一些基本特性,可以让 CSS 更加简洁、适应性更强、可读性更佳,更易于代码的维护等诸多好处。
开发时用预处理语言,在打包上线时,用webpack再配合loader工具给转成css给浏览器使用。
Sass
在实际开发过程中,scss是常用写法,scss还是越直观越好,这种运算类型的特别是map类型的,尽量不要在实际项目中使用,后续维护成本很高的。
Sass 和 SCSS 其实是同一种东西,我们平时都称之为 Sass,
后缀扩展名:
- Sass :“.sass”,
- SCSS :“.scss”
语法书写方式:
- Sass 是以严格的缩进式语法规则来书写,不带大括号({})和分号(;),
- SCSS 的语法书写和我们的 CSS 语法书写方式非常类似
嵌套
选择器嵌套
#css
nav a {
color:red;
}
header nav a {
color:green;
}
#scss
nav {
a {
color: red;
header & {
color:green;
}
}
}
复制代码
属性嵌套
#css
.box {
border-top: 1px solid red;
border-bottom: 1px solid green;
}
#scss
.box {
border: {
top: 1px solid red;
bottom: 1px solid green;
}
}
伪类嵌套
.clearfix{
&:after {
clear:both;
overflow: hidden;
}
}
变量
全局变量
局部变量
在选择器、函数、混合宏内定义的变量
em {
$color: red;//定义局部变量
a {
color: $color;//调用局部变量
}
}
默认值
$btn-primary-color : #fff !default;
混入指令/混合指令/宏指令@mixin
Vue中叫它混入指令,可以设置参数,复用重复代码块。但会生成冗余的代码块。比如在不同的地方调用一个相同的混合宏时,不能将两个合成并集形式。
不带参数混合宏
@mixin border-radius{
border-radius: 5px;
}
带参数混合宏
# 带值参数
@mixin border-radius($radius:5px){
border-radius: $radius;
}
# 不带值参数
@mixin border-radius($radius){
border-radius: $radius;
}
# 带多个参数
@mixin center($width,$height){
width: $width;
height: $height;
margin-top: -($height) / 2;
margin-left: -($width) / 2;
}
带特别多参数混合宏
当混合宏传的参数过多之时,可以使用“…”来替代
# 带特别多参数
@mixin box-shadow($shadows...){
@if length($shadows) >= 1 {
-webkit-box-shadow: $shadows;
box-shadow: $shadows;
} @else {
$shadows: 0 0 2px rgba(#000,.25);
-webkit-box-shadow: $shadow;
box-shadow: $shadow;
}
}
调用混合宏@include
关键词“@include
”来调用声明好的混合宏
button {
@include border-radius;
}
.box {
@include border-radius(3px);
}
.box-center {
@include center(500px,300px);
}
.box {
@include box-shadow(0 0 1px rgba(#000,.5),0 0 2px rgba(#000,.2));
}
继承@extend
继承已存在的类样式块,从而实现代码的继承
.btn {
border: 1px solid #ccc;
padding: 6px 10px;
font-size: 14px;
}
.btn, .btn-primary, .btn-second {
border: 1px solid #ccc;
padding: 6px 10px;
font-size: 14px;
}
.btn-primary {
background-color: #f36;
color: #fff;
}
.btn-second {
background-clor: orange;
color: #fff;
}
写成继承的形式,而且编译出来的 CSS 会将选择器合并在一起,形成组合选择器.
.btn-primary {
background-color: #f36;
color: #fff;
@extend .btn;
}
.btn-second {
background-color: orange;
color: #fff;
@extend .btn;
}
占位符 %placeholder
%声明的代码,如果不被 @extend 调用的话,不会产生任何代码
%mt5 {
margin-top: 5px;
}
.btn {
@extend %mt5;
}
.block {
@extend %mt5;
}
通过 @extend 调用的占位符,编译出来的代码会将相同的代码合并在一起.
.btn, .block {
margin-top: 5px;
}
混合宏VS继承VS占位符
基础语法
插值#{}
(1)构建一个选择器
@mixin generate-sizes($class, $small, $medium, $big) {
.#{$class}-small { font-size: $small; }
.#{$class}-medium { font-size: $medium; }
.#{$class}-big { font-size: $big; }
}
@include generate-sizes("header-text", 12px, 20px, 40px);
(2) 属性变量
$properties: (margin, padding);
@mixin set-value($side, $value) {
@each $prop in $properties {
#{$prop}-#{$side}: $value;
}
}
.login-box {
@include set-value(top, 14px);
}
@mixin中插值不能作为赋值语句的值部分,只能用做属性名定义或者选择器构建时@include中不能使用插值
注释
1、类似 CSS 的注释方式,使用 ”/* ”开头,结属使用 ”*/ ”
2、类似 JS的注释方式,使用“//” 两者区别,
前者会在编译出来的 CSS 显示,后者在编译出来的 CSS 中不会显示
加减乘除连带单位一起计算
加减法
在变量或属性中都可以做加法运算,但对于携带不同类型的单位时,在 Sass 中计算会报错
.content {
width: $full-width - $sidebar-width;
}
字符串拼接
div {
cursor: e + -resize;
}
编译后
div {cursor: e-resize;}
乘法
当一个单位同时声明两个值时会有问题
只能有一个值带单位
(比如 em ,px , %)
# 编译的时候报“20px*px isn't a valid CSS value.”错误信息。
.box {
width:10px * 2px;
}
# 正确的写法
.box {
width: 10px * 2;
}
除法
如果数值或它的任意部分是存储在一个变量中或是函数的返回值。
• 如果数值被圆括号包围。
• 如果数值是另一个数学表达式的一部分
在除法运算时,如果两个值带有相同的单位值时,除法运算之后会得到一个不带单位的数值
.box {
width: (1000px / 100px);
}
编译后
.box {
width: 10;
}
@if, @else if
,@else
条件
@mixin blockOrHidden($boolean:true) {
@if $boolean {
display: block;
}
@else {
display: none;
}
}
.block {
@include blockOrHidden;
}
.hidden{
@include blockOrHidden(false);
}
@for
@for $i from <start> through <end>
@for $i from <start> to <end>
关键字 through
表示包括 end ,而 to
则不包括 end
@for $i from 1 through 3 {
.item-#{$i} { width: 2em * $i; }
}
@while循环
@while $types > 0 {
.while-#{$types} {
width: $type-width + $types;
}
$types: $types - 1;
}
@each循环
@each
循环就是去遍历一个列表,然后从列表中取出对应的值
@each $var in <list>
$list: adam john wynn mason kuroir;
@mixin author-images {
@each $author in $list {
.photo-#{$author} {
background: url("/images/avatars/#{$author}.png") no-repeat;
}
}
}
.author-bio {
@include author-images;
}
@import引入 SCSS 和 Sass
Sass 扩展了 CSS 的 @import 规则,让它能够引入 SCSS 和 Sass 文件。 所有引入的 SCSS 和 Sass 文件都会被合并并输出一个单一
瀑布流
描述:页面元素的宽度按照屏幕分辨率进行适配调整,但整体布局不变。主要特征是像瀑布一样往下流,有规律的无限遍历模块。(阿里、字节考过)
css
给图片添加样式让图片等宽并同行显示。
1、首先我们定义一个container容器来装所有图片,在这个容器中用box容器装box-img容器再装入每张图片,这样方便之后样式的编写。
2、使图片同行显示--给box容器使用float:left;属性。
3、让图片等宽显示--给box-img容器设置width:150px;,img标签设置width:100%;继承父容器box-img高度的100%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>waterFall</title>
<script src="./index.js"></script>
</head>
<style>
<!--清除浏览器自带样式-->
*{
margin: 0;
padding: 0;
}
<!--overflow: hidden触发BFC-->
.container {
overflow: hidden;
position: relative;
}
<!--float: left同行显示-->
.box{
float: left;
padding: 5px;
}
<!--图片等宽-->
.box-img{
width: 150px;
padding: 5px;
border: 1px solid #484848;
box-shadow: 0 0 5px #484848;
}
<!--图片占满父容器-->
.box-img img{
width: 100%;
}
</style>
<body>
<div id="container">
<div class="box">
<div class="box-img">
<img src="./img/1.jpg" alt="">
</div>
</div>
/*.......后面接39个box,此处省略*/
</div>
</body>
</html>
js
1、首先用window.οnlοad=function(){}来实现页面加载完毕后立即执行的功能
调用imgLocation('container','box')函数来呈现最终效果,
传入的实参是父容器'container',装图片的子容器'box'。
window.onload=function() {
imgLocation('container','box')
}
2、实现imgLocation()函数功能
1)首先我们得 获取所有要摆放的图片
,并将其存入一个数组
中
function imgLocation(parent,content){
//得到父容器
var cparent=document.getElementById(parent)
//cparent下的所有的第一层的子容器 box
var ccontent=getChildElement(cparent,content)//数组,40个div
}
//取到父容器中的某一层子容器
function getChildElement(parent,content){
var contentArr=[]
var allContent=parent.getElementsByTagName('*')//通过标签来选中得到一个数组
//遍历allContent把其中类名为content的容器都存到contentArr数组中
for(var i=0;i<allContent.length;i++){
if(allContent[i].className==content){ //当前这个容器的类名是否为content
contentArr.push(allContent[i])
}
}
return contentArr;
}
2)得到这个数组
后,找出从谁开始
是需要被摆放
位置的
首先获取视窗的宽度和每张图片的宽度,
将两者相除并向下取整可得到第一行可以放置图片的数量,
自然也就知道了我们需要操作的那张图片的序号。
//从谁开始是需要被摆放位置的
var winWidth=document.documentElement.clientWidth;//视窗宽度
var imgWidth=ccontent[0].offsetWidth;//图片宽度
var num=Math.floor(winWidth/imgWidth)//第一行能放几张图
3)得到需要被摆放
位置的图片序号后,确定其摆放位置
定义一个存储高度的数组,对前一行元素的高度进行遍历并存入数组,
当遍历到需要被摆放位置的图片时,
用Math.min()方法获取前一行高度最矮的元素高度,并用indexOf()方法获取到其下标。
再对我们所操作的这个图片容器的样式调整:
position:absolute;绝对定位,
top值设置为前一行高度最矮的图片高度minHeight,
left值设置为单张图片宽度乘这张图片的下标minIndex。
最后,摆放好图片后,还要更新摆放的那一列的高度
//操作num+1张图
var BoxHeightArr=[]
for(var i=0;i<ccontent.length;i++){
//前num张只要计算高度
if(i<num){
BoxHeightArr[i]=ccontent[i].offsetHeight
}
else{
//我们要操作的box :ccontent[i]
var minHeight=Math.min.apply(null,BoxHeightArr)//apply:把最小值这个方法借给它用
var minIndex=BoxHeightArr.indexOf(minHeight)//返回数组下标
ccontent[i].style.position='absolute'//style设置样式
ccontent[i].style.top=minHeight+'px'
ccontent[i].style.left=imgWidth*minIndex+'px'
//更新最矮的那一列的高度
BoxHeightArr[minIndex]+=ccontent[i].offsetHeight
}
}
共性
问项目中有哪些难点,怎么解决的
代码遇到冲突怎么办
业务系统的搭建过程
复盘过程
拆分组件
复用和业务逻辑拆分
如何实现自适应页面
说到媒体查询和多个样式表
React如何实现动画的(数字不断变大的动画)
最后是用库实现的
*个性
介绍
打卡考勤项目
实现功能
后端接口
- Navicat创建数据库和空表
- 加载MySQL等模块
- 创建MySQL连接池(设置服务器地址host,服务器端口号port) 和 服务器对象server
- 3306是MySQL的默认端口
- 默认的服务端口就是8080。
- 跨源资源共享CORS
-
Origin
字段用来说名本次请求来自哪个源,服务器根据这个值,决定是否同意这次请求。如果
Origin
指定的源不在允许范围之内,服务器就会返回一个正常的HTTP
回应,然后浏览器发现头信息中没有包含Access-Control-Allow-Origin
字段,就知道出错啦,然后抛出错误
// 加载Express模块
const express = require('express');
// 加载MySQL模块
const mysql = require('mysql');
...
// 创建MySQL连接池
const pool = mysql.createPool({
host: '127.0.0.1', //MySQL服务器地址
port: 3306, //MySQL服务器端口号
user: 'root', //数据库用户的用户名
password: '', //数据库用户密码
database: 'reg_log', //数据库名称
connectionLimit: 20, //最大连接数
charset: 'utf8' //数据库服务器的编码方式
});
// 创建服务器对象
const server = express();
// 加载CORS模块
const cors = require('cors');
// 使用CORS中间件
server.use(cors({
origin: ['http://localhost:8080', 'http://127.0.0.1:8080']
}));
登陆注册
*0.0.0.0,localhost,127.0.0.1
网络需求 | 数据传输 | 访问 | 性质 | |
---|---|---|---|---|
localhost | 不联网 | 不使用网卡,不受防火墙和网卡限制 | 本机访问 | 域名,默认是指向 127.0.0.1 |
127.0.0.1 | 不联网 | 网卡传输,受防火墙和网卡限制 | 本机访问 | 环回地址 |
本机IP | 联网 | 网卡传输 ,受防火墙和网卡限制 | 本机or外部访问 | 本机对外放开访问的 IP 地址 |
0.0.0.0
- 在服务器中:0.0.0.0表示本机上的任意ip地址,
- 在路由中:0.0.0.0表示的是默认路由,即当路由表中没有找到完全匹配的路由的时候所对应的路由。表示”任意IPV4主机”。
- 当一台主机还没有被分配一个IP地址的时候,用于表示主机本身。
localhost
localhost是个域名,而不是一个ip地址。可修改。
用于指代 this computer 或者 this host,可以用它来获取运行在本机上的网络服务。
在大多数系统中,localhost被指向了 IPV4 的 127.0.0.1 和 IPV6 的 ::1,这就是把localhost与127.0.0.1等同的原因。
127.0.0.1
本机地址,主要用于测试
(127.x.x.x)是本机回送地址(Loopback Address),即主机IP堆栈内部的IP地址,主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回,不进行任何网络传输。
示例都使用端口3000作为HTTP服务器的默认监听端口。
3000 是整数。 0 ~ 1023 (常用端口)49152 ~ 65535 号端口是动态端口
//用户注册接口
server.post('/register', (req, res) => {
//console.log(md5('12345678'));
// 获取用户名和密码信息
let username = req.body.username;
let password = req.body.password;
//以username为条件进行查找操作,以保证用户名的唯一性
let sql = 'SELECT COUNT(id) AS count FROM reg_log WHERE username=?';
pool.query(sql, [username], (error, results) => {
if (error) throw error;
let count = results[0].count;
if (count == 0) {
// 将用户的相关信息插入到数据表
sql = 'INSERT reg_log(username,password) VALUES(?,MD5(?))';
pool.query(sql, [username, password], (error, results) => {
if (error) throw error;
res.send({
message: 'ok',
code: 200
});
})
} else {
res.send({
message: 'user exists',
code: 201
});
}
});
});
// 用户登录接口
server.post('/login', (req, res) => {
//获取用户名和密码信息
let username = req.body.username;
let password = req.body.password;
// SQL语句
let sql = 'SELECT id,username FROM reg_log WHERE username=? AND password=MD5(?)';
pool.query(sql, [username, password], (error, results) => {
if (error) throw error;
if (results.length == 0) { //登录失败
res.send({
message: 'login failed',
code: 201
});
} else { //登录成功
res.send({
message: 'ok',
code: 200,
result: results[0]
});
}
});
// 指定服务器对象监听的端口号
server.listen(3000, () => {
console.log('server is running...');
})
注册页,使用token,能避免CSRF攻击Cross-site request forgery。JSON Web Token(JWT)
//注册
checkForm() {
// 点击注册按钮后调用此方法,验证用户名、密码、二次密码是否均正确,正确则发送axios请求
if (this.checkName() && this.checkPwd() && this.checkrePwd()) {
console.log(`验证成功,执行注册业务......`);
// 发送注册(post)请求
this.axios
.post("/register", `username=${this.name}&password=${this.pwd}`)
.then((result) => {
console.log(result);
if (result.data.code == 200) {
// 弹窗提示注册成功
...
// 注册成功后直接跳转到登录页
this.$router.push("/login");
} else if (result.data.code == 201) {
...
}
});
}
//登陆
checkForm() {
// 点击登录按钮后调用此方法,同时验证用户名和密码
if (this.checkName() && this.checkPwd()) {
// 发送登录(post)请求
this.axios
.post("/login", `username=${this.name}&password=${this.pwd}`)
.then((result) => {
console.log(result);
if (result.data.code == 200) {
// 弹窗提示登录成功
this.$toast({
message: `欢迎您 ${this.name}`,
position: "bottom",
duration: 3000,
});
...
} else {
this.$toast({
message: "登录失败,请检查您的用户名和密码",
position: "bottom",
duration: 3000,
});
}
});
}
分页
currentPage(当前页码)、total(总条数)、pageSize(每页展示的数据量)
把所有的数据请求回后,通过arr.slice(开始索引,结束索引)来进行截取每一页的数据;
假设当前页是currentPage = 1,pageSize = 5,那么应该从(currentPage-1)*pageSize开始截取,到currentPage*pageSize结束
弹窗组件modal
.modal {
display: none; /* 默认隐藏 */
position: fixed; /* 固定定位 */
z-index: 1; /* 设置在顶层 */
...
overflow: auto;
}
// 点击按钮打开弹窗
btn.onclick = function() {
modal.style.display = "block";
}
如何实现点击弹窗外面区域,弹窗消失
事件委托,往上遍历target的父元素如果有弹窗的id就不做反应,如果没有就触发消失
相关技术
主要目的是为了巩固基础知识,和适应大型项目的需求
- 框架,选择了React,说说其不同....,Router,Redux基本使用
- 保持TS限定类型的习惯,减少报错,说下区别和原因及其常用,
- 使用懒加载,组件库按需加载,优化性能,
- 使用ant-design做ui,学会利用API,省去设计样式的时间,实现样式和行为分离
- webpack打包
框架为一二面,面试官尤其喜欢问为什么要用+怎么用
为了简洁,相关文章参考链接在标题里
React18
React是用来构造用户界面的JS库
在React18中,需要使用两个文件来初始化框架:
-
react.development.js 或 react模块 -> 生成虚拟DOM
-
react-dom.development.js 或 react-dom/client模块 -> Diff算法 + 处理真实DOM
下面就是初始化React程序的代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../react.development.js"></script>
<script src="../react-dom.development.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// React对象 -> react.development.js(产生虚拟DOM)
// ReactDOM对象 -> react-dom.development.js(渲染成真实DOM)
//原生DOM获取
let app = document.querySelector('#app');
// root根对象,渲染react的DOM,React18
let root = ReactDOM.createRoot(app);
// React.createElement() -> 创建虚拟DOM
//createElement(标签,属性,内容)
let element = React.createElement('h2', {title: 'hi'}, 'hello world');
root.render(element);
</script>
</body>
</html>
特点
虚拟DOM,组件化设计模式,声明式代码,单向数据流,使用jsx描述信息
声明式编码
- 命令式编程:流程(每一步指令怎么去做)
- 声明式编码:目标,而非流程
因没有指令的概念,所以条件渲染和列表渲染都要通过命令式编程来实现(即JS本身的能力)
map()方法
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let data = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' }
];
let element = (
<ul>
{
data.map(v=><li key={v.id}>{v.text}</li>)
}
</ul>
);
root.render(element);
单向数据流 ⭐⭐⭐
数据主要从父节点传到子节点(通过props),如果父级的某个props改变了,React会重新渲染所有子节点
组件化⭐⭐⭐
可复用的代码可以抽成组件共同使用(UI,方法等)
每个UI块都是一个组件,每个组件都有一个状态。
虚拟DOM(Virtual Dom)(同Vue)⭐⭐⭐
虚拟 DOM,根据模板生成一个js对象(使用createElement,方法),取代真实的 DOM 。
当页面打开时浏览器会解析 HTML 元素,构建一颗 DOM 树,将状态全部保存起来
Vue和React框架都会自动控制DOM的更新,而直接操作真实DOM是非常耗性能的,所以才有了虚拟DOM的概念
React遵循可观察的模式,并监听状态变化。当组件的状态改变时,React更新虚拟DOM树。
缺点:首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢
Diff算法(同Vue)⭐⭐⭐
通过同层的树节点进行比较的高效算法,比较方式:diff整体策略为:深度优先,同层比较
总的来说就是减少DOM,重绘和回流
react生成的新虚拟DOM和旧虚拟DOM的比较规则:
- 如果旧的虚拟DOM中找到了与新虚拟DOM相同的key:
如果内容没有变化,就直接只用之前旧的真实DOM
如果内容发生了变化,就生成新的真实DOM
- 如果旧的虚拟DOM中没有找到与新虚拟DOM相同的key:
根据数据创建新的真实的DOM,随后渲染到页面上
- key 用于识别唯一的 Virtual DOM 元素及其驱动 UI 的相应数据。
- 它们通过回收 DOM 中当前所有的元素来帮助 React 优化渲染。这些 key 必须是唯一的数字或字符串,
- React 只是重新排序元素而不是重新渲染它们。这可以提高应用程序的性能
渲染原理⭐⭐
- 单向数据流。React 是一个 MVVM 框架,简单来说是在 MVC 的模式下在前端部分拆分出数据层和视图层。单向数据流指的是只能由数据层的变化去影响视图层的变化,而不能反过来(除非像Vue双向绑定)
- 数据驱动视图。我们无需关注页面的 DOM,只需要关注数据即可
- 渲染过程:生命周期
- setState()大部分时候是异步执行的,提升性能。
组件属性
每个 React 组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。
构建组件的方式⭐⭐
React.createClass()、ES6 class 、无状态函数。
props
对外公开属性,只读
传递数据
<body>
<div id = "div"></div>
</body>
<script type="text/babel">
// 函数组件
function Welcome(props){
return (
<div>hello world, {props.msg}</div>
);
}
let element = (
<Welcome msg="hi react" />
);
// 类组件
class Person extends React.Component{
render(){
return (
<ul>
<!--接受数据并显示-->
<li>{this.props.name}</li>
<li>{this.props.age}</li>
<li>{this.props.sex}</li>
</ul>
)
}
}
const p = {name:"张三",age:"18",sex:"女"}
ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
//上下等同
ReactDOM.render(<Person name="tom" age = "41" sex="男"/>,document.getElementById("div"));
</script>
传递函数
// 子组件
class Head extends React.Component {
render(){
this.props.getData('子组件的问候~~~')
return (
<div>Head Component</div>
);
}
}
// 父组件
class Welcome extends React.Component {
getData = (data) => {
console.log(data)
}
render(){
return (
<div>
hello world, {this.props.msg}
<br />
<Head getData={this.getData} />
</div>
);
}
}
构造函数获取props
class Foo {
constructor(props){
this.props = props;
}
}
class Bar extends Foo {
constructor(props){
super(props);
console.log(this.props);
}
render(){
console.log(this.props);
return '';
}
}
let props = {
msg: 'hello world'
};
let b = new Bar(props);
b.props = props;
b.render();
多属性传递props
class Welcome extends React.Component {
render(){
//解构
let { msg, username, age } = this.props;
console.log( isChecked );
return (
<div>hello world, {msg}, {username}, {age}</div>
);
}
}
let info = {
msg: 'hi react',
username: 'xiaoming',
age: 20
};
let element = (
<Welcome {...info} />
);
设置props初始值和类型
import PropTypes from 'prop-types'
class Welcome extends React.Component {
static defaultProps = {
age: 0
}
static propTypes = {
age: PropTypes.number
}
...
}
状态提升⭐⭐
将共享状态提升到最近的共同父组件中去,在父组件上改变状态,然后通过props分发给子组件。
state
组件的私有属性,值是对象(可以包含多个key-value的组合)
通过state的变化设置响应式视图,受控于当前组件
class Welcome extends React.Component {
state = {
msg: 'hello',
count: 0
}
handleClick = () => {
this.setState({
msg: 'hi'
});
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
refs
React操作原生DOM跟Vue框架是类似的,都是通过ref属性来完成的
class Welcome extends React.Component {
myRef = React.createRef()
handleClick = () => {
//console.log(this.myRef.current); // 原生DOM input
this.myRef.current.focus();//获取焦点
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<input type="text" ref={this.myRef} />
</div>
);
}
}
总结
props
公开,单向数据流值,父子组件间的唯一通信,不可改
1.每个组件对象都会有props(properties的简写)属性
2.组件标签的所有属性都保存在props中
3.内部读取某个属性值:this.props.propertyName
这有助于维护单向数据流,通常用于呈现动态生成的数据
state
私有(通过Ajax获取回来的数据,一般都是私有数据),
React 把组件看成是一个状态机(State Machines),
只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
refs
当需要获取某一个真实的DOM元素来交互,比如文本框的聚焦、触发强制动画等
当需要操作的元素和获取的元素是同一个时,无需ref
受控组件和非受控组件⭐⭐⭐
受控和非控:对某个组件状态的掌控,它的值是否只能由用户设置,而不能通过代码控制。
非受控组件
现用现取,官方建议尽量少用ref,用多了有一定的效率影响
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
const {username, password} = this//拿到的是form下的username, password结点
alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username"/>
密码:<input ref={c => this.password = c} type="password" name="password"/>
<button>登录</button>
受控组件
将输入维护到state,等需要时再从state取出来
class Login extends React.Component {
//state最好要初始化状态
state = {
username: '', //用户名
password: '' //密码
}
//保存用户名到状态中
saveUsername = (event) => {
this.setState({username: event.target.value})
}
//保存密码到状态中
savePassword = (event) => {
this.setState({password: event.target.value})
}
//表单提交的回调
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
const {username, password} = this.state//获得的是state下的username,password值
alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
事件event
React中的事件都是采用事件委托的形式,所有的事件都挂载到组件容器上,其次event对象是合成处理过的
事件处理的几种方法
import React from 'react'
class Test extends React.Component{
handleClick2(){
console.log('click2')
}
hangleClick4 = () =>{
console.log('click4')
}
render(){
return(
<button onClick={ console.log('click1')}>click1</button>
<button onClick={ this.handleClick2.bind(this)}>click2</button>
<button onClick={ () => {console.log('click3')}>click3</button>
<button onClick={ this.hangleClick4 }>click3</button>
)
}
}
export default Test
事件中this的处理
class Welcome extends React.Component {
handleClick = (ev) => { //推荐 public class fields语法,箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。
console.log(this); //对象
console.log(ev);
}
handleClick(){ //不推荐 要注意修正指向
console.log(this); //按钮
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
hello world
</div>
);
}
}
let element = (
<Welcome />
);
事件传参处理
推荐采用函数的高阶方式,具体代码如下:
class Welcome extends React.Component {
handleClick = (num) => { // 高阶函数
return (ev) => {
console.log(num);
}
}
render(){
return (
<div>
<button onClick={this.handleClick(123)}>点击</button>
hello world
</div>
);
}
}
let element = (
<Welcome />
);
鼠标事件 mouseenter与mouseover区别
mouseenter: 鼠标进入被绑定事件监听元素节点时触发一次,再次触发是鼠标移出被绑定元素,再次进入时。而当鼠标进入被绑定元素节点触发一次后没有移出,即使鼠标动了也不再触发。
mouseover: 鼠标进入被绑定事件监听元素节点时触发一次,如果目标元素包含子元素,鼠标移出子元素到目标元素上也会触发。
mouseenter 不支持事件冒泡 mouseover 会冒泡
跨组件通信
Welcome传递给Title:
let MyContext = React.createContext();
class Welcome extends React.Component {
state = {
msg: 'welcome组件的数据'
}
render(){
return (
<div>
Hello Welcome
<MyContext.Provider value={this.state.msg}>
<Head />
</MyContext.Provider>
</div>
);
}
}
class Head extends React.Component {
render(){
return (
<div>
Hello Head
<Title />
</div>
);
}
}
class Title extends React.Component {
static contextType = MyContext
componentDidMount = () => {
console.log( this.context );
}
render(){
return (
<div>
Hello Title <MyContext.Consumer>{ value => value }</MyContext.Consumer>
</div>
);
}
}
let element = (
<Welcome />
);
通过<MyContext.Provider>
组件携带value
属性进行向下传递的,
那么接收的语法是通过<MyContext.Consumer>
组件。
也可以定义一个静态方法static contextType = MyContext
,这样就可以在逻辑中通过this.context
来拿到同样的值。
生命周期
生命周期钩子函数就是回调函数,
挂载
- constructor():在 React 组件挂载之前,会调用它的构造函数。(注:如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。)
- render(): class 组件中唯一必须实现的方法。
- componentDidMount():在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。
更新
- render(): class 组件中唯一必须实现的方法。一旦组件被添加到 DOM,它只有在 prop 或状态发生变化时才可能更新和重新渲染。
- componentDidUpdate():在更新后会被立即调用。首次渲染不会执行此方法。
卸载
- componentWillUnmount():在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
可以看到挂载时和更新时都有render
这个方法。这就是为什么state改变的时候,会触发render
重渲染操作。
class Welcome extends React.Component {
state = {
msg: 'hello world'
}
constructor(props){
super(props);
console.log('constructor');
}
componentDidMount = () => {
// react中发起ajax请求的初始操作,在这个钩子中完成
console.log('componentDidMount');
}
componentDidUpdate = () => {
// 等DOM更新后触发的钩子
console.log('componentDidUpdate');
}
componentWillUnmount = () => {
console.log('componentWillUnmount');
}
handleClick = () => {
/* this.setState({
msg: 'hi react'
}); */
//this.forceUpdate();
root.unmount(); // 触发卸载组件
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{ this.state.msg }
</div>
);
}
}
状态提升⭐⭐
状态是 React 组件的核心,是数据的来源,必须尽可能简单。
基本上状态是确定组件呈现和行为的对象。
与 Props 不同,它们是可变的,并创建动态和交互式组件。可以通过this.state() 访问它们。
多个组件需要共享的状态提升到它们最近的父组件上,在父组件上改变这个状态然后通过props分发给子组件。对子组件操作,子组件不改变自己的状态。
复用组件
Render Props模式
组件之间使用一个值为函数的 prop 共享代码的简单技术。
class MouseXY extends React.Component {
state = {
x: 0,
y: 0
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (ev) => {
this.setState({
x: ev.pageX,
y: ev.pageY
});
}
render(){
return (
<React.Fragment>
{ this.props.render(this.state.x, this.state.y) }
</React.Fragment>
);
}
}
class Welcome extends React.Component {
render(){
return (
<MouseXY render={(x, y)=>
<div>
hello world, {x}, {y}
</div>
} />
);
}
}
let element = (
<Welcome />
);
render属性后面的值是一个回调函数,通过这个函数的形参可以得到组件中的数据,从而实现功能的复用。
HOC高阶组件模式⭐⭐
参数为组件,返回值为新组件的函数
function withMouseXY(WithComponent){
return class extends React.Component {
state = {
x: 0,
y: 0
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (ev) => {
this.setState({
x: ev.pageX,
y: ev.pageY
})
}
render(){
return <WithComponent {...this.state} />
}
}
}
class Welcome extends React.Component {
render(){
return (
<div>
hello world, { this.props.x }, { this.props.y }
</div>
);
}
}
const MouseWelcome = withMouseXY(Welcome)
let element = (
<MouseWelcome />
);
Hooks
Hook 是 React 16.8 的新增特性,是一个特殊的函数,它可以在不写 class(即不用extends React.Component) 的情况下“钩入” React 特性(即组件化模块的特性)。
为什么用Hooks⭐⭐⭐
以下帮助了解Hooks
Hooks(计算机术语):运行到某时机会调用某函数,例
onload
,addEventListener
Hooks(React):一系列以
“use”
作为开头的方法,可以完全避开class式写法
,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
useState
等同组件中的setState()
let { useState } = React;//只能在最顶层使用Hook
let Welcome = (props) => {//只能在函数组件中使用Hook
//count的初始值0,设置count的函数
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>hello world, { count }</div>
</div>
);
}
useState中的值在修改的时候,并不会进行原值的合并处理,所以使用的时候要注意。可利用扩展运算符的形式来解决合并的问题。
const [info, setInfo] = useState({
username: 'xiaoming',
age: 20
})
setInfo({
...info,
username: 'xiaoqiang'
})
如果遇到初始值需要大量运算才能获取的话,可采用惰性初始state,useState()添加回调函数的形式来实现。
const initCount = () => {
console.log('initCount');
return 2*2*2;
}
const [count, setCount] = useState(()=>{
return initCount();
});
这样初始只会计算一次,并不会每次都重新进行计算。
useReducer
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
let { useReducer } = React;
let loginState = {//体现整体关联性与统一性**
isLogin: true,
isLogout: false
}
let loginReducer = (state, action) => {
switch(action.type){
case 'login':
return { isLogin: true, isLogout: false }
case 'logout':
return { isLogin: false, isLogout: true }
default:
throw new Error()
}
}
let Welcome = (props) => {
const [ state, loginDispatch ] = useReducer(loginReducer, loginState);
const handleLogin = () => {
loginDispatch({ type: 'login' });
}
const handleLogout = () => {
loginDispatch({ type: 'logout' });
}
return (
<div>
{ state.isLogin ? <button onClick={handleLogout}>退出</button> : <button onClick={handleLogin}>登录</button> }
</div>
);
}
useEffect
在函数组件中执行副作用操作,副作用即:DOM操作、获取数据、记录日志等
代替类组件中的生命周期钩子函数。
如果不传参:相当于render之后就会执行
如果传空数组:相当于componentDidMount
如果传数组:相当于componentDidUpdate
如果返回回调函数:相当于componentWillUnmount,会在组件卸载的时候执行清除操作。
effect 在每次渲染的时候都会执行。React 会在执行当前 effect 之前对上一个 effect 进行清除。
let Welcome = (props) => {
const [count, setCount] = useState(0);
//异步函数,在浏览器渲染DOM后触发的
useEffect(()=>{
// 初始 和 更新 数据的时候会触发回调函数
console.log('didMount or didUpdate');
return ()=>{ // 这里回调函数可以用来清理副作用
console.log('beforeUpdate or willUnmount');
}
})
const handleClick = () => {
//setCount(count + 1);
root.unmount();//卸载
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>hello world, { count }</div>
</div>
);
}
关注点分离后,改变一个数据后,例如count,那么msg相关的useEffect也会触发,
给useEffect设置第二个参数,只重新触发自己的useEffect回调函数,即响应式的数据
const [count, setCount] = useState(0);
useEffect(()=>{
console.log(count);
}, [count])
const [msg, setMsg] = useState('hello');
useEffect(()=>{
console.log(msg);
}, [msg])
- useEffect()是在渲染之后且屏幕更新之后,,是异步的;
- useLayoutEffect()是在渲染之后但在屏幕更新之前,是同步的。
大部分情况下我们采用useEffect(),性能更好。
但当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,
就需要useLayoutEffect,否则可能会出现闪屏问题。
useRef
let { useRef } = React;
let Welcome = (props) => {
const myRef = useRef()
...}
等同于
React.createRef()
函数转发
把ref添加到函数组件上,把ref对应的对象转发到子组件的内部元素身上。
let Head = React.forwardRef((props, ref) => {
return (
<div>
hello Head
<input type="text" ref={ref} />
</div>
)
})
let Welcome = (props) => {
const myRef = useRef()
const handleClick = () => {
myRef.current.focus();
}
return (
<div>
<button onClick={handleClick}>点击</button>
<Head ref={myRef} />
</div>
);
}
let count = useRef(0); // 与state类似,有记忆功能,可理解为全局操作
const handleClick = () => {
count.current++;
...
}
倒计时(⭐手写)
//利用setTimeOut,每秒将数值减一
//利用useRef设置定时器,方便清楚
const [time,setTime]=useState(null)//倒计时时间
const timeRef=useRef()//设置延时器
//倒计时
useEffect(()=>{
//如果设置倒计时且倒计时不为0
if(time&&time!==0)
timeRef.current=setTimeout(()=>{
setTime(time=>time-1)
},1000)
//清楚延时器
return ()=>{
clearTimeout(timeRef.current)
}
},[time])
<button onClick={()=>{settime(60)}} >开始计时</button>
useCallback和useMemo
React 使用 Object.is 比较算法 来比较 state。
当组件数据没有变化时,是不会重新渲染试图,如下,cosole.log不会执行
let Welcome = (props) => {
const [ count, setCount ] = useState(0);
const handleClick= () => {
setCount(0);
}
console.log(123);
return (
<div>
<button onClick={handleClick}>点击</button>
hello Welcome { Math.random() }
</div>
);
}
当变化后state与当前state相同时,包括变化前的渲染一共会渲染两次
因为React 可能仍需要在跳过渲染前 渲染该组件。
但React 不会对组件树的“深层”节点进行不必要的渲染
React.memo
避免可以没有必要的重新渲染,类似于类组件中的纯函数概念。
将{ Math.random() } 包装成函数组件
let Welcome = (props) => {
const [ count, setCount ] = useState(0);
const handleClick= () => {
setCount(1);
}
return (
<div>
<button onClick={handleClick}>点击</button>
hello Welcome
<Head count={count} />
</div>
);
}
let Head = React.memo(() => {
return (
<div>hello Head, { Math.random() }</div>
)
})
在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
useCallback返回一个可记忆的函数,useMemo返回一个可记忆的值,useCallback只是useMemo的一种特殊形式。
let Welcome = (props) => {
const [ count, setCount ] = useState(0);
const handleClick= () => {
setCount(count+1);
}
const foo = () => {}
return (
<div>
<button onClick={handleClick}>点击</button>
hello Welcome
<Head onClick={foo} />
</div>
);
}
let Head = React.memo(() => {
return (
<div>hello Head, { Math.random() }</div>
)
})
当点击按钮的时候,<Head>组件会进行重新渲染,因为每次重新触发<Welcome>组件的时候,后会重新生成一个新的内存地址的foo函数。
通过useCallback和useMemo可以不让foo函数重新生成,使用之前的函数地址
从而减少子组件的渲染,提升性能。
const foo = useCallback(() => {}, [])
const foo = useMemo(()=> ()=>{}, []) // 针对函数
const bar = useMemo(()=> [1,2,3], []) // 针对数组
const foo = useMemo(()=> ()=>{}, [count]) // 第二个参数为依赖项,当count改变时,函数重新创建
自定义Hook
实现函数组件复用
let { useState, useEffect } = React
let useMouseXY = () => {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(()=>{
function move(ev){
setX(ev.pageX)
setY(ev.pageY)
}
document.addEventListener('mousemove', move)
//如果返回回调函数:相当于componentWillUnmount,会在组件卸载的时候执行清除操作。
return () => {
document.removeEventListener('mousemove', move)
}
}, [])//如果传空数组:相当于componentDidMount
return {
x,
y
}
}
let Welcome = ()=>{
const {x, y} = useMouseXY()
return (
<div>
hello Welcome, {x}, {y}
</div>
)
}
StrictMode严格模式
StrictMode
是一个用来突出显示应用程序中潜在问题的工具。用于开发环境
与 Fragment
一样,StrictMode
不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。例如:
- 识别不安全的过时的生命周期
- 关于使用过时字符串 ref API 的警告
发布环境下关闭严格模式,以避免性能损失。
Router
路由是根据不同的url地址展示不同的内容或页面,是SPA(单页应用)的路径管理器,
用于开发单页 Web 应用
1.一个路由就是一个映射关系(key:value)
2.key为路径, value可能是function或component
Router 用于定义多个路由,当用户定义特定的 URL 时,如果此 URL 与 Router 内定义的任何 “路由” 的路径匹配,则用户将重定向到该特定路由。
路由模式(同Vue)
React中的路由模式跟Vue中一样,分为history和hash模式。默认是hash
Hash模式
hash——即地址栏URL中的#符号(此hash不是密码学里的散列运算)。比如在 http://localhost:8080/#/donate 中,hash的值就是#/donate,我们在浏览器中可以通过BOM中的window.location.hash来获取当前URL的hash值
注:BOM(Browser Object Model) 是指浏览器对象模型,BOM由多个对象组成,其中代表浏览器窗口的Window对象是BOM的顶层对象,其他对象都是该对象的子对象。
history模式
通过在host后,直接添加斜线和路径来请求后端的一种URL模式。
图中pathname变量为/donate,而hash值为空。
原理
- hash通过window.addEventListener监听浏览器的onhashchange()事件变化,查找对应的路由规则
- HTML5的history API监听URL变化,所以有浏览器兼容问题
区别
- 是否向后端传参:
在hash模式中,我们刷新一下上面的网页,可以看到请求的URL为http://localhost:8080/,没有带上#/donate,说明hash 虽然出现 URL 中,#后面的内容是不会包含在http请求中的,对后端完全没有影 响,因此改变 hash 不会重新加载页面。
在history模式中,刷新一下网页,明显可以看到请求url为完整的url,url完整地请求了后端:
前端的 URL 必须和实际向后端发起请求的 URL 一致,否则返回 404 错误
在React中
-
history模式:createBrowserRouter
IE9及以下不兼容,需要由web server支持,在web client这边window.location.pathname被react router解析
-
hash模式:createHashRouter
不需要由web server支持,因为它的只有‘/’path需要由web server支持,而#/react/route URL不能被web server读取,在web client这边window,location.hash被react router解析
history的好处是可以进行修改历史记录,并且不会立刻像后端发起请求。不过如果对于项目没有硬性标准要求,可以直接使用hash模式开发。
基础路由搭建
import { createBrowserRouter, createHashRouter } from 'react-router-dom'
//路由表
export const routes = [];
//路由对象
const router = createBrowserRouter(routes);
export default router;
接下来让路由配置文件与React结合,需要在主入口index.js进行操作,如下:
import { RouterProvider } from 'react-router-dom'
import router from './router';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router}></RouterProvider>
</React.StrictMode>
);
路由表的配置字段如下:
-
path:指定路径
-
element:对应组件
-
children:嵌套路由
//路由表
export const routes = [
{
path: '/',
element: <App />,
children: [
{
path: '',
element: <Home />
},
{
path: 'about',
element: <About />,
children: [
{
path: 'foo',
element: <Foo />,
},
{
path: 'bar',
element: <Bar />,
}
]
}
]
}
];
接下来就是显示路由区域,利用<outlet>组件占位,表明子路由渲染的位置
import React from "react";
import { Outlet, Link } from 'react-router-dom'
function App() {
return (
<div className="App">
<h2>hello react</h2>
<Link to="/">首页</Link> | <Link to="/about">关于</Link>
<Outlet />
</div>
);
}
export default App;
可以看到 <Link>组件用于声明式路由切换使用。同样<outlet>组件也可以给嵌套路由页面进行使用,从而完成二级路由的切换操作。
import React from 'react'
import './About.scss'
import { Outlet, Link } from 'react-router-dom'
export default function About() {
return (
<div>
<h3>About</h3>
<Link to="/about/foo">foo</Link> | <Link to="/about/bar">bar</Link>
<Outlet />
</div>
)
}
重定向路由
访问的URL跳转到另一个URL上,从而实现重定向的需求。
<Navigate>组件就是实现重定向需求的组件。
import { createBrowserRouter, createHashRouter, Navigate } from 'react-router-dom'
children: [
{
index: true,// 默认路由
element: <Navigate to="/about/foo/123" />,
},
{
path: 'foo',
element: <Foo />
},
{
path: 'bar',
element: <Bar />
}
]
自定义全局守卫
全局守卫:包裹根组件,访问根组件下面的所有子组件都要先通过守卫进行操作
在React里面,它不像Vue一样,为我们提供和很多方便的功能,一些功能都需要自己去进行封装比如说路由拦截
在/src/components/BeforeEach.jsx下创建守卫的组件。
import React from 'react'
import { Navigate } from 'react-router-dom'
import { routes } from '../../router';
export default function BeforeEach(props) {
if(true){
return <Navigate to="/login" />
}
else{
return (
<div>{ props.children }</div>
)
}
}
根据判断的结果,是否进入到组件内,还是重定向到其他的组件内。
调用BeforeEach.jsx,index.js通过路由配置文件引入,如下:
export const routes = [
{
path: '/',
element: <BeforeEach><App /></BeforeEach>//包裹根组件APP
}
]
动态路由
根据不同的URL,可以访问同一个组件。在React路由中,通过path字段来指定动态路由的写法。
foo/xxx都能访问到Foo组件,本身就有实现了动态路由
import { Outlet, Link } from 'react-router-dom'
export default function About() {
return (
<div>
<Link to="/about/foo/123">foo 123</Link> | <Link to="/about/foo/456">foo 456</Link>
</div>
)
}
{
path: 'foo/:id',
element: <Foo />
}
id
就是变量名,可以在组件中用useParams
来获取到对应的值。
import { useParams } from 'react-router-dom'
export default function Foo() {
const params = useParams()
return (
<div>Foo, { params.id }</div>
)
}
Redux状态管理库
Redux就像Vue中的Vuex或Pinia是一样专门用于做状态管理的JS库(不是react插件库)。
只不过Redux比较独立,可以跟很多框架结合使用,不过主要还是跟React配合比较好,也是最常见的React状态管理的库。
redux相当于在顶层组件之上又加了一个组件
单一事实来源⭐
Redux:所有组件的状态都存储在store 中,并且它们从 store 本身接收更新。
单一状态树可以更容易地跟踪随时间的变化,并调试或检查程序。
Redux 的组件⭐⭐⭐
- Action 这是一个用来描述发生了什么事情的对象
- Reducer 这是一个确定状态将如何变化的地方
- Store 整个程序的状态/对象树保存在 Store 中
- View 查只显示 Store 提供的数据
Redux优点⭐
- 结果的可预测性
- 可维护性
- 服务器端渲染
- 易于测试 -
//state存储共享数据
function counterReducer(state={count: 0}, action) {//reducer修改state
switch(action.type){
case 'inc':
return {count: state.count + state.payload}
default:
return state;
}
}
const store = createStore(counterReducer)
export default store
这样store对象就可以在其他组件中进行使用了,例如在<Bar>组件中。
import React from 'react'
import './Bar.scss'
import { useSelector,useDispatch } from 'react-redux'
export default function Bar() {
const count = useSelector((state)=> state.count)//获取共享状态
const dispatch=useDispatch();//修改共享状态
const handleClick = () => {
dispatch({//dispatch触发Reducer,分发action
type: 'inc',
payload: 5
})
}
return (
<div>
<button onClick={handleClick}>修改count</button>
Bar, { count }
</div>
)
}
在主模块中进行注册。
import { RouterProvider } from 'react-router-dom'
import router from './router';
import { Provider } from 'react-redux'
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>//注册状态管理与React结合,自动完成重渲染
<RouterProvider router={router}></RouterProvider>
</Provider>
</React.StrictMode>
);
应用
从项目的整体看
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了 WebSocket
- View 要从多个来源获取数据
从组件角度看
- 某个组件的状态,需要共享
- 组件有相当大量的,随时间变化的数据
- state 需要有一个单一可靠数据源
React18新增特性⭐⭐
自动批处理以减少渲染
批处理是 React将多个状态更新分组到单个重新渲染中以获得更好的性能。
function App () {
const [ count , setCount ] = useState ( 0 ) ;
const [ flag , setFlag ] = useState ( false ) ;
function handleClick ( ) {
setCount ( c => c + 1 ) ; // 还没有重新渲染
setFlag ( f => ! f ) ; // 还没有重新渲染
// React 只会在最后重新渲染一次(这是批处理!)
}
return (
< div >
< button onClick = { handleClick } > Next < / button >
< h1 style = { { color : flag ? "blue" : "black" } } > { count } < / h1 >
< / div >
) ;
}
flushSync清洗同步
与组件中的state一样自动批处理(即合并修改,每次set只渲染一次),可用flushSync
方法消除
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
<Suspense >组件的 SSR 支持
这基本上是服务器端渲染 (SSR) 逻辑的扩展。
在典型的 React SSR 应用程序中,会发生以下步骤:
-
服务器获取需要在 UI 上显示的相关数据
-
服务器将整个应用程序呈现为 HTML 并将其发送给客户端作为响应
-
客户端下载 JavaScript 包(除了 HTML)
-
在最后一步,客户端将 javascript 逻辑连接到 HTML(称为 hydration)
在下一步可以开始之前,必须立即完成整个应用程序的每个步骤。在初始加载时变慢且无响应。
<Suspense> 组件将应用程序分解为更小的独立单元,这些单元经过提到的每个步骤。这样一旦用户看到内容,它就会变成互动的。
startTransition
状态更新分类
-
紧急更新反映直接交互,如打字、悬停、拖动等。
-
过渡更新将 UI 从一个视图过渡到另一个视图。
使用
默认情况下,React 18 仍然将更新处理为紧急更新,您可以通过将更新包装到startTransition.
import { startTransition } from 'react' ;
// 紧急:显示输入的内容
setInputValue ( input ) ;
// 将内部的任何状态更新标记为转换
startTransition ( ( ) => {
// Transition: 显示结果
setSearchQuery ( input ) ;
} ) ;
包装在其中的更新startTransition被视为非紧急处理,如果出现更紧急的更新(如点击或按键),则会中断。
// 显示你输入的内容
setInputValue ( input ) ;
// 显示结果
setTimeout ( ( ) => {
setSearchQuery ( input ) ;
} , 0 ) ;
应用场景
startTransition来包装要移动到后台的任何更新
-
缓慢渲染:React 需要执行大量工作才能转换 UI 以显示结果。
-
慢速网络:React 正在等待来自网络的一些数据。此用例与 Suspense 紧密集成。
React脚手架
开始一个react项目,不用手动配置,直接开发
Angular,Vue,React对比⭐⭐
Angular
框架比较成熟完整,过于庞大,上手难;
React和Vue
相同点 :
创建UI的js库
都是使用了虚拟dom
组件化开发
父子之间通信单项数据流
都支持服务端渲染
不同点:
vue轻量级框架,其核心库只关注视图层,简单小巧、易学易上手;
个人维护项目; 适合于移动端开发;
reacct 的jsx,可读性好
vue的是 template
数据变化,react 手动 setState vue自动响应式处理 proxy object.DefineProperty
react 单向数据流 ,vue双向数据流
react 的 redux mobx
vue 的vuex pinia
MVC、MVP、MVVM模式⭐⭐⭐
MVC (Model View Controller)
- Model(模型):提供数据
- View(视图):显示数据
- Controller(控制器):用户交互
【优点】
耦合性低,方便维护,可以利于分工协作 重用性高
【缺点】
使得项目架构变得复杂,对开发人员要求高
- key 对 DOM 操作的代价非常高
- 程序运行缓慢且效率低下
- 内存浪费严重
- 由于循环依赖性,组件模型需要围绕 models 和 views 进行创建
MVP(Model View Presenter)
从MVC演变而来,它们的基本思想有相通的地方Controller/Presenter负责逻辑的处理,
MVVM (Model View View Model)
视图和业务逻辑分开。
View视图层,Model 数据模型,ViewModel 是它们双向绑定的桥梁,自动同步更新
【优点】
相比mvp各层的耦合度更低,一个viewmodel层可以给多个view层共用(一对多),提高代码的可重用性。
*耦合度:模块间依赖的程度。
【缺点】
因为使用了dataBinding,增加了大量的内存开销,增加了程序的编译时间,所以适合轻量级项目。
数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题
React和Vue都用了MVVM
React单向数据流:只能由数据层的变化去影响视图层的变化
Vue双向数据绑定
更多推荐
所有评论(0)