面试集锦

34 阅读10分钟

flex布局,怎么让文字竖起来?

  • 设置
flex-direction: column

useContext和redux有什么区别?

  • useContext 管理的状态更新,下面的子组件全部更新,即使子组件没有订阅它的状态;
  • redux 也是管理全局状态,但是能够精准的让订阅的组件更新;
  • useContext 会带来性能问题;

useRef.current能被useEffect监听到吗?

  • 不会。
  • useEffect 只会遍历那些已经注册的状态,或者是props的变更;

nodejs中的token怎么生成?

const jwt = require('jsonwebtoken');

const payload = { userId: 123, username: 'alice', jti: uuidv4()}; // 放用户信息
const secret = 'your_jwt_secret_key'; // 建议用环境变量管理
const options = {
  expiresIn: '7d' // 例如 7 天过期
};

const token = jwt.sign(payload, secret, options);

后端使用nodejs,怎么主动销毁token?

  • 比如当前用户重复登录,然后就可以将token放到redis的黑名单中,并且设置过期时间;
    • 查找token可以根据 jti 去查找;
  • 过期时间是根据剩余时间计算的;
  • 或者用户点击“登出”,那么就在redis中销毁token;

GET请求中可以带body吗?

  • 可以带,但是不能这么做;
  • express中如果放了中间件强行解析,自己编写的逻辑也会把它忽略掉;
  • GET是幂等性的。
    • 幂等,就是依次执行不会引起其它的变化。这个只是查询数据;
  • POST是非幂等性的。
    • 新增数据会引起其它的变化。

判断300ms之内的点击是单击还是双击

import React, { useRef } from "react";

/**
 * 1、区分一下单击和双击
 * 2、300ms以内点击一次就是单击,300ms以内点击两次就是双击。
 * 3、这两个状态互斥
 */
export default function index() {
  const count = useRef(0);
  const clock = useRef(null);

  const handlerClickV3 = () => {
    count.current = count.current + 1;

    if (clock.current) {
      clearTimeout(clock.current);
    }

    clock = setTimeout(() => {
      // 用户点击一次
      if (count.current === 1) {
        console.log("单击");
      } else if (count.current === 2) {
        console.log("双击");
      }
      count.current = 0;
    }, 300);
  };

  return (
    <>
      <div>
        <button onClick={handlerClickV3}></button>
      </div>
    </>
  );
}

箭头函数和普通函数

  • 如果没有使用 严格模式 strict mode 那么箭头函数的this指向undefined。如果是严格模式,那么箭头函数的this指向window或者服务器的nodejs
  • 普通函数的this指向Object。例如:
const obj = {
    name: "张三",
    doSometings: function(){}
}
  • 这里的 doSomtings 的this指向obj
function name() {
  'use strict';
  function age() {
    console.log(this); // undefined
  }
  age();
}
name();
  • 这里的age函数已经是严格模式,那么this指向undefined。
  • name函数指向windows或者nodejs环境。

箭头函数和普通函数的参数

  • 箭头函数没有arguments,但是可以使用 ...rest 指代剩下的参数
  • 普通函数有arguments

typescript 中的interface和type有什么区别?

  • 能用interface的时候就用
  • type主要是给类型另外起一个别名
  • interface能够融合在一起
interface Person {
    name: string
    age: number
}
interface Person {
    weight: number
}
  • 如果代码块中定义了同名的interface,那么会自动把他们两个 结合起来
  • type就没法结合合并类型
  • 但是type可以做基本类型的联合类型
type unionTypeCustom = number | string
  • type 能做到,但是 interface 做不到的
// 联合类型
type Status = 'idle' | 'loading' | 'success' | 'error';

// 元组
type Point = [number, number];

// 基于其他类型的别名
type ID = string | number;

// 使用泛型 + 条件类型
type NonNullable<T> = T extends null | undefined ? never : T;

// 映射类型
type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };
  • 继承 上的写法差异
// interface extends
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }

// type 使用交叉类型
type Animal = { name: string };
type Dog = Animal & { breed: string };
  • type 也可以约束变量的值
type Status = "1" | "2"

const status: Status = "3" // 这里会报错的

下面是AI给出的 映射类型 的案例:

这段代码:

type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };

是 TypeScript 中 映射类型(Mapped Types) 的一个经典例子。它的作用是:基于已有的类型 Person,创建一个新类型,其中所有属性都变成只读(readonly)


🌟 核心概念拆解

