存档

55 阅读24分钟

1. <img>标签的alt属性用来做什么的?

如果图片显示不出来用于进行文字提示;帮助seo索引。

2. 简述HTML新特性,请至少列举5个。

  • 语义化标签,方便阅读,代码清晰,以及SEO索引
  • 新增<audio><video>标签,原生支持音视频播放
  • 新增<canvas>标签,支持画布
  • 新增localStorage和sessionStorage两种存储方式
  • 新增更多的Form格式,比如<input>标签新增type字段,像是password/date等等
  • 提供了web workers的语法,支持浏览器多线程执行代码。

3. <meta>标签的作用是什么

提供相关浏览器编码的元数据,必填的像是charset和viewport。其他还有向页面提供SEO索引的name="description"name="keywords";控制浏览器加载的http-equiv="Cache-Control"http-equiv="Refresh"等等。

4. <a>标签的target有哪些取值?

target决定超链接跳转页面在哪个窗口、标签页打开。

  • _self:打开的页面会替换当前页面,不会新开页面
  • _blank: 会新建空白标签页打开链接。同时需要添加rel="noopener noreferer",防止新打开的页面通过window.opener篡改原页面,提升安全性。

因为当设置target="_blank"打开标签页的时候,新页面可以通过window.opener这个属性拿到原页面的window对象,所以设置noopener禁止拿到原网页的window对象,直接将原页面的属性变成null;同时不加的时候跳转到新页面的HTTP请求头会包含refer字段,包含前网页的url字段,url上有时候query会包含部分隐私信息,noreferer直接禁止新网页包含这个字段。

  • _parent: 仅在iframe框架中使用,在当前父框架里面替换掉iframe里面的页面内容
  • _top: 在浏览器的顶级窗口替换对应的链接内容。

5. 请求头常见的Header有哪些?

  • 请求地址Request URL: https: // baidu.com /rest/api
  • 请求方式Request Method:POST/GET/DELETE/PUT
  • 状态码Status code:2XX正常接收/3XX重定向/4XX客户端请求不存在或者出问题/5XX服务器没响应或者响应错误
  • 200 正常返回数据;
  • 201 请求成功并创建了新资源,比如创建了新用户表单,上传文件成功
  • 204 请求成功但是无内容返回(比如删除成功,无需返回数据)
  • 301 永久重定向,比如域名更换
  • 302 临时重定向,未登录的用户临时重定向到登录页
  • 304 协商缓存发现用户的资源没有变化,不需要重新拉取数据
  • 400 请求参数的字段有问题,比如请求参数漏传错传
  • 401 未授权,需要登录
  • 403 当前用户无权限访问该内容
  • 404 当前请求的资源不存在
  • 500 服务器内部有问题
  • 502 网关错误
  • 503 服务器正在更新、维护中
  • 连接方式Connection:KEEP-ALIVE长连接
  • 响应体的编码类型Content-type: application/json; application/javascript; image/png; text/html;
  • 客户端支持的请求体的类型Accept:和Content-type基本一致
  • 客户端支持的请求体的编码格式Accept-Encoding:gzip,deflate,br
  • 客户端支持的语言格式Accept-Language:zh-CN;zh
  • 客户端支持的缓存形式Cache-control:no-cache;no-store;max-age。如果是强缓存,会有expires字段。如果是协商缓存,则可能会有Etag,LastModified等字段。
  • Cookie
  • 前置页面的链接Referer: https: //baidu.com /a11111
  • 用户使用的设备User-Agent
  • 客户端认证信息Authorization:存放Token

6. 请简述CSS定位position的几种取值及其含义

  • static 默认值
  • relative 相对于自身的位置偏移,不脱离文档流
  • absolute 相对于父组件的位置偏移,脱离文档流,经常和relative一起使用,父组件使用relative固定位置,子组件使用absolute相对变化
  • fixed 固定在屏幕某个位置不发生变化,比如面包屑、顶部滚动提示条等等
  • sticky 未触发滚动时遵循relative定位,触发后会fixed

7. display存在哪些常见取值?

  • block 块级显示,元素单独占一行,可自由设置width/height,margin/padding
  • inline 行内显示,无法设置宽高
  • inline-block 行内显示,但是可以设置宽高
  • none 隐藏该空间,没有该div在dom树上
  • flex 弹性盒子布局
  • grid 网格布局

8. CSS3中的新属性,至少列举5个。

  • border-radius 圆角
  • box-shadow 盒阴影
  • linear-gradient 线渐变 background: linear-gradient(to right,red,blue)
  • transition 过渡,为css属性变化提供过渡动画 transition: all 0.3s ease
  • transform 变换 里面包含平移translate,transform:translate(-50%,-50%); 缩放scale, transform:scale(1.2); 旋转rotate等等
  • flexbox 弹性盒子布局
  • gridbox 网格布局
  • media 媒体查询
  • CSS变量

9. 如何实现一个三角形

.div{
    width: 0px;
    height:0px;
    border-top:50px solid red;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: solid transparent;
}

9. 设置元素可见的方法,至少列举三种

  • display:none 元素完全不见,占位空间也消失
  • visibility:hidden 元素不可见,但占位空间存在
  • opacity:0 元素不可见,但占位空间存在

