前端面试题收集
# HTML、CSS
这里收录一些html、css的经典面试题
# 1.盒模型
答案
盒模型是css3中提出的对于元素大小描述的概念:概念中指出把每一个元素都比喻成一个盒子,他们有各自的宽高。 css3中盒模型分为两种:标准盒模型、怪异盒模型
盒模型一共由四个部分组成:margin、padding、border、content
。两种盒模型的区别如下:
- 标准盒模型:
- 在dom结构中实际占用 =
width + padding + border + margin
- 实际宽度 =
width + padding + border
- 在dom结构中实际占用 =
- 怪异盒模型:
- 在dom结构中实际占用 =
width + margin
- 实际宽度 =
width
- 在dom结构中实际占用 =
修改盒模型可以通过box-sizing
来实现:
- box-sizing:
content-box
表示标准盒模型 - box-sizing:
border-box
表示 IE 盒模型
# 2.响应式布局
- 使用
css
媒体查询:根据不同的屏幕尺寸写不同的样式 flex
弹性盒子- 流媒体布局:使用百分比来设置宽高
- 采用
css
相对单位
# 3.说一说flexible的实现原理
答案
flexible
是淘宝移动端的响应式适配方案,通过动态计算根元素的字体大小+使用rem
作为css尺寸单位来实现适配不同分辨率的屏幕。
- 动态计算根元素的字体大小
- 根据设备的
dpr(设备像素比)
值来计算html根元素的字体大小,公式:html的字体大小=基准字体大小×缩放比例
,基准字体大小是一个预先设定的值,它对应于基准dpr(通常是1)
时的字体大小。缩放比例是当前dpr
与基准dpr
的比值。
- 根据设备的
- px转rem
- rem =
px / 根元素字体大小
- rem =
# 4.重绘和回流
答
重绘是指元素外观(颜色,背景,边框)发生变化,布局未受影响,浏览器重新绘制该元素外观。
回流是指元素的布局(位置,大小,显隐)发生改变,导致页面重新计算布局和渲染,比重绘的性能开销要大。
# 5.什么是事件委托,他有什么用
答
事件委托从字面意义来说就是把本该自身触发的事件委托给别人来触发,在DOM中存在一个事件冒泡机制,指的是当DOM树种的一个元素发生事件时,这个事件会从目标元素开始逐级向上传播,直到根节点。 而事件委托正是利用了这一点,将事件监听绑定到父元素上而不是子元素上。这样可以监听多个子元素的点击事件从而不需要去写那么多的监听。
应用场景:对一个有非常多选项的导航栏添加监听事件就可以采取事件委托的形式,将监听绑定在父元素,这样只需要一个事件监听即可监听所有导航菜单的事件
# 6.css优化策略
- 尽量减少选择器的复杂度
- 少用!important,他会破坏css正常的优先级规则,导致代码难以维护和调试
- 动画和过渡效果尽量采用transform来实现,这样可以使得GPU加速,以及减少页面的回流
- 采用css预处理器(sass,less等)
- 压缩css
# JS
这里收录一些js的经典面试题,涉及到js的方方面面
# 1.for循环
for(var i = 1;i<=5;i++){
setTimeout(()=>{
console.log(i)
},0)
}
//输出 6,6,6,6,6
for(let i = 1;i<=5;i++){
setTimeout(()=>{
console.log(i)
},0)
}
//输出 1,2,3,4,5
2
3
4
5
6
7
8
9
10
11
12
13
上面这两段代码 为什么var
会输出5个6
换成let
后就能正确输出1,2,3,4,5
呢?
var
声明的变量会提升,因此代码中的i
会被提升为全局
变量,作用域不止在for
循环中。根据事件循环机制,
定时器会在for
循环全部执行完毕之后再执行,当for
循环执行完成后,由于都是对同一个i
去自增,所以这时定时器
中访问的i
就是自增5
次后的i
。所以会连续输出5个6
。
而在let
不存在变量提升,所以每次迭代都会执行一遍let i = 1
,并且for
循环会记住上一次迭代的值并将他赋值给i
这样每次迭代执行的定时器都会访问到一个新的i
,并且这个i
的值还是上一次迭代后的结果。所以可以正确的输出1,2,3,4,5
如果不使用let
还有没有别的办法呢?
其实这里主要考验的就是作用域和事件循环机制,为什么var
不行?因为var
声明的i
作用域是全局的,导致定时器最后访问的都是同一个i
。
而我们其实只要保证定时器每次访问的i
是一个独立的变量一个作用域为定时器内的就行了。
for(var i = 1;i<=5;i++){
(function (j){
setTimeout(()=>{
console.log(j)
},0)
}) (i)
}
2
3
4
5
6
7
这里我们用一个自执行函数
,将每次迭代后的i
作为参数传进去,这样函数体内接受到的形参就是一个独立的值(根据函数参数是值传递的原理)。
定时器也每次也就会访问到只属于这个自执行函数作用域内的i
了。
# 2.数组map方法和reduce方法的区别
答案
- 相同点
- 都可以用来遍历可迭代对象
- 都接受一个
callback
函数,并且数据里的每个元素都会去执行一遍这个callback
- 对稀疏数组的处理逻辑一致,都会跳过空槽
- 都不改变原数组,返回新值(注意如果修改原数组中引用类型数据的值则会影响到原数组,但这不是这两个api造成的)
- 不同点
- callback的入参不同:
map
接受三个参数(当前遍历项,当前遍历项的索引,原数组)。reduce
接收(上一次调用callback
的结果,当前遍历项)等4个参数 map
的callback支持可以指定this的值,reduce
不行
- callback的入参不同:
# 3.说一说new的过程中发生了什么
答
js的new
关键字通常用来创建一个新的对象,他的后边一般会跟一个class
或者一个构造函数,在构造函数中会先创建一个对象,然后讲这个对象的显示原型指向构造函数的隐式原型
最后在return
这个对象。这样以来所有通过这个构造函数创建的对象都会共享同一个原型
# 4.Proxy与Object.defineProperty的用法和传参区别
二者都是用来拦截对象操作的
详解:
- Object.defineProperty
- Object.defineProperty(obj, prop, descriptor);
- 参数:
- obj:要在其上定义属性的对象。
- prop:要定义或修改的属性的名称。
- descriptor:属性描述符,可以包含以下属性:
- value:属性的值。
- writable:属性是否可写。
- enumerable:属性是否可枚举。
- configurable:属性是否可配置。
- get:获取属性值的函数。
- set:设置属性值的函数。
- 示例:
const obj = {}; Object.defineProperty(obj, 'prop', { value: 'value', writable: true, enumerable: true, configurable: true });
1
2
3
4
5
6
7
- Proxy
- new Proxy(target, handler);
- 参数:
- target:要创建代理的目标对象。
- handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数。
- 示例
const target = { prop: 'value' }; const proxy = new Proxy(target, { get(target, prop) { console.log(`Property ${prop} has been read.`); return Reflect.get(target, prop); }, set(target, prop, value) { console.log(`Property ${prop} set to ${value}.`); return Reflect.set(target, prop, value); } });
1
2
3
4
5
6
7
8
9
10
11
区别:
Object.defineProperty
主要用于拦截对象或定义对象的某个属性Proxy
用于创建一个对象的代理可以拦截自定义对象上的操作,并且可以映射到原对象上
# 5.Proxy为何要结合Reflect使用
答
Reflect
提供了一组静态方法,这些方法和Proxy
的陷阱方法都相对应,通常在Proxy
中我们需要执行一些自定义逻辑然后继续执行对象的默认行为,这时候Reflect
刚好提供了这些操作默认对象的方法,来确保对象的行为与没有被拦截是一致。
他们俩配合使用可以保持代码纯净
# 6.说一说js的六中继承模式
1.原型链继承
通过将一个对象的原型设置为另一个对象来实现继承。
function Parent() {}
function Child() {}
Child.prototype = new Parent();
2
3
2.构造函数继承
使用父类的构造函数来增强子类实例,等同于复制父类的实例属性给子类(不包括方法)。
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 调用 Parent 构造函数
}
2
3
4
5
6
3.组合继承(原型链 + 构造函数)
结合原型链和构造函数的优点,既能保证原型方法的复用,又能避免构造函数的重复调用。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name) {
Parent.call(this, name); // 继承 Parent
}
Child.prototype = new Parent(); // 继承 Parent 原型方法
Child.prototype.constructor = Child; // 修复 Child 原型的 constructor 指向
2
3
4
5
6
7
8
9
10
11
12
4.原型式继承
创建一个对象来作为其他对象的原型。
var parent = {
name: "parent",
sayName: function() {
console.log(this.name);
}
};
var child1 = Object.create(parent);
child1.name = "child1";
2
3
4
5
6
7
8
9
5.寄生式继承
创建一个函数,创建一个对象,通过函数的返回值来增强对象。
function createAnother(original) {
var clone = Object.create(original); // 创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
2
3
4
5
6
7
6.寄生组合式继承
通过组合寄生式继承和组合继承的优点,来实现更完美的继承方案。
function inheritPrototype(childObject, parentObject) {
var prototype = Object.create(parentObject.prototype); // 创建对象
prototype.constructor = childObject; // 增强对象
childObject.prototype = prototype; // 指定对象的原型
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
inheritPrototype(Child, Parent); // 继承方法
Child.prototype.sayAge = function() {
console.log(this.age);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.知道AbortController吗?说说他的作用
答
AbortController
接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。
主要用法如下
const controller = new AbortController();
const signal = controller.signal; // 是否中止的标识,将他传递给需要被中止的异步任务
fetch('https://example.com/data', { signal })
controller.abort() // 调用控制器的abort方法就会中止掉这个fetch请求
2
3
4
5
6
7
除了可以中止网络请求之外,它还可以用来清除事件监听,比如我们常用的addEventListener
const cb = ()=>{}
window.addEventListener("test", cb);
window.removeEventListener("test", cb);
2
3
通常我们需要将回调函数抽象出来以用于清楚这个事件监听,用上AbortController
之后就会方便很多
const controller = new AbortController();
const signal = controller.signal; // 是否中止的标识,将他传递给需要被中止的异步任务
window.addEventListener("test", cb,{
signal
});// addEventListener的第三个参数中支持配置signal
controller.abort() // 调用控制器的abort方法就会清楚掉这个事件监听
2
3
4
5
6
7
8
9
# 8.原型和原型链
答
js是面向对象的编程语言,每个对象都有一个__proto__
属性指向他的原型对象,而这个对象的构造函数则有一个prototype
属性,与这个对象的__proto__
指向的是同一个对象。这个被指向的就是原型对象
实例可以访问原型对象上的属性,当我们在访问一个实例的属性时会先在自身查找,没找到就会到原型对象上查找,也就是去__proto__
查找。直到找到最终的Object
对象则会返回null
,这一整个查找的过程就是原型链
# 9.call、apply、bind
答
这三个api都是用来改变this
指向的,区别在于call,apply
是立即执行,只是传参方式不同,bind
是返回一个新的函数,以便来保持调用时的this
上下文
# 10.箭头函数
答
1.箭头函数没有自己的this
,他会将当前所处的上下文作为自己的this
2.箭头函数不能作为构造函数使用
3.箭头函数不能绑定arguments
,取而代之用reset
参数解决,同时没有super
和new.target
4.使用call,apply,bind
并不会改变箭头函数的this
指向
# 11.proxy能拦截到对象中的对象的变化吗?
答
proxy
只会拦截对象第一层属性的变化,如果这其中存在引用类型的属性,那只能拦截到他本身的变化 不能拦截到他的属性的变化。
例如:
const a = {
name:'xxx',
obj:{
age:2
}
}
a.obj = 111 // 这样是可以被proxy拦截到的
a.obj.age = 3 // 这样不会被拦截
2
3
4
5
6
7
8
由此可见 如果需要对目标对象完全拦截则需要对他的属性进行递归的proxy
代理
# 12.new function和new Class 有什么区别
答
二者都是用来创建对象的,不同的是
class
是es6引入的新语法,使得基于类的面向对象编程在语法上更加清晰和直观class
相较于function
,能通过extend
更方便的实现继承,而function
则需要手动设置原型- 语义上来说
class
更加简洁直观
# 13.如何理解闭包
答
要理解闭包就得先理解作用域和作用域链
- 作用域:
- 作用域指的是一个变量被定义的区域,他决定了如何查找这个变量以及变量被访问的权限,作用域又被分为全局作用域,函数作用域和块级作用域。而闭包关联的就是函数作用域
- 作用域链:
- js中作用域是嵌套的,就如同原型链那样(当我们访问一个对象的属性的时候会由自身-原型对象-Object这样一层一层向上查找),我们在某一个作用域里访问变量的时候也会一层一层向上级作用域查找
闭包则是在函数嵌套的情况下形成的,当内部函数被返回或者被传递到外部的时候,由于作用域链的关系我们任然何以在内部函数中访问到外部函数的变量,即使外部函数已经执行完毕。
闭包的用途:
- 封装私有变量,使其不能直接被外部访问实现私有化
- 在回调函数中使用,保存函数内部状态。通常在递归的时候会用到
闭包的缺陷:
- 内存泄漏:由于函数内的变量一直被闭包引用,导致不会被垃圾回收机制回收,这样会一直占着内存
- 性能问题:大量的使用闭包会导致性能问题,由于过多的闭包变量得不到释放,会一直占用内存
# 14.说一说事件循环机制
答
首先需要知道微任务和宏任务,js代码在处理异步任务上将异步任务分为了微任务和宏任务 常见的有如下几种
- 微任务:
- process.nextTick(nodejs独有)
- Promise.then()
- Object.observe
- MutationObserver
- 宏任务:
- script(整体代码)
- setTimeout、setInterval、setImmediate(nodejs独有)
- requestAnimationFrame(浏览器独有)
- IO、UIrender(浏览器独有)
当一段js代码在执行的时候会去维护一个微任务队列和宏任务队列,在执行过程中遇到了微任务或者宏任务就会将他们压入对应的任务队列。等当前代码执行完毕后就会将微任务队列中的微任务取出来执行 如果在执行过程中产生了新的微任务或者宏任务则会将他又压入相应的任务队列,直到整个微任务队列清空,然后去执行宏任务队列里的宏任务,并且循环上述过程,直到两个队列里的任务都被清空。这就是整个事件循环机制
# 15.es6的新特性
- let、const 关键字
- 箭头函数
- ...扩展运算符
- map、set结构
- Promise异步编程
- class语法
- 模块化 import
# 16.说一说es6的模块化
提示
模块化是很早就有的一个概念,他可以让代码复用更加简洁,以及保证代码的纯净度
nodejs中的模块化和es6的模块化略有不同
- nodejs模块化
- 关键字为require,module.export
- 动态导入,代码运行时确定模块依赖关系
- 通过缓存所有模块的方式来解决循环引用的问题
- 模块导出后修改模块的值不会影响外部
- ESM
- 静态导入,代码在编译时就会加载
- 支持tree shaking
- 导入会提升,优先加载
# Vue
这里收录一些vue的面试题,包括vue2和vue3
# 1.vue3 为什么需要用reflect配合proxy使用
答案
Reflect
是一个内置的全局对象,它提供了拦截 JavaScript
操作的方法。Reflect
对象的方法与 Proxy
处理的陷阱(trap
)一一对应。使用Reflect
的主要原因是为了保持与普通对象操作的一致性。
Reflect
确保了即使在 Proxy
拦截了某些操作的情况下,对象的默认行为也能被保持,同时提供了一种避免代码重复和确保兼容性的方法。
# 2.watch和computed是否可以异步,为什么?
答案
watch
支持异步,computed
不支持异步:watch
的回调支持异步是因为他的触发之后就已经脱离了依赖项,在检测到依赖项变化后会触发回调函数,此时回调函数即使是异步也不会影响到什么。而computed
不支持异步是因为其内部存在的缓存机制,如果回调是异步的话那么缓存机制就无法拿到最新的值,会永远返回缓存的旧值。
# 3.vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?
答案
简单来说,diff
算法有以下过程
- 同级比较,再比较子节点
- 先判断一方有子节点一方没有子节点的情况(如果新的
children
没有子节点,将旧的子节点移除) - 比较都有子节点的情况(核心
diff
) - 递归比较子节点
正常Diff
两个树的时间复杂度是O(n^3)
,但实际情况下我们很少会进行跨层级的移动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才需要用核心的Diff
算法进行同层级比较。
Vue2
的核心Diff
算法采用了双端比较的算法,同时从新旧children
的两端开始进行比较,借助key
值找到可复用的节点,再进行相关操作。相比React
的Diff
算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x
借鉴了 ivi
算法和 inferno
算法
在创建VNode
时就确定其类型,以及在mount/patch
的过程中采用位运算来判断一个VNode
的类型,在这个基础之上再配合核心的Diff
算法,使得性能上较Vue2.x
有了提升。(实际的实现可以结合Vue3.x
源码看。)
该算法中还运用了动态规划的思想求解最长递增子序列
。
# 4.keep-alive的原理
答案
keep-alive
可以实现组件的缓存,当组件切换时不会对当前组件进行卸载
其内部主要采用了缓存淘汰策略来实现
# 5.nextTick的作用是什么?实现原理?
答案
nextTick
会在dom
更新结束之后执行回调,主要是利用了宏任务微任务的执行顺序不同来实现的,根据当前环境支持成都不同以降级的方式采用以下几个api
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
# 6.vue2和vue3的响应式原理
答
- vue2:
- 在我们
new
一个vue
的时候,vue
会首先进行一次代理data
中的属性,从而使我们可以直接通过this
来访问data
的属性。然后vue
会进行属性的递归遍历,一一进行拦截也就是Observe
这个类的作用。这其中会有一个Dep
类来收集依赖,并且负责派发更新,然后有一个watcher
负责提供依赖,并且负责更新视图。vue
不会为每一个属性都生成一个watcher
,而是在这个watcher
被使用到了模板中才会去实例化watcher
,然后保存在Dep
的静态属性target
上,这样在模板编译的时候会访问这个属性,从而使dep
收集到这个属性对应的watcher
。然后当属性的值发生变化的时候通过dep
去派发更新,调用watcher
的update
方法,update
又会调用实例化watcher
的时候 保存下来的更新模板的callback
,从而更新视图
- 在我们
- vue3:
Vue3
的响应式主要是通过proxy
去代理对象,然后拦截get
和set
等一系列操作。在响应式系统的内部维护了一个WeakMap
结构的依赖对象用于存放所有的响应式key
和其对应的依赖(这里其实就对标的是Vue2
的Dep
实例)。而产生依赖则是通过Effect
这个函数去创建依赖,这里其实对应的就是Vue2
里的Watcher
观察者。然后在get
和set
的拦截操作里去收集和触发依赖。学习响应式主要搞清楚在整个系统中有哪些角色,以及每个角色是干嘛的包括他们之间是如何串联起来的。搞明白了这些,就会学的非常轻松了。换句大白话概述一下vue
的响应式:编译模板的时候生成更新函数也就是大家口中所说的依赖。然后在拦截到get
的时候把当前生成的依赖保存起来,接着在set
的时候去调用他们就行了。
# 7.vue中如何只让一个对象的第一层为响应式?
答
vue3中可以使用reactive
和toRefs
两个api配合达到目的
const obj = reactive({
nest:{
a:1,
b:2
}
})
const {nest} = toRefs(obj)
2
3
4
5
6
7
8
这样nest
的a
和b
属性就会失去响应式,因为toRefs
只会将第一层转为响应式
vue2中可以通过Object.freeze
来冻结属性
# 8.vue如何做到监听数组的push等操作方法,缺陷原因
答
由于Object.defineProperty()
拦截数组太过消耗性能,所以vue2中对7中可以修改元数据的数组api进行了重写
# 9.v-for为什么需要加key,为什么不建议使用数组下标作为key
答
1.vue在更新视图的时候需要比对新旧vnode
,尽可能的复用那些没有改变的vnode
,这个时候如果有key
会更方便diff
算法去比对。
2.如果使用数组的下标作为key
的时候,当数组长度发生了变化 下标就会跟着变化,这个时候由于key
变了 就会导致一些原本可以服用的节点被视为了需要更新的节点。
例如v-for
遍历一个长度为5的数组,key
的值依次为0,1,2,3,4,这个时候如果往0后面添加一项,原本的1234这四个节点的key
就都会发生变化。而原本只需要新增一项就可以了
其他的节点都是能复用的,但因为key
发生变化了 所以就会导致这四个节点也会重新渲染,造成不必要的性能开销
# 10.scope 是怎么做的样式隔离的
答
1.Vue 为每个带有 scoped
属性的组件生成一个唯一的作用域ID(如 data-v-f3f3eg9)
。这个ID
是随机的,确保每个组件的作用域ID
是独一无二的。
2.Vue 会在编译组件模板的过程中,将这个作用域ID
作为自定义属性添加到组件模板的所有元素上。例如,如果作用域ID
是data-v-f3f3eg9
,那么在该组件模板的所有元素上都会添加一个属性data-v-f3f3eg9
。
# React
这里收录一些react的面试题
# 工程化
这里收录一些工程化的面试题
# 1.webpack中file-loader和url-loader的区别
二者都是用于处理文件,url-loader
是file-loader
的封装
- file-loader: 不对文件进行处理只用于复制文件,将文件复制到目标路径下
- url-loader:可以将小文件转成
base64
格式
# 2.webpack和vite的区别
webpack
和vite
都是现代工程化构建工具,二者出于不同的设计理念。主要区别在于开发环境
开发环境:webpack
本质上是一个模块打包器,他把项目中的所有模块都打包成一个或者多个bundle
,在处理大项目时由于webpack
需要构建完所有模块才能产出构建产物,所以通常来说速度会很慢,而vite
采用浏览器支持esmodule
导入的特性,在开发阶段只对依赖进行与构建(产出每个依赖和他的路径的映射表)。在浏览器需遇到import
语句的时候会去发起请求,vite
这时动态的去构建被请求的文件 并且响应给浏览器由此完成整个链路。
也就是说vite
并不需要在项目启动时就构建完所有的文件 而是浏览器用到了哪个他才去构建哪个,这也就是为什么vite
开发环境速度飞快的原因。
生产环境:vite
生产环境和webpack
大同小异,也是需要构建所有的产物,值得注意的时vite
在生产环境采用的时rollup
,而开发环境采用的时esbuild
去构建
# 3.vite衍生问题:esbuild和rollup的区别
esbuild
由GO
编写,速度由于rollup
,并且他对跨平台的支持和多语言的支持更加良好,使用起来简单。rollup
是一个专注es
模块的打包器,它使用es
语法来打包代码,他的摇树优化非常优秀,打包速度也很快并且支持输出不同格式的模块代码如ESM
,CommonJS
,UMD
。
vite
在开发环境选择esbuild
是为了快速相应浏览器的请求,而生产阶段用rollup
则是更好的支持构建。
# 4.npm和npx有什么区别?
答
npm
是包管理器,npx
是包运行器。nodejs
会默认采用npm
作为依赖管理器来安装,共享和管理各种依赖项。npm
提供了许多命令如npm install
,npm run
等来方便管理包
npx
则是运行某一个包而无需全局安装他,他会自动在npm
注册表里找到相应的包并运行他,通常他用来执行一些不常用的命令,避免全局安装过多的包
# 浏览器
浏览器相关面试题
# 1.什么是强缓存和协商缓存
浏览器缓存是提升用户体验和减轻服务器压力的重要手段,这其中包括强缓存
和协商缓存
两种:
强缓存:
强缓存
是通过Expires
和Cache-Control
来实现的,当浏览器请求一个资源的时候会先判断是否命中强缓存
,是则直接使用本地缓存的内容Expires
: 这是http1.0
的头部,用来指定资源到期时间,浏览器会根据这个时间判断是否需要更新资源Cache-Control
: 这事http1.1
新增的头部,由于Expires
不够灵活,所以新增了这个头部,来提供更灵活的配置:如max-age:3600
(表示资源能够被缓存多久),no-cache
(标识不缓存资源)。Cache-Control
的优先级高于Expires
协商缓存:当
强缓存
没有命中时,浏览器会使用协商缓存
,这个时候就需要跟服务器进行交互了。服务器在响应式会带上Last-Modified
或ETag
这两个头部信息。Last-Modified / If-Modified-Since
:Last-Modified
表示资源最后一次修改的时间If-Modified-Since
表示上次请求时的最后修改时间
,也就是服务器返回的Last-Modified
。由此让服务器判断是否更新了资源。ETag / If-None-Match
:ETag
表示资源的唯一标识
,浏览器再次请求时会携带If-None-Match
,If-None-Match
就是上次请求时服务器返回的资源的ETag
,通过这种方式让服务器判断是否需要更新资源。
总结
强缓存主要通过Expires(资源到期时间)
和Cache-Control(内涵很多字段并且可设置不缓存)
来实现,这两个字段都由服务器返回
,浏览器自行判断
。此时浏览器和服务器不进行
资源交换。协商缓存通过Last-Modified / If-Modified-Since
和ETag / If-None-Match
这两对来和服务器进行交互,简单来说就是服务器返回一个字段给浏览器,浏览器下次请求时携带上这个字段,由服务器来判断是否需要更新资源,如不需要服务器会相应304(Not Modified)
状态码,如需要则返回新的资源。
# 2.cookie和session
这两个都是用来跟踪验证用户信息的手段
- cookie:
- 由服务端生成,通过
http
协议发送给客户端,客户端存储后在每次请求时携带上 - 大小受限,一般不超过
4kb
- 可以跨页面访问
- 由服务端生成,通过
- session:
- 由服务器生成,存储与服务器。每个用户都会被分配一个唯一的
Session ID
,该ID通过Cookie
或URL
重写的方式发送给客户端浏览器,并在后续的请求中携带。 - 可以存储更多数据
- 可以跨页面访问
- 由服务器生成,存储与服务器。每个用户都会被分配一个唯一的
关闭浏览器后 两者会有何变化
- 会话级
cookie(没设置过期时间)
会被销毁 - 持久级
cookie(设置了过期时间)
不会被销毁 session
会被销毁,由于session
是由浏览器当前窗口记录sessionID
来何服务器交互的所以即使没有关闭当前窗口 新开一个窗口session
也会失效
# 3.cookie是否可以跨域名使用
答案
是可以的,但是需要配置domain
主域名的形式来实现,不同的域名必须归属于同一主域名下才可以实现cookie
的跨域名访问
# 4.cdn为什么不会受同源策略影响
答案
浏览器的同源策略限制了网络请求必须相同协议,相同ip
,相同的端口。但是cdn
通常会何当前域名不同源,他没有受影响主要是
因为cdn
一般通过script
标签或者img
标签或者link
标签加载。通过标签的src
属性发送的请求不会受同源策略影响,jsonp
解决跨域问题也是利用了这个原理
# 5.浏览器都包含哪些进程
答
- Browser进程
- 浏览器的主进程(负责协调、主控),只有一个
- 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- GPU进程
- 最多一个,用于
3D
绘制等 GPU
的使用初衷是为了实现3D CSS
的效果,只是随后网页、Chrome 的 UI 界面都选择采用GPU
来绘制,这使得GPU
成为浏览器普遍的需求。
- 最多一个,用于
- 渲染进程
- 浏览器内核,
Renderer
进程,内部是多线程的 - 核心任务是将
HTML、CSS
和JavaScript
转换为用户可以与之交互的页面,排版引擎Blink
和Javascript
引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- 浏览器内核,
- 网络进程
- 主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
- 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
# 6.渲染进程包含了哪些线程?
# 计算机网络
这里收录一些计算机网络的面试题
# 1.http和https有什么区别?
答
http
和https
都是数据传输协议,他们的主要区别如下:
- 安全性
http
传输采取明文的方式容易被篡改,窃听。它不提供数据加密、身份验证和数据完整性验证https
在http
的基础上新增了SSL/TSL
协议,提供了数据加密、身份验证和数据完整性验证。同时也验证了服务器身份,防止第三方攻击
- 端口
- http默认
80
端口 - https默认
443
端口
- http默认
- 性能
- http性能优于https,因为它不需要加密解密操作
- https由于新增了数据加密解密的操作所以会多一点细微的性能开销
- 证书
- http不需要证书
- https需要一个受信任机构(CA)签发的
SSL/TSL
证书,来验证服务器是否安全
# 2.说一说https的请求过程中发生了什么
答
1.客户端请求https
网址,然后连接到server
的443
端口(https默认443端口)
2.采用https
协议的服务器上必须有一套数字CA证书
并且证书会带有一个公钥和一个私钥,私钥由服务端自己保存,不可泄露。
3.响应请求的时候服务器会把证书以及证书携带的公钥一起传输给客户端
4.客户端校验证书是否合法,不合法浏览器会给出警告。如果合法则取出公钥,并生成一个随机码KEY
并且使用公钥加密这个KEY
5.客户端把加密后的随机码KEY
传输给服务器,服务器使用私钥解密这个KEY
,并用这个解密出来的随机码KEY
对数据进行对称加密然后响应数据给客户端
6.客户端使用随机码KEY
对数据进行解密
整个过程中可以分为三大步骤:
- 1.校验证书是否合法
- 2.证书合法的情况下进行通过非对称加密的方式生成了一个随机码
KEY
- 3.服务器,客户端双方在后续的数据传输过程中通过这个随机码
KEY
以对称加密的形式传输数据
这里附上一个详细的步骤图

# 3.说一说http1.x,http2的区别
答
http1.0缺陷:
- 线程阻塞,同一时间同一域名的请求有数量限制,超出数量的会被阻塞
- 只保持短连接:每次请求都要重新建立
tpc
连接(即三次握手)这样非常消耗性能
http1.1改进:
- 持久链接,
tcp
链接默认不关闭,可以被多次使用 - 管道机制:在同一个
tcp
链接里客户端可以发送多个请求 - 分块传输:即服务端每产生一块数据就传输一块给客户端,采用流模式代替缓存模式
http1.1缺陷:
- 管道机制虽然可以在同一个
tcp
链接里发送多个请求,但请求依然是按次序进行的,后面的请求必须等待前面的请求相应完毕才能执行。这就造成了对头堵塞的问题
http2.0改进:
- 采用二进制协议传输
- 完全多路复用:解决了
http1.1
的对头阻塞问题 - 报头压缩:
http
协议是没有状态的,这就导致每次传输都必须携带上所有信息,造成很多头字段重复传输。http2
引入报头压缩的概念,头信息使用gzip
或compress
压缩后再发送 并且客户端和服务端头信息表,所有字段都会存入这个表,后续的请求只需要传输索引就可以了 - 服务器推送:
http2
之前只支持客户端主动请求服务器,http2
新增了服务器可以主动向客户端推送资源,具有代表性的就是sse
协议
# 4.完整域名的构成部分有哪些
答
一个完整域名通常由协议、二级域名、顶级域名三部分构成。拿https://baidu.com
来举例
- 顶级域名:通常有
.com
、.cn
、.net
、```.org``等等,位于完整域名的最后面 - 二级域名:在顶级域名的左边,例如例子中的
baidu
- 子域名:位于二级域名的左边,例如
blog.baidu.com
中blog
就是子域名 - 协议:网络通信协议,例子中的
https
,http
等
# 项目题
这里收录一些工作中遇到的问题
# 1.你项目中有写到低代码生成,可以说说拖拽的实现吗?
答
首先画布分为左侧(菜单栏)中间(渲染区域)右侧(组件表单)。拖拽是发生在将左侧菜单栏中某一个菜单拖入中间渲染区域,然后会自动生成对应的组件。
项目中的拖拽主要是用到dragstart
和drop
事件,首先给可拖拽的组件设置draggable
属性和dragstart
事件,拖拽的时候会触发dragstart
事件,这个时候会去设置一个组件的信息到参数中。
<div @dragstart="handleDragStart" class="component-list">
<div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
</div>
handleDragStart(e) {
e.dataTransfer.setData('index', e.target.dataset.index)
}
2
3
4
5
6
7
8
9
10
然后鼠标拖到可放置区域的时候会触发放置区域dom
的drop
事件,我们就可以拿到对应的组件信息
<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
<Editor />
</div>
handleDrop(e) {
e.preventDefault()
e.stopPropagation()
const component = deepCopy(componentList[e.dataTransfer.getData('index')])
this.$store.commit('addComponent', component)
}
2
3
4
5
6
7
8
9
10
这个时候就可以去操作渲染对应的组件了。
# 2.你的组件库如何接收外界传入的参数呢?
答
组件库接受参数的方式一共两种,第一种是通过props
。第二种是通过provide
和inject
。组件库内部暴露了一个方法出来供使用方传入参数,该方法会将这个参数通过inject
注册到全局。然后每个组件都可以通过provide
将这些参数注入并加以使用。
# 3.你说你的组件库的点击事件是由使用方统一注入的,说说你是如何设计的
答
该组件库是一个业务组件库,每个组件都是业务组件并非通用组件,所以在编辑页面的时候就可以去为每个组件设置他的点击事件以及参数例如:
- 轮播图组件:
- 可以给每个轮播图设置他自己的点击事件,并且设置他的参数值
- 比如这里我给其中一个轮播图设置
linkType=shop
linkType表示点击事件的类型 - 然后再设置
linkTarget = 1
linkTarget表示点击事件的参数
这两个值会被保存到组件的配置信息中。
在c端使用的时候我们通过接口拿到当前页面配置的组件列表,然后将这些配置信息传入组件。并且c端维护了一份点击事件的map
映射。承接上文的轮播图组件:
c端维护这样一个对象:
const linkMap = {
shop(linkTarget){
// 这里拿到linkTarget 然后执行相应的跳转操作
},
...
}
2
3
4
5
6
c端将这个map
对象通过组件库暴露的方法传给组件库,组件库会通过provide
和inject
将这份数据注册。
然后组件库里其实一开始就写好了点击事件的,比如轮播图组件里的点击事件如下:
<mySwiper>
<swiperItem v-for="(item,index) in config" :key="index" @click="hanldeClickSwiper(item)"></swiperItem>
</mySwiper>
<script>
export default{
props:{
config:{
type:Object,
default:()=>({}) // config就是上文中我们保存的linkType和linkTarget
}
},
methods:{
hanldeClickSwiper(item){
//getLinkMap 是一个全局函数 里面的内容就是获取provide的值
// 这里拿到的linkMap就是c端维护的那个表了
const {linkMap} = getLinkMap()
linkMap[item.linkType](item.linkTarget)
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
整体的思路就是这样,c端维护一份点击事件表 传给组件库 组件库支持可配置的点击事件 触发点击事件的时候会去调用c段维护的那个点击事件表里对应的函数
# 4.如何判断用户设备?
在开发过程经常会需要判断用户设备而做出一些特殊处理,那么有哪些方式可以用来判断用户设备呢?
- 使用用户代理字符串(User-Agent)
- 用户代理字符串包含了浏览器类型、版本、操作系统等信息,可以通过分析这些信息来大致判断用户的设备类型。navigator.userAgent 属性用于获取用户代理字符串。
function detectDevice() { const userAgent = navigator.userAgent; if (/mobile/i.test(userAgent)) { return "Mobile"; } if (/tablet/i.test(userAgent)) { return "Tablet"; } if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { return "iOS Device"; } // Android, Windows Phone, BlackBerry 识别可以类似添加 return "Desktop"; } console.log(detectDevice());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 使用视口的尺寸 用户代理是可以被修改的,所以有些时候我们还可以通过判断视口的尺寸来判断设备,但这样就无法精确到是哪个设备了
function detectDeviceByViewport() {
const width = window.innerWidth;
if (width < 768) {
return "Mobile";
}
if (width >= 768 && width < 992) {
return "Tablet";
}
return "Desktop";
}
console.log(detectDeviceByViewport());
2
3
4
5
6
7
8
9
10
11
12
13
# 5.平时对项目做过哪些优化?
提示
对项目的优化主要可以分为以下三个部分:
- 构建产物体积的优化
- 静态资源存放在cdn上,以减少项目体积
- 优化冗余的代码
- 一些庞大的外部依赖库采用cdn的形式加载,如vue,react核心包
- 组件库采用按需加载的形式
- 开启tree shaking 将一些没有用到的代码不打包进构建产物
- 加载优化
- 路由的懒加载
- 图片的懒加载(可以通过IntersectionObserver观察元素可见性或者监听滚动条判断图片是否进入可视区域以及img标签自带的loading=lazy属性来实现)
- 图片预加载(通过Web Worker在子线程中预加载图片然后转为临时url在主线程中渲染)
- 图片采用webp格式减小体积
- 代码分割(将大的js文件分割成小的chunks)
- cdn加速
- gzip压缩资源减小资源体积
- 用户体验优化
- 白屏的loading状态
- 图片的加载状态