前端综合能力考察面试题

125 阅读8分钟

字符串和数组处理(动手能力+逻辑处理)

题目:实现一个函数 formatQueryParams,将对象转换为 URL 参数字符串,并处理特殊要求和数组格式: const params = { name: "张三&李四", hobbies: ["篮球", "编程"], filters: { min: 20, max: 30 }, empty: null }

// 目标输出:“name=张三%26 李四&hobbies[]=篮球&hobbies[]=编程&filters[min]=20&fi1ters[max]=30^"

要求:

  • 处理值中的特殊字符(如&,=) //encodeURIComponent,为了确保查询字符串符合 URL 编码规范,防止中文、空格、特殊符号(如 &、=、#、[、] 等)被浏览器或服务器误解析,避免请求出错或参数混乱。
  • 数组转换为 key[]=value 格式 // instanceof
  • 对象转换为嵌套键名(如 filters[min])// instanceof
  • 忽略 null/undefined 值 // == null
  • 不使用 URLSearchParams
formatQueryParams(params) {
  // 用于存储最终拼接好的键值对字符串
  const result = [];

  /**
   * 递归处理对象属性
   * @param {object} obj - 当前正在处理的对象
   * @param {string} prefix - 键名前缀(用于嵌套对象键名拼接)
   */
  const processObject = (obj, prefix = "") => {
    // 遍历对象的所有键值对
    for (const [key, value] of Object.entries(obj)) {
      // 跳过 null 或 undefined 的值,不进行处理
      if (value == null) continue;

      // 构建完整的键名,处理嵌套对象时会带前缀,如 user[name]
      const fullKey = prefix ? `${prefix}[${key}]` : key;

      // 如果当前值是数组
      if (value instanceof Array) {
        // 遍历数组中的每一项
        for (const item of value) {
          // 同样跳过 null 或 undefined
          if (item != null) {
            // 将数组项格式化为 key[]=value 形式,并进行 URL 编码
            result.push(
              `${encodeURIComponent(fullKey + "[]")}=${encodeURIComponent(item)}`
            );
          }
        }
      }
      // 如果当前值是对象(且不是数组,因为上面已处理)
      else if (value instanceof Object) {
        // 递归调用自身处理嵌套对象,键名前缀更新为当前键名
        processObject(value, fullKey);
      }
      // 如果是基本类型(字符串、数字、布尔值等)
      else {
        // 将键值对格式化为 key=value 形式,并进行 URL 编码
        result.push(
          `${encodeURIComponent(fullKey)}=${encodeURIComponent(value)}`
        );
      }
    }
  };

  // 开始处理传入的参数对象
  processObject(params);

  // 使用 & 将所有格式化好的键值对连接成查询字符串并返回
  return result.join("&");
}

JS 核心逻辑与性能优化(作用域+性能)

题目:分析以下代码问题,并重构优化:

function processData(items) {
  var results = [];
  for (var i = 0; i < items.length; i++) {
    setTimeout(function () {
      const processed = expensiveOperation(items[i]);
      results.push(processed);
    }, 0);
  }
  return results;
}
  • 指出作用域、异步逻辑、性能的三处问题
  • 使用 ES6+语法重构函数
  • 添加并行处理优化(使用 Promise/Pool 控制)

原始代码问题分析:

  • 作用域问题(闭包陷阱)

    • var i 是函数作用域,不是块级作用域。
    • setTimeout 回调中访问的是 同一个 i 变量,循环结束后 i === items.length。
    • 所有回调执行时都访问了相同的 i,导致 items[i] 访问越界,结果是 undefined。

示例:

const items = ["a", "b", "c"];
for (var i = 0; i < items.length; i++) {
  setTimeout(() => {
    console.log(i); // 都会打印3,而不是0,1,2
  }, 0);
}
  • 异步逻辑问题

    • setTimeout(..., 0) 是异步的,但 processData 函数同步返回 results。
    • 因为异步任务还未执行,返回的 results 还是空数组。
    • 造成函数调用方无法正确拿到处理结果
  • 性能问题

    • 所有回调几乎同时放入宏任务队列,任务堆积。
    • 不保证执行顺序,且不能限制并发数量(资源可能被耗尽)。
    • results.push 的顺序和原数组顺序可能不一致,难保证数据顺序。
