likes
comments
collection
share

跳槽准备,总结了点面试题刷刷

作者站长头像
站长
· 阅读数 15

一、HTML与CSS

1.什么是盒模型?说一说你对它的理解!

盒模型Box Model是指在网页设计中用于描述和布局 HTML元素的一种模型。 它将每个HTML元素看作是一个矩形的盒子,由以下几个部分组成

  1. 内容(content):盒子中实际显示的文本、图像或其他内容。
  2. 内边距(padding):内容与边框之间的空白区域,用于控制内容与边框的距离。
  3. 边框(border):围绕内容和内边距的线条,用于定义盒子的边界。
  4. 外边距(margin):盒子与周围元素之间的空白区域,用于控制盒子与其他元素之间的距离。
2.脱离文本流的方式?

1.浮动(Float)

通过将元素的float属性设置为leftright,可以使元素脱离文本流并浮动到其容器的左侧或右侧。浮动元素不再占据正常的文档流位置,而是尽可能向左或向右靠拢,周围的内容会围绕其周围布局。

2.绝对定位(Position: absolute)

通过将元素的position属性设置为absolute,可以使元素脱离文本流并相对于其最近的非静态定位的祖先元素进行定位。绝对定位的元素不会占据正常的文档流位置,它的位置由设置的toprightbottomleft属性决定。

3.固定定位(Position: fixed)

通过将元素的position属性设置为fixed,可以使元素脱离文本流并相对于视口进行定位。固定定位的元素会固定在页面上的某个位置,不随滚动而移动。

4.弹性布局(Flexbox)

通过使用弹性布局的属性和值,如display: flexflex: 1,可以控制元素在容器中的位置和大小。弹性布局可以用于实现多种布局需求,并且元素可以脱离文本流。

3.讲一讲什么是块级格式化上下文(BFC)?

BFC

是一种在CSS中影响元素布局和定位的机制。它是一个独立的渲染区域,其中块级盒子按照一定规则进行布局。

BFC的主要特点和作用包括

  1. 清除浮动:在一个元素形成BFC时,它会包含其内部的浮动元素,从而防止浮动元素溢出到父元素的外部。
  2. 阻止外边距折叠:在BFC中,相邻的块级盒子的外边距不会发生折叠,而是会保留各自的外边距。
  3. 控制元素定位:在BFC中,可以通过一些属性和特性控制元素的定位,例如floatposition: absoluteposition: fixed等。
  4. 自适应容器高度:当一个元素形成BFC时,它的高度会被其内部的浮动元素所撑开,使得父元素能够自适应内容的高度。
  5. 独立的渲染环境:BFC内部的元素布局不受外部的影响,同时也不会影响到外部元素的布局。
4.margin重叠

1.相邻兄弟元素的外边距会重叠,即一个元素的下边距和紧接着的下一个元素的上边距会合并成一个外边距。

2.父元素的上边距和第一个子元素的上边距、以及父元素的下边距和最后一个子元素的下边距也会重叠。

3.空的块级元素(没有边框、padding、内部内容)的上下外边距会重叠。

4.添加内边距或边框:在相邻元素之间添加内边距(padding)或边框(border)可以阻止外边距重叠。

5.使用浮动或定位:将相邻元素浮动(float)或进行定位(positioning)也可以解决外边距重叠的问题。

6.使用 display: flexdisplay: grid:使用 Flexbox 或 Grid 布局可以防止外边距重叠。

5.CSS新增属性
6.元素水平垂直居中的方式有哪些?

1.使用 Flexbox 布局

将父容器设置为 display: flex;

并使用 justify-content: center;align-items: center;

将子元素水平和垂直居中。

.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

2.使用绝对定位和 transform

将子元素设置为 position: absolute;

并使用 top: 50%;left: 50%; 将元素的左上角移动到父容器的中心

然后使用 transform: translate(-50%, -50%); 将元素居中。

.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

3.使用表格布局

将父容器设置为 display: table;

并将子元素设置为 display: table-cell;vertical-align: middle;

以实现水平和垂直居中。

.parent {
  display: table;
}

.child {
  display: table-cell;
  vertical-align: middle;
}

4.使用 Grid 布局

将父容器设置为 display: grid;

并使用 place-items: center;

将子元素水平和垂直居中。

.parent {
  display: grid;
  place-items: center;
}

使用文本对齐和行高

将父容器的高度设置为与子元素相等,并使用 text-align: center;line-height: 指定高度;

将子元素水平和垂直居中。

.parent {
  height: 200px;
  text-align: center;
  line-height: 200px;
}
7.CSS量子化讲一讲?

CSS量子化是一种将CSS样式表按照某种规则进行拆分、压缩和优化的技术。

其目的是减少CSS文件的大小加载时间,从而提高网页的性能和响应速度。

`压缩和合并`
    将多个CSS文件压缩成一个文件,或将多个CSS样式合并到一个文件中
    从而减少HTTP请求和文件大小。
    
`剔除无用样式`
    通过工具或手动方法,剔除无用的样式,如注释、空白、重复和未使用的样式
    从而减少CSS文件的大小。
 
`简化选择器`
CSS选择器简化为最少的标签和类名,从而减少样式的层级和复杂度。
    
`使用缩写和简化语法`
    使用CSS缩写和简化语法,如用“#fff”代替“#ffffff”等,从而减小CSS文件的大小。
    
`使用CSS预处理器`
    使用CSS预处理器,如LessSass等,可以使CSS代码更加简洁、易于维护和扩展。
8.CSS选择器的权重问题

1.内联样式(Inline Styles):

内联样式的权重最高,即在元素的 style 属性中直接定义的样式。例如:<div style="color: red;">Content</div>

2.ID 选择器(ID Selectors):

ID 选择器的权重较高,通过 # 符号加上元素的 id 属性进行选择。例如:#myElement { color: blue; }

3.类选择器、属性选择器和伪类选择器:

类选择器、属性选择器和伪类选择器的权重相同,通过类名、属性名或伪类进行选择。例如:.myClass { font-size: 16px; }[type="text"] { border: 1px solid gray; }:hover { color: green; }

4.元素选择器和伪元素选择器:

元素选择器和伪元素选择器的权重相同,通过元素名或伪元素进行选择。例如:p { font-weight: bold; }::before { content: ">>"; }

5.通配符选择器和关系选择器:

通配符选择器和关系选择器的权重较低。通配符选择器 * 匹配所有元素,关系选择器包括后代选择器、子选择器、相邻兄弟选择器等。例如:* { margin: 0; }div p { color: gray; }div > span { font-size: 18px; }

权重计算规则是根据选择器的不同组成部分进行累加,权重值越高,优先级越高。一般的权重计算规则如下:

  • 内联样式的权重为 1000。
  • 每个 ID 选择器的权重为 100。
  • 每个类选择器、属性选择器和伪类选择器的权重为 10。
  • 每个元素选择器和伪元素选择器的权重为 1。
  • 通配符选择器和关系选择器的权重为 0。
9.px-%-em-rem-vw-vh的区别?
  1. px(像素):像素是屏幕上最小点的单位,1px表示一个像素点,相对于屏幕分辨率来说是固定的。在网页开发中经常使用px来设置字体大小和元素的尺寸,但是在移动端开发中不推荐使用。
  2. %(百分比):百分比是相对于父元素的大小来计算的,例如,如果一个div的宽度设置为50%,那么它的宽度将是其父元素宽度的50%。
  3. em(相对长度单位):em是相对于当前元素的字体大小来计算的,如果一个元素的字体大小是16px,那么1em等于16px。如果嵌套的元素没有设置字体大小,则em会继承父元素的字体大小。em的缺点是在嵌套多层元素时计算比较麻烦。
  4. rem(根元素相对长度单位):rem是相对于根元素的字体大小来计算的,根元素是HTML元素,默认的字体大小是16px。如果一个元素的字体大小是2rem,那么它的大小就是根元素字体大小的2倍(32px)。
  5. vw(视窗宽度单位):vw是相对于视口宽度的长度单位,1vw等于视口宽度的1%。例如,如果视口宽度是1000px,那么1vw等于10px。
  6. vh(视窗高度单位):vh是相对于视口高度的长度单位,1vh等于视口高度的1%。例如,如果视口高度是800px,那么1vh等于8px。
10. css写一个三角形
.triangle {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
}

二、JavaScript与ES6

1.说一下js的数据类型?

基本数据类型:

  • 数字(Number)
  • 字符串(String)
  • 布尔(Boolean)
  • 空值(null)
  • 未定义(undefined)
  • Symbol
  • bigint

引用数据类型:

  • 对象(Object)
  • 数组(Array)
  • 函数(Function)
  • 日期(Date)
  • 正则表达式(RegExp)

基本数据类型是按值传递的,也就是说,对一个基本数据类型进行赋值或传递时,传递的是该数据的副本,而不是该数据本身。

引用数据类型则是按引用传递的,也就是说,对一个引用数据类型进行赋值或传递时,传递的是该数据在内存中的地址,而不是该数据本身。

这里基本数据类型中的,null和undefined虽然都是基本数据类型,但它们各自有自己的含义。null表示一个空对象引用,而undefined表示未定义的值。

2.说说你对闭包的理解?应用场景?

闭包就是能够读取其它函数内部变量的函数

1.使用方法

在一个函数内部创建另一个函数

2.作用

 1.读取其他函数的变量值,让这些变量始终保存在内存中
 2.这是因为闭包的执行依赖外部函数中的变量,只有闭包执行完,
   才会释放变量所占的内存

3.缺点

  • 1.意外的全局变量
  • 2.被遗忘的定时器和回调函数
  • 3.闭包
  • 4.没有被清理的DOM元素引用
1.会引起内存泄漏(引用无法被销毁,一直存在)
   - 1.意外的全局变量
   - 2.被遗忘的定时器和回调函数
   - 3.闭包
   - 4.没有被清理的DOM元素引用
      
2.由于闭包会使得函数中的变量都被保存在内存中,内存消耗大,所以不能滥用闭包
  否则会造成网页的性能问题,在IE中可能会造成内存泄露。
      
3.闭包会在父函数外部,改变父函数内部变量的值。
  所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法
  Public Method),
把内部变量当作它的私有属性(private value),不要随便改变父函数内部变量的值。

4.解决方案

1.在退出函数之前,将不使用的局部变量全部删掉  
2.闭包是能够读取其他函数内部的变量的函数。
3.减少不必要的全局变量,使用严格模式避免意外创建全局变量
4.组织好代码逻辑,避免死循环造成浏览器卡顿,奔溃的问题

5.特点

 1.让外部访问函数内部变量成为可能;
 2.局部变量会常驻在内存中;
 3.可以避免使用全局变量,防止全局变量污染;
 4.会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
3.map和forEach的区别?
  1. 返回值:map 方法会返回一个新的数组,该数组包含对原数组的每个元素应用回调函数后的结果。而 forEach 方法没有返回值,仅用于对数组进行迭代操作。

  2. 使用目的:map 方法常用于对数组进行转换或映射操作,即通过对每个元素应用回调函数来创建一个新的数组。而 forEach 方法主要用于遍历数组并执行回调函数,通常用于在迭代过程中对数组元素进行某种操作或执行副作用。

  3. 回调函数的参数:map 方法的回调函数接受三个参数:当前元素的值、当前元素的索引和原数组本身。而 forEach 方法的回调函数也接受三个参数:当前元素的值、当前元素的索引和原数组本身。回调函数的参数顺序和含义在两种方法中都是相同的。

  4. 修改原数组:map 方法不会修改原数组,而是返回一个新的数组。而 forEach 方法也不会修改原数组,仅用于遍历数组。

map 方法更适用于对数组进行转换或映射操作,并返回一个新数组,而 forEach 方法更适用于在遍历过程中执行一些操作或副作用,而不需要返回一个新的数组。

4.some与every的区别?
  1. 返回值:some 方法在数组中至少有一个元素满足条件时返回 true,否则返回 false。而 every 方法在数组中的所有元素都满足条件时返回 true,否则返回 false

  2. 条件判断:some 方法会对数组中的每个元素应用条件判断函数,只要有一个元素满足条件,则 some 方法立即返回 true。而 every 方法会对数组中的每个元素应用条件判断函数,只有当所有元素都满足条件时,every 方法才返回 true

  3. 提前终止:some 方法在找到满足条件的元素后会立即停止遍历,不再继续判断剩余的元素。而 every 方法在遇到不满足条件的元素时会立即停止遍历,不再继续判断剩余的元素。

  4. 使用目的:some 方法常用于判断数组中是否存在满足某个条件的元素。而 every 方法常用于判断数组中的所有元素是否都满足某个条件。

5.什么是原型及原型链?

每个对象都有一个原型(prototype)对象。原型对象是一个普通的对象,它通过JavaScript的属性继承机制来提供属性和方法给其他对象使用。如果一个对象需要访问某个属性或方法,但是它本身并没有该属性或方法,那么它就会去它的原型对象中查找。如果仍然没有找到,那么它就会去原型对象的原型对象中查找,以此类推,直到找到最顶层的原型对象或找到该属性或方法为止,这就是原型链的概念。

原型链的形成是因为每个对象都会有一个__proto__属性指向它的原型对象,而原型对象也有__proto__属性,指向它的原型对象,这样就构成了一条链状结构,从而形成了原型链。

例如,如果一个对象obj需要访问一个属性x,但是它本身并没有x这个属性,那么它会去它的原型对象中查找,如果原型对象也没有x属性,那么它就会去原型对象的原型对象中查找,直到找到x属性或者查找到顶层的Object对象,如果还没有找到,就会返回undefined。

6.let,const,var的区别?
1. letconst 都是块级作用域。
2. 以{}代码块作为作用域范围 只能在代码块里面使用不存在变量提升
3. 只能先声明再使用,否则会报错。在代码块内,在声明变量之前,

该变量 都是不可用的。这个问题在js语法上,称为暂时性死区(TDZ)在同一个代码块内,不允许重复声明

const 声明的是一个只读常量,在声明时就需要赋值。

如果 const 的是一个对象,对象所包含的值是可以被修改的。简单点说就是对象所指向的地址不能改变,而变量成员 是可以修改的

7.const定义的变量能修改吗?

js中使用 const 关键字声明的变量是常量,意味着其值不能被重新赋值。一旦使用 const 声明一个变量并初始化,就无法再修改该变量的值。

const x = 10;
x = 20; // 这里会抛出错误,因为无法重新赋值给 x