10. 如何判断一个变量的数据类型

  • typeof 判断是否是基础的数据类型,缺点是没办法区分object的细节。typeof ipt === 'number'
  • instanceof 从原型链上判断是否继承这个构造函数的实例,缺点是只能判断出来引用类型 ipt instanceof Array
  • Object.prototype.toString.call() 精准判断 Object.prototype.toString.call(ipt) === '[object Array]'
  • constructor 判断其构造函数,但是null和undefined没有constructor属性,同时这个属性可能被篡改 ipt.constructor === Array
  • Array.isArray 判断是否是数组(最精准的判断是否是数组的方法)

11. 什么是闭包?闭包有什么作用?请举例说明。

一个函数内部封装另外的函数和变量,并且内部函数可以访问外部函数的变量,由此外部函数内部形成封闭的私有作用域,变量会被缓存和私有化,在外部函数被垃圾回收前,都不会被垃圾回收掉

function counter() {
    let count = 0;
    return function () {
        return ++count;
    };
}
let myCounter = counter();
console.log(myCounter()); // 输出 1
console.log(myCounter()); // 输出 2

12. 前端安全相关的问题,XSS和CSRF攻击,如何防范

  • 跨站脚本攻击XSS:

攻击者将危险的代码注入到网页中,当其他用户访问该网页时,恶意脚本会被执行,致使用户Cookie等信息被窃取。

常见攻击场景:用户输入、富文本编辑器等

常见防范手段:对于用户输入内容进行HTML转义;避免直接操作innerHTML等操作,改用innerText等;将部分数据,比如Cookie在服务器NGINX设置为HttpOnly

  • 跨站请求伪造csrf:

攻击者冒充用户发起请求,窃取信息、资料以及转账等等。因为csrf涉及到token等知识,所以后续会单独整理一期。

13. localStorage,sessionStorage和cookie的区别以及应用场景

  • cookie 内存很小,有效期可以自己设定,不设定有效期的话会话关闭会自动删除,会随着请求一起发给后端,同源共享,可以通过document.cookie访问,也可以设置HTTPOnly禁止js访问

  • localstorage 内存5MB,相对偏大,有效期永久,不会随请求发给后端,同源共享,打开两个相同页面,修改其中一个页面localstorage增加一个key-value,另一个页面会自动在localstorage下面增加该信息数据,通过localstorage.getItem等api获取数据,没办法设置httpOnly

  • sessionStorage 内存5MB, 标签页关闭就失效了,并且只在自己的标签下生效,打开两个相同页面,修改其中一个页面localstorage增加一个key-value,另一个页面的sessionstorage下面不会有该数据,通过sessionstorage.getItem等api获取数据,没办法设置httpOnly

14. react响应式原理

react的响应式原理主要依托于虚拟DOM和diff算法。

  • 虚拟DOM: 本质上是一串json字符串,用来替换真实DOM,setState变化变量后,会批量更新变量到虚拟DOM上,最后再一次性更新反馈在真实DOM上,减少浏览器的渲染次数,优化渲染性能。虚拟DOM包含的attrs有:tagName标识dom节点的标签;props标识属性,里面有className,id,src等等属性;有子节点为children
{
    // 标签名
    "tagName": "div",
    // 标签属性
    "props": {
        "className": "container"
    },
    // 子节点,这里包含两个子节点
    "children": [
        {
            "tagName": "h1",
            "props": {},
            "children": ["Hello, Virtual DOM"]
        },
        {
            "tagName": "p",
            "props": {},
            "children": ["This is an example of virtual DOM structure."]
        }
    ]
}
  • DIFF算法:react的diff算法是分层识别,同层先比较tag类别,如果不一致则整个更新;如果一致则比较其props,更新对应的属性;最后比较其children。

这也是为什么key对于react的性能优化十分重要,因为如果是同层数组div比较,react是从左到右比较,如果没有key,就会直接按照顺序比较,即使原div没有发生变化,只是数组内顺序发生变化,也会导致div被删除更新;而如果被标记了key,就会在diff算法中定位到对应的dom节点位置,对比更新前后dom是否变化

15. flex弹性布局

首先设置display:flex开启弹性布局

  • flex-direction 设置弹性布局的方向,比如column和row
  • justify-content 设置水平方向的布局,比如center,flex-end,space-between等等
  • align-items 设置垂直方向的布局,参数和justify-content是一致的
  • flex-wrap 是否开启换行,比如wrap和no-wrap
  • flex-shrink 收缩倍率,取值0-1
  • flex-grow 拉伸倍率,取值0-1
  • flex-basis 拉伸伸缩规则,取值有0 和 auto,0是可以拉伸内部的宽高比例适应外部空间;auto是按照宽高倍率等比拉伸空间;所以flex:1本质上是 flex-shrink:1 flex-grow:1 flex-basis:0

16. JS事件循环机制

JS事件循环主要是解决JS作为单线程语言,处理密集浏览器任务而提出来的机制;防止耗时的异步任务阻塞导致浏览器后续同步任务都被卡住。

任务队列包含同步任务队列,微任务队列以及宏任务队列。

浏览器会先执行同步任务,中途遇到宏任务和微任务,都会先放入对应的任务队列中,等到同步任务执行完,再去微任务队列中将微任务清空,最后清空宏任务,如果执行宏任务的中途插入微任务,会优先执行插入的微任务,完成后再继续执行接下来的宏任务。

