【15-20K】你该知道的前端升级版面试题 (1.7W字含答案!)

Vue相关:

1、面试官:说说你对Vue的理解?

  • 答:

  • 1、什么是Vue:

  • Vue.js是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的web应用框架。

2、Vue的核心特性:

  • 2.1、数据驱动(MVVM)表示的是Model-View-ViewModel

  • Model:模型层,负责处理业务逻辑及和服务器端进行交互。

  • View:视图层,负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面。

  • ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁。

  • 2.2、组件化:什么是组件化一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现的开发模式,在Vue中每一个.vue文件都可以视为一个组件。

  • 2.3、组件化的优势:

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求。

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候可以根据报错的组件快速定位问题。

  • 高可维护性,由于每个组件的单一指责,并且组件在系统中是复用的,对代码进行优化可获得系统的整体升级。

  • 2.4、指令系统:指令是带有v-前缀对特殊属性作用,如当表达式的值改变时,将其产生的连带影响,响应式地作用于DOM。

2、面试官:说说你对SPA(单页应用)的理解?

  • 答:

  • 2.1、SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站对模型,它通过动态重写当前页面来与用户交互。像我们熟知的Vue,React,Angular都属于SPA应用。

  • 2.2、SPA的优缺点,优点:具有桌面应用的即时性,网站的可移植性和可访问性。用户体验好,快,内容的改变不需要重新加载整个页面。良好的前后端分离,分工明确。缺点:不利于搜索引擎的抓取,首次渲染速度相对较慢。

3、面试官:Vue实例挂载的过程中发生了什么?

  • 答:

  • 3.1:new Vue的时候会调用_init方法。

  • 定义setset、get、deletedelete、watch等方法。

  • 定义onon、off、$emit等事件。

  • 定义_update、forceUpdateforceUpdate、destroy生命周期。

  • 3.2:调用$mount进行页面的挂载。

  • 3.3:挂载的时候主要是通过mountCompoent方法。

  • 3.4:定义updateComponent更新函数。

  • 3.5:执行render生成虚拟DOM。

  • 3.6:_update将虚拟DOM生成真实DOM结构,并且渲染到页面中。

4、面试官: 数据请求在created和mouted的区别?

答:created是在组件实例一旦创建完成的时候立刻调用,这时候页面DOM节点并未生成;mounted是在页面DOM节点渲染完成之后就立刻执行的。触发时机上created是比mounted要更早的。两者的相同点:都能拿到实例对象的属性和方法。区别:对页面有改动的数据请求放在mounted中可能会导致页面闪动(mounted时页面DOM结构已经生成)。结论:数据请求对页面有改动建议放在created中,对页面无改动可以放在mounted中。

5、面试官:为什么Vue中的v-if和v-for不建议放在一起?

  • 答:

  • 5.1:v-if和v-for的作用:

  • v-if指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回true值的时候被渲染。

  • v-for指令基于一个数组来渲染一个列表。v-for指令需要使用item in list形式的特殊语法,其中list是源数据数组或者对象,而item则是被迭代的数组元素的别名。在v-for的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化。

  • 5.2、v-if和v-for的优先级:

  • vue2.x下:v-for要优先于v-if,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能。

  • vue3.x下:v-if要优先于v-for,如果同时出现,每次渲染都会先执行判断条件。

  • 5.3、注意事项:

  • 永远不要把v-if和v-for同时用在同一个元素上,带来性能上的浪费。

  • 避免出现这种情况,可以在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环。

  • 如果判断条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的内容。

6、面试官:SPA(单页应用)首屏加载速度慢怎么解决?

  • 答:

  • 6.1、:什么是首屏加载?

  • 首屏时间,是指浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。

  • 6.2、:计算首屏时间

  • 通过DOMContentLoad或者performance来计算出首屏时间

JavaScript

复制代码

// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};
  • 6.3、:加载慢的原因?

  • 在页面渲染的过程中,导致加载速度慢点因素可能如下:

  • 网络延迟问题。

  • 资源文件体积是否过大。

  • 资源是否重复发送请求去加载。

  • 加载脚本的时候,渲染内容堵塞了。

  • 6.4、:解决方案:

  • 常见的几种SPA首屏优化方式:

  • 减小入口文件体积。

  • 静态资源本地缓存。

  • ui框架按需加载。

  • 图片资源的压缩。

  • 组件重复打包。

  • 开启GZip压缩。

  • 使用SSR。

7、面试官:为什么data属性是一个函数而不是一个对象?

  • 答:

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况。

  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染,采用函数的形式,会形成一个私有作用域来避免这个问题。

8、面试官:Vue组件间通信方式都有哪些?

  • 答:

  • 8.1、:组件间通信的分类

  • 组件间通信的分类可以分成以下几类

  • 父子组件之间的通信。

  • 兄弟组件之间的通信。

  • 祖孙与后代组件之间的通信。

  • 非关系组件之间的通信。

  • 8.2、:组件间通信的方案

  • Vue中8种常规的通信方案

  • 通过props传递。

  • 通过$emit触发自定义事件。

  • 使用ref。

  • EventBus。

  • parentparent或root。

  • attires与listeners。

  • Provide与Inject。

  • Vuex

9、面试官:说说你对nexttick的理解?

  • 答:

  • 官方对其对定义:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM。

  • 什么意思呢?:我们可以理解成,Vue在更新DOM时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等待队列中所有数据变化完成之后,再统一进行更新。

  • 使用场景:如需正则处理input输入的内容,就需要使用Vue.nexttick()。

10、面试官:说说你对Vue的mixin的理解?有什么应用场景?

  • 答:

  • 10.1、mixin是什么?:Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类。Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂。

  • 10.2、Vue中的mixin:

  • 官方定义:mixin(混入),提供了一种非常灵活的方式,来分发Vue组件中的可复用功能。

  • 本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods、created、computed等等。我们只要将共用的功能以对象的方式传入mixins选项中,当组件使用mixins对象时所用mixins对象的选项都将被混入该组件本身的选项中来。

  • 10.3、使用场景:在日常的开发中,我们经常会遇到不同的组件中经常会需要使用到一些相同或者相似的代码,这些代码的功能相对独立,此时就可以通过Vue的mixin功能将相同或者相似的代码提出来。减少冗余代码,提高可阅读性。