{ readonly [K in keyof Person]: Person[K] }
  • keyof Person:获取 Person 所有属性名的联合类型(比如 "name" | "age")。
  • [K in keyof Person]:遍历这些属性名(类似 for-in 循环)。
  • Person[K]:获取 Person 中属性 K 对应的类型(索引访问类型)。
  • readonly:给每个新属性加上只读修饰符。
  • 整体:创建一个新对象类型,结构和 Person 一样,但所有属性只读

💡 这其实就是内置工具类型 Readonly<T> 的实现原理!


✅ 具体代码示例

1. 定义原始类型
interface Person {
  name: string;
  age: number;
  email: string;
}
2. 使用映射类型创建只读版本
type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };

等价于手动写:

type ReadonlyPerson = {
  readonly name: string;
  readonly age: number;
  readonly email: string;
};
3. 使用效果
const person: ReadonlyPerson = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

// 尝试修改属性 → ❌ 编译错误!
person.age = 31; // Cannot assign to 'age' because it is a read-only property.

🔧 实用变体(扩展)

✅ 内置工具类型

TypeScript 已经内置了 Readonly<T>,所以你通常可以直接写:

type ReadonlyPerson = Readonly<Person>;
🛠 自定义映射类型:可选变必填、只读变可写等
// 所有属性变为可选
type OptionalPerson = { [K in keyof Person]?: Person[K] };

// 所有属性变为可写(移除 readonly)
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type MutablePerson = Mutable<Readonly<Person>>;

// 所有属性变为只读 + 可选
type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] };

注意 -readonly 是移除 readonly 修饰符的语法(类似 ?-? 移除可选)。K


📌 什么时候用映射类型?

  • 需要基于现有类型“批量修改”属性(加 readonly?、改变类型等)。
  • 编写通用工具类型(如 Partial<T>, Pick<T, K>, Record<K, T> 等底层都用映射类型实现)。
  • 减少重复代码,提高类型安全性。

✅ 总结

你的例子:

type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };

→ 是 映射类型 的典型应用,用于将任意对象类型的所有属性变为只读,在状态管理(如 Redux/Vuex)或不可变数据场景中非常有用。

如果你正在用 Vue + TypeScript,这类工具类型在定义 store state(防意外修改)时尤其实用。

ts中的never

  • 这是一个 类型, 表示不会存在的类型
  • 被 never 修饰的变量,不能赋值给任何变量,只能赋值给同样用 never 修饰的变量
  • 和void的比较:void表示这个函数 能够正常结束 并且不会返回值。但是never表示这个函数不会正常结束,没有返回值。
  • 下面是naver在 穷尽检查 ,也就是 switch 中的应用:
type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape) {
  switch (shape) {
    case 'circle':
      return Math.PI;
    case 'square':
      return 1;
    case 'triangle':
      return 0.5;
    default:
      // 如果 Shape 新增了类型但没处理,这里会报错
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck; // ✅ 正常编译通过
  }
}

函数参数的 any 和 unknown

function myFunc(param: unknown) {
  (param as number[]).forEach((element) => {
    element = element + 1;
  });
}

function myFunc(param: unknown) {
  (param as unknown[]).forEach((element) => {
    element = (element as number) + 1;
  });
}

  • 基本上用于数组的复杂类型 断言
  • 如果数组里面传递了各种类型的object,就使用这个 unknown
  • 如果使用 any ,那么表示放弃了类型的检查

as 的使用

interface IUser {
  name: string;
  job?: IJob;
}

interface IJob {
  title: string;
}

const user: IUser = {
  name: 'foo',
  job: {
    title: 'bar',
  },
};

const { name, job = {} } = user;

const { title } = job; // 类型“{}”上不存在属性“title”,这里会报错

  • 要解决这个问题,应该使用 as
const { name, job = {} as IJob } = user;

const { title } = job;

  • 由于我们在第一次解构赋值时,为 job 提供了一个空对象作为默认值,TypeScript 会认为此时 job 的类型就是一个空对象,所以我们在第二次解构赋值时,就无法从 job 上获得 title

联合类型 | 和 交叉类型 $

type statusOne = 'string' | 'number' // 联合类型,并且值的类型只能是这两个基础类型中的任何一个

type statusTwo = 'string' & 'number' // 这是一个 交叉类型 ,但是这个类型实际上是 never 。表示:不可能存在的值

interface Person {
    name: string;
    age: number;
}

interface User {
    age: number;
}

type statusThree = Person & User // 这是一个 交叉类型, 这里的最终类型是 “name:string age: number”
  • 交叉类型。表示同时拥有两个类型的所有属性
  • 如果相同的属性的类型不同,那么就会返回never

interface Person {
    name: string;
    age: number;
}

interface User {
    age: string;
}

type statusThree = Person & User // 这是 never
  • 注意这里的 age 的类型是不同的。