使用 const 声明的变量具有块级作用域,并且在声明时必须进行初始化赋值。一旦变量被赋值,就无法再改变它的值。

需要注意的是,const 只保证变量指向的内存地址不会发生改变,但并不意味着变量的值是不可变的。如果 const 变量引用的是一个对象或数组,那么该对象或数组的属性或元素仍然是可变的。

例如:

const person = {
  name: 'John',
  age: 30
};

person.age = 40; // 这是有效的,因为修改的是 person 对象的属性

console.log(person); // 输出: { name: 'John', age: 40 }

在上述示例中,const 变量 person 引用了一个对象,虽然不能重新赋值给 person,但是可以修改对象的属性值。

综上所述,const 关键字确保了变量的指向不可变,但并不保证变量值的不可变性。如果需要完全不可变的值,可以使用其他方式,如使用 Object.freeze() 方法来冻结对象,或使用不可变数据结构库(如 Immutable.js)来管理数据。

8.map循环会修改原数组吗?

在js中,Array.prototype.map() 方法不会修改原始数组,它会返回一个新的数组。map() 方法通过对原始数组的每个元素应用提供的回调函数来创建新数组,而不会改变原始数组的内容。

map() 方法的回调函数接收三个参数:当前元素的值、当前元素的索引和被遍历的数组。回调函数执行后的返回值将作为新数组的元素。

9.展开运算符是深拷贝还是浅拷贝?

当使用展开运算符复制数组或对象时,它会创建一个新的数组或对象,并将原始数组或对象的元素或属性复制到新的数组或对象中。对于数组来说,它会复制数组的元素值,对于对象来说,它会复制对象的属性和属性值。

然而,对于复杂数据类型(如嵌套数组或对象),展开运算符只会复制它们的引用,而不是递归复制它们的内容。这意味着如果原始数组或对象中存在嵌套的数组或对象,那么复制后的数组或对象中的嵌套部分仍然会与原始数组或对象共享相同的引用。

这就是为什么展开运算符被认为是执行浅拷贝的原因。它只复制了最外层的数组或对象的内容,而对于嵌套的数据结构,仍然共享同一份数据。

如果需要进行深拷贝(即递归复制所有嵌套的数组和对象),可以使用其他方法,例如使用递归函数、Object.assign()JSON.parse(JSON.stringify())等。这些方法可以复制整个数据结构,包括嵌套的数组和对象,从而实现深拷贝操作。

10.宏观微观任务分别是?

宏观任务(MacroTask)

  1. setTimeout 和 setInterval:通过调用 setTimeoutsetInterval 函数创建的任务会在指定的延迟时间后执行。

  2. I/O 操作:包括读取文件、发送网络请求等异步的 I/O 操作通常会作为宏观任务被添加到任务队列中。

  3. 页面渲染:浏览器中的页面渲染过程也是一个宏观任务,例如重绘页面、处理 DOM 变更等。

  4. 用户交互事件:鼠标点击、键盘输入等用户交互事件也会被作为宏观任务添加到任务队列中。

微观任务(MicroTask)

  1. Promise 回调:通过 Promise 对象的 then 方法添加的回调函数会作为微观任务在当前任务执行完毕后立即执行。

  2. async/await:使用 async/await 语法进行异步操作时,await 关键字后的代码会作为微观任务在当前任务执行完毕后立即执行。

  3. MutationObserver:通过 MutationObserver 监听 DOM 变化时,回调函数会作为微观任务在当前任务执行完毕后立即执行。

11.数组扁平化有哪些方式?

使用递归

  • 编写一个递归函数,遍历数组的每个元素。
  • 如果元素是数组,则对该数组进行递归调用,直到遍历到最内层的元素。
  • 将每个元素添加到结果数组中。
  • 返回最终的扁平化数组。