11、面试官:说说你对slot的理解?slot使用场景有哪些?

  • 答:

  • 11.1、什么是slot?

  • 简单来说就是子组件中的提供给父组件使用的一个坑位,用“slot”表示,父组件可以在这个坑位中填充任何模版代码然后子组件中的“slot”就会被替换成这些内容。

  • 11.2、使用场景:

  • 通过插槽可以让用户可以拓展组件,去更好地复用组件和对其定制化处理。

  • 通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用如布局组件,表格列,下拉选择,弹框显示内容等。

  • 11.3、分类:

  • slot可以分为以下三种:

  • 默认插槽:子组件用slot标签来确定渲染位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面。父组件在使用的时候,直接在子组件的标签内写入内容即可。

javascript

复制代码

//子组件Child.vue
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>

//父组件
<Child>
<div>默认插槽</div>
</Child>
  • 具名插槽:子组件用name属性来表示插槽的名字,不传为默认插槽。父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值。

javascript

复制代码

//子组件Child.vue
<template>
<slot>插槽后备的内容</slot>
<slot name="content">插槽后备的内容</slot>
</template>

//父组件
<child>
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</child>
  • 作用域插槽:子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上。父组件中使用时通过v-slot:(简写:#)获取子组件信息,在内容中使用。

javascript

复制代码

//子组件Child.vue
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>

//父组件
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>

12、面试官:说说你对keep-alive的理解?

  • 答:

  • 12.1、keep-alive是什么?

  • keep-alive是Vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

  • keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

  • keep-alive可以设置一下props属性:

  • include -字符串或者正则表达式。只有名称匹配的组件会被缓存。

  • exclude -字符串或者正则表达式。任何名称匹配的组件都不会被缓存。

  • max – 数字。最多可以缓存多少组件实例。

xml

复制代码

//关于keep-alive的基本用法:
<keep-alive>
<component :is="view"></component>
</keep-alive>

//使用includes和exclude:
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
  • 12.2、使用场景:

  • 使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keep-alive。

  • 例:

  • 当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive

  • 首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive

  • 12.3、缓存后如何获取数据?

  • 解决方案可以有以下两种:

  • beforeRouteEnter

  • actived

javascript

复制代码

//beforeRouteEnter
//每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},

//actived
//在keep-alive缓存的组件被激活的时候,都会执行actived钩子。
activated(){
this.getData() // 获取数据
},
//注意:服务器端渲染期间actived不被调用。

13、面试官:Vue常用的修饰符有哪些?有什么应用场景?

  • 答:

  • 13.1、修饰符是什么?

  • 在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号。

  • 在Vue中,修饰符处理了许多DOM事件的细节,让我们不需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。

  • Vue中的修饰符可以分为以下五种:

  • 表达修饰符

  • 事件修饰符

  • 鼠标按键修饰符

  • 键值修饰符

  • v-bind修饰符

  • 13.2、修饰符的作用:

  • 表单修饰符:

  • 在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model,关于表单的修饰符有如下:

  • lazy

  • trim

  • number

bash

复制代码

//lazy:在我们填完信息,光标离开标签的时候,才会将值赋予value,也就是在change事件之后再进行信息同步。
<input type="text" v-model.lazy="value">
<p>{{value}}</p>

//trim:自动过滤用户输入的首空格字符,而中间的空格不会过滤。
<input type="text" v-model.trim="value">

//number:自动将用户的输入值转为数字类型,但如果这个值无法被parseFloat解析,则会返回原来的值
<input type="number" v-model.number="value">
  • 事件修饰符:

  • 事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop

  • prevent

  • self

  • once

  • capture

  • passive

  • native

xml

复制代码

//stop:阻止了事件冒泡,相当于调用了event.stopPropagation方法。
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>

//prevent:阻止了事件的默认行为,相当于调用了event.preventDefault方法。
<form v-on:submit.prevent="onSubmit"></form>

//self:只当在event.target是当前元素自身时触发处理函数。
<div v-on:click.self="doThat">...</div>

//once:绑定了事件以后只能触发一次,第二次就不会触发。
<button @click.once="shout(1)">ok</button>

//capture:使事件触发从包含这个元素的顶层开始往下触发。
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3

//passive:在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符。
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

//native:让组件变成像HTML内置标签那样监听根元素的原生事件,否则组件上使用v-on只会监听自定义事件。
<my-component v-on:click.native="doSomething"></my-component>
  • 鼠标按钮修饰符:

  • 鼠标按钮修饰符针对的就是左键,右键,中键点击,有如下:

  • left 左键点击

  • right 右键点击

  • middle 中键点击

ini

复制代码

<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
  • 键盘修饰符:

  • 键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的,有如下:

  • keyCode存在很多,但Vue为我们提供了别名,分为以下两种:

  • 普通键(enter,tab,delete,space,esc,up…)

  • 系统修饰键(ctrl,alt,meta,shift…)

ini

复制代码

// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="shout()">

//还可以通过以下方式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
  • v-bind修饰符:

  • v-bind修饰符主要是为属性进行操作,有如下:

  • async

  • prop

  • camel

javascript

复制代码

//async:能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp>
//子组件
this.$emit('update:myMessage',params);

//以上这种方法相当于以下的简写
//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
this.bar = e;
}
//子组件js
func2(){
this.$emit('update:myMessage',params);
}

//使用async需要注意以下两点:
//使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
//注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
//将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的

//props:设置自定义标签属性,避免暴露数据,防止污染HTML结构
<input id="uid" title="title1" value="1" :index.prop="index">

//camel:将命名变为驼峰命名法,如将view-box属性名转换为viewBox
<svg :viewBox="viewBox"></svg>
  • 应用场景:

  • .stop:阻止事件冒泡

  • .native:绑定原生事件

  • .once:事件只执行一次

  • .self:将事件绑定在自身身上,相当于阻止事件冒泡

  • .prevent:阻止默认事件

  • .caption:用于事件捕获

  • .keyCode:监听特定键盘按下

  • .right:鼠标右键

