CSS / HTML
1rem、1em、1vh、1px、1%各自代表的含义
px
px即像素,在前端开发中视口的水平方向和垂直方向是由很多小方格组成的,一个小方格就是一个像素,例如div尺寸是100 x 100,那么水平方向就占用100个小方格,垂直方向就占用100个小方格。 特点:不会随着视口大小的变化而变化,像素是一个固定的单位(绝对单位)。
%
百分比是前端开发中的一个动态单位,永远都是以当前元素的父元素作为参考进行计算,例如父元素宽高都是200px,设置子元素宽高是50%,那么子元素宽高就是100px。
特点:
- 子元素宽高是参考父元素宽度计算的
- 子元素padding/margin无论是水平还是垂直方向都是参考父元素宽度计算的
- 不能用百分比设置元素的border
结论: 百分比是一个动态的单位,会随着父元素宽高的变化而变化(相对单位)
em
em是前端开发中的一个动态单位,是一个相对于元素字体大小的单位,例如font-size: 12px,那么1em就等于12px。 特点:
- 当前元素设置了字体大小,那么就相对于当前元素的字体大小
- 当前元素没有设置字体大小,那么就相当于第一个设置字体大小的祖先元素的字体大小
- 如果当前元素和所有祖先元素都没有设置大小,那么就相当于浏览器默认的字体大小
结论: em是一个动态的单位,会随着参考元素字体大小的变化而变化(相对单位)
rem
rem就是root em,和em一样是前端开发中的一个动态单位,rem和em的区别在于, rem是一个相对于根元素字体大小的单位,例如根元素(html) font-size: 12px,那么1em就等于12px。 特点:
- 除了根元素以外,其它祖先元素的字体大小不会影响rem尺寸
- 如果根元素设置了字体大小,那么就相对于根元素的字体大小
- 如果根元素没有设置字体大小,那么就相对于浏览器默认的字体大小
结论: rem是一个动态的单位, 会随着根元素字体大小的变化而变化(相对单位)
vm/vh
vw(Viewport Width)和vh(Viewport Height)是前端开发中的一个动态单位,是一个相对于网页视口的单位。
系统会将视口的宽度和高度分为100份,1vw就占用视口宽度的百分之一,1vh就占用视口高度的百分之一。
vw和vh和百分比不同的是,百分比永远都是以父元素作为参考,而vw和vh永远都是以视口作为参考。
结论: vw/vh是一个动态的单位,会随着视口大小的变化而变化(相对单位)
vmin、vmax:
vmin: vw和vh中较小的那个;
vmax: vw和vh中较大的那个。
使用场景: 保证移动开发中屏幕旋转之后尺寸不变。
rem和em的区别
em是针对于父元素的font-size,rem针对于根(HTML)元素的font-size。
如何理解HTML语义化
通俗的说,语义化就是让正确的标签做正确的事,比如段落用p标签,标题用h标签。合理正确的使用语义化的标签来创建页面结构。
这样做的好处?
- 在没CSS样式的情况下,页面整体也会呈现很好的结构效果。
- 代码结构清晰,易于阅读。
- 利于开发和维护 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
- 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重。
说一下CSS选择器
总结排序:!important > 行内样式>ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性。
1.属性后面加!import 会覆盖页面内任何位置定义的元素样式
2.作为style属性写在元素内的样式
3.id选择器
4.类选择器
5.标签选择器
6.通配符选择器(*)
7.浏览器自定义或继承
同一级别:后写的会覆盖先写的
margin负值问题
对 margin 的 top left right bottom 设置负值,有何效果?
margin-top
和margin-left
负值,元素向上、向左移动。margin-right
负值,右侧元素左移,自身不受影响。margin-bottom
负值,下方元素上移,自身不受影响。
去除两个span之间的默认间距
- 将两个span的父级元素 font-size 设置为0。
- 然后再分别设置两个span的font-size即可解决。
只读(Readonly)与禁用(Disable)的区别
表面上可看到的区别就是当这两个词都设置为true时,都为禁用状态,当鼠标移上时使用disable的相关控件时鼠标出现禁用样式,并且不可做任何操作,而Readonly还可以获取文本框里的焦点。
Disable比readonly的使用范围比广,适用文本框、文本域、下拉框、button按钮、单选框…….而readonly只适用于input(text、passwork、textarea)。
Disable设置为true之后是不可以向后台提交数据的,此时可以选择改用readonly进行禁用,或者在提交数据时取消禁用。
使元素消失的方法
visibility:hidden、display:none、z-index=-1、opacity:0
1.opacity:0
//该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定了一些事件,如click事件也能触发
2.visibility:hidden
//该元素隐藏起来了,但不会改变页面布局,但是不会触发该元素已经绑定的事件
3.display:none
//把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素删掉
BFC理解和应用
什么是BFC?如何应用?
Block format contex,块级格式化上下文。 一块独立的渲染区域,内部元素的渲染不会影响到边界以外的元素。
形成 BFC 的常见条件
float:left、float:right
position:absolute、position:fixed
overflow:hidden、overflow:auto、overflow:scroll
display:flex、display:inline-block
BFC 常见应用
- 清除浮动:通过改变浮动元素的父元素的属性值,触发 BFC,以此来清除浮动。
- 阻止元素被浮动元素覆盖:一个正常文档流的块级元素可能被一个浮动元素覆盖,因此可以设置一个元素的 float、position、overflow 或者 display 值等方式触发 BFC,以阻止被浮动盒子覆盖。
- 阻止相邻元素的 margin 合并:属于同一个 BFC 的两个相邻块级子元素的上下 margin 会发生重叠,所以当两个相邻块级子元素分属于不同的 BFC 时可以阻止 margin 重叠。
回流和重绘是什么
回流:回流又称之为 「重排」,因元素的规模,尺寸,布局等改变,而需要重新构建页面,就会触发回流。
具体总结为:
- 页面初始渲染
- 添加、删除可见的 DOM 元素
- 改变元素位置,尺寸,内容
触发回流的属性:
- 盒子模型相关属性:width、height、display、border、border-width…
- 定位及浮动:position、left、right、top、bottom、float、padding、margin…
- 文字相关:text-align、overflow、font-weight、font-family、line-height,vertical-align、font-size、white-space…
重绘:元素需要更新属性,而这些属性只是影响到元素的外观,风格而不影响布局,就会触发重绘。
触发重绘的属性:
- color、border-style、border-radius、outline、visibility、background-color、text-decoration、background、background-image、box-shadow…
回流一定重绘,但是重绘不一定回流
如何减少回流和重绘
- 用 translate 代替 top
- 用 opacity 代替 visibility
- 预先定义好 className,然后统一修改 Dom 的 className
- 不要把 Dom 结点的属性值放在一个循环里面变成循环变量
- 让要操作的元素进行“离线处理”,处理完后一起更新
- 通过 fragment 批量修改DOM
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。
圣杯布局&&双飞翼布局
圣杯布局和双飞翼布局的目的:
- 三栏布局,中间一栏最先加载和渲染(内容最重要)
- 两侧内容固定,中间内容随着宽度自适应
- 一般用于 PC 网页
圣杯布局和双飞翼布局的技术总结:
- 使用 float 布局
- 圣杯布局-相对定位;双飞翼布局-无需定位
- 两侧使用 margin 负值,以便和中间内容横向重叠
- 防止中间内容被两侧覆盖,圣杯布局用 padding,双飞翼布局用 margin
圣杯布局
<style>
* {
margin: 0;
padding: 0;
}
header {
background-color: rgb(151, 69, 14);
text-align: center;
}
.center,
.left,
.right {
float: left;
}
.clearfix::after {
content: "";
display: block;
clear: both;
}
.wrapper {
padding: 0 100px;
}
.center {
background-color: rgb(238, 68, 96);
text-align: center;
width: 100%;
}
.left {
position: relative;
background-color: rgb(84, 210, 189);
text-align: center;
margin-left: -100%;
width: 100px;
right: 100px;
}
.right {
background-color: rgb(106, 39, 214);
text-align: center;
width: 100px;
margin-left: -100px;
position: relative;
left: 100px;
}
footer {
background-color: rgb(151, 69, 14);
text-align: center;
}
</style>
<body>
<header>头部</header>
<div class="clearfix wrapper">
<div class="center">主区域</div>
<div class="left">左区域</div>
<div class="right">右区域</div>
</div>
<footer>底部</footer>
</body>
双飞翼布局
<style>
* {
margin: 0;
padding: 0;
}
.header {
background-color: rgb(151, 69, 14);
text-align: center;
}
.wrapper{
width: 100%;
float: left;
}
.center {
background-color: rgb(238, 68, 96);
text-align: center;
margin: 0 100px;
}
.left {
background-color: rgb(84, 210, 189);
text-align: center;
float: left;
margin-left: -100%;
width: 100px;
}
.right {
background-color: rgb(106, 39, 214);
text-align: center;
float: left;
width: 100px;
margin-left: -100px;
}
.footer {
background-color: rgb(151, 69, 14);
text-align: center;
clear: both;
}
</style>
<body>
<div class="header">头部</div>
<div class="wrapper">
<div class="center">主区域</div>
</div>
<div class="left">左区域</div>
<div class="right">右区域</div>
<div class="footer">底部</div>
</body>
tips:上述代码中 margin-left: -100%
相对的是父元素的 content
宽度,即不包含 paddig
、 border
的宽度。
其实以上问题需要掌握 margin 负值问题 即可很好理解。
定位
absolute 和 relative 分别依据什么定位
- relative 依据自身定位
- absolute 依据最近一层的定位元素定位
- 找父元素或者祖先元素中最近的定位元素(absolute、relative、fixed),最终没有找到则依据 body 定位
居中对齐的实现方式
水平居中
- inline 元素:
text-align:center
- block 元素:
margin:auto
- absolute 元素:
left 50% + margin-left 负值
垂直居中
- inline 元素:
line-height 的值等于 height 的值
- absolute 元素:
top:50% + margin-top 负值
水平垂直居中多种实现方式
利用绝对定位,设置 left: 50%
和 top: 50%
现将子元素左上角移到父元素中心位置,然后再通过 translate
来调整子元素的中心点到父元素的中心。该方法可以不定宽高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
复制代码
利用绝对定位,子元素所有方向都为 0
,将 margin
设置为 auto
,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高。
.father {
position: relative;
}
.son {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
margin: auto;
height: 100px;
width: 100px;
}
复制代码
利用绝对定位,设置 left: 50%
和 top: 50%
现将子元素左上角移到父元素中心位置,然后再通过 margin-left
和 margin-top
以子元素自己的一半宽高进行负值赋值。该方法必须定宽高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 200px;
margin-left: -100px;
margin-top: -100px;
}
复制代码
利用 flex
,最经典最方便的一种了,不用解释,定不定宽高无所谓的。
.father {
display: flex;
justify-content: center;
align-items: center;
}
复制代码
其实还有很多方法,比如 display: grid
或 display: table-cell
来做,有兴趣点击下面这篇文章可以了解下: 面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高)。
opacity/visibility/display 区别对比
opacity 用来设置透明度
display 定义建立布局时元素生成的显示框类型
visibility 用来设置元素是否可见。
opacity、visibility、display 这三个属性分别取值 0、hidden、none 都能使元素在页面上看不见,但是他们在方方面面都还是有区别的。
display:none
- 浏览器不会生成属性为display: none;的元素。
- display: none;不占据空间,把元素隐藏起来,所以动态改变此属性时会引起重排(改变页面布局),可以理解成在页面中把该元素删除掉一样。
- display: none;不会被子孙继承,但是其子孙是不会显示的,毕竟都一起被隐藏了。
- transition无效。
visibility:hidden
- 元素会被隐藏,但是不会消失,依然占据空间,隐藏后不会改变html原有样式。
- visibility: hidden会被子孙继承,子孙也可以通过显示的设置visibility: visible;来反隐藏。
- visibility: hidden;不会触发该元素已经绑定的事件。
- visibility: hidden;动态修改此属性会引起重绘。
- transition无效。
opacity:0;filter:alpha(opacity=0-100;(考虑浏览器兼容性的问题,最好两个都写上)
- opacity:0;filter:alpha(opacity=0-100;只是透明度为100%,元素隐藏,依然占据空间,隐藏后不会改变html原有样式。
- opacity:0;filter:alpha(opacity=0-100;会被子元素继承,且子元素并不能通过opacity=1,进行反隐藏。
- opacity:0;filter:alpha(opacity=0-100;的元素依然能触发已经绑定的事件。
- transition有效。
什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
我对 Virtual DOM 的理解是,
首先对我们将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后我们将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
当页面的状态发生改变,我们需要对页面的 DOM 的结构进行调整的时候,我们首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
我认为 Virtual DOM 这种方法对于我们需要有大量的 DOM 操作的时候,能够很好的提高我们的操作效率,通过在操作前确定需要做的最小修改,尽可能的减少 DOM 操作带来的重绘和回流的影响。其实 Virtual DOM 并不一定比我们真实的操作 DOM 要快,这种方法的目的是为了提高我们开发时的可维护性,在任意的情况下,都能保证一个尽量小的性能消耗去进行操作。
CSS样式初始化
<!-- 引入初始化 -->
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
a {
text-decoration: none;
color: #666;
}
body {
background: #fff;
color: #666;
font-size: 14px;
}
input {
outline: none;
}
.clearfix::before,
.clearfix::after {
content: '';
display: block;
clear: both;
}
.clearfix {
*zoom: 1;
}
</style>
JS
JS数据类型
总共7种
基本数据类型:string/number/boolean/null/undefined/symbol(代表创建之后独一无二并且 不可变的数据类型)
引用数据类型:object(object/function/array)
区别:
-
声明变量时的存储分配。
基本数据类型存储在栈中,引用数据类型存储在堆中。
栈中存储引用数据类型的引用地址,堆中存储引用数据类型的实际值。
-
不同内存分配机制夜带来了不同的访问机制。
-
复制变量时的不同。
typeof、instanceof、类型转换
-
string、number、boolean、null、undefined、object(function、array)、symbol(ES10 BigInt)
-
typeof
主要用来判断数据类型 返回值有数值 number
布尔 boolean
字符串 string
数组 object
函数 function
对象 object
undefined undefined
null object (浏览器bug)
-
instanceof
判断该对象是谁的实例。可以正确判断对象的类型,不能判断基本数据类型。
内部运行机制,判断它的原型链上是否能找到这个类型的原型。
判断构造函数的prototype属性是否出现在对象的原型链中的任何位置。
数值 false
布尔 false
字符串 false
数组 true
函数 true
对象 true
-
null
表示空对象undefined
表示已在作用域中声明但未赋值的变量
null和undefined的区别及应用
number(null) => 0
number(undefined) => NaN
null表示一个值被定义了,但是这个值是空值
undefined表示变量声明了但未赋值
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
原型、原型链(高频)
原型: 对象中固有的__proto__
属性,该属性指向对象的prototype
原型属性。
原型链: 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是Object.prototype
所以这就是我们新建的对象为什么能够使用toString()
等方法的原因。
特点: JavaScript
对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
Promise是什么
promise是异步编程的一种解决方案。
从语法上讲,promise是一个对象,从它可以获取异步操作的消息;从本意来说,promise是承诺,承诺过一段时间会给你结果。
promise有3个状态:pending(等待态),fulfield(成功态),reject(失败态);状态一旦改变,就不会再变。创造promise后,它会立即执行。
闭包
闭包的实质是因为函数嵌套而形成的作用域链。
闭包的定义即:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
闭包就是能读取其它函数内部变量的函数。内层的作用域访问它外层函数作用域里的参数/变量/函数时,闭包就产生了。
用途:局部变量无法共享和长期保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长期保存变量又不会造成变量污染。
优点:可以避免变量被全局变量污染。
缺点:函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包。
解决方法:在退出函数之前,将不使用的局部变量全部删除。
缺点(详):
javascript中的垃圾回收(GC)规则是这样的:如果对象不再被引用,或者对象互相引用形成数据孤岛后且没有被孤岛之外的其他对象引用,那么这些对象将会被JS引擎的垃圾回收器回收;反之,这些对象一直会保存在内存中。
由于闭包会引用包含它的外层函数作用域里的变量/函数,因此会比其他非闭包形式的函数占用更多内存。当外层函数执行完毕退出函数调用栈(call stack)的时候,外层函数作用域里变量因为被引用着,可能并不会被JS引擎的垃圾回收器回收,因而会引起内存泄漏。过度使用闭包,会导致内存占用过多,甚至内存泄漏。
// 闭包的经典题,输出什么?
for(var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 3个3,首先,for 循环是同步代码,先执行三遍 for,i 变成了 3;
// 然后,再执行异步代码 setTimeout,这时候输出的 i,只能是 3 个 3 了
// 有什么办法输出 0 1 2:
// 第一种:
// 把var改成let,每个 let 和代码块结合起来形成块级作用域,
// 当 setTimeout() 打印时,会寻找最近的块级作用域中的 i,所以依次打印出 0 1 2
// 第二种:使用立即执行函数,创建一个独立的作用域
for(let i = 0; i < 3; i++) {
(function(i){
setTimeout(function() {
console.log(i);
}, 1000);
})(i)
}
EventLoop
JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.then、MutationObserver,宏任务的话就是setImmediate、setTimeout、setInterval。
事件冒泡、捕获(委托)
- 事件冒泡指在在一个对象上触发某类事件,如果此对象绑定了事件,就会触发事件,如果没有,就会向这个对象的父级对象传播,最终父级对象触发了事件。
- 事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
event.stopPropagation()
或者 ie下的方法event.cancelBubble = true;
// 阻止事件冒泡
原生ajax
ajax是一种异步通信的方法,从服务端获取数据,达到局部刷新页面的效果。 过程:
- 创建
XMLHttpRequest
对象; - 调用
open
方法传入三个参数 请求方式(GET/POST)、url、同步异步(true/false)
; - 监听
onreadystatechange
事件,当readystate
等于4时返回responseText
; - 调用send方法传递参数。
new 操作符具体干了什么呢?如何实现?
- 创建一个新的空对象;
- 将对象的原型指向构造函数的对象,
obj.__proto__ = Person.prototype
;- 让函数的
this
指向这个对象,执行构造函数(为这个新对象添加属性);- 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,返回这个引用类型的对象。
function Person(name, age) {
this.name = name
this.age = age
return 123 //返回值为值类型,则new操作返回新创建的对象
//return {a:1},若构造函数返回值为引用类型,则new操作直接返回该引用类型
}
const _new = function (constructor, ...args) {
const obj = {}
// obj.__proto__ = constructor.prototype
Object.setPrototypeOf(obj,constructor.prototype)
const res = constructor.call(obj, ...args)
return res instanceof Object ? res : obj
}
const obj = _new(Person, '小明', 18)
console.log(obj.name); // 小明
console.log(obj.age); // 18
深拷贝与浅拷贝
浅拷贝: 只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“浅拷贝”,换句话说,浅拷贝仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。
深拷贝: 创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。 JSON.parse、JSON.stringify()
call bind apply 的相同点及区别?
call()、apply()、bind()的第一个参数相同,它们的作用都是改变this的指向。
call()和apply()的区别就在于传给func的参数不同。call()在第一个参数之后的 后续所有参数就是传入该函数的值。apply() 只有两个参数,第一个是对象,第二个是数组,这个数组就是该函数的参数。
bind() 方法和前两者不同在于返回值不同,call/apply的返回值是func的执行结果,在改变this指向后会立即执行函数。而bind的返回值是func的拷贝,在改变this指向后不会立即执行函数,即需要自行调用得到的这个新函数。
js继承方式有哪些?
正则表达式
所有空白字符和非数字非字母的字符
防抖&&节流
节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次
。
防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行
!
使用场景:
- 节流:滚动加载更多、搜索框的搜索联想功能、高频点击、表单重复提交。
- 防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。
// 防抖函数
/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay = 200) {
let timer = null
return function (...args) {
if (timer) {
// 清除上一次的延时器
clearTimeout(timer)
timer = null
} else {
// 对第一次输入立即执行
fn.apply(this, args)
}
// 重新设置新的延时器
timer = setTimeout(() => {
// 修正this指向问题
fn.apply(this, args)
}, delay)
}
}
// 节流函数
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @param fn要被节流的函数
* @param delay规定的时间
*/
function throttle(fn, delay) {
let pre = 0
let timer = null
return function (...args) {
const now = Date.now()
// 如果时间差超过了规定时间间隔
if (now - pre > delay) {
pre = now
fn.apply(this, args) //this指向本匿名函数
} else {
// 如果在规定时间间隔内,则后续事件会直接清除
if (timer) {
clearTimeout(timer)
timer = null
}
// 最后一次事件会触发
timer = setTimeout(() => {
pre = now
fn.apply(this, args)
}, delay)
}
}
}
setTimeout、Promise、Async/Await 的区别
-
setTimeout
settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行。
-
Promise
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。
console.log('script start') let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end') }).then(function () { console.log('promise2') }) setTimeout(function(){ console.log('settimeout') }) console.log('script end') // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
-
async/await
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
async function async1(){ console.log('async1 start'); await async2(); console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start'); async1(); console.log('script end') // 输出顺序:script start->async1 start->async2->script end->async1 end
传送门 ☞ # JavaScript Promise 专题
实现AJAX请求
-
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
- 创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
function ajax(url) { return new Promise((resolve,reject) => { // 创建一个 XHR 对象 const xhr = new XMLHttpRequest() // 指定请求类型,请求URL,和是否异步 xhr.open('GET', url, true) xhr.onreadystatechange = funtion() { // 表明数据已就绪 if(xhr.readyState === 4) { if(xhr.status === 200){ // 回调 resolve(JSON.stringify(xhr.responseText)) } else{ reject('error') } } } // 发送定义好的请求 xhr.send(null) }) }
浏览器&&网络
localStorage sessionStorage cookies 有什么区别?
localStorage:以键值对的方式存储,存储时间没有限制,永久生效,除非自己删除。
sessionStorage:当页面关闭后被清理,与其他相比不能同源窗口共享,是会话级别的存储方式。
cookies:数据不能超过4k。同时因为每次http请求都会携带cookies,所有cookies只适合保存很小的数据,比如会话标识。
http状态码
1XX 服务器收到请求
2XX 请求成功
- 200 请求成功
- 204 服务器成功处理了请求,但没有返回任何内容。
3XX 重定向
- 301 资源(网页等)被永久转移到其它URL
- 304 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
4XX 客户端错误(表示请求可能出错,妨碍了服务器的处理)
- 404 服务器找不到请求的网页
- 401 用户没有权限(用户名,密码输入错误)
- 403 用户得到授权(401相反),但是访问被禁止
5XX 服务端错误(表示服务器在处理请求的时候发生了内部错误)
- 500 服务器遇到错误,无法完成请求
- 503 服务器目前无法使用(超载/停机维护)
浏览器从输入url到渲染页面,发生了什么?
- DNS域名解析:浏览器向DNS服务器发起请求,解析该url中的域名对应的ip地址;
- 建立TCP连接(TCP三次握手);
- 浏览器根据ip地址向服务器发起http请求;
- 服务器收到请求后,将经过后端的一些处理的html代码返还给浏览器;
- 关闭TCP连接:通过四次挥手释放TCP连接;
- 浏览器渲染:客户端(浏览器)解析HTML内容并渲染出来;
- 遇到script则暂停渲染,优先加载并执行js代码,完成后再继续,直到把一个完整的页面渲染完成。
浏览器渲染的主要流程是什么?
- 构建DOM树:词法分析然后解析成DOM树(dom tree),是由dom元素及属性节点组成,树的根是document对象。
- 构建CSS规则树:生成CSS规则树(CSS Rule Tree)。
- 构建render树:Web浏览器将DOM和CSSOM(CSS Object Model,CSS对象模型)结合,并构建出渲染树(render tree)。
- 布局(Layout):计算出每个节点在屏幕中的位置。
- 绘制(Painting):即遍历render树,并使用UI后端层绘制每个节点。
性能优化
性能优化方法:
- 让加载更快 减少资源体积:压缩代码 减少访问次数:合并代码,SSR 服务端渲染,缓存 使用更快的网络:CDN
- 让渲染更快 CSS 放在 head,JS 放在 body 最下面 尽早开始执行 JS,用 DOMContentLoaded 触发 懒加载(图片懒加载,上滑加载更多) 对 DOM 查询进行缓存 频繁 DOM 操作,合并到一起插入到 DOM 结构中 节流 throttle ,防抖 debounce
前端性能如何优化,一般从几个方面考虑?
- 原则:多使用内存、缓存或其他方法,减少 CPU 计算量、减少网络请求
- 方向:加载页面,页面渲染,页面操作流畅度
性能监控一般看哪些核心指标?
用户体验核心指标 | 定义 | 衡量指标 |
---|---|---|
白屏时间 | 页面开始有内容的时间,在没有内容之前是白屏 | FP 或 FCP |
首屏时间 | 可视区域内容已完全呈现的时间 | FSP |
可交互时间 | 用户第一次可以与页面交互的时间 | FCI |
可流畅交互时间 | 用户第一次可以持续与页面交互的时间 | TTI |
前端有哪些页面优化方法?
- 减少 HTTP请求数
- 从设计实现层面简化页面
- 合理设置 HTTP缓存
- 资源合并与压缩
- 合并 CSS图片,减少请求数的又一个好办法。
- 将外部脚本置底(将脚本内容在页面信息内容加载后再加载)
- 多图片网页使用图片懒加载。
- 在js中尽量减少闭包的使用
- 尽量合并css和js文件
- 尽量使用字体图标或者SVG图标,来代替传统的PNG等格式的图片
- 减少对DOM的操作
- 在JS中避免“嵌套循环”和 “死循环”
- 尽可能使用事件委托(事件代理)来处理事件绑定的操作
VUE
什么是渐进式框架
vue.js只是一个核心库,根据项目需要再添加vue-router、vuex,不断让项目壮大,这就是渐进式框架。
谈谈对MVVM的理解
传统组件,只是静态渲染,更新还要依靠于操作 DOM,Vue 数据驱动视图
当前端发展起来后,这时前端开发就暴露出了三个痛点问题:
- 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。
- 大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
- 当 Model 频繁发生变化,开发者需要主动更新到 View ;当用户的操作导致 View 发生变化,开发者同样需要将变化的数据同步到 Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。
MVVM 由 Model、View、ViewModel 三部分构成
Model 代表数据模型,也可以在 Model 中定义数据和业务逻辑; View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来; ViewModel 是一个同步 View 和 Model 的对象;
MVVM模式:不需要手动的操作 dom ,主要是实现数据双向绑定
描述 Vue 组件生命周期(有父子组件的情况,vue2版本)
vue组件生命周期:就是vue实例从创建到销毁的过程。
什么是钩子函数:在特定情况下被自动调用的函数。
生命周期的钩子函数:vue实例从创建到销毁的过程中被自动调用的函数。
系统自带的构造函数。
create阶段:vue实例被创建。 beforeCreate
: 最初调用触发,创建前,此时data和methods中的数据都还没有初始化,不能获得DOM节点。(没有data,没有 el:组件的根节点) created
:在这个阶段 vue 实例已经创建,可以获取 data 和 methods,仍然不能获取 DOM 元素。(有 data,没有 $el)
mount阶段: vue实例被挂载到真实DOM节点。 beforeMount
:已经编译好了最终模板, 但是还没有将最终的模板渲染到界面上,相关的 render 函数首次被调用。(有 data,没有 el)
update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染。 beforeUpdate
:数据已经更新了, 但是界面还没有更新,切勿使用它监听数据变化 updated
:数据已经更新了, 界面也更新了。
destroy阶段:vue实例被销毁。 beforeDestroy
:实例被销毁前,组件卸载前触发,此时可以做一些重置的操作,清理dom事件、定时器或者取消订阅操作 destroyed
:实例销毁之后调用,调用后,所有的事件监听器会被移除,所有的子实例已经被销毁。
什么时候用哪些生命周期
created
请求接口
mounted
dom操作的时候
vue父子组件生命周期的执行顺序
加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程 父beforeUpdate->父updated
销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
keep-alive
vue自带的一个组件,实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。
它提供了include与exclude两个属性,允许组件有条件地进行缓存。
include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用LRU (least recently used) 的策略置换缓存数据。
在 keep-alive 中,vue 新增了两个钩子函数
activated
:因为使用了 keep-alive 的组件会被缓存,所以 created, mounted 这种的钩子函数只会执行一次, 如果我们的子组件需要在每次加载的时候进行某些操作,可以使用 activated 钩子触发。
deactivated
:组件被移除时使用。
Vue 组件如何通讯
(1)父子组件间通信
第一种方法是子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
第二种是通过 ref 属性给子组件设置一个名字。父组件通过 parent 获得父组件,这样也可以实现通信。
第三种是使用 provider/inject,在父组件中通过 provider 提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provider 中的数据。
(2)兄弟组件间通信
第一种是使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
-
定义一个空的Vue实例
const eventBus = new Vue();
-
传递数据
eventBus.$emit(事件名,数据);
-
获取数据
eventBus.$on(事件名,data => {});
第二种是通过 refs 来获取到兄弟组件,也可以进行通信。
(3)任意组件之间
使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作。
Vue 为何是异步渲染,$nextTick 有何用
面试高频指数:⭐⭐⭐
- 异步渲染,$nextTick待 DOM 渲染完再回调;
- 页面渲染时会将 data 的修改做整合,多次 data 修改只做一次渲染,减少 DOM 操作次数,提高性能。
$nextTick 原理
面试高频指数:⭐⭐⭐⭐
Vue 是异步渲染的,data 改变之后,DOM 不会立即渲染,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新,经常我们会在DOM还未更新的时候,就使用了某个元素,这样是拿不到变更后的DOM的,为了确保能拿到更新后的DOM,就设置了nextTick方法。$nextTick 是将回调函数延迟在下一次 dom 更新后调用。
主要是为了处理数据动态变化后,DOM未及时更新的问题,用nextTick可以获取数据更新后最新DOM的变化。
v-show 和 v-if 的区别
面试高频指数:⭐⭐⭐⭐
v-show 是通过 CSS 的 display 属性控制来显示和隐藏,v-if 是组件真正的渲染和销毁,而不是显示和隐藏。
v-show 有更高的初始渲染开销,v-if 有更高的切换开销,频繁切换显示状态用 v-show,否则用 v-if。
vue中的虚拟DOM 和diff算法
提到虚拟DOM就需要提一下vue的就地复用策略
vue就地复用策略:Vue会尽可能的就地(同层级,同位置),对比虚拟dom,复用旧dom结构,进行差异化更新。
虚拟dom: 本质就是一个个保存节点信息、属性和内容的描述真实dom的 JS 对象。
虚拟Dom就是一个个描述真实dom结构的对象,为什么进行虚拟dom对比,因为真实的dom,比较复杂,拥有大量的无关属性,比较起来比较费时间和性能,所以利用虚拟dom进行对比,但是就算是虚拟的dom结构直接比也很麻烦,所以,vue中就提供了diff算法。
diff算法比较的规则
1.先比较根级元素,如果虚拟的dom元素不同,就直接删除旧dom节点,创建新新节点。
2.比较同级,在没有设置key属性的情况下,自动按下标进行差异性比较,然后更新渲染;在设置了key值的情况下,按相同key值进行比较,这样可以提高虚拟dom对比的效率,进而提高渲染性能。
注意点:这个key 必须是数子或者字符串,还必须是唯一的,如果有id就用id,没有用name也行,最后再考虑用index。
虚拟DOM 和 diff算法
diff算法:
策略1:
先同层级根元素比较,如果根元素变化,那么不考虑复用,整个dom树删除重建。
先同层级根元素比较,如果根元素不变,对比属性的变化更新,并考虑往下递归复用。
策略2:
对比同级兄弟元素时,默认按照下标进行对比复用。
对比同级兄弟元素时,如果指定了 key,就会 按照相同 key 的元素 来进行对比。
v-for 的key的说明
设置 和 不设置 key 有什么区别?
不设置 key, 默认同级兄弟元素按照下标进行比较。
设置了key,按照相同key的新旧元素比较。
key值要求是?
字符串或者数值,唯一不重复。
有 id 用 id, 有唯一值用唯一值,实在都没有,才用索引。
key的好处?
key的作用:提高虚拟DOM的对比复用性能。
以后:只要是写到列表渲染,都推荐加上 key 属性。且 key 推荐是设置成 id, 实在没有,就设置成 index。
为何 v-for 中要用 key
面试高频指数:⭐⭐⭐⭐⭐
-
key的作用是为了在diff算法执行时更快的找到对应的节点,
提高diff速度,更高效的更新虚拟DOM
;vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种是map映射,另一种是遍历查找。相比而言。map映射的速度更快。
-
为了在数据变化时强制更新组件,以避免
“就地复用”
带来的副作用。当 Vue.js 用
v-for
更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。
快速查找到节点,减少渲染次数,提升渲染性能,高效的更新虚拟DOM。
key
的作用主要是为了更高效的对比虚拟DOM中每个节点是否是相同节点。
为什么 v-for 和 v-if 不建议一起使用
面试高频指数:⭐⭐⭐⭐
当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费 。 这种场景建议使用 computed,先对数据进行过滤。
注意:3.x 版本中 v-if
总是优先于 v-for
生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
Vue 常见性能优化
面试高频指数:⭐⭐⭐⭐⭐
- 合理使用 v-show 和 v-if
- 合理使用 computed
- v-for 时要加 key,以及避免和 v-if 同时使用
- data 层级不要太深(因为深度监听一次性监听到底)
- 自定义事件、DOM 事件、定时器及时销毁
- 合理使用路由懒加载,异步组件
- 合理使用 keep-alive
- 图片懒加载
- 浏览器缓存
- 开启 gzip 压缩
- 使用 CDN
- 使用 SSR
组件中的data为什么是一个函数?
- 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
- 如果data是对象的话,对象属于引用类型,会影响到所有的实例。
所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
computed
计算属性,基于响应式依赖来进行缓存,有缓存,只有在相关的依赖发生改变时才会重新计算,data 不变不会重新计算;提高性能。
watch
监听器,用于监听和观察页面上的vue实例的变化。如果数据变化的同时需要进行异步操作,或者比较大的开销,就可以使用watch。
methods
给vue定义方法。只要发生重新渲染,methods调用总是会被执行。
computed 和 watch 有什么区别
watch 属性监听 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用。
computed 计算属性 属性的结果会被缓存,当computed
中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed
中的函数必须用return
返回最终的结果 computed
更高效,优先使用。
使用场景
computed
:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能 。
watch
:当一条数据影响多条数据的时候使用,例:搜索数据。
computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch
是监听已经存在且已挂载到 vm
上的数据,所以用 watch
同样可以监听 computed
计算属性的变化。(其它还有 data
、props
)
computed
具有缓存性,只有当依赖变化后,第一次访问 computed
属性,才会计算新的值,而 watch
则是当数据发生变化便会调用执行函数。
computed 怎么实现的
computed 本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的 getter,而不是用户定义的方法。
computed 内部实现了一个惰性的 watcher,在实例化的时候不会去求值,其内部通过 dirty 属性标记计算属性是否需要重新求值。当 computed 依赖的任一状态(不一定是 return 中的)发生变化,都会通知这个惰性watcher,让它把 dirty 属性设置为 true。所以,当再次读取这个计算属性的时候,就会重新去求值。
惰性watcher/计算属性在创建时是不会去求值的,是在使用的时候去求值的。
何时需要使用 keep-alive?
面试高频指数:⭐⭐
缓存组件,不需要重复渲染,如多个静态 tab 页的切换
优化性能
什么是作用域插槽?
面试高频指数:⭐
路由懒加载
整个网页默认是刚打开就去加载所有页面,路由懒加载就是只加载你当前点击的那个模块。
按需去加载路由对应的资源,提高首屏加载速度(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。
实现原理:将路由相关的组件,不再直接导入了,而是改写成异步组件的写法,只有当函数被调用的时候,才去加载对应的组件内容。
传统路由配置:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login/index.vue'
import Home from '@/views/home/home.vue'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
export default router
异步加载性能会优化很多,配置:component: () => import(......)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: () => import('@/views/login/index.vue') },
{ path: '/home', component: () => import('@/views/home/home.vue') }
]
export default router
算法
回溯法
// 模板
subSet(nums){
result = []
backtrack(start, curr){
把curr添加入result数组
for(let i = start; i<nums.length; i++){
把nums[i]加入curr数组
backtrack(i+1, curr)
把curr数组最后一个元素移除
}
}
backtrack(0, [])
return result
}
// 案例1 leetcode78.子集
var subsets = function (nums) {
let res = []
function backtrack(start, curr) {
res.push([...curr])
for (let i = start; i < nums.length; i++) {
curr.push(nums[i])
backtrack(i + 1, curr)
curr.pop()
}
}
backtrack(0, [])
return res
};
// 案例2 leetcode39.组合总和
var combinationSum = function (candidates, target) {
let res = []
function backtrack(remain, start, curr) {
if (remain < 0) return
// if (remain === 0) res.push([...curr])
if (remain === 0) res.push(curr.slice())
for (let i = start; i < candidates.length; i++) {
curr.push(candidates[i])
// remain = target-i
backtrack(remain - candidates[i], i, curr)
curr.pop()
}
}
backtrack(target, 0, [])
return res
};
反转链表
// 模板
while(curr !== null){
const temp = curr.next
curr.next = prev
prev = curr
curr = temp
}
快速排序
function quickSort(arr) {
if (arr.length <= 1) return arr
let pivotIndx = Math.floor(arr.length / 2)
let pivot = arr.splice(pivotIndx, 1)[0]
let left = []
let right = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...quickSort(left), pivot, ...quickSort(right)]
}
冒泡排序
arr = [1, 34, 25, 17, 68, 7]
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}
console.log(bubbleSort(arr));
生成区间内随机整数
Math.floor(Math.random() * (max - min + 1)) + min
构造二维数组
const matrix = new Array(n).fill(0).map(() => new Array(n).fill(0))
// 初始化为0
时间复杂度分析
二分查找 O(logn),只适用于有序顺序存储结构。
自带的排序API,O(nlogn)。
空间复杂度
数组:O(n)
二维数组:O(n^2)
十大排序
网络安全
对称加密
定义:有一个密钥,可以对一段内容加密,加密后只有用它才能看到原文。
如果通信双方各持有同一个密钥,且没有别人只带,那么这双方的通信安全可以保证,除非密钥被破解。
但是存在问题:这个密钥怎么让通信双方知道,同时不被别人知道。因为在服务器生成密钥传输给客户端的时候有可能被劫持。
这时候就需要非对称加密了。
非对称加密
定义:有两把密钥,一把叫公钥,一把叫私钥。用公钥加密的内容必须用私钥才能解开,用私钥加密的内容只有公钥能解开。
服务器把公钥明文传给客户端可能会被劫持,服务器向客户端传递的数据可能会被解析,只能保证客户端到服务器的通信安全。假如使用两对非对称密钥可以解决,但太过耗时。
非对称加密比较耗时,对称加密快很多。
所以 https 采用 非对称加密+对称加密,且非对称加密解密只使用一次就可以。
- 服务器用于非对称加密的公钥 A、私钥 AA
- 浏览器向服务器请求,服务器把公钥 A 明文发给浏览器
- 浏览器随机生成一个对称加密密钥 B,用公钥 A 加密后传给服务器
- 服务器用私钥 AA 解密得到密钥 B
- 这样双方都拥有对称密钥 B 了,且别人无法知道它
这样还是存在漏洞,例如中间人攻击
中间人攻击
- 服务器用于非对称公钥 A、私钥 AA,将公钥 A 明文发给浏览器
- 中间人劫持公钥 A,将自己的公钥 X替换数据包里面的 A 发给浏览器。当然,中间人有私钥 XX
- 浏览器随机生成一个对称加密密钥 B,用公钥 X 加密传回服务器
- 中间人劫持后用私钥 XX 解密得到密钥 B,再用公钥 A 加密后传给服务器。
- 服务器拿到后用私钥 AA 解密得到密钥 B
最后结果是:
- 服务器:公钥 A、私钥 AA、公钥 B
- 中间人:公钥 A、自己的公钥 X 私钥 XX、公钥 B
- 浏览器:中间人的公钥 X、自己生成的公钥 B
这样在服务器浏览器双方都不发现异常的情况下,中间人得到了密钥 B。
原因是:浏览器无法确定自己收到的公钥是不是网站自己的。需要给网站服务器颁发一个“身份证”,也就是 CA 证书。
数字证书
证书:证书持有者,证书持有者的公钥。
服务器把证书传输给浏览器,浏览器从证书里取公钥。
需要做的是,证书的传输过程中,如何防止被修改。使用数字签名。
把证书内容生成一份签名,比对证书内容和签名是否一致就能察觉是否被修改。
Https 获取密钥过程
【注】:HTTPS 必须在每次请求中都要先在 SSL/TLS 层进行握手传输密钥吗?
可以通过一个会话标识符 session ID(在 TLS 握手中生成),服务器可以保存会话的相关信息,在服务器和服务器都保存了 session id 的情况下,就可以完成一次快速握手。
SSL/STL 协议
SSL/TLS 协议的基本过程是这样的:
(1) 客户端向服务器端索要并验证公钥。 (2) 双方协商生成”对话密钥”。 (3) 双方采用”对话密钥”进行加密通信。
上面过程的前两步,又称为”握手阶段”(handshake)。
“握手阶段”涉及四次通信,需要注意的是,”握手阶段”的所有通信都是明文的。
编码
ASCII
来源:上个世纪 60 年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。
一个字节,ASCII 码一共规定了 128 个字符的编码(包括 32 个不能打印出来的控制符号),只占用了后 7 位,第一位统一用 0。
UNICODE
英语用 128 个符号编码就够了,但是用来表示其他语言,128 个符号是不够的。
如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
于是 unicode 的实现方式 utf-8 出现了。
UTF-8
UTF-8 是 Unicode 的实现方式之一,还包括 utf-16(两字节)和 utf-32(四字节),后两种基本不用
可变长度编码,使用1-4 个字节表示一个符号
- 对于单字节,第一位设为 0,后面 7 位是这个符号的 unicode 码
- 对于 n 字节,第一字节的前 n 位设为 1,第 n+1 位设为 0;后面自己的前两位设为 0;剩下其余二进制位全部为这个符号的 unicode 吗
注:utf-8 兼容 ascii 码
原码、补码、反码、移码
对于一个十六进制的数:0x,八位存储
- 原码:正数是其二进制本身;负数是符号位为 1,数值部分取绝对值的二进制。
- 反码:正数的反码和原码相同;负数是符号位为 1,其它位是原码取反。
- 补码:正数的补码和原码,反码相同;负数是反码未位加 1
- 移码:将符号位取反的补码(不区分正负)
编码 | 10810(sbyte) | -10810(sbyte) |
---|---|---|
原码 | 01101100 | 11101100 |
反码 | 01101100 | 10010011 |
补码 | 01101100 | 10010100 |
移码 | 11101100 | 00010100 |
来由:将符号位参与运算, 并且只保留加法的方法。
如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的.这也就是为何计算机内部不使用原码表示一个数.
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原= [10000010]原 = -2
为了解决原码做减法的问题, 出现了反码
发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在”0”这个特殊的数值上. 虽然人们理解上+0 和-0 是一样的, 但是 0 带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示 0.
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反+ [1111 1110]反= [1111 1111]反= [1000 0000]原= -0
于是补码的出现, 解决了 0 的符号以及两个编码的问题
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补+ [1111 1111]补= [0000 0000]补=[0000 0000]原
-1-127 的结果应该是-128, 在用补码运算的结果中, [1000 0000]补就是-128. 但是注意因为实际上是使用以前的-0 的补码来表示-128, 所以-128 并没有原码和反码表示.(对-128 的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的)
使用补码, 不仅仅修复了 0 的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么 8 位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127].
因为机器使用补码, 所以对于编程中常用到的 32 位 int 类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.
javascript 数字表示
JavaScript 使用 Number 类型表示数字(整数和浮点数),遵循 IEEE 754 标准 通过64 位来表示一个数字。
- 第 0 位:符号位,0 表示正数,1 表示负数(s)
- 第 1 位到第 11 位:储存指数部分(e)
- 第 12 位到第 63 位:储存小数部分(即有效数字)f
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个整数。
计算机无法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。这样运算就分成了两个部分:先按照 IEEE 754 转成相应的二进制,然后对阶运算
0.1 和 0.2 转换成二进制后会无限循环
0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
但是由于 IEEE 754 尾数位数限制,需要将后面多余的位截掉,在进制之间的转换中精度已经损失
由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失
按照上面两步运算(包括两步的精度损失),最后的结果是
0.01001100110011001100110011001100110011001100110011;
结果转换成十进制之后就是 0.30000000000000004,这样就有了前面的“秀”操作:0.1 + 0.2 != 0.3
精度损失可能出现在进制转化和对阶运算过程中
怎么解决精度问题?
-
将数字转成整数
-
BigInt
BigInt
是一种内置对象,它提供了一种方法来表示大于253 - 1
的整数。这原本是 Javascript 中可以用Number
表示的最大数字。BigInt
可以表示任意大的整数。可以用在一个整数字面量后面加
n
的方式定义一个BigInt
,如:10n
,或者调用函数BigInt()
。不需要加 new。使用
typeof
测试时,BigInt
对象返回 “bigint”
汉字编码
gb2312,标准字符集 ,6K+字
gbk 是 gb2312 的扩展规范,2W+字
现在统一用 utf-8