Simonzhangs' blog Simonzhangs' blog
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Simonzhangs

前端学习探索者
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • JS设计模式总结
  • 《Vue》
  • 《React》
  • 《TypeScript 从零实现 axios》
  • TypeScript
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • apple music
  • extension
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • HTML

  • CSS

    • CSS教程和技巧收藏
    • flex布局语法
    • flex布局案例-基础
    • flex布局案例-骰子
    • flex布局案例-圣杯布局
    • flex布局案例-网格布局
    • flex布局案例-输入框布局
    • CSS3之transition过渡
    • CSS3之animation动画
    • 「布局技巧」图片未加载前自动撑开元素高度
    • 文字在一行或多行时超出显示省略号
    • 从box-sizing属性入手,了解盒子模型
    • 水平垂直居中的几种方式-案例
    • 如何根据系统主题自动响应CSS深色模式
    • 「css技巧」使用hover和attr()定制悬浮提示
    • CSS-function汇总
    • CSS基础

      • CSS实现垂直居中
      • position种类以及对应效果和用法
      • display常见属性
      • CSS选择器的优先级
      • 总结
        • HTML语义化
        • script标签中defer和async
        • url全过程
          • 1. url解析
          • 1.1 HTTP请求
          • 1.2 强缓存与协商缓存
          • 2. DNS域名解析
          • 3. TCP连接
          • 4. HTTP请求
          • 5. 服务器处理请求并返回HTTP报文
          • 6. 浏览器渲染页面
          • 7. TCP四次挥手
        • 盒模型
        • CSS优先级
        • 重排和重绘
        • BFC
        • js基础类型
        • instanceof
        • 手写深拷贝
        • 0.1 + 0.2 !== 0.3
        • 原型与原型链
        • 作用域与作用域链
          • 作用域
          • 作用域链
        • 作用域与执行上下文
        • 立即执行函数
        • 观察者模式(发布-订阅模式)
  • JavaScript文章

  • 学习笔记

  • 前端
  • CSS
  • CSS基础
simonzhangs
2022-04-26
目录

总结

# HTML语义化

  • 增加代码可读性,让开发者更好地理解;
  • 能让搜索引擎更容易读懂,有助于爬虫爬取更多有效信息,有利于SEO;
  • 在没有CSS样式下,页面也能呈现出更好地内容结构、代码结构;
  • 方便视力不好人员浏览网页,可以通过阅读屏幕的方式阅读。

# script标签中defer和async

由于script标签会阻塞HTML解析,只有下载好并执行完脚本之后才会继续解析HTML。如果某个脚本的网络请求时间太长,或者JS脚本执行时间过长,都会导致页面白屏,用户看不到内容,于是出现了defer 和 async两个属性。

  • async: 解析HTML过程中进行脚本的异步下载,下载成功立即执行,有可能阻断HTML解析;

当浏览器遇到带有async属性的script时,请求该脚本的网络请求是异步的,不会阻塞浏览器解析HTML,一旦网络请求回来之后,如果此时HTML还没有解析完,浏览器会暂停解析,现在JS引擎执行代码,代码执行完毕之后再进行解析,如图所示:

async script

但是,如果在JS脚本请求回来之前,HTML已经解析完毕了,那也就不会阻塞HTML解析啦,会立即执行JS,如图所示:

async script

缺点:async属性是不可控的,因为该script标签请求到的网络资源依赖于网络传输的快慢,哪个脚本先获取到哪个就先执行;多个async脚本执行顺序不确定;因此如果在该异步JS脚本中获取某个DOM元素,有可能获取得到,也有可能获取不到。

  • defer:完全不会阻碍HTML的解析,解析完成之后再按照顺序执行脚本。

当浏览器遇到带有defer属性的脚本时,获取该脚本的网络请求也是异步的,不会阻塞浏览器解析HTML,即使网络请求回来之后,如果此时HTML还没有解析完,浏览器也不会暂停解析并执行JS代码,而是等待HTML解析完毕再执行JS代码,如图所示:

defer script

优点:如果存在多个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协议:采用对称加密和非对称加密两种方式结合,客户端第一次请求的时候,服务端将证书的公钥传给客户端,客户端验证证书的合法性后,客户端产生随机数,并采用证书的公钥加密发回去,服务端采用自己的私钥解出来,随后用解出来的密钥进行对称加密通信。

  1. 为什么用非对称加密传输密钥:非对称加密只作用在证书验证阶段,防止中间人截获;

什么是中间人攻击?简单来说就是,中间人截获客户端发送的消息,并返回给客户端中间人证书,然后获取到客户端的密钥;接着中间人再利用此信息和服务端进行通信,可以通过密钥得到传回给客户端的消息。所以通过证书来验证合法性防止中间人攻击。

  1. 为什么用对称加密进行内容传输:非对称加密的解密效率非常低,消息传输交互大,需要提高效率;而且私钥只保存在服务端,一对公私钥只能实现单向的加解密,服务端又不是只给你一个人服务。

关于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四次挥手

客户端请求断开连接时,有可能服务端资源还没有传输完毕。


参考链接;

  1. async vs defer attributes (opens new window)
  2. 阿里面试官的”说一下从url输入到返回请求的过程“问的难度就是不一样! (opens new window)

# 盒模型

  • 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

参考链接:

  1. 你真的了解回流和重绘吗 (opens new window)

# 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;
}
1
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)
1
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;
}
1
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属性,用于记录实例是哪个构造函数创建的。

两个准则:

  1. 原型对象(Person.prototype)的constuctor指向构造函数本身;Person.prototype.constructor == Person
  2. 实例(即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)
1
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()
1
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
  }
}
1
2
3
4
5
6

用户的点击一定是在for运行完了之后才点击的,此时候i为6;因为i是一个全局作用域的。那怎么解决呢?当然是给每个循环创建一个独立的作用域:

  1. 用立即执行函数给每个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)
}
1
2
3
4
5
6
7
8
9
  1. 直接用let为每次循环创建子作用域
var liList = ul.getElementsByTagName('li')
for(let i=0; i<6; i++){
    liList[i].onclick = function(){
    alert(i)
  }
}
1
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()
    })
  }
}
1
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'>

编辑 (opens new window)
上次更新: 2022/05/17, 21:50:47
CSS选择器的优先级
33个非常实用的JavaScript一行代码

← CSS选择器的优先级 33个非常实用的JavaScript一行代码→

最近更新
01
一些有意思的类比
06-16
02
the-super-tiny-compiler解析
06-06
03
计算机编译原理总概
06-06
更多文章>
Theme by Vdoing | Copyright © 2021-2022
蜀ICP备2021023197号-2
Simonzhans | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