14、面试官:Vue中的过滤器了解吗?过滤器的应用场景有哪些?

  • 答:

  • 14.1、什么是过滤器?

  • 过滤器(filter)是输送介质管道上不可缺少的一种装置。大白话:就是把一些不必要的东西过滤掉,过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,也可以理解其为一个纯函数。

  • Vue允许你自定义全局过滤器,局部过滤器

  • Vue3中已废弃filter

  • 14.2、如何用:

  • vue中的过滤器可以用在两个地方:双花括号插值和v-bind表达式,过滤器应该被添加在JavaScript表达式的尾部,由“管道:|”符号指示:

xml

复制代码

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
  • 14.3、如何定义过滤器:

scss

复制代码

//局部过滤器
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}

//全局过滤器
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
// ...
})

//ps:局部过滤器优先于全局过滤器被调用,一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。执行顺序为:从左到右。
  • 14.4、应用场景:

  • 平时开发中,需要用到过滤器的地方有很多,比如单位转换,数字打点,时间格式化之类。

15、面试官:说说你对虚拟DOM的理解?

  • 答:

  • 15.1、什么是虚拟DOM?

  • 虚拟DOM(Virtual DOM)这个概念相信大家都不陌生, 从React到Vue,虚拟DOM为这两个框架都带来了跨平台的能力(React-Native 和 Weex)。虚拟DOM实际上它只是一层对真实DOM的抽象,以JavaScript对象(VNode节点)作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这颗树映射到真实环境上。创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应。

  • 15.2、为什么需要虚拟DOM?

  • DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的。真实DOM节点,哪怕一个最简单的div也包含着很多属性,可以贴图出来直观感受一下。

【15-20K】你该知道的前端升级版面试题 (1.7W字含答案!)

【15-20K】你该知道的前端升级版面试题 (1.7W字含答案!)

  • 由此可见,操作DOM的代价仍旧是昂贵的,频繁操作DOM还是会出现页面卡顿,影响用户体验。

  • 举个例子:

  • 你用传统的原生api或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程,当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终会执行10次流程。

  • 而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓的计算。

很多人认为虚拟DOM最大的优势是diff算法,减少JavaScript操作真实DOM带来的性能消耗。虽然这也是一个虚拟DOM带来的一个优势,但并不是全部。虚拟DOM最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的DOM,可以是安卓和IOS的原生组件,可以是很火热的小程序,也可以是各种GUI。

16、面试官:说说你对Vue中diff算法的理解?

  • 答:

  • 16.1、什么是diff算法:

  • diff算法是一种通过同层的树节点进行比较的高效算法,其中有两个特点:

  • 1、比较只会在同层级进行,不会跨层级比较。

  • 2、在diff比较的过程中,循环从两边向中间比较。

  • 16.2、比较方式:

  • diff整体策略为:深度优先,同层比较。

17、面试官:Vue项目中有封装过axios吗?主要是封装哪些方面?

  • 答:

  • 17.1、什么是axios?

  • axios是一个轻量的HTTP客户端,基于XMLHttpRequest服务来执行HTTP请求,支持丰富的配置,支持Promise,支持浏览器端和Node.js端。自Vue2.0起,尤大取消对vue-resource的官方推荐,转而推荐axios。现在axios已经成为大部分Vue开发者的首选。

  • 17.2、axios的特性:

  • 从浏览器中创建XMLHttpRequests。

  • 从node.js创建HTTP请求。

  • 支持Promise API。

  • 支持拦截请求,拦截响应。

  • 转换请求数据,转换响应数据

  • 支持取消请求。

  • 自动转换JSON数据。

  • 客户端支持防御XSRF。

  • 17.3、为什么要封装axios?

  • axios的API很友好,你完全可以很轻松地在项目中直接使用。不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些设置超时时间,设置请求头,根据项目环境判断使用哪个请求地址,错误处理等等操作,都需要写一遍。这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高代码质量,我们应该在项目中二次封装axios再使用。

javascript

复制代码

//如:利用.node环境变量来作判断,用来区分开发,测试,生产环境
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}

//如:单独用一个文件来处理API,实现高效管理请求。
import axios from 'axios'

//微信登录
export const wechatLoginApi = (data) => axios.post(`user/user/login`, { data, baseUrl });
//获取用户信息
export const userInfoApi = (data) => axios.post(`user/api.userInfo/artificerIndex`, { data });

//如:请求拦截器:
//请求拦截器可以在每个请求里加上token,签名等。做了统一处理后维护起来也方便。
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token
// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
token && (config.headers.Authorization = token)
return config
},
error => {
return Promise.error(error)
})

//如:响应拦截器:
axios.interceptors.response.use(res => {
//判断是否登陆
if (res.data.code === 704001) {
//删除已有登陆信息
store.commit('deleteUserInfo')
//跳转登陆页...
return Promise.reject()
};
//判断业务code是否为成功
return res.data.code === 0 ? res.data : Promise.reject(res.data)
},err =>{
//HTTP状态码系列错误
return Promise.reject({
msg: '当前访问人数较多,请稍后再试'
})

})
  • 17.4、小结:

  • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们领略到它的魅力。

  • 封装axios没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案。

18、面试官:说说你对SSR的理解?

  • 答:

  • 18.1、SSR是什么?

  • Server-Side Rendering 我们称其为SSR,意为服务端渲染。指由服务端完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可以交互页面的过程。

  • 先来看看Web3个阶段的发展史:

  • 传统服务端渲染SSR:网页内容在服务端渲染完成,一次性传输到浏览器。

  • 单页面应用SPA:单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式我们称为客户端渲染。

  • 服务端渲染SSP:SSR解决方案,后端渲染出完整的首屏的DOM结构返回,前端拿到的内容包括首屏及完整SPA结构,应用激活后依然按照SPA方式运行。

  • 18.2、SSR解决了什么?

  • SSR主要解决了以下两种问题:

  • SEO:搜索引擎优先爬取页面HTML结构,使用SSR时,服务端已经生成了和业务相关联的HTML,有利于SEO。

  • 首屏呈现渲染:用户无需等待页面所有JS加载完成就可以看到页面视图(压力来到了服务区,所以需要权衡哪些用服务端渲染,哪些交给客户端渲染)

  • SSR缺陷:

  • 复杂度:整个项目的复杂度增加。

  • 库的支持性,代码兼容。

  • 性能问题:

  • 每个请求都是n个实例的创建,不然会污染,消耗会变得很大。

  • 缓存node serve,nginx判断当前用户有没有过期,如果没过期的话就缓存用刚刚的结果。

  • 降级:监控cpu,内存占用过多,就SPA,返回单个的壳。

  • 服务器负载变大,相当于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用。

  • 在选择使用SSR之前,应着重考虑:

  • 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现。

  • 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢。

  • 18.3、实现方案:

  • Vue项目实现SSR方案一般都采用:Nuxt.js

  • React项目实现SSR方案一般都采用:Next.js

