Vue的优点
- 轻量级框架:只关注视图层,大小只有几十Kb
- 简单易学:中文文档,易于学习,容易上手
- 双向数据绑定:操作数据更简单
- 组件化:实现了html的封装和复用,在构建单页面应用时有优势
- 视图、数据结构分离:不需要逻辑代码的修改,操作数据就可以完成相关操作
- 虚拟DOM:不再使用操作原生DOM的方式,节省了性能开销
- 运行速度快:运行速度有很大优势
MVC和MVVM框架的区别
MVC:
- M:Model(模型),指的是后端传递的数据
- V:View(视图),看到的页面
- C:Controller(控制器),页面业务逻辑
MVC是单向通信,也就是view和model,必须通过controller来承上启下,控制器负责从视图读取数据,控制用户输入,并向模型发送数据
MVVM:
- M:Model(模型),后端传递的数据
- V:View(视图),看到的页面
- VM:ViewModel(视图模型),mvvm模式的核心,是连接view和model的桥梁
MVVM和MVC的区别:
- mvvm通过数据来驱动视图层的显示,而不是节点操作
- mvc中model和view是可以直接打交道的,造成model和view之间的耦合度很高,mvvm中model和view不直接交互,而是通过viewmodel来同步
- mvvm主要解决了mvc中大量的DOM操作使页面渲染性能降低,加载速度变慢,影响用户体验
vue双向绑定原理
vue是通过数据劫持结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的getter、setter方法,在数据变动时发布消息给订阅者,触发相对应的监听回调,将变化的数据更新到视图
首先需要一个监听器Observer,在vue初始化的时候,监听data中数据的所有属性的getter、setter方法
还需要一个订阅者Watcher,当属性发生变化的时候,接收属性变化的消息
因为订阅者有很多个,需要一个消息订阅器Dep来收集订阅者,在监听器Observer和订阅者Watcher之间进行统一管理
最后需要一个解析器Compile,将模板中的变量替换成数据,初始化渲染视图,并将节点上的指令绑定对应的更新函数,添加监听数据的订阅者Watcher,当数据发生变化时,订阅者接收消息,执行对应的更新函数,从而更新视图
Vue2.0和Vue3.0的区别
- 双向绑定原理发生变化:vue2使用Object.defineProperty()对数据进行劫持,vue3使用es6的Proxy对数据进行代理
- defineProperty只能监听某个属性,不能对全对象监听,Proxy可以直接监听全对象
- paoxy可以监听数组,不用再去单独对数组使用$set做特异性操作
- vue3支持碎片(Fragments),也就是说可以拥有多个根节点
- 最大的区别是vue2使用选项类型API(Options API),vue3使用组合型API(Composition API),旧的选项型API在代码里分割了不同的属性:data、computed属性、methods等,新的组合型API能让我们用方法(function)来分割,代码会更加简便整洁
- 建立数据:vue2直接把数据放在data属性里,vue3需要使用新的setup()方法,使用ref和reactive定义响应式数据,ref一般用于定义普通数据类型和dom节点,使用.value去取值;reactive一般用于定义复杂数据类型,直接取值
- 生命周期钩子函数发生变化
setup函数中不能使用this,setup函数中的props是响应式的,当传入新的prop时,它将被更新,不能使用es6结构,会消除prop的响应性
如果组件被<keep-alive>包含,会多出两个钩子函数
onActivated():被激活时执行
onDeactivated():如从A组件切换到B组件,A组件消失时执行
使用kepp-alive时,销毁函数beforeDestory和destroyed无效
vue-loader是什么,用途有哪些
是基于webpack的一个解析器,解析和转换.vue文件,提取出其中的逻辑代码script、样式代码style、以及html模板template,再分别把它们交给对应的loader去处理
vue中key的作用
key是vnode的唯一标识,通过这个key,diff操作可以更准确、更快速
更准确:v-for更新已渲染过的元素列表时,默认使用"就地复用"策略,如果数据项的顺序发生改变,vue不会移动dom元素来匹配数据项的顺序,而是直接复用此处的元素,造成数据错位的情况,使用key唯一标识元素的身份,实现高效复用
更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方式更快
computed和watch的使用场景
computed:是计算属性,依赖其它属性,并且computed的值有缓存,只有它依赖的属性值发生变化,computed的值才会重新计算,不变时直接从缓存取值
watch:更多的是'观察'的作用,类似某些数据的监听回调,当监听的数据发生变化时,会执行回调进行后续操作,允许进行异步操作
vue组件中的data为什么必须是一个函数
组件中的data写成一个函数,数据以函数返回值的形式定义,这样每次复用组件的时候都会返回一个新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱,数据污染
单页面和多页面的区别
单页面应用(SPA):指只有一个主页面的应用,浏览器一开始必须要加载所有的js、css等资源,所有的内容都包含在主页面,对每一个功能模块组件化,单页面应用的跳转,就是切换组件,仅仅刷新局部资源
单页面优点:用户体验好,内容的改变不用重新加载整个页面
多页面应用(MPA):指有多个独立页面的应用,每个页面必须重复加载js、css等资源,多页面应用的跳转,需要整页资源刷新
多页面优点:有利于SEO
react和vue的异同
相同:
- 都将注意力保持在核心库,其他的功能如路由和全局状态管理都放在其他的库
- 都鼓励组件化应用,将应用拆分成一个个功能明确的模块,提高复用性
- 都使用了虚拟DOM,提高重绘性能
- 都有props,允许组件间的数据传递
- 都有自己的构建工具,能够提供项目模板
不同:
- 数据流:vue默认支持数据双向绑定,react提倡单向数据流
- 虚拟DOM:vue计算出虚拟DOM的差异,在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树;react每当应用的状态发生变化,全部组件树会重新渲染
- 模板的编写:vue的模板接近html的模板,写起来很接近标准html;react推荐使用js的语法扩展JSX来书写
- 组件的引用:react中render函数是支持闭包特性的,import的组件在render中可以直接使用;vue中,由于模板中使用的数据必须挂在this上进行一次中转,import一个组件之后,还需要在components中再声明一下
vuex和pinia的区别
pinia是基于vue3的组合式api构建的,这使得它更加灵活和可组合,vuex是基于vue2的选项性api构建的,pinia采用了类似于react hooks的方式来管理状态,使得它更加直观和易于使用,vuex采用了基于mutation和actions的方式来管理状态,可能需要更多的代码来实现相同的功能
pinia优点:
- 更加轻量级:不需要使用vuex的一些复杂概念,如模块和getter
- 更加简单易用:api设计使用了vue3的组合式api,更加简单
- 更加灵活:提供了更加灵活的状态管理方式,因为它支持多个store实例,vuex只支持一个store实例
vuex优点:
- 更加成熟:已经被广泛使用和测试
- 更加稳定:经过了多个版本的迭代和改进
- 更加强大:vuex提供了一些高级功能,如中间件和插件,使得它可以处理更加复杂的状态管理请求
pinia适合想要一个简单、轻量级的状态管理库的开发者,vuex适合需要更多功能和灵活性的开发者
vuex和redux的区别
相同点:state共享数据;流程一致,定义全局state,触发,修改state
不同点:vuex定义了state、getter、mutation、action四个对象;redux定义了state、reducer、action三个
vuex触发方式有两种:commit同步和dispatch异步;redux同步异步都使用dispatch
vuex中state统一存放,方便理解;redux中state依赖所有reducer的初始值
redux使用的是不可变数据,vuex的数据是可变的,redux每次都使用新的state替换旧的state,vuex是直接修改
redux在检测数据变化时,是通过diff的方式比较差异的,vuex是通过getter、setter来比较的
vuex的流向:
同步操作:
view-->commit-->mutations-->state变化-->view变化
异步操作:
view-->dispatch-->actions-->mutations-->state变化-->view变化
redux流向:
同步、异步一样:
view-->actions-->reducer-->state变化-->view变化
JavaScript有哪些数据类型,它们的区别?
JavaScript共有八种数据类型:分别是Undefined、Null、Bollean、Number、String、Object、Symbol、BigInt
- Symbol代表创建后独一无二且不可变的数据类型,它主要是为了 解决可能出现的全局变量冲突的问题
- BigInt是一种数字类型的数据,它可以表示任意精度格式的整数, 使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了 Number能够表示的安全整数范围
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)--直接存储在栈(stack)中的简单数据段,占据空间 小、大小固定,属于被频繁使用数据,所以放入栈中存储
- 堆:引用数据类型(对象、数组和函数)--占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址
数据类型检测的方式有哪些
(1)typeof
其中数组、对象、null都会被判断为object,其他判断都正确
(2)instanceof
instanceof可以正确判断对象的类型,其内部运行机制是判断在其 原型链中能否找到该类型的原型,而不能判断基本数据类型。instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性
(3)constructor
constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor对象访问它的构造函数。需要注意,如果创建一个对象 来改变它的原型,constructor就不能用来判断数据类型了:
从打开一浏览器开始,输入url到页面加载发生了什么
每打开一个tab页,就相当于创建了一个独立的浏览器进程
输入url之后,总体分为以下几个过程:
- DNS解析:将url解析成IP地址
- TCP连接:TCP三次握手
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束:TCP四次挥手
(1)DNS(domain name system,域名系统)解析:
输入rul后,首先要经过域名解析,浏览器首先查看自身的缓存,缓存中有对应的解析记录,直接返回结果;浏览器没有缓存,电脑会查看本地操作系统的缓存,如果有记录,直接返回结果(host文件),如果没有缓存该域名的IP地址,就需要通过递归或迭代的方式向根域名服务器、顶级域名服务器、权威域名服务器发起查询请求,直至返回一个IP地址给浏览器
(2)TCP连接:三次握手
在客户端发送数据之前会发起TCP三次握手,用以同步客户端和服务端的序列号和确认号,并交换TCP窗口大小信息
- ACK:应答
- Fin:结束;结束会话
- seq:一个数据段的第一个序列号
- SYN:同步;表示开始会话请求
第一次握手:客户端A将标志位SYN置为1,随机产生一个值为seq=X(X的取值范围为1234567)的数据包到服务器,客户端A进入SYN_SENT状态,等待服务端B确认(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
第二次握手:服务端B收到数据包后由标志位SYN=1,知道客户端A请求建立连接,服务端B将标志位SYN和ACK都置为1,ack=X+1,随机产生一个值seq=Y,并将该数据包发送给客户端A以确认连接请求,服务端B进入SYN_RCVD状态(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
第三次握手:客户端A收到确认后,检查ack是否为X+1,ACK是否为1,如果正确,则将标志位ACK置为1,ack=Y+1,并将该数据包发送给服务端B,服务端B检查ack是否为Y+1,ACK是否为1,如果正确则连接建立成功,客户端A和服务端B进入ESTABLISHED状态,完成三次握手,随后客户端A与服务端B之间可以开始传输数据了(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)
三次握手的目的是:为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
"已失效的连接请求报文段"的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。 假设不采用"三次握手",那么只要server发出确认,新的连接就建立了。 由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用"三次握手"的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。"。主要目的防止server端一直等待,浪费资源。
(3)浏览器向web服务器发送HTTP请求
TCP三次握手之后,开始发送HTPP请求报文到服务器,HTTP请求报文格式:请求行+请求头+空行+消息体,请求行包括请求方式(GET/POST/DELETE/PUT)、请求资源路径(URL)、HTTP版本号
(4)服务器处理请求并返回HTTP报文
服务器收到请求后会发出应答,即响应数据,HTTP响应与HTTP请求相似,HTTP响应报文格式:状态行+响应头+空行+消息体,状态行包括HTTP版本号、状态码、状态说明
状态码主要包括以下部分:
- 1xx:指示信息--表示请求已接收,继续处理
- 2xx:成功--表示请求已被成功接收
- 3xx:重定向--要完成请求必须进行更进一步的操作
- 4xx:客户端错误--请求有语法错误或请求无法实现
- 5xx:服务端错误--服务器未能实现合法的请求
响应头主要由Cache-Control、Connection、Date、Pragma等组成
响应体为服务器返回给浏览器的信息,主要由HTML、css、js、图片等文件组成
(5)浏览器解析渲染页面
浏览器拿到响应文本后,解析HTML代码,请求js、css等资源,最后进行页面渲染,呈现给用户,分为以下几个步骤:
- 根据HTML文件解析出DOM tree
- 根据css解析出CSSOM tree(css规则树)
- 将DOM tree和CSSOM tree合并,构建Render tree(渲染树)
- 重排(reflow):根据Render tree进行节点信息机计算(Layout)
- 重绘(repaint):根据计算好的信息绘制整个页面(Painting)
(6)TCP四次挥手
当数据传输完毕,需要断开TCP连接,此时发起TCP四次挥手
第一次挥手:客户端向服务端发送报文,Fin、Ack、Seq,表示已经没有数据传输了,并进入Fin_WAIT_1状态(由浏览器告诉服务器,我请求报文发送完了,你准备关闭吧)
第二次挥手:服务端向客户端发送报文,Ack、Seq,表示同意关闭请求,此时主机发起方进入FIN_WAIT_2状态(由服务器告诉浏览器,我请求报文接受完了,准备关闭,你也准备吧)
第三次挥手:服务器向客户端发送报文段,Fin、Ack、Seq,请求关闭连接,并进入LAST_ACK状态(由服务器告诉浏览器,我响应报文发送完了,你准备关闭吧)
第四次挥手:客户端向服务端发送报文段,Ack、Seq,进入等待TIME_WAIT状态,被动方收到发起方的报文段以后关闭连接,发起方等待一定时间未收到回复,则正常关闭(由浏览器告诉服务器,我响应报文接受完了,我准备关闭,你也准备吧)
vue和react的数据流的区别
vue是响应式的数据双向绑定系统,即双向绑定数据流,当数据发生变化,视图也发生变化,当视图发生变化,数据也跟着同步发生变化
双向数据绑定的优点:无需和单向数据绑定那样进行CRUD操作(Create、Retrieve、Update、Delete),双向数据绑定最常应用在表单上,当用户在前端页面输入完成后,不用任何操作,就已经拿到了用户输入好的数据,并放在数据模型中了
react是单向数据流,没有双向绑定,数据主要从父组件流向子组件,如果腹肌的某个props改变了,react会重新渲染所有的子节点
在react 中,数据仅朝一个方向流动,即从父组件流向子组件,如果数据在兄弟组件之间共享,那么数据应该存储在父组件,并同时传递给需要数据的两个子组件
前端存储数据的方式
- Cookie(HTTP Cookie):用于在客户端存储会话信息,Cookie是与特定域名绑定的,设置cookie后,它会与请求一起发送到创建它的域,这个限制能保证cookie中存储的信息只能对被认可的接收者开发,不被其他域访问;每个域大概存20个cookie,大小限制为4K,兼容性好
- localStorage:是HTML5提供的一种持久化存储的方法,用于在浏览器中存储键值对,数据存储在浏览器端,不会过期,除非手动清除或者浏览器数据被删除,大小为5M,兼容IE8+
- sessionStorage:也是HTML5提供的一种持久化存储的方法,用于在浏览器中存储键值对,数据存储在浏览器端,与cookie和localStorage不同,它不能在所有同源窗口中共享,是会话级别的存储方式,会话关闭后数据会被清除
- IndexedDB:是HTML5提供的一种非关系型数据库,用于在浏览器中存储大量的结构化数据,是一个提供结构化存储的浏览器API,可以存储大量的数据,并支持索引进行高效查询;
IndexedDB是一个基于事件的数据库系统,支持事务操作,它允许创建对象存储空间来存储和检索JavaScript对象,由于其功能强大,可以处理大量的数据,因此适用于离线应用、缓存数据等场景
JavaScript内存管理、JS引擎
JavaScript内存管理是自动进行的,在创建变量(对象、字符串等)时自动进行了内存分配,之后在代码执行、使用变量时占用这个内存,不再使用变量后内存会被回收,释放掉,在JS中,这个过程被称为垃圾回收机制
JavaScript引擎:是一个专门处理JavaScript脚本的虚拟机,它本质上是一段程序,可以将JavaScript代码编译为不同CPU对应的汇编代码,此外还负责执行代码,分配内存和垃圾回收等
JavaScript引擎的内存结构可以粗略分为两个部分:栈(Stack)、堆(Heap)
栈:主要用于存放基本类型和变量类型的指针,栈内存自动分配大小相对固定的内存空间,并由系统自动释放
堆:主要用于存放对象类型数据,如对象、数组、函数等,堆内存是动态分配内存,内存大小不一,也不会自动释放
JavaScript垃圾回收算法
为了更好的回收内存,JS引擎中有一个垃圾回收器(gc),它的主要作用是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它
垃圾回收机制的原理是定期找出那些不再用到的变量,然后释放其占用的内存,不是实时的找出无用的内存并释放,原因是实时开销太大
不是所有语言都有gc,一般高级语言会自带gc,如java、Python、JavaScript等,没有gc的语言,如c、c++等,需要程序员手动管理内存
如何判断内存不再被使用,JS提供了一系列的算法来帮助判断变量是否被引用
(1)标记清除法(Mark-Sweep)
目前在JS引擎里,这种算法是最常用的,分为标记和清除两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(就是非活动对象)销毁
标记阶段:通过一个二进制的位,在变量进行执行环境上下文时进行标记将0标记为1,(维护两个列表,list1:进入环境的变量;list2:退出环境的变量)
标记执行的策略执行过程:
- 进入环境,遍历内存中的所有对象加tag(出发点不一定),初始化,默认都是0
- 从根节点或者window开始,遍历内存中的值,如果有被引用(不是垃圾),标记为1
- 销毁标记为0的垃圾,收回它们占用的内存空间
- 把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点:实现比较简单,打标记也无非就是打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点:在清除之后,剩余的对象内存位置是不变的,就会导致空闲内存空间是不连续的,出现了内存碎片,并且由于剩余空间内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题
常见包括三种分配策略找到合适的内存块:
- First-fit:找到大于等于size的块立即返回
- Best-fit:遍历整个空闲列表,返回大于等于size的最小分块
- Worst-fit:遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
这三种策略里面Worst-fit的空间利用率看起来最合理,但实际上切分之后会造成更多的小块,形成内存碎片,不推荐使用,对于First-fit和Best-fit来说,考虑到分配的速度和效率,First-fit是更为明智的选择
综上所述,标记清除算法有两个很明显的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能出现分配所需内存过大的对象时找不到合适的块
- 分配速度慢,即使是使用First-fit策略,其操作仍然是一个O(n)的操作,最坏的情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
(2)标记整理(Mark-Compact)
它的标记阶段和第一种标记清除法没有什么不同,只是在标记结束后,标记整理法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
- 进入环境,遍历内存中的所有对象加tag(出发点不一定),初始化,默认都是0
- 从根节点或者window开始,遍历内存中的值,如果有被引用(不是垃圾),标记为1
- 将标记为1的移向内存的一端,清理标记为0的
- 销毁标记为0的垃圾,收回它们占用的内存空间
- 把所有内存中对象标记修改为0,等待下一轮垃圾回收
(3)引用计数算法(Reference Counting)
JS第一版提出的垃圾回收机制,它把对象是否不再需要,简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收
它的策略是跟踪记录每个变量值被引用的次数
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1;
- 如果同一个值又被赋给另一个变量,那么引用数加 1;
- 如果该变量的值被其他的值覆盖了,则引用次数减 1;
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
js
let a = new Object() // 此对象的引用计数为 1(a引用)
let b = a // 此对象的引用计数是 2(a,b引用)
a = null // 此对象的引用计数为 1(b引用)
b = null // 此对象的引用计数为 0(无引用)
... // GC 回收此对象
虽然这种方式很简单,但是在引用计数出现没多久,就遇到了一个很严重的问题--循环引用,即对象A有一个指针指向对象B,而对象B也引用了对象A:
js
function test(){
let A = new Object() // new Object1引用计数为1
let B = new Object() // new Object2引用计数为1
A.b = B // new Object2引用计数为2
B.a = A // new Object1引用计数为2
}
test()执行之后由于作用域A、B被清除,但a、b仍存在,new Object1、newObject2引用计数为1,不能被清理
优点:引用计数在引用值为0时,就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾;标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中,线程就必须要暂停去执行一段时间的gc,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数只需要在引用是计数就可以了
缺点:需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限;无法解决循环引用无法回收的问题
V8对垃圾回收的优化
上面说的垃圾回收策略在每次垃圾回收时都要检查内存中所有的对象,这样对于一些大、老、存活时间长的对象来说,同新、小、存活时间短的对象一个频率的检查很不好,因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,这也是分代式的原则
(1)新老生代
V8中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器,也就是不同的策略管理垃圾回收
新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持1~8M的容量,而老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大
新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率
(2)新生代垃圾回收
新生代对象是通过一个名为Scavenge的算法进行垃圾回收,主要采用了一种复制式的方法,即Cheney算法,将堆内存一分为二,一个是处于使用状态的空间,称之为使用区,一个是处于闲置状态的空间,称之为空闲区
新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作,开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉,最后进行角色互换,把原来的使用区清空变成空闲区,把原来的空闲区变成使用区
当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理
如果复制一个对象到空闲区时,空闲区空间占用超过25%,那么这个对象会被直接晋升到老生代空间中,设置25%的比例原因是:当完成Scavenge回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
(3)老生代垃圾回收
老生代垃圾回收器的整个流程采用的就是标记清除:
标记阶段:从一组根元素开始,递归遍历这组根元素,遍历过程中能达到的元素称为活动对象,没有达到的元素就可以判断为非活动对象
清除阶段:老生代垃圾回收器会直接将非活动对象清理掉
V8中就采用了标记整理算法来解决标记清除算法造成的内存碎片问题,来优化空间
(4)并行回收(Parallel)
JS是一门单线程的语言,它是运行在主线程上的,在进行垃圾回收时就会阻塞JS脚本的执行,需等待垃圾回收完毕之后再恢复脚本执行,这种行为叫做全停顿(Stop-The-World)
比如一次gc需要60ms,那我们的应用逻辑就得暂停60ms,假如一次gc时间过长,对用户来说就可能造成页面卡顿等问题,引入多个辅助线程来同时处理,以此加速垃圾回收的执行速度,因此V8团队引入了并行回收机制
并行指的是垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作
新生代对象空间就采用并行策略,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针,这就是并行回收
(5)增量标记
并行策略虽然可以增加垃圾回收的效率,对于新生代垃圾回收器能够有很好的优化,但它还是一种全停顿的垃圾回收方式,对于老生代来说,它的内部存放的都是一些较大的对象,这些大的对象,gc时哪怕我们使用并行策略依然可能会消耗大量时间
所以为了减少全停顿的时间,在2011年,V8对老生代的标记进行了优化,从全停顿标记切换到增量标记
增量就是将一次gc过程,分成很多小步,每执行完一小步就让应用逻辑执行一会,这样交替多次后完成一轮gc标记
在一次完整的gc标记分块暂停后,执行任务程序时,内存中标记好的对象引用关系被修改了怎么办,V8团队采用了一种特殊方式:三色标记法
(6)并发回收(Concurrent)
并行回收依然会阻塞主线程,增量标记同样有增加了总暂停时间、降低应用程序吞吐量两个缺点
并发回收,指的是主线程在执行JS过程中,辅助线程能够在后台完成垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不被挂起,这是并发的优点,同样也是并发回收实现的难点,因为它需要考虑主线程在执行JS时,堆中的对象引用关系随时都可能发生变化,这时辅助线程之前做的一些标记或者正在进行的标记就会要有所改变,所以它需要额外实现一些读写锁机制来控制这一点
总结:
V8的垃圾回收策略主要基于分代式垃圾回收机制,关于新生代垃圾回收器,使用并行回收可以很好的增加垃圾回收的效率,老生代垃圾回收器中,是融合使用的:
- 老生代主要使用并发标记,主线程在开始执行JS时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)
- 标记完成后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)
- 清理的任务会采用增量的方式分批在各个JS任务之间执行
JavaScript的内存泄漏(Memory Leak)
引擎虽然针对垃圾回收做了各种优化从而尽可能的确保垃圾得以回收,但不是所有无用对象内存都可以被回收,当不再用到的对象内存,没有及时被回收时,这种场景称之为内存泄漏
常见的内存泄漏:
(1)不正当的闭包
闭包是指有权访问另一个函数作用域中的变量的函数
js
function fn1(){
let test = new Array(1000).fill('xianzao')
return function(){
console.log('zaoxian')
}
}
let fn1Child = fn1()
fn1Child()
这是一个典型闭包,但是它没有造成内存泄漏,因为返回的函数中并没有对fn1函数内部的引用,也就是说,函数fn1内部的test变量完全是可以被回收的
js
function fn2(){
let test = new Array(1000).fill('xianzao')
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2()
fn2Child()
这个也是闭包,并且因为return的函数中存在函数fn2中的test变量引用,所以test并不会被回收,也就造成了内存泄漏
解决办法:在函数调用后,把外部的引用关系置空就好了
js
function fn2(){
let test = new Array(1000).fill('xianzao')
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2()
fn2Child()
fn2Child = null
(2)隐式全局变量
函数中的局部变量在函数执行结束后,这些变量已经不再被需要,所以垃圾回收器会识别并释放,但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收
js
function fn(){
// 没有声明从而制造了隐式全局变量test1
test1 = new Array(1000).fill('xianzao')
// 函数内部this指向window,制造了隐式全局变量test2
this.test2 = new Array(1000).fill('xianzao')
}
fn()
在开发中可以使用严格模式或者通过lint检查来避免这些情况的发生,从而降低内存成本,在使用全局变量存储数据时,要确保使用后将其置空或者重新分配,当然也很简单,在使用后将其置为null即可
(3)游离DOM引用
代码中进行DOM时会使用变量缓存DOM节点的引用,但移除节点的时候,应该同步释放缓存的引用,否则游离的子树无法释放
html
<div id="root">
<ul id="ul">
<li></li>
<li></li>
<li id="li3"></li>
<li></li>
</ul>
</div>
<script>
let root = document.querySelector('#root')
let ul = document.querySelector('#ul')
let li3 = document.querySelector('#li3')
// 由于ul变量存在,整个ul及其子元素都不能GC
root.removeChild(ul)
// 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
ul = null
// 已无变量引用,此时可以GC
li3 = null
</script>
使用变量缓存DOM节点引用后删除了节点,如果不将缓存引用的变量置空,依然不能进行gc,也就会出现内存泄漏
假如将父节点置空,但是被删除的父节点,其子节点引用也缓存在变量里,那么就会导致整个父DOM节点树下的整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空
(4)定时器
计时器setTimeout和setInterval
在setInterval没有结束前,回调函数里的变量以及回调函数本身都无法被回收,调用了clearInterval才是结束,没有被clear的话,就会造成内存泄漏
当不需要计时器时,使用clearInterval和clearTimeout来清除,另外,浏览器中的requestAnimationFrame也存在这个问题,需要在不使用的时候用cacelAnimationFrameAPI来取消使用
(5)事件监听器
当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的,而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这就造成意外的内存泄漏
(6)MAP、Set对象
当使用Map或者Set存储对象时,同Object一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收
js
let obj = {id: 1}
let user = {info: obj}
let set = new Set([obj])
let map = new Map([[obj, 'xianzao']])
// 重写obj
obj = null
console.log(user.info) // {id: 1}
console.log(set)
console.log(map)
重写了obj以后,{id:1}依然存在于内存中,因为user对象以及后面的set/map都强引用了它,Set/Map、对象、数组对象等都是强引用,所以仍然可以获取到{id:1},想要清除,那就只能重写所有引用将其置空了
js
let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'xianzao']])
// 重写obj引用
obj = null
// {id: 1} 将在下一次 GC 中从内存中删除
使用了 WeakMap 以及 WeakSet 即为弱引用,将 obj 引用置为 null 后,对象 {id: 1} 将在下一次 gc 中被清理出内存
(7)console
之所以在控制台能看到数据输出,是因为浏览器保存了我们输出对象的信息数据引用,也正是因此,未清理的console,如果输出了对象也会造成内存泄漏
开发环境下可以使用控制台输出来便于调试,在生产环境下,一定要及时清理掉输出
排查定位:
Chrome的devTool中找到Performance这一面板,它可以记录并分析在网站的生命周期内所发生的各类事件
Memory面板,它可以为我们提供更多详细信息,比如,记录JS、CPU执行时间细节、显示JS对象和相关的DOM节点的内存消耗、记录内存的分配细节等
常见的前端内存问题
- 内存泄漏:不再使用的对象内存,没有被及时回收造成的
- 内存膨胀:即在短时间内内存占用急速上升到达一个峰值,想要避免需要使用技术手段减少对内存的占用
- 频繁gc:gc执行的特别频繁,一般出现在频繁使用大的临时变量导致新生代空间被装满的速度极快,而每次新生代装满时,就会触发gc,频繁gc同样会导致页面卡顿,想要避免的话,就不要使用太多的临时变量,因为临时变量不用了就会被回收
闭包的6种应用场景
闭包的定义:如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包
闭包会创建一个包含外部函数作用域变量的环境,并将其保存在内存中,这意味着,即使外部函数已经执行完毕,闭包仍然可以访问和使用外部函数的变量
js
//闭包实例代码
function fn1() {
let a = 1;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
const fn2 = fn1(); //闭包函数执行完后外部作用域变量仍然存在,并保持状态
fn2() //2
fn2() //3
闭包的特性:
- 函数嵌套:闭包的实现依赖于函数嵌套,即在一个函数内部定义另一个函数
- 记忆外部变量:闭包可以记住并访问外部函数的变量,即使外部函数已经执行完毕
- 延长作用域链:闭包将外部函数的作用域链延长到内部函数,使得内部函数可以访问外部函数的变量
- 返回函数:闭包通常以函数的形式返回,使得外部函数的变量仍然可以被内部函数引用和使用
闭包的优点:
- 保护变量:闭包可以将变量封装在函数内部,避免全局污染,保护变量不被外部访问和修改
- 延长变量生命周期:闭包使得外部函数的变量在函数执行完后仍然存在,可以在内部函数继续使用
- 实现模块化:闭包可以创建私有变量和私有方法,实现模块化的封装和隐藏,提高代码的可维护性和安全性
- 保持状态:闭包可以捕获外部函数的变量,并在函数执行时保持其状态,这使得闭包在事件处理、回调函数等场景中常有用
闭包的缺点:
- 内存占用:闭包会导致外部函数的变量无法被垃圾回收,从而增加内存占用,如果滥用闭包,会导致内存泄漏问题
- 性能损耗:闭包涉及到作用域链的查找过程,会带来一定的性能损耗,在性能要求高的场景下,需注意闭包的使用
闭包的应用场景: (1)自执行函数:
js
let say = (function(){
let val = 'hello world';
function say(){
console.log(val);
}
return say;
})()
(2)节流防抖:
防抖函数:是为了防止快速且频繁的触发事件而导致多次执行时间函数,使用防抖后,多次触发的事件只执行一次事件函数
如何定义频繁,可以设定一个时间间隔,如果两次事件触发的间隔时间低于设定的时间,则定义为频繁,场景有很多,如监听滚动、鼠标移动事件onmousemove、频繁点击表单的提交按钮、联想输入搜索等
如联想输入搜索,不加防抖,每次输入一个字符就会产生新的搜索词,触发数据请求接口,浪费性能,很容易触发接口的限流措施,使用防抖后,判断指定的时间内是否存在多次调用,若存在,清除上一次的定时器,重新开始计时,在指定的时间内,如果没有再次调用,就执行传入的回调函数
js
function debounce(func, delay) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
节流函数:节流额防抖的本质不一样,防抖是多次触发,但只执行一次,节流是多次触发,但周期内只执行一次
节流的策略是,每个时间周期内,不论触发多少次事件,也只会执行一次动作,上个时间周期结束后,又有事件触发,开始新的时间周期,同样,新的时间周期也只会执行一次操作
如滚动到顶部,当页面向下滚动出现一个Top按钮,点击之后能够回到顶部,这时需要获取滚动位置与顶部的距离,判断是否要展示TOP按钮,不加节流处理,滚动一下就会触发多次判断滚动距离的事件,浪费性能,使用节流后,通过标志位判断是否已经被触发,当已经触发后,再进来的请求直接结束掉,直到上一次指定的时间间隔到达,且回调函数执行之后,再接受下一个处理
js
function throttle(func, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
};
}
(3)函数柯里化:
函数柯里化:是把接受多个参数的函数变成一系列接受单个参数的函数的过程
每次接受最初函数的第一个参数,返回接受余下的参数的函数
求取两个数之和的函数:
js
function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // 3
console.log(add(5, 7)); // 12
进行柯里化:
js
function add(x) {
return function (y) {
return x + y;
}
}
console.log(add(1)(2)); // 3
console.log(add(5)(7)); // 12
柯里化函数的特点: 一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值
柯里化的实际应用:
参数复用:将相同的参数固定下来
js
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
// 即使是相同的正则表达式,也需要重新传递一次
console.log(check(/\d+/g, 'test1')); // true
console.log(check(/\d+/g, 'testtest')); // false
console.log(check(/[a-z]+/g, 'test')); // true
// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt)
}
}
// 正则表达式通过闭包保存了起来
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
console.log(hasNumber('test1')); // true
console.log(hasNumber('testtest')); // false
console.log(hasLetter('21212')); // false
示例是一个正则的校验,正常来说直接调用check函数就可以,但如果有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber、hasLetter等函数,让参数能够复用,调用起来也方便
前端常见网络请求协议
- HTTP(HyperText Transfer Protocol):是一种用于传输超文本的协议,是Web应用中最为常用的协议之一,HTTP协议是一种客户端--服务器模型的协议,客户端通过发送HTTP请求与Web服务器进行通信,Web服务器则通过发送HTTP应答来响应请求
- HTTPS(HyperText Transfer Protocol Secure):是一种更加安全的HTTP协议,在HTTP基础上添加了SSL/TLS加密措施,可以保证数据传输的安全性,HTTPS协议在请求数据时,先与服务器进行身份验证,然后通过加密数据传输来保证数据的安全
- FTP(File Transfer Protocol):是一种用于文件传输的协议,可以通过FTP协议在Web应用中进行文件的上传和下载操作,FTP协议由客户端与服务器建立连接,然后进行文件传输操作
- WebSocket:是一种全双工通信协议,可以在客户端与服务器之间建立长连接,事件实时的双向数据传输,WebSocket协议使用HTTP协议进行握手,然后建立起一个WebSocket连接,这个连接会一直保持打开状态
- AJAX(Asynchronous JavaScript and XML):是一种使用JavaScript和XML等技术实现异步数据传输的技术,AJAX通过XMLHttpRequest对象与Web服务器进行交互,可以在不刷新整个页面的情况下更新部分页面内容
HTTP和HTTPS的区别
- HTTPS协议需要ca申请证书,一般免费证书较少,因而需要一定费用
- HTTP是超文本传输协议,信息是明文传输,HTTPS是具有安全向的ssl加密传输协议
- HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,HTTP是80,HTTPS是443
- HTTP的连接很简单,是无状态的,HTTPS是由ssl+http协议构建的可进行加密传输,身份认证的网络协议,比HTTP协议安全
HTTP和WebSocket
WebSocket连接的过程:
- 客户端发起http请求,经过3次握手后,建立TCP连接,http请求里存放WebSocket支持的版本号等信息,如Upgrade、Connection、websocket-Version等
- 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据
- 客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信
- 连接成功后,可以通过send()方法来向服务器发送数据,并通过onmessage事件来接收服务器返回的数据
相同点:都是一样基于TCP的,都是可靠性传输协议
不同点:
- WebSocket是一个持久化的协议,HTTP不支持(循环连接的不算)
- WebSocket支持双向通讯(可以让服务器主动向客户端推送消息,客户端也可以主动向服务器发送信息),HTTP只能由客户端发起,一个request对应一个response
跨域及解决方案
跨域:是指一个网页或web应用在浏览器中发起对另一个域名下资源的请求,由于浏览器的同源策略限制,跨域请求会被浏览器拦截
同源策略:指协议、域名、端口都完全相同时,才会被认为是同源(不同源的网页,由于安全性考虑,不能读取对方网页的内容或使用对方网页的JS接口)
常见的跨域场景:
- 前后端分离开发中,前端请求后端api
- 使用CDN加载第三方库
- 前端页面嵌入其他网站的评论/分享等组件
- H5页面与小程序/App通信
跨域解决方案: (1)CORS:后端人员在返回响应的时候加特殊的响应头,于是当服务器把数据回传给浏览器的时候,尽管浏览器监测到了跨域问题,但它收到服务器同意将数据给开发人员的指令,于是浏览器将数据给开发人员展示出来,解决了跨域
缺点:当使用CORS解决跨域问题时,会产生安全隐患,因为这样所有人都可以请求该服务器拿到数据
示例代码,展示了如何在常见的服务器端框架(Node.js+Express)中启用CORS
js
const express = require('express');
const app = express();
// 允许所有源的跨域请求
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
next();
});
// 处理跨域请求的路由
app.get('/api/data', function(req, res) {
// 在这里处理跨域请求的逻辑
res.json({ message: '跨域请求成功!' });
});
app.listen(3000, function() {
console.log('服务器已启动,监听端口 3000');
});
用Express框架,在服务器的中间件中添加了一个处理跨域请求的函数,这个函数设置了响应头,允许来自所有源的跨域请求,能修改'Access-Control-Allow_origin'的值,将其设置为特定的域名,限制只允许指定源的跨域请求
然后定义了一个处理跨域请求的路由/api/data,在这个路由中编写处理跨域请求的逻辑
(2)JSONP:利用了script标签中的src属性,在引入外部资源不受同源策略限制的特点,解决跨域问题
缺点:需要前后端人员配合才能实现,比较麻烦,最重要的是,该方法只能解决get请求跨域问题,不能解决post、delete等跨域问题
JSONP的示例代码:
js
function handleResponse(data) {
// 在这里处理从远程服务器返回的数据
console.log(data);
}
function makeJsonpRequest(url) {
// 创建一个带有随机回调函数名称的全局函数
const callbackName = 'jsonpCallback' + Math.floor(Math.random() * 100000);
window[callbackName] = function(data) {
handleResponse(data);
// 请求完成后删除回调函数
delete window[callbackName];
script.parentNode.removeChild(script);
};
// 创建一个 <script> 标签,并将其 src 属性设置为远程 URL,包括回调函数名称
const script = document.createElement('script');
script.src = url + '?callback=' + callbackName;
// 将 <script> 标签添加到文档中开始加载远程脚本
document.body.appendChild(script);
}
makeJsonRequest函数用于发起JSONP请求,创建一个全局的随机回调函数名称,并将该函数名称作为参数附加到远程URL中,然后创建一个script标签,将其属性设置为带有回调函数名称的远程URL,将script标签添加到文档中后,浏览器会开始加载远程脚本
在客户端,定义了一个全局的回调函数handleResponse来处理从远程服务器返回的数据,一旦数据返回并执行了回调函数,可以在handleResponse函数中进行进一步的处理,之后删除全局的回调函数,并移除script标签,以清理相关的资源
(3)代理服务器:
nginx开启代理服务器:需要掌握一定的后端技术,学习成本大
vue-cli开启代理服务器:
请求前缀:'/atguigu',作用:代理服务器如果检测到浏览器发送的请求中有请求前缀,会将请求继续转发给5000这台服务器,否则不会转发,这就实现了灵活控制代理服务器是否将请求发送给5000这台服务器
target:(请求地址)向哪里发送请求
pathRewrite:路径重写,这样代理服务器可以识别atguigu是请求前缀,不会将/atguigu/students继续带给5000这台服务器了
在.vue文件中,有浏览器先向代理服务器发送Ajax请求并携带请求前缀
前端安全常见的攻击类型及如何防御
(1)CSRF攻击:Cross-site request forgery(跨站请求伪造),简单来说,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求,如果用户再被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作,比如:发消息、转账等,对服务器来说,这些请求又是合法的,所以可能会造成个人信息的泄漏、财产的损失等
CSRF攻击的过程
第一步 ,用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A
;
第二步 ,在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器
,此时用户登录网站A成功,可以正常发送请求到网站A;
第三步 , 用户未退出网站A之前,在同一浏览器中,打开一个新的tab页访问网站B
;
第四步 , 网站B接收到用户请求后,返回一些攻击性代码,通过这个代码,在用户不知情的情况下携带Cookie信息,向网站A发出请求
,比如说这个请求是获取用户信息的。
第五步 ,网站A根据cookie,会认为是用户C发送的该请求,从而导致该请求被执行
,进而给用户C造成个人信息或财产的损失。
原理:在浏览器中,所有的cookie,可以在任意一个标签页中被访问(没有设置http-only为true的情况下),不论是否跨域
CSRF攻击的本质是利用cookie会在同源请求中主动携带发送给服务器的特点,以此来实现用户的冒充
预防CSRF攻击:
- 服务器进行同源检测,验证HTTP的origin、referer字段:服务器根据http请求头中origin或referer信息来判断请求是否为允许访问的站点发送的,当origin或referer信息不存在时,直接阻止请求
- 设置http-only为true:禁止cookie被浏览器通过js访问到
- 验证token:浏览器发送请求时,携带token,因为token存放在sessionStorage中,不会被其他网站窃取到
(2)XSS攻击:Cross site Scripting(跨站脚本攻击),攻击者通过各种方式将恶意代码注入到用户的页面中,这样就可以通过脚本进行一些操作,如:在评论区植入js代码,使得页面被植入广告
XSS攻击的类型:
- 存储型XSS攻击:也叫持久性XSS,主要讲恶意代码提交存储在服务器,当目标用户访问该页面获取数据时,恶意代码会从服务器返回到浏览器做正常的HTML和JS解析执行,混在其中的恶意代码也被执行,这样XSS攻击就发生了,一般存储型XSS出现在网站留言、评论等交互处
- 反射型XSS攻击:一般是攻击者通过特定手法,如电子邮件,诱使用户去访问一个包含恶意代码的url,当用户点击后,恶意代码会拼接在html中返回给浏览器,浏览器解析时,恶意代码也被执行,反射型XSS通常出现在网站的搜索栏、用户登陆口等地方,常用来窃取客户端Cookies(存储型CSS的恶意代码存放在数据库里,反射型XSS的恶意代码存放在url里)
- DOM型XSS攻击:指通过恶意脚本修改页面的DOM结构,是纯粹发生在客户端的攻击,DOM型XSS攻击中,取出和执行恶意代码由浏览器端完成,属于前端JavaScript自身的安全漏洞,其他两种属于服务端的安全漏洞
XSS攻击可以进行的操作:
- 获取页面数据,如DOM、cookie、localStorage
- 破坏页面结构
- DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器
- 流量劫持(将链接指向某网站)
如何预防XSS攻击:
- 针对反射型XSS攻击,在cookie中设置http-only为true,防止客户端通过脚本(document.cookie)读取cookie
- 针对反射型XSS攻击,在输出url参数之前,先对url进行URLEncode操作
- 针对存储型XSS攻击,对需要插入到HTML中的代码做好充分的转义,如:对表示html标记的<>等符号进行编码
- 开启白名单,阻止白名单以外的资源的加载和运行,主要有两种方式:一是在HTTP的header中设置Content-Security-Policy;二是设置meta标签,<meta http-equiv="Content-Security-Policy">
(3)DDOS攻击:分布式拒绝服务攻击(Distributed Denial of Service),简单说就是发送大量请求使服务器瘫痪,DDOS是在DOS攻击基础上的,可以通俗理解,dos是单挑,ddos是群殴,因为现代技术的发展,dos攻击的杀伤力降低,所以出现了ddos
攻击者借助公共网络,将大量的计算机设备联合起来,向一个或多个目标攻击
(4)SQL注入攻击:通过对web连接的数据库发送恶意的SQL语句而产生的攻击,从而产生安全隐患和对网站的威胁,可以造成逃过验证或私密信息泄露等危害,SQL注入的原理是通过在对SQL语句调用方式上的疏漏,恶意注入SQL语句
预防:md5加密
(5)iframe的滥用:iframe中的内容是由第三方来提供的,页面不受控制,他们可以在iframe中运行js脚本、flash插件、弹出对话框等,破坏前端用户体验
(6)恶意第三方库:无论前端还是后端应用开发,绝大多数都是在借助开发框架和各种类库进行快速开发,一旦第三方库被植入恶意代码,很容易引起安全问题
网络劫持有哪几种,如何防范
- DNS劫持:如输入京东被强制跳转到淘宝,由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持
- HTTP劫持:如访问谷歌,但一直有贪玩蓝月的广告,由于http明文传输,运营商会修改你的http响应内容,即添加广告;http劫持依然非常盛行,最有效的办法就是全站HTTPS,将HTTP加密,使得运营商无法获取明文,就无法劫持你的响应内容
cookie、token
cookie:登录后,后端生成一个sessionid放在cookie中返回给客户端,并且服务端一直记录着这个sessionid,客户端以后每次请求都会自动带上这个sessionid,服务端通过这个sessionid来验证身份之类的操作,所以别人拿到了cookie就等于拿到了sessionid,完全可以代替你
token:登录后,后端会返回一个token给客户端,客户端将这个token存储起来,然后每次客户端请求都需要开发者手动将token放在header中带过去,服务端每次只需要对这个token进行验证就能使用token中的信息来进行下一步操作
对于XSS攻击,cookie和token都能被拿到,没什么区别,对于crsf攻击来说,token拿不到
浏览器的进程和线程
进程: CPU是计算机的核心,承担所有的计算任务,进程是CPU资源分配的最小单位
进程字面意思就是进行中的程序,是一个可以独立运行且拥有自己的资源空间的任务程序,进程包括运行中的程序和程序所使用到的内存和系统资源
CPU可以有很多进程,CPU在运行一个进程时,其他的进程处于非运行状态,CPU使用时间片轮转调度算法,来实现同时运行多个进程
线程: 是CPU调度的最小单位,线程是建立在进程的基础上的一次程序运行单位,通俗解释,线程就是程序中的一个执行流,一个进程可以有多个线程
一个进程中只有一个执行流称作单线程,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个进程中有多个执行流称作多线程,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,允许单个程序创建多个并行执行的线程来完成各自的任务
进程和线程的关系区别:
进程和线程的关系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源
- 线程在执行过程中,需要协作同步,不同进程的线程间要利用消息通信的办法实现同步
- 处理机分给线程,即真正在处理机上运行的是线程
- 线程是指进程内的一个执行单元,也是进程内的可调度实体
进程和线程的区别:
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源时相互独立的
- 影响关系:一个进程崩溃后,在保护模式下不会对其他的进程产生影响,但是一个线程崩溃,整个进程都死掉,所以多进程要比多线程健壮
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
JS是单线程
是由它的用途决定的,JS的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题
HTML5提出的Web Worker标准,允许JS脚本创建多个线程,但是子线程是完全受主线程控制的,而且不得操作DOM,这个标准并没有改变JS是单线程的本质
浏览器是多进程
Chrome是一个多进程的程序,每打开一个Tab页就会产生一个进程
浏览器的进程包含:
-
一个主进程:主要负责界面显示、用户交互、紫禁城管理,同时提供存储等功能
-
一个网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程
-
一个GPU进程:GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求,最后,Chrome在其多进程架构上也引入了GPU进程
-
多个渲染进程:核心任务是将HTML、CSS、JavaScript转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程,出于安全考虑,渲染进程都是运行在沙箱模式下
-
多个插件进程:主要负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
-
Browser:主进程,只有一个,负责网络资源的下载和管理,是核心
-
第三方插件进程:同Tab页一样,是独立的进程
-
GPU进程:只有一个,用于3D绘制,图形渲染等
-
渲染(render)进程:每个Tab页都有一套自己的进程,浏览器的内核
渲染进程的线程
(1)GUI渲染线程:负责渲染浏览器页面,解析HTML、CSS,构建DOM树、CSSOM(CSS规则树)、渲染树和绘制页面,当界面需要重新绘制或由于某种操作引发回流时,该线程就会执行
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时,GUI想成会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
(2)JS引擎线程:JS引擎线程也称为JS内核,负责处理JavaScript脚本程序,解析JavaScript脚本,运行代码,JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序
注意:GUI渲染线程与JS引擎线程的互斥关系,如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞
(3)事件触发线程:属于浏览器而不是JS引擎,用来控制时间循环;当JS引擎执行代码块,如setTimeout时(也可能是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
(4)定时器触发线程:即setInterval和setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确性;因此使用单线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中
注意:W3C在HTML标准中规定,定时器的定时事件不能小于4ms,小于,默认为4ms
(5)异步http请求线程:XMLHttpRequest连接后通过浏览器新开一个线程请求;检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行
重绘和回流
重绘:当元素的一部分属性发生变化,如外观背景色,不会引起布局变化而需要重新渲染的过程(改变样式)
回流:当render树中的一部分或者全部因为大小边距等问题发生改变,而需要重建的过程(改变大小)
回流必将引起重绘,重绘不一定会引起回流
哪些情况会发生重绘:
改变背景色、字体颜色、outline、visibility等
哪些情况会导致回流:
- 添加或删除可见的DOM元素
- 通过style控制元素的位置变化
- 元素尺寸的变化,包括外边距、内边距、边框大小、高度、宽度等
- 内容改变引发的尺寸改变:文本改变或图片大小改变而引起的计算值宽度和高度的改变
- 浏览器窗口尺寸改变,resize事件的触发
如何减少会避免页面回流:
(1)CSS优化法:
- 使用visibility替换display:none(前者之后引起重绘,后者会引起回流)
- 避免使用table布局,因为可能一个小小的改动会造成整个页面布局的重新改动
- 将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局(脱离文档流之后,对其他的元素影响小,从而提升性能)
- 避免使用CSS的JavaScript表达式,可能会引发回流
- 不要一个个改变元素的样式属性,直接改变className,若是动态改变样式,则用cssText,如:box.style.cssText="width:200px;height:200px"
- 尽可能在DOM树的末端改变class,可以限制回流的范围,使其影响尽可能少的节点
(2)JavaScript优化法:
- 避免频繁操作样式,最好一次性重写style属性,会将样式列表定义为class,并一次性更改class属性
- 避免频繁操作DOM,创建一个DocumentFragment进行缓存操作,此处引发一次回流或重绘,接着在它上面的所有DOM操作在最后再添加(append)到body中
- 使用display:none的技术,先将元素用display:none隐藏起来,接着对此元素进行所有的操作,最后再将此元素展示出来(对display:none的元素进行操作时不会引起回流重绘的,所有操作只会有两次回流)
- 让要操作的元素进行"离线处理",处理完一起更新,即让元素不存在与render tree中,如读取offsetLeft等属性
JS事件循环(Event Loop)
JS是单线程,只有一个线程存在,同一时间只能做一件事,这可能会导致JS在处理某些长时间运行的操作(如网络请求、文件系统访问等)时出现阻塞,从而影响用户体验,为了解决单线程运行阻塞问题,JS用到了计算机系统的一种运行机制,即事件循环
在JS中,所有的任务都可以分为:
- 同步任务:立即执行的任务,指的就是前一个任务结束之后再执行后一个任务,程序执行的顺序与任务排序的顺序是一样的,保持同步的,同步任务会直接进入到主线程中执行
- 同步任务在主线程里执行,当浏览器第一遍过滤html文件的时候可以执行完(在当前作用域可以直接执行的所有内容,包括执行的方法、new出来的对象)
- 异步任务:与同步任务是相对的,异步任务不按照任务排序的顺序执行,也可以理解为异步是从主线程中发出一个子线程来完成任务,不进入主线程,直接进入任务队列,只有任务队列通知主线程某个异步任务可以执行的时候,该任务才会进入主线程执行
- 异步任务比较耗费时间与性能,浏览器执行到这些的时候会将其丢到异步任务队列中,不会立即执行
JS中常用的异步任务:setTimeout、setInterval、Promise、Ajax异步请求、DOM事件(click、热size、onload)
同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行,不断重复上述过程就是事件循环
异步任务又可以划分为微任务与宏任务
宏任务是由宿主发起的,而微任务由JavaScript自身发起。
在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务:
- Promise.then
- MutaionObserver
- Object.observe(已废弃,Proxy对象替代)
- process.nextTick(Nodejs)
宏任务:时间粒度比较大,执行的事件间隔不能精确控制,对一些高实时性的需求不太适合
常见的宏任务:
- srcipt(可以理解为外层同步代码)
- setTimeout、setInterval
- UI rendering、UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Nodejs)
执行一个宏任务,如果遇到微任务,就将它放到微任务的事件队列中,当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
async、await:
async是异步的意思,await可以理解为等待,async函数返回一个promise对象,下面这两种方法是等效的
正常情况下,await命令后面是一个Promise对象,返回该对象的结果,如果不是Promise对象,就直接返回对应的值
await会阻塞下面的代码(即加入微任务队列,先执行async外面的同步代码,同步代码执行完,再回到async函数中,执行之前阻塞的代码)
输出结果为:1、fn2、3、2
流程分析:
- 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
- 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
- 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 async1 end
- 继续执行下一个微任务,即执行 then 的回调,打印 promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout
- 最后结果:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout
ES6新特性
- 块级作用域变量,let 、const:用来声明块级作用域,使得变量只在当前作用域内有效
- 箭头函数:一种新的函数声明方式,可以更简洁的定义函数
- 解构赋值:可以用来快速的从数组或对象中提取值,并赋给变量
- 模板字符串:可以用来更方便的拼接字符串
- 默认参数:函数可以设置默认参数,当调用函数没有传入参数时,会使用默认值
- 扩展运算符:可以用来将数组或对象展开成一系列使用逗号分隔的参数序列
- 类和继承:引入了class关键字定义类和继承关系
- Promise:一种新的异步编程方式,可以更方便的处理异步操作,解决地狱回调
promise构造函数是同步执行的,then方法是异步执行的
- async、await:async也是处理异步的,是对Promise的一种扩展,让异步更方便,async返回一个Promise对象,可以使用then方法添加回调函数,当函数执行时,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句
- 数据结构Map:是一个构造函数,通过new生成Map数据结构实例,类似于对象,也是提供键值对的集合,但"键"的范围不限于字符串,各种类型的值(包括对象)都可以当做键
- 数据结构Set:是一个构造函数,通过new生成Set数据结构实例,类似于数组,但是成员的值都是唯一的,没有重复的值
- for...of循环:可以使用的范围包括数组、Set、Map结构,某些类似数组的对象(比如arguments对象、DOM NodeList对象)、字符串,不能遍历普通Object对象,for...in循环,只能获得对象的键名,不能直接获取属性值,for...of允许遍历获得属性值
- Symbol:是一种基本类型,用于表示独一无二的值,用来解决对象属性太多导致属性名冲突覆盖的问题
js
const a=Symbol()
const b=Symbol()
a===b // false
const c=a
a===c //true
const d=Symbol('xxx') //使用字符串标识
通过Symbol()创建的变量a,除了通过a,任何人在任何作用域都无法重新创建出这样一个值
Symbol真正存储了什么并不重要,重要的是它的值永远不会与别的值相等,Symbol的中文释义为"标志、符号",一个Symbol类型的变量只是为了标记一块唯一的内存而存在的,所以,Symbol类型的值不参与运算