function flattenArray(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flattenArray(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

使用 reduce()方法

  • 使用 reduce() 方法遍历数组。
  • 对于每个元素,判断是否为数组。
  • 如果是数组,则对该数组进行递归调用并将结果连接到累加器数组中。
  • 如果不是数组,则直接将元素添加到累加器数组中。
  • 返回最终的扁平化数组。
1.累加器(accumulator):该参数表示累积的结果值,在每次迭代时更新。
  初始值为 `reduce` 方法的第二个参数(可选),如果没有提供初始值,
  则使用数组的第一个元素作为初始值。在每次迭代中,回调函数的返回值将被赋值给累加器。
  
2.当前值(current value):该参数表示当前被处理的元素值。
3.当前索引(current index):该参数表示当前元素在数组中的索引。
4.源数组(array):该参数表示正在被迭代的原始数组。
function flattenArray(arr) {
  return arr.reduce((acc, curr) => {
    if (Array.isArray(curr)) {
      return acc.concat(flattenArray(curr));
    } else {
      return acc.concat(curr);
    }
  }, []);
}
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

使用 flat() 方法(ES2019)

  • 使用 flat() 方法直接将多维数组转换为一维数组。
  • 可以传递一个可选的参数指定扁平化的层数。
  • 返回扁平化后的数组。
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = nestedArray.flat(Infinity);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
12.普通函数与箭头函数的区别?

语法

1.普通函数使用关键字 `function` 来定义,
  后面跟着函数名和一对圆括号,再接着是函数体的代码块。
  
2.箭头函数使用箭头符号 `=>` 来定义,
  `符号左侧是函数参数`
     一个参数可以省略括号,反之不能省略
  `右侧是函数体` 
     可以是一个表达式,或者一条执行语句可以省略大括号
     代码块则是需要

作用域

1.普通函数有自己的作用域
  函数体内部的 `this` 关键字指向调用函数的对象(或全局对象,如果没有指定对象)
  
2.箭头函数没有自己的作用域,它继承外层函数的作用域,
  也就是,箭头函数内部的 `this` 关键字与外层函数的 `this` 关键字保持一致

this 的绑定

1.普通函数的 `this` 值在运行时确定,它的值取决于函数的调用方式。
2.如果函数被作为对象的方法调用,`this` 将绑定到该对象;
3.如果函数是全局(ctx)中的普通函数,`this` 将绑定到全局对象`window`
4.箭头函数没有自己的 `this` 绑定,
  它会捕获并继承外层作用域的 `this` 值。这意味着在箭头函数内部,
  `this` 的值是定义箭头函数的上下文中的 `this`

其他注意事项

1.箭头函数不能用作构造函数,不能使用 `new` 关键字实例化。
2.箭头函数没有 `arguments` 对象,但可以使用剩余参数 `...args` 来获取函数的参数。
3.箭头函数没有 `prototype` 属性,所以无法被继承。
13.改变this指向的方法和区别?
  1. call()方法

call()方法可以在函数调用时,将函数内部的this指向指定的对象

function.call(thisArg, arg1, arg2, ...)
  1. apply()方法

apply()方法与call()方法类似,也可以指定函数内部的this指向

thisArg表示需要绑定的this对象,argsArray为传递给函数的参数数组

function.apply(thisArg, [argsArray])
  1. 箭头函数
14.Map对象和普通对象的区别?
  1. 键的类型:在普通对象中,键只能是字符串或符号(Symbol)类型,而在 Map 对象中,键可以是任意数据类型,包括对象、函数、原始值等。在 Map 中,键的比较是基于值的严格相等性(===),而在普通对象中,键是字符串类型,比较是基于字符串的值。

  2. 插入顺序:在普通对象中,键值对的插入顺序不被保证。当遍历普通对象时,无法保证键值对的顺序与插入顺序一致。而在 Map 对象中,键值对的插入顺序被保留,当遍历 Map 时,键值对的顺序与插入顺序一致。

  3. 大小获取:普通对象没有内置的方法来获取对象的大小(即键值对的数量),需要手动编写逻辑来计算对象的大小。而 Map 对象提供了内置的 size 属性,可以直接获取 Map 的大小。

  4. 遍历方式:普通对象可以使用 for...in 循环来遍历对象的属性,但这也会遍历对象原型链上的属性。而 Map 对象提供了内置的 forEachfor...ofentries 等方法来遍历 Map 的键值对,且只遍历 Map 自身的键值对,不包括原型链上的属性。

  5. 对象属性和方法:普通对象是 JavaScript 中的基本数据结构之一,它具有一些内置的属性和方法,如 toStringhasOwnProperty 等。而 Map 对象是 JavaScript 的标准内置对象,它有自己的属性和方法,如 sizesetgetdelete 等。

  6. 适用场景:普通对象适用于存储简单的键值对,可以用于一般的数据结构和字典。而 Map 对象适用于需要灵活的键类型、保留插入顺序、快速查找和迭代的场景。Map 对象通常比普通对象在处理大量数据时具有更好的性能。

15.协商缓存与强制缓存的区别?

强制缓存(Force Cache)

强制缓存是通过在服务器端设置缓存响应的过期时间来控制的。当客户端发起请求时,如果缓存仍然有效(即未过期),服务器会直接返回缓存的响应,而不会再次向服务器发起请求。常见的强制缓存的设置方式包括 Expires 头和 Cache-Control 头中的 max-age 指令。

协商缓存(Conditional Cache)

协商缓存是通过客户端与服务器之间的交互来确定是否使用缓存的。当客户端发起请求时,服务器会检查请求中的一些条件标头,如 If-Modified-SinceIf-None-Match,与服务器上资源的信息进行比较。如果资源没有发生变化,则服务器会返回一个 304 状态码(Not Modified),告知客户端使用缓存。如果资源发生了变化,则服务器会返回新的资源内容和 200 状态码。常见的协商缓存的设置方式包括 Last-Modified 头和 ETag 头。

主要区别总结如下

  • 强制缓存是基于时间的,由服务器设置响应的过期时间。客户端在缓存有效期内不需要向服务器发起请求,直接使用缓存的响应。

  • 协商缓存是基于条件的,通过客户端与服务器之间的交互来确定是否使用缓存。客户端发送请求时,服务器会根据资源的变化情况进行比较,并返回相应的状态码和响应。

  • 在浏览器中,当同时存在强制缓存和协商缓存时,强制缓存优先级更高,即如果缓存仍然有效,浏览器将直接使用缓存,而不会发起协商缓存的请求。

  • 通过设置合适的缓存策略可以最大程度地提高网页的加载性能,减少对服务器的请求,提升用户体验。

16.setTimeout的时效性为什么不准?

setTimeout 函数的时效性(timing accuracy)是指它设置的定时器的准确性和精确性。在理论上,setTimeout 的延迟时间应该是精确的,即在指定的延迟时间过后,回调函数应该立即执行。然而,在实际应用中,setTimeout 的时效性可能不是完全准确的,可能存在一定的延迟。

这是由于 JavaScript 是单线程执行的语言,且运行在浏览器环境中。在 JavaScript 中,存在执行栈(执行主线程)和任务队列(Event Loop)。当 setTimeout 函数被调用时,它会在指定的延迟时间后将回调函数添加到任务队列中,等待执行。然而,由于 JavaScript 的单线程特性,当执行栈中的任务没有执行完毕时,任务队列中的任务无法立即得到执行,需要等待执行栈为空时才能执行。

因此,setTimeout 的时效性可能受到以下几个因素的影响:

  1. 执行栈中其他任务的影响:如果在指定的延迟时间内,执行栈中还有其他任务在执行,那么 setTimeout 的回调函数可能会被推迟执行,直到执行栈为空。
  2. 浏览器或设备负载:如果浏览器或设备负载较高,处理其他任务的时间可能会延长,从而影响 setTimeout 的准确性。
  3. 最小延迟时间限制:根据 HTML5 规范,最小的 setTimeout 延迟时间是 4 毫秒。这意味着设置的延迟时间小于 4 毫秒时,实际的延迟时间会被调整为最小延迟时间。

需要注意的是,setTimeout 并不是实时的定时器,它不能保证回调函数在指定的延迟时间后立即执行。如果需要更精确的定时器,可以使用 requestAnimationFrameWeb Workers 来实现。

17.事件循环(Event Loop)和宏伟任务?

事件循环(Event Loop)和宏伟任务

18.js是单线程,怎么保证多个任务同时执行?
  1. 使用 Web Workers:Web Workers 是一种在后台运行的 JavaScript 线程,可以在单独的线程中执行任务,而不会影响主线程。这样可以允许在后台同时执行多个任务,从而实现并发性。Web Workers 之间可以通过消息传递进行通信。
  2. 使用异步编程:在 JavaScript 中,通过使用异步编程方式(例如回调函数、Promise、async/await),可以在主线程上执行一些耗时的操作,同时不会阻塞其他任务的执行。这样可以实现并发性,使得在执行耗时操作的同时,其他任务可以继续执行。
  3. 使用事件循环:JavaScript 的事件循环机制允许任务以异步的方式在主线程上执行。通过将任务添加到事件队列中,主线程可以继续处理其他任务。当主线程空闲时,它会从事件队列中取出任务并执行。
  4. 使用定时器:通过使用定时器函数(如setTimeoutsetInterval),可以在一定的时间间隔后执行任务。这样可以实现一些定时执行的操作,而不会阻塞其他任务的执行。

需要注意的是,尽管上述方法可以实现在 JavaScript 中多个任务同时执行的效果,但在同一个时刻,JavaScript 的执行仍然是单线程的。所以在设计并发操作时,要避免长时间阻塞主线程,以免导致页面失去响应。合理使用异步编程和 Web Workers 等技术可以提高 JavaScript 的并发性和性能。

19.谈谈你对Promise的理解?

Promise 主要是用来解决异步产生层层嵌套出现毁掉地狱的问题

它有三种状态

pending(进行中)、fulfilled(已成功)和rejected(已失败)。

一旦 Promise 的状态发生变化,就会触发相应的处理逻辑。

Promise提供了以下几个主要的特点优势

  1. 异步操作的链式调用:通过使用 Promise 的 then 方法,可以将多个异步操作按顺序串联起来,形成一个链式调用。这使得异步操作的逻辑更加清晰和可读,避免了多层嵌套的回调函数。
  2. 错误处理机制:Promise 提供了 catch 方法用于捕获和处理异步操作中的错误。在 Promise 链式调用中,如果任何一个操作出现错误,就会跳过后续的操作并进入到 catch 方法中,方便统一处理错误。
  3. 状态的不可逆性:Promise 的状态一旦改变,就无法再次改变。这意味着一旦 Promise 变为 fulfilled 或 rejected 状态,它的结果或错误就会被固定下来,不会再改变。这种特性使得 Promise 在处理一次性的异步操作时更加可靠。
  4. 解决回调地狱问题:通过使用 Promise,可以将异步操作的逻辑串联起来,并在每个异步操作完成后继续处理下一个操作。这样可以避免回调函数的层层嵌套,提高代码的可读性和可维护性。
  5. 支持并发和并行操作:Promise 提供了 Promise.allPromise.race 方法,用于处理多个异步操作的并发和并行。Promise.all 方法会等待所有 Promise 都变为 fulfilled 状态后返回结果,而 Promise.race 方法会在任何一个 Promise 变为 fulfilled 或 rejected 状态后立即返回结果。
20.判断js数据类型的方法有哪些?

1.typeof操作符

typeof 用于确定值的基本类型,返回一个字符串,表示值的类型。

typeof 返回值 返回字符串

1. `"string"`
2. `"number"`
3. `"boolean"`
4. `"object"`
5. `"function"`
6. `"undefined"`
7. `"symbol"`

2.instanceof操作符

1. `instanceof` 操作符用于检查对象是否是某个类的实例。
2. `value instanceof Array` 
3. 用于检查 `value` 是否是一个数组的实例。

3.Array.isArray()方法

Array.isArray(value)` 方法用于检查一个值是否为数组。
它返回一个布尔值,如果值是数组,则返回 `true`,否则返回 `false`。

4.Object.prototype.toString() 方法:

`Object.prototype.toString.call(value)`方法可以返回一个表示值的类型的字符串。

`Object.prototype.toString.call([])` 返回 `"[object Array]"`

`Object.prototype.toString.call(null)` 返回 `"[object Null]"`

5.typeof null === "object"的特殊判断

由于js历史遗留bug原因,`typeof null` 返回 `"object"`

所以,可以使用 `value === null` 来检查一个值是否为 `null`

6.isNaN() 函数

`isNaN(value)` 函数用于检查一个值是否为 NaN(非数字)。
返回一个布尔值,如果值为 NaN,则返回 `true`,否则返回 `false`
21.怎么往数组中间插入一个元素

如果要在数组中间插入一个元素,可以使用数组的splice()方法。splice()方法可以在指定的位置插入一个或多个元素,并返回被删除的元素。

要在数组中间插入一个元素,需要确定要插入的位置。假设数组长度为n,那么可以在第n/2的位置插入元素,这样就可以

let arr = [1, 2, 3, 4, 5]; 
let middleIndex = Math.floor(arr.length / 2); // 取数组长度的一半,向下取整 let newItem = 'a'; 
arr.splice(middleIndex, 0, newItem); 
console.log(arr); // [1, 2, 'a', 3, 4, 5]
22.什么是函数柯里化?
  1. 这个curry函数接受一个函数fn作为参数,并返回一个新的函数curried。
  2. 当调用curried函数时,它会判断传入的参数数量是否达到了原始函数fn所需的参数数量。
  3. 如果达到了,就直接调用原始函数fn并返回结果。
  4. 如果还没有达到,就返回一个新的函数,继续接收更多的参数,并将之前的参数与新参数合并,
  5. 直到达到原始函数fn所需的参数数量为止。
23.什么是浏览器同源策略?

浏览器同源策略(Same-Origin Policy)是一种安全机制,用于限制来自不同源(域名、协议或端口)的网页间的交互。同源策略旨在防止恶意网站利用跨域请求获取用户的敏感信息或执行恶意操作。

根据同源策略,以下内容受到限制:

  1. DOM 访问限制:JavaScript 在一个源中的网页只能访问同一源中的 DOM 对象。这意味着无法通过 JavaScript 跨域访问其他网页的 DOM,如获取或修改其内容。
  2. Cookie、LocalStorage 和 IndexedDB 限制:浏览器只会发送同源的 Cookie,因此 JavaScript 也只能访问同一源中的 Cookie。类似地,LocalStorage 和 IndexedDB 数据也只能在同一源中共享。
  3. XMLHttpRequest 和 Fetch 请求限制:通过 XMLHttpRequest 或 Fetch 发起的网络请求也受到同源策略的限制。这意味着脚本只能发起与当前页面同源的请求,无法跨域访问其他网站的数据。
  4. 安全限制:某些安全限制仅适用于跨域场景,例如,JavaScript 无法访问跨域的 iframes 的内容,无法执行跨域脚本。
24.实现跨域的常见方式?

实现跨域请求(Cross-Origin Request)是在浏览器环境下从一个域向另一个域发送 HTTP 请求的过程。由于浏览器的同源策略限制,直接跨域请求是不被允许的。以下是一些常见的跨域解决方式:

  1. JSONP(JSON with Padding):JSONP 是一种通过动态创建 <script> 标签来实现跨域请求的方式。在服务端返回的响应数据包裹在一个函数调用中,并在前端定义一个与响应函数同名的回调函数来接收数据。由于 <script> 标签的跨域特性,因此可以绕过同源策略的限制。
  2. CORS(Cross-Origin Resource Sharing):CORS 是一种由服务端设置的机制,用于允许跨域请求。服务端通过设置响应头中的 Access-Control-Allow-Origin 字段来指定允许访问的源。可以通过配置该字段来控制允许跨域请求的源,可以是单个域名、多个域名或使用通配符表示允许所有源。
  3. 代理服务器:通过在同源的服务器上创建一个代理服务器,将跨域请求转发到目标服务器,并将响应返回给前端。前端发送请求时,实际上是发送到同源的代理服务器上,然后由代理服务器转发请求到目标服务器上获取响应。这样可以绕过浏览器的同源策略限制。
  4. WebSocket:WebSocket 是一种双向通信协议,它在建立连接时使用 HTTP 协议,但后续通信不受同源策略的限制。通过使用 WebSocket,可以在不同源之间建立持久的、双向通信的连接。
25.如何理解cookie?
1.本身用于浏览器和server通讯(本身价值在于 本地和服务器端进行通讯) 
2.被借用到本地存储来 
3.前端可用document.cookie = '' 来修改() 同一个key会覆盖,不同key会追加 
4.(后端也是可以修改的) 缺点: cookie 最大存储4kb 

http请求时需要发送到服务端,增加请求数据量 document.cookie = "..." 不太方便操作
26.localStorage和sessionStorage区别?
1.HTML5 专门为存储而设计的,最大可存储5M 
2.API也是比较简单好记,setItem,getItem 
3.不会随着http请求被发送出去 
4.localStorage数据会永久存储,除非代码或手动删除 
5.sessionStorage 数据只存在于当前会话,浏览器关闭则会清空

27.为什么0.1+0.2 !== 0.3?

浮点数运算可能会导致精度问题,这也是为什么在某些情况下,0.1 + 0.2 的结果不等于 0.3 的原因。这是由于浮点数在计算机中的表示方式导致的。

0.1 和 0.2 都是浮点数,它们的二进制表示并不是精确的 0.1 和 0.2。当进行 0.1 + 0.2 的计算时,实际上是对近似的二进制表示进行运算,导致了一个微小的舍入误差。这个误差在输出结果时可能变得明显,因此得到的结果并不等于精确的 0.3

可以使用其他方式来处理浮点数,例如将它们转换为整数进行精确计算,或者使用特定的库或函数来处理浮点数运算,例如 Decimal.js、big.js 等。这些库提供了更精确的浮点数运算方法。

28. 深度优先遍历一个DOM树
function dfsTraversal(node, callback) {
  // 访问当前节点
  callback(node);

  // 遍历子节点
  const children = node.children;
  for (let i = 0; i < children.length; i++) {
    dfsTraversal(children[i], callback);
  }
}

// 示例用法
const root = document.documentElement; // 获取DOM树的根节点

function logNode(node) {
  console.log(node.tagName); // 打印节点的标签名
}

dfsTraversal(root, logNode); // 开始深度优先遍历
29.广度优先遍历一个DOM树
输入:
const arr = [
    { id: 1, name: '部门A', parentId: 0 },
    { id: 2, name: '部门B', parentId: 1 },
    { id: 3, name: '部门C', parentId: 1 },
    { id: 4, name: '部门D', parentId: 2 },
    { id: 5, name: '部门E', parentId: 2 },
    { id: 6, name: '部门F', parentId: 3 }
]
输出:
[
    {
        id: 1,
        name: '部门A',
        children: [
          {
              id: 2,
              name: '部门B',
              children: [
                  {
                      id: 4,
                      name: '部门D',
                      children: []
                  },
                  {
                      id: 5,
                      name: '部门E',
                      children: []
                  }
              ]
          },
          {
              id: 3,
              name: '部门C',
              children: [
                	{
                      id: 6,
                      name: '部门F',
                      children
                  }
              ]
          }
        ]
    }
]
// 解题思路: 遍历数组方式  效果较慢
function convert(arr) {
    const map = {};
    const tree = [];

   // 将每个节点以 id 为键存储到 map 对象中
    arr.forEach(item=> {
        map[item.id] = { ...item, children: [] };
    })

   // 遍历数组,将每个节点添加到其父节点的 children 数组中
    arr.forEach(item=> {
        if (item.parentId !== 0) {
            map[item.parentId].children.push(map[item.id])
        } else {
            tree.push(map[item.id])
        }
    })
    return tree
}

// 解题思路: 使用一个Map来维护关系,便于查找  广度优先遍历
function convert (arr) {
    const nodeParent = new Map();
    const result = [];
    
    // 构建映射关系
    arr.forEach(node => {
        node.children = [];
        nodeParent.set(node.id, node)
    })
   

    // 构建树形结构
    arr.forEach(node => {
        const parentId = node.parentId;
        const parentNode = nodeParent.get(parentId);
        if (parentNode) {
            parentNode.children.push(node);
        } else {
            result.push(node);
        }
    })
    return result
}
30.深度优先遍历可以不用递归吗?

可以的

深度优先遍历也可以通过使用栈(Stack)来实现,从而避免使用递归。这种方式称为迭代法实现深度优先遍历。

在迭代法中,我们使用一个栈数据结构来保存待访问的节点。开始时,将根节点入栈,然后从栈中取出节点进行访问,并将该节点的子节点按照逆序(从右到左)入栈,重复此过程直至栈为空

示例代码:

function dfsTraversal(root, callback) {
  const stack = [root];
  while (stack.length > 0) {
    const node = stack.pop();
    callback(node); // 访问当前节点

    // 将当前节点的子节点按逆序入栈
    const children = node.children;
    for (let i = children.length - 1; i >= 0; i--) {
      stack.push(children[i]);
    }
  }
}

示例用法
function logTagName(node) {
  console.log(node.tagName);
}
const root = document.documentElement; // 获取DOM树的根节点
dfsTraversal(root, logTagName); // 开始深度优先遍历
31.模板字符串原理

在模板字符串中,可以通过 ${} 语法在字符串中插入表达式或变量,这样可以在字符串中动态地插入值。具体原理如下:

  1. 模板字符串会保留所有的换行和空格,因此可以方便地创建多行字符串,而不需要使用转义字符或拼接字符串。
  2. 在模板字符串中,${} 语法用于插入表达式或变量的值。当 JavaScript 执行模板字符串时,会先处理 ${} 中的表达式,然后将其结果插入到模板字符串中。
  3. ${} 中可以是任意有效的 JavaScript 表达式,可以包含变量、函数调用、算术运算等。
  4. 模板字符串中的插入部分会被解析为字符串,即使插入的是非字符串类型的值。
  5. 模板字符串还可以与标签函数一起使用,通过在模板字符串前面加上标签函数来自定义字符串的处理方式。标签函数可以对插入的值进行处理,从而实现自定义字符串拼接的功能

使用实例

const name = 'John';
const age = 30;
const greeting = `Hello, my name is ${name} and I'm ${age} years old.`;
console.log(greeting);
// 输出: "Hello, my name is John and I'm 30 years old."
32.如何检测JS内存泄露?

监测内存占用: 使用开发者工具中的内存分析器,例如 Chrome 浏览器的 Performance 面板或 Firefox 浏览器的 Memory 面板

33.JS内存泄露的场景有哪些?
  1. 未清理的定时器或回调函数:如果定时器或回调函数没有正确清理或取消,它们会持续引用相关的对象,导致这些对象无法被垃圾回收。特别是在长时间运行的应用程序中,累积的定时器和回调函数可能会导致内存泄漏。
  2. 闭包:当函数内部定义了其他函数,并且内部函数引用了外部函数的变量时,形成了闭包。如果闭包函数未被及时释放,那么外部函数的变量也无法被垃圾回收。在循环中创建闭包时尤其需要注意,因为每次循环都会创建一个新的闭包函数。
  3. DOM 引用:当页面中的 DOM 元素被 JavaScript 引用,而这些引用没有被正确清理时,可能导致内存泄漏。特别是在使用全局变量或缓存对象来存储 DOM 元素引用时,需要注意在不再需要时及时清理这些引用。
  4. 大量数据的缓存:如果应用程序中存在大量数据的缓存,而这些缓存没有及时清理或过期,会导致内存占用过高。在设计缓存机制时,需要考虑合理的过期策略和清理机制,以防止内存泄漏。
  5. 循环引用:当两个或多个对象相互引用,形成了循环引用时,即使这些对象不再被使用,它们也无法被垃圾回收。这种情况常见于对象间的相互引用、事件监听器未正确移除等场景。
  6. 跨页面的引用:在多页面应用中,如果一个页面持有对另一个页面的引用,而这个引用没有被及时释放,就会导致跨页面的内存泄漏。
34.什么时候不能使用箭头函数?
  1. 对象的方法:箭头函数没有自己的 this 绑定,它会继承外层作用域的 this 值。因此,如果需要在函数内部引用当前对象的属性或方法,应该使用普通函数而不是箭头函数。
  2. 构造函数:箭头函数不能用作构造函数来创建对象,它没有自己的 prototype 属性,也不能使用 new 关键字实例化。
  3. 原型方法:如果希望在对象的原型上定义方法,以便实例对象可以访问该方法,那么应该使用普通函数,因为箭头函数没有自己的 prototype,无法作为原型方法使用。
  4. 方法的动态绑定:当需要根据运行时的情况动态绑定函数的 this 值时,箭头函数无法实现这一点,因为它们没有自己的 this 绑定。
35.JS中for-in和for-of的区别?

for-in 循环:

  • 用于遍历对象的可枚举属性。
  • 循环变量是对象属性的键(key)。
  • 循环的顺序是不确定的,因为对象的属性没有固定的顺序。
  • 可以遍历对象自身的属性以及继承的属性。
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
  console.log(key + ': ' + obj[key]);
}
// 输出结果:
// a: 1
// b: 2
// c: 3

for-of 循环:

  • 用于遍历可迭代对象的元素,例如数组、字符串、Map、Set 等。
  • 循环变量是对象的值(value)。
  • 循环的顺序是按照对象迭代器返回的顺序进行的。
  • 只能遍历对象自身的元素,不能遍历继承的属性或方法。
const arr = [1, 2, 3];
for (let value of arr) {
  console.log(value);
}
// 输出结果:
// 1
// 2
// 3
36.for-await-of有什么作用?

在 JavaScript 中,通常使用 for...of 循环来遍历可迭代对象,但是在处理异步操作时,使用传统的 for...of 循环可能会导致问题。因为 for...of 循环在处理异步可迭代对象时,无法正确处理异步操作的等待。

for-await-of 循环通过在遍历异步可迭代对象时等待每个异步操作完成,确保在遍历到下一个元素之前等待上一个异步操作的完成。这样,它能够正确地处理异步操作,并按顺序依次处理每个异步操作的结果。

for-await-of循环的语法和for...of循环非常相似,只是在 of后面加上了await关键字:

async function asyncIteratorExample() {
  const asyncIterable = createAsyncIterable(); // 创建异步可迭代对象
  for await (const element of asyncIterable) {
    // 处理异步操作的元素
    console.log(element);
  }
}
// 示例函数,返回一个异步可迭代对象
async function* createAsyncIterable() {
  yield 1;
  yield 2;
  yield 3;
}
asyncIteratorExample(); // 调用异步迭代函数
37.offsetHeight-scrollHeight-clientHeight有什么区别?
  1. offsetHeightoffsetHeight 属性返回一个元素的完整高度,包括元素的高度、内边距和边框。它包括元素的可见内容区域、内边距和边框的高度。如果有滚动条,滚动条的尺寸也会计入 offsetHeight

  2. scrollHeightscrollHeight 属性返回一个元素的内容区域的高度,包括被隐藏的部分。它包括元素的实际内容的高度,无论内容是否溢出并隐藏在滚动区域中。如果元素的内容没有溢出,那么 scrollHeight 的值等于元素的 clientHeight

  3. clientHeightclientHeight 属性返回一个元素的可见内容区域的高度,不包括元素的边框、外边距或滚动条。它表示了元素在垂直方向上的视口高度,即元素实际可见的部分的高度。

38.防抖节流有什么区别?分别用于什么场景?

防抖(Debounce)

场景:适用于需要在连续触发事件后等待一段时间后才执行的情况,常见的场景包括输入框搜索、窗口大小调整等。

原理:当事件触发后,设置一个定时器,在定时器设定的时间内没有再次触发事件时,执行相应的操作。如果在定时器设定的时间内再次触发事件,会重新计时。

效果:在连续触发事件时,只会执行最后一次触发的操作,前面的操作会被忽略。

function debounce(func, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}
// 在连续输入过程中,只在最后一次输入后的 300 毫秒执行搜索操作
const searchInput = document.querySelector('#search-input');
searchInput.addEventListener('input', debounce(search, 300));

节流(Throttle)

场景:适用于需要限制函数在一定时间间隔内执行的场景,常见的场景包括滚动事件、鼠标移动事件等。

原理:在指定的时间间隔内,无论事件触发频率如何,都只会执行一次操作。如果在时间间隔内触发事件,操作会被忽略。

效果:在连续触发事件时,按照设定的时间间隔执行操作,而丢弃间隔内的其他触发事件。

function throttle(func, delay) {
  let timerId;
  let lastExecutedTime = 0;
  return function (...args) {
    const currentTime = Date.now();
    if (currentTime - lastExecutedTime >= delay) {
      func.apply(this, args);
      lastExecutedTime = currentTime;
    }
  };
}
// 在滚动过程中,每 300 毫秒执行一次滚动处理函数
window.addEventListener('scroll', throttle(handleScroll, 300));

区别

  • 防抖和节流都是用于控制函数执行频率的方法,但关注点不同。
  • 防抖侧重于限制函数的执行次数,将连续触发的事件合并为最后一次触发的操作。适用于只关心最后一次触发的结果的场景。
  • 节流侧重于限制函数的执行频率,按照一定的时间间隔执行函数,丢弃间隔内的其他触发事件。
39.Map和Object区别
  1. 键类型:

    • Map: 在Map中,键可以是任何数据类型,包括基本数据类型(例如字符串、数字、布尔值等)和复杂数据类型(例如对象、数组等)。
    • Object: 在Object中,键必须是字符串或符号(Symbol)类型。如果尝试使用其他数据类型作为键,JavaScript会将其隐式转换为字符串。
  2. 键值对的顺序:

    • Map: Map保留键值对的插入顺序,因此在遍历Map时,键值对的顺序与插入顺序相同。
    • Object: 在早期的JavaScript标准中,Object并不保证键值对的顺序,但在现代的JavaScript引擎中,Object通常保持键值对的插入顺序。然而,为了保证顺序,推荐使用Map而不是Object
  3. 继承关系:

    • Map: 是一个独立的数据结构,没有原型(prototype)链。可以直接创建一个纯净的Map
    • Object: 是JavaScript中的原生对象,有一个原型链,它继承自Object.prototype。因此,Object具有一些默认属性和方法,比如toString()hasOwnProperty()等。
  4. 方法和功能:

    • Map: Map提供了一些用于操作和遍历数据的方法,比如set()get()delete()has()等。Mapsize属性可以方便地获取键值对的数量。
    • Object: Object是原生JavaScript对象,提供了一些默认的方法和功能,如Object.keys()Object.values()Object.entries()等。但是,它没有像Map那样专为处理键值对而设计的方法。
  5. 性能:

    • Map: 在大型数据集的情况下,由于Map使用哈希表来存储键值对,查找操作的时间复杂度通常是O(1),因此在查找和操作上具有较好的性能。
    • Object: Object在查找操作时需要遍历属性,时间复杂度通常是O(n),其中n是对象的属性数量。对于大型数据集,性能可能会较差。
40.Set和Array区别

用于存储多个值的数据结构,但它们之间有一些重要的区别

  1. 数据结构:

    • Array 是有序的列表数据结构,其中的元素按照插入的顺序排列,并且可以通过索引访问每个元素。
    • Set 是一个无序的集合数据结构,其中的元素是唯一的,不会出现重复的值。Set 中的元素没有固定的顺序,不能通过索引来访问元素。
  2. 元素唯一性:

    • Array 中可以包含重复的元素,例如 [1, 2, 2, 3, 3, 3]。
    • Set 中的元素是唯一的,不会包含重复的值,例如 new Set([1, 2, 2, 3, 3, 3]) 的结果是 {1, 2, 3}。
  3. 值的查找:

    • Array 可以使用索引来查找元素,例如 array[0]。
    • Set 不能通过索引来查找元素,可以使用 Set.prototype.has() 方法来检查是否包含某个元素。
  4. 方法和属性:

    • Array 有很多内置的方法和属性,用于操作和访问数组的元素,比如 push(), pop(), shift(), unshift(), slice(), splice() 等等。
    • Set 也有一些内置的方法和属性,用于操作和访问集合中的元素,比如 add(), delete(), has(), clear() 等等。但 Set 不像 Array 那样拥有丰富的数组操作方法,因为它主要用于确保元素的唯一性。
  5. 遍历:

    • Array 可以使用 for 循环、forEach 方法等来遍历数组中的元素。
    • Set 可以使用 for...of 循环来遍历集合中的元素。
  6. 兼容性:

    • Array 是 ECMAScript 的一部分,所以几乎所有的 JavaScript 环境都支持 Array。
    • Set 是在 ECMAScript 6 (ES6) 中引入的新特性,因此在一些较旧的 JavaScript 环境中可能不支持 Set,需要使用 polyfill 或者转换工具来提供兼容性
41.什么是变量提升

变量提升是JavaScript中的一种特性,指的是在代码执行过程中,变量的声明会在代码执行阶段之前被处理,并且可以在声明之前访问和使用。

变量提升的本质是JavaScript引擎在代码执行前,将变量和函数的声明提升到作用域的顶部,即变量和函数的声明被提升到它们所在作用域的顶部,然后才执行代码。这意味着在变量声明之前就可以使用这些变量,而不会报错。

42.WeakMap和WeakSet

WeakMapWeakSet 是 JavaScript 中的两种弱引用集合数据结构,它们在某些情况下非常有用。它们的主要特点是对存储的对象是弱引用,这意味着它们不会阻止对象被垃圾回收。

WeakMap:

  • WeakMap 是一种键值对的集合,其中的键是对象,而值可以是任意的数据类型。和 Map 不同,WeakMap 中的键必须是对象,而不能是原始数据类型(如字符串、数字等)。
  • 当一个键对象不再被引用,并且垃圾回收时,WeakMap 会自动移除对应的键值对。这使得 WeakMap 非常适合于临时存储对象和附加数据,而无需担心内存泄漏。
  • WeakMap 没有提供像 Map 那样的迭代方法,因为键对象可能被随时删除,迭代可能不稳定。
const weakMap = new WeakMap();
const key1 = { name: 'John' };
const key2 = { name: 'Jane' };
weakMap.set(key1, 10);
weakMap.set(key2, 20);
console.log(weakMap.get(key1)); // 10
console.log(weakMap.get(key2)); // 20
// 当 key1 对象被垃圾回收后,与 key1 相关联的键值对会自动被删除
key1 = null;
console.log(weakMap.get(key1)); // undefined

WeakSet:

  • WeakSet 是一种存储对象引用的集合,它只能包含对象,而不能包含原始数据类型。
  • Set 不同,WeakSet 中的对象引用是弱引用。如果一个对象不再被引用,并且垃圾回收时,WeakSet 会自动移除该对象。
  • WeakSet 没有提供像 Set 那样的迭代方法,因为对象引用可能被随时删除,迭代可能不稳定。
const weakSet = new WeakSet();
const obj1 = { name: 'John' };
const obj2 = { name: 'Jane' };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
console.log(weakSet.has(obj2)); // true
// 当 obj1 对象被垃圾回收后,与 obj1 相关联的对象引用会自动被删除
obj1 = null;
console.log(weakSet.has(obj1)); // false

需要注意的是,由于 WeakMapWeakSet 中的键和值是弱引用,因此不能直接通过循环或迭代来遍历它们。它们主要用于在需要临时存储对象或附加数据的场景中,确保不会阻止对象被垃圾回收。

43.同步和异步有何不同

同步是指两个或多个任务必须按顺序依次执行,一个任务完成后才能执行下一个任务。异步是指两个或多个任务可以同时执行,不需要按顺序依次执行。在同步模型中,一个任务必须等待另一个任务完成才能继续执行,这会导致等待时间延长,降低了系统的效率。而在异步模型中,一个任务可以在等待另一个任务的同时继续执行其他任务,提高了系统的效率

44.异步的应用场景

在 React 中,异步编程在以下场景中非常常见和有用:

  1. 数据获取:在组件渲染之前需要从服务器获取数据时,使用异步编程非常重要。例如,在组件的 componentDidMount 生命周期方法中进行数据请求,并在数据返回后更新组件的状态,从而实现数据的异步加载和展示。
  2. 表单提交:当用户提交表单时,通常需要将表单数据发送到服务器进行处理。使用异步编程可以避免阻塞用户界面,并在发送请求后更新页面状态或进行页面导航。
  3. 延迟加载组件:对于复杂的页面或大型组件,可以使用异步编程实现按需加载。通过使用 React 的动态导入功能(如 React.lazySuspense),可以在需要时异步加载组件,减少初始加载时间。
  4. 条件渲染:当需要根据条件动态加载组件或内容时,异步编程非常有用。例如,根据用户权限或动态配置决定是否加载某个组件或显示某个特定的内容。
  5. 异步操作:在处理异步操作时,如定时器、动画效果、数据更新等,使用异步编程可以更好地管理和控制这些操作。例如,在 useEffect 钩子中使用定时器实现动画效果或定时更新组件状态。
  6. 路由导航:在使用 React 路由库(如 React Router)进行页面导航时,异步编程可以用于在切换页面时进行数据预加载或执行其他异步操作。
  7. 第三方库集成:在集成第三方库或 API 时,通常需要进行异步操作。例如,使用异步加载地图库、图表库或社交媒体 API,以确保页面加载和渲染不受阻塞。
45.真实和虚拟dom区别

虚拟DOM(Virtual DOM)是指由JavaScript对象构建的一种虚拟的DOM树,它是React等一些前端框架采用的一种技术。虚拟DOM本质上是一个JavaScript对象,它保存了一份真实DOM的副本,并且可以在内存中进行操作,而不必直接操作真实的DOM,从而提高了性能。

真实DOM(Real DOM)是指浏览器中实际存在的DOM树,它是由浏览器根据HTML代码生成的,它是一个非常庞大的树状结构,每个节点都有很多属性和方法。

区别如下:

  1. 虚拟DOM是JavaScript对象,真实DOM是浏览器生成的树状结构。
  2. 虚拟DOM可以在内存中进行操作,而真实DOM需要进行重排和重绘。
  3. 虚拟DOM可以进行批量更新,而真实DOM每次更新都需要重新计算样式等操作,性能较低。
  4. 虚拟DOM可以提高性能,减少DOM操作的次数,而真实DOM操作次数多会导致页面性能下降。
46.http与https的区别?
http 
    1.http是明文传输,敏感信息容易被中间劫持(危险) 
    2.用户名密码都是明文传输,如果中间环节有被劫持那么就会泄露 
    3.还有邮箱,手机号 
https 
    1.https = http+加密,可以劫持,但是劫持了也无法解密 
    2.现代浏览器已经慢慢地开始强制https协议
    
1.对称加密: 一个key同时负责加密,解密 
    过程: 
     1.客户端存在一个密钥, 客户端发起请求的时候 ,服务端会把key返回给客户端 
     2.这时候客户端服务端都有一个key 
     3.在传输的过程中是加密的,加密数据,服务端的key用来解密数据,这就是对称加密 
2.非对称加密: 一对key,A加密之后,只能用B来解密 
     1.客户端向服务端请求的时候,服务端会把公钥传递给客户端 
     2.客户端通过publickey加密的东西传递给服务端,服务端通过私钥(key)来解密 
     3.那么此时使用对称加密就安全了,因为不可能被劫持了 
3.https 同时用到了这俩种加密方式

三、React面经

1.MVVM与MVC的区别?

MVC 模式

  1. Model(模型):负责存储应用程序的数据和业务逻辑。它是应用程序的数据层。
  2. View(视图):负责展示数据并接收用户的输入。它是应用程序的界面层。
  3. Controller(控制器):负责协调 Model 和 View,接收用户的输入,并根据输入更新 Model 和 View。它是应用程序的控制层。

在 MVC 模式中,View 是被动的,它只负责展示数据,而 Controller 负责处理用户的输入和更新 Model 和 View。当 Model 更新时,Controller 通知 View 更新界面。MVC 模式更加关注数据和业务逻辑的处理,视图只是数据的展示。

MVVM 模式

  1. Model(模型):同样负责存储应用程序的数据和业务逻辑,与 MVC 中的 Model 类似。
  2. View(视图):负责展示数据,与 MVC 中的 View 类似。
  3. ViewModel(视图模型):是连接 Model 和 View 的桥梁。它将 View 中的数据绑定到 Model 上,并在 Model 更新时更新 View。ViewModel 也包含业务逻辑,相当于 Controller 和一部分 Model 的组合。

在 MVVM 模式中,ViewModel 是比较主动的,它通过数据绑定将 Model 中的数据同步到 View 上,而不需要手动去更新视图。当用户与 View 交互时,ViewModel 处理用户输入并更新 Model。

区别

  • MVC 更加关注数据和业务逻辑的处理,将 View 和 Model 解耦,但需要手动控制数据的传递和更新。
  • MVVM 通过数据绑定将 View 和 Model 解耦,让 ViewModel 自动处理数据同步,更加便于维护和开发。

总体来说,MVC 模式适合传统的 Web 应用程序开发,而 MVVM 模式适用于现代前端开发,特别是在使用框架如 Vue.js 和 Angular 等时,MVVM 模式更为常见和推荐。

2.说说对React的认识及理解?

React 是 Facebook 开发的前端 js 库

V 层:React 并不是完整的 MVC 框架,而是MVC 中的 C (control)层

虚拟 DOM:react 引入虚拟 DOM,每当数据变化通过 React Diff 运算,将上一次的虚拟DOM 与本次渲染的 DOM 进行对比,只渲染更新的,从而有效减少了 DOM 操作。

JSX 语法: js+xml,是 js 的语法扩展,编译后转换成普通的 js 对象。

组件化思想: 将具有独立功能的 UI 模块封装为一个组件,而小的组件又可以通过不同的组合嵌套组成大的组件,最终完成整个项目的构建。

单向数据流:指数据的流向只能由父级组件通过 props 讲数据传递给子组件,不能由子组件向父组件传递数据。要想实现数据的双向绑定只能由子组件接收父组件 props 传过来的方法去改变父组件数据,而不是直接将子组件数据传给父组件

3.React事件机制原理?

React 的事件机制是建立在浏览器的原生事件系统之上的,并使用了一种合成事件(SyntheticEvent)的机制来封装和处理事件

  1. 事件委托(Event Delegation):React 使用事件委托的方式来处理事件。它将事件监听器添加到组件的最外层容器上,而不是每个具体的子元素上。当事件触发时,事件会冒泡到父容器,然后通过 React 的事件系统进行处理。这样可以减少事件监听器的数量,提高性能和内存利用率。

  2. 事件绑定:React 使用一种特殊的语法来绑定事件处理函数。通常,在 JSX 中使用 onEvent 的形式来绑定事件,例如 onClickonSubmit 等。当组件渲染时,React 会将事件处理函数绑定到相应的 DOM 元素上。这样可以确保事件处理函数在正确的上下文中执行,并能够正确地访问到组件的状态和属性。

  3. 合成事件池(Event Pool):为了提高性能和减少内存占用,React 使用了一个合成事件池来重用合成事件对象。每次触发事件时,React 会从事件池中获取一个合成事件对象,并将事件相关的信息填充到该对象中。在事件处理函数执行完成后,合成事件会被重置并放回事件池中,以供下次使用。

4.说一下React的合成事件机制

合成事件(SyntheticEvent):

React 提供了一种合成事件的机制,将浏览器的原生事件进行封装。合成事件是一个跨浏览器兼容的、高性能的事件对象。它提供了与原生事件相似的属性和方法,同时屏蔽了不同浏览器之间的差异。使用合成事件,可以在不同浏览器中保持一致的事件处理逻辑。

5.说一下React的batchUpdate机制

React的batchUpdate机制是指在React更新DOM时,将多个setState()或者其他更新方法放在一个批处理中进行,从而减少更新次数,提高性能的机制。

具体来说,React在进行批处理更新时,会将所有的setState()或者其他更新操作放入一个队列中,然后在执行更新时,会先根据一定的策略(如优先级、时间等)对队列中的更新操作进行排序,然后再一次性执行这些操作,从而减少DOM操作的次数,提高性能。

React的batchUpdate机制有以下几个特点和优点:

  1. 减少DOM操作:通过批处理,可以将多个setState()或者其他更新操作合并成一个,从而减少DOM操作的次数,提高性能。

  2. 减少重复渲染:在批处理更新时,React会自动合并相同的更新操作,避免了重复渲染的问题。

  3. 提高代码效率:通过合理使用batchUpdate机制,可以更加高效地编写React代码,提高代码效率。

6.简述React事务机制?

React 的事务机制主要基于 Transaction 类。每次进行组件更新时,React 会创建一个新的事务,并在其中执行所有的更新操作。在事务中,React 会将所有的状态更新操作合并,然后在事务结束时统一进行更新。这样可以保证组件在一个更新周期内只进行一次渲染,减少渲染次数和提高性能。

事务机制主要涉及以下几个概念:

  1. Transaction 类:React 中的事务机制是通过 Transaction 类来实现的。Transaction 类提供了管理和合并更新的能力。
  2. 事务生命周期钩子:Transaction 类中定义了一些生命周期钩子,包括 initialize, close, willMount, willUpdate 等。这些钩子可以用来控制事务的开始、结束和更新过程中的操作。
  3. enqueueCallback():组件的更新可能会包含一些回调函数,例如在 setState 中传递的回调函数。React 使用 enqueueCallback() 方法来将这些回调函数添加到事务队列中,以确保在适当的时机调用它们。
  4. 事务嵌套:React 的事务机制支持嵌套,可以在一个事务中开启另一个事务。嵌套的事务会继承外层事务的特性,同时也可以有自己的生命周期钩子。

通过事务机制,React 可以高效地管理组件的更新,并在适当的时机进行批量更新和合并,从而提高应用程序的性能和响应速度。

需要注意的是,React v16 之后,事务机制已经被废弃,取而代之的是使用 batchedUpdates() 来实现批量更新。现代 React 使用 Fiber 架构,使用异步渲染和调度算法来处理更新,从而更好地管理组件更新和渲染。

7.说一下React组件渲染和更新的过程
  1. 初始渲染(Initial Rendering):

    • React 组件首次被创建并添加到 DOM 中。
    • 调用组件的构造函数(constructor)和生命周期方法,进行初始化设置和准备工作。
    • 调用组件的 render 方法,生成组件的虚拟 DOM(Virtual DOM)结构。
    • 将虚拟 DOM 转换为真实 DOM,并添加到页面中。
  2. 组件更新(Component Update):

    • 组件的状态(state)或属性(props)发生变化。
    • 调用组件的生命周期方法,如 shouldComponentUpdate(可选)、componentWillUpdate(已弃用)或 getDerivedStateFromProps(React 16.3+)。
    • 调用组件的 render 方法,生成更新后的虚拟 DOM。
    • 使用虚拟 DOM 对比算法(Diffing Algorithm)比较新旧虚拟 DOM 的差异。
    • 根据差异,只更新需要更新的部分,而不是重新渲染整个组件。
    • 更新后的虚拟 DOM 转换为真实 DOM,并进行页面更新。
  3. 组件卸载(Component Unmounting):

    • 组件从 DOM 中移除,不再需要进行渲染。
    • 调用组件的生命周期方法 componentWillUnmount 进行清理工作,例如取消订阅、清除定时器等。
8.React事件和DOM事件的区别
  1. 事件绑定方式不同

在DOM事件中,我们通常使用addEventListener()方法来绑定事件处理函数,而在React中,事件处理函数是通过JSX语法绑定到元素上的

<button onClick={handleClick}>Click me</button>
  1. 事件对象不同

在DOM事件中,事件处理函数会接收到一个事件对象event,其中包含了事件的相关信息,例如事件类型、目标元素、鼠标位置等。而在React中,事件处理函数会接收到一个合成事件对象SyntheticEvent,它是React自己实现的一套事件系统,提供了与原生事件对象类似的接口。

  1. 事件处理方式不同

在DOM事件中,事件处理函数可以通过event.target获取事件触发的元素,并在函数中直接修改元素的属性或者样式等。而在React中,为了保持数据的单向流动和组件的可维护性,通常采用修改组件状态的方式来处理事件,从而引发组件重新渲染,例如:

function handleClick() {
  setState({count: state.count + 1});
}
9.什么是 JSX?为什么浏览器无法读取 JSX?

JSX 是 JavaScript XML 的简写,是 React 使用的一种文件,它利用 JavaScript 的表现力和类似 HTML 的模板语法,得 HTML 文件非常容易理解。并能够提高其性能,浏览器只能处理 JavaScript 对象,而不能读取常规 JavaScript 对象中的 JSX,所以为了使浏览器能够读取 JSX,需要用 Babel 转换器将 JSX 文件转换为 JavaScript 对象,然后再将其传给浏览器,js是浏览器识别的语言

10.React 中 key 的作用是什么?

key 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。

在 React Diff 算法中 react 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染

11.setState之后发生了什么?setState 是同步还是异步?
1.setState之后进行了一个异步更新的操作
2.不会拿到更新之后的值
3.至于说setState是同步还是异步,它其实是分情况的setState有可能是同步也有可能是异步
4.直接使用是会进行一个异步的操作,比如说直接this.setState({},()=>{}) 
第二个会掉参数中会拿到更新之后的值
5.setTimeout和自定义DOM事件中setState是同步的,可以拿到实时最新的值
12.setState何时会合并State?

1.传入对象,会被合并,执行结果只一次 +1

this.setState({
  count: this.state.count + 1
})
this.setState({
  count: this.state.count + 1
})
this.setState({
  count: this.state.count + 1
})

类似于(Object.assign({count:1}, {count: 1})) === 1

2.传入函数,不会被合并,执行结果是+3

this.setState((prevState, props) => {
  return {
    count: preState.count + 1
  }
})
this.setState((prevState, props) => {
  return {
    count: preState.count + 1
  }
})
this.setState((prevState, props) => {
  return {
    count: preState.count + 1
  }
})
13.React18中setState的变化
批处理(Automatic Batching): 合并state + 异步更新
同步更新: setTimeout, DOM事件: 同步更新,不合并state React <= 17
异步更新: setTimeout, DOM事件: 异步更新,合并state React 18
1.React <= 17: 只有React组件事件才批处理
2.React 18: 组件事件+DOM+setTimeout都进行批处理
3.React 18: 操作一致,更加简单,降低了用户的负担和学习成本
14.React事件为何bind this

如果不进行绑定,事件处理函数中的 this 将指向事件触发的 DOM 元素,而不是 React 组件实例。

为了确保事件处理函数中的 this 指向组件实例,常见的做法是使用 bind 方法或箭头函数来绑定 this 上下文。这样可以确保在事件处理函数中使用 this 访问组件的状态和方法。

bind方法

class MyComponent extends React.Component {
  handleClick() {
    // 在事件处理函数中使用this访问组件实例
    console.log(this.props.name);
  }
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>Click me</button>
    );
  }
}

