Bootstrap

【每日十题系列】前端面试高频题目

以下是作为前端面试官常用的10道手撕代码题目,涵盖JavaScript核心、CSS、算法及框架原理,结合高频考点与实际开发场景设计:


1. 手写防抖(debounce)与节流(throttle)

要求:实现防抖函数(延迟执行)和节流函数(固定频率执行),并说明应用场景(如搜索框输入、窗口resize事件)。

1.防抖节流

2. 数组去重的多种实现

要求:用至少3种方法实现(如Setfilter+indexOfreduce),并分析时间复杂度的差异。


3. 深拷贝与浅拷贝的实现

要求:手写深拷贝函数,处理循环引用、函数、Symbol等特殊类型,对比JSON.parse(JSON.stringify())的局限性。

前端中常用的数据类型判断方法各有其适用场景和局限性,以下是四种主要方式及其分析:
浅拷贝与深拷贝
深拷贝


1. typeof

作用与示例

原理:返回变量类型的字符串表示,适用于基本数据类型判断。

console.log(typeof 1);           // 'number'  
console.log(typeof 'str');       // 'string'  
console.log(typeof undefined);   // 'undefined'  

局限性
无法区分引用类型:数组、对象、null均返回 'object'
函数类型特殊处理typeof function(){} 返回 'function',但函数本质仍是对象。


2. instanceof

作用与示例

原理:通过原型链检查对象是否为某个构造函数的实例,适合判断引用类型。

console.log([] instanceof Array);   // true  
console.log({} instanceof Object);  // true  
console.log(new Number(1) instanceof Number);  // true  

局限性
无法判断基本类型字面量1 instanceof Number 返回 false
跨窗口失效:不同全局环境(如iframe)的实例可能误判。
原型链干扰:所有引用类型(如数组)的原型链最终指向 Object,导致 [] instanceof Object 返回 true


3. constructor

作用与示例

原理:通过对象的 constructor 属性获取其构造函数。

console.log([].constructor === Array);  // true  
console.log('str'.constructor === String); // true  

局限性
易被修改:若重写对象的 prototypeconstructor 可能丢失或指向错误。
无法处理 nullundefined:这两个值没有 constructor 属性。


4. Object.prototype.toString.call()

作用与示例

原理:调用对象内部的 [[Class]] 标识,返回格式如 [object Type] 的标准字符串。

console.log(Object.prototype.toString.call([]));   // '[object Array]'  
console.log(Object.prototype.toString.call(null));  // '[object Null]'  

优势
全面性:支持所有数据类型,包括 nullundefined 和 ES6 新增类型(如 Symbol)。
局限性
无法判断继承关系:如无法区分自定义类实例的具体父类。


总结与建议

  1. 基本类型:优先使用 typeof,但需注意 null 的误判问题。
  2. 引用类型
    • 若需判断具体类型(如数组),结合 Array.isArray()Object.prototype.toString.call()
    • 避免依赖 instanceof 判断跨环境对象。
  3. 高精度场景:统一使用 Object.prototype.toString.call(),通过字符串截取获取类型标识(如 slice(8, -1))。

例如,实现一个通用类型判断函数:

