使用ChatGPT汇总了十道手撕高频题

1,608 阅读12分钟

嗨!我是日渐圆润的团子,大家好久不见~

在前端的技术面试中,手撕题是一个非常重要的环节。手撕题不仅能考察面试者的编程水平,还能考察面试者的思考方式和解决问题的能力。

因此,今天就来分享一下借助ChatGPT为大家整理的十道前端高频手撕题

1. 深拷贝

所谓深拷贝,指的是创建一个新的对象,将原始对象中的所有属性和嵌套的对象完全复制到新对象中。

新对象与原对象在内存中完全独立。在新对象中修改属性不会影响原始对象。

与之相对的,浅拷贝只复制对象的引用,而不是对象本身,因此修改新对象中的属性可能会影响原始对象。

下列展示了使用ts实现深拷贝的代码示例:

function deepCopy<T>(obj: T): T {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  let result: any;
  if (Array.isArray(obj)) {
    result = [];
    for (let i = 0; i < obj.length; i++) {
      result[i] = deepCopy(obj[i]);
    }
  } else {
    result = {};
    for (let key in obj) {
      result[key] = deepCopy(obj[key]);
    }
  }

  return result;
}

在上述代码中,deepCopy函数接受一个泛型参数 T,用于指定待拷贝对象的类型。

在函数中,首先判断待拷贝对象是否为基本类型或 null,如果是,则直接返回原对象。否则,根据对象类型创建一个新对象,并递归拷贝原对象的每个属性值到新对象中。

该函数使用了递归算法来实现深拷贝,对于复杂对象或嵌套对象,也能正确地进行拷贝。

需要注意的是,对于循环引用的对象,上述写法可能会陷入死循环,因此需要特别处理循环引用的情况。

下列代码可以正确处理循环引用问题:

function deepCopy<T>(obj: T, hash = new WeakMap()): T {
  if (Object(obj) !== obj) return obj; // 原始类型直接返回
  if (obj instanceof RegExp) return new RegExp(obj); // 正则类型特殊处理
  if (obj instanceof Date) return new Date(obj); // 日期类型特殊处理
  if (hash.has(obj)) return hash.get(obj); // 处理循环引用
  const cloneObj = Array.isArray(obj) ? [] : {}; // 创建一个新的对象或数组
  hash.set(obj, cloneObj); // 存储原对象和新对象的对应关系
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepCopy(obj[key], hash); // 递归拷贝对象的属性
    }
  }
  return cloneObj as T;
}

在上述代码中,使用了 WeakMap 来解决循环引用的问题。

WeakMap 是一种弱引用的数据结构,它可以将对象作为键存储在其中,并且不会影响对象的垃圾回收。

在这个实现中,我们使用 WeakMap 来记录原始对象和拷贝对象的对应关系,以避免循环引用导致的死循环。

2. 函数柯里化

柯里化是一种函数式编程技术,它将一个多参数函数转换为一系列单参数函数的调用链。

使用柯里化的优点是可以将函数变得更加灵活可复用,支持预先传递一些参数,留下另一些参数等待后续调用。

另外,柯里化也可以用于创建更加通用和抽象的函数,因此可以将柯里化函数看作一种构建其他函数的基础组件。

下面是使用 ts 实现函数柯里化的代码:

function curry(fn: Function) {
  return function curried(...args: any[]) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs: any[]) {
        return curried.apply(this, args.concat(moreArgs));
      }
    }
  }
}

该函数接受一个多参数函数 fn 作为参数,并返回一个新的函数 curried

curried 函数会检查当前传递的参数数量是否足够调用 fn 函数,如果是,则调用 fn 函数并返回结果,否则返回一个新的函数,等待更多参数的传递。

这个新的函数也是一个柯里化函数,它会将之前传递的参数和当前传递的参数合并,并递归调用 curried 函数,直到所有参数都被传递完毕。

使用 ts实现柯里化后,我们可以将一个多参数函数转换为一系列单参数函数的调用链,例如:

