前端常见面试题
HTML
HTML5 有哪些新特性?
2008年发布。
link 与 @import 的区别
- link是HTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS。
- 加载页面时,link引入的CSS被同时加载,@import引入的CSS将在页面加载完毕后加载;
- link标签作为HTML元素,不存在兼容性问题,而@import是CSS2.1才有的语法,故老版本浏览器(IE5之前)不能识别;
- link支持使用Javascript控制DOM去改变样式;而@import不支持。
建议使用link的方式引入CSS
src 和 href 的区别
请求资源类型不同
- href是 Hypertext Reference 的缩写,表示超文本引用。用来建立当前元素和文档之间的链接,常用的有:link、a;
- 在请求 src 资源时会将其指向的资源下载并应用到文档中,常用的有script、img 、iframe;
作用结果不同
- href 用于在当前文档和引用资源之间确立联系;
- src 用于替换当前内容;
浏览器解析方式不同
- 若在文档中添加href,浏览器会识别该文档为 CSS 文件,就会并行下载资源并且不会停止对当前文档的处理。
- 当浏览器解析到src,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等也如此,类似于将所指向资源应用到当前内容。这也是为什么建议把 js 脚本放在底部而不是头部的原因。
meta 标签
元数据(metadata)提供关于 HTML 文档的元数据。
元数据不会显示在页面上,但是对于机器是可读的。
典型的情况是,meta 元素被用于规定页面的描述、关键词、文档的作者、最后修改时间以及其他元数据。
标签始终位于 head 元素中。
元数据可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务,有助于SEO。
属性
-
name
名称/值对中的名称。author、description、keywords、generator、revised、others。把content 属性关联到一个名称。
-
http-equiv
没有name时,会采用这个属性的值。content-type、expires、refresh、set-cookie。把content 属性关联到http头部
-
content
名称/值对中的值,可以是任何有效的字符串。始终要和name 属性或http-equiv属性一起使
-
scheme
用于指定要用来翻译属性值的方案。
DOCTYPE 的作用
doctype是一种标准通用标记语言的文档类型声明,目的是告诉标准通用标记语言解析器要使用什么样的文档类型定义(DTD)来解析文档。
<!DOCTYPE>
声明是用来指示web浏览器关于页面使用哪个HTML版本进行编写的指令。
<!DOCTYPE>
声明必须是HTML文档的第一行,位于html标签之前。
浏览器本身分为两种模式,一种是标准模式,一种是怪异模式,浏览器通过doctype来区分这两种模式,doctype在html中的作用就是触发浏览器的标准模式,如果html中省略了doctype,浏览器就会进入到Quirks模式的怪异状态,在这种模式下,有些样式会和标准模式存在差异,而html标准和dom标准值规定了标准模式下的行为,没有对怪异模式做出规定,因此不同浏览器在怪异模式下的处理也是不同的,所以一定要在html开头使用doctype。
HTML4.01 的 doctype
在HTML4.01中,<!DOCTYPE>声明引用DTD,因为HTML4.01基于SGML。DTD规定了标记语言的规则,这样浏览器才能正确的呈现内容。在HTML4.01中有三种<!DOCTYPE>声明。
严格模式:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
过渡模式:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
框架模式:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
HTML5 的 doctype
HTML5不基于SGML,所以不需要引用DTD。在HTML5中<!DOCTYPE>只有一种
<!DOCTYPE html>
如何实现浏览器内多个标签之间的通信?
本地存储方式:localStorage
、Cookie
服务器存储方式:WebSocket
、SharedWorker
如何实现浏览器内多个标签页之间的通信?_meijory的博客
本题主要考察数据存储的知识,数据存储有本地和服务器存储两种方式。
这里主要讲解用本地存储方式解决。即调用
localStorage
、Cookie
等本地存储方式。
- 调用
localStorage
在一个标签页里面使用 localStorage.setItem(key,value)
添加(修改、删除)内容;
在另一个标签页里面监听 storage
事件。
即可得到 localstorge
存储的值,实现不同标签页之间的通信。
标签页1:
<input id="name">
<input type="button" id="btn" value="提交">
<script type="text/javascript">
$(function(){
$("#btn").click(function(){
var name=$("#name").val();
localStorage.setItem("name", name);
});
});
</script>
标签页2:
<script type="text/javascript">
$(function(){
window.addEventListener("storage", function(event){
console.log(event.key + "=" + event.newValue);
});
});
</script>
注意quirks:Safari在无痕模式下设置localStorage值时会抛出QuotaExceededError的异常
- 调用
cookie + setInterval()
将要传递的信息存储在cookie中,每隔一定时间读取cookie信息,即可随时获取要传递的信息。
页面1:
<input id="name">
<input type="button" id="btn" value="提交">
<script type="text/javascript">
$(function(){
$("#btn").click(function(){
var name=$("#name").val();
document.cookie="name="+name;
});
});
</script>
页面2:
<script type="text/javascript">
$(function(){
function getCookie(key) {
return JSON.parse("{\"" + document.cookie.replace(/;\s+/gim,"\",\"").replace(/=/gim, "\":\"") + "\"}")[key];
}
setInterval(function(){
console.log("name=" + getCookie("name"));
}, 10000);
});
</script>
Cookies,SessionStorage 和 LocalStorage 的区别
特性 | Cookie | SessionStorage | LocalStorage |
---|---|---|---|
特性 | 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效 | 仅在当前会话下有效,关闭页面或浏览器后被清除 | 除非被清除,否则永久保存 |
存放数据大小 | 4K左右 | 一般为5MB | |
与服务器端通信 | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 | 仅在客户端(即浏览器)中保存,不参与和服务器的通信 | |
易用性 | 需要程序员自己封装,源生的Cookie接口不友好 | 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 |
Cookie 属性
- name字段为一个cookie的名称。
- value字段为一个cookie的值。
- domain字段为可以访问此cookie的域名。
- path字段为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。
- expires/Max-Age 字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
- Size字段 此cookie大小。
- http字段 cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。
- secure字段 设置是否只能通过https来传递此条cookie
浏览器是如何渲染页面的?
-
解析HTML文件,创建DOM树
自上而下,遇到任何样式(link、style)与脚本(script)都会阻塞。(外部样式不阻塞后续外部脚本的加载)
-
解析CSS
优先级:浏览器默认设置 < 用户设置 < 外部样式 < 内联样式 < HTML中的style样式
-
构建渲染树
将CSS与DOM合并,构建渲染树(Render Tree)
-
布局和绘制
布局和绘制,重绘(repaint)和 重排(layout)
- 重绘:当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制,因此损耗较少。
- 回流(reflow):又叫重排。当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。
Canvas 和 SVG 图形的区别
svg是什么?
SVG 指可伸缩矢量图形 (Scalable Vector Graphics),用来定义用于网络的基于矢量的图形,使用 XML 格式定义图形。
SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失。
SVG 是万维网联盟的标准,与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体。
canvas是什么?
HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
canvas和svg的区别比较
- 从时间上看
svg并不是html5专有的标签,最初svg是用xml技术(超文本扩展语言,可以自定义标签或属性)描述二维图形的语言。svg存在的历史要比canvas久远,已经有十几年了。
canvas是html5提供的新元素<canvas>
。
- 从功能上看
SVG 是一种使用 XML 描述 2D 图形的语言。
SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的,可以为某个元素附加 JavaScript 事件处理器。
在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。
Canvas 通过 JavaScript 来绘制 2D 图形。
Canvas 是逐像素进行渲染的。
在 Canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。
- 从技术应用上
svg依赖分辨率。
svg不支持事件处理器。
svg弱的文本渲染能力。
svg能够以 .png 或 .jpg 格式保存结果图像。
svg最适合图像密集型的游戏,其中的许多对象会被频繁重绘**。**
canvas不依赖分辨率。
canvas支持事件处理器。
canvas最适合带有大型渲染区域的应用程序(比如谷歌地图)。
canvas复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)。
canvas不适合游戏应用。
CSS
CSS选择器有哪些?
- 标签选择器
- 类选择器
- id选择器
- 子选择器 (div>p)
- 包含选择器 (div p)
- 兄弟选择器 (.first~p)
- 相邻选择器 (.first+p)
- 全局选择器 (*)
- 群选择器 (.first,span)
- 属性选择器 ([type=next])
- 伪类选择器 (li:first-child{} li:last-child{} li:nth-child(){} li:not(){})
如果要重构一个页面,从CSS的角度来讲如何进行性能优化?
网站重构:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。也就是说是在不改变UI的情况下,对网站进行优化,在扩展的同时保持一致的UI。
优雅降级:Web站点在所有新式浏览器中都能正常工作,如果用户使用的是老式浏览器,则代码会针对旧版本的IE进行降级处理了,使之在旧式浏览器上以某种形式降级体验却不至于完全不能用。
如:border-shadow
渐进增强:从被所有浏览器支持的基本功能开始,逐步地添加那些只有新版本浏览器才支持的功能,向页面增加不影响基础浏览器的额外样式和功能的。当浏览器支持时,它们会自动地呈现出来并发挥作用。
如:默认使用flash上传,但如果浏览器支持 HTML5 的文件上传功能,则使用HTML5实现更好的体验;
float 和 clear
强力推荐!经验分享:CSS浮动(float,clear)通俗讲解
清除浮动的方法
使用after伪元素清除浮动(推荐使用)
.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
content: "";
display: block;
height: 0;
clear:both;
visibility: hidden;
}
.clearfix{
*zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
}
<body>
<div class="fahter clearfix">
<div class="big">big</div>
<div class="small">small</div>
<!--<div class="clear">额外标签法</div>-->
</div>
<div class="footer"></div>
</body>
overflow
TODO: 解释overflow
display
TODO: 解释display
对元素设置display:inline-block ,元素不会脱离文本流,而float就会使得元素脱离文本流,且还有父元素高度坍塌的效果
position
static:无特殊定位,对象遵循正常文档流。top,right,bottom,left等属性不会被应用。
relative:对象遵循正常文档流,但将依据top,right,bottom,left等属性在正常文档流中偏移位置。而其层叠通过z-index属性定义。
absolute:对象脱离正常文档流,使用top,right,bottom,left等属性进行绝对定位。而其层叠通过z-index属性定义。
fixed:对象脱离正常文档流,使用top,right,bottom,left等属性以窗口为参考点进行定位,当出现滚动条时,对象不会随着滚动。而其层叠通过z-index属性定义。
sticky:具体是类似 relative 和 fixed,在 viewport 视口滚动到阈值之前应用 relative,滚动到阈值之后应用 fixed 布局,由 top 决定。
值 | 描述 |
---|---|
absolute | 生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。 |
fixed | 生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。 |
relative | 生成相对定位的元素,相对于其正常位置进行定位。因此,“left:20” 会向元素的 LEFT 位置添加 20 像素。 |
static | 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。 |
inherit | 规定应该从父元素继承 position 属性的值。 |
行内元素有哪些?块级元素有哪些?有什么区别?空(void)元素有哪些?
块级元素
- address - 地址
- blockquote - 块引用
- center - 举中对齐块
- dir - 目录列表
- div - 常用块级容易,也是 css layout 的主要标签
- fieldset - form控制组
- p - 段落
行内元素(内联元素)
- a - 锚点
- em - 强调
- img - 图片
<img>
标签有两个必需的属性:src 和 alt.强烈推荐在开发中每个图像中都使用 alt 属性。这样即使图像无法显示,用户还是可以看到关于丢失了什么东西的一些信息。而且对于残疾人来说,alt 属性通常是他们了解图像内容的唯一方式<img />
标签属于替换元素,具有内置的宽高属性,所以可以设置宽高- img标签到底是行内元素还是块级元素 - 全栈道路 - 博客园
- font - 字体设定 ( 不推荐 )
- i - 斜体
- input - 输入框
行内元素和块级元素的区别
区别一:
- 块级:块级元素会独占一行,默认情况下宽度自动填满其父元素宽度
- 行内:行内元素不会独占一行,相邻的行内元素会排在同一行。其宽度随内容的变化而变化。
区别二:
- 块级:块级元素可以设置宽高
- 行内:行内元素不可以设置宽高
区别三:
- 块级:块级元素可以设置margin,padding
- 行内:行内元素水平方向的margin-left; margin-right; padding-left; padding-right;可以生效。但是竖直方向的margin-bottom; margin-top; padding-top; padding-bottom;却不能生效。
区别四:
- 块级:display:block;
- 行内:display:inline;
- 可以通过修改display属性来切换块级元素和行内元素
空(void)元素:
<br/>
//换行<hr>
//分隔线<input>
//文本框等<img>
//图片<link> <meta>
盒子模型
CSS盒模型,本质上是一个盒子包裹着HTML元素,盒子由四个属性组成,由外到内分别是:margin(外边距),border(边框),**padding(内填充)**和 content(内容)。
分类 - 标准盒模型 & 怪异盒模型
- W3C 盒子模型(标准盒模型)
宽度和高度的计算方式:
width = contentWidth
height = contentHeight
如何在CSS中设置标准盒模型:
box-sizing: content-box
-
IE 盒子模型(怪异盒模型)
宽度和高度的计算方式:
width = contentWidth + paddingWidth + borderWidth height = contentHeight + paddingHeight + borderHeight
如何在CSS中设置怪异盒模型:
box-sizing: border-box
外边距合并(高度坍塌)
MDN是这样的定义外边距合并的:
块的顶部外边距和底部外边距有时被组合(折叠)为单个外边距,其大小是组合到其中的最大外边距,这种行为称为外边距合并。
所谓外边距合并,其实就是margin合并。
外边距合并的几种情况
都正取大
一正一负相加
- 相邻兄弟元素
<div class="up">我在上面</div>
<div class="down">我在下面</div>
.up {
width: 100px;
height: 100px;
border: 1px solid blue;
margin: 100px;
}
.down {
width: 100px;
height: 100px;
border: 1px solid red;
margin: 100px;
}
- 父子元素
块级元素和其第一个子元素的存在外边距合并,也就是上边距“挨到一起”,此时父元素展现出来的外边距,将会是父元素和子元素的margin-top
的较大值。
<div class="parent">
<div class="child">我是儿子</div>
</div>
.parent {
width: 100px;
height: 200px;
background: red;
margin-top: 50px;
}
.child {
width: 50px;
height: 50px;
margin-top: 100px;
border: 1px solid blue;
}
解决外边距合并的方法
方法:使用BFC 容器,将两个外边距重合的元素放在不同的BFC 容器中。
- 相邻兄弟元素
<div class="up">我在上面</div>
<div class="down">我在下面</div>
.up {
width: 100px;
height: 100px;
border: 1px solid blue;
margin: 100px;
}
.down {
width: 100px;
height: 100px;
border: 1px solid red;
margin: 100px;
display: inline-block; //触发BFC
}
- 父子元素
<div class="parent">
<div class="child">我是儿子</div>
</div>
.parent {
width: 100px;
height: 200px;
background: red;
margin-top: 50px;
overflow: hidden; //触发BFC
}
.child {
width: 50px;
height: 50px;
margin-top: 100px;
border: 1px solid blue;
}
BFC
一个**块格式化上下文(block formatting context)**是Web页面的可视化CSS渲染的一部分。它是块盒子的布局发生,浮动互相交互的区域。
具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。
通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。
BFC 的布局规则(特点)
- BFC 是一个隔离的容器,内部子元素不会影响到外部元素
- 内部块级盒子垂直方向排列
- 盒子垂直距离由 margin 决定,同一个BFC 盒子的外边距会重叠
- BFC 的区域不会与float box叠加
- 每个元素的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
BFC 的触发条件
- 根元素,即HTML元素
- float的值为left、right、both;不为none
- overflow的值为hidden、auto、scroll;不为visible
- display的值为inline-block、table-cell、table-caption
- position的值为absolute或fixed;不为relative等
BFC 的作用
- 自适应两栏布局
- 父元素要清除浮动,否则父元素会坍塌
- 左侧定宽,向左浮动,保证两栏并列
- 右侧不需要再浮动,但是要消除文字环绕,可以为右侧定义一个BFC,因为BFC不会与float重叠
- 阻止元素被浮动元素覆盖
- 包含浮动元素时,清除内部浮动
- 分属于不同的BFC时,阻止外边距合并(margin重叠)
IFC
既然块级元素会触发BFC,那么内联元素会触发的则是IFC。
IFC 只有在一个块元素中仅包含内联级别元素时才会生成。
IFC 的布局规则(特点)
- 内部的 box 会在水平方向排布
- 这些 box 之间的水平方向的margin、boder、padding 都有效
- Box 垂直对齐方式:以它们的底部、顶部对齐,或以它们里面的文本的基线(baseline)对齐(默认,文本与图片对其),例:
line-heigth
与vertical-align
。
Flex 布局
布局的传统解决方案,基于盒状模型,依赖 display
属性 + position
属性 + float
属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
2009年,W3C 提出了一种新的方案——Flex 布局,可以简便、完整、响应式地实现各种页面布局。是 CSS3 的特性。
Flex 是 Flexible Box 的缩写,意为“弹性布局”,用来为盒状模型提供最大的灵活性。
Flex 容器 & Flex 项目
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start
,结束位置叫做main end
;交叉轴的开始位置叫做cross start
,结束位置叫做cross end
。
项目默认沿主轴排列。单个项目占据的主轴空间叫做main size
,占据的交叉轴空间叫做cross size
。
Flex 容器的属性
以下6个属性设置在容器上。
flex-direction
flex-wrap
flex-flow
:flex-direction + flex-wrapjustify-content
align-items
align-content
:justify-content + align-items
flex-direction 属性
flex-direction
属性决定主轴的方向(即项目的排列方向)。
.box {
flex-direction: row | row-reverse | column | column-reverse;
}
它的四个值:
row
(默认值):主轴为水平方向,起点在左端。row-reverse
:主轴为水平方向,起点在右端。column
:主轴为垂直方向,起点在上沿。column-reverse
:主轴为垂直方向,起点在下沿。
flex-wrap 属性
默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap
属性定义,如果一条轴线排不下,如何换行。
.box{
flex-wrap: nowrap | wrap | wrap-reverse;
}
它的三个值:
nowrap
(默认):不换行。
wrap
:换行,第一行在上方。
wrap-reverse
:换行,第一行在下方。
flex-flow 属性
flex-flow
属性是flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrap
。
.box {
flex-flow: <flex-direction> || <flex-wrap>;
}
justify-content 属性
justify-content
属性定义了项目在主轴上的对齐方式。
.box {
justify-content: flex-start | flex-end | center | space-between | space-around;
}
它有五个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。
flex-start
(默认值):左对齐flex-end
:右对齐center
: 居中space-between
:两端对齐,项目之间的间隔都相等。space-around
:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
align-items 属性
align-items
属性定义项目在交叉轴上如何对齐。
.box {
align-items: flex-start | flex-end | center | baseline | stretch;
}
它也有五个值,具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。
flex-start
:交叉轴的起点对齐。flex-end
:交叉轴的终点对齐。center
:交叉轴的中点对齐。stretch
(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。baseline
: 项目的第一行文字的基线对齐。
align-content 属性
align-content
属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
.box {
align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}
它有六个值:
flex-start
:与交叉轴的起点对齐。flex-end
:与交叉轴的终点对齐。center
:与交叉轴的中点对齐。space-between
:与交叉轴两端对齐,轴线之间的间隔平均分布。space-around
:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。stretch
(默认值):轴线占满整个交叉轴。
flex 项目的属性
以下6个属性设置在项目上。
order
flex-grow
flex-shrink
flex-basis
flex
:flex-grow + flex-shrink + flex-basisalign-self
order 属性
order
属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
.item {
order: <integer>;
}
flex-grow 属性
flex-grow
属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
.item {
flex-grow: <number>; /* default 0 */
}
如果所有项目的flex-grow
属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow
属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
flex-shrink 属性
flex-shrink
属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
.item {
flex-shrink: <number>; /* default 1 */
}
如果所有项目的flex-shrink
属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink
属性为0,其他项目都为1,则空间不足时,前者不缩小。
负值对该属性无效。
flex-basis 属性
flex-basis
属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto
,即项目的本来大小。
.item {
flex-basis: <length> | auto; /* default auto */
}
它可以设为跟width
或height
属性一样的值(比如350px),则项目将占据固定空间。
flex 属性
flex
属性是flex-grow
, flex-shrink
和 flex-basis
的简写,默认值为0 1 auto
。后两个属性可选。
.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
该属性有两个快捷值:auto
(1 1 auto
) 和 none (0 0 auto
)。
flex: 1 === flex: 1 1 0
建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。
align-self 属性
align-self
属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items
属性。默认值为auto
,表示继承父元素的align-items
属性,如果没有父元素,则等同于stretch
。
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
该属性可能取6个值,除了auto,其他都与align-items属性完全一致。
CSS 水平垂直居中
绝对定位元素的居中实现
.center-vertical{
width: 100px;
height: 100px;
background: orange;
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px; /*高度的一半*/
margin-left: -50px; /*宽度的一半*/
}
这一种工作中用的应该是最多的,兼容性也是很好。
缺点:需要提前知道元素的尺寸。如果不知道元素尺寸,这个时候就需要JS获取了。
CSS3.0的兴起,使这个问题有了更好的解决方法,就是使用 transform
代替 margin
。transform 中 translate
偏移的百分比是相对于自身大小而说的。
.content{
background: orange;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
优点:无论绝对定位元素的尺寸是多少,它都是水平垂直居中显示的。
缺点:兼容性问题。
margin: auto; 实现绝对定位元素的居中
.center-vertical{
width: 100px;
height: 100px;
background: orange;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
CSS3.0 弹性布局
html,body{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body{
display: flex;
justify-content: center;/*定义body的元素水平居中*/
align-items: center;/*定义body的元素垂直居中*/
}
.content{
width: 300px;
height: 300px;
background: orange;
}
vertical-align:middle; 垂直方向居中
.big-box{
width: 500px;
height: 400px;
background: green;
text-align: center;
}
.box{
line-height: 400px;
}
img{
vertical-align: middle;
}
display:table实现
.parent{
width: 300px;
height: 300px;
background: orange;
text-align: center;
display: table;
}
.son{
display: table-cell;
background-color: yellow;
vertical-align: middle;
}
CSS的权重和优先级
权重
- 从0开始,一个行内样式+1000,一个id选择器+100,一个属性选择器、class或者伪类+10,一个元素选择器,或者伪元素+1,通配符+0
- 权重相同,写在后面的覆盖前面的
- 继承也有权值但很低,有的文献提出它只有0.1,所以可以理解为继承的权值最低。
优先级
- 使用
!important
达到最大优先级,都使用!important
时,权重大的优先级高
CSS 动画有哪些?
animation
、transition
、transform
、translate
这几个属性要搞清楚:
- animation:用于设置动画属性,他是一个简写的属性,包含6个属性
- transition:用于设置元素的样式过渡,和animation有着类似的效果,但细节上有很大的不同
- transform:用于元素进行旋转、缩放、移动或倾斜,和设置样式的动画并没有什么关系
- translate:只是transform的一个属性值,即移动,除此之外还有scale等
visibility、display 和 opacity的区别
- display:设置了none属性会隐藏元素,且其位置也不会被保留下来,所以会触发浏览器渲染引擎的回流和重绘。
- visibility:设置hidden会隐藏元素,但是其位置还在页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘
- opacity:会将元素设置为透明,但是其位置也在页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘
JavaScript基础
ES6有哪些新特性?
基本类型 引用类型
基本类型(6种):
undefined、null、string、number、boolean、Symbol(ES6 新增的)、BigInt(ES2020)
普通基本类型:undefined、null、symbol(ES6)
特殊基本包装类型:string、number、boolean
引用类型(5种):Object、Array、RegExp、Date、Function
区别:引用类型值可添加属性和方法,而基本类型值则不可以。
基本类型
基本类型的变量是存放在栈内存(Stack)里的
基本数据类型的值是按值访问的
基本类型的比较是它们的值的比较
引用类型
引用类型的值是保存在堆内存(Heap)中的对象(Object)
引用类型的值是按引用访问的
引用类型的比较是引用的比较
问:JS 隐式转换,显式转换
一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString
字符串和数字
- + 操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接
- - 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
布尔值到数字
- 1 + true = 2
- 1 + false = 1
转换为布尔值
- for 中第二个
- while
- if
- 三元表达式
- || (逻辑或) && (逻辑与)左边的操作数
符号
- 不能被转换为数字
- 能被转换为布尔值(都是 true)
- 可以被转换成字符串 “Symbol(cool)”
宽松相等和严格相等
宽松相等允许进行强制类型转换,而严格相等不允许
字符串与数字
转换为数字然后比较
其他类型与布尔类型
先把布尔类型转换为数字,然后继续进行比较
对象与非对象
执行对象的 ToPrimitive(对象)然后继续进行比较
假值列表
- undefined
- null
- false
- +0, -0, NaN
- “”
var、let、const 的区别
-
var声明的变量会挂载在window上,作用在全局,而let和const声明的变量作用在块作用域中
-
var声明变量存在变量提升,let和const不存在变量提升
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值 var a = 100; console.log(b); // 报错:b is not defined ===> 找不到b这个变量 let b = 10; console.log(c); // 报错:c is not defined ===> 找不到c这个变量 const c = 10;
-
同一作用域下let和const不能声明同名变量,而var可以
-
let、const 存在暂时性死区
var a = 100; if(1){ a = 10; //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a, // 而这时,还未到声明时候,所以控制台Error:a is not defined let a = 1; }
-
const
- 一旦声明必须赋值,且不能用 null 占位
- 声明后不能修改
- 如果声明的是复合类型数据,可以修改其属性
为什么要添加 let?
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>
<script>
let buttons = document.getElementsByTagName("button");
for(var i=0; i<buttons.length; i++){
buttons[i].addEventListener("click", function (){
console.log("用户点击了按钮" + (i+1));
})
}
</script>
在上面的例子中无论用户如何点击都将打印“用户点击了按钮5”,是不是很奇怪,具体原因的话,得细品,这里就不赘述了。
在ES6之前,为了解决这个问题,我们得用IIFE来解决:
let buttons = document.getElementsByTagName("button");
for(var i=0; i<buttons.length; i++){
// buttons[i].addEventListener("click", function (){
// console.log("用户点击了按钮" + (i+1));
// })
(function (i){
buttons[i].addEventListener("click", function (){
console.log("用户点击了按钮" + (i+1));
})
})(i);
}
这是因为函数是有作用域的。但是现在ES6横空出世,为我们带来了let
关键字,我们就只需要将for循环中的var改为let就可以实现同样的功能啦。
问:0.1 + 0.2 === 0.3 吗?为什么?
console.log(0.1 + 0.2); //0.30000000000000004
在两数相加时,会先转换成二进制,0.1和0.2转换成二进制的时候尾数会发生无限循环,然后进行对阶运算,JS引擎对二进制进行截断,所以造成精度丢失。
JavaScirpt使用Number类型来表示数字(整数或浮点数),遵循IEEE754标准,通过64位来表示一个数字(1+11+52)
- 1符号位,0表示正数,1表示负数 s
- 11指数位(e)
- 52尾数,小数部分(即有效数字)
最大安全数字:Number.MAX_SAFE_INTEGER = Math.pow(2,53) - 1
,转换成整数就是16位,所以
0.1===0.1,是因为通过toPrecision(16)
去有效位之后,两者是相等的。
总结:精度丢失可能出现在进制转换和对阶运算中。
问:JS 整数是怎么表示的?
通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对于 16 位十进制。(符号位 + 指数位 + 小数部分有效位)
问:Number() 的存储空间是多大?如果后台发送了一个超过最大自己的数字怎么办
Math.pow(2, 53) ,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。
写代码:实现函数能够深度克隆基本类型
浅克隆
function shallowClone(obj) {
let cloneObj = {};
for (let i in obj) {
cloneObj[i] = obj[i];
}
return cloneObj;
}
深克隆
实现方法:
-
JSON.stringify() 以及 JSON.parse()
- 不可以拷贝 undefined , function, RegExp 等等类型
-
Object.assign(target, source)
-
多层嵌套(对象属性的值为引用类型)时,失效,因为只拷贝了引用。
var obj1 = { a: 1, b: 2, c: ['a','b','c'] } var obj2 = Object.assign({}, obj1); obj2.c[1] = 5; console.log(obj1.c); // ["a", 5, "c"] console.log(obj2.c); // ["a", 5, "c"]
-
手写深拷贝:递归拷贝
function deepClone(obj) {
if (typeof obj === 'object') {
var result = obj.constructor === Array ? [] : {};
for (let i in obj) {
result[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
}
} else {
let result = obj;
}
return result;
}
问:事件是如何实现的?
基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。
在 Web 端,我们常见的就是 DOM 事件:
- DOM0级事件,直接在html元素上绑定
on-event
,比如onclick;取消的话,dom.onclick=null
,同一个事件只能有一个处理程序,后面的会覆盖前面的。 - DOM2级事件,通过
addEventListener
注册事件,通过removeEventListener
来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件 - DOM3级事件,增加了事件类型,比如Ul事件,焦点事件,鼠标事件
参考:
https://zhuanlan.zhihu.com/p/73091706
问:事件流
事件流描述了页面接收事件的顺序。
有意思的是,IE 和 Netscape 开发团队居然提出了两个截然相反的事件流概念:
- IE的事件流是 事件冒泡流,
- 标准的浏览器事件流是 事件捕获流。
不过 addEventLister 给出了第三个参数同时支持冒泡与捕获,下文将介绍。
分类
事件冒泡流
IE 的事件流叫事件冒泡,也就是说事件的传播为:从事件开始的具体元素,一级级往上传播到较为不具体的节点。
案例如下:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件冒泡</title>
</head>
<body>
<div>点我</d>
</body>
</html>
当我们点击 div 元素时,事件是这样传播的:
div ➡️ body ➡️ html ➡️ document
现代浏览器都支持事件冒泡,IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。
事件捕获流
Netscape 团队提出的另一种事件流叫做事件捕获。它的原理刚好和事件冒泡相反,它的用意在于在事件到达预定目标之前捕获它,而最具体的节点应该是最后才接收到事件的。
比如还是上面的案例,当点击 div 元素时,事件的传播方向就变成了这样:
document ➡️ html ➡️ body ➡️ div
IE9、Firefox、Chrome 和 Safari 目前也支持这种事件流模型,但是有些老版本的浏览器不支持,所以很少人使用事件捕获,而是用事件冒泡的多一点。
DOM 事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段(在事件处理中被看作冒泡阶段的一部分)、事件冒泡阶段。
事件捕获最先发生,为提前拦截事件提供了可能。然后是实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。以前面的例子,则会按下图顺序触发事件。
事件处理程序
事件就是用户或者浏览器自身执行某种动作,比如click、load、mouseover。
而响应某个事件的函数就叫做事件处理程序(事件监听器),事件处理程序的名字以 on 开头,click=>onclick、load=>onload
DOM2提供了两个方法来让我们处理和删除事件处理程序的操作:addEventListener() 和 removeEventListener() 。
btn.addEventListener(eventType, function () {}, false);
// 该方法应用至DOM节点
// 第一个参数为事件名;
// 第二个为事件处理程序;
// 第三个为布尔值,true 为事件捕获阶段调用事件处理程序,false 为事件冒泡阶段调用事件处理程序。
IE 兼容
- attchEvent(‘on’ + type, handler)
- detachEvent(‘on’ + type, handler)
事件监听器执行顺序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件冒泡</title>
</head>
<body>
<div id="parEle">我是父元素
<p id="sonEle">我是子元素</p>
</div>
</body>
</html>
<script type="text/javascript">
let sonEle = document.getElementById('sonEle');
let parEle = document.getElementById('parEle');
parEle.addEventListener('click', function () {
console.log('父级 捕获');
}, true);
parEle.addEventListener('click', function () {
console.log('父级 冒泡');
}, false);
sonEle.addEventListener('click', function () {
console.log('子级捕获');
}, true);
sonEle.addEventListener('click', function () {
console.log('子级冒泡');
}, false);
/* 父级 捕获
* 子级捕获
* 子级冒泡
* 父级 冒泡 */
</script>
当容器元素及嵌套元素,既在捕获阶段,又在冒泡阶段调用事件处理程序时,事件按DOM事件流的顺序执行事件处理程序:父级捕获 ➡️ 子级冒泡 ➡️ 子级捕获 ➡️ 父级冒泡 。
参考链接
https://juejin.im/entry/5826ba9d0ce4630056f85e07
问:new 一个函数发生了什么
源于 MDN:
对于
var o = new Foo();
//JavaScript 实际上执行的是: var o = new Object(); o.[[Prototype]] = Foo.prototype; Foo.call(o); ///由于这里this是指向o,可以把什么this.name/getName绑定到o上
- 创造一个全新的对象
- 这个对象会被执行 [[Prototype]] 连接:将这个新对象的 [[Prototype]] 连接到其构造函数.prototype 所指向的对象上
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象;否则直接返回函数的返回值
问:new 一个构造函数,如果函数返回 return {}
、 return null
, return 1
, return true
会发生什么情况?
如果函数返回一个对象,那么new 这个函数调用返回这个函数的返回对象,否则返回 new 创建的新对象
function A () { return {} };
function B () { return null };
function C () { return 1 };
function D () { return true };
const a = new A();
const b = new B();
const c = new C();
const d = new D();
console.log(a); // {}
console.log(b); // B {}
console.log(c); // C {}
console.log(d); // D {}
问:symbol
有什么用处?
可以用来表示一个独一无二的变量防止命名冲突。
还可以利用 symbol
不会被常规的方法(除了 Object.getOwnPropertySymbols
外)遍历到,所以可以用来模拟私有变量。
主要用来提供遍历接口,布置了
symbol.iterator
的对象才可以使用for···of
循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕。Symbol.for() 可以在全局访问 symbol
for…in 和 for…of 区别
let arr = [{name: '张三'}, {name: '李四'}];
let obj = {name: '张三', age: '12'};
for (let i in arr) console.log(i); // 0 1
for (let i in obj) console.log(i); // name age
for (let i of arr) console.log(i); // { name: '张三' } { name: '李四' }
for (let i of obj) console.log(i); // TypeError: obj is not iterable
for…in
Array.prototype.method=function(){
console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组"
for (var index in myArray) {
console.log(myArray[index]);
}
// 1
// 2
// 4
// 5
// 6
// 7
// 数组
// [Function (anonymous)]
- index索引为字符串型数字,不能直接进行几何运算
- 遍历顺序有可能不是按照实际数组的内部顺序
- 使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性
所以for in更适合遍历对象,不要使用for in遍历数组。
for…of
Array.prototype.method=function(){
console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组"
for (var item of myArray) {
console.log(item);
}
// 1
// 2
// 4
// 5
// 6
// 7
所有拥有Symbol.iterator的对象被称为可迭代的。for-of 循环首先调用集合的Symbol.iterator方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象;for-of循环将重复调用这个方法,每次循环调用一次。举个例子,这段代码是我能想出来的最简单的迭代器:
var zeroesForeverIterator = {
[Symbol.iterator]: function () {
return this;
},
next: function () {
return {done: false, value: 0};
}
};
所以:
- for…of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句
- for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(这也是它的本职工作)或内建的Object.keys()方法:
问:什么是作用域?
ES5 中只存在两种作用域:全局作用域和函数作用域。在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。
什么是作用域链?
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止。而作用域链,就是由当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
let outer = function( ){
let name = 'Jake';
return function() {
return name;
};
};
闭包产生的本质
当前环境中存在指向父级作用域的引用。
闭包的应用场景
柯里化
柯里化是指一个函数,它接受函数A,并返回一个新的函数,这个新的函数能够处理函数A的剩余参数。
function getAddress(province,city,area){
return province + city + area;
}
getAddress('浙江省','杭州市','西湖区'); //浙江省杭州市西湖区
进行柯里化后,如下:
function getAddress(province){
return function (city) {
return function (area) {
return province + city + area;
}
}
}
getAddress('浙江省','杭州市','西湖区'); //浙江省杭州市西湖区
防抖 和 节流
应用场景
-
搜索框input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者使用防抖方案实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。
-
页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)
防抖
函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。
- 持续触发不执行
- 不触发的一段时间之后再执行
/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助闭包,这个返回值用于clearTimeout
return function() {
if(timer){
// 进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
clearTimeout(timer)
timer = setTimeout(fn,delay)
}else{
// 进入该分支说明当前并没有在计时,那么就开始一个计时
timer = setTimeout(fn,delay)
}
}
}
节流
节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。
- 持续触发并不会执行多次
- 到一定时间再去执行
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){ //当valid === false
return false // 休息时间 暂不接客
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn()
valid = true
}, delay)
}
}
原型链
什么是原型链?
当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。
这种通过原型链接的逐级向上的查找链被称为原型链。
什么是原型继承?
一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。
function SuperType(name) {
this.name = name;
}
// 组合式继承(P244)
function SubType(name) {
// 继承属性
SuperType.call(this, name);
}
// 继承方法
SubType.prototype = new SuperType();
为什么原型链的终点是 null?
我们先假设Object.prototype
为终点。那么,当我们取它的原型时,应该怎么办?即 Object.prototype.__proto__
应该返回什么?
取一个对象的属性时,可能发生三种情况:
- 如果属性存在,那么返回属性的值。
- 如果属性不存在,那么返回undefined。
- 不管属性存在还是不存在,有可能抛异常。
我们已经假设Object.prototype
是终点了,所以看起来不能是情况1。另外,抛出异常也不是好的设计,所以也不是情况3。那么情况2呢,它不存在原型属性,返回undefined怎么样?也不好,因为返回 undefined 一种解释是原型不存在,但是也相当于原型就是 undefined。这样,在原型链上就会存在一个非对象的值。
所以,最佳选择就是 null。一方面,你没法访问null的属性,所以起到了终止原型链的作用;另一方面,null 在某种意义上也是一种对象,即空对象。这样一来,就不会违反“原型链上只能有对象”的约定。
所以,“原型链的终点是null”虽然不是必须不可的,但是却是最合理的。
问: 函数中的arguments是数组吗?类数组转数组的方法了解一下?
是类数组,是属于鸭子类型的范畴,长得像数组。
类数组转数组的方法:
-
… 运算符
-
Array.from
-
Array.prototype.slice.apply(arguments)
function foo(a) {
console.log(arguments); // [Arguments] { '0': 'a' }
console.log(typeof arguments); // object
}
foo('a');
instanceof 如何使用
左边可以是任意值,右边只能是函数
'hello tuture' instanceof String // false
判断数据类型的四种方法
-
typeof
-
instanceof: instanceof 用来判断A是否为B的实例,表达式为:
A instanceof B
。如果A是B的实例,则返回true,否则返回false。instanceof 检测的是原型,内部机制是通过判断对象的原型链中是否有类型的原型。let myInstanceof = (target,origin) => { while(target) { if(target.__proto__ === origin.prototype) { // target.__proto__ 也可以写作 Object.getPrototypeOf(target) return true } target = target.__proto__ } return false } let a = [1,2,3] console.log(myInstanceof(a,Array)); // true console.log(myInstanceof(a,Object)); // true
-
constructor
-
Object.prototype.toString(): toString()是Object的原型方法,调用该方法,默认返回当前对象的[[Class]]。这是一个内部属性,其格式为[object Xxx],其中Xxx就是对象的类型。
对于Object对象,直接调用toString()就能返回[object Object],而对于其他对象,则需要通过call、apply来调用才能返回正确的类型信息。
typeof NaN会输出什么?
typeof NaN === 'number'
合并对象的方法
-
Object.assign(target, …sources)
var o1 = { a: 1 }; var o2 = { b: 2 }; var o3 = { c: 3 }; var obj = Object.assign(o1, o2, o3); console.log(obj); // { a: 1, b: 2, c: 3 } console.log(o1); // { a: 1, b: 2, c: 3 }, 且目标对象自身也会改变。 - 扩展运算符 ```js var obj1 = { a: 1}; var obj2 = { b:2}; var obj ={...obj1, ...obj2} console.log(obj); // { a: 1, b: 2}
-
JQuery的 $.extend方法
var object1 = {name: '张三', sex: 'man'} var object2 = {name: '李四', age: 15} $.extend(object1, object2);
写代码:数组扁平化
const arr = [1,[2,[3,4]]];
console.log(arr.flat(Infinity))
function flatten(arr) {
let ans = [];
arr.forEach(e => {
if (Array.isArray(e)) ans = ans.concat(flatten(e));
else ans.push(e);
});
return ans;
}
console.log(flatten([1, [2, [3, 4]]]));
问:bind,call,apply 具体指什么
使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
call: Array.prototype.call(this, args1, args2])
apply: Array.prototype.apply(this, [args1, args2]) 。ES6 之前用来展开数组调用,foo.appy(null, []),ES6 之后使用 ...
操作符
写代码:手写 call、apply、bind
// call:参数为数组的展开
Function.prototype.myCall = function(context) {
context = context || window // context为可选参数,如果不传的话默认上下文是window
const args = [...arguments].slice(1) // 数组元素为除 context 之外的所有参数
context.fn = this // 给content创建一个fn属性,并将值设置为需要调用的函数
const result = context.fn(...args)
delete context.fn
return result
}
// apply:参数为数组
Function.prototype.myApply = function(context) {
context = context || window;
context.fn = this;
let result;
if (arguments[1]) {
const args = arguments[1];
result = context.fn(...args);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
// bind
Function.prototype.myBind = function(context) {
context = context || window;
context.fn = this;
const args = [...arguments].slice(1);
return function() {
const _args = [...arguments].concat(args);
const result = context.fn(..._args);
delete context.fn;
return result;
}
}
function bindThis(f, oTarget) {
return function(){
return f.apply(oTarget, arguments);
}
}
问:如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象
箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
普通函数通过 function 关键字定义,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
箭头函数使用被称为 “胖箭头” 的操作 =>
定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。
- 箭头函数常用于回调函数中,包括事件处理器或定时器
- 没有原型、没有 super,没有 arguments,没有 new.target
- 不能通过 new 关键字调用
- 因为,箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错
- 一个函数内部有两个方法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上。当直接调用时,执行 [[Call]] 方法,直接执行函数体
- 因为,箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错
var obj = {
i: 10,
b: () => console.log(this.i, this),
c: function() {
console.log( this.i, this)
}
}
obj.b(); // undefined Window
obj.c(); // 10 Object {...}
问:知道 ES6 的 Class 嘛?Static 关键字有了解嘛?
class Person {
constructor() {
// this 的内容定义在实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let person = new Person();
person.locate(); // instance Person { locate: [Function (anonymous)] }
Person.prototype.locate(); // prototype {}
Person.locate(); // class [class Person]
写代码:Promise 原理
class myPromise {
constructor(fn) {
this.resolveCallBack = [];
this.rejectCallBack = [];
this.state = 'PENDING';
this.value = '';
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
this.state = 'RESOLVED';
this.value = value;
this.resolveCallBack.map(cb => cb(value));
}
reject(value) {
this.state = 'REJECTED';
this.value = value;
this.rejectCallBack.map(cb => cb(value));
}
then(onResolved, onRejected) {
if (this.state === 'PENDING') {
this.resolveCallBack.push(onResolved);
this.rejectCallBack.push(onRejected);
}
if (this.state === 'RESOLVED') {
onResolved(this.value);
}
if (this.state === 'REJECTED') {
onRejected(this.value);
}
}
}
问:js脚本加载问题,async、defer问题
<script src="script.js"></script>
这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
<script async src="script.js"></script>
async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
<script defer src="myscript.js"></script>
defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个 myscript.js 文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
因此:
- 如果与 DOM 和其他脚本依赖不强时,使用 async
- 如果依赖其他脚本和 DOM 结果,使用 defer
如何判断一个对象是不是空对象?
Object.keys(obj).length === 0
说下对js的了解吧
JavaScript严格意义上来说分为:语言标准部分(ECMAScript)+ 宿主环境部分
语言标准部分
2015年发布的ES6,引入了诸多新特性,使得编写大型项目变成可能
宿主环境部分
- 在浏览器宿主环境包括 DOM + BOM 等
- 在Node中,宿主环境包括一些文件、数据库、网络、与操作系统的交互等
写代码:数组去重
Array.from(new Set([1, 1, 2, 2]))
问:setTimeout(fn, 0)
多久才执行,Event Loop
setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定
什么是EventLoop
Event Loop
即事件循环,指浏览器或Node的一种解决JavaScript单线程运行时,阻塞的一种机制,也就是异步的原理。
javascript是一门单线程语言,虽然HTML5提出了Web-workers这样的多线程解决方案,但是并没有改变JaveScript是单线程的本质。
什么是H5 Web Workers?
就是将一些大计算量的代码交由Web Worker运行而不冻结用户界面,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
javascript事件循环 Event Loop
两类任务:
- 同步任务
- 异步任务
- 同步和异步任务分别进入不同的“场所”执行。
- 所有同步任务都在主线程上执行,形成一个执行栈;
- 而异步任务进入Event Table,并注册回调函数
- 当这个异步任务有了运行结果,Event Table会将这个回调函数移入Event Queue,进入等待状态
- 当主线程内同步任务执行完成,会去Event Queue读取对应的函数,并结束它的等待状态,进入主线程执行
- 主线程不断重复上面3个步骤,也就是常说的Event Loop(事件循环)。
那么我们怎么知道什么时候主线程是空的呢?
js引擎存在monitoring process进程,会持续不断地检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
宏任务 和 微任务
宏任务,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:
- script (可以理解为外层同步代码)
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
setTimeout和setInterval
setTimeout(fn,0)
这里的延迟0秒时什么意思呢?
含义是,当主线程执行栈内为空时,不用等待,就马上执行。
setInterval和setTimeout类似,只是前者是循环的执行。对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。对于setInterval(fn,ms)
来说,我们已经知道不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
promise、process.nextTick和async/await
setTimeout(fn, 0)
在下一轮“事件循环”开始时执行,Promise.then()
在本轮“事件循环”结束时执行。
不同类型的任务会进入对应的Event Queue:
Promise中的异步体现在then
和catch
中,所以写在Promise中的代码是被当做同步任务立即执行的。
await实际上是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码;
因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
浏览器的 EventLoop
这张图将浏览器的Event Loop完整的描述了出来,我来讲执行一个JavaScript代码的具体流程:
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
- 重复第3-7个步骤;
- …
可以看到,这就是浏览器的事件循环Event Loop
这里归纳3个重点:
- 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;
- 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
举个例子
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
script end
promise1
promise2
setTimeout
*/
图解
再举个例子
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
/*
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
*/
JS为什么不能多线程?
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,JavaScript就是单线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
复制一个数组有哪些方法?哪个方法最好?
- for()循环(浅拷贝)
- while()循环(浅拷贝)
- Array.map(浅拷贝)
- Array.filter(浅拷贝)
- Array.reduce(浅拷贝)
reduce()
方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。——Array.prototype.reduce() | MDN
- Array.slice(浅拷贝) ⭐️⭐️
- Array.cancat(浅拷贝)
- Array.from(浅拷贝)
- 扩展运算符[…](深拷贝/浅拷贝)⭐️
- JSON.parse & JSON.stringify(深拷贝)
命令式代码 与 声明式代码
TODO
DOM操作
Vue
面向对象编程 与 函数式编程
TODO
第一公民
例子:
需求:
- 对一个数组,只保留小于100的数
- 对这个数组所有元素,都乘以2
- 对这个数组所有元素求和
函数式编程:
let nums = [1, 3, 234, 32, 344, 21];
let res = nums.filter(n => n<100).map(n => n*2).reduce((pre, n) => pre+n);
console.log(res);
JavaScript 高阶函数
TODO
- filter():过滤,不改变数组
- map():映射,不改变数组,返回新数组
- reduce():累加,不改变数组,返回一个值
- forEach():只是简单的将数组遍历,不改变数组,返回undefined
写代码:reduce 实现一个 map,filter
Array.prototype.myMap = function(handle, context) {
return this.reduce((pre, cur) => {
pre.push(handle.call(context, cur));
return pre;
}, []);
}
const arr1 = [1, 2, 3].myMap(n => n * 2);
console.log(arr1);
Array.prototype.myFilter = function(handle, context) {
return this.reduce((pre, cur) => {
if (handle.call(context, cur)) pre.push(cur);
return pre;
}, []);
}
const arr2 = [1, 2, 3].myFilter(n => n > 2);
console.log(arr2);
写代码:手写 forEach
Array.prototype.myForEach = function(handle, context) {
for (let i = 0; i < this.length; i++)
handle.call(context, this[i]);
}
const arr = [1, 2, 3].myForEach(e => console.log(e));
写代码:快速排序
function quickSort (array) {
if (array.length <= 1) return array;}
let pivotIndex = Math.floor(array.length/2);
let pivot = array.splice(pivotIndex, 1)[0]; //基准数
let left = [];
let right = [];
for(let 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));
}
let array = [3, 1, 4, 5, 2, 4];
console.log(quickSort(array)); // [ 1, 2, 3, 4, 4, 5 ]
找出数组中出现次数最多的元素
function more(arr) {
let max=null;//定义一个用来存储出现次数最多的元素
let num=1;//定义一个用来存储最出现的次数
arr.reduce((p,k)=>{ //对该数组进行reduce遍历
p[k]?p[k]++:p[k]=1;
if(p[k]>num){
num=p[k]
max=k
}
return p
},{})
return {max:max,num:num}//返回最多元素对象
}
思路:reduce的第二个参数是传递给函数的初始值,第一个参数是一个函数。那么此方法中第一次将{}传递给了p参数,k参数为当前遍历的对象相当于Foreach中的item参数
跨域
什么是同源策略?
同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。
所谓同源是指**"协议+域名+端口"三者相同**,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM和JS对象无法获得
- AJAX 请求不能发送
9种跨域解决方案
-
JSONP跨域:JSONP 的原理就是利用
<script>
标签没有跨域限制,通过<script>
标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。缺点是仅支持GET请求。<!--原生实现--> <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>
//服务器返回 handleCallback({"status": true, "user": "admin"})
//JQuery AJAX实现 $.ajax({ url: 'http://www.domain2.com:8080/login', type: 'get', dataType: 'jsonp', // 请求方式为jsonp jsonpCallback: "handleCallback", // 自定义回调函数名 data: {} });
- 用原生 AJAX 实现;用 JQuery AJAX 实现;用 Vue axios 实现
-
跨域资源共享(CORS):它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
-
浏览器将CORS跨域请求分为:简单请求 和 非简单请求。
- 对于简单请求(HEAD/GET/POST),浏览器在头信息中,增加一个 Origin 字段,直接发出CORS请求。响应头字段中,Access-Control-Allow-Origin字段值要么是请求时Origin字段的值,要么是
*
,表示接受任意域名请求。 - 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
- 预检请求的请求方法是 OPTIONS,表示这个请求时用来询问的。
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容 // 前端设置是否带cookie xhr.withCredentials = true; xhr.open('post', 'http://www.domain2.com:8080/login', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('user=admin'); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { alert(xhr.responseText); } };
- 对于简单请求(HEAD/GET/POST),浏览器在头信息中,增加一个 Origin 字段,直接发出CORS请求。响应头字段中,Access-Control-Allow-Origin字段值要么是请求时Origin字段的值,要么是
-
前端设置:可以用原生 AJAX;可以用 JQuery AJAX
-
服务端设置:nodejs
-
-
nginx 代理跨域:实际上与CORS跨域原理一样,通过配置文件设置请求响应头 Access-Control-Allow-Origin等字段
- Nginx(engine x)是一个高性能的HTTP和反向代理web服务器
-
nodejs中间件代理跨域:原理与nginx相同,都是通过启动一个代理服务器,实现数据的转发。
-
document.domain
+<iframe>
跨域:仅限主域相同,子域不同的跨域应用场景。原理是,两个页面都通过 js 强制设置 document.domain 为基础主域,实现了同域。 -
location.hash
+<iframe>
跨域:原理是,A想与B跨域通信,通过中间页C(与A同域)来实现。三个页面,通过域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。 -
window.name
+<iframe>
跨域:原理是,window.name
值在不同的页面加载后依旧存在,而且可以支持非常长的 name 值(2MB) -
postMessage 跨域:它是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,具体见链接。
-
WebSocket 协议跨域:WebSocket protocol 是HTML5的一种新协议,它实现了浏览器和服务器的全双工通信(双向同时通信),同时允许跨域通讯,是 server push 技术的一种很好体现。
项目中是如何跨域的
知乎懂你大数据分析平台:在Spring Boot中,通过全局配置一次性解决跨域问题,全局配置只需要在配置类中重写addCorsMappings方法即可
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //表示本应用的所有方法都会去处理跨域请求
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
.maxAge(3600);
}
}
更常规的方法是,使用CORS可以在前端代码不做任何修改的情况下,实现跨域:
@RestController
public class HelloController {
@CrossOrigin(value = "http://localhost:8081")
@GetMapping("/hello")
public String hello() {
return "hello";
}
@CrossOrigin(value = "http://localhost:8081")
@PostMapping("/hello")
public String hello2() {
return "post hello";
}
}
AJAX异步调用和异步刷新的过程
- 创建XMLHttpRequest对象,也就是创建一个异步调用对象
- 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
- 设置响应HTTP请求状态变化的函数
- 发送HTTP请求
- 获取异步调用返回的数据
- 使用JavaScript和DOM实现局部刷新
图片懒加载
有时候一个网页会包含很多的图片,例如淘宝京东这些购物网站,商品图片多只之又多,页面图片多,加载的图片就多。服务器压力就会很大。不仅影响渲染速度还会浪费带宽。比如一个1M大小的图片,并发情况下,达到1000并发,即同时有1000个人访问,就会产生1个G的带宽。
为了解决以上问题,提高用户体验,就出现了懒加载方式来减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。
如果:offsetTop-scroolTop<clientHeight,则图片进入了可视区内,则被请求。
前端路由
后端路由阶段
早期的网站开发中,整个HTML页面是由服务器渲染的:服务器直接生成渲染好的HTML页面,返回给客户端进行展示。
当有多个页面时,每个页面具有相应的URL。客户端将URL发送给服务器,服务器通过正则对该URL进行匹配,交给Controller进行处理,最终生成HTML或者数据,返回给客户端。
由服务器渲染好整个页面,然后返回给客户端的这种方式叫做后端渲染。
后端渲染:JSP(Java Server Page)技术,HTML+CSS+Java
这种情况下渲染好的页面,不需要单独加在任何JS或CSS,可以直接交给浏览器展示,同时也有利于SEO的优化。
缺点:
- 整个页面的模块需要由后端人员编写和维护,或前端人员需要通过PHP和Java等语言编写页面代码
- HTML代码和数据以及对应的逻辑会混在一起,给编写和维护带来麻烦
前后端分离阶段
随着Ajax的出现,有了前后端分离的开发模式。
后端只提供API来返回数据,前端通过Ajax获取数据,并通过JS将数据渲染到页面中。
这样就解决了后端渲染的问题,使前后端责任清晰:后端专注于数据,前端专注于交互和可视化。并且当移动端出现后,后端不需要做任何改变,依然可以使用之前的API。
单页面富应用(SPA)阶段
SPA(single page web application,单页面富应用)最主要的特点是,在前后端分离的基础上加了一层前端路由,也就是前端来维护一套路由规则。
前端路由的核心就是,改变URL页面是不会刷新的。
计算机网络
HTTP报文
HTTP请求报文
- 请求行
- 请求头部
- 空行
- 请求数据
HTTP响应报文
- 状态行
- 响应头部
- 空行
- 响应包体
从输入URL到页面成功展示到浏览器的过程?
- 从浏览器 **接收到url **到 开启网络请求线程(这一部分涉及浏览器的机制以及进程与线程之间的关系)
- 从 **开启网络线程 **到 发出一个完整的http请求(这一部分涉及到dns查询,tcp/ip请求,五层因特网协议栈等知识)
- 从 服务器接收到请求 到 对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)
- 后台和前台的http交互(这一部分包括http头部、响应码、报文结构、cookie,cookie优化,以及编码解码,如gzip压缩等)
- 单独拎出来的 缓存问题,http的缓存(这部分包括http缓存头部,etag,catch-control等)
- 浏览器接收到http数据包后的解析流程(这部分包括dom树、css规则树、合并成render树,然后layout、painting渲染、复合图层合成、GPU绘制、外链资源处理、loaded和domcontentloaded等)
- CSS的可视化格式模型(元素的渲染规则,如css三大模型,BFC,IFC等概念)
- JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)
- 其它(可以拓展不同的知识模块,如跨域,web安全,hybrid模式等等内容)
1. 网络通信
-
在浏览器中输入url
-
应用层DNS解析域名
客户端先检查本地是否有对应的IP地址,若找到则返回响应的IP地址。若没找到则请求上级DNS服务器,直至找到或到根节点。
-
应用层客户端发送HTTP请求
HTTP请求包括请求报头和请求主体两个部分,其中请求报头包含了至关重要的信息,包括请求的方法(GET / POST)、目标url、遵循的协议(http / https / ftp…),返回的信息是否需要缓存,以及客户端是否发送cookie等。
-
传输层TCP传输报文
位于传输层的TCP协议为传输报文提供可靠的字节流服务。它为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。TCP协议通过“三次握手”等方法保证传输的安全可靠。
-
网络层IP协议查询MAC地址
IP协议的作用是把TCP分割好的各种数据包传送给接收方。而要保证确实能传到接收方还需要接收方的MAC地址,也就是物理地址。IP地址和MAC地址是一一对应的关系,一个网络设备的IP地址可以更换,但是MAC地址一般是固定不变的。ARP协议可以将IP地址解析成对应的MAC地址。当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标。
-
数据到达数据链路层
在找到对方的MAC地址后,就将数据发送到数据链路层传输。这时,客户端发送请求的阶段结束。
-
服务器接收数据
接收端的服务器在链路层接收到数据包,再层层向上直到应用层。这过程中包括在运输层通过TCP协议讲分段的数据包重新组成原来的HTTP请求报文。
-
服务器响应请求
服务接收到客户端发送的HTTP请求后,查找客户端请求的资源,并返回响应报文。
-
服务器返回相应文件
请求成功后,服务器会返回相应的HTML文件。接下来就到了页面的渲染阶段了。
2. 页面渲染
现代浏览器渲染页面的过程是这样的:解析HTML以
构建DOM树 –> 构建渲染树 –> 布局渲染树 –> 绘制渲染树。
DOM树是由HTML文件中的标签排列组成;渲染树是在DOM树中加入CSS或HTML中的style样式而形成,渲染树只包含需要显示在页面中的DOM元素,像<head>
元素或display属性值为none的元素都不在渲染树中。
在浏览器还没接收到完整的HTML文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送HTTP请求重复上述的步骤。在收到CSS文件后会对已经渲染的页面重新渲染,加入它们应有的样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会触发页面的重绘或重排。
发出完整的HTTP请求
DNS查询得到IP
如果输入的是域名,需要进行DNS解析成IP,大致流程:
- 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用host。
- 如果本地没有,就向dns域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的IP。
DNS解析流程
- 浏览器缓存
当用户通过浏览器访问某域名时,浏览器首先会在自己的缓存中查找是否有该域名对应的IP地址(若曾经访问过该域名且没有清空缓存便存在);
- 系统缓存
当浏览器缓存中无域名对应IP则会自动检查用户计算机系统Hosts文件DNS缓存是否有该域名对应IP;
- 路由器缓存
当浏览器及系统缓存中均无域名对应IP则进入路由器缓存中检查,以上三步均为客服端的DNS缓存;
- ISP(互联网服务提供商)DNS缓存
当在用户客服端查找不到域名对应IP地址,则将进入ISP DNS缓存中进行查询。比如你用的是电信的网络,则会进入电信的DNS缓存服务器中进行查找;
- 根域名服务器
当以上均未完成,则进入根服务器进行查询。全球仅有13台根域名服务器,1个主根域名服务器,其余12为辅根域名服务器。根域名收到请求后会查看区域文件记录,若无则将其管辖范围内顶级域名(如.com)服务器IP告诉本地DNS服务器;
- 顶级域名服务器
顶级域名服务器收到请求后查看区域文件记录,若无则将其管辖范围内主域名服务器的IP地址告诉本地DNS服务器;
- 权威域名服务器
主域名服务器接受到请求后查询自己的缓存,如果没有则进入下一级域名服务器进行查找,并重复该步骤直至找到正确纪录;
- 保存结果至缓存
本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端通过这个IP地址与web服务器建立链接。
TCP/IP
互联网的协议就是TCP/IP。注意:TCP/IP不是一个协议,而是一个协议族的统称。里面包括IP协议、IMCP协议、TCP协议等等等。
浏览器发出的http请求的本质上就是TCP/IP请求。TCP将http长报文划分为短报文,通过三次握手与服务端建立连接
,进行可靠传输。建立连接成功后,接下来就正式传输数据。然后,待到断开连接时,需要进行四次挥手
(因为是全双工的,所以需要四次挥手)。
SYN(synchronous建立联机) ACK(acknowledgement 确认)
FIN(finish结束) Sequence number(顺序号码)
GET和POST的区别
- POST更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录和书签中)
- POST发送的数据更大(GET有url长度限制)
- POST能发送更多的数据类型和编码方式(GET只能发送ASCII字符进行url编码)
- GET参数通过URL传递,POST放在Request body中
- GET在浏览器回退时是无害的,而POST会再次提交请求
- POST用于修改和写入数据,GET一般用于搜索排序和筛选之类的操作
- POST比GET慢
GET和POST的相同点
GET和POST是HTTP协议中的两种发送请求的方法。HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP连接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
计算机网络的三大体系结构
计算机网络体系可以大致分为一下三种,OSI七层模型、TCP/IP四层模型和五层模型。
- OSI七层模型:大而全,但是比较复杂、而且是先有了理论模型,没有实际应用。
- TCP/IP四层模型:是由实际应用发展总结出来的,从实质上讲,TCP/IP只有最上面三层,最下面一层没有什么具体内容,TCP/IP参考模型没有真正描述这一层的实现。
- 五层模型:五层模型只出现在计算机网络教学过程中,这是对七层模型和四层模型的一个折中,既简洁又能将概念阐述清楚。
七层网络体系结构各层的主要功能
- 应用层:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等。
- 表示层:主要负责数据格式的转换,如加密解密、转换翻译、压缩解压缩等。
- 会话层:负责在网络中的两节点之间建立、维持和终止通信,如服务器验证用户登录便是由会话层完成的。
- 运输层:有时也译为传输层,向主机进程提供通用的数据传输服务。该层主要有以下两种协议
- TCP:提供面向连接的、可靠的数据传输服务;
- UDP:提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性。
- 网络层:选择合适的路由和交换结点,确保数据及时传送。主要包括IP协议。
- 数据链路层:数据链路层通常简称为链路层。将网络层传下来的IP数据包组装成帧,并再相邻节点的链路上传送帧。
- 物理层:实现相邻节点间比特流的透明传输,尽可能屏蔽传输介质和通信手段的差异。
TCP和UDP区别
TCP | UDP | |
---|---|---|
是否连接 | 面向连接 | 无连接 |
是否可靠 | 可靠传输,使用流量控制和拥塞控制 | 不可靠传输,不适用流量控制和拥塞控制 |
是否有序 | 有序,消息在传输过程中可能会乱序,TCP会重新排序 | 无序 |
传输速度 | 慢 | 快 |
连接对象个数 | 只能一对一通信 | 支持一对一、一对多、多对一和多对多交互通信 |
传输方式 | 面向字节流 | 面向报文 |
首部开销 | 首部最小20字节,最大60字节 | 首部开销小,仅8字节 |
适用场景 | 适用于要求可靠传输的应用,如文件传输 | 适用于实时应用,如IP电话、视频会议、直播 |
HTTPS是什么?具体流程
HTTPS是在HTTP和TCP之间建立了一个安全层,HTTP与TCP通信的时候,必须先进过一个安全层,对数据包进行加密,然后将加密后的数据包传送给TCP,相应的TCP必须将数据包解密,才能传给上面的HTTP。
- 浏览器传输一个client_random和加密方法列表
- 服务器收到后,传给浏览器一个server_random、加密方法列表和数字证书(包含了公钥)
- 然后浏览器对数字证书进行合法验证,如果验证通过,则生成一个pre_random,然后用公钥加密传给服务器
- 服务器用client_random、server_random和pre_random,使用公钥加密生成secret,然后之后的传输使用这个secret作为秘钥来进行数据的加解密。
进程和线程的区别
线程是CPU调度程序执行的最小单位;进程是操作系统分配资源的最小单位。
做个简单的比喻:线程=车厢,进程=火车
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下,不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,线程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。-“互斥锁”(比如火车上的洗手间)
- 进程使用的内存地址可以限定使用量。-“信号量”(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)
进程间通信
- 管道
- 系统IPC
- 信号量
- 消息队列
- 共享内存
- 套接字(socket)
并行 和 并发 的区别
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』。
HTTP常用状态码及使用场景
- 1xx:表示目前是协议的中间状态,还需要后续请求
- 2xx:表示请求成功
- 3xx:表示重定向状态,需要重新请求
- 4xx:表示请求报文错误
- 5xx:服务器端错误
常用状态码:
- 101切换请求协议,从HTTP切换到WebSocket
- 200请求成功,有响应体
- 301永久重定向:会缓存
- 代表访问的地址的资源被永久移除了,以后都不应该访问这个地址,搜索引擎抓取的时候也会用新的地址替换这个老的,比如域名换了。可以在返回的响应的location首部去获取到返回的地址。
- 302临时重定向:不会缓存
- 表示这个资源只是暂时不能被访问了,但是之后过一段时间还是可以继续访问,一般是访问某个网站的资源需要权限时,会需要用户去登录,跳转到登录页面之后登录之后,还可以继续访问。
- 304协商缓存命中
- 400请求错误
- 403服务器禁止访问
- 404资源未找到
- 500服务器端错误
- 503服务器繁忙
HTTP缓存
HTTP缓存又分为强缓存和协商缓存:
- 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是 200。
- 协商缓存:会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是 304。
首先通过Cache-Control
验证强缓存是否可用,如果强缓存可用,那么直接读取缓存;如果不可以,那么进入协商缓存阶段,发起HTTP请求,服务器通过请求头中是否带上lf-Modified-Since
和If-None-Match
这些条件请求字段检查资源是否更新:
- 若资源更新,那么返回资源和200状态码
- 如果资源未更新,那么告诉浏览器直接使用缓存获取资源
HTTP常用的请求方式
HTTP/1.1规定如下请求方法:
- GET:通用获取数据
- HEAD:获取资源的元信息
- POST:提交数据
- PUT:修改数据
- DELETE:删除数据
- CONNECT:建立连接隧道,用于代理服务器
- OPTIONS:列出可对资源实行的请求方法,常用于跨域
- TRACE:追踪请求-响应的传输路径
TCP 三次握手 和 四次挥手
三次握手
为什么要进行三次握手
为了确认双方发送和接受能力。
三次握手主要流程
- 一开始双方处于
CLOSED
状态,然后服务端开始监听某个端口,进入LISTEN
状态 - 客户端主动发起连接,发送
SYN = 1
、seq = x
,然后自己变成SYN-SENT
- 服务端收到之后,返回
SYN = 1
、seq = y
和ACK = 1
、ack = x + 1
,自己变为SYN-REVD
- 客户端再次发送
ACK = 1
、seq = x + 1
、ACK = y + 1
,进入ESTABLISHED
为什么不是两次?
主要有三个原因:
-
防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。
在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。
客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。
此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。
-
三次握手才能让双方均确认自己和对方的发送和接收能力都正常。
第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;
第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。
总结:两次握手没有办法让服务器知道自己发送能力正常!
-
告知对方自己的初始序号值,并确认收到对方的初始序号值。
TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。
什么是 SYN洪泛攻击?如何防范?
SYN洪泛攻击属于 DOS 攻击的一种,它利用 TCP 协议缺陷,通过发送大量的半连接请求,耗费 CPU 和内存资源。
原理:
- 在三次握手过程中,服务器发送
[SYN/ACK]
包(第二个包)之后、收到客户端的[ACK]
包(第三个包)之前的 TCP 连接称为半连接(half-open connect),此时服务器处于SYN_RECV
(等待客户端响应)状态。如果接收到客户端的[ACK]
,则 TCP 连接成功,如果未接受到,则会不断重发请求直至成功。 - SYN 攻击的攻击者在短时间内伪造大量不存在的 IP 地址,向服务器不断地发送
[SYN]
包,服务器回复[SYN/ACK]
包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时。 - 这些伪造的
[SYN]
包将长时间占用未连接队列,影响了正常的 SYN,导致目标系统运行缓慢、网络堵塞甚至系统瘫痪。
**检测:**当在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。
防范:
- 通过防火墙、路由器等过滤网关防护。
- 通过加固 TCP/IP 协议栈防范,如增加最大半连接数,缩短超时时间。
- SYN cookies技术。SYN Cookies 是对 TCP 服务器端的三次握手做一些修改,专门用来防范 SYN 洪泛攻击的一种手段。
三次握手连接阶段,最后一次ACK包丢失,会发生什么?
服务端:
- 第三次的ACK在网络中丢失,那么服务端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便客户端重新发送ACK包。
- 如果重发指定次数之后,仍然未收到 客户端的ACK应答,那么一段时间后,服务端自动关闭这个连接。
客户端:
客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset,标示复位,用于异常的关闭连接)响应。此时,客户端知道第三次握手失败。
四次挥手
四次挥手主要流程
- 一开始都处于
ESTABLISH
状态,然后客户端发送FIN = 1
、seq = u
,状态变为FIN-WAIT-1
- 服务端收到后,发送
ACK = 1
、seq = v
、ack = u + 1
,然后进入CLOSE-WAIT状态 - 客户端收到之后,进入
FIN-WAIT-2
状态 - 过了一会儿,等数据处理完,服务端再次发送
FIN = 1
、ACK = 1
、seq =w
、ack = u + 1
,进入LAST-ACK
阶段 - 客户端收到FIN之后,发送
ACK = 1
、seq = u + 1
、ack = w + 1
,进入TIME-WAIT
(等待 2MSL),如果服务端没有重发请求,就表明ACK成功到达,客户端变为CLOSED状态,否则重发 ACK - 服务端收到后,进入
CLOSED
状态
为什么连接的时候是三次握手,关闭的时候却是四次握手?
服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段。
接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接。服务器的ACK和FIN一般都会分开发送,从而导致多了一次,因此一共需要四次挥手。
为什么要等待 2MSL?
MSL:Maximum Segement Lifetime
因为如果不等待的话,如果服务端还有很多数据包要给客户端发,但此时客户端端口被新应用占据,那么就会接收到无用的数据包,造成数据包混乱
- 1个MSL保证四次挥手中主动关闭方最后的ACK报文能最终到达对端
- 1个MSL保证,如果对端没有收到ACK,那么进行重传的FlIN报文能够到达
另外的解答:
主要有两个原因:
-
确保 ACK 报文能够到达服务端,从而使服务端正常关闭连接。
第四次挥手时,客户端第四次挥手的 ACK 报文不一定会到达服务端。服务端会超时重传 FIN/ACK 报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到 FIN/ACK 报文的确认,就无法正常断开连接。
MSL 是报文段在网络上存活的最长时间。客户端等待 2MSL 时间,即「客户端 ACK 报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」,就能够收到服务端重传的 FIN/ACK 报文,然后客户端重传一次 ACK 报文,并重新启动 2MSL 计时器。如此保证服务端能够正常关闭。
如果服务端重发的 FIN 没有成功地在 2MSL 时间里传给客户端,服务端则会继续超时重试直到断开连接。
-
防止已失效的连接请求报文段出现在之后的连接中。
TCP 要求在 2MSL 内不使用相同的序列号。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者即使收到这些过时的报文,也可以不处理它。
TCP协议如何保证可靠性?
TCP主要提供了检验和、序列号/确认应答、超时重传、滑动窗口、拥塞控制和 流量控制等方法实现了可靠性传输。
-
检验和:通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。
-
序列号/确认应答:
序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。
TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文,这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
-
滑动窗口:滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法正常处理的异常。
-
超时重传:超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。
-
拥塞控制:在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时,提高性能。
-
流量控制:如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的数据量。流量控制与TCP协议报头中的窗口大小有关。
详细讲一下拥塞控制?
TCP 一共使用了四种算法来实现拥塞控制:
- 慢开始 (slow-start);
- 拥塞避免 (congestion avoidance);
- 快速重传 (fast retransmit);
- 快速恢复 (fast recovery)。
发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。当cwndssthresh时,改用拥塞避免算法。
**慢开始:**不要一开始就发送大量的数据,由小到大逐渐增加拥塞窗口的大小。
**拥塞避免:**拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口按线性规律缓慢增长。
**快速重传:**我们可以剔除一些不必要的拥塞报文,提高网络吞吐量。比如接收方在收到一个失序的报文段后就立即发出重复确认,而不要等到自己发送数据时捎带确认。快重传规定:发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
**快速恢复:**主要是配合快速重传。当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞),但接下来并不执行慢开始算法,因为如果网络出现拥塞的话就不会收到好几个重复的确认,收到三个重复确认说明网络状况还可以。
详细讲一下TCP的滑动窗口?
在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包。这样一来,就会在等待确认应答包环节浪费时间。
为了避免这种情况,TCP引入了窗口概念。窗口大小指的是不需要等待确认应答包而可以继续发送数据包的最大值。
从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。
滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。
可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。
拆包和粘包
TODO
WebSocket 和 Ajax 的区别
Ajax即异步JavaScript和XML,是一种创建交互式网页的应用的网页开发技术
Websocket是HTML5的一种新协议,实现了浏览器和服务器的实时通信
生命周期不同:
- websocket是长连接,会话一直保持
- ajax发送接收之后就会断开
适用范围不同:
-
websocket 用于前后端实时交互数据
-
ajax非实时
发起人不同:
- WebSocket 服务器端和客户端相互推送
- ajax客户端发起
了解 WebSocket 吗?
WebSocket是长轮询(持久连接)。
短轮询:比如在一个电商场景,商品的库存可能会变化,所以需要及时反映给用户,所以客户端会不停的发请求,然后服务器端会不停的去查变化,不管变不变,都返回,这个是短轮询。
长轮询:表现为如果没有变,就不返回,而是等待变或者超时(一般是十几秒)才返回,如果没有返回,客户端也不需要一直发请求,所以减少了双方的压力。
HTTP如何实现长连接?在什么时候会超时?
通过在头部(请求和响应头)设置Connection: keep-alive
,来实现长连接。
HTTP/1.0协议支持,但默认关闭;从HTTP/1.1协议以后,连接默认都是长连接。
实际上HTTP没有长短连接,只有TCP有。TCP长连接可以复用一个TCP连接来发起多次HTTP请求,这样可以减少资源消耗,比如一次请求HTML,可能还需要请求后续的JS/CSS/图片等。
上图中的Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。对于客户端来说,如果服务器没有告诉客户端超时时间也没关系,服务端可能主动发起四次握手断开TCP连接,客户端能够知道该TCP连接已经无效;另外TCP还有心跳包来检测当前连接是否还活着,方法很多,避免浪费资源。
Cookie 和 Session
-
cookie数据存放在客户的浏览器上,session数据放在服务器上.
简单的说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上面,客户端每次请求服务器的时候会发送当前会话的session_id,服务器根据当前session_id判断相应的用户数据标志,以确定用户是否登录,或具有某种权限。
由于数据是存储在服务器上面,所以你不能伪造,但是如果你能够获取某个登录用户的session_id,用特殊的浏览器伪造该用户的请求也是能够成功的。
session_id是服务器和客户端链接时候随机分配的,一般来说是不会有重复,但如果有大量的并发请求,也不是没有重复的可能性。
Session是由应用服务器维持的一个服务器端的存储空间,用户在连接服务器时,会由服务器生成一个唯一的SessionID,用该SessionID 为标识符来存取服务器端的Session存储空间。而SessionID这一数据则是保存到客户端,用Cookie保存的,用户提交页面时,会将这一SessionID提交到服务器端,来存取Session数据。这一过程,是不用开发人员干预的。所以一旦客户端禁用Cookie,那么Session也会失效。
session是因为HTTP无状态而诞生的。
注意:session不因因为浏览器的关闭而删除。但是存有session ID的cookie的默认过期时间是会话级的。也就是说,用户关闭了浏览器,那么存储在客户端的session ID便会丢失,但是存储在服务器端的session数据并不会被立即删除。从客户端即浏览器看来,好像session被删除了一样,其实是因为丢失了session ID,所以找不到原来的session数据了。
-
cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
-
设置cookie时间可以使cookie过期;但是使用session-destory(),我们将会销毁会话。
-
session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
-
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie;Session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型。
注意:
- session很容易失效,用户体验很差;
- 虽然cookie不安全,但是可以加密;
- cookie也分为永久和暂时存在的;
- 浏览器 有禁止cookie功能,但一般用户都不会设置;
- 一定要设置失效时间,要不然浏览器关闭就消失了;
两者最大的区别在于生存周期,一个是IE启动到IE关闭(浏览器页面一关,session就消失了);一个是预先设置的生存周期,或永久的保存于本地的文件(cookie)。
Token
为什么要引入 Token?
Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
什么是 Token?
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
使用 Token的目的
为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
DDOS攻击
什么是DDOS攻击
分布式拒绝服务攻击(Distributed denial of service attack)向目标系统同时提出数量庞大的服务请求。
DDOS攻击方式
- 通过使网络过载来干扰甚至阻断正常的网络通讯;
- 通过向服务器提交大量请求,使服务器超负荷;
- 阻断某一用户访问服务器;
- 阻断某服务与特定系统或个人的通讯。
如何应对DDOS攻击
- 黑名单
- DDOS 清洗:对用户请求数据进行实时监控,及时发现DDOS攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。
- CDN加速:CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。CDN有别于镜像,因为它比镜像更智能,或者可以做这样一个比喻:CDN=更智能的镜像+缓存+流量导流。因而,CDN可以明显提高Internet网络中信息流动的效率。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。
- 高防服务器:高防服务器主要是指能独立硬防御50Gbps以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点。
XSS
TODO
“XSS是跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。”
XSS的危害
- 窃取网页浏览中的cookie值
- 劫持流量实现恶意跳转
防范手段
- 首先是过滤。对诸如
<script>
、<img>
、<a>
等标签进行过滤。 - 其次是编码。像一些常见的符号,如
<>
在输入的时候要对其进行转换编码,这样做浏览器是不会对该标签进行解释执行的,同时也不影响显示效果。 - 最后是限制。通过以上的案例我们不难发现xss攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。
CSRF攻击
CSRF(Cross-site request forgery,中文为跨站请求伪造)是一种利用网站可信用户的权限去执行未授权的命令的一种恶意攻击。通过伪装可信用户的请求来利用信任该用户的网站,这种攻击方式虽然不是很流行,但是却难以防范,其危害也不比其他安全漏洞小。
CSRF也称作one-click attack或者session riding,其简写有时候也会使用XSRF。
什么是CSRF?
简单点说,CSRF攻击就是 攻击者利用受害者的身份,以受害者的名义发送恶意请求。
与XSS(Cross-site scripting,跨站脚本攻击)不同的是,XSS的目的是获取用户的身份信息,攻击者窃取到的是用户的身份(session/cookie),而CSRF则是利用用户当前的身份去做一些未经过授权的操作。
CSRF攻击最早在2001年被发现,由于它的请求是从用户的IP地址发起的,因此在服务器上的web日志中可能无法检测到是否受到了CSRF攻击,正是由于它的这种隐蔽性,很长时间以来都没有被公开的报告出来,直到2007年才真正的被人们所重视。
CSRF有哪些危害
CSRF可以盗用受害者的身份,完成受害者在web浏览器有权限进行的任何操作,想想吧,能做的事情太多了。
- 以你的名义发送诈骗邮件,消息
- 用你的账号购买商品
- 用你的名义完成虚拟货币转账
- 泄露个人隐私
- …
产生原理以及利用方式
要完成一个CSRF攻击,必须具备以下几个条件:
- 受害者已经登录到了目标网站(你的网站)并且没有退出
- 受害者有意或者无意的访问了攻击者发布的页面或者链接地址
整个步骤大致是这个样子的:
- 用户小明在你的网站A上面登录了,A返回了一个session ID(使用cookie存储)
- 小明的浏览器保持着在A网站的登录状态,事实上几乎所有的网站都是这样做的,一般至少是用户关闭浏览器之前用户的会话是不会结束的
- 攻击者小强给小明发送了一个链接地址,小明打开了这个地址,查看了网页的内容
- 小明在打开这个地址的时候,这个页面已经自动的对网站A发送了一个请求,这时候因为A网站没有退出,因此只要请求的地址是A的就会携带A的cookie信息,也就是使用A与小明之间的会话
- 这时候A网站肯定是不知道这个请求其实是小强伪造的网页上发送的,而是误以为小明就是要这样操作,这样小强就可以随意的更改小明在A上的信息,以小明的身份在A网站上进行操作
利用方式
利用CSRF攻击,主要包含两种方式,一种是基于GET请求方式的利用,另一种是基于POST请求方式的利用。
GET请求利用
使用GET请求方式的利用是最简单的一种利用方式,其隐患的来源主要是由于在开发系统的时候没有按照HTTP动词的正确使用方式来使用造成的。对于GET请求来说,它所发起的请求应该是只读的,不允许对网站的任何内容进行修改。
但是事实上并不是如此,很多网站在开发的时候,研发人员错误的认为GET/POST的使用区别仅仅是在于发送请求的数据是在Body中还是在请求地址中,以及请求内容的大小不同。对于一些危险的操作比如删除文章,用户授权等允许使用GET方式发送请求,在请求参数中加上文章或者用户的ID,这样就造成了只要请求地址被调用,数据就会产生修改。
现在假设攻击者(用户ID=121)想将自己的身份添加为网站的管理员,他在网站A上面发了一个帖子,里面包含一张图片,其地址为http://a.com/user/grant_super_user/121
<img src="http://a.com/user/grant_super_user/121" />
设想管理员看到这个帖子的时候,这个图片肯定会自动加载显示的。于是在管理员不知情的情况下,一个赋予用户管理员权限的操作已经悄悄的以他的身份执行了。这时候攻击者121就获取到了网站的管理员权限。
POST请求利用
相对于GET方式的利用,POST方式的利用更加复杂一些,难度也大了一些。攻击者需要伪造一个能够自动提交的表单来发送POST请求。
<script>
$(function() {
$('#CSRF_forCSRFm').trigger('submit');
});
</script>
<form action="http://a.com/user/grant_super_user" id="CSRF_form" method="post">
<input name="uid" value="121" type="hidden">
</form>
只要想办法实现用户访问的时候自动提交表单就可以了。
防范原理
防范CSRF攻击,其实本质就是要求网站能够识别出哪些请求是非正常用户主动发起的。这就要求我们在请求中嵌入一些额外的授权数据,让网站服务器能够区分出这些未授权的请求,比如说在请求参数中添加一个字段,这个字段的值从登录用户的Cookie或者页面中获取的(这个字段的值必须对每个用户来说是随机的,不能有规律可循)。攻击者伪造请求的时候是无法获取页面中与登录用户有关的一个随机值或者用户当前cookie中的内容的,因此就可以避免这种攻击。
防范技术
Synchronizer token pattern
**令牌同步模式(Synchronizer token pattern,简称STP)**是在用户请求的页面中的所有表单中嵌入一个token,在服务端验证这个token的技术。token可以是任意的内容,但是一定要保证无法被攻击者猜测到或者查询到。攻击者在请求中无法使用正确的token,因此可以判断出未授权的请求。
简单实现STP:
首先在index.php中,创建一个表单,在表单中,我们将session中存储的token放入到隐藏域,这样,表单提交的时候token会随表单一起提交
<?php
$token = sha1(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form action="buy.php" method="post">
<input type="hidden" name="token" value="<?=$token; ?>" />
... 表单内容
</form>
在服务端校验请求参数的buy.php
中,对表单提交过来的token与session中存储的token进行比对,如果一致说明token是有效的
if ($_POST['token'] != $_SESSION['token']) {
// TOKEN无效
throw new Exception('Token无效,请求为伪造请求');
}
// TOKEN有效,表单内容处理
对于攻击者来说,在伪造请求的时候是无法获取到用户页面中的这个token
值的,因此就可以识别出其创建的伪造请求。
Cookie-to-Header Token
对于使用Js作为主要交互技术的网站,将CSRF的token写入到cookie中
Set-Cookie: CSRF-token=i8XNjC4b8KVok4uw5RftR38Wgp2BFwql; expires=Thu, 23-Jul-2015 10:25:33 GMT; Max-Age=31449600; Path=/
然后使用javascript读取token的值,在发送http请求的时候将其作为请求的header
X-CSRF-Token: i8XNjC4b8KVok4uw5RftR38Wgp2BFwql
最后服务器验证请求头中的token是否合法。
验证码
使用验证码可以杜绝CSRF攻击,但是这种方式要求每个请求都输入一个验证码,显然没有哪个网站愿意使用这种粗暴的方式,用户体验太差,用户会疯掉的。
DOM和BOM
其它
前端性能优化的方法?
- 减少http请求次数:CSS Sprites(雪碧图), JS、CSS源码压缩、图片大小控制合适;网页Gzip,CDN托管,data缓存 ,图片服务器。
- 使用模板:前端模板 JS+数据,减少由于HTML标签导致的带宽浪费。
- 减少AJAX请求:前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数。
- 用innerHTML代替DOM操作:减少DOM操作次数,优化javascript性能。
- 使用className:当需要设置的样式很多时设置className而不是直接操作style。
- 少用全局变量,缓存DOM节点查找的结果。
- 减少IO读取操作。
- 避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)。
- 图片预加载:将样式表放在顶部,将脚本放在底部加上时间戳。
- 避免在页面的主体布局中使用table:table要等其中的内容完全下载之后才会显示出来,显示比div+css布局慢。
SQL索引
TODO: SQL索引