常见的宏任务有:setTimeout/setInterval,I/O操作,script标签等等

常见的微任务有:promise.then / MutationObserver等等

17. async和await是宏任务还是微任务?

都不是,async和await是语法糖,宏任务还是微任务,取决于他“拦截”的是宏任务还是微任务,await会拦截之后的代码,直到await执行完,才会继续执行后续的结果

const func = async()=>{
    const promise = Promise.resolved("resolved")
    const res = await promise
    console.log(0, res)
}
func()
console.log(1)
//输出顺序:1->0,res

18. 谈谈对于前端工程化的理解

工程化本质的目的是实现更高效,更好管理,更加风险可控,团队风格一致的代码方案。

  • 框架技术选型:react/vue,优先采用团队成员更熟悉的现代响应式框架
  • 前端编码语言:ts,作为类型限制语言更加安全规范
  • 组件化和模块化的代码构建
  • 代码自动检测库,与语法格式化库:eslint/prettier
  • 打包构建工具:webpack/vite
  • 远程版本代码管理仓:github
  • CICD流水线打包管理:jenkins

19. 在React Hook中useReducer和useState有什么区别

  • useState适用于简单的状态管理
import React, { useState } from'react';

const Counter = () => {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
};
  • useReducer适用于复杂的状态更新逻辑
import React, { useReducer } from'react';

const initialState = { count: 0 };

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

const Counter = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
    );
};

20. vue中的keep-alive组件的作用,以及原理和使用场景

keep-alive是组件缓存,被缓存的组件不会被销毁,而是会处于deactivited的失活状态,再次进入组件会activited激活组件状态,而不是重新创建组件挂载。一般会设置includes和excludes判断包裹的组件哪些需要被缓存,哪些不需要缓存,同时会设置max参数,避免缓存的组件数量太多,影响浏览器性能,一般缓存的组件都会控制在最多3个左右。

本质上是keep-alive会内部使用闭包缓存组件实例。

常见场景,比如小说浏览器,希望切到外面再切进来的时候还是定位在之前的段落,而不是回到顶层。

21. 在react中,什么是高阶组件?请举例说明其使用场景和实现方式

高阶组件本质上是一个函数,它接收一个组件作为参数,并返回一个新的组件。在hooks提出前使用偏多,但现在主流使用还是hooks,因为高阶组件是组件嵌套组件,在功能复用以及代码迭代上都很困难。

import React from'react';

const withLogging = (WrappedComponent) => {
    return (props) => {
        console.log('Component will render');
        return <WrappedComponent {...props} />;
    };
};
const MyComponent = () => {
    return <div>Hello, World!</div>;
};
const LoggedComponent = withLogging(MyComponent);

export default LoggedComponent;

22. 说说react的fiber机制

fiber主要解决的问题是:在提出fiber机制之前,整个组件去执行浏览器的diff,更新,渲染过程,不会中间中断,这就导致如果这个过程耗时很长,浏览器就会被卡死在这个流程。

而fiber机制,支持整个组件更新过程以更小的链表单位标记effectTag,中途可以中断停止,插入执行更高优先级的任务。

过程使用scheduler调度器,会分析当前任务的优先级顺序以及判断现在浏览器是否有空闲,比如如果更新中途出现用户的交互任务,会判断当前任务的优先级较高,先暂停缓存当前diff标记进度,优先执行高优先级的任务,等到执行完,浏览器空闲下来,再从缓存中继续执行更新流程。

等到currentTree虚拟DOM变更节点全部标记完再整个更新workProgress工作树,最后依据工作树更新到真实DOM上,最后工作树更新到虚拟DOM这个过程则是不可中断的。这是Fiber的双缓存树

为什么说react即使有了fiber机制还是比vue的更新效率更差,因为vue是精准定位到响应式变量,react的最小更新单元只能定位到DOM节点上,比如一个div节点,而不是div节点内部的某个变量参数。

最后:fiber使用的是深度优先的先序遍历。先找到最左子树,再根节点再右子树,遍历完一颗子树后,再溯源到其他分支。

23. 说说Javascript的原型链

每个Javascript的对象都有自己的proto属性,只想其构造函数的prototype,然后其构造函数也有自己的proto属性,向上指向对应的prototype,最后层层向上,指向null,这样的链式结构就叫做原型链。

原型链的作用是什么? 实现继承作用,让子类实例能够共享父类原型上的方法和属性。比如如果一个对象的属性或方法,如果自身没有,就会沿着原型链向上查找。

24. ES6的class继承和原型链的继承有什么区别?

25. 说说浏览器的渲染过程

  1. 首先是DNS解析阶段会根据DNS解析到对应的域名地址,然后这个DNS的域名地址会缓存下来,方便下一次请求的时候直接走缓存,跳过搜索解析阶段
  2. 接着进入到下一步的TCP阶段开启三次握手,这个过程,请求客户端会向服务端发送一段报文和一个标记码,服务器返回对应的报文和标记码表示自己能接收到信息,客户端再发送对应的通信开启的标识信息开始通信正式建立。