Vue3系列:

1、面试官:Vue3有了解过吗?能说说跟Vue2的区别吗?

  • 答:

  • 关于Vue3的重构背景,尤大是这样说的:

  • Vue新版本的理念成型于2018年末,当时Vue2点代码库已经有两岁半了。比起通用软件的生命周期这好像也没那么久,但在这段时期,前端世界已经今非昔比了。在我们更新(和重写)Vue的主要版本时,主要考虑两点因素:首先是新的JavaScript语言特性在主流浏览器中的受支持水平。其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题。

  • 简要就是:

  • 利用新的语言特性(ES6)

  • 解决架构问题

  • Vue3的新特性:

  • 速度更快

  • 体积减少

  • 更易维护

  • 更接近原生

  • 更易使用

速度更快:

  • 重写了虚拟DOM实现

  • 编译模版的优化

  • 更高效的组件初始化

  • undate性能提高1.3~2倍

  • SSR速度提高了2~3倍

  • 体积更小

  • 通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的。

  • 对开发人员,能够对Vue实现更多其他的功能,而不必担忧整体体积过大。

  • 对使用者,打包出来的包体积变小了。

  • 更易维护

  • 可与现有的Options API一起使用

  • 灵活的逻辑组合与复用

  • Vue3模块可以和其他框架搭配使用

  • 更好的Typescript支持:Vue3是基于 Typescript编写的,可以享受到自动的类型定义提示

  • 更接近原生

  • 可以自定义渲染API

  • 更易使用

  • 响应式API暴露出来,轻松识别组件重新渲染原因

  • 移除API

  • keyCode 支持作为v-on的修饰符

  • 过滤filter

  • on,on,off和$once实例方法

  • 内联模版 attribute

  • $destroy 实例方法。用户不应再手动管理单个Vue组件的生命周期。

2、面试官:Vue3.0性能提升主要是通过哪几个方面体现的?

  • 答:

编译阶段

  • 回顾Vue2,我们知道每个组件实例都对应一个watcher实例,它会在组件渲染过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染。

  • 因此Vue3在编译阶段,做了进一步优化,主要有如下:

  • diff算法优化

  • 静态提升

  • 事件监听缓存

  • SSR优化

  • diff算法优化:

  • Vue3在diff算法中相比Vue2增加了静态标记。关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较。

  • 静态提升:

  • Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用。这样就免去了重复创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时的内存占用。

  • 事件监听缓存:

  • 默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化。开启了缓存后,没有了静态标记。下次diff算法的时候直接使用。

  • SSR优化:

  • 当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。

  • 源码体积

  • 相比Vue2,Vue3整体体积变小了,除了移除了一些不常用的API,更重要的是Tree shanking任何一个函数,如ref,reavtived,computed等,仅仅在用到的时候才打包,没有用到的模块都被摇掉,打包体积整体变小。

  • 响应式系统

  • Vue2中采用defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式。

  • Vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历。

  • 可以监听动态属性的添加

  • 可以监听到数组的索引和数组length属性

  • 可以监听删除属性等等。

3、面试官:Vue3.0里为什么要用Proxy API替代defineProperty API ?

  • 答:

  • Object.defineProperty

  • 定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 为什么能实现响应式:

  • 通过defineProperty两个属性,get及set。

  • get

  • 属性的getter函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。

  • set

  • 属性的setter函数,当属性值被修改时,会调用此函数。该方法接受一个参数。(也就是被赋予的新值),会传入赋值时的this对象。默认为undefined。

  • Object.defineProperty 小结

  • 检测不到对象属性的新增和删除(所以Vue2中增加了set,delete API)

  • 数组API方法无法监听到(所以Vue2中重写了数组API方法)

  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

  • Proxy

  • Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。

  • Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式的目的。

  • Proxy可以直接监听数组的变化(如:push,shift,splice等)

  • Proxy有多达13种拦截方法,不限于apply,ownKeys,deleteProperty,has等。

  • Proxy不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

4、面试官:说说你对Vue3中的Tree shaking特性的理解?举例说明一下?

  • 答:

  • 4.1、什么是Tree shaking ?

  • Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是保持代码运行结果不变的前提下,去除无用的代码。

  • 例:如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去。而Tree shaking则是一开始就把需要的蛋白蛋清(也就是import)放入搅拌,最后直接做出蛋糕。也就是说 Tree shaking 其实是找出使用的代码。

  • 4.2、Tree shaking 如何做?

  • Tree shaking是基于 ES6模版语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。

  • Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载

  • 判断哪些模块和变量未被使用或者引用,进而删除对应代码

  • 4.3、Tree shaking 的作用:

  • 通过Tree shaking,Vue3给我们带来的好处是:

  • 减少程序体积(更小)

  • 减少程序执行时间(更快)

  • 便于后续对程序架构进行优化(更友好)

ES6系列:

1、面试官:说说你对 var,let,const 的理解?

  • 答:

  • 1.1、 var :

  • 在ES5中,顶层对象的属性和全局变量是等价的,用 var 声明的变量既是全局变量,也是顶层变量(顶层对象:在浏览器环境指的是 window 对象,在 Node 环境指的是global 对象)

javascript

复制代码

//全局变量,顶层变量
var a = 10;
console.log(window.a) // 10

//使用 var 声明的变量存在变量提升的情况
console.log(a) // undefined
var a = 20
//在编译阶段会将其变成以下执行
var a
console.log(a)
a = 20

//使用 var 我们能够对一个变量进行多次声明,后面声明对变量会覆盖前面的变量声明
var a = 20
var a = 30
console.log(a) // 30

//在函数内部使用 var 声明变量的时候,该变量是局部的
var a = 20
function change(){
var a = 30
}
change()
console.log(a) // 20
//在函数外使用 var 声明变量,该变量是全局的
var a = 20
function change(){
a = 30
}
change()
console.log(a) // 30
  • 1.2、 let :

  • let 是 ES6新增的命令,用来声明变量,用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效(也就是局部变量)。