vue2和vue3的响应机制有什么区别?

  • vue2 是用的 Object.defineProperty 做的双向数据绑定。
  • vue3 是用的 proxy,加上 reflect 元数据 、 track 记录订阅 和 trigger 触发订阅事件
function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key); // 依赖收集(Vue 的逻辑)当前的 effect函数,盯着 obj[key]
      return Reflect.get(obj, key, receiver);
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver);
      // 通知 所有盯着 obj[key] 的effect函数运行,这个effect大概率是放在value中。
      // 而且trigger会去Proxy中的元数据中找到 key 对应的value,也就是effect函数
      trigger(obj, key); 
      return result; // 保持 set 行为
    },
    deleteProperty(obj, key) {
      const hadKey = Object.hasOwn(obj, key);
      const result = Reflect.deleteProperty(obj, key);
      if (hadKey && result) {
        trigger(obj, key); // 删除属性也应触发更新
      }
      return result;
    }
  });
}
  • 像这个reactive,那么如果我这样做:const obj = reactive({name:12, info:{address:"南宁"}),如果我设置 obj.info.address = '柳州' 就是触发了set对吧?那么这里的 Reflect.set 会不会设置info中的value呢?
    • 这里涉及到 懒响应 的机制
    • 如果 获取了 info 中的数据,这时候 vue3 才会使用 reactive 对info实现代理
    • 当重新设置 info.address 的值的时候,才会触发更新。
  • 总结:
    • 1、为什么要在obj的外面再加上一层 weakMap ?
    • 2、因为,如果不加这一层,那么数据变化的时候谁来响应?我首先想到的就是在当前obj的后面加上一个响应函数。那么只要我修改了obj中的某个数据,那么我就要手动的去执行这个响应函数,这时候维护成本就抬高了,而且有可能出现同名函数。
    • 3、现在由于js中没有 针对属性改变之后的响应函数,那么就只能在外面加上一层 weakMap。
    • 4、明白1/2/3点之后,就可以明白 this 的指向问题了。由于不能在当前obj后面加上响应函数,this指向响应函数就没有意义。但是 我又想自动执行,跟当前的obj相关的响应函数,怎么办?
    • 5、这时候就需要 Proxy 上场。因为Proxy给每一层的obj都添加到了 WeakMap, 尤其是 get返回的是Proxy的当前层obj的 WeakMap 值,那么就可以在 get/set 中,通过 track/trigger 这两个函数去找到 Proxy的当前层obj的 WeakMap 对应的响应函数。
    • 6、还好我手动敲了 Nest.js 的代码 ,知道 元数据和元编程。虽然 vue3 中的Proxy没有使用元数据,但是道理是相通的,不然看vue3原理的时候真的不知道怎么着手。

针对react和vue的,永恒不变的讨论

  • 1、看上一点的总结
  • 2、react是使用 fiber 引擎,使用diff算法去操作虚拟DOM的,性能肯定是比不过vue3的Proxy了。vue3是用ES6原生的Proxy语法去决定哪个节点的虚拟DOM要更新。react是根据批处理去更新的,当然有人会说react的性能不好。
    • 实际上react在处理复杂表单的时候性能比vue更好
  • 3、但是从 平台适用性 来说,vue2/vue3 又比不过 react。vue3不能在IE11之前运行,因为IE11之前的浏览器不支持proxy语法。万幸的是IE系列浏览器写得足够烂,没人用。我们程序员不用针对这些垃圾浏览器做兼容,拯救了两斤头发!
  • 4、react有fiber引擎各种平台无所谓,爱怎么跑怎么跑,就像JVM那样。虽然性能差了点,但是能跑啊!
    • vue虽然也有UNIAPP,但是生态上比不过react。

useEffect 中异步请求数据,能不能写成

useEffect(async()=>{
    await fetch(url);
},[])
  • 不能。因为useEffect的第一个参数要求的都是同步函数
  • 可以这样定义
useEffect(()=>{
    const fetchData = async ()=>{
        await fetch(url);
    }
    
    fetchData()
},[])

useState 在react18中和react18之后,对比react18之前,有什么变化

const [count, useCount] = useState(0);

useEffect(()=>{
    useCount(prev=>prev+1)
    useCount(prev=>prev+1)
},[])

setTimeout(()=>{
    useCount(prev=>prev+1)
    useCount(prev=>prev+1)
}, 500)
  • react17
    • useEffect中的都会合并到一个批处理中,只会触发一次重新渲染;
    • 但是setTimeout这个不是react控制的,会触发两次;
  • react18
    • 所有的都会合并到一次批处理,并且只触发一次重新渲染;