function add(x: number, y: number, z: number) {
  return x + y + z;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
console.log(curriedAdd(1, 2, 3)); // 输出 6

3. 数组扁平化

数组扁平化是指将一个嵌套的数组展开成一个一维的数组。

例如,将 [1, [2, [3, 4], 5], 6] 扁平化成 [1, 2, 3, 4, 5, 6]。

使用ts实现数组扁平化,可以使用递归算法或者使用 ES6 中的 flat 方法。下面分别介绍这两种方法。

1. 递归算法

递归算法的思路是遍历数组中的每个元素,如果当前元素是数组,则继续递归处理,直到最终得到一个一维数组为止。

具体实现代码如下:

function flatten(arr: any[]): any[] {
  let result: any[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
2. 使用 flat 方法

ES6 中的 flat 方法可以将多维数组扁平化为一维数组。

flat 方法接受一个可选参数,用于指定扁平化的深度,默认为 1。如果指定深度为 Infinity,则可以扁平化任意维度的数组。

具体实现代码如下:

function flatten(arr: any[]): any[] {
  return arr.flat(Infinity);
}

3. 洋葱模型

洋葱模型是指 Koa 的中间件机制,中间件可以看做是对请求和响应的预处理

通过洋葱模型,每一个中间件都会在请求到来时先被执行,然后再执行下一个中间件,最后执行响应操作,整个过程就像是一个洋葱,由内向外一层层地执行,最后再由外向内一层层地返回

下面是使用 ts 实现 Koa 洋葱模型的代码示例:

function f(next: () => void): void {
  console.log(1);
  next(); // next的作用就是执行下一个函数
  console.log(2);
}

function g(next: () => void): void {
  console.log(3);
  next();
  console.log(4);
}

function h(next: () => void): void {
  console.log(5);
  next();
  console.log(6);
}

function compose(...funcs: ((next: () => void) => void)[]): () => void {
  return function (): void {
    function execute(index: number): void {
      // 依次处理函数
      const fn = funcs[index];
      if (!fn) return null; // 代表遍历结束
      fn(function next(): void {
        execute(index + 1); // 进入下一个函数
      });
    }

    execute(0);
  };
}

在上述代码中,compose函数用来将一组函数按照洋葱模型的方式组合起来,返回一个函数。这个函数在被调用时,会按照从外到内的顺序执行一组函数。

compose函数中,我们定义了一个execute函数,它会按照顺序依次执行每个函数,每个函数中都调用next函数进入下一个函数,直到函数数组遍历结束。

最后,我们返回一个新的函数,它会按照洋葱模型的顺序执行这一组函数。

4. 睡眠函数

睡眠函数的作用是在程序执行时延迟一段时间。

在某些情况下,我们可能需要程序在一定时间后才能执行下一步操作,例如:

  • 需要等待某个事件的发生,比如等待用户输入、等待服务器返回数据等;
  • 需要等待一段时间后再执行下一步操作,比如在轮询操作中等待一段时间后再向服务器发起请求;
  • 需要模拟程序的等待行为,比如在测试代码中等待一段时间后再检查某个条件是否满足等。

在这些情况下,使用睡眠函数可以让程序在指定时间内等待,避免了程序不必要的忙等待,同时也方便了代码的编写和调试。

下面是使用 ts 实现睡眠函数的代码示例:

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

5. 防抖函数

防抖函数的作用是在某个事件被触发后,如果在指定的时间内再次触发该事件,就会取消之前的事件处理函数,并重新开始计时。

这样可以避免事件处理函数被频繁触发,从而减少一些不必要的操作。

常见的应用场景包括:

  • 搜索框自动补全:当用户输入关键字时,防抖函数可以避免在用户连续输入时频繁触发搜索请求;
  • 滚动事件处理:当页面滚动时,防抖函数可以避免过多的计算和DOM操作,从而提高页面的性能。

下面是使用ts实现一个防抖函数的代码:

function debounce(func: Function, delay: number): Function {
  let timer: ReturnType<typeof setTimeout>;

  return function (...args: any[]) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

这个函数接受两个参数:第一个参数是需要执行的函数,第二个参数是需要防抖的延迟时间。

它返回一个新的函数,在调用该函数时,如果在延迟时间内再次调用该函数,就会取消之前的定时器,重新开始计时,直到延迟时间过去后再执行该函数。

6. 节流函数

节流函数的作用是在某个事件被触发时,按照一定的时间间隔执行事件处理函数

这样可以避免事件处理函数被频繁触发,从而减少一些不必要的操作。

常见的应用场景包括:

  • 滚动事件处理:当页面滚动时,节流函数可以避免过多的计算和DOM操作,从而提高页面的性能;
  • 鼠标移动事件处理:当鼠标移动时,节流函数可以避免频繁地更新DOM元素,从而提高页面的性能。

下面是使用ts实现一个节流函数的代码示例:

function throttle(func: Function, delay: number): Function {
  let timer: ReturnType<typeof setTimeout>;

  return function (...args: any[]) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = undefined;
      }, delay);
    }
  };
}