箭头函数

class MyComponent extends React.Component {
  handleClick = () => {
    // 在事件处理函数中使用this访问组件实例
    console.log(this.props.name);
  }

  render() {
    return (
      <button onClick={this.handleClick}>Click me</button>
    );
  }
}

15.什么是React非受控组件?

在React中,组件可以分为受控组件(Controlled Components)和非受控组件(Uncontrolled Components)两种类型。非受控组件是指在表单元素中,其值不由React组件控制,而是由DOM本身管理的一类组件。

在非受控组件中,组件的值由DOM直接控制,而不是通过React组件的状态或props来管理。这意味着React组件无法控制非受控组件的值,也无法追踪非受控组件的状态变化。

class UncontrolledInput extends React.Component {
  inputRef = React.createRef();

  handleSubmit = (event) => {
    event.preventDefault();
    const inputValue = this.inputRef.current.value;
    console.log("Submitted value:", inputValue);
    // 此时的值由DOM管理,React组件无法控制输入框的值
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input ref={this.inputRef} type="text" />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

在上面的例子中,输入框的值由DOM本身管理,而非由React组件控制。当用户在输入框中输入内容后,React组件并不知道输入框的当前值,直到表单提交时,React组件才能获取输入框的值。

非受控组件适用于一些简单的场景,尤其是当需要处理大量表单元素时,通过非受控组件可以减少React组件状态的管理和更新,从而简化代码。然而,需要注意的是,使用非受控组件时,必须小心处理表单数据的获取和处理,避免出现数据不一致或不可预测的情况。通常情况下,推荐使用受控组件,以便更好地管理表单数据和状态。

16.什么场景需要用React Portals
  • React Portals是React提供的一种功能
  • 用于将组件的渲染输出插入到 DOM 树中不同的位置
  • 通常是在组件的父组件之外
1. 模态框(Modal
2. 弹出菜单(Dropdowns
3. 对话框(Dialogs
4. 悬浮提示框(Tooltips
5. 全局消息通知(Snackbar/Toast
17.React如何异步加载组件?
import React, { lazy, Component } from 'react'

const ContextDemo = lazy(() => import('./ContextDemo'))

class App extends Component {
    render() {
        return <div>
            <p>引入一个动态组件</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>}>
                <ContextDemo/>
            </React.Suspense>
        </div>
        // 1. 强制刷新,可看到 loading (看不到就限制一下)控制台 Performance
        // 2. 看 network 的 js 加载
    }
}
18.Vue与React中Diff算法的区别?

Vue中的Diff算法

  1. Vue使用双向比较策略。在进行虚拟DOM的Diff过程中,会同时对新旧虚拟DOM进行深度优先遍历,对比节点的差异。

  2. Vue通过为每个节点添加唯一的key属性,可以减少比较的开销,提高Diff的效率。在更新DOM时,Vue会根据节点的key属性来判断节点的复用、移动和删除。

  3. Vue对比节点时,会先比较节点的标签名和key属性,如果不同则直接替换该节点及其子节点;如果相同则继续比较节点的属性和子节点。

  4. 在比较子节点时,Vue会使用一种叫做“双端比较”的策略,它会同时从新旧虚拟DOM的两端开始比较,逐渐向中间靠拢。这样可以最大程度地复用已有的DOM节点,减少DOM操作。

React中的Diff算法

  1. React使用单向比较策略。在进行虚拟DOM的Diff过程中,只会从上到下遍历新旧虚拟DOM的节点树,对比节点的差。

  2. React并没有像Vue那样强制要求添加key属性,而是通过一种叫做“heuristics(启发式算法)”的策略来尽量复用已有的DOM节点。当节点的类型相同时,React会更新该节点及其子节点的属性;当节点类型不同时,React会直接替换该节点及其子节点。

  3. 在比较子节点时,React采用了一种称为“同层比较”的策略。它会比较相同位置的子节点,尽量找到最小的差异集合。如果节点的顺序发生了变化,React会通过“key”来判断节点的复用、移动和删除。

总结

  • Vue使用双向比较和key属性来优化Diff算法,同时利用“双端比较”减少DOM操作。
  • React使用单向比较和“heuristics”策略来优化Diff算法,并通过“同层比较”尽量减少差异集合。
19.useMemo与useCallback的区别?
    • useMemo 用于对计算结果的记忆化,即在依赖项发生变化时重新计算并缓存结果。它适用于需要根据某些依赖项进行复杂计算并返回结果的场景。
    • useCallback 用于对函数的记忆化,即在依赖项发生变化时重新创建并缓存函数。它适用于需要传递给子组件的回调函数,以避免不必要的函数重复创建。
  1. 返回值类型不同:

    • useMemo 返回计算的结果,可以是任何 JavaScript 值。
    • useCallback 返回一个记忆化后的函数。
  2. 依赖项的使用方式不同:

    • useMemo 接收一个依赖项数组作为第二个参数,当数组中的任何一个依赖项发生变化时,将重新计算并返回新的值。
    • useCallback 接收一个依赖项数组作为第二个参数,当数组中的任何一个依赖项发生变化时,将重新创建并返回新的函数。
  3. 使用场景不同:

    • useMemo 适用于在渲染过程中进行昂贵的计算,避免在每次渲染时重复计算相同的结果。常见的应用场景包括计算、筛选或转换大型数据集,或根据 props 计算派生状态。
    • useCallback 适用于传递给子组件的回调函数,以避免在每次渲染时重新创建新的函数实例。这在需要对回调函数进行记忆化,以确保子组件能够正确地依赖于该函数的引用时非常有用。
20.hook为什么不能放在条件语句中
  1. 难以追踪和管理 Hook 的调用顺序:条件语句的执行逻辑可能会导致不同的分支中使用不同的 Hook,这样会使 React 无法准确追踪每个 Hook 的状态和更新。这可能导致组件状态出现错误、Hook 调用顺序混乱等问题。
  2. Hook 调用次数可能发生变化:条件语句可能会导致组件的渲染次数发生变化。如果 Hook 被放在条件语句中,并且条件切换时 Hook 的调用次数发生变化,那么 React 将无法正确地处理 Hook 的状态和更新,可能会导致错误的行为和渲染结果。

为了避免这些问题,React 对 Hook 的使用规定必须在每次渲染中以相同的顺序调用 Hook。通常,Hook 应该放在组件的顶层作用域中,而不是放在条件语句、循环或嵌套函数中。

如果需要根据条件来应用不同的逻辑,可以将条件判断放在 Hook 调用之前,并在 Hook 调用之前提前返回或提前结束函数。这样可以确保 Hook 在每次渲染中都按照相同的顺序调用,从而保证 React 能够正确追踪和管理状态和更新。

21.Redux的工作流程?
  1. 定义状态(State):在 Redux 中,所有的应用程序状态都被存储在一个称为 "store" 的中央状态容器中。首先,需要定义应用程序的初始状态,并根据需要将其划分为不同的状态片段。
  2. 创建 Action:Action 是一个用于描述状态变化的普通 JavaScript 对象。它必须包含一个 type 字段,用于指示要执行的操作类型,以及其他任意的数据字段,用于传递与操作相关的数据。
  3. 分发 Action:通过调用 Redux 的 dispatch 方法,并传入 Action 对象,将 Action 分发给 Redux Store。这将触发 Redux 的状态更新流程。
  4. Reducer 处理:Reducer 是一个纯函数,接收当前的状态和分发的 Action 作为参数,根据 Action 类型来决定如何更新状态。Reducer 应该返回一个新的状态对象,而不是直接修改原始状态。
  5. 更新 Store:Redux Store 接收到来自 Reducer 的新状态后,会更新自身的状态。这是通过创建一个全新的状态对象来实现的,确保不会直接修改原始状态。
  6. 通知订阅者:Redux Store 的状态发生变化后,会自动通知所有订阅者(通过调用订阅者提供的回调函数)。订阅者可以根据需要获取更新后的状态,并进行相应的处理,例如更新用户界面或触发其他操作。

这个工作流程可以根据需要进行重复,通过分发不同的 Action 来触发不同的状态更新。Redux 的工作流程遵循单向数据流的原则,确保状态的变化是可控和可追踪的,简化了应用程序的状态管理和调试过程。

22.Redux action 如何处理异步?

在Redux中,处理异步操作需要借助中间件(Middleware)来实现。Redux本身是一个同步的状态管理库,它的action只能是一个简单的JavaScript对象,用于描述发生了什么事件以及更新状态的内容。而异步操作,比如发送网络请求或定时器等,是无法直接在action中处理的。

常见的处理异步操作的中间件有redux-thunkredux-sagaredux-observable等。

1.安装redux-thunk中间件:

npm install redux-thunk
  1. 在创建store时将redux-thunk中间件应用到Redux中:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
  1. action中使用异步操作:
// 异步操作的action创建函数
const fetchData = () => {
  return (dispatch) => {
    // 在这里执行异步操作,比如发送网络请求等
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => {
        // 异步操作完成后,调用dispatch触发同步action,更新状态
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      })
      .catch((error) => {
        dispatch({ type: 'FETCH_ERROR', payload: error });
      });
  };
};

在上述代码中,创建了一个异步操作的action创建函数fetchData。这个函数返回一个回调函数,这个回调函数接收dispatch作为参数。在回调函数中执行异步操作,比如发送网络请求,然后根据异步操作的结果,再调用dispatch触发同步的action,更新状态。

redux-thunk中间件允许我们在action中编写异步的业务逻辑,并能够在异步操作完成后再进行状态的更新。这样可以更灵活地处理异步操作,使得Redux在处理异步逻辑时更加方便和易用。

23.简述Redux中间件原理

Redux中间件是对Redux的dispatch方法进行扩展和增强的一种机制。中间件允许你在action被dispatch之后,到达reducer之前,拦截、处理或者转换action。这样可以在Redux的基础上实现一些额外的功能,比如处理异步操作、日志记录、错误处理等。

Redux中间件的原理主要涉及函数的柯里化(currying)和函数组合(compose):

  1. 柯里化(currying):在Redux中,中间件是一个柯里化函数。它会接收Redux的store的dispatch和getState方法作为参数,并返回一个函数,这个返回的函数用于处理和拦截action。
  2. 函数组合(compose):Redux的applyMiddleware函数用于将多个中间件组合成一个中间件。在compose函数的帮助下,多个中间件会依次被调用,并形成一个函数链条。这样,在dispatch action的时候,action会依次经过多个中间件的处理,从而实现对action的拦截、转换或增强。

Redux中间件的基本原理流程如下:

  1. 创建中间件函数:中间件函数需要接收store的dispatch和getState方法作为参数,并返回一个函数,这个返回的函数用于处理和拦截action。

  2. 执行compose:Redux的applyMiddleware函数会将多个中间件组合成一个中间件函数。通过compose函数,多个中间件形成一个函数链条。

  3. 创建store:在创建Redux的store时,使用applyMiddleware将中间件应用到store。

  4. dispatch action:当dispatch一个action时,action会依次经过中间件的处理。每个中间件都有机会处理这个action,或者拦截它,并传递给下一个中间件。

  5. 调用reducer:action经过所有中间件处理后,会到达reducer,然后更新store的状态。

简单来说,Redux中间件通过函数柯里化和函数组合的方式,对dispatch方法进行增强和拦截,从而实现一些额外的功能。这样,Redux可以通过中间件来处理异步操作、记录日志、错误处理等功能,使得Redux的功能更加灵活和强大。常用的Redux中间件包括redux-thunkredux-sagaredux-logger等。

24.讲一讲什么是React高阶组件?

React高阶组件(Higher-Order Component,HOC)是一种用于复用组件逻辑的高级技术。HOC本质上是一个函数,它接收一个组件作为参数,并返回一个新的组件。这个新的组件可以包装或增强原始组件,从而在不修改原始组件代码的情况下,为其添加一些额外的功能或属性。

HOC的作用类似于装饰器模式,它可以将通用的逻辑提取出来,封装为一个独立的函数组件,然后在需要的地方使用它来增强其他组件。这样可以实现代码的复用和逻辑的分离,提高组件的可维护性和可扩展性。

HOC的基本模式如下:

function withEnhancedComponent(WrappedComponent) {
  // 可在这里添加额外的逻辑或属性
  return class EnhancedComponent extends React.Component {
    // ...
    render() {
      // 可以在这里处理传递给原始组件的props
      return <WrappedComponent {...this.props} />;
    }
  };
}

// 使用HOC包装组件
const EnhancedComponent = withEnhancedComponent(BaseComponent);

在上述例子中,withEnhancedComponent是一个HOC函数,它接收一个WrappedComponent作为参数,并返回一个新的组件EnhancedComponent。在EnhancedComponent中,我们可以添加额外的逻辑或属性,并将传递给EnhancedComponent的props传递给原始组件WrappedComponent

HOC的应用场景非常广泛,常见的用途包括:

  1. 添加共享的状态逻辑:将组件的状态管理提取出来,通过HOC进行共享,以便多个组件可以共享相同的状态逻辑。
  2. 权限控制:通过HOC在组件渲染之前检查用户的权限,决定是否渲染组件或重定向到其他页面。
  3. 日志记录:通过HOC记录组件的渲染、更新等操作,用于调试和性能优化。
  4. 动态渲染:根据一些条件来决定是否渲染组件,从而实现动态渲染。

总之,React高阶组件是一种非常强大和灵活的模式,它可以帮助我们实现代码的复用和逻辑的分离,提高React组件的可维护性和可扩展性。

25.怎么解决高阶组件props丢失的问题?
  1. 使用 hoist-non-react-statics 库:这是一个常用的解决方案,它可以将原始组件的静态属性复制到 HOC 返回的包装组件中,确保包装组件拥有与原始组件相同的静态属性和方法。使用该库可以解决由于 HOC 包装而导致的静态属性丢失问题。
  2. 使用 react-clone-children 库:在 HOC 中使用 react-clone-children 库可以确保将原始组件的子组件正确地传递给包装组件,防止子组件在包装过程中丢失。
  3. 使用 forwardRef:如果 HOC 是用于转发 ref 的,可以使用 React 的 forwardRef 函数包装包装组件,将 ref 传递给原始组件。这样可以确保在包装过程中不会丢失 ref。
  4. 传递额外的 props:在 HOC 的实现中,可以通过将原始组件的 props 作为参数传递给包装组件,确保原始组件的 props 在包装过程中得以保留。例如,可以使用 props 参数将原始组件的 props 传递给包装组件的 JSX。
  5. 使用组合而非继承:有时,将原始组件作为 HOC 的子组件,而不是通过继承的方式使用 HOC,可以避免 props 丢失的问题。通过组合的方式,将原始组件作为子组件传递给 HOC,并在包装组件内部正确地处理 props 的传递。
26.什么是React Render Props

React Render Props 是一种在 React 中共享代码的技术。通过 Render Props,可以将一个组件的代码逻辑封装成一个函数,并通过 props 将该函数传递给其他组件,从而使其他组件能够共享该代码逻辑并渲染相应的内容。

Render Props 的基本思想是,一个组件通过 props 接收一个函数,并将其用于渲染结果。这个函数通常被称为“render prop”。它将组件的内部状态、方法或其它信息作为参数,并返回一个 React 元素(通常是 JSX)来描述需要渲染的内容。这样,其他组件可以通过调用 render prop 来渲染相应的内容,并且可以根据需要将自己的逻辑注入到渲染过程中。

一个简单的 Render Props 示例:

// RenderPropsComponent 是一个带有 render prop 的组件
class RenderPropsComponent extends React.Component {
  render() {
    return this.props.render("Hello, from RenderPropsComponent!");
  }
}

// 使用 RenderPropsComponent 渲染内容
class App extends React.Component {
  render() {
    return (
      <div>
        <RenderPropsComponent render={(message) => <p>{message}</p>} />
      </div>
    );
  }
}

在上述示例中,RenderPropsComponent 是一个带有 render prop 的组件。它通过 this.props.render 将一段消息传递给其他组件进行渲染。在 App 组件中,我们使用 RenderPropsComponent 并通过 render prop 来渲染一段消息。

Render Props 的优点包括:

  1. 可复用性:通过将代码逻辑封装成 render prop,可以实现在多个组件之间的复用,从而减少代码重复。
  2. 灵活性:Render Props 可以让父组件将一些逻辑交给子组件处理,从而实现更灵活的组件组合。
  3. 组件解耦:通过 Render Props,组件可以将渲染逻辑与状态管理分离,使组件更加解耦,提高可维护性。

需要注意的是,Render Props 不是唯一的代码共享技术,还有其他模式,比如高阶组件(HOC)和组件组合等。根据实际情况,可以选择最合适的共享代码的方式。

27.使用useEffect模拟WillUnMount时的注意事项
  1. 清除副作用:componentWillUnmount 生命周期主要用于清除组件中创建的副作用,例如取消订阅、清除定时器、释放资源等。在 useEffect 中,可以返回一个清理函数,该函数会在组件销毁时执行。确保在清理函数中取消订阅或清除资源,以防止内存泄漏和意外的副作用。
  2. 依赖项:在 useEffect 中,需要注意指定正确的依赖项数组,以控制副作用的触发和清除。如果没有指定依赖项数组,副作用将在每次组件重新渲染时触发,类似于 componentDidUpdate。如果提供一个空的依赖项数组 [],副作用将只在组件首次渲染时触发,类似于 componentDidMountcomponentWillUnmount 结合。
  3. 外部资源的清理:如果副作用涉及到外部资源,例如订阅或事件监听,确保在清理函数中正确地取消订阅或移除事件监听器。否则,即使组件销毁,这些订阅或监听器仍然存在,可能导致内存泄漏或不必要的副作用。
  4. 注意清理函数的执行时机:清理函数会在组件销毁时执行,但也可能在下一次副作用触发之前执行。这是因为在组件卸载时,React 会先执行清理函数,然后再执行下一次渲染,并可能触发新的副作用。因此,清理函数应该能够处理组件已经销毁的情况,避免访问已卸载的组件实例。
28.useReducer能代替redux吗?

useReducer 是 React 提供的一种状态管理钩子,而 Redux 是一个独立的状态管理库。尽管它们都可以用于管理应用程序的状态,但在不同的场景下,它们有各自的优势和适用性。

useReducer 和 Redux 都可以用于处理复杂的状态逻辑,但它们在设计和使用上有一些区别:

  1. 复杂性:useReducer 是 React 的内置功能,它相对来说更简单和轻量,特别适合处理组件内部的局部状态。而 Redux 是一个专门用于状态管理的第三方库,可以处理全局的应用状态,同时提供了更多的高级功能,如中间件、时间旅行调试等。
  2. 依赖性:使用 useReducer 不需要额外的依赖,因为它是 React 自带的功能。而 Redux 需要单独安装和集成到项目中,引入了额外的依赖。
  3. 基于Context:useReducer 和 Redux 都可以结合 React 的 Context API 来实现状态的全局共享。但是在复杂的全局状态管理情况下,Redux 更容易组织和扩展。
  4. 生态系统:Redux 拥有丰富的插件和工具,如 Redux DevTools,使得开发者可以方便地调试和监控状态变化。而 useReducer 虽然也可以结合一些工具,但其生态系统相对 Redux 来说还不够完善。

综上所述,useReducer 可以用于简单的状态管理和组件内部的局部状态,而 Redux 更适用于复杂的全局状态管理和具有大量状态交互的应用程序。在选择使用哪种方式时,可以根据项目的规模、复杂性以及个人或团队的偏好来决定。对于小型的项目或简单的状态管理,useReducer 可以代替 Redux,而对于大型的复杂项目,使用 Redux 可以更好地组织和管理状态。

29.什么是自定义Hook?

自定义 Hook 是普通的 JavaScript 函数,但它们有两个特殊的约定:

  1. 自定义 Hook 函数的名称应该以 use 开头,这是为了让 React 在使用自定义 Hook 时能够识别和应用其特殊行为。
  2. 自定义 Hook 可以使用 React 的其他 Hook。这意味着你可以在自定义 Hook 中使用 useStateuseEffectuseContext 等 React 提供的 Hook 来访问状态和实现副作用。

通过自定义 Hook,你可以将组件中的逻辑抽离出来,并将其封装成可复用的函数。这样可以提高代码的可维护性和重用性,避免代码重复和逻辑分散的问题。

例如,假设你在多个组件中都需要订阅和取消订阅某个事件,你可以创建一个名为 useEventSubscription 的自定义 Hook,它封装了订阅和取消订阅的逻辑。然后,在需要订阅事件的组件中,可以直接使用 useEventSubscription Hook 来获取订阅相关的状态和函数。

30.使用Hooks的两条重要规则
  1. 只在最顶层使用 Hooks:确保在 React 函数组件的最顶层(也就是函数组件的首次执行)使用 Hooks,不要在循环、条件判断或嵌套函数中使用 Hooks。这样可以确保 Hooks 的调用顺序在每次渲染中都是一致的,避免出现状态更新错误或副作用问题。
  2. 只在 React 函数组件中使用 Hooks:Hooks 只能在 React 函数组件中使用,不能在类组件中使用。这是因为 Hooks 的设计初衷是为了解决类组件中复杂逻辑复用的问题,而且 Hooks 的实现也依赖于 React 函数组件的渲染流程。

遵循这两条规则可以确保使用 Hooks 的正确性和稳定性。如果违反这些规则,可能会导致不可预料的错误或副作用。另外,需要注意的是,Hooks 的命名必须以 use 开头,这是 React 对自定义 Hook 的约定,以便识别和区分普通函数和 Hook 函数。例如,useEffectuseStateuseCustomHook 等都是有效的 Hook 命名。

31.为何Hooks要依赖于调用顺序?

Hooks在设计上依赖于调用顺序,这是因为Hooks的工作原理和状态管理机制决定了它们需要维护状态的顺序和一致性。

React中的Hooks是为了解决组件之间状态逻辑复用的问题。使用Hooks,可以在函数组件中引入状态和副作用的管理,取代了之前类组件中的生命周期方法和类成员属性。

Hooks的调用顺序非常重要,这是因为Hooks本身是基于数组的,并且使用数组索引来关联组件的状态。当你在组件中调用一个Hook时,React会根据Hook在数组中的位置来确定该Hook是与哪个组件实例相关联的。如果Hooks的调用顺序发生变化,那么React将无法正确地识别和管理组件的状态,可能会导致意外的行为和错误。

举个例子来说明,假设有两个状态钩子useState,其中一个用于管理计数,另一个用于管理一个对象。如果在一个函数组件中交换了这两个Hook的调用顺序,React将会错误地将计数值应用到对象的状态,而将对象应用到计数状态。这显然会导致混乱和错误的结果。

为了保持状态管理的一致性和正确性,React需要确保Hooks的调用顺序是稳定的和不变的。这也是为什么在函数组件中使用Hooks时,必须遵循一些规则,比如不能在条件语句或循环中使用Hooks,以确保Hooks的调用顺序是固定的。

总结起来,Hooks依赖于调用顺序是为了维护状态管理的一致性和正确性,保证Hooks能够正确地关联到组件实例的状态,并且能够正确地进行状态更新和副作用管理。遵循Hooks的调用规则是保障代码正确运行和可维护性的重要一环。

32.class组件与函数组件的区别?

函数组件

1. 纯函数,输入props,输出JSX
2. 没有实例,没有声明周期,没有state
3. 不能扩展其他方法
4. 组件更易用函数表达,一个组件一个函数
5. 函数更加灵活,易于拆分,易于测试
6. 但是函数组件太简单,需要增强能力--hooks

类组件

1.大型组价很难拆分和重构,不易于测试
2.相同业务逻辑,分散到各个方法中逻辑混乱
3.复用逻辑变得比较复杂,如Mixins,HOC,Render Props
33.context
  • 公共信息(语言,主题)如何传递给每个组件
  • 用props太繁琐
  • 用redux小题大做
34.react生命周期
  1. 挂载阶段:在组件实例被创建并插入到DOM中时调用。主要方法有:
  • constructor(): 构造函数,在组件被创建时调用,用于初始化state和绑定事件等。
  • static getDerivedStateFromProps():在组件实例化时和更新时都会被调用。
  • render():渲染组件内容,返回React元素。
  • componentDidMount():组件挂载后执行,一般用来进行数据请求、DOM操作等操作。
  1. 更新阶段:在组件props或state发生变化时调用。主要方法有:
  • static getDerivedStateFromProps():在组件实例化时和更新时都会被调用。
  • shouldComponentUpdate():用于控制组件是否需要进行更新,返回Boolean值。
  • render():渲染组件内容,返回React元素。
  • componentDidUpdate():组件更新后执行,一般用来进行数据请求、DOM操作等操作。
  1. 卸载阶段:在组件从DOM中移除时调用。主要方法有:
  • componentWillUnmount():组件卸载前执行,一般用来进行清理工作,如取消定时器、取消事件监听等。

总的来说,生命周期函数提供了一种机制,用于管理组件的状态和行为,从而让开发者更好地掌控组件的生命周期,以达到更好的开发效果。

35.如何避免子组件重复渲染

子组件重复渲染通常是因为父组件重新渲染导致的,这可能会导致不必要的计算和虚拟DOM的重新构建

使用React.memo React.memo是一个高阶组件,用于封装子组件,以确保只有在其props发生变化时才会重新渲染。React.memo会对子组件进行浅层比较,如果props没有发生变化,就会使用之前的渲染结果。这样可以避免不必要的渲染。

import React from 'react';
const ChildComponent = React.memo(({ data }) => {
  console.log('ChildComponent re-rendered');
  return <div>{data}</div>;
});

使用useCallback 如果将回调函数传递给子组件,可以使用useCallback来缓存函数引用,以确保只有在依赖项发生变化时才会重新创建函数。这样可以避免在父组件重新渲染时将新的回调函数传递给子组件,从而避免子组件重复渲染。

import React, { useCallback } from 'react';
const ParentComponent = () => {
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);
  return <ChildComponent onClick={handleClick} />;
};

使用useMemo 如果子组件中有一些昂贵的计算或需要大量处理的数据,可以使用useMemo来缓存计算结果或处理后的数据,以确保只有在依赖项发生变化时才会重新计算。

import React, { useMemo } from 'react';
const ChildComponent = ({ data }) => {
  const processedData = useMemo(() => {
    console.log('Data processing');
    // 进行一些昂贵的计算或数据处理
    return data.toUpperCase();
  }, [data]);

  return <div>{processedData}</div>;
};
  1. 避免不必要的父组件渲染: 如果父组件没有必要重新渲染,可以使用React.memouseCallbackuseMemo等技术来确保父组件只有在必要时才会重新渲染。
  2. 使用key属性: 如果在循环中渲染多个子组件,确保给每个子组件添加唯一的key属性。这样React会根据key属性来确定子组件的增删和重排,避免不必要的重复渲染。
import React from 'react';
const ParentComponent = () => {
  const data = ['Item 1', 'Item 2', 'Item 3'];
  return (
    <div>
      {data.map((item, index) => (
        <ChildComponent key={index} data={item} />
      ))}
    </div>
  );
};

四、性能优化

1.React性能优化-SCU的核心问题在哪里?
  • React 默认: 父组件有更新,子组件则无条件也更新
  • 性能优化对React更加重要,性能优化永远是面试重点
  • SCU 一定有每次都用? ---- 需要的时候才会进行优化
shouldComponentUpdate(nextProps, nextState) {
    // nextProps 更新之前
    // nextState 更新之后
    if (nextState.count !== this.state.count) {
        return true // 可以渲染
    }
    return false // 不重复渲染
}
2.React性能优化-SCU的默认返回什么?
  • React 默认: 父组件有更新,子组件则无条件也更新
shouldComponentUpdate(nextProps, nextState) {
    // 默认值返回true
}
3.React性能优化-SCU一定要配合不可变值
4.React性能优化-PureComponent和memo
5.React性能优化-了解immutable.js
6.React-fiber 如何优化性能
7.使用useMemo做性能优化

useMemo是一个用于性能优化的Hook,它用于缓存计算结果,避免在每次渲染时重新计算。这对于一些复杂的计算或昂贵的操作特别有用,可以减少不必要的重复计算,提高组件的渲染性能。

useMemo的基本用法是:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • computeExpensiveValue:是你要进行计算的函数或表达式。
  • [a, b]:是一个数组,包含所有影响计算的变量,当这些变量发生改变时,useMemo会重新计算,并返回计算结果。
import React, { useState, useMemo } from 'react';

const ParentComponent = () => {
  const [count, setCount] = useState(1);
  const [multiplier, setMultiplier] = useState(1);

  // 没有使用 useMemo,每次渲染都会重新计算 result
  const result = count * multiplier;

  // 使用 useMemo,只有当 count 或 multiplier 发生变化时才会重新计算 result
  const memoizedResult = useMemo(() => count * multiplier, [count, multiplier]);

  return (
    <div>
      <p>Result without useMemo: {result}</p>
      <p>Result with useMemo: {memoizedResult}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <button onClick={() => setMultiplier(multiplier + 1)}>Increase Multiplier</button>
    </div>
  );
};

export default ParentComponent;

在上面的例子中,我们在ParentComponent中定义了一个名为result的变量,并计算了count * multiplier的值。如果我们没有使用useMemo,每次ParentComponent重新渲染时都会重新计算result,即使countmultiplier没有变化。这可能会导致一些昂贵的计算操作重复进行,影响性能。

通过使用useMemo,我们将count * multiplier包裹起来,并将[count, multiplier]作为依赖项传递。这样,只有当countmultiplier发生变化时,才会重新计算memoizedResult的值。这样可以避免不必要的重复计算,提升性能。

8.使用useCallback做性能优化

在React中,useCallback是一个用于性能优化的Hook,它用于缓存函数,避免在每次渲染时创建新的函数实例。这对于传递给子组件的回调函数特别有用,可以减少不必要的重新渲染。

当一个组件渲染时,其内部定义的函数会在每次渲染时都被重新创建,这可能会导致子组件接收到新的回调函数,即使传入的参数没有变化。使用useCallback可以解决这个问题,只有在依赖项变化时,才会创建新的函数实例。

基本用法

const memoizedCallback = useCallback(callbackFunction, dependencies);
  • callbackFunction:是你要缓存的回调函数。
  • dependencies:是一个数组,包含所有影响回调函数的变量,当这些变量发生改变时,useCallback会返回一个新的回调函数实例。

例子

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent re-rendered');
  return <button onClick={onClick}>Click me</button>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 没有使用 useCallback,每次 ParentComponent 渲染时都会创建新的函数实例
  const handleClick = () => {
    console.log('Button clicked');
    setCount(count + 1);
  };

  // 使用 useCallback,只有当 count 发生变化时才会创建新的函数实例
  const memoizedHandleClick = useCallback(handleClick, [count]);

  return (
    <div>
      <ChildComponent onClick={memoizedHandleClick} />
      <p>Count: {count}</p>
    </div>
  );
};

export default ParentComponent;

在上面的例子中,我们在ParentComponent中定义了一个名为handleClick的回调函数,并将其作为onClick属性传递给ChildComponent子组件。如果我们没有使用useCallback,每次ParentComponent重新渲染时都会创建一个新的handleClick函数实例,即使count没有变化。这样ChildComponent会接收到新的onClick回调函数,导致ChildComponent不必要地重新渲染。

通过使用useCallback,我们将handleClick包裹起来,并将[count]作为依赖项传递。这样,只有当count发生变化时,才会创建新的handleClick函数实例。这样可以有效减少ChildComponent的不必要重新渲染,提升性能。

请注意,useCallback并不是一定要使用的,而是在需要优化性能时使用的工具。在大多数情况下,React会自动处理回调函数的性能优化,因此只有在确实遇到性能问题时,才考虑使用useCallback进行手动优化。

9.大文件上传怎么做分段上传
10.组件封装思想
  • 封装组件的角度,如果只是当前组件使用,那么只需要适应当前需求就行
  • 如果把组件上升到整个项目的角度,不仅在当前需求使用,还在其他页面使用
  • 那么这个时候封装组件要考虑以下类容
  • 因为其他人也要用,所以要有完善的文档,驶入这个组件干什么的 每个属性的所用等等
  • 因为提升到项目层面,还要考虑组件的容错性,扩展性,比如一个组件的宽高,不要写死,支持扩展 使用者可以自定义
  • 还要考虑用户传进来的值是否正确 所以还要做props类型的判断,也就是容错
  • 可读性 扩展性 完整性 耦合性(度)

五、手写代码?

1.手动实现forEach,map

forEach

function forEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i], i, array);
  }
}