至于为什么是三次握手而不是两次握手,是因为需要确认双方都能接收到消息。

  1. 接着进入到HTTP通信阶段,获取对应的HTML,CSS,JS资源,然后构建DOMTREE和CSSTREE,两者结合生成渲染树。同时JS代码更新渲染树,进行重绘和重排完成最终的布局。

image.png

27. watch和created谁先执行

看immediate是否设置为true,如果是则是watch先执行,然后再执行created,如果不是则是先created再执行watch

因为watch注册监听器的时间节点是create时间,这个时候是初始化参数变量,然后再created,然后数据变化后,再执行watch的回调参数。

28. 在 Vue 里面挂载全局方法?

  • vue2选项式API的方法优先是Vue.prototype上挂载全局方法,组内直接用this.FUNCNAME访问该方法
// main.js(Vue2)
// 1. 定义并挂载全局方法(前缀加$,符合Vue内置API规范)
Vue.prototype.$myToast = function (msg, type = 'info') {
  alert(`[${type}] ${msg}`);
};

// 2. 挂载全局工具函数(比如格式化时间)
Vue.prototype.$formatTime = function (timestamp) {
  return new Date(timestamp).toLocaleString();
};

// 任意组件中使用
<template>
  <button @click="callGlobalMethod">调用全局方法</button>
</template>
<script>
export default {
  methods: {
    callGlobalMethod() {
      // 直接通过this调用全局方法
      this.$myToast('操作成功', 'success');
      console.log(this.$formatTime(Date.now()));
    }
  }
}
</script>
  • vue3实现全局方法的主流是在main.ts或者APP.vue上使用provide/inject依赖注入
  • vue3兼容的选项式方法是app.config.globalProperties,然后通过getCurrentInstance拿到vue3的实例,调用instance.appContext.config.globalProperties使用
// main.js(Vue3)
const app = createApp(App);

// 挂载全局方法
app.config.globalProperties.$myToast = (msg) => {
  alert(`全局提示:${msg}`);
};

app.mount('#app');

// 场景2:setup语法中使用
<script setup>
import { getCurrentInstance } from 'vue'

// 获取组件实例,拿到全局方法
const instance = getCurrentInstance();
const $myToast = instance.appContext.config.globalProperties.$myToast;

const showToast = () => {
  $myToast('Setup中调用全局方法');
};
</script>

29. Vue怎么强制刷新组件?

  • 通过组件外部绑定修改Key
<template>
  <!-- 给需要刷新的组件绑定动态key -->
  <MyTargetComponent :key="refreshKey" />
  <button @click="refreshComponent">强制刷新组件</button>
</template>

<script setup>
import { ref } from 'vue'
import MyTargetComponent from './MyTargetComponent.vue'

// 定义刷新标识,初始值为0
const refreshKey = ref(0);

// 刷新方法:仅需修改key值
const refreshComponent = () => {
  refreshKey.value++; // 每次+1,触发组件重建
};
</script>
  • 绑定v-if=mainShow,配合nextTick改变mainShow的值两次(我们组的项目使用的方法)
<template>
  <MyTargetComponent v-if="isComponentShow" />
  <button @click="refreshComponent">强制刷新</button>
</template>

<script setup>
import { ref, nextTick } from 'vue'
import MyTargetComponent from './MyTargetComponent.vue'

const isComponentShow = ref(true);

const refreshComponent = async () => {
  // 先销毁组件
  isComponentShow.value = false;
  // 等待DOM更新后重建
  await nextTick()
  isComponentShow.value = true;
};
</script>

30. React Hooks的核心原理

函数组件最开始的痛点是没办法持久化响应式变量的状态,Hooks的作用是提供这个能力。

  1. 闭包变量缓存;
  2. 提供fiber节点setState和useEffect等
  3. 闭包里面维护对应fiber节点的一个链表,这个链表因为闭包被状态持久化保存在函数组件内部,由此实现状态持久化管理能力。

这也是为什么fiber节点必须按照顺序写在最上面,因为对于函数组件的内部管理的是fiber节点构成的链表。

Hook 触发更新后,React 会标记对应的 Fiber 节点为「待更新」,通过调度器根据当前事件的优先级,安排组件重渲染,重渲染时按顺序遍历 整个Hook 链表,同步最新状态并执行副作用。这也是为什么react需要使用缓存等方式优化渲染性能,因为react是以组件为单位进行重渲染的。

31. hooks的闭包,为什么拿不到最新的值

因为react hooks实际上拿到的是数据当时生命周期时间节点的数据快照,如果需要摆脱快照状态,需要增加数据依赖更新

比如以下这个问题:

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

function Counter() {
  const [count, setCount] = useState(0);

  // 场景1:useEffect 里的定时器(最常见)
  useEffect(() => {
    const timer = setInterval(() => {
      // 期望:每秒打印最新的 count,但实际一直打印 0
      console.log('定时器中的count:', count);
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 依赖为空,只在首次挂载执行

  // 场景2:useCallback 包裹的回调(点击时拿不到最新值)
  const handleClick = useCallback(() => {
    // 点击按钮时,期望打印最新 count,但实际打印的是绑定回调时的 count
    console.log('点击时的count:', count);
  }, []); // 依赖为空,回调只创建一次

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>加1</button>
      <button onClick={handleClick}>打印count</button>
    </div>
  );
}

处理办法是添加详细的依赖

// 修复 useEffect 定时器
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器中的count:', count); // 能拿到最新值
  }, 1000);
  return () => clearInterval(timer);
}, [count]); // 依赖 count,count更新时重新创建定时器