这个函数接受两个参数:第一个参数是需要执行的函数,第二个参数是需要节流的时间间隔。它返回一个新的函数,在调用该函数时,如果在时间间隔内再次调用该函数,就会忽略该次调用,直到时间间隔过去后再次执行该函数。

需要注意的是,节流函数和防抖函数的作用有所不同。

防抖函数会在延迟时间内只执行一次事件处理函数,而节流函数会按照一定的时间间隔执行事件处理函数。

7. PromiseAll

Promise.all()方法通常用于在一个异步操作中执行多个并发的异步请求,然后在所有请求都完成后,将它们的结果合并成一个结果。

下面是使用ts实现一个PromiseAll函数的代码示例:

function promiseAll<T>(promises: Promise<T>[]): Promise<T[]> {
  return new Promise((resolve, reject) => {
    let count = 0;
    const result: T[] = [];

    promises.forEach((promise, index) => {
      promise
        .then((res) => {
          count++;
          result[index] = res;
          if (count === promises.length) {
            resolve(result);
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  });
}

promiseAll函数接受一个Promise对象数组作为参数,返回一个新的Promise对象。

promiseAll函数会同时执行数组中的所有Promise对象,并在所有Promise对象都成功时返回一个数组,包含了所有Promise对象的结果值。

如果有任何一个Promise对象被拒绝(rejected),则返回的Promise对象会被拒绝,并带有该Promise对象的拒绝原因。

8. 数组去重

所谓数组去重,指的是删除数组中重复的元素,使得最终数组中的元素各不相同。

下面是使用 ts来对包含各种类型数据的数组去重的代码示例:

function removeDuplicates(arr: (number | object | boolean | string | null | undefined)[]): (number | object | boolean | string | null | undefined)[] {
  const uniqueArr: (number | object | boolean | string | null | undefined)[] = [];
  const seen: Set<string> = new Set();

  for (const item of arr) {
    const itemType: string = typeof item;

    if (itemType === 'number' && isNaN(item as number)) {
      if (!seen.has('NaN')) {
        seen.add('NaN');
        uniqueArr.push(item);
      }
    } else if (!seen.has(itemType + JSON.stringify(item))) {
      seen.add(itemType + JSON.stringify(item));
      uniqueArr.push(item);
    }
  }

  return uniqueArr;
}

这个函数接受一个数组作为输入,然后对其进行去重,并返回一个新的数组。

它会遍历输入数组中的每一个元素,根据元素的类型以及其值来判断是否需要将它加入到输出数组中。

9. 树形结构转列表

树形结构和列表互相转换是前端开发中常见的一个操作。

将树形结构转换为列表后,可以方便地对数据进行处理,比如排序、过滤、搜索等操作。

下面是使用 ts来实现树形结构转列表的代码示例:

interface TreeNode {
  id: number;
  name: string;
  children?: TreeNode[];
}

interface ListItem {
  id: number;
  name: string;
  depth: number;
}

function flattenTreeToList(tree: TreeNode[], depth = 0): ListItem[] {
  let result: ListItem[] = [];

  for (const node of tree) {
    result.push({ id: node.id, name: node.name, depth });

    if (node.children) {
      result = result.concat(flattenTreeToList(node.children, depth + 1));
    }
  }

  return result;
}

这个代码实现了一个 flattenTreeToList 函数,它接受一个树形结构的数组和一个可选的深度参数。如果不提供深度参数,则默认为 0。该函数将树形结构转换为一个扁平的列表,其中每个节点都是一个 ListItem 对象,包含节点的 ID、名称和所在的深度。

函数通过递归遍历树形结构来生成列表。对于每个节点,它将一个新的 ListItem 对象推入结果数组中,并检查该节点是否有子节点。如果有,它将递归调用自身来处理子节点,并将结果合并到结果数组中。

例如,假设有以下树形结构:

const tree: TreeNode[] = [
  {
    id: 1,
    name: "Node 1",
    children: [
      { id: 2, name: "Node 1.1" },
      { id: 3, name: "Node 1.2" },
    ],
  },
  {
    id: 4,
    name: "Node 2",
    children: [
      { id: 5, name: "Node 2.1" },
      { id: 6, name: "Node 2.2" },
      {
        id: 7,
        name: "Node 2.3",
        children: [{ id: 8, name: "Node 2.3.1" }],
      },
    ],
  },
];

可以使用以下代码将其转换为列表:

const list = flattenTreeToList(tree);
console.log(list);

输出应该类似于以下内容:

[
  { id: 1, name: 'Node 1', depth: 0 },
  { id: 2, name: 'Node 1.1', depth: 1 },
  { id: 3, name: 'Node 1.2', depth: 1 },
  { id: 4, name: 'Node 2', depth: 0 },
  { id: 5, name: 'Node 2.1', depth: 1 },
  { id: 6, name: 'Node 2.2', depth: 1 },
  { id: 7, name: 'Node 2.3', depth: 1 },
  { id: 8, name: 'Node 2.3.1', depth: 2 }
]

10. 列表转树形结构

在前端页面中,对于需要展示树形结构的组件,例如树形菜单、树形表格、树形选择器等组件,就需要将列表数据转换为树形结构以便于展示。

下面是使用 ts来实现列表转树形结构的代码示例:

interface TreeNode {
  id: number;
  name: string;
  parentId?: number;
  children?: TreeNode[];
}

function listToTree(list: TreeNode[]): TreeNode[] {
  const map: Record<number, TreeNode> = {};
  const roots: TreeNode[] = [];

  // 将节点映射到 ID
  for (const node of list) {
    node.children = [];
    map[node.id] = node;

    // 如果该节点没有父节点,则将其添加到根节点列表中
    if (!node.parentId) {
      roots.push(node);
    }
  }

  // 遍历列表中的每个节点,并将其添加到其父节点的 children 数组中
  for (const node of list) {
    if (node.parentId) {
      const parent = map[node.parentId];
      if (parent) {
        parent.children.push(node);
      }
    }
  }

  return roots;
}

这个代码实现了一个 listToTree 函数,它接受一个节点数组,并返回一个包含树形结构的节点数组。每个节点都是一个 TreeNode 对象,包含节点的 ID、名称、父节点 ID 和子节点数组。

函数通过两个循环来构建树形结构

首先,它遍历节点列表,并将每个节点映射到其 ID。如果该节点没有父节点,则将其添加到根节点列表中。

接下来,它再次遍历节点列表,并将每个节点添加到其父节点的 children 数组中。

例如,假设有以下节点列表:

const list: TreeNode[] = [
  { id: 1, name: "Node 1" },
  { id: 2, name: "Node 1.1", parentId: 1 },
  { id: 3, name: "Node 1.2", parentId: 1 },
  { id: 4, name: "Node 2" },
  { id: 5, name: "Node 2.1", parentId: 4 },
  { id: 6, name: "Node 2.2", parentId: 4 },
  { id: 7, name: "Node 2.3", parentId: 4 },
  { id: 8, name: "Node 2.3.1", parentId: 7 },
];

可以使用以下代码将其转换为树形结构:

const tree = listToTree(list);
console.log(tree);

输出应该类似于以下内容:

[
  {
    id: 1,
    name: 'Node 1',
    children: [
      { id: 2, name: 'Node 1.1', parentId: 1, children: [] },
      { id: 3, name: 'Node 1.2', parentId: 1, children: [] }
    ]
  },
  {
    id: 4,
    name: 'Node 2',
    children: [
      { id: 5, name: 'Node 2.1', parentId: 4, children: [] },
      { id: 6, name: 'Node 2.2', parentId: 4, children: [] },
      {
        id: 7,
        name: 'Node 2.3',
        parentId: 4,
        children: [{ id: 8, name: 'Node 2.3.1', parentId: 7, children: [] }]
      }
    ]
  }
]