javascript

复制代码

{
let a = 20
}
console.log(a) // ReferenceError: a is not defined.

//let 不存在变量提升 表示在声明它之前变量a是不存在的,这时如果用到它,就会抛出一个错误。
console.log(a) // 报错ReferenceError
let a = 2

// let 不允许在相同作用域中重复声明
let a = 20
let a = 30
// Uncaught SyntaxError: Identifier 'a' has already been declared


//不能在函数内部重新声明参数
function func(arg) {
let arg;
}
func()
// Uncaught SyntaxError: Identifier 'arg' has already been declared
  • 1.3、 const :

  • const 是ES6新增的命令,用来声明一个只读的常量。一旦声明,常量的值就不能改变。

  • const 声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

ini

复制代码

const a = 1
a = 3
// TypeError: Assignment to constant variable.

const a;
// SyntaxError: Missing initializer in const declaration

//如果之前用 var 或 let 声明过变量,再用 const 声明同样会报错。
var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错


// const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量。对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const 智能保证这个指针是固定的,并不能确保改变量的结构不变。其他情况,const 与 let 一致。
onst foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

2、面试官:ES6中数组新增了哪些扩展?

  • 答:

  • 2.1、扩展运算符的应用:

javascript

复制代码

//ES6通过扩展运算符(...),好比rest参数的逆运算,将一个数组转为逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
  • 2.2、构造函数新增的方法:

  • 关于构造函数数组新增的方法有如下:

  • Array.from()

  • Array.of()

javascript

复制代码

// Array.from() 将两类对象转为真正的数组:类似数组的对象和可遍历的对象,包括ES6新增的数据结构 Set 和 Map
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

//还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]


// Array.of() 用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]

// Array 没有参数的时候,返回一个空数组。
Array() // []
// Array 当参数只有一个的时候,实际上是指定数组的长度。
Array(3) // [, , ,]
// Array 参数个数不少于2个时,Array()才会返回由参数组成的新数组。
Array(3, 11, 8) // [3, 11, 8]
  • 2.3、实例对象新增的方法:

  • 关于数组实例对象新增的方法有如下:

  • copyWithin()

  • find()、findIndex()

  • fill()

  • entries(),keys(),values()

  • includes()

  • flat(),flatMap()

scss

复制代码

// copyWithin() 将指定位置的成员复制到其他位置(会覆盖原油成员),然后返回当前数组。
// 参数如下:
// target(必需): 从该位置开始替换数据。如果为负值,表示倒数。
// start(可选): 从该位置开始读取数据,默认为0。如果为负值,表示从末尾开始计算。
// end(可选): 到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
// [4, 5, 3, 4, 5]

// find() 用于找出第一个符合条件的数组成员。参数是一个回调函数,接受三个参数依次为当前的值,当前的位置和原数组。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

// findIndex() 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2


// fill() 使用给定值,填充一个数组
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

// 还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
// ps:如果填充的类型为对象,则是浅拷贝



// keys() 是对键名的遍历,values()是对键值的遍历,entries()是对键值对的遍历。
or (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"



// includs() 用于判断数组是否包含给定的值。
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true

//方法的第二个参数表示搜索的起始位置,默认为0,参数为负数则表示倒数的位置。
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true



// flat() , flatMap() 将数组扁平化处理,返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

// flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写出一个整数,表示想要拉平的层数,默认为1
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]


// flatMap()方法对原数组的每个成员执行一个函数相当于执行 Array.prototype.map(),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

3、面试官:你是怎么理解ES6新增Set,Map两种数据结构的?

  • 答:

  • 3.1、Set

  • Set 是 ES6 新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合。

  • Set 本身是一个构造函数,用来生产Set数据结构。

javascript

复制代码

const s = new Set();

// Set 的实例关于增删改查的方法:

// add() 添加某个值,返回 Set 结构本身,当添加实例中已经存在的元素,Set 不会进行添加。
s.add(1).add(2).add(2); // 2只被添加了一次

// delete() 删除某个值,返回一个布尔值,表示删除是否成功
s.delete(1)

// has() 返回一个布尔值,判断该值是否为 Set 的成员
s.has(2)

// clear() 清楚所有成员,没有返回值。


// Set 实例遍历的方法有如下:

// keys() 返回键名的遍历器
let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

// values() 返回键值的遍历器
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

// entries() 返回键值对的遍历器
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

// forEach() 用于对每个成员执行某种操作,没有返回值。键值,键名都相等,同样的 forEach 方法有第二个参数,用于绑定处理函数的 this
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

// ... 扩展运算符和 Set 结构相结合实现数组或者字符串去重
// 数组
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; // [3, 5, 2]

// 字符串
let str = "352255";
let unique = [...new Set(str)].join(""); // "352"

// 实现并集,交集,差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
  • 3.2、 Map

  • Map类型是键值对的有序列表,而键和值都可以是任意类型。

  • Map本身是一个构造函数,用来生成Map数据结构

javascript

复制代码

const map = new Map()

// Map 结构的实例针对增删改查有以下属性和操作方法。
// size 属性返回 Map 结构的成员总数
map.set('foo', true);
map.set('bar', false);
map.size // 2

// set() 设置键名 key 对应的键值为 value ,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。同时返回的是当前 Map 对象,可采用链式写法。
const map = new Map();

map.set('edition', 6) // 键是字符串
map.set(262, 'standard') // 键是数值
map.set(undefined, 'nah') // 键是 undefined
map.set(1, 'a').set(2, 'b').set(3, 'c') // 链式操作

// get() get 方法读取 key 对应的键值,如果找不到 key ,返回 undefined。
const map = new Map();

const hello = function() {console.log('hello');};
map.set(hello, 'Hello ES6!') // 键是函数

map.get(hello) // Hello ES6!

// has() 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const map = new Map();

map.set('edition', 6);
map.set(262, 'standard');
map.set(undefined, 'nah');

map.has('edition') // true
map.has('years') // false
map.has(262) // true
map.has(undefined) // true

// delete() 方法删除某个键,返回 true 。如果失败,返回 false。
const map = new Map();
map.set(undefined, 'nah');
map.has(undefined) // true

map.delete(undefined)
map.has(undefined) // false

// clear() 方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0



// Map 结构原生提供三个遍历器生成函数和一个遍历方法。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
// keys() 返回键名的遍历器
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"

