UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 面试

    • 前端面试题收集
      • HTML、CSS
        • 1.盒模型
        • 2.响应式布局
        • 3.说一说flexible的实现原理
        • 4.重绘和回流
        • 5.什么是事件委托,他有什么用
        • 6.css优化策略
      • JS
        • 1.for循环
        • 2.数组map方法和reduce方法的区别
        • 3.说一说new的过程中发生了什么
        • 4.Proxy与Object.defineProperty的用法和传参区别
        • 5.Proxy为何要结合Reflect使用
        • 6.说一说js的六中继承模式
        • 7.知道AbortController吗?说说他的作用
        • 8.原型和原型链
        • 9.call、apply、bind
        • 10.箭头函数
        • 11.proxy能拦截到对象中的对象的变化吗?
        • 12.new function和new Class 有什么区别
        • 13.如何理解闭包
        • 14.说一说事件循环机制
        • 15.es6的新特性
        • 16.说一说es6的模块化
      • Vue
        • 1.vue3 为什么需要用reflect配合proxy使用
        • 2.watch和computed是否可以异步,为什么?
        • 3.vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?
        • 4.keep-alive的原理
        • 5.nextTick的作用是什么?实现原理?
        • 6.vue2和vue3的响应式原理
        • 7.vue中如何只让一个对象的第一层为响应式?
        • 8.vue如何做到监听数组的push等操作方法,缺陷原因
        • 9.v-for为什么需要加key,为什么不建议使用数组下标作为key
        • 10.scope 是怎么做的样式隔离的
      • React
      • 工程化
        • 1.webpack中file-loader和url-loader的区别
        • 2.webpack和vite的区别
        • 3.vite衍生问题:esbuild和rollup的区别
        • 4.npm和npx有什么区别?
      • 浏览器
        • 1.什么是强缓存和协商缓存
        • 2.cookie和session
        • 3.cookie是否可以跨域名使用
        • 4.cdn为什么不会受同源策略影响
        • 5.浏览器都包含哪些进程
        • 6.渲染进程包含了哪些线程?
      • 计算机网络
        • 1.http和https有什么区别?
        • 2.说一说https的请求过程中发生了什么
        • 3.说一说http1.x,http2的区别
        • 4.完整域名的构成部分有哪些
      • 项目题
        • 1.你项目中有写到低代码生成,可以说说拖拽的实现吗?
        • 2.你的组件库如何接收外界传入的参数呢?
        • 3.你说你的组件库的点击事件是由使用方统一注入的,说说你是如何设计的
        • 4.如何判断用户设备?
        • 5.平时对项目做过哪些优化?
  • 更多
  • 面试
hanhanbuku
2024-02-27
目录

前端面试题收集

# HTML、CSS

这里收录一些html、css的经典面试题

# 1.盒模型

答案

盒模型是css3中提出的对于元素大小描述的概念:概念中指出把每一个元素都比喻成一个盒子,他们有各自的宽高。 css3中盒模型分为两种:标准盒模型、怪异盒模型

盒模型一共由四个部分组成:margin、padding、border、content。两种盒模型的区别如下:

  • 标准盒模型:
    • 在dom结构中实际占用 = width + padding + border + margin
    • 实际宽度 = width + padding + border
  • 怪异盒模型:
    • 在dom结构中实际占用 =width + margin
    • 实际宽度 = width

修改盒模型可以通过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 / 根元素字体大小

# 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
1
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)
}
1
2
3
4
5
6
7

这里我们用一个自执行函数,将每次迭代后的i作为参数传进去,这样函数体内接受到的形参就是一个独立的值(根据函数参数是值传递的原理)。 定时器也每次也就会访问到只属于这个自执行函数作用域内的i了。

# 2.数组map方法和reduce方法的区别

答案

  • 相同点
    • 都可以用来遍历可迭代对象
    • 都接受一个callback函数,并且数据里的每个元素都会去执行一遍这个callback
    • 对稀疏数组的处理逻辑一致,都会跳过空槽
    • 都不改变原数组,返回新值(注意如果修改原数组中引用类型数据的值则会影响到原数组,但这不是这两个api造成的)
  • 不同点
    • callback的入参不同:map接受三个参数(当前遍历项,当前遍历项的索引,原数组)。reduce接收(上一次调用callback的结果,当前遍历项)等4个参数
    • map的callback支持可以指定this的值,reduce不行

# 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();
1
2
3

2.构造函数继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例属性给子类(不包括方法)。

function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name); // 调用 Parent 构造函数
}
1
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 指向
1
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";
1
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; // 返回这个对象
}
1
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);
};
1
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请求

1
2
3
4
5
6
7

除了可以中止网络请求之外,它还可以用来清除事件监听,比如我们常用的addEventListener

const cb = ()=>{}
window.addEventListener("test", cb);
window.removeEventListener("test", cb);
1
2
3

通常我们需要将回调函数抽象出来以用于清楚这个事件监听,用上AbortController之后就会方便很多

const controller = new AbortController();
const signal = controller.signal; // 是否中止的标识,将他传递给需要被中止的异步任务

window.addEventListener("test", cb,{
  signal
});// addEventListener的第三个参数中支持配置signal

controller.abort() // 调用控制器的abort方法就会清楚掉这个事件监听

1
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 // 这样不会被拦截
1
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)

1
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性能优于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)
}
1
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)
}
1
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 然后执行相应的跳转操作
    },
  ...
}
1
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>

1
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());
1
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状态
    • 图片的加载状态
编辑 (opens new window)
上次更新: 2025/02/10, 10:59:21
最近更新
01
前端检测更新,自动刷新网页
06-09
02
swiper渲染大量数据的优化方案
06-06
03
仿抖音短视频组件实现方案
02-28
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式