vue3
1、 vue2和vue3有什么区别,做了哪些改变?
Options API:
- 在处理一个大型的组件时,内部的逻辑点容易碎片化,代码的可读性随着组件变大而变差。
- 每一种代码复用的方式,都存在缺点,服用mixin容易造成命名冲突,来源不清晰
- TypeScript支持有限
Composition API:
- 低内聚,高耦合,所有代码放在setup中,可读性高(逻辑组织和逻辑复用方面较高)
- 数据来源清晰,引入外部编写的hook函数,会有更好的类型推断
- 对 tree-shaking 友好,代码也更容易压缩
- 减少了this的不明指向
- TypeScript 提供了更好的类型检查,能支持复杂的类型推断
vue2和vue3生命周期:
vue3性能提升方面:
(编译阶段)
- 重写 Vdom(diff算法优化):增加了静态标记,会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较
- 进行模板编译优化静态提升:对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用
- 事件监听缓存:默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化
- SSR优化:当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染
(打包阶段)
- 利用Tree shanking任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
(响应式系统)
vue2:采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式。 Object.defineProperty只能遍历对象属性进行劫持。
- 检测不到对象属性的添加和删除
- 数组API方法无法监听到
- 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。
vue3:采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历。
- 可以直接监听对象而非属性
- 可以监听到数组的索引和数组length属性
- 可以监听删除属性
- Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has
- Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象
vue3新增组件:
- Fragment 不再限制 template 只有一个根节点。
- Teleport 传送门,允许我们将控制的内容传送到任意的 DOM 中。
- Suspense 等待异步组件时渲染一些额外的内容,让应用有更好的用户体验。
- vue3中v-if的优先级更高
通信方式:
vue3通信:
- props
- $emit
- expose / ref
- $attrs
- v-model
- provide / inject(原理:原型链)
- Vuex/pinia
- mitt
vue2通信:
- props
- $emit / v-on
- .sync
- v-model
- ref
- children/children / children/parent
- attrs/attrs / attrs/listeners
- provide / inject
- EventBus
- Vuex
- $root
- slot
2、什么是hooks函数,怎么用?
hooks函数:以函数形式抽离一些可复用的方法像钩子一样挂着,随时可以引入和调用,实现高内聚低耦合的目标。
- 将可复用功能抽离为外部js文件
- 函数名/文件名以use开头,形如:useXX
- 引用时将响应式变量或者方法显式解构暴露出来如:const {nameRef,Fn} = useXX() (在setup函数解构出自定义hooks的变量和方法)
3、 谈谈对Tree shanking的理解?
Tree shanking 是一种通过清除多余代码方式来优化项目打包体积的技术。 基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。编译阶段利用ES6 Module判断哪些模块已经加载,判断那些模块和变量未被使用或者引用,进而删除对应代码。 作用:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序架构进行优化(更友好)
4、 什么是setup语法糖?
- 属性和方法不需要返回,直接使用
- 引入的组件components会自动注册
- 使用 defineProps 接收父组件传递的值
const prop = defineProps(['isActive'])
const prop = defineProps({ isActive: Boolean })
const prop = defineProps<{ isActive: boolean }>()
- 使用 widthDefaults 接收父组件传递的值和设置默认值
// 分离式
const props = withDefaults( defineProps<{ isActive?: boolean }>(), { isActive: false, });
type Props = { isActive?: boolean; }
// 组合式
const props = withDefaults(defineProps<Props>(), { isActive: false })
const { isActive } = toRefs(prop) /prop.isActive
- useAttrs 获取属性
const attrs = useAttrs();
- useSlots 获取插槽
const slots = useSlots();
- defineEmits 获取自定义事件
// 传递父组件的参数
const emit = defineEmits(["toggleClick"])
const toggleClick = () => { emit('toggleClick') }
- defineExpose 对外暴露
const dialogVisible = ref(false);
const openDialog = () => { dialogVisible.value = true; };
// 暴露属性和方法
defineExpose({ openDialog });
// 暴露出来的方法
passwordRef.value?.openDialog();
5、 ref和reactive区别?
ref: 创建的数据在模板中可以直接使用,js中需要加.value,可以接收原始数据类型与引用数据类型,ref 底层还是使用 reactive 来做,在其基础上增强,支持对原始数据的处理 。 reactive:只能接收引用数据类型。
6、 watch和watchEffect的区别?
watch:
- 既要指明监视的数据源,也要指明监视的回调
- 可以访问改变之前和之后的值
- 不会立即执行,值改变后才会执行
watchEffect:
- 可以自动监听数据源作为依赖
- 只能获取改变后的值
- 立即执行,没有返回值
vue2
1、 什么是MVVM模式?
MVVM 即Model-View-ViewModel(模型-视图-控制器)是一种双向数据绑定的模式, 用 viewModel 来建立起 model 数据层和 view 视图层的连接,数据改变会影响视图,视图改变会影响数据。
2、 vue2响应式原理?
采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 数据劫持,来劫持各个属性的setter,getter,在数据更新时发布消息给订阅者,触发相应监听回调。(无法监听对象属性的添加和删除以及数组内部的变化)
原理:通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。
数据观察者:observer ,订阅-发布者:watcher ,compile:把 template 模板编译成浏览器可识别的 HTML
var obj = {
foo: 'foo'
}
Object.defineProperty(obj, 'foo', {
get: function () {
console.log('将要读取obj.foo属性');
},
set: function (newVal) {
console.log('当前值为', newVal);
}
});
obj.foo; // 将要读取obj.foo属性
obj.foo = 'name'; // 当前值为 name
3、 v-model双向数据绑定原理?
v-model指令的实现,一个input事件和value的语法糖,通过v-model指令为组件添加上input事件处理和value属性的赋值。
核心:v-bind:绑定响应式数据,触发input事件并传递数据。
v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件
- checkbox 和 radio 使用 checked 属性和 change 事件
- select 字段将 value 作为 prop 并将 change 作为事件
<input v-model='localValue'/>
<!-- 相当于:添加input事件的监听和value的属性绑定 -->
<input @input='onInput' :value='localValue' />
data(){return{ localValue:' '}}
<!-- 在input事件处理函数中更新value的绑定值 -->
methods: {onInput(v){ this.localValue=v.target.value;}}
4、 vue生命周期有哪些?
定义:Vue 实例从创建到销毁的过程,从开始创建、初始化数据、编译模板、挂载DOM-渲染、更新-渲染、卸载等一系列的过程。
作用:在生命周期的不同阶段调用对应的钩子函数可以实现组件数据管理和DOM渲染。
| 生命周期 | 说明 |
|---|---|
| beforeCreate | Vue实例创建之前,data和methods中的数据或方法还未初始化 |
| created | 在Vue实例化之后,data和methods已经初始化完成,模板还没有编译,不能获取到DOM |
| beforeMount | 在模板渲染之前,DOM节点挂载到真实DOM树之前调用,模板进行编译,会调用render函数生成vDom(虚拟DOM),无法获取DOM节点 |
| mounted | 模板编译好了,虚拟节点挂载到真实DOM树上 |
| beforeUpdate | 在data数据发生变化之后调用,页面上DOM还没有更新最新的数据 |
| updated | 在data数据更新之后执行,而且此时页面也渲染更新完成了,显示的就是最新的数据 |
| beforeDestory | Vue组件实例销毁之前,Vue组件实例销毁之前,还可以正常使用 |
| destoryed | 组件实例销毁之后执行,此时所有的组件包括子组件都被销毁了 |
| activated | 被 keep-alive 缓存的组件激活时调用 |
| deactivated | 组件被移除时使用 |
第一次页面加载会触发:beforeCreate , created , beforeMount ,mounted
vue父子生命周期执行顺序:
加载渲染过程:父beforeCreate ➜ 父create ➜ 父beforeMount ➜ 子beforeCreate ➜ 子created ➜ 子mounted ➜ 父mounted
更新渲染过程:父beforeUpdate ➜ 子beforeUpdate ➜ 子updated ➜ 父updated
销毁过程:父beforeDestroy ➜ 子beforeDestroy ➜ 子destroyed ➜ 父destroyed
5、什么单页面(SPA)与多页面(MPA)?
- 单页面:只有一个html文件,浏览器一开始必须加载所有的html,js,css,在vue中通过 vue-router来局部切换组件,而非刷新整个页面,来实现无刷新切换页面的技术。
优点:前后端分离,组件化规范,内容改变不需要加载整个页面,减少了请求体积,加快页面响应速度,降低了对服务器的压力
缺点:不利于seo,首次加载比较耗费时间,导航不可用。 - 多页面:一个应用中有多个页面,页面跳转时是整页刷新
优点: 容易seo 页面重复代码多
缺点:不好维护,页面加载缓慢
6、 v-show和v-if的区别?
相同点:都可以控制dom元素的显示和隐藏
不同点:
v-show:基于css切换,改变css中的display属性,dom元素依旧还在 适合频繁切换时使用
v-if:对dom元素进行添加和删除操作,渲染条件为假时,并不做操作,再次切换需要重新渲染页面,有一个局部编译/卸载的过程。
7、vue中的data为啥是函数?
每个组件可能被多个组件复用,复用组件的时候,都会返回一份新的data,防止产生数据污染,相当于每个组件实例都有自己私有的数据空间, 不会共享同一个data对象。
8、vue有哪些常用指令?
v-text:更新元素的文本内容
v-html:更新元素的 innerHTML
v-show:根据表达式的真假值,切换元素的display值,用于控制元素的展示和隐藏
v-if:根据表达式的真假值来有条件地渲染元素
v-for:基于数组来渲染列表
v-on:给元素绑定事件
v-bind:用于绑定数据和元素属性
v-model:双向数据绑定
v-slot:用于提供具名插槽或需要接收 prop 的插槽
v-cloak:用于解决插值表达式在页面闪烁问题
v-once:用于表示只渲染一次
9、watch和computed区别?
watch:监听属性 一对多( 当一条数据影响多条数据的时候) 没有缓存 异步监听拿到最新值,immediate(立即执行) deep(深度监听对象)
应用场景:搜索数据,监听某个数据拿到更改值进行某些操作,异步操作
computed:计算属性 存在缓存 多对一(当一个属性受多个属性影响的时候) 有缓存 同步监听 只要数据没改变只会执行一次(不支持异步)
应用场景:购物车商品结算,字符串拼接,复杂计算
10、vue组件通信有哪几种方式?
父子组件:
- props/$emit
- 插槽slot
- .async修饰符:子组件内部改变props属性值并更新到父组件中
父组件 利用eventBus,子组件触发事件,父组件响应事件并实现数据的更新
<child :money.sync="total"></child>
<child :money="total" v-on:update:money="total = $event"/>
子组件
<button @click="$emit('update:money', money-100)">
props: ["money"]
- ref:在普通的 DOM 元素上使用,指向的就是 DOM 元素,在子组件上,引用就指向组件实例
this.$refs.属性/方法 this.$root/this.$parent/this.$children:访问根/ 父 / 子组件上的方法和属性
兄弟、跨级:
- 中央事件总线(事件中心)eventBus
import Vue from 'vue';
window.eventBus = new Vue(); //1.直接注册在window上
Vue.prototype.$eventBus = new Vue(); // 2.通过修改Vue原型链的方式注册全局变量 this.$eventBus
eventBus.$emit(事件名,数据); 组件emit触发事件
eventBus.$on(事件名,data => {}); on接收事件
祖先组件向其所有子孙后代注入依赖:
- Provide Inject(非响应式)
provide:{name:'Mary'}
inject: ['name']
provide() {
return {
color:'red',
changeMonthSelected: this.changeMonthSelected
})
//解决响应式
provide() { return { color: this}} //方法一:提供祖先组件的实例
provide() {this.color = Vue.observable({color: "blue"})} //方法二:使用2.6最新API Vue.observable 优化响应式
// 函数式组件取值不一样
inject:{color:{default:() => {}},changeMonthSelected: { default: () => {}} }
多级组件嵌套:
-
$attrs:包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 ( class 和 style 除外),作为普通的HTML特性应用在子组件的根元素上, inheritAttrs的值设为false, 这些默认的行为会禁止掉, 通过实例属性attrs,这些特性可以生效 A——>C
-
$listeners: 在组件A中,监听组件C触发的事件,就能把组件C发出的数据,传递给组件A C——>A
//A组件
<template>
<div>
<B :foo="foo" :bar="bar" @changeData="changeData" @another="another"></B>
</div>
</template>
data(){return{ foo:'foo',bar:'bar'}}
changeData (params) {this.foo = params} //foo ——> basktball
another (params) {this.bar = params} //bar——> foooooooooooo
//B组件
<template>
<div>
<C v-bind="$attrs" v-on="$listeners"></C>
</div>
</template>
props: ['foo'],
inheritAttrs: false,
console.log(this.$attrs); //bar
//C组件
<template>
<div>
<button @click='$listeners.changeData("basktball")'>改变数组</button>
<button @click='handleOther'>其他</button>
</div>
</template>
console.log(this.$attrs); //bar
handleOther () {
this.$listeners.another('foooooooooooo')
//或者
this.$emit("another", "foooooooooooo");
}
全局通信:
- 全局状态管理:vuex
- 缓存:cookie,localStorage,sessionStorage
this.$store.state.名称
commit('mutation 名称')
this.$store.dispatch('action 名称', data)
//获取单个值
localStorage["name"]="vanida";
localStorage.setItem("name","vanida");
//获取单个值
localStorage["name"]
localStorage.getItem("name");
//移除单个值
localStorage.removeItem("name");
localStorage.name="";
//清空所有缓存
localStorage.clear()
//设置获取多个值
let person = {name:"vanida","age":25};
localStorage.setItem("person",JSON.stringify(person));
JSON.parse(localStorage.getItem("person"));
路由传参:
- params:再次刷新页面时参数就会丢失(可以把参数缓存),不能和path一起使用
- qeury:传递的参数会显示在 url 后面,不安全,如TestVueRouterTo?code=8989
//params
this.$router.push({ name: `TestVueRouterTo`,params: { code: '8989'}})
console.log(this.$route.params.code)
//query
this.$router.push({ name: `TestVueRouterTo`,query: { page: '1', code: '8989'}})
this.$router.push({ path: `/testVueRouterTo`,query: { page: '1', code: '8989'}})
console.log(this.$route.query.code)
11、什么是vuex全局状态管理器?
用于集中式存储管理应用的所有组件状态,vuex的状态存储是响应式的,改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation,主要有以下五个属性:
- State:数据源
- Getter:对state数据过滤
- Mutation:只能同步更改state里面的数据
- Action:可以进行异步操作,并提交 Mutaion来 变更状态
- Module: Store 对象过大时,可根据具体的业务需求分为多个 Module
页面刷新vuex数据被清空解决办法:用localStorage 存储到本地,state里面的数据直接从缓存里面读取
state:{ count:2},
getters: {
getCount(state)=>state.count*state.count
}
mutations:{add(state, payload){
state.count += payload.amount
}}
actions:{
addAsync (context){
setTimeout(()=>{
//提交mutations
context.commit("add")
},1000)
}
}
import store from'@/store/index.js'
import {mapState,mapGetters,mapMutations,mapActions} from vuex
//获取state数据
this.$store.state.count
this.$store.getters.todoCount
computed:{
...mapState(['count']),
...mapGetters(['getCount'])
}
methods:{
//提交mutations
...mapMutations(['add','addn']),
addCount(){
this.$store.commit('add',{description:'abc'})
}
//提交actions
...mapActions(['addAsync'])
addCountAsync(){
this.$store.dispatch('addAsync',this.description)
}
}
12、keep-alive是干嘛用的?
keep-alive主要是用来组件缓存,包裹动态组件时,能在组件切换过程中将状态保留在内存中,缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。include 和 exclude 属性,允许组件有条件的进行缓存和不被缓存,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
场景:一个列表和一个详情重复切换,对列表组件使用进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染。
13、什么是插槽slot?
父组件往子组件里面放东西,像一个盒子,位置由子组件决定,放什么内容由父组件决定。
- 默认插槽
- 具名插槽:利用name标识
- 作用域插槽:子组件给父组件传递数据
<!-- 父组件 -->
<div>
<child-component>
<p>默认</p>
<template #header>
<p>具名</p>
</template>
<template v-slot:main="slotProps">
<p>作用域: {{ slotProps.data }}</p>
</template>
</child-component>
</div>
<!-- 子组件,组件名:child-component -->
<div>
<slot></slot>
<h1>子页面</h1>
<slot name="header"></slot>
<slot name="main" :data="data"></slot>
</div>
14、什么是mixins?
mixins是编写可复用的功能的一种方式,将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来,组件本身有的也被保留。
15、vue事件修饰符及应用场景?
stop:阻止事件冒泡(不会再向上传递触发其他事件)
prevent:阻止默认事件(a标签不再跳转)
capture :使用事件捕获(改变触发顺序从外到内)
self:事件本身(只有点击本身的时候才会触发,只阻止自身冒泡,然后继续冒泡)
once:只触发一次(和prevent联合使用,只第一次阻止默认事件,第二次不在阻止)
16、什么是$nextTick?
定义:当数据更新了,在dom中渲染后,自动执行该函数(异步)
原理:Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。
应用场景:
- 在created函数进行的DOM操作
- 在数据变化后要执行的某个操作
- 在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件
nextTick、set、forceUpdate区别:
- nextTick:在数据修改触发dom更新完成之后调用(异步)
- set:不能检测到对象属性的添加和删除,可以触发更新视图
- $forceUpdate:给对象或数组添加属性,页面无法识别,重新渲染虚拟dom,强制刷新
17、vue-router有什么作用?
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用(SPA)。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质就是建立起url和页面之间的映射关系。正是因为Vue做的都是单页应用,相当于只有一个主的index.html页面,所以无法用a标签来实现页面切换和跳转的。
18、vue中的路由钩子函数有哪些?
- 全局钩子: beforeEach(路由跳转前)、 afterEach(路由跳转后)、beforeResolve (在所有组件内守卫及异步路由组件解析后触发)
- 单个路由里面的钩子: beforeEnter (在路由配置上直
- 组件路由:beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave 在路由组件内
场景:
beforeEach:进入页面登录判断、管理员权限判断、浏览器判断
beforeRouteLeave :清除当前组件中的定时器、页面中有未关闭的窗口, 或未保存的内容时, 阻止页面跳转、保存相关内容到Vuex中或Session中
19、history和hash两种路由模式的区别?
history:
- 基于HTML5的history新增的API通过监听pushState与replaceState实现无刷新跳转页面的功能。
- 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
- history刷新会重新向服务器发起请求,需要在服务端做处理,如果服务端没有匹配当前的url,就会出现404页面。 hash:
- 在前端完成的,不需要后端服务的配合,兼容性好
- URL中会带有#,通过监听URL中hash值的变化,改变页面路由,每次 hash 值的变化会触发 onhashchange 事件
- 浏览器既不会向服务器发出请求,浏览器也不会刷新
20、style中scoped属性有什么作用?
作用:CSS 只作用于当前组件中的元素,组件之间的样式不互相污染,实现了组件的私有化,样式的模块化。
原理:
给HTML的DOM节点加一个不重复data属性(形如:data-v-2311c06a)来表示他的唯一性
在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器的哈希特征值(如[data-v-2311c06a])来私有化样式。
父组件覆盖子组件中的样式或更改第三方组件的样式:
- 两个style,一个用于私有样式,一个用于公有样式不加scoped
- 深度作用选择器(即样式穿透)
css: >>>
less: /deep/
scss: ::v-deep
21、什么是路由懒加载,有哪几种方式可以实现?
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。
- ES6标准语法import()
- vue异步组件
- webpack的require.ensure()
component: () => import('@/views/list.vue') }
component:(resolve) => require(['@/views/list.vue'], resolve)
component:(resolve)=>require.ensure([], () => resolve(require('@/components/.vue')),'list')
22、vue中Diff算法的作用?
Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,只会进行同级比较(深度优先算法),在diff比较的过程中,循环从两边向中间比较,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,实现精准地更新真实DOM,进而提高效率。
-虚拟DOM:用来描述真实DOM的javaScript对象,可以将多次修改的DOM一次性渲染到页面上,减少 JavaScript 操作真实 DOM 的带来的性能消耗,减少页面的重排重绘,提高渲染性能。
- 有key和没key的区别:key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点 更高效的查找到虚拟DOM
- v-for使用key目的:为了高效的更新渲染虚拟 DOM(使用key,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素)
- 为什么不推荐index作为key:当以数组的下标index作为index值时,其中一个元素(如增删改查)发生了变化就有可能导致所有元素的key值发生变化。
23、npm run serve的执行过程?
通过package.json文件里找到scripts下的serve 并执行指令,实际上就是执行了vue-cli-service serve 这条命令,不能直接执行这条指令,因为操作系统中不存在这条命令
而npm run serve能执行成功,是因为在安装依赖的时候,是通过npm install 来执行的,npm 在安装依赖的时候,会在node_modules/.bin/ 目录中创建好vue-cli-service 为名的几个可执行文件作为脚本来执行。相当于执行了 ./node_modules/.bin/vue-cli-service serve。
24、vue怎么做性能优化,可以从哪几个方面入手?
SPA优化:
- 减少入口文件体积
- 静态资源本地缓存
- 开启Gzip压缩
- 使用SSR,nuxt.js
编码阶段:
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 频繁切换的使用v-show,不频繁切换的使用v-if
- 循环调用子组件时添加key,key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
SEO优化:
- 预渲染
- 服务端渲染SSR,nuxt.js
打包优化:
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
客户体验:
- 骨架屏
Javascript
1、数据类型有哪几种?
基本数据类型:Number、String、Boolean、Null、Undefined、symbol(独一无二的值)、BigInt (任意大的整数)
复杂数据类型:Object(Function、Array、Date、RegExp)
2、什么是变量提升?
JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为
,js与其他语言一样,都要经历编译跟执行阶段。而js在编译阶段的时候,JS 引擎会搜集所有的变量声明,并且提前让声明生效。而剩下的语句需要等到执行阶段、等到执行到具体的某一句时才会生效。
原因:提高性能,容错性更好
问题:变量被覆盖、变量没有被销毁
console.log(num) //undefined
var num = 1
function getNum() {
console.log(num) //undefined
var num = 1
}
getNum()
3、判断类型可以用哪些方法?
- typeof:适用于判断(除null)基础类型,引用类型,除了function 全返回object类型
- instanceof:变量的原型链上是否有构造函数的prototype属性,(两个对象是否属于原型链的关系)
- constructor:每一个实例对象都可通过constructor来访问它的构造函数,null和undefined无效
-toString:Object.prototype.toString.call 返回对象的类型字符串 - IsNaN:判断一个值是否是非数字 NaN:非数字
typeof '5' // string
typeof 5 // number
typeof null // object
typeof undefined // undefined
typeof true // boolean
typeof NaN //number
typeof Symbol('5') // symbol
typeof 5n // bigint
typeof new Object(); // object
typeof Array; // object
typeof new Function(); // function
[] instanceof Array; // true
[] instanceof Object; // true
手写instanceof
function myInstanceof(Fn, obj) {
// 获取该函数显示原型
const prototype = Fn.prototype;
// 获取obj的隐式原型
let proto = obj.__proto__;
// 遍历原型链
while (proto) {
// 检测原型是否相等
if (proto === prototype) {
return true;
}
// 如果不等于则继续往深处查找
proto = proto.__proto__;
}
return false;
}
1.constructor === Number //true
'hello'.constructor === String //true
'5'.__proto__.constructor === String // true
[5].__proto__.constructor === Array // true
undefined.__proto__.constructor // Cannot read property '__proto__' of undefined
null.__proto__.constructor // Cannot read property '__proto__' of undefined
Object.prototype.toString.call('5') // [object String]
Object.prototype.toString.call(5) // [object Number]
Object.prototype.toString.call([5]) // [object Array]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
isNaN(123) //false
isNaN(0) //false
isNaN('123') //false
isNaN('Hello') //true
isNaN('2005/12/12') //true
isNaN('') //false
isNaN(true) //false
isNaN(undefined) //true
isNaN('NaN') //true
isNaN(NaN) //true
isNaN(null) //false
4、怎么进行类型转换?
转字符串:string() 变量.toString()
转数字:Number(str)、parseInt(str) 整数
5、==和===的区别?
== 会自动转换类型,再判断是否相等
===不会自动类型转换,直接去比较
6、字符串常用方法?
查找:indexOf lastIndexOf charAt
截取:substring(起始位置,结束位置(不包含)) substr(起始位置,截取长度) slice(起始位置,结束位置(不包含))
大小写:toUpperCase toLowerCase
分割:split 分割成数组
转换成字符串:toString
合并:contact
7、数组常用方法?
- 首、尾添加/删除元素:unshift shift push pop 添加(返回长度) 删除(返回删除的数据)
- 添加/删除元素:splice(start,n,添加元素) 开始位置 删除个数,添加元素
- 截取:slice(start,end) 选中start.end之间的元素
- 循环:map(返回新数组) forEach (会改变原数组,没有返回值)
- 筛选:filter(过滤满足条件,返回新数组) every(是否都符合条件) some(是否有满足条件)
- 查找:find(第一个符合条件的数组元素) findIndex(第一个符合条件的数组索引) indexOf includes
- 排序:sort reverse 颠倒顺序
- 合并:contact 扩展运算符 [...a,..b]
- 将数组转换成字符串:join()
- 将多维数组转换成一维数组:flat
- 转换数组:Array.from() Array.of() 'aaa'.split('')
8、js对象和json字符串互转?
JSON.parse(str) 将json解析为一个JS对象
JSON.stringify(obj) 将一个JS对象转换为JSON
{"name":"张三", "age":36} {name:"张三", age:36}
{name:"张三", age:36} {"name":"张三", "age":36}
9、js添怎么加事件?
dom元素.onclick=function(){}
dom元素.addvenListener(click(事件类型),function(){}(事件),false(冒泡)/true(捕获))
10、什么是事件流模型、事件传播、事件委托?
事件冒泡:当节点事件被触发时,会由内圈到外圈 div-->body-->html-->document 依次触发其祖先节点的同类型事件,直到DOM根节点。
事件捕获:当节点事件被触发时,会从DOM根节点开始,依次触发其子孙节点的同类型事件,直到当前节点自身。由外圈到内圈 document-->html-->body-->div。
事件流(事件传播)三个阶段:一个事件发生后,会在子元素和父元素之间传播
- 事件捕获阶段(目标在捕获阶段不接收事件) window对象传导到目标节点 外层——>内层
- 目标阶段 (事件的执行阶段,此阶段会被归入冒泡阶段) 在目标节点上触发
- 事件冒泡阶段 (事件传回Dom根节点)从目标节点导回window对象 内层——>外层
let btn = document.getElementById('btn3')
btn.addEventListener('click',function (event){
//事件名 触发的函数 是否在捕获(true)或者冒泡(false)阶段执行
//阻止事件冒泡
event.stopPropagation()
alert('btn1')
},true)
事件委托(事件代理):利用冒泡原理,让自己的所触发的事件,让他的父元素代替执行,也就是子元素委托它们的父级代为执行事件。
场景:监听所有li标签,可以直接使用事件委托来进行优化
优化:
提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件 。
动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。
let ulDom = document.getElementsByTagName('ul')
ulDom[0].addEventListener('click', function(event){ alert(event.target.innerHTML)})
11、offsetHeight、clientHeight、scrollHeight区别?
- offsetHeight = clientHeight + 滚动条 + 边框
- clientHeight=内边距+高度
- scrollHeight= clientHeight+当前不可见部分的元素的高度
- scrollTop: 代表在有滚动条时,滚动条向下滚动的距离也就是元素顶部被遮住部分的高度
- offsetTop: 当前元素顶部距离最近父元素顶部的距离,和有没有滚动条没有关系。
element.clientHeight=element.scrollHeight - element.scrollTop
12、DOM节点操作?
1)创建新节点
- createElement() //创建一个具体的元素
- createTextNode() //创建一个文本节点
2)添加、移除、替换、插入
- appendChild() //添加
- removeChild() //移除
- replaceChild() //替换
- insertBefore() //插入
3)查找
- getElementById(); //id
- getElementsByTagName();//标签名
- getElementsByClassName();//类名
- getElementsByName() //通过元素的Name属性的值
13、说说this指向?
- 规则1:在全局作用域中,this指向window
- 规则2:在普通函数中 的this,谁调用就指向谁
对象.方法() 对象
方法() 直接调用 window - 规则3:数组下标 数组/类数组对象
- 规则4:在IIFE(立即执行)函数中 window
- 规则5:定时器 ,延时器调用函数,采用回调函数作为处理函数 回调函数的this指向 window
- 规则6:事件处理函数 this指向绑定事件的dom元素
事件源.onclik = function(){ }
事件源.addEventListener(function(){ }) //this->事件源 - 规则7:call,apply,bind 任意指向上下文
- 规则8:用new操作符来执行函数相当于构造函数来实例化对象this->实例化对象
14、call、apply、bind区别?
- call: 函数.call(this要指向的对象,参数1,参数2) 参数列表,函数被调用时,会立即执行
- apply: 函数.apply(this要指向的对象,[参数1,参数2]) 参数数组 ,函数被调用时,会立即执行
- bind: 函数.bind(this要指向的对象,参数1,参数2)() 参数列表 函数被调用时,不会立即执行,而是返回一个新的函数。需要自己手动调用新的函数来改变this指向
注意:当this要指向的对象为null和undefined时,this指向window
相同点: 三者都可以把一个函数应用到其他对象身上,注意不是自身对象
不同点:
call,apply是直接执行函数调用。bind是绑定,执行需要再次调用。 call,bind接收逗号分隔的无限个参数列表;apply接收数组作为参数 。
15、new操作符做了什么?
new操作符用于创建一个给定构造函数的实例对象。
- 创建一个新的空对象
- 将这个空对象的__proto__指向构造函数的原型 prototype
- 将this指向空对象
- 对构造函数返回值做判断,如果构造函数有返回一个对象,则返回这个对象,否则返回新创建的那个对象 手写new操作符:
//手写new操作符:
function myNew(Con, ...args) {
// 创建一个新的空对象
let obj = {};
// 将这个空对象的__proto__指向构造函数的原型
// obj.__proto__ = Con.prototype;
Object.setPrototypeOf(obj, Con.prototype);
// 将this指向空对象
let res = Con.apply(obj, args);
// 对构造函数返回值做判断,然后返回对应的值
return res instanceof Object ? res : obj;}
//验证测试:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = myNew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui
16、什么是闭包有什么作用?
定义:闭包就是函数内部定义的函数,被返回了出去并在外部调用,内层函数中访问到其外层函数的作用域。一个闭包变量的值改变不会影响到另一个闭包的变量
闭包产生的原因:内部函数存在对外部函数局部变量的引用就会导致闭包。
特点:
- 希望一个变量长期驻扎在内存当中(不被垃圾回收机制回收)
- 避免全局变量的污染
- 私有成员的存在
- 安全性提高
缺点:局部变量会常驻内存中,长期占用,容易造成内存泄漏。
防止内存泄露:
- 不要随便改变上级作用域私有的变量值
- 不能滥用闭包,将不使用的局部变量全部删除 null
- 释放内存
场景:
- 立即执行函数,实现块级作用域 IFEE
- 希望某些函数内的变量在函数执行后不被销毁 创建内部变量不被其外部变量修改
- 使用回调函数就是在使用闭包 setTimeout传参、回调、防抖、节流
function foo(){
var result = [];
for(var i = 0;i<10;i++){
result[i] = function(){
console.log(i)
}
}
return result;
}
var result = foo();
result[0](); // 10
result[1](); // 10
//变量 i 被提升到了函数 foo 的作用域中。所以每个函数的作用域链中都保存着同一个变量 i
function foo(){
var result = [];
for(var i = 0;i<10;i++){
(function(i){
result[i] = function(){
console.log(i)
}
})(i)
}
return result;
}
var result = foo();
result[0](); // 0
result[1](); // 1
//立即执行函数形成一个新的闭包环境,这样即时函数内部就保存了本次循环的 i
function makeAdder(x){
return function(y){
return x+y;
}
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2));//7
console.log(add10(2));//12
for(let i = 0; i < 10; i++){
setTimeout(
function(){
console.log(i);
},1000)
}//1,2,3,4,5,6,7,8,9,10
//因为setTimeout是异步的,for循环是同步的,同步代码执行完,i已经是10了
17、浅拷贝与深拷贝的区别?
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)。
深拷贝:拷贝一份引用数据的引用地址 克隆所有层,使拷贝的值和被拷贝的值之间都不受影响
JSON.parse(JSON.stringify(obj))
原理: 将对象转成JSON字符串,把字符串解析成对象,新的对象产生了,会开辟新的栈,实现深拷贝。- 递归去复制所有层级属性
浅拷贝:拷贝一份引用数据的引用地址 只克隆数组/对象的第一层
- Object.assign({}, base1, base2)
- { ...base1, ...base2 }
- arr.concat([1,1,2,3])
//深拷贝
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断ojb子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];}}}}
return objClone;
}
let a=[1,2,3,4],
b=deepClone(a);
a[0]=2;
console.log(a,b);
18、数组怎么去重?
//1.set去重
Array.from(new Set([1,2,1,2,4,5,6]) //1,2,4,5,6
[...new Set([1,2,1,2,4,5,6])] //1,2,4,5,6
//2.循环去重
function removeDuplicate(arr) {
let len = arr.length
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
len--
// 减少循环次数提高性能
j--
// 保证j的值自加后不变
} } }
return arr
}
//3.indexOf去重
function removeDuplicate(arr) {
const newArr = [];
arr.forEach((item) => {
if (newArr.indexOf(item) === -1) {
newArr.push(item);
}
});
return newArr; // 返回一个新数组
}
//4.includes去重
function removeDuplicate(arr) {
const newArr = [];
arr.forEach((item) => {
if (!newArr.includes(item)) {
newArr.push(item);
}
});
return newArr;
}
const result = removeDuplicate(arr)
//5.reduce去重
let data = [
{ id: 1, name: "obj" },
{ id: 3, name: "string" },
{ id: 2, name: "arr" },
{ id: 1, name: "num" },
{ id: 1, name: "tttt" },
];
//首先设立一个空对象(逻辑:利用键的唯一性)
let hash = {};
data = data.reduce(function (prev, cur, index, arr) {
console.log(hash[cur.id]);
//把id作为键,如果键存在那么就不添加,否则就添加到数组里(prev为一个init默认数组)
// hash[cur.id]判断对象的键是否存在,不存在为undefind
hash[cur.id] ? "" : (hash[cur.id] = true && prev.push(cur));
return prev;
}, []);
console.log(data); //打印出来就看到已经去重了
//5.reduce借助indexof去重去重
let arr = [1,2,3,4,5,1,2,1];
// indexOf()判断数组第一次出现的下标
// console.log(arr.indexOf(5))
let newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
19、为什么要跨域,有哪几种方式?
指浏览器不能执行其他网站的脚本,由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。
同源策略: “协议+域名+端口” 三者都要相同
实现方式:
- jsonp 请求方式 dataType: 'jsonp', 原理:利用script标签没有跨域限制地漏洞来达到第三方通讯的目的
- CROS资源跨域共享 Response Headers:Access-Control-Allow-Origin
原理:允许浏览器向跨源服务器,发出XMLHttpRequest请求
- webpack代理跨域请求 vue.config.js 下的proxy添加baseUrl的target设置地址 request.js 中配置baseUrl
原理:网站A将访问网站B的请求通过参数的形式发送给代理服务器(proxy),代理服务器收到请求后转而去访问网站B,然后再将获取的信息再返回个网站A,形成一个数据请求回路。
- nginx反向代理服务 原理:利用服务端请求不会跨域的特性,让接口和当前站点同域。
20、防抖与节流有什么区别?
防抖:当一个动作连续触发,只执行最后一次
使用场景:登录、发短信等按钮避免用户点击太快造成多次请求、搜索框搜索输入\调整浏览器窗口大小
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
节流:限制一个动作在一段时间内只能执行一次
使用场景:scroll事件、input下拉框实时搜索
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
21、作用域和作用域链是什么?
作用域:指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限,解决了 全局作用域污染和变量名冲突的问题。 类型: 全局作用域 函数作用域 ES6块级作用域
作用域链:当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找...一直找到全局作用域为止。 执行上下文:当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中
22、原型和原型链是什么?
因为JS中没有类(Class)这个概念,所以JS的设计者使用了构造函数来实现继承机制。
原型对象:JS的每个函数在创建的时候,都会生成一个属性prototype,这个属性指向一个对象,这个对象就是此函数的原型对象。该原型对象中有个属性为constructor,指向该函数。使用原型对象的好处是利用原型创建出来的实例对象会共享原型上的所有属性和方法。
每个通过构造函数创建出来的实例对象,其本身有个属性__proto__,这个属性会指向该实例对象的构造函数的原型对象prototype,该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。
__proto__:是每个实例都有的属性,可以访问 [[prototype]] 属性
prototype:是构造函数的属性
实例的__proto__与其构造函数的prototype指向的是同一个对象
原型对象:当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数的原型对象prototype,如果还没有找到就会再在其构造函数的prototype的__proto__中查找,如果还没有就通过构造函数的prototype的__proto__找直到找到Object对象的原型,到这样一层一层向上查找就会形成一个链式结构,我们称为原型链。所有的对象都会有继承Object.prototype上的属性和方法,直到找到Object对象的原型,对象的原型链的尽头是null。
区别:原型是为了实现对象间的联系,解决构造函数无法数据共享而引入的一个属性,而原型链是一个实现对象间联系即继承的主要方法。
//构造函数
function Person(name){
//构造函数每执行一次就会创建一次sayName
this.name=name;
this.sayName(){
console.log(this.name)
}
}
//构造函数原型对象上的属性和方法,所有实例共享
Person.a='a'
Person.sayHello=function(){
console.log('hello')
}
//创建的实例对象
const p=new Person('张三');
const p1=new Person('李四');
p.sayName();//张三
p1.sayName();//李四
console.log(p.__proto__ === Person.prototype)//true
console.log(Object.getPrototypeof(p)===Person.prototype)//true
console.log(Person.prototype.constructor===Person)//true
console.log(Person.prototype.__proto__=Object.prototype)//true
arr.__proto__.reverse === Array.prototype.reverse // true
Array.prototype.constructor===Array // true
function Human(name, level) {
this.name = name;
this.level = level;
}
//设置一个指定的对象的原型 将Human原型设置成为Person原型
Object.setPrototypeOf(Human.prototype,Person.prototype);
23、谈谈js运行机制Eventloop事件循环?
js是一门单线程语言,同一时间内只能做一件事,所有任务都需要排队,而实现单线程非阻塞的方法就是事件循环,事件循环是js实现异步的一种方法。
任务分为同步任务和异步任务
- 同步任务:立即执行,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步任务:不进入主线程、而进入"任务队列"(
task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行 - 异步任务又分为微任务和宏任务,微任务先执行
- 事件循环:所有同步任务都在主线程上执行,形成一个执行栈(
execution context stack),异步任务进入任务队列,主线程内的任务执行完毕为空,系统会去任务队列读取对应的任务,根据顺序循环调用异步任务(微任务——>宏任务),进入执行栈,开始执行,直到所有任务队列异步任务执行完。主线程从"任务队列"中读取事件,这个过程是循环不断的被称为事件循环。
微任务:js引擎发起的
- Promise.then
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
宏任务:宿主(node,浏览器)发起的
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
js执行顺序:主线程任务(执行栈)——>(异步事件)——>微任务(任务队列)——>宏任务(任务队列)。
24、高阶函数、回调函数以及纯函数的区别?
高阶函数回调函数:一个函数就可以接收另一个函数作为参数 前者是高阶函数,后者是回调函数。
纯函数:相同的输入总会得到相同的输出
25、判断元素是否在可视区域?
- 公式: el.offsetTop-document.documentElement.scrollTop <= viewPortHeight
- 公式: el.getBoundingClientReact().top <= viewPortHeight
- 公式: intersectionRatio > 0 && intersectionRatio <= 1
26、web常见的攻击方式有哪些?
SQL注入攻击:攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变 注入的位置包括:
(1)表单提交,主要是POST请求,也包括GET请求; 密码 OR '1'='1' (2)URL参数提交,主要为GET请求参数; (3)Cookie参数提交; (4)HTTP请求头部的一些可修改的值,比如Referer、User_Agent等;
防御:
- 严格检查输入变量的类型和格式
- 过滤和转义特殊字符
- 对访问数据库的 Web 应用程序采用 Web 应用防火墙
CSRF跨站点请求伪造:攻击者盗用了你的名义,以你的名义发送了其他请求。本质是利用了 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。攻击一般发起在第三方网站。攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作
攻击类型:
- GET 类型的 CSRF 攻击,在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交
- POST 类型的 CSRF 攻击,构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单
- 链接类型的 CSRF 攻击,在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击
防御:(阻止不明外域的访问)
- cookie设置
Samesite,限制 cookie 不能作为被第三方使用, 提交时要求附加本域才能获取的信息。 - 增加token
- 同源检测,服务器根据 http 请求头中
origin或者referer信息来判断请求是否为允许访问的站点。
XSS跨站脚本攻击:攻击者将恶意代码植入到提供给其它用户使用的页面中,为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。
攻击类型:
- 存储型:恶意代码提交到了网站的数据库中,当用户请求数据的时候,服务器将其拼接为 HTML 后返回给了用户。
- 反射型:攻击者构建了特殊的 URL,当服务器接收到请求后,从 URL 中获取数据,拼接到 HTML 后返回。
- DOM型:DOM 型指的是攻击者构建了特殊的 URL,用户打开网站后,js 脚本从 URL 中获取数据,从而导致了恶意代码的执行。
防御:
- cookie 使用 http-only,使得脚本无法获取
- 对存入数据库的数据都进行的转义处理
- 使用CSP,CSP的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。
27、JS引擎解析过程?
词法分析 -> 得到token -> 语法分析 -> 得到AST -> 翻译器 -> 字节码(bytecode) -> 字节码解释器 -> 机器码
28、js加载脚本的方式有哪几种,分别是什么?
- 正常模式 浏览器是同步加载 JavaScript 脚本 即渲染引擎遇到
<script>标签就会停下来,等到执行完脚本,再继续向下渲染。 - async 异步,脚本在执行时不会影响页面的构造,浏览器会立即下载,但延迟执行延迟到整个页面都解析完毕之后再执行无顺序
- defer 异步,defer 资源要等到整个页面渲染完再执行,有顺序
- module 浏览器加载ES6模块,浏览器会对其内部的 import 引用发起 HTTP 请求,获取模块内容 页面渲染完再执行有顺序 Vite 就是利用浏览器支持原生的 es module 模块,开发时跳过打包的过程,提升编译效率
- preload 提前加载一些需要的依赖,会优先加载 浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件
- prefetch 利用浏览器的空闲时间,加载加载其他页面(非首页)所需要的资源
<!-- 页面内嵌的脚本 -->
<script type="application/javascript"></script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js"></script>
<script src="index.js"></script>
<script async src="index.js"></script>
<script defer ></script>
<script type="module">import { a } from './a.js'</script>
<link rel="preload" as="script" href="index.js">
<link rel="prefetch" as="script" href="index.js">
ES6
1、var、let、const有什么区别?
var:变量 重复声明 变量提升 函数作用域可以跨函数访问
let: 变量 不允许重复声明 暂时性死区 不存在变量提升 块级作用域
const:常量 声明后必须初始化 不能重复赋值 (对象的属性除外) 暂时性死区(只能在声明的位置后面使用)
暂时性死区:在代码块内,使用let命令声明变量之前,该变量都是不可用的
声明变量的方式:var、let、const、 function、 import、class
2、什么叫暂时性死区?
ES6新增的let、const关键字声明的变量会产生块级作用域,如果变量在当前作用域中被创建之前被创建出来,由于此时还未进行词法绑定,如果我们访问或使用该变量,就会产生暂时性死区的问题,从变量的创建到词法绑定之间这一段时间,称为暂时性死区。
3、模板字符串有什么作用?
用反引号标识,它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量,如:hello ${name}。
4、什么是扩展运算符?
用...标识,将一个数组转为用逗号分隔的参数序列,如:...[2, 3, 4] 变成:2,3,4。
5、链判断运算符?.是什么 ?
判断一下该对象是否存在,相当于三元运算符? :,如:user?.firstName
- obj?.prop // 对象属性
- obj?.[expr] // 同上
- func?.(...args) // 函数或对象方法的调用
6、null判断运算符??是什么?
某个属性的值是null或undefined,可以指定默认值,如: settings.headerText ?? 'Hello, world!'
7、说说数组新增的方法?
- Array.from ,扩展运算符... :将类数组转换成数组 类数组:NodeList对象,arguments对象。可以用来去重 Array.from(new Set(['a',’b', 'b'])) ———>[a,b]
- Array.of:将一组值,转换为数组 Array.of(3, 11, 8)———> [3,11,8]
- find:找出第一个符合条件的组数成员,否则undefined findIndex:返回第一个符合条件的数组成员的位置 否则-1
- includes:查找某个数组是否包含给定的值,返回布尔值 indexOf:找到参数值的第一个出现位置,否则返回-1
- fill:用给定值填充一个数组 ['a', 'b', 'c'].fill(7) // [7, 7, 7]
- flat:多维数组拉平成一维数组
- entries(),keys(),values()——用于遍历数组
- foreach map filter every some——用于数组迭代
- for...of :遍历数组
for (let index of ['a', 'b'].keys()) { console.log(index); } //0,1
for (let elem of ['a', 'b'].values()) { console.log(elem); } //a,b
for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem); } // 0 "a" // 1 "b"
8、说说对象新增的方法?
- Object.assign:用于对象的合并
- Object.setPrototypeOf(object, prototype):设置对象的原型
- Object.getPrototypeOf():获取对象的原型
- for...in:遍历对象 遍历的key值
- Object.keys()、Object.values()、Object.entries():返回一个数组,对象所有可遍历属性的键名、值、键值对
let obj = { a: 1, b: 2, c: 3 };
for (let key of Object.keys(obj)) { console.log(key); } // 'a', 'b', 'c'
for (let value of Object.values(obj)) { console.log(value); } // 1, 2, 3
for (let [key, value] of Object.entries()) { console.log(key, value); } // ['a', 1], ['b', 2], ['c', 3]
9、箭头函数与普通函数的区别?
- 语法简洁 书写方式:参数 => 函数体
- 内部没有arguments,用剩余参数rest代替 ...args 是数组
- 箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this
- 使用call,apply,bind并不会改变箭头函数中的this指向
- 箭头函数是匿名函数,不能作为构造函数,不可以使用new命令
- 箭头函数没有原型对象prototype这个属性
单行语句:let fn = a => a * a;
多行语句:let print = () => {}
10、说说Set和Map区别?
Set:类似数组,值是唯一的,没有重复的值,一种集合的数据结构,具有以下属性和方法:add delete has clear .size
Map:键值对的集合,“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键 一种叫做字典的数据结构,具有一下属性和方法:set get has delete clear .size
共同点:都可以存储不重复的值
不同点:
集合:是由一堆无序的、相关联的,且不重复的内存结构【组成的组合 以[值,值]的形式存储元素
字典:是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同 以[键,值]的形式存储
11、什么是Promise,解决了什么问题以及应用场景?
Promise异步编程的一种解决方案,主要是为了解决回调地狱。 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息,Promise本身是同步的。
优点:
- 链式操作减低了编码难度
- 代码可读性明显增强
特点:
- 对象的状态不受外界影响,只有异步操作的结果,可以决定当前状态。
- 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。pending ——>fulfilled 或者 pending——>rejected
实例方法:(异步)
- then():是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数
- catch():用于指定发生错误时的回调函数
- finally():用于指定不管 Promise 对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
构造函数方法:
all、race、allSettled、用于将多个 Promise实例,包装成一个新的 Promise实例,接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例。
- all() :所有的Promise实例状态都为fulfilled,才会为fulfilled,否则为rejected 谁跑的慢,以谁为准执行回调。
- race():Promise实例状态中第一个改变状态,实例的状态跟着改变 谁跑的快,以谁为准执行回调。
- allSettled():只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束
- resolve():将现有对象转为 Promise对象
- reject():返回一个新的 Promise 实例,该实例的状态为rejected
使用方式和使用场景:
const p = Promise.all([p1, p2, p3]);
const p = Promise.resolve('成功了');
const p = new Promise((resolve, reject) => resolve('成功了'))
const p = Promise.reject('出错了');
const p = new Promise((resolve, reject) => reject('出错了'))
//使用场景:
//Promise 将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
Promise.then
当下个异步请求依赖上个请求结果可以通过链式操作
// 各司其职
getInfo().then(res=>{ let { bannerList } = res //渲染轮播图 console.log(bannerList) return res})
.then(res=>{ let { storeList } = res //渲染店铺列表 console.log(storeList) return res})
.then(res=>{ let { categoryList } = res console.log(categoryList) //渲染分类列表 return res })
//Promise.all
//1.多个验证是否都是满足条件
onSubmit() {
const basicsFormValidate =this.$refs['basicsForm'].$refs.ruleForm.validate()
const infoFormValidate = this.$refs['infoForm'].$refs.ruleForm.validate()
Promise.all([basicsFormValidate, infoFormValidate]).then((res) => {
this.$confirm('你确定要提交吗?', null, async () => {
this.doSendHttp() })
}).catch((err) => {
this.$message('请填写完整信息再提交!', 'warning')
})
}
//2.多个请求合并,汇总所有请求结果,只需要设置一个loading
initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getCategoryList()]).then(res=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//Promise.race
//1.接口请求超时 2.图片请求超时
sleep(delay) {
return new Promise((_, reject) => {
setTimeout(() => reject('超时喽'), delay)
})
}
request() {
// 假设请求需要 1s
return new Promise(resolve => {
setTimeout(() => resolve('成功喽'), 1000) })
}
function timeoutPromise(requestFn, delay) {
// 如果先返回的是延迟Promise则说明超时了
return Promise.race([requestFn(), sleep(delay)]).then(res=>{ console.log(res) }).catch(err=>{
console.log(err)//超时喽
})
}
12、谈谈async await有什么作用?
async await是Promise的语法糖,为优化then链而开发出来的。async 用来声明一个异步方法,而 await 用 于等待一个异步方法执行完成。它可以很好的替代promise 中的 then。async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇 到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句,不管await后面跟着的是什么,await都会阻塞后面的代码。
async 会将其后的函数的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,它会阻塞后面的代码,等着 Promise 对象返回内容(resolve或reject的参数),然后得到Promise的值,作为 await 表达式的运算结果。如果等待的不是 Promise 对象,就直接返回对应的值。
await必须放在async函数原因:await 会阻塞后面的代码,async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。
与Promise的区别:
- 不再需要多层.then方法
- 可以对Promise进行并行处理
async f(){ return 'hello world'; }
f().then(v => console.log(v)) // "hello world"
async function f() { throw new Error('出错了'); }
f().then( v => console.log(v),e => console.log(e)) // Error: 出错了
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行状态已经变成了reject
}
//当第一个异步操作失败,也不要中断后面的异步操作,将第一个await放在try...catch结构里面
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {}
return await Promise.resolve('hello world');
}
//或者 await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了').catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出错了
// hello world
13、说说什么是Proxy?
Proxy:代理、拦截目标对象进行过滤。
语法:var proxy = new Proxy(target, handler);
target:要拦截的目标对象,handler:是一个对象,用来定制拦截行为 Reflect操作对象。
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
return Reflect.set(target, propKey, value, receiver);
}
});
14、什么是Module?
使用 import, export 关键字来进行模块输入输出。ES6 不再是使用闭包和函数封装的方式进行模块化,而是从语法层面提供了模块化的功能。
export { default as AppMain } from './AppMain.vue'
import { AppMain} from "../components";
export const useListEffect=()=>{}
import { useListEffect } from './hooks/useListEffect'
export default storage
import storage from '@/utils/storage'
HTTP
1、五大浏览器的内核分别是什么?
2、HTTP常见状态码有哪些?
- 200:请求成功
- 301:永久重定向 请求的资源已被永久的移动到新URI
- 302:临时性重定向
- 304:所请求的资源未修改,用于缓存的目的
- 400:客户端请求报文中存在语法错误,服务器无法理解
- 401:请求要求用户的身份认证
- 403:没有访问内容的权限,拒绝执行此请求
- 404:服务器找不到请求的资源(网页)
- 500:服务器内部错误,无法完成请求
- 501:服务器不支持请求的功能
- 502: 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
- 503:服务器暂时处于超负载或正在停机维护
2、HTTP请求方式?get和post区别?
- 主要有GET、POS、HEAD、PUT、DELETE、CONNECT、OPTIONS、TRACE。
- get: 获取数据、参数会放在url中、安全性差、请求的数据长度有限制在 2~8K 请求可以被缓存 请求会被保存在浏览器历史记录当中
- post:提交数据 、安全性较高、请求的数据长度无限制、不会被缓存、不会被保存在浏览器、请求回退时会重新提交数据请求 通过请求体request body传递参数
3、谈谈ajax、原理、请求步骤?
ajax是一种异步请求数据的web开发技术、在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。
Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。
请求步骤:
- 创建XMLHttpRequest对象;
- 注册回调函数;
- 配置请求信息,(如open,get方法),使用open方法与服务器建立链接;
- 向服务器发送数据;
- 创建回调函数、在回调函数中针对不同的响应状态进行处理;
//第一步,创建XMLHttpRequest对象
var xmlHttp = new XMLHttpRequest();
function CommentAll() {
//第二步,注册回调函数
xmlHttp.onreadystatechange =callback1;
//第三步,配置请求信息,open(),get
//get请求下参数加在url后,.ashx?methodName = GetAllComment&str1=str1&str2=str2
xmlHttp.open("post", "/ashx/myzhuye/Detail.ashx?methodName=GetAllComment", true);
//post请求下需要配置请求头信息
//xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
//第四步,发送请求,post请求下,要传递的参数放这
xmlHttp.send("methodName = GetAllComment&str1=str1&str2=str2");//"
}
//第五步,创建回调函数
function callback1() {
if (xmlHttp.readyState == 4)
if (xmlHttp.status == 200) {
//获取数据
console.log(xmlHttp.responseText,xmlHttp.response);
}
}
4、HTTP缓存方式?
- cookie: 4k 可设置失效时间,否则默认为关闭浏览器后消失 会随着HTTP请求发送到服务器 标记跟踪用户行为
- localStorage:5M 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据 本地保存 token令牌
- sessionStorage:5M 存储持久数据,仅在当前网页会话下有效,关闭页面或浏览器后就会被清除 本地保存 敏感账号一次性登录
5、强缓存和协商缓存区别?
- 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是200,主要有Expires和Cache-Control来访问本地缓存并直接验证看是否过期,设置了 no-cache 和 no-store,会忽略本地缓存。
- 协商缓存:会去请求服务器验证资源是否更新,如果没更新才继续使用本地缓存,返回的状态码是304,主要包括 last-modified和etag来验证是否被修改。
使用场景:
- 标签进入、输入url回车进入:如果本地缓存没过期会走走强缓存路线,否则走协商缓存
- 按刷新按钮、F5 刷新、网页右键“重新加载”:浏览器将 cache-control 的 max-age 直接设置成了 0,直接走协商缓存路线
- ctrl + F5 强制刷新:无任何缓存
6、地址栏输入url到页面展示经历的过程?
- 浏览器输入url并解析
- 查询当前url是否存在缓存,并对比缓存是否过期,有就直接返回资源给浏览器进程
- DNS解析url到指定的ip ( DNS域名解析得到IP地址) 请求协议是https,需要建立TLS连接
- 利用IP地址和服务器建立TCP连接(3次握手)
- 发送HTTP/HTTPS请求(请求行/头/体)
- 服务器响应请求(服务器解析这个请求,然后返回一个HTTP响应报文)
- 浏览器拿到响应文本 HTML 后解析并渲染页面
- 根据 HTML 解析出 DOM 树
- 根据 CSS 解析生成 CSS 规则树
- 结合 DOM 树和 CSS 规则树,生成渲染树
- 根据渲染树进行布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息,然后将各个节点渲染到屏幕上
- 断开TCP连接
7、浏览器运行机制,页面渲染过程?
- HTML 被 HTML 解析器解析成 DOM 树;
- CSS 被 CSS 解析器解析成 CSSOM 树;
- 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
- 根据渲染树进行布局(flow),负责各个元素节点的尺寸、位置计算
- 绘制Render树(Painting),绘制页面像素信息
- 浏览器主进程将默认图层和复合图层交给GPU进程,GPU进程再将各个图层合成(composite),最后显示出页面
8、浏览器渲染进程包含哪些线程?
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时器出发线程
- 异步HTTP请求线程
9、谈谈重绘与回流?
- 重绘:某些元素的外观被改变并不影响布局 。例如:color、background-color、visibility等
- 重排(重构/回流reflow):引起dom结构变化,改变页面布局,重新生成布局,重新排列元素 。例如:height,width、padding、margin、display、border等
重绘不一定导致重排,但重排一定会导致重绘。
引发重排:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
重排影响范围:
- 全局范围:从根节点html开始对整个渲染树进行重新布局。
- 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
重排优化:
- 减少重排范围(尽量样式直接对子元素进行操作)
- 使用style的cssText合并操作样式/给标签直接添加类名
- 避免使用table布局
- 将需要多次重排的元素,position属性设为absolute或fixed,元素脱离了文档流,它的变化不会影响到其他元素
- 先设置元素为display:none;然后进行页面布局等操作;设置完成后将元素设置为display:block。
10、说说网络通信协议http和https区别?
通信协议,简单来说就是浏览器和服务器之间沟通的语言。网站中的通信协议一般就是HTTP协议和HTTPS协议。
http:超文本传输协议,是一种使用明文数据传输的网络协议,HTTP工作于应用层,默认端口号:80
https:超文本传输安全协议 有ssl/tsl证书,依靠证书来验证服务器的身份并为浏览器和服务器通信加密,信息安全,HTTPS工作于传输层,默认端口号:443
11、http1.0到http3.0协议都有什么变化?
- http1.0:无状态无连接的应用层协议 短连接
- http1.1:使用TCP长连接的方式、支持管道(pipeline)网络传输
- http2.0:会压缩头(Header)、全面采用了二进制格式、数据包不是按顺序发送、可以指定数据流的优先级、可以并发多个请求不会出现队头阻塞、服务器推送可以主动向客户端发送消息
- http3.0:把HTTP下层的TCP协议改成了UDP
12、HTTP长连接和短连接?
指的是客户端和服务端建立和保持TCP连接的机制。
长连接:浏览器向服务器进行一次HTTP会话访问后,并不会直接关闭这个连接,而是会默认保持一段时间,那么下一次浏览器继续访问的时候就会再次利用到这个连接。通过请求头Connection: keep-alive字段进行指定。
优点:通信双方因为在保活机制的保证下可以保证数据收发的实时性。
缺点:服务器需要一直保存和客户端的这条链接、是有状态的,那么在大量并发连接请求过来时,系统资源可能就不够了
使用场景:
- 服务器需要主动发送资源给客户端时
- 客户端和服务器通信很频繁时
- 客户端宕机或者掉线时需要服务器做一些处理时
短连接:浏览器向服务器每进行一次HTTP操作都要建立一个新的连接。
优点:短链接不占服务器的内存,服务器能处理的连接数量会比较多。
缺点:在有实际的资源要进行数据通信的时候才建立连接,那么在客户端发送完数据释放连接之后当服务器有向客户端发送数据时就不能做到发送消息的实时性、频繁地建立连接、释放连接会耗费大量的CPU和网络带宽资源
13、HTTP短轮询和长轮询?
客户端每隔一段时间就向服务端发出一个询问,获取服务端最新的消息。
短轮询:浏览器每隔一段时间向服务端发送http请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。
- 优点:比较简单,易于理解;
- 缺点:由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。
长轮询:首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将 这个请求挂起,然后判断服务器端数据是否有更新。
- 优点:减少了很多不必要的 http 请求次数。
- 缺点:实现复杂,且连接挂起也会导致资源的浪费。
14、TCP和UDP的区别和应用场景?
TCP:(Transmission Control Protocol,传输控制协议)面向连接的协议,全双工模式、在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来。面向字节流、只支持点对点通信、报文首部20个字节、有拥塞控制机制。
UDP:(User Data Protocol,用户数据报协议)非连接的协议、传输数据之前源端和终端不建立连接.传输途中出现丢包,UDP 也不负责重发、不需要维护连接状态,包括收发状态。 UDP信息包的标题很短只有8个字节 、吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制、UDP是面向数据报文连接。
15、TCP传输层三次握手,四次挥手策略?
主要是为了客户端和服务端都需要知道各自可收发。
三次握手:建立一个 可靠的TCP 连接服务,连接是通过三次握手进行初始化的,需要客户端和服务器总共发送3个报文(包)。
目的:连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。
四次挥手:是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。