// 修复 useCallback 回调
const handleClick = useCallback(() => {
  console.log('点击时的count:', count); // 能拿到最新值
}, [count]); // 依赖 count,count更新时重新创建回调

或者拿useRef保存最新值(不常用),useRef的作用是创建一个可变的引用容器,容器具备全局上下文信息,一般useRed的使用场景和vue的dom节点的this.$ref一样,获取组件实体

function Counter() {
  const [count, setCount] = useState(0);
  // 用 ref 保存最新的 count
  const countRef = useRef(count);
  
  // 每次 count 更新时,同步更新 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  // 定时器闭包访问 ref.current(最新值)
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('定时器中的count:', countRef.current); // 始终拿到最新值
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 依赖为空,只创建一次定时器

  // ... 其他代码
}

32. 路由懒加载的原理

利用闭包的原理,ES模块的()=>import('./Home')异步加载模块,会让组件在打包的时候单独打包成chunk文件,这样子在只有真的执行到这行代码的时候,才会从服务器获取这个chunk文件,由此实现的路由懒加载。

33. 常见react hooks的常用场景

  1. useRed的使用场景和vue的dom节点的this.$ref一样,获取组件实体

  2. useCallback和useMemo的常用场景:

(1)因为引用变量地址的原因,使用数组作为useEffect的依赖时,使用useMemo缓存对应的数组;同样如果是函数作为useEffect的依赖时,使用useCallback作为依赖。

function DataFetch() {
  const [id, setId] = useState(1);

  // 缓存请求函数,只有 id 变化时才重新创建
  const fetchData = useCallback(async () => {
    const res = await fetch(`/api/data?id=${id}`);
    const data = await res.json();
    console.log('请求结果:', data);
  }, [id]); // 依赖 id,id 变才重新创建函数

  // 依赖 fetchData,只有 fetchData 引用变(即 id 变)时,才重新请求
  useEffect(() => {
    fetchData();
  }, [fetchData]); // 若不用 useCallback,每次渲染 fetchData 引用变,会重复请求

  return (
    <div>
      <button onClick={() => setId(id + 1)}>修改id,重新请求</button>
    </div>
  );
}

(2)传递给子组件的回调函数,用 useCallback 缓存函数引用,只有依赖变化时才更新引用,子组件才会真正重渲染。

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三');

  // 优化写法:用 useCallback 缓存函数引用,只有 name 变化时,函数引用才变
  const handleClick = useCallback(() => {
    console.log('点击了', name);
  }, [name]); // 依赖数组:只有 name 变,函数才重新创建

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>修改count</button>
      <button onClick={() => setName('李四')}>修改name</button>
      {/* 点击「修改count」时,handleClick 引用不变,ChildButton 不重渲染;
          点击「修改name」时,handleClick 引用变,ChildButton 重渲染 */}
      <ChildButton onClick={handleClick} text="子组件按钮" />
    </div>
  );
}

(3)防抖节流函数需要使用useCallback;防抖 / 节流函数需要同一个函数引用才能生效(比如防抖函数要记录上次触发时间),若每次渲染都创建新的防抖函数,会导致防抖失效(每次都是新函数,无法记录时间)

function SearchInput() {
  const [keyword, setKeyword] = useState('');

  // 缓存防抖函数,只有组件卸载才会重新创建
  const handleSearch = useCallback(
    debounce((val) => {
      console.log('搜索:', val); // 输入停止 500ms 后执行
    }, 500),
    [] // 依赖为空,函数引用永久不变
  );

  const onChange = (e) => {
    setKeyword(e.target.value);
    handleSearch(e.target.value); // 调用缓存的防抖函数
  };

  return <input value={keyword} onChange={onChange} placeholder="搜索..." />;
}

(4)缓存昂贵的计算结果常使用用useMemo,比如大数据处理、复杂逻辑运算(如循环遍历上千条数据)、数学公式计算等,这些操作若每次渲染都执行,会严重消耗性能。

(5)缓存复杂静态的JSX 片段可以使用useMemo缓存

function ComplexJSX() {
  const [show, setShow] = useState(true);

  // 缓存复杂 JSX 片段,只有 show 变化时才重新创建
  const content = useMemo(() => {
    if (!show) return null;
    // 模拟复杂 JSX 结构
    return (
      <div className="complex-content">
        <h1>标题</h1>
        <p>内容1</p>
        <p>内容2</p>
        {/* ... 更多内容 */}
      </div>
    );
  }, [show]);

  return (
    <div>
      <button onClick={() => setShow(!show)}>切换显示</button>
      {content}
    </div>
  );
}

34. SameSite:Strict 和 None 有什么区别?

SameSite 是Cookie 的一个属性,用来控制跨站时是否携带 Cookie,防 CSRF。

  • SameSite=Strict(最严格),跨站请求完全不携带 Cookie,即使你从别的网站点链接进来,也不会带 Cookie,只允许同站使用,完全禁止跨站

  • SameSite=None,任何情况都可以携带 Cookie,必须配合 Secure(HTTPS),允许跨站携带

  • SameSite=Lax(现代浏览器默认),普通跨站请求不发,链接跳转、GET 表单会发