function getType(val) {
  if (val === null) return 'null';
  const type = typeof val;
  if (type !== 'object') return type; // 基本类型直接返回
  return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
console.log(getType([])); // 'array'  
console.log(getType(Symbol())); // 'symbol'  

4. Promise并发控制

要求:实现一个函数limitRequest(urls, max),限制同时最多处理max个请求,按顺序返回结果(考察异步控制与队列管理)。
如何应对页面请求接口的大规模并发问题

手写Promise


class Promise {
  static resolve(value) {
    if (value instanceof Promise) {
      return value;
    }
    return new Promise((resolve) => {
      resolve(value);
    });
  }

  static reject(reason) {
    return new Promise((_, reject) => {
      reject(reason);
    });
  }

  static race(promises) {
    return new Promise((resolve, reject) => {
      promises.forEach((promise) => {
        Promise.resolve(promise).then(resolve, reject);
      });
    });
  }

  static all(promises) {
    return new Promise((resolve, reject) => {
      const results = [];
      let completedCount = 0;

      if (promises.length === 0) {
        resolve(results);
        return;
      }

      promises.forEach((promise, index) => {
        Promise.resolve(promise).then(
          (value) => {
            results[index] = value;
            completedCount++;
            if (completedCount === promises.length) {
              resolve(results);
            }
          },
          (reason) => {
            reject(reason);
          }
        );
      });
    });
  }

  constructor(executor) {
    // 状态
    this.status = "pending";
    // 成功结果
    this.value = undefined;
    // 失败原因
    this.reason = undefined;
    // 成功回调函数队列
    this.onResolvedCallbacks = [];
    // 失败回调函数队列
    this.onRejectedCallbacks = [];

    let resolve = (value) => {
      if (this.status === "pending") {
        // 新的值需要是 Promise,则递归解析
        if (value instanceof Promise) {
          return value.then(resolve, reject);
        }
        this.status = "fulfilled";
        this.value = value;
        // 异步执行所有成功回调
        this.onResolvedCallbacks.forEach((fn) => fn());
      }
    };

    let reject = (reason) => {
      if (this.status === "pending") {
        this.status = "rejected";
        this.reason = reason;
        // 异步执行所有失败回调
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    };
    // 执行器本身错误,直接 reject
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected = typeof onRejected === "function" ? onRejected : (reason) => {
      throw reason;
    };
    let context = this;

    return new Promise((resolve, reject) => {
      function handleOnFulfilled() {
        try {
          const result = onFulfilled(context.value);
          if (result instanceof Promise) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      }

      function handleOnRejected() {
        try {
          const result = onRejected(context.reason);
          if (result instanceof Promise) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      }

      if (this.status === "fulfilled") {
        setTimeout(() => {
          handleOnFulfilled();
        }, 0);
      }
      if (this.status === "rejected") {
        setTimeout(() => {
          handleOnRejected();
        }, 0);
      }
      // 当状态为 pending 时
      if (this.status === "pending") {
        // onFulfilled 传入到成功数组
        this.onResolvedCallbacks.push(handleOnFulfilled);
        // onRejected 传入到失败数组
        this.onRejectedCallbacks.push(handleOnRejected);
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(onFinally) {
    return this.then(
      (value) => Promise.resolve(onFinally()).then(() => value),
      (reason) => Promise.resolve(onFinally()).then(() => {
        throw reason;
      })
    );
  }
}

// // 示例用法
// const promise1 = new Promise((resolve) => setTimeout(() => resolve('one'), 1000));
// const promise2 = new Promise((resolve) => setTimeout(() => resolve('two'), 2000));
//
// // 使用 Promise.race
// Promise.race([promise1, promise2]).then((value) => {
//   console.log('Promise.race result:', value); // 输出: Promise.race result: one
// });
//
// // 使用 Promise.all
// Promise.all([promise1, promise2]).then((values) => {
//   console.log('Promise.all result:', values); // 输出: Promise.all result: [ 'one', 'two' ]
// });
//
// // 生成多个测试 Promise
// function generatePromises(count) {
//   const promises = [];
//   for (let i = 0; i < count; i++) {
//     const delay = Math.random() * 1000; // 随机延迟 0 - 1000ms
//     promises.push(new Promise((resolve) => {
//       setTimeout(() => {
//         resolve(i);
//       }, delay);
//     }));
//   }
//   return promises;
// }
// // 测试 Promise.race
// function testPromiseRace(count) {
//   const promises = generatePromises(count);
//   const startTime = performance.now();
//   Promise.race(promises).then(() => {
//     const endTime = performance.now();
//     console.log(`Promise.race with ${count} promises took ${endTime - startTime} ms`);
//   });
// }
//
// // 测试 Promise.all
// function testPromiseAll(count) {
//   const promises = generatePromises(count);
//   const startTime = performance.now();
//   Promise.all(promises).then(() => {
//     const endTime = performance.now();
//     console.log(`Promise.all with ${count} promises took ${endTime - startTime} ms`);
//   });
// }
//
// // 测试不同数量的 Promise
// const testCounts = [10, 100, 1000];
// testCounts.forEach((count) => {
//   testPromiseRace(count);
//   testPromiseAll(count);
// });


5. CSS实现三角形与垂直居中

要求
• 用CSS画一个三角形(利用border透明属性);
• 用Flex布局实现元素垂直居中(至少两种方式)。

    #triangle {
      width: 0;
      height: 0;
      border-left: 50px solid transparent;
      border-right: 50px solid transparent;
      border-bottom: 100px solid red;
      border-top: 100px solid red;
      /*border: 20px solid transparent;*/
      /*border-top-color: red;*/
      /*border-bottom-color: red;*/
    }

在这里插入图片描述
Grid布局回顾


6. 括号匹配算法

要求:编写函数判断字符串中的括号是否有效(如"({[]})"有效,"([)]"无效),使用栈结构实现。


7. 函数柯里化(Currying)

要求:实现一个通用柯里化函数,支持curry(add)(1)(2)(3)调用形式,其中add接收多个参数求和。


8. 数组扁平化(Flatten)

要求:将多维数组展开为一维数组(如[1, [2, [3]]]转为[1,2,3]),用递归、reduce或ES6的flat方法实现。


9. React/Vue的虚拟DOM对比

要求:手写一个简化版虚拟DOM的Diff算法,比较两棵树的不同并输出差异(考察节点复用与Key的作用)。


10. 实现一个简易的EventEmitter

要求:实现事件订阅(on)、触发(emit)、取消订阅(off)功能,支持异步事件队列。


考察重点

代码健壮性:处理边界条件(如空值、异常输入);
性能优化:避免不必要的计算或内存泄漏;
原理理解:解释实现思路与底层机制(如闭包、原型链、事件循环)。


提示:以上题目可根据候选人经验调整难度(如增加异步流程、复杂数据结构处理)。建议要求候选人现场编码,并逐步追问优化方案(如防抖节流的时间参数动态化、深拷贝的循环引用检测等)。

以下是一些常见 JavaScript 代码实现的详解,结合应用场景和实现原理进行说明:


嗯,用户让我帮忙回答10个JavaScript的手写题目,每个题目都需要详细实现和解释。首先,我得逐一看看每个问题,然后结合提供的搜索结果来整理答案。

第一个题目是防抖和节流。搜索结果里有很多关于防抖和节流的实现,比如非立即执行和立即执行两种防抖方式。防抖的核心是延迟执行,而节流是固定频率执行。应用场景方面,搜索框输入用防抖,窗口resize可能用节流或防抖,得看具体需求。我需要写出两者的代码,并说明应用场景,注意处理闭包中的变量。

第二个是数组去重,至少三种方法。搜索结果里提到了Set、filter+indexOf、reduce等方法。Set最简单,时间复杂度是O(n);filter+indexOf因为每次都要检查索引,可能到O(n²);reduce同样可能接近O(n²)。需要比较它们的性能差异。

第三个是深拷贝,处理循环引用和特殊类型。JSON方法的局限性是不能处理函数和循环引用,递归实现时需要用WeakMap来跟踪已拷贝的对象,处理Date、RegExp等类型。需要对比JSON方法的缺点,并展示如何处理这些情况。

第四个是Promise并发控制,实现limitRequest函数。搜索结果里提到了使用队列和递归,或者async/await结合Promise.race。需要维护一个执行队列,控制同时进行的请求数量,按顺序返回结果。要注意异步管理和队列的执行顺序。

第五个是CSS画三角形和垂直居中。三角形用border的透明属性,比如设置三个边透明,另一边显示颜色。垂直居中Flex布局的话,可以用align-items和justify-content,或者用margin:auto的方式。需要给出两种Flex实现方式。

第六个括号匹配,用栈结构。遇到左括号入栈,右括号时检查栈顶是否匹配。最后栈应该为空。需要处理各种括号类型,确保顺序正确。

第七个函数柯里化,支持多参数调用。柯里化函数需要递归或闭包保存参数,直到参数数量足够执行原函数。比如curry(add)(1)(2)(3)需要累积参数,当参数数目满足时调用add求和。

第八个数组扁平化,可以用递归、reduce或flat方法。递归的话遍历数组,将元素展开;reduce方法累加结果;flat方法最简单但可能有兼容性问题。需要比较不同方法的实现。

第九个虚拟DOM的Diff算法。简化版的话,可以比较节点的类型和属性,处理子节点的差异。Key的作用是复用节点,减少DOM操作。需要手写一个简单的Diff函数,输出差异。

第十个EventEmitter,实现on、emit、off。使用对象存储事件和回调数组,emit时触发所有回调,支持异步可能需要用Promise或setTimeout。要注意取消订阅时从数组中移除回调,避免内存泄漏。

每个问题都需要结合搜索结果中的代码示例,并解释实现原理和注意事项。比如防抖中的闭包和定时器管理,深拷贝中的循环引用处理,Promise并发中的队列控制等。同时,要引用对应的搜索结果作为参考,确保答案的准确性。

1. 防抖(Debounce)与节流(Throttle)

防抖实现
核心:延迟执行,若重复触发则重置计时器。

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

节流实现
核心:固定时间间隔执行一次。

function throttle(func, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

应用场景
• 防抖:搜索框输入联想(延迟请求)、窗口 resize 停止后计算布局
• 节流:滚动事件触发加载、高频点击按钮防重复提交


2. 数组去重的多种实现

方法 1:Set(时间复杂度 O(n))

const unique = arr => [...new Set(arr)];

方法 2:filter + indexOf(时间复杂度 O(n²))

const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);

方法 3:reduce(时间复杂度 O(n²))

const unique = arr => arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);

性能对比
• Set 最快(哈希表特性),reduce 和 filter 需遍历检查重复项,适合小规模数据


3. 深拷贝与浅拷贝

深拷贝实现(处理循环引用、特殊类型)

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (hash.has(obj)) return hash.get(obj); // 解决循环引用
  
  const clone = new obj.constructor();
  hash.set(obj, clone);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
  return clone;
}

JSON 方法的局限性
• 无法处理函数、Symbol、undefined
• 丢失原型链、无法处理循环引用


4. Promise 并发控制

实现 limitRequest(urls, max)

async function limitRequest(urls, max) {
  const results = [];
  const executing = [];
  for (const url of urls) {
    const p = fetch(url).then(res => {
      executing.splice(executing.indexOf(p), 1);
      return res;
    });
    results.push(p);
    executing.push(p);
    if (executing.length >= max) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

原理
• 维护执行队列,通过 Promise.race 动态控制并发数


5. CSS 实现三角形与垂直居中

三角形(利用 border)

.triangle {
  width: 0;
  height: 0;
  border: 20px solid transparent;
  border-top-color: red;
}

Flex 垂直居中(两种方式)

  1. 容器设置:
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}
  1. 子元素设置:
.child {
  margin: auto;
}

在前端开发中,实现元素的水平垂直居中是常见的布局需求。以下是几种主流方法及其适用场景,综合多个技术文档整理而来:

一、Flexbox 弹性布局(现代推荐方案)

通过设置父容器为弹性布局,可快速实现子元素居中:

.parent {
  display: flex;
  justify-content: center;  /* 水平居中 */
  align-items: center;      /* 垂直居中 */
}

该方法无需子元素固定尺寸,支持响应式布局,适用于大多数现代浏览器。

二、Grid 网格布局(高效简洁方案)

利用网格布局的place-items属性可一键实现居中:

.parent {
  display: grid;
  place-items: center;  /* 同时定义行列对齐 */
}

Grid 布局特别适合复杂布局场景,代码简洁且维护性强。

三、绝对定位 + Transform(传统兼容方案)

适用于需要脱离文档流的元素:

.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);  /* 自适应宽高 */
}

优势在于不依赖元素尺寸,但需要父容器设置定位上下文。

四、行内元素特殊处理

针对行内或文本元素:

  1. 单行文本:父元素设置相同的line-height与高度
  2. 多行内容:结合伪元素实现垂直居中:
.parent {
  text-align: center;
}
.parent::before {
  content: '';
  display: inline-block;
  height: 100%;
  vertical-align: middle;
}
.child {
  display: inline-block;
  vertical-align: middle;
}

这种方法适合表单控件、按钮等行内元素的对齐。

五、固定尺寸元素的经典方案

当元素尺寸已知时:

.child {
  width: 200px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -50px;  /* 高度的一半 */
  margin-left: -100px; /* 宽度的一半 */
}

或使用calc()动态计算:

.child {
  top: calc(50% - 50px);
  left: calc(50% - 100px);
}

适合传统项目或需要精确控制尺寸的场景。

扩展建议:

响应式布局优先选择 Flex/Grid 方案
动态内容推荐使用 Transform 方案
IE兼容需注意 Grid 布局仅支持 IE10+,Flexbox 需添加-ms-前缀
• 容器高度未知时,Flex/Grid 仍能保持居中效果


6. 括号匹配算法(栈结构)

实现

function isValidBrackets(str) {
  const stack = [];
  const map = { '(': ')', '[': ']', '{': '}' };
  for (const char of str) {
    if (map[char]) {
      stack.push(char);
    } else if (Object.values(map).includes(char)) {
      if (stack.length === 0 || map[stack.pop()] !== char) return false;
    }
  }
  return stack.length === 0;
}

原理
• 左括号入栈,右括号匹配栈顶元素


7. 函数柯里化

通用柯里化函数

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return (...args2) => curried.apply(this, args.concat(args2));
    }
  };
}
// 示例:const add = (...nums) => nums.reduce((a, b) => a + b, 0);

调用形式
curry(add)(1)(2)(3)() 输出 6


8. 数组扁平化

方法 1:递归

function flatten(arr) {
  return arr.reduce((acc, cur) => 
    acc.concat(Array.isArray(cur) ? flatten(cur) : cur), []);
}

方法 2:ES6 flat

const flatten = arr => arr.flat(Infinity);

方法 3:扩展运算符

function flatten(arr) {
  while (arr.some(Array.isArray)) {
    arr = [].concat(...arr);
  }
  return arr;
}

9. 虚拟 DOM Diff 算法(简化版)

实现差异对比

function diff(oldNode, newNode) {
  const patches = [];
  if (oldNode.type !== newNode.type) {
    patches.push({ type: 'REPLACE', newNode });
  } else if (oldNode.props !== newNode.props) {
    patches.push({ type: 'UPDATE_PROPS', props: newNode.props });
  }
  // 递归对比子节点(需处理 Key 优化复用)
  return patches;
}

Key 的作用
• 识别节点身份,减少不必要的重新渲染


10. 简易 EventEmitter

实现

class EventEmitter {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    (this.events[event] ||= []).push(callback);
  }
  emit(event, ...args) {
    (this.events[event] || []).forEach(cb => cb(...args));
  }
  off(event, callback) {
    this.events[event] = this.events[event]?.filter(cb => cb !== callback);
  }
}

异步队列支持
• 在 emit 中使用 setTimeoutPromise.resolve() 包裹回调


代码健壮性建议
• 防抖/节流:处理 this 绑定和参数透传
• 深拷贝:使用 WeakMap 避免内存泄漏
• Promise 并发:处理请求失败和重试机制

原理总结
• 闭包:防抖/节流、柯里化通过闭包保存状态
• 原型链:EventEmitter 基于类实现事件管理
• 异步队列:Promise 并发控制依赖事件循环调度

;