// 使用
const numbers = [1, 2, 3, 4, 5];
forEach(numbers, (item, index) => {
  console.log(item); // 输出数组元素
});

map

function map(array, callback) {
  const result = [];
  for (let i = 0; i < array.length; i++) {
    result.push(callback(array[i], i, array));
  }
  return result;
}

// 使用
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = map(numbers, (item, index) => {
  return item * 2;
});
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]
2.手写深拷贝,考虑各种数据类型和循环引用
function deepClone(obj, clonedMap = new WeakMap()) {
  // 检查是否已经拷贝过该对象,避免循环引用导致的无限递归
  if (clonedMap.has(obj)) {
    return clonedMap.get(obj);
  }

  // 处理特殊数据类型
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 创建新的对象或数组
  const clone = Array.isArray(obj) ? [] : {};

  // 将新对象添加到已拷贝对象的映射中
  clonedMap.set(obj, clone);

  // 遍历原对象的属性/元素并进行深拷贝
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], clonedMap);
    }
  }

  return clone;
}
3.手动实现lodash的get方法
function get(obj, path, defaultValue) {
  // 将路径字符串转换为路径数组
  const pathArray = path.split('.');

  // 遍历路径数组以获取目标值
  let target = obj;
  for (let key of pathArray) {
    // 如果当前目标值为 undefined 或 null,则返回默认值
    if (target === undefined || target === null) {
      return defaultValue;
    }
    // 更新目标值为下一层级的属性值
    target = target[key];
  }

  // 返回最终的目标值,如果目标值为 undefined,则返回默认值
  return target === undefined ? defaultValue : target;
}