35. http1 http2 有什么区别?

  • http1.1: 1. 长连接connection:keep-alive请求响应头;2.只能支持最多6-8个TCP请求同时发送;3.纯文本协议,以文件为单位进行信息传输,一个TCP请求只能同时处理一个文件传输;4. 每次都会发送完整的Header信息,导致信息冗余
  • http2.0: 核心是二进制01字段帧的信息传送。1. 只开一个TCP请求,但是因为不再是文本传输协议,而是二进制传输,所以可以并发很多请求,不受到TCP数量的限制;实现多路复用,解决HTTP层的队首阻塞 2. 使用HPACK压缩Header字段,HPACK压缩本质上是前后端信息传输会维护一个缩写和全程的map,压缩使用缩写,再通过map转换得到最终的Header

36. 如何在原生事件中实现setState的批量更新,而不是立刻更新?

使用react提供的原生API ReactDOM.unstable_batchedUpdates 包裹原生事件的回调逻辑,强制开启批量更新模式。

handleNativeClick = () => {
    // 核心:用 unstable_batchedUpdates 包裹多次 setState
    ReactDOM.unstable_batchedUpdates(() => {
      // 多次 setState 会被批量合并,只触发一次重渲染
      this.setState({ count: this.state.count + 1 });
      console.log('第一次 setState 后:', this.state.count); // 输出 0(异步批量,未更新)
      
      this.setState({ count: this.state.count + 1 });
      console.log('第二次 setState 后:', this.state.count); // 输出 0(异步批量,未更新)
    });

    // 若想拿到更新后的状态,用 setState 回调
    this.setState(
      (prev) => ({ count: prev.count + 1 }),
      () => console.log('最终状态:', this.state.count) // 输出 2(合并两次更新)
    );
  };

37. JS的继承方式有哪些

  1. 原型链继承

Child函数的prototype指向父函数的实例,但是Child构造函数指向自己

缺点是一旦父函数修改部分属性,所有子属性都会被修改;并且创建初始化的时候没办法立刻改变继承的数据,必须后续再次赋值

function Parent(){
  this.name = "父类"
  this.hobbies = ["打球","看书"]
}
Parent.prototype.sayHi = function(){
  console.log(this.name)
}

function Child(){}
Child.prototype = new Parent()
Child.prototype.constructor = Child
  1. 构造函数继承

通过call改变this指向,能在初始化的时候进行赋值

缺点是无法继承父函数后续通过prototype添加的方法

function Parent(){
  this.name = "父类"
  this.hobbies = ["打球","看书"]
}
Parent.prototype.sayHi = function(){
  console.log(this.name)
}

function Child(name,age){
    //转移父函数的this指向,并且传值name
    Parent.call(this,name)
    this.age = age
}
  1. 组合继承(原型链+构造函数继承)

所有功能都能实现,只是继承这个动作会实现两次,但实际其实无感知

function Parent(name,age){
    this.name = name
    this.age = age
}
Parent.prototype.sayHi = function(){
    console.log(this.age)
}

function Child(name,hobbies){
    Parent.call(this,name)
    this.hobbies = ["学习"]
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
  1. 寄生继承

不用构造函数就可以实现继承,但是缺点还是子对象没办法在初始化的时候更改继承属性,父对象一旦修改属性,所有的子对象都会发生属性变更

function createChild(parent){
  const child = Object.create(parent)
  child.age = "18"

  child.sayAge = function(){
    console.log(this.age)
  }
  return child
}

const parent1 = {
  name:'父对象',
  sayHi:function(){
    console.log("hi",this.name)
  }
}

const child = createChild(parent1)
child.sayAge()//18
child.sayHi()//hi 父对象

说到这个题目,可以看看下面的题:

//题1
function createChild(parent){
  const child = Object.create(parent)
  child.age = "18"

  child.sayAge = function(){
    console.log(this.age)
  }
  return child
}

const parent1 = {
  name:'父对象',
  sayHi:()=>{
    console.log("hi",this.name)
  }
}

const child = createChild(parent1)
child.sayAge()//18
child.sayHi()//报错

//题2
function createChild(parent) {
  const child = Object.create(parent);
  child.age = "18";

  child.sayAge = function () {
    console.log(this.age);
  };
  return child;
}

const parent1 = {
  name: "父对象",
  sayHi: () => {
    let name = "中间值"
    return function () {
      console.log("hi", this.name);
    };
  },
};

const child = createChild(parent1);
child.sayAge();//18
child.sayHi()();//报错,没有找到name

//题3
function createChild(parent) {
  const child = Object.create(parent);
  child.age = "18";

  child.sayAge = function () {
    console.log(this.age);
  };
  return child;
}

const parent1 = {
  name: "父对象",
  sayHi: function(){
    let name = "中间值"
    return function () {
      console.log("hi", this.name);
    };
  },
};

const child = createChild(parent1);
child.sayAge();//18
child.sayHi()();//报错,没有找到name

const child2 = createChild(parent1);
child2.sayAge();//18
const res = child2.sayHi();
res()//报错,没有找到name

至于为什么改成非箭头函数而是普通函数还是显示没有找到,因为如果是单层普通函数,实际上是child.sayHi(),是child在调用,双层普通函数解构出来,是window.res()或者外部函数.res(),而外部函数没有name属性,所以才会报错。

  1. 组合寄生继承

最理想的继承方式,也是ES6 CLASS语法糖的底层实现。

function createChild(child,parent){
    const prototype = Object.create(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}

function Parent(name,age){
    this.name = "父类"
    this.age = 18
}
Parent.prototype.sayHi = function(){
    console.log(this.age)
}
function Child(name,hobbies){
    Parent.call(this,name)
    this.hobbies = []
}

createChild(Child,Parent)
  1. ES6 class继承

super关键字实际上封装的就是call的this指向,extends实际上是封装prototype的原型链指向

class Parent {
  constructor(name) {
    this.name = name;
    this.hobbies = ["打球", "看书"];
  }
  sayHi() {
    console.log(`Hi,我是${this.name}`);
  }
}

// 子类
class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用super,才能使用this
    this.age = age;
  }
  sayAge() {
    console.log(`我${this.age}岁`);
  }
}