/**
 * 并发受控的异步数据处理函数
 * @param {Array} items - 要处理的元素数组
 * @param {number} poolLimit - 最大并发数(默认同时最多处理的任务数量)
 * @returns {Promise<Array>} - 返回一个 Promise,解析为所有处理后的结果数组,顺序与原数组一致
 */
async function processData(items, poolLimit = 5) {
  // 存放最终处理后的结果,按原顺序填充
  const results = [];

  // 当前要处理的数组下标,多个并发任务共享该变量
  let currentIndex = 0;

  // 创建一个固定长度的任务池,每个异步任务都是一个 "工人"
  const pool = new Array(poolLimit).fill(null).map(async () => {
    // 每个"工人"不断从共享的 currentIndex 获取下一个任务
    while (currentIndex < items.length) {
      // 记录当前要处理的索引,并递增以便下一个工人获取下一个任务
      const idx = currentIndex++;

      // 执行耗时操作(可为同步或异步),使用 await 保证正确顺序
      const processed = await expensiveOperation(items[idx]);

      // 把结果放到对应索引,确保最终结果与原数组顺序一致
      results[idx] = processed;
    }
  });

  // 等待所有池中工人任务完成
  await Promise.all(pool);

  // 返回结果数组
  return results;
}

Vue 问题处理能力(Vue 实践)

题目:在 Vue3 中遇到以下场景:

<script setup>
  import (ref, watch)from 'vue' const searchText = ref(’’) const results =
  ref([]) 
   //现有实现(存在性能问题) 
  watch(searchText,async(newVal)=>(
  results.value = await fetchResults(newVal) ))
</script>
  • 分析输入"vue"时可能发起的请求次数(3 次)及问题
  • 使用防抖优化(要求保留最新结果)
  • 添加竟态处理(取消过期请求)
  • 修改为 Composition AP 最佳实践

答案:

    • 输入"vue"时,会发起 3 次请求,分别是"v", "vu", "vue"。
    • 存在的问题:
      • 请求风暴:短时间内发起多次请求
      • 结果错乱:后发请求可能先返回,导致显示旧结果
      • 资源浪费:无效请求占用带宽和服务器资源
      • 竞态问题:无法保证最终显示的是最后一次请求的结果
import { ref, watch } from "vue";
import { debounce } from "lodash-es";

export function useSearchResults(fetchFn, debounceMs = 300) {
  const searchText = ref("");
  const results = ref([]);
  let requestId = 0;

  const fetchAndSet = async (keyword) => {
    const id = ++requestId;
    const res = await fetchFn(keyword);
    if (id === requestId) {
      results.value = res;
    }
  };

  const debouncedFetch = debounce(fetchAndSet, debounceMs);

  watch(searchText, (val) => {
    debouncedFetch(val);
  });

  return {
    searchText,
    results,
  };
}

HTTP 请求知识(网络层)

题目:设计一个带完整错误处理的请求函数:

async function apiRequest(config) {
  //需实现以下能力:
  //1.自动处理不同环境域名(dev/test/prod)
  //2.请求超时(3s)//3.401自动刷新token重试
  // 4.支持取消请求
  //5.错误分级(网络错误/服务端错误/业务逻辑错误)
}
  • 补全函数实现
  • 解释如何避免 CORS 问题
  • 描述 CSRF 防御方案;
// 环境配置
const envConfig = {
  dev: { baseURL: "http://dev.api.com" },
  test: { baseURL: "http://test.api.com" },
  prod: { baseURL: "https://api.com" },
};

// 获取当前环境
function getBaseUrl() {
  const env = process.env.NODE_ENV || "dev";
  return envConfig[env].baseURL;
}