使用

const user = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    postalCode: '12345'
  }
};
console.log(get(user, 'name')); // 输出 'John'
console.log(get(user, 'age')); // 输出 30
console.log(get(user, 'address.city')); // 输出 'New York'
console.log(get(user, 'address.street', 'N/A')); // 输出 'N/A',因为属性不存在
4.手动实现lodash的isEmpty方法
function isEmpty(value) {
  // 检查基本类型的值是否为空
  if (value === null || value === undefined || value === '') {
    return true;
  }

  // 检查数组类型的值是否为空
  if (Array.isArray(value)) {
    return value.length === 0;
  }

  // 检查对象类型的值是否为空
  if (typeof value === 'object') {
    return Object.keys(value).length === 0;
  }

  // 其他类型的值都被视为非空
  return false;
}

5.手动实现lodash的isEqual方法
/**
 * @method isObject 
 * @description 判断是否是对象或者数组
 * @param obj
 */
const isObject = (obj) => {
    return typeof obj === "object" && obj !== null
}

/**
 * @method isEqual
 * @description 判断俩个数组或者对象是否全相等 
 */
const isEqual = (obj1, obj2) => {
    // 判断obj1或者obj2 只要有一个不是对象或者数组
    if (!isObject(obj1) || !isObject(obj2)) {
        // 值类型 (!!! 参与equal的不会是函数)
        return obj1 === obj2
    }
    if (obj1 === obj2) {
        return true
    }
    /**
     * @description 俩个都是数组或者对象,而且不相等
     * 1. 先取出obj1,obj2的keys,比较个数  
     * 2. Object.keys返回的key是放到数组中的 
     * 3. 数组是以索引为key
     * 4. 对象是以属性为key
     * 5. 判断俩个对象的keys长度
     */
    const obj1Keys = Object.keys(obj1)
    const obj2Keys = Object.keys(obj2)
    if (obj1Keys.length !== obj2Keys.length) {
        return false
    }
    /**
     * 以obj1 为基准,和obj2 依次递归比较
     */
    for (let key in obj1) {
        // 比较当前key的value
        const res = isEqual(obj1[key], obj2[key])
        if (!res) {
            return false
        }
    }
    /**
     * 全部相等
     */
    return true
}
6.手动实现防抖节流