38. useState 直接 setState 和 setState 传入一个函数做处理,这个两者的区别

直接传值的场景,一般是新值不依赖旧值,因为react的useState的机制是异步的,所以如果直接useStATE里面传值,会导致拿到的很有可能是旧值:

function Demo() {
  const [name, setName] = useState('张三');

  // 直接传值:新值是固定的,不依赖旧name
  const changeName = () => {
    setName('李四'); 
  };

  return (
    <div>
      <p>{name}</p>
      <button onClick={changeName}>改名字</button>
    </div>
  );
}

传入函数的场景:

function Counter() {
  const [count, setCount] = useState(0);

  // 点击后期望count从0→2,但实际只到1!
  const handleClickWrong = () => {
    // 两次setCount都捕获了闭包中旧的count=0,最终都是0+1=1
    setCount(count + 1); //1
    setCount(count + 1); //1
    //修正的方法
    //setCount(prev=>prev+1) //1
    //setCount(prev=>prev+1) //2
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClickWrong}>错误累加</button>
    </div>
  );
}

39. useEffect和useLayoutEffect的区别有哪些,应用场景有哪些

首先需要清楚react的组件更新过程:effect触发组件更新 => 计算新UI => DOM计算变更 => useLayoutEffect执行同步 => 浏览器绘制页面 => useEffect执行异步

所以对于耗时的DOM更新操作以及对于更新感受不明显,最好使用useEffect,避免长时间白屏没有反馈;对于闪烁高敏感的场景,最好是使用useLayoutEffect,比如某些追踪鼠标的提示tooltip。

image.png

40. react里面的useTranslation是做什么用的

用于国际化的一个函数。

根目录配置i18n.js文件

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend'; // 加载远程/本地翻译文件
import LanguageDetector from 'i18next-browser-languagedetector'; // 自动检测浏览器语言

i18n
  .use(Backend) // 加载翻译文件
  .use(LanguageDetector) // 自动检测语言(如浏览器默认语言)
  .use(initReactI18next) // 绑定到 react-i18next
  .init({
    fallbackLng: 'zh', // 兜底语言(检测不到时用)
    debug: false, // 开发时可设为true,控制台打印调试信息
    interpolation: {
      escapeValue: false, // React 已自带XSS防护,无需重复处理
    },
    react: {
      useSuspense: true, // 适配 React 18 的 Suspense(可选)
    },
  });

export default i18n;

创建翻译的json文件

//zh.json文件
{
  "common": {
    "submit": "提交",
    "welcome": "欢迎{{name}}来到我的网站"
  },
  "button": {
    "cancel": "取消"
  }
}
//en.json
{
  "common": {
    "submit": "Submit",
    "welcome": "Welcome {{name}} to my website"
  },
  "button": {
    "cancel": "Cancel"
  }
}

最终的组件使用文件

import React from 'react';
import { useTranslation } from 'react-i18next';

function MyComponent() {
  // 解构核心API:t(翻译函数)、i18n(语言管理实例)
  const { t, i18n } = useTranslation();

  // 切换语言的方法
  const changeLanguage = (lng) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      {/* 基础翻译:根据当前语言渲染对应文本 */}
      <button>{t('common.submit')}</button>
      <button>{t('button.cancel')}</button>

      {/* 带插值的翻译:动态替换文本中的变量 */}
      <p>{t('common.welcome', { name: '张三' })}</p>

      {/* 切换语言按钮 */}
      <button onClick={() => changeLanguage('zh')}>中文</button>
      <button onClick={() => changeLanguage('en')}>English</button>
    </div>
  );
}

export default MyComponent;

41. useTransition和useDeferredValue的区别是什么

都是用于标记任务优先级,只是useTransition标记的是需要延迟的函数;useDeferredValue标记的是延迟的依赖传入,因为依赖变更感知延迟,所以最终得到的结果也会自然延迟。

useTransition标记更新、

image.png

import { useState, useTransition } from 'react';