// 请求函数
async function apiRequest(config) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 3000);

  try {
    const response = await fetch(`${getBaseUrl()}${config.url}`, {
      method: config.method || "GET",
      headers: {
        "Content-Type": "application/json",
        ...config.headers,
      },
      body: config.data ? JSON.stringify(config.data) : null,
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    // 处理401未授权
    if (response.status === 401) {
      const newToken = await refreshToken();
      // 更新header后重试
      return apiRequest({
        ...config,
        headers: { ...config.headers, Authorization: `Bearer ${newToken}` },
      });
    }

    const data = await response.json();

    // 处理业务错误
    if (!response.ok) {
      throw {
        type: response.status < 500 ? "business" : "server",
        message: data.message || "请求失败",
        code: data.code || response.status,
      };
    }

    return data;
  } catch (error) {
    clearTimeout(timeoutId);

    // 错误分类处理
    if (error.name === "AbortError") {
      throw { type: "network", message: "请求超时" };
    } else if (!navigator.onLine) {
      throw { type: "network", message: "网络连接失败" };
    } else {
      throw error.type
        ? error
        : {
            type: "server",
            message: error.message || "服务器错误",
          };
    }
  }
}

// 避免CORS方案:
// 1. 服务器设置Access-Control-Allow-Origin
// 2. 开发环境使用代理服务器
// 3. 预检请求(OPTIONS)处理

// CSRF防御方案:
// 1. 使用SameSite Cookie属性
// 2. 添加CSRF Token到请求头
// 3. 验证Origin/Referer头

算法能力(中等问题)

题目:实现函数计算最长有效括号子串:

function longestValidParentheses(s) {
  //输入:")()())"输出:4(因为"()()")
  //输入:"(())(()"输出:4(因为"()()")
}
-使用动态规划或栈实现 -
  解释时间复杂度 -
  边界测试用例(空字符串 / 全无效 / 全有效);
function longestValidParentheses(s) {
  let maxLen = 0;
  // DP数组:dp[i]表示以i结尾的最长有效括号长度
  const dp = new Array(s.length).fill(0);

  for (let i = 1; i < s.length; i++) {
    if (s[i] === ")") {
      // 情况1:前一个字符是 '('
      if (s[i - 1] === "(") {
        dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
      }
      // 情况2:前一个字符是 ')' 且前面有匹配的 '('
      else if (i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] === "(") {
        const prevLen = i - dp[i - 1] >= 2 ? dp[i - dp[i - 1] - 2] : 0;
        dp[i] = dp[i - 1] + prevLen + 2;
      }

      maxLen = Math.max(maxLen, dp[i]);
    }
  }

  return maxLen;
}

// 边界测试用例:
console.log(longestValidParentheses("")); // 0 (空字符串)
console.log(longestValidParentheses(")))(((")); // 0 (全无效)
console.log(longestValidParentheses("()()")); // 4 (全有效)
console.log(longestValidParentheses(")()())")); // 4
console.log(longestValidParentheses("(())(()")); // 4

/**
 * 时间复杂度:O(n) 单次遍历字符串
 * 空间复杂度:O(n) 使用DP数组
 *
 * 算法思路:
 * 1. 使用动态规划数组dp记录以每个位置结尾的有效括号长度
 * 2. 遇到')'时检查两种情况:
 *    a. 前一个字符是'(':形成一对括号
 *    b. 前一个字符是')':检查是否有嵌套结构
 * 3. 维护最大值maxLen
 */

处理数据

const options = [
   {
       "label": "标签1",
       "value": 101,
   },
   {
       "label": "标签2",
       "value": 102,
       "children": []
   },
   {
       "label": "标签3",
       "value": 103,
       "children": [
           {
               "label": "标签31",
               "value": 103001,
               "children": []
           }
       ]
   }
];

写一个 function getLabel(value, options) 函数,据传入的 value 在嵌套的 options 结构中查找对应的 label,并返回一个数组(表示层级路径),如果找不到则返回 null

测试用例

console.log(getLabel(103001, options)); // ["标签3", "标签31"]

console.log(getLabel(104, options)); // null

题解

function getLabel(value, options) {
    // 遍历 options 数组中的每一个 option 对象
    for (const option of options) {
        // 检查当前 option 的 value 是否等于传入的 value
        if (option.value === value) {
            // 如果匹配,返回包含当前 label 的数组
            return [option.label]; // 直接匹配,返回 [label]
        }

        // 检查当前 option 是否有 children 且 children 数组不为空
        if (option.children && option.children.length > 0) {
            // 递归调用 getLabel,在 children 中继续查找 value
            const childResult = getLabel(value, option.children); // 递归查找子级

            // 如果在子级中找到匹配项
            if (childResult) {
                // 返回当前 label 和子级结果的拼接数组
                return [option.label, ...childResult]; // 拼接当前 label 和子级结果
            }
        }
    }

    // 如果遍历完所有 option 都没找到匹配项,返回 null
    return null; // 没找到,返回 null
}

事件循环

setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function(resolve, reject) { 
  console.log(2);
  resolve();
}).then(function() {
  console.log(3);
}).then(function() {
  console.log(4);
});

console.log(6);

打印结果

26341