防抖使用场景(Debounce)

  • 搜索框搜索
  • 窗口大小调整

原理

当事件触发后,设置一个定时器,在定时器设定的时间内没有再次触发事件时,执行相应的操作。如果在定时器设定的时间内再次触发事件,会重新计时。

function debounce(func, delay) {
  let timerId;
  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 在连续输入过程中,只在最后一次输入后的 300 毫秒执行搜索操作 
const searchInput = document.querySelector('#search-input'); 
searchInput.addEventListener('input', debounce(search, 300));

节流使用场景(Throttle)

  • 需要限制函数在一定时间间隔内执行的场景,常见的场景包括滚动事件鼠标移动事件等。

原理

在指定的时间间隔内,无论事件触发频率如何,都只会执行一次操作。如果在时间间隔内触发事件,操作会被忽略。

解读

使用了一个变量 lastExecutionTime 来记录上一次函数执行的时间戳。

当触发事件时,我们检查当前时间与上次执行时间的差值,如果小于指定的时间间隔 delay,则启动定时器延迟执行函数。

如果超过时间间隔,则立即执行函数,并更新 lastExecutionTime

这样实现的节流函数能够确保在指定的时间间隔内只执行一次函数,并且在事件频繁触发时能够合理地控制执行次数。

function throttle(func, delay) {
  let timerId;
  `lastExecutedTime  记录上一次函数执行的时间戳`
  let lastExecutedTime = 0;
  return function (...args) {
    const currentTime = Date.now();
    if (currentTime - lastExecutedTime >= delay) {
      func.apply(this, args);
      lastExecutedTime = currentTime;
    }
  };
}
// 在滚动过程中,每 300 毫秒执行一次滚动处理函数
window.addEventListener('scroll', throttle(handleScroll, 300));
7.手动实现Promise.all, race,then

手动实现Promise.all, race,then

8.使用useState实现state和setState功能
import React, { useState } from 'react';
function MyComponent() {
  // 使用 useState 钩子来定义状态变量和状态更新函数
  const [state, setState] = useState(initialState);

  // 在组件中可以通过 state 来访问状态值,通过 setState 来更新状态
  // ...

  return (
    // JSX 元素和其他组件
    // ...
  );
}
9.使用useEffect模拟组件生命周期
import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);

  // 模拟 componentDidMount,组件挂载时调用
  useEffect(() => {
    console.log('Component did mount');
    fetchData();
  }, []);

  // 模拟 componentDidUpdate,data 发生变化时调用
  useEffect(() => {
    console.log('Component did update');
    // 执行其他操作...
  }, [data]);

  // 模拟 componentWillUnmount,组件卸载时调用
  useEffect(() => {
    return () => {
      console.log('Component will unmount');
      // 执行清理操作...
    };
  }, []);

  // 模拟其他函数
  const fetchData = () => {
    // 模拟异步请求数据
    setTimeout(() => {
      setData('Fetched data');
    }, 2000);
  };

  return (
    <div>
      <p>{data}</p>
    </div>
  );
}
10.手写JS函数,实现数组深度扁平化
function deepFlatten(arr) {
  let flattened = [];

  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      // 递归调用 deepFlatten 函数处理嵌套的数组
      flattened = flattened.concat(deepFlatten(arr[i]));
    } else {
      flattened.push(arr[i]);
    }
  }
  return flattened;
}