function Search() {
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition({ timeoutMs: 300 });

  // 输入框更新(高优先级,立即执行)
  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // 高优先级:输入框立即响应

    // 主动标记列表更新为低优先级
    startTransition(() => {
      // 耗时操作:生成10万条数据(模拟接口返回大量数据)
      const newList = Array(100000).fill(value);
      setList(newList); // 低优先级:等输入完成后再渲染
    });
  };

  return (
    <div>
      <input value={input} onChange={handleChange} placeholder="输入搜索" />
      {isPending && <div>加载中...</div>}
      <ul>
        {list.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

useDeferredValue 实现(被动派生延迟值)

image.png

import { useState, useDeferredValue } from 'react';

function Search() {
  const [input, setInput] = useState('');
  // 基于原input创建延迟值,最大延迟300ms
  const deferredInput = useDeferredValue(input, { timeoutMs: 300 });
  // 基于延迟值渲染耗时列表(低优先级)
  const list = Array(100000).fill(deferredInput);

  // 输入框更新(高优先级,立即执行)
  const handleChange = (e) => {
    setInput(e.target.value); // 原状态立即更新,输入框不卡顿
  };

  return (
    <div>
      <input value={input} onChange={handleChange} placeholder="输入搜索" />
      {/* 列表基于延迟值渲染,不会阻塞输入 */}
      <ul>
        {list.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

42. ts中的satisfies有什么用

声明一个变量满足一种类型的类型约束,同时保留这个变量推断出来的自身的特性

// 定义一个类型:颜色可以是字符串,或 RGB 数字数组
type Color = string | [number, number, number];

// 问题1:用 : 注解类型 → 约束了类型,但丢失具体字面量类型
const red: Color = '#ff0000';
// ❌ TS 无法推断 red 是具体的 '#ff0000',只能推断为 Color(string | [number,number,number])
// 比如想访问 red.length 会报错:因为 Color 可能是数组,TS 不确定
// red.length; // 报错:Property 'length' does not exist on type 'Color'

// 问题2:不用 : 注解 → 保留具体类型,但无法约束结构
const green = [0, 255, 0]; // TS 推断为 number[],而非 [number, number, number]
// ❌ 可以随意修改数组长度,破坏约束
green.push(100); // 数组变成 [0,255,0,100],不符合 Color 类型,但 TS 不报错


type Color2 = string | [number, number, number];

// ✅ satisfies 做两件事:
// 1. 校验 '#ff0000' 符合 Color2 类型(约束);
// 2. 保留 red2 的具体类型为 '#ff0000'(而非 Color2)。
const red2 = '#ff0000' satisfies Color2;
// ✅ TS 知道 red2 是具体的字符串 '#ff0000',可以安全访问 length
console.log(red2.length); // 输出 7

// ✅ 校验 [0,255,0] 符合 Color2 类型,且保留具体类型为 [0,255,0](元组)
const green2 = [0, 255, 0] satisfies Color2;
// ❌ 尝试修改数组长度会报错:因为 green2 是具体的元组 [0,255,0],长度固定
// green2.push(100); // 报错:Argument of type '100' is not assignable to parameter of type 'never'

// ❌ 如果值不符合 Color 类型,satisfies 会直接报错
// const blue2 = [0, 0] satisfies Color2; // 报错:长度为2的数组不符合 Color2 约束

// 定义配置类型:theme 只能是 light/dark,size 只能是 sm/md/lg
type ComponentConfig = {
  theme: 'light' | 'dark';
  size: 'sm' | 'md' | 'lg';
  disabled?: boolean;
};

// ✅ satisfies 约束对象符合 ComponentConfig,同时保留每个值的具体类型
const buttonConfig = {
  theme: 'dark',
  size: 'md',
  disabled: false
} satisfies ComponentConfig;

// ✅ TS 知道 buttonConfig.theme 是具体的 'dark',而非 'light'|'dark'
if (buttonConfig.theme === 'dark') {
  console.log('暗黑模式'); // 类型安全,无报错
}

// ❌ 如果对象结构不符合,会报错
// const inputConfig = {
//   theme: 'red', // 报错:'red' 不符合 'light'|'dark'
//   size: 'md'
// } satisfies ComponentConfig;

43. ts 的 enum、const enum 的区别

  1. enum 编译后会生成一个索引对象,所以可以支持反向取值
// 编译前
enum Status {
  Success = 200,
  Error = 500,
  NotFound = 404
}

const code = Status.Success;
console.log(Status.Error);

// 编译后
var Status;
(function (Status) {
    Status[Status["Success"] = 200] = "Success";
    Status[Status["Error"] = 500] = "Error";
    Status[Status["NotFound"] = 404] = "NotFound";
})(Status || (Status = {}));

var code = Status.Success;
console.log(Status.Error);

//因此可以反向取值,当做对象索引使用
function getStatusText(code: number): string {
  return StatusCode[code] || 'Unknown';
}

console.log(getStatusText(200));
  1. const enum编译后会在使用的地方自动替换成枚举之后的数据
//编译前
const enum Status {
  Success = 200,
  Error = 500,
  NotFound = 404
}

const code = Status.Success;
console.log(Status.Error);

//编译后,不会生成枚举对象,无法反向索引,但是编译出来的结果更加轻量
var code = 200 /* Success */;
console.log(500 /* Error */);

44.如何实现动态组件

component 和 :is相结合,动态输入需要绑定展示的组件