总结
# HTML语义化
- 增加代码可读性,让开发者更好地理解;
- 能让搜索引擎更容易读懂,有助于爬虫爬取更多有效信息,有利于SEO;
- 在没有CSS样式下,页面也能呈现出更好地内容结构、代码结构;
- 方便视力不好人员浏览网页,可以通过阅读屏幕的方式阅读。
# script标签中defer和async
由于script标签会阻塞HTML解析,只有下载好并执行完脚本之后才会继续解析HTML。如果某个脚本的网络请求时间太长,或者JS脚本执行时间过长,都会导致页面白屏,用户看不到内容,于是出现了defer 和 async两个属性。
async
: 解析HTML过程中进行脚本的异步下载,下载成功立即执行,有可能阻断HTML解析;
当浏览器遇到带有async属性的script时,请求该脚本的网络请求是异步的,不会阻塞浏览器解析HTML,一旦网络请求回来之后,如果此时HTML还没有解析完,浏览器会暂停解析,现在JS引擎执行代码,代码执行完毕之后再进行解析,如图所示:
但是,如果在JS脚本请求回来之前,HTML已经解析完毕了,那也就不会阻塞HTML解析啦,会立即执行JS,如图所示:
缺点:async属性是不可控的,因为该script标签请求到的网络资源依赖于网络传输的快慢,哪个脚本先获取到哪个就先执行;多个async脚本执行顺序不确定;因此如果在该异步JS脚本中获取某个DOM元素,有可能获取得到,也有可能获取不到。
defer
:完全不会阻碍HTML的解析,解析完成之后再按照顺序执行脚本。
当浏览器遇到带有defer属性的脚本时,获取该脚本的网络请求也是异步的,不会阻塞浏览器解析HTML,即使网络请求回来之后,如果此时HTML还没有解析完,浏览器也不会暂停解析并执行JS代码,而是等待HTML解析完毕再执行JS代码,如图所示:
优点:如果存在多个defer script标签,浏览器会保证它们按照在HTML中出现的顺序执行,不会破坏JS脚本之前的依赖。、
使用场景:
- 推荐如果可以使用async,则优先使用async,然后是defer,最后才是不设置任何属性;
- 如果脚本是模块化的,并且不依赖其他任何脚本,可以使用async;
- 如果脚本依赖于另外一个脚本,推荐使用defer;
- 如果脚本很小,并且依赖于异步脚本,可以使用内联脚本,不用使用任何属性。
# url全过程
# 1. url解析
浏览器对url解析出协议、主机、端口、路径等信息,构造出一个HTTP请求。
# 1.1 HTTP请求
HTTP请求包含三个部分:请求行、请求头和请求体。
- 请求行包括:请求方法、请求路径、所用的协议版本
- 请求头:键值对的信息
- 请求体:请求的主体信息
请求方法包括:GET、POST、HEAD、PUT、TRACE、DELETE、OPTIONS
关于状态码:HTTP常见状态码 (opens new window)
关于HTTPS协议:采用对称加密和非对称加密两种方式结合,客户端第一次请求的时候,服务端将证书的公钥传给客户端,客户端验证证书的合法性后,客户端产生随机数,并采用证书的公钥加密发回去,服务端采用自己的私钥解出来,随后用解出来的密钥进行对称加密通信。
- 为什么用非对称加密传输密钥:非对称加密只作用在证书验证阶段,防止中间人截获;
什么是中间人攻击?简单来说就是,中间人截获客户端发送的消息,并返回给客户端中间人证书,然后获取到客户端的密钥;接着中间人再利用此信息和服务端进行通信,可以通过密钥得到传回给客户端的消息。所以通过证书来验证合法性防止中间人攻击。
- 为什么用对称加密进行内容传输:非对称加密的解密效率非常低,消息传输交互大,需要提高效率;而且私钥只保存在服务端,一对公私钥只能实现单向的加解密,服务端又不是只给你一个人服务。
关于HTTP协议发展史:
HTTP1.0:
- 浏览器与服务器只保持短暂的链接,每次请求都需要建立一个TCP连接,服务器完成请求处理后立刻断开TCP连接;
- 出现的问题:当网页中包含多个图片链接、JS文件、CSS文件时,会频繁进行TCP连接与断开;
- 缺点:连接无法复用,浏览器对于同一个域名,同时只能有4个连接,超过后续请求会被阻塞;
HTTP1.1:
- 增加管线化技术,允许客户端不用等到服务器响应就能发送下一个请求;目的是为了在一次TCP连接上可以并发多个请求,来提高网络利用率;
- 缺点:服务器必须按照请求的顺序来响应,即后续请求的响应必须等到第一个响应发送之后才能发生(HTTP队头阻塞)
- HTTP对头阻塞解决:并发链接;域名分片;
HTTP2.0:
- 增加了一层二进制分帧层,消息可以分为多帧,利用一个TCP连接中可以用多个帧流,但是仍然无法解决TCP队头阻塞。
HTTP1.0 vs HTTP1.1:
- 支持持久连接,一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟;默认开启Connection:keep-alive;
- 允许客户端不必等待上一次请求结果返回,就可以发出下一次请求;
- 缓存处理,增加ETag/If-None-Match字段;
- 带宽优化以及网络链接优化,即206部分资源请求
HTTP1.1 vs HTTP2.0
- 对于请求头的压缩:客户端与服务端之间建立哈希表,只传索引;对于整数和字符串进行哈夫曼编码;
- 多路复用;
- 二进制分帧;
- 服务器推送(埋坑:与preload、prefetch有关系嘛?)
# 1.2 强缓存与协商缓存
根据构造的HTTP请求,浏览器会判断该请求信息是否在浏览器端存在缓存,从而加快资源获取速度,提升用户体验,减少网络传输,缓解服务端压力。
哪么,浏览器怎么判断是否存在缓存呢?首先需要说明的是浏览器存在两者HTTP缓存:强缓存和协商缓存。
关于强缓存,不需要发送请求到服务端,直接读取浏览器本地缓存,HTTP状态码是200;强缓存是由Expires、Cache-Control和Pragma 3个Header属性共同来控制。
Expires
:浏览器在发起请求时,会判断系统时间和Expires
值进行比较,如果系统时间超过Expires的值,则缓存失效。由于系统时间有可能和服务器时间不一致,会有缓存有效期不准的问题,优先级最低。
Cache-Control
:HTTP/1.1中新增的属性,常见的属性值有:
- max-age:单位为秒,距离发起的时间的秒数,超过间隔的秒数缓存失效
- no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
- no-store:禁止是同缓存,包括协商缓存
- private:专用与个人的缓存,中间代理、CDN等不能缓存此响应
- public:响应可以被中间代理、CDN缓存
- must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证
Pragma
:只有no-cache属性值,不使用强缓存,需要与服务器验证缓存是否新鲜,优先级最高
关于协商缓存,当浏览器的强制缓存失效或者请求头中设置了no-cache,并且在请求头中设置了If-Modified-Since或者If-None-Match的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,返回304状态码,加载浏览器缓存,并且响应头会设置Last-Modified或者ETag属性。
Etag/If-None-Match
:是一串hash值,代表的是一个资源的标识符,当服务端的文件变化的时候,它的hash码会随之变化,通过请求头中的If-None-Match和当前文件的hash值进行比较,如果相等则表示命中协商缓存。Etag有强弱校验之分,如果是弱校验,只有服务器上的文件差异(根据Etag计算方式来决定)达到能够触发hash值后缀变化的时候,才会真正地请求资源,否则返回304并加载浏览器缓存。
Last-Modified/If-Modified-Since
:该值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到Last-Modified响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的Last-Modified的时间,并放到If-Modified-Since请求头属性中,服务端根据文件最后一次修改时间和If-Modified-SInce的值进行比较,如果相等返回304,并加载浏览器缓存。
Etag/If-None-Match
的出现解决了Last-Modified/If-Modified-Since
解决不了的问题:
- 如果文件的修改频率过快,
Last-Modified/If-Modified-Since
会错误地返回304,因为来不及不对服务器端修改的时间; - 如果文件被修改了,但是内容没有任何改变时,
Last-Modified/If-Modified-Since
会错误返回304。
# 2. DNS域名解析
DNS域名解析分为递归查询和迭代查询两种方式。解析流程是:先查看本地hosts文件是否有映射,然后去查本地DNS服务器,如果没有委托本地服务器去根DNS、顶级域名服务器、再到权威域名服务器查询。可以通过DNS缓存、DNS负载均衡、DNS预解析技术优化。
# 3. TCP连接
在经过DNS域名解析之后,浏览器拿到了目的IP地址,然后经过传输层的TCP协议,将请求报文进行TCP头封装,分成适合TCP传输的TCP段,TCP报文中包含与服务端连接的状态信息、端口号,源IP和目的IP地址等,在进行网络层IP头封装和网络接口层的MAC头封装,进行与服务器连接。通过TCP三次握手建立可靠连接,从而进行后续的资源请求。
为什么要进行三次握手呢?因为TCP为了建立可靠连接,需要保证客户端和服务端接收和发送能力。第一次握手可以确定客户端的发送能力,第二次握手来确定服务端的发送能力和接收能力,第三次握手才可以确定客户端的接收能力,从而减少丢包的现象。
如果只进行两次握手的话,第一次握手之后,第二次握手的请求在网络传输时间过长,从而客户端重新进行握手建立连接之后,上一次的握手响应被客户端接收到,会导致资源请求混乱。
# 4. HTTP请求
# 5. 服务器处理请求并返回HTTP报文
# 6. 浏览器渲染页面
根据HTTP返回的报文进行页面的渲染,渲染过程可以参考:浏览器渲染 (opens new window)
注意页面渲染过程中JS阻塞问题,还有关于重绘和重排的问题及优化。
关于页面渲染优化:
- HTML文档结构层次尽量少,最好不深于6层
- 脚本尽量后放,或者异步执行
- 在JS脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;
- 动画尽量使用绝对定位,或固定定位的元素上;或者是同tranform。
# 7. TCP四次挥手
客户端请求断开连接时,有可能服务端资源还没有传输完毕。
参考链接;
# 盒模型
- content-box:盒子的width和height 不包含padding和border
- border-box:盒子的width和height 包含padding和border
# CSS优先级
!important > style内联样式 > id > 类选择器、属性选择器、伪类选择器 > 标签选择器、伪元素
# 重排和重绘
- 重排:影响了元素的位置或者尺寸,浏览器需要重新计算元素在视口内的几何数学
- 重绘:样式改变并没有影响到元素的尺寸和位置。
优化:
- 样式集中改变;
- 批量操作DOM;
- 脱离文档流;
- 开启GPU加速,不在主线程,在合成器线程渲染。
# BFC
以下情况会创建BFC:
- float浮动定位
- 绝对定位absolute、fixed
- overflow不为visible
参考链接:
# js基础类型
- BigInt:产生任意大的整数
- Symbol:代表独一无二的值,可以用来定义对象的唯一属性名
# instanceof
用来判断对象类型,不能判断基本类型;判断左边的对象是否为右边的实例对象,即判断左原型链上是否存在右边的原型上。
function myInstanceof(left,right) {
let leftVal = left.getPrototypeof(left);
const rightVal = right.prototype;
while(leftVal !== null) {
if(leftVal === rightVal) {
return true;
}
leftVal = leftVal.getPrototypeof(leftVal);
}
return false;
}
2
3
4
5
6
7
8
9
10
11
此外,Obeject.prototype.toString.call()
可以判断所有原始数据类型。
扩展:如果判断变量是否为数组:
Array.isArray(arr)
arr.__proto__ === Array.prototype
arr instanceof Array
Object.prototype.toString.call(arr)
2
3
4
# 手写深拷贝
function deepClone(obj = {}, map = new Map()) {
if(typeof obj !== "object") {
return obj;
}
if(map.get(obj)) {
return map.get(obj);
}
// 初始化返回结果
let result = {};
// || 后面方式Array的prototype被重写
if(obj instanceof Array || Object.prototype.toString.call(obj) === "[object Array]") {
result = [];
}
// 防止循环引用
map.set(obj, result);
for(const key in obj) {
// 保证key不是原型属性
if(obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map)
}
}
//返回结果
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 0.1 + 0.2 !== 0.3
因为JS在做数字计算的时候,0.1和0.2都会被转为二进制后无限循环,js采用的IEEE754二进制浮点计算,最大可以存储53位有效数字,后面的都会被截取,导致精度丢失。
解决方法:
- 采用BigInt转成大数计算
- 使用Number.EPSILON误差范围,它是一个可以接受的最小误差范围,一般来说是Math.pow(2,-52)
# 原型与原型链
每个JavaScript对象在创建的时候,就会自动关联上另外一个对象,也就是原型,可以继承原型上的一些属性,即prototype对象;原型链则是创建的对象通过原型连接起来的一个结构。
两个概念:
JS分为函数对象和普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性;
Object、Function都是JS内置的函数,类似的还有常见的Array、RegExp、Data、Boolean、Number、String
属性__proto__是一个对象,它有两个属性,constuctor和__proto__;
原型对象prototype有一个默认的constructor属性,用于记录实例是哪个构造函数创建的。
两个准则:
- 原型对象(Person.prototype)的constuctor指向构造函数本身;
Person.prototype.constructor == Person
- 实例(即person01)的__proto__和原型对象指向同一个地方;person01.proto == Person.prototype
# 作用域与作用域链
# 作用域
JS采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6之前没有块级作用域,只有全局作用域和函数作用域。
全局作用域:
- 最外层函数、最外层函数外面定义的变量;
- 所有未定义直接赋值的变量自动声明为拥有全局作用域;
- 所有window对象的属性拥有全局作用域。
缺点:如果定义的变量都没有用函数包括,全都会在全局作用域下,会污染全局命名空间,容易引起命名冲突。
函数作用域:声明在函数内部的变量,和全局作用域相反,函数作用域一般只在固定的代码片段内可访问到,比如函数内部。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
块级作用域:通过const和let声明,创建方式:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部。
for循环有一个特别之处:设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
# 作用域链
自由变量:当前作用域没有定义的变量;它的值需要向父级作用域寻找。具体一点来说,要到创建这个函数的那个域去找,注意是创建,而不是调用,这就是所谓的静态作用域。
var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
(function() {
f() //10,而不是20
})()
}
show(fn)
2
3
4
5
6
7
8
9
10
11
再来看一个例子:
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() //bar()
2
3
4
5
6
7
8
9
10
11
fn()返回的是bar函数,赋值给x;执行x(),即执行bar函数代码。取b值,直接在fn作用域取出;去a值,在fn作用域取不到3,从创建fn的那个作用域查找,拿到10.
# 作用域与执行上下文
JavaScript属于解释性语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
- 解释阶段:
- 执行阶段:
在解释阶段就已经确定了作用域规则,即函数定义的时候;而执行上下文是函数执行之前创建的。在执行上下文期间,this的指向是执行时确认的。
因此一个作用域下可能包含多个上下文环境;有可能从来就没有上下文环境;有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。总之,同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
# 立即执行函数
立即执行函数常用于第三方库,可以用来隔离变量作用域,因为第三方库都会存在大量的变量和函数,为了避免变量污染。立即执行函数的代码,外部是无法访问的。因为在ES5的时候是没有块级作用域,立即执行函数它类似于函数声明,但是由于被包含在括号中,所以会被解释为函数表达式,紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式,就像是模拟的块级作用域。
此外,它可以防止变量定义外泄,因为不存在对这个匿名函数的引用,函数执行完毕之后,它的作用域链就可以被销毁。
常见的一个问题:
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
liList[i].onclick = function(){
alert(i) // 为什么 alert 出来的总是 6,而不是 0、1、2、3、4、5
}
}
2
3
4
5
6
用户的点击一定是在for运行完了之后才点击的,此时候i为6;因为i是一个全局作用域的。那怎么解决呢?当然是给每个循环创建一个独立的作用域:
- 用立即执行函数给每个li创建一个独立作用域即可
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
for(var i=0;i<6;i++) {
(function(ii) {
liList[ii].onclick = function(){
alert(ii)
}
})(i)
}
2
3
4
5
6
7
8
9
- 直接用let为每次循环创建子作用域
var liList = ul.getElementsByTagName('li')
for(let i=0; i<6; i++){
liList[i].onclick = function(){
alert(i)
}
}
2
3
4
5
6
# 观察者模式(发布-订阅模式)
思想:被观察对象(subject)维护一组观察者(observe),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。观察者模式一般需要实现:
- subscribe():接收一个观察者observe对象,使其订阅自己
- unsubscribe():接收一个观察者observe对象,使其取消订阅自己;
- fire():触发事件,通知到所有观察者。
function Subject(){
this.observers = [];
}
Subject.prototype = {
subscribe: function(observer) {
this.observers.push(observer);
},
unsubscribe: function(observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
fire: function(){
this.oberservers.forEach(observer => {
observer.call()
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue中的数据双向绑定就是通过观察者模式以及数据劫持实现的。首先通过ES5提供的Object.defineProperty()方法来劫持并监听各属性的getter和setter,并在监听的属性发生变动时候通知订阅者,是否需要更新,若更新的话就会执行对应的更新函数。
Vue3新特性:支持树摇;新增了setup生命周期函数,在beforeCreate之前执行;Proxy进行数据劫持;
比如说表单的v-model可以实现数据的双向绑定,<input v-model="xxx">
,原理是`<input :value='xxx' @input='xxx=$event.target.value'>