// 使用
const nestedArray = [1, [2, [3, 4], 5], 6, [7, 8]];
const flattenedArray = deepFlatten(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6, 7, 8]
11.手写一个getType函数,获取详细的数据类型
function getType(value) {
  const type = typeof value;

  if (type !== 'object') {
    return type;
  }

  if (value === null) {
    return 'null';
  }

  if (Array.isArray(value)) {
    return 'array';
  }

  if (value instanceof Date) {
    return 'date';
  }

  if (value instanceof RegExp) {
    return 'regexp';
  }

  return 'object';
}

// 使用
console.log(getType('Hello')); // 输出 'string'
console.log(getType(123)); // 输出 'number'
console.log(getType(true)); // 输出 'boolean'
console.log(getType(null)); // 输出 'null'
console.log(getType([1, 2, 3])); // 输出 'array'
console.log(getType({ name: 'John' })); // 输出 'object'
console.log(getType(new Date())); // 输出 'date'
console.log(getType(/pattern/)); // 输出 'regexp'
12.手写curry函数,实现函数柯里化
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}
    1. 这个curry函数接受一个函数fn作为参数,并返回一个新的函数curried。
    2. 当调用curried函数时,它会判断传入的参数数量是否达到了原始函数fn所需的参数数量。
    3. 如果达到了,就直接调用原始函数fn并返回结果。
    4. 如果还没有达到,就返回一个新的函数,继续接收更多的参数,并将之前的参数与新参数合并,
    5. 直到达到原始函数fn所需的参数数量为止。
    
// 示例函数,将两个数相加
function add(a, b) {
  return a + b;
}

const curriedAdd = curry(add);

console.log(curriedAdd(2)(3)); // 输出: 5
console.log(curriedAdd(2, 3)); // 输出: 5
console.log(curriedAdd(2)(3, 4)); // 输出: 5
13.使用代码表示instanceof 原理
function myInstanceof(obj, constructor) {
  // 判断参数的有效性
  if (typeof obj !== 'object' || obj === null) {
    return false;
  }

  // 获取对象的原型
  let proto = Object.getPrototypeOf(obj);

  // 循环向上遍历原型链
  while (proto !== null) {
    // 判断原型是否为目标构造函数的原型
    if (proto === constructor.prototype) {
      return true;
    }

    // 获取当前原型的上一级原型
    proto = Object.getPrototypeOf(proto);
  }

  return false;
}

// 使用
function Person() {}
const john = new Person();

console.log(myInstanceof(john, Person)); // 输出 true
console.log(myInstanceof(john, Object)); // 输出 true
console.log(myInstanceof(john, Array)); // 输出 false

1. `myInstanceof` 函数接受两个参数:要检查的对象 `obj` 和目标构造函数 `constructor`
2. 函数首先检查 `obj` 是否为对象类型,以及是否为 `null`
3. 然后,通过 `Object.getPrototypeOf` 方法获取 `obj` 的原型,并将其赋值给变量 `proto`
4. 然后,使用一个循环向上遍历原型链,直到达到原型链的顶端(即 `proto``null`)或找到目标构造函数的原型。
5. 在循环中,每次将当前原型的上一级原型赋值给 `proto`
6. 如果在循环中找到目标构造函数的原型,则返回 `true`,表示 `obj` 是目标构造函数的实例。
7. 如果循环结束仍未找到目标构造函数的原型,则返回 `false`
14.手写函数bind,call, apply功能
15.手写EventBus自定义事件-包括on和once
16.手写EventBus自定义事件-on和once分开存储

TS中的问题

interface 与 type区别是什么?

语法差异

  • interface 使用 interface 关键字定义
  • type 使用 type 关键字定义

合并能力

  • interface 接口可以重名 type不可以重名
  • interface 同名接口会被合并为一个接口 type会产生冲突 发生报错565656565656

对象类型 vs 声明类型

  • interface 用于定义对象类型,可以描述对象的结构``属性方法 也可以实现继承(extends)
  • type 支持联合类型 '|', 交叉类型 '&'

4.用过TS哪些东西?

类型注解

原始类型(基本类型)====> 对象类型(Fn,Array,Object) ===>  

TS新增类型
    联合类型   | 
    交叉类型   & 
    自定义类型 type
    接口类型   interface
		// 定义一个数组中具体索引位置的类型时  可以使用元祖
    元祖(turple)let position:[number, number]=[1, 3] 
    枚举enum
    可选类型   ?
    any类型(任意类型)
类型断言  as
    // TS只能知道给你的类型是HTMLElement 
    // HTMLAnchorElement  为  HTMLElement 的子类型
    // 使用  as  实现类型断言   强制转换为我们想要的类型
    const link = document.getElementById('link') as HTMLAnchorElement

   res1 = {
      user: 'name',
      age: 19,
      sex: 'man'
    } as test2    //  类型断言可以转换类型   但是不能改变初始对象
泛型
    /**
    * 什么是泛型? 
    * 让函数等 与  多种类型一起工作  实现复用
    * 常用于 函数  接口  class中
    */
    function test1 (value: string): string {
        return value
    }
    
    function test2 (value: number): number {
        return value
    }
    
    function test3 (value: boolean): boolean {
        return value
    }
    
    // 这里的 T  就是泛型的变量  T 可以是任意类型 number  / { } /
    // 保证类型的安全 不丢失类型信息的同时 函数可以和多种不同的类型一起工作 灵活使用
    /**
     * @param T 
     * @returns   T他=它处理的是类型而不是值
     * 
     * 能够捕获用户提供的类型  具体的类型有用户调用该函数时<T>指定
     */
    function com<T>(value: T): T {
        return value
    }
    com<string>('1')   //类型推论
    com(1) 
    
    /**
     * 泛型函数语法  保证类型安全  不是any
     */
    type test = <T>(value1: T, value2: T, value3: T) => T
    let fun: test = (value) => {
        return value
    }
    // let str:string = fun<number>(1,3,3) 
    fun<number>(1,3,3)

泛型工具类型
	Partial<接口类型> 可以将传入的属性都变为可选的
  ReadOnly  将类型中的所有属性变为只读属性
  Pick 选择从属性中构造新类型