// values() 返回键值的遍历器。
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"

// entries() 返回所有成员的遍历器。
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// forEach() 遍历 Map 的所有成员。
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});

JavaScript 系列 :

1 、面试官:说说 JavaScript 中的数据类型?存储上的区别?

  • 答:JavaScript 中,我们可以分成两种类型:

  • 基本数据类型

  • 复杂数据类型(引用数据类型)

  • 区别:存储位置不同

基本数据类型主要为以下6种:

  • 1、Number

  • 2、String

  • 3、Boolean

  • 4、Undefined

  • 5、null

  • 6、symbol

javascript

复制代码

// Number 数值最常见的整数类型,格式为十进制,八进制(零开头),十六进制(0x开头),浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示。在数值类型中,存在一个特殊数值 NaN ,意为 ‘不是数值’,用于表示本来要返回数值的操作失败了(而不是抛出错误)。
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10
console.log(0/0); // NaN
console.log(-0/+0); // NaN

// String 字符串可以使用双引号(""),单引号(''),反引号(`) 表示。字符串是不可变的,意思是一旦创建,它们的值就不能变了。
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
let lang = "Java";
lang = lang + "Script"; // 先销毁再创建

// Boolean (布尔值)类型有两个字面值: true 和 false
// 通过 Boolean 可以将其他类型的数据转化成布尔值规则如下:
数据类型 转换为 true 的值 转换为 false 的值
String 非空字符串 ''
Number 非零数值(包括无穷值) 0NaN
Object 任意对象 null
Undefined N/A (不存在) undefined

// Undefined 类型只有一个值,就是特殊值 undefined 。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值。
let message;
console.log(message == undefined); // true

// 包含 undefined 值当变量跟为定义变量是有区别的。
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错

// Null 类型同样只有一个值,即特殊值 null 。逻辑上讲,null 值表示一个对象指针,这也是给 typeof 传一个 null 会返回 object 的原因。
let car = null;
console.log(typeof car); // "object"

// undefined 值是由 null 值派生而来。
console.log(null == undefined); // true
// 只要变量要保存对象,而当时又没有那个对象可保存,就可用 `null`来填充该变量

// Symbol (符号)是原始值,且符号实例是唯一,不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false
  • 引用数据类型,我们主要讲述以下三种:

  • Object

  • Array

  • Function

javascript

复制代码

// Object (对象)创建 object 常用方式为对象字面量表示法,属性名可以是字符串或者数值。
let person = {
name: "Nicholas",
"age": 29,
5: true
};

// Array (数组) JavaScript 数组是一组有序的数据,但根其他语言不同的是,数组中每个槽位可以存储任意类型的数据,并且数组也是动态大小的,会随着数据添加而自动增长。
let colors = ["red", 2, {age: 20 }]
colors.push(2)

// Function (函数) 实际上是对象,每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。函数存在三种常见的表达方式:
//函数声明
function sum (num1, num2) {
return num1 + num2;
}

//函数表达式
let sum = function(num1, num2) {
return num1 + num2;
};

//箭头函数
//函数声明和函数表达式两种方式
let sum = (num1, num2) => {
return num1 + num2;
};

//其他引用类型还包括 Date,RegExp,Map,Set等等
  • 区别

  • 1、声明变量时不同的内存地址分配:

  • 基本数据类型:保存在栈中(存放的就是对应的值),在内存中占有固定大小。

  • 引用数据类型:对应的值存放在堆中,引用地址(指针地址)存放在栈中。

  • 2、不同的数据类型赋值时的不同:

  • 基本数据类型:生成相同的值,两个对象对应不同的地址。

  • 引用数据类型:是将存放栈中的引用地址(指针地址)赋值给另一个变量,两个变量指向堆中同一个对象。

2、面试官:说说你对闭包的理解?闭包的使用场景?

  • 答:

2.1、是什么?

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。

  • 函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存作用。

作用:

  • 保护:避免命名冲突。

  • 保存:解决循环绑定引发的索引问题。

  • 变量不会销毁:可以使用函数内部的变量,使变量不会被垃圾回收机制回收。

2.2、应用场景:

  • 设计模式中的单例模式。

  • for循环中的保留i的操作。

  • 防抖和节流。

  • 函数柯里化。

2.3、缺点:

  • 慎用闭包,会出现内存泄露的问题。

3、面试官:说说你对作用域链的理解?

  • 答:

3.1、作用域:

  • 变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。

  • 换句话说:规定变量和函数的可使用范围称作作用域。

3.2、作用域分类:

javascript

复制代码

// 1、全局作用域:任何不在函数中或者大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();

// 2、函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就是在函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
// 下方代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域。
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

// 3、块级作用域:ES6引入了 let 和 const 关键字,和 var 关键字不同,在大括号中使用 let 和 const 声明的变量存在于块级作用域中。大括号之外不能访问这些变量。
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3.3、作用域链:

  • 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。

4、面试官:说说你对 JavaScript 原型,原型链的理解,有什么特点?

  • 答:

4.1、什么是原型:

  • JavaScript 常被描述为一种基于原型的语言 — 每个对象拥有一个原型对象。原型可以分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。每个构造方法都有一个显式原型。

4.2、原型的特点:

  • __proto__ 是隐式原型 是隐式原型,prototype 是显式原型.

  • 所有实例的 __proto__ 都指向它们构造函数的 prototype 。

  • 所有的 prototype 都是对象,自然它的 __proto__ 指向的是 Object() 的 prototype。

  • 所有的构造函数隐式原型指向的都是 Function() 的显式原型。

  • Object() 的隐式原型是 null 。

4.3、原型链:

  • 多个 __proto__ 组成的集合称为原型链(概念类似于作用域链)

  • instanceof 就是判断某对象是否位于某构造方法的原型链上。

5、面试官:说说你对防抖节流的理解?

  • 答:

  • 防抖节流本质是优化高频率执行代码的一种手段。

5.1、定义:

  • 防抖:N秒后执行事件,N秒内重复触发,则重新计时。

  • 节流:N秒后执行事件,N秒内重复触发,仅触发一次。

5.2、实现:

javascript

复制代码

// 防抖
/**
* 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/

let timeout = null

function debounce(func,wait = 500,immediate = false){
// 清除定时器
if(timeout !== null) clearTimeout(timeout)
// 判断是否立即执行
if(immediate){
var callNow = !timeout
timeout = setTimeout(function () {
timeout = null
},wait)
if(callNow) typeof func === 'function' && func()
}else{
// 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法
timeout = setTimeout(function(){
typeof func === 'function' && func()
},wait)
}
}





// 节流
/**
* 节流原理:在一定时间内,只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/

let timer,flag
function throttle(func,wait = 500,immediate = false){
if(immediate){
if(!flag){
flag = true
// 如果是立即执行,则在wait毫秒内开始时执行
typeof func === 'function' && func()
timer = setTimeout(()=>{
flag = false
},wait)
}
}else{
if(!flag){
flag = true
// 如果是非立即执行,则在wait毫秒内的结束处执行
timer = setTimeout(()=>{
flag = false
typeof func === 'function' && func()
},wait)
}
}
}

5.3、应用场景:

  • 防抖应用场景:

  • 1、搜索框搜索输入。只需用户最后一次输入完,再发送请求。

  • 2、手机号,邮箱验证输入检测。

  • 3、窗口大小resize,只需窗口调整完成后。计算窗口大小,防止重复渲染。

  • 节流应用场景:

  • 1、滚动条滚动事件

  • 2、加载更多。

  • 3、搜索框,搜索联想功能。

6、面试官:说说JavaScript数字精度丢失的问题?如何解决?

  • 答:

  • 6.1、场景复现:

  • 一个经典面试题:

ini

复制代码

0.1 + 0.2 === 0.3 //false
  • 解析:比如一个数 1 ÷ 3 = 0.33333333…,3会一直无限循环,数学可以表示,但是计算机需要存储,方便下次取出来再使用,但0.33333333…,这个数无限循环,再大的内存也存不下,所以不能存储一个相对于数学来说的值,只能存储一个近似值,当计算机存储后再取出时就会出现精度丢失问题。

  • 6.2、问题分析:

  • 在JavaScript语言中0.1和0.2都转化为二进制后再进行运算。

arduino

复制代码

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004
  • 计算机存储双精度浮点数需要先把十进制数转换为二进制的科学计数法的形式,然后计算机以自己的规则{符号位 + (指数位 + 指数偏移量的二进制)+ 小数部分}存储二进制的科学计数法。因存储时有位数限制(64位),并且某些十进制浮点数转换为二进制数时出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差。

  • 解决方案:

  • 1、先给他们放大倍数,随后再除以相应倍数。

ini

复制代码

const a = 0.1;
const b = 0.2;
console.log(a + b === 0.3) // false
console.log((a * 1000 + b * 1000) / 1000 === 0.3) // true
  • 2、使用第三方库如:Math.js ,big.js等。

HTTP系列:

1、面试官:说说你对HTTP,HTTPS的理解?

  • 答:

  • 什么是HTTP?

  • HTTP(HyperText Transfer Protocol,超文本传输协议)是一种应用非常广泛的应用层协议。

  • 所谓“超文本”的含义,就是传输的内容不仅仅是文本(比如HTML,CSS这个就是文本),还可以是一些其他的资源,比如图片,视频,音频等二进制的数据。

  • HTTP版本:

  • 1、HTTP0.9:

  • 第一个版本的HTTP协议(已过时)。它的组成极其简单,只允许客户端发送GET一种请求,且不支持请求头。由于没有协议头,造成了HTTP0.9协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。

  • HTTP0.9具有典型的无状态性,每个事务独立进行处理,事务结束就释放这个连接。即发起一次HTTP0.9的传输首先要建立一条客户端到服务端到TCP连接,服务端返回数据后,关闭TCP连接。

  • 2、HTTP1.0:

  • 第二个版本的HTTP协议。在HTTP0.9版本基础上扩展支持了( POST,PUT,HEAD,DELETE )请求方法。新增了5类状态响应码。

  • 状态响应码:信息响应 ( 100-199 ),如:100 ( 这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。)

  • 状态响应码:成功响应( 200-299 ),如:200(请求成功,目前最常见的成功响应码)

  • 状态响应码:重定向信息( 300-399 ),如:301( 永久重定向,请求资源的URL已永久更改。在响应中给出了新的URL。 ),304( 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。协商缓存生效时返回此状态码 )

  • 状态响应码:客户端错误响应( 400-499 ),如:403( 客户端没有访问内容的权限,也就是说,它是未经授权的,因此服务器拒绝提供请求的资源。 ),404( 服务器找不到请求的资源,在浏览器中,这意味着无法识别URL。 )

  • 状态响应码:服务端错误响应( 500-599 ),如:502网关错误,此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。 ),503( 服务器超载或停机维护,服务器没有准备好处理请求。常见原因是服务器维护或重载而停机。 )

  • 存在的问题:

  • 无法复用连接,每次发送请求,都需要进行一次TCP连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率变低。

  • 队头阻塞( head of line blocking ),由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送,假设前一个请求响应一直不到达,那么下一个请求就不发送,后面的请求就阻塞了。

  • 不支持断点续传,也就是说,每次都会传送全部的页面和数据。

  • 3、HTTP1.1:

  • HTTP协议的第三个版本,是目前使用最广泛,主流的协议版本。

  • 新增了( OPTIONS,TRACE,CONNECT )请求方法。

  • HTTP1.1继承了HTTP1.0的简单,克服了HTTP1.0性能上的问题,特色为( 长连接,支持断点续传,管道传输,新的字段如cache-control,Host字段

  • 长连接: HTTP1.1增加Connection字段,对于同一个Host,通过设置Keep-Alive保持HTTP连接不断。避免每次客户端与服务器请求都要重复建立释放TCP连接。提高了网络的利用率,如果客户端想 关闭HTTP连接,可以在请求头中携带Connection:false,来告知服务器关闭请求。(长连接会给服务器造成压力)

  • 断点续传: 通过使用请求头中的Range来实现

  • 管道传输: 可以使用管道传输,多个请求可以同时发送,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。

  • 4、HTTP2.0:

  • HTTP2.0相比于HTTP1.1有很大改动,主要包括:

  • 它必须搭配TLS1.2一起使用,也就是说他是HTTPS。

  • 二进制分帧:HTTP2.0之所以能够突破HTTP1.X标准的性能限制,改进传输性能,实现低延迟和高吞吐量,就是因为其新增了二进制分帧层,而不再是ASCLL形式的文本。

  • 多路复用/连接共享: 允许同时通过单一的HTTP2.0连接发起多重的请求-响应信息。即在一条连接内支持多条流并行( 使用多个stream,每个stream又分帧传输,使得一个 TCP 连接能够处理多个HTTP请求 )

  • 首部压缩/头部压缩:客户端与服务端各自维护一个header的索引表,使得不需要直接发送值,通过发送索引表中的索引缩减头部大小。

  • 服务端推送: 服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且服务器推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。

  • HTTPS:

  • HTTPS也是一个应用层协议,是在HTTP协议的基础上引入了一个加密层( SSL/TLS )

  • HTTP是明文传输,本来要传什么,实际上就传了什么,但是这样传输,在传输过程中,被第三方截获到了,就可能造成信息泄露。于是引入了HTTPS在HTTP基础上进行了加密,进一步的保护了用户的信息安全。

  • 加密方式:

  • 对称加密:简单说就是有一个密钥,它可以加密一段信息,也可以对加密后的信息进行解密,和我们日常生活中用的钥匙作用差不多。

  • 缺点: 当客户端把密钥进行明文传输的时候,也可能被别人截获,再次发送密文,别人就可以通过密钥获取到明文,那此时的加密就没什么作用了(解决办法: 对密钥进行加密传输)

  • 非对称加密:简单说就是有两把密钥,通常一把叫做公钥、一把叫私钥,用公钥加密的内容必须用私钥才能解开,同样,私钥加密的内容只有公钥能解开。公钥和私钥是配对的. 最大的缺点就是运算速度非常慢,比对称加密要慢很多。

  • 缺点:可能获取到的公钥就是假的。(解决办法:引入数字证书)。

  • 数字证书:网站在使用HTTPS前,需要向CA机构申领一份数字证书,数字证书里含有证书持有者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明“该公钥对应该网站”。

  • 缺点: 证书本身的传输过程中,容易被篡改

  • 解决办法: 客户端获取到这个证书之后, 对证书进行校验(防止证书是伪造的)。

  • 1、判定证书的有效期是否过期

  • 2、判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构)

  • 3、验证证书是否被篡改: 从系统中拿到该证书发布机构的公钥, 对签名解密, 得到一个 hash 值(称为数据摘要), 设为 hash1. 然后计算整个证书的 hash 值, 设为 hash2. 对比 hash1 和 hash2 是否相等.如果相等, 则说明证书是没有被篡改过的。

  • PS: HTTP协议默认80端口,HTTPS协议默认443端口,FTP协议默认21端口等

2、面试官:说说你对UDP,TCP的理解?应用场景?

  • UDP : 用户数据包协议,是一个简单的面向数据报的通信协议。

  • 特点如下:

  • UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。

  • 传输途中出现丢包,UDP也不负责重发。

  • 当包的到达顺序出现乱序时,UDP没有纠正的功能。

  • 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的机制。即使是出现网络拥堵的情况,UDP也无法进行流量控制等避免网络拥塞行为。

  • TCP: 传输控制协议,是一种可靠,面向字节流的通信协议。

  • 特点如下:

  • TCP充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。

  • 此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才发送数据,从而可以控制通信流量的浪费。

  • 根据TCP的这些机制,在IP这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和,序列号,确认应答,重发控制,连接管理以及窗口控制等机制实现 )

  • 区别:

标题 TCP UDP
可靠性 可靠 不可靠
连接性 面向连接 无连接
报文 面向字节流 面向报文
效率 传输效率低 传输效率高
双共性 全双工 一对一、一对多、多对一、多对多
流量控制 滑动窗口
拥塞控制 慢开始、拥塞避免、快重传、快恢复
传输效率
  • 应用场景:

【15-20K】你该知道的前端升级版面试题 (1.7W字含答案!)

3、面试官:说说你对三次握手四次挥手的理解:

  • 答:具体解析见CSDN:https://blog.csdn.net/jiang_jin3323/article/details/124882194?spm=1001.2014.3001.5502

4、面试官:说说你WebSocket的理解?应用场景?

  • 答:

  • 什么是 WebSocket?

  • WebSocket,是一种网络传输协议,位于OSI模型对应用层。可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通讯。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

  • 优点:

  • 较少的控制开销: 数据包头部协议较小,不同于HTTP每次请求需要携带完整的头部。

  • 更强的实时性: 相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

  • 保持长连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证。

  • 更好的二进制支持:定义了二进制帧,更好处理二进制内容。

  • 支持扩展: 用户可以扩展WebSocket协议,实现部分自定义的子协议。

  • 更好的压缩效果: Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

  • 应用场景:

  • 基于Websocket的实时通信的特点,其存在的应用场景大概有:

  • 弹幕

  • 媒体聊天

  • 协同编辑

  • 基于位置的应用

  • 体育实况更新

  • 股票基金实时更新

3、面试官:说说你是怎么保证接口数据安全?

  • 答:

  • 1、接口数据加密:前端应该对敏感数据进行加密,防止数据被窃取或篡改。可以采用AES、RSA等加密算法。

  • 2、Token验证:前端应该对用户身份进行验证,防止非法用户访问系统。可以采用JWT等Token验证方式。

  • 3、timestamp超时机制( 时间戳超时机制 ,保证接口安全。就是:用户每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后,解密,验签通过后,与服务器当前时间进行比对,如果时间差大于一定时间 (比如3分钟),则认为该请求无效。)

  • 4、 采用HTTPS:前端应该采用HTTPS协议,保证数据传输的安全性。

  • 5、 数据加签名,验证签名:前端应该对数据进行签名,防止数据被篡改。可以采用HMAC等签名算法。

  • 6、防止XSS攻击  :前端应该对用户输入的数据进行过滤和转义,防止XSS攻击。

  • 7、防止CSRF攻击:前端应该采用CSRF Token等方式防止CSRF攻击。

  • 8、 防止点击劫持  :前端应该采用X-Frame-Options等方式防止点击劫持。

  • 9、防止DDoS攻击:前端应该采用CDN等方式防止DDoS攻击。


原文始发于微信公众号(猿来是前端):【15-20K】你该知道的前端升级版面试题 (1.7W字含答案!)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/250431.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!