前端常见手撕题

内容分享2周前发布
0 0 0

目录

异步

Promise

手写Promise

手写Promise.race

Promise.all

myPromiseAll

批量请求、限制并发

只返回所有失败请求信息

手写Promise.allSettled

类和对象

new

深拷贝

给普通对象加上迭代器

函数

显示绑定

闭包

防抖

节流

数组

数组去重

对象数组去重

set去重拼接的key-value

数组转树

Map实现

树转数组

1.深度优先遍历

2.非递归深度优先遍历

3.广度优先遍历

4.reduce递归实现

数组扁平化

时间

时间求平均

前端常见手撕题型

数字千分位

版本号从大到小排序

CSS

双栏布局

可拖动双栏布局

总结要点

flex布局

grid布局

画圆

画三角形

算法

三数之和

第K大


异步

Promise

手写Promise



手写Promise
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
    state = PENDING
    result = undefined
    constructor(func){
        // 改状态:pending -> fulfilled 记录原因
        const resolve = (result)=>{
            if(this.state == PENDING){
                this.state = FULFILLED
                this.result = result
            }
        }
        // 改状态:pending -> rejected 记录原因
        const reject = (result)=>{
            if(this.state == PENDING){
                this.state = REJECTED
                this.result = result
            }
        }
        func(resolve,reject)
    }
    then(onFulfilled, onRejected){
         onFulfilled =  typeof onFulfilled === 'function'? onFulfilled:x=>x
         onRejected =  typeof onRejected === 'function'? onFulfilled:x=>{throw x}
         if(this.state == FULFILLED)onFulfilled(this.result)
         if(this.state == REJECTED)onRejected(this.result)
    }
}
const p = new MyPromise((resolve, reject)=>{
    setTimeout(()=>{
        resolve('suc')
    },1000)
    setTimeout(()=>{
        reject('error')
    },1000)
})
p.then(res=>{
    console.log('成功回调',res)
},err=>{
    console.log('失败回调',err)
})

手写Promise.race

竞速,只取第一个结束的结果



function myPromiseRace(promises) {
  return new Promise((resolve, reject) => {    
    // 遍历所有传入的 Promise(非Promise值会被包装成resolved状态的Promise,本就是Promise对象的话就没影响)     
    for (const p of promises) {      
      Promise.resolve(p).then(   // 关键:每个Promise决议后,立即调用resolve/reject(仅第一次生效)  两个参数:成功回调,失败回调            
        (value) => resolve(value), // 任意一个成功,直接resolve         
        (reason) => reject(reason) // 任意一个失败,直接reject       
      );     
     }   
  });
}

Promise.all

成功时返回结果数组,失败时返回第一个失败原因

myPromiseAll


function myPromiseAll(iterable) {
  // 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 1. 校验输入是否为可迭代对象(如数组、Set 等)
    if (typeof iterable[Symbol.iterator] !== 'function') {
      reject(new TypeError('The input is not iterable'));
      return;
    }
 
    // 2. 将可迭代对象转为数组(方便处理索引和长度)
    const promises = Array.from(iterable);
    const len = promises.length;
 
    // 3. 若输入为空数组,直接 resolve 空数组
    if (len === 0) {
      resolve([]);
      return;
    }
 
    // 4. 初始化结果数组(按输入顺序存储结果)和完成计数器
    const result = new Array(len);
    let resolvedCount = 0;
 
    // 5. 遍历每个元素,处理 Promise 或普通值
    promises.forEach((item, index) => {
      // 用 Promise.resolve 包装,确保 item 是 Promise(处理非 Promise 值的情况)
      Promise.resolve(item)
        .then((value) => {
          // 存储当前结果(按索引位置,保证顺序)
          result[index] = value;
          // 计数器加 1,判断是否所有 Promise 都完成
          resolvedCount++;
          if (resolvedCount === len) {
            resolve(result); // 所有完成,返回结果数组
          }
        })
        .catch((reason) => {
          // 若有任何一个失败,立即 reject(仅触发一次)
          reject(reason);
        });
    });
  });
}
批量请求、限制并发

思路:每个请求结束后,看队列中似乎有有未完成的请求,如果有,就按顺序进行下一个请求。



// @returns {Promise<Array>} 所有请求的结果数组(按原任务顺序排列,包含成功结果或错误信息)
function batchRequest(tasks, limit = 3) {
  // 边界处理:任务为空时直接返回空数组
  if (!tasks || tasks.length === 0) {
    return Promise.resolve([]);
  }
 
  const results = new Array(tasks.length); // 按原顺序存储结果
  let index = 0; // 当前待执行的任务索引
  let running = 0; // 当前正在执行的请求数
 
  return new Promise((resolve) => {
    // 执行单个任务的函数
    const runTask = () => {
      // 终止条件:所有任务已执行且无运行中请求
      if (index >= tasks.length && running === 0) {
        resolve(results);
        return;
      }
 
      // 若未达并发限制且任务还未执行完,继续执行
      while (running < limit && index < tasks.length) {
        const taskIndex = index; // 记录当前任务的原始索引(保证结果顺序)
        const task = tasks[index]; // 取出当前任务
        index++;
        running++;
 
        // 执行任务(任务需返回Promise)
        Promise.resolve(task())
          .then((res) => {
            results[taskIndex] = { success: true, data: res }; // 成功结果
          })
          .catch((err) => {
            results[taskIndex] = { success: false, error: err }; // 错误信息
          })
          .finally(() => {
            running--; // 任务完成,运行数减1
            runTask(); // 继续执行下一个任务
          });
      }
    };//单个任务执行的函数 END
 
    // 启动第一批任务
    runTask();
  });
}
//测试
const url='https://www.baidu.com/s?wd=javascript';
// tasks: Array<Function> - 请求任务数组,每个元素是返回Promise的函数(如 () => fetch(url))
const urls=new Array(100).fill(()=>fetch(url));
console.log(`开始 ${new Date()}`);
(async()=>{
    const res=await batchRequest(urls,30);
    console.log(res[0].success)
    console.log(res[0].data)
    console.log(`结束 ${new Date()}`);
})();


{
  "success": true,
  "data": {}//就是下面的Response
}
Response {
  [Symbol(realm)]: null,
  [Symbol(state)]: {
    aborted: false,
    rangeRequested: false,
    timingAllowPassed: true,
    requestIncludesCredentials: false,
    type: 'default',
    status: 200,
    timingInfo: {
      startTime: 80.16190000250936,
      redirectStartTime: 80.16190000250936,
      redirectEndTime: 350.71169999986887,
      postRedirectStartTime: 350.71169999986887,
      finalServiceWorkerStartTime: 0,
      finalNetworkResponseStartTime: 0,
      finalNetworkRequestStartTime: 0,
      endTime: 0,
      encodedBodySize: 2027,
      decodedBodySize: 1488,
      finalConnectionTimingInfo: null
    },
    cacheState: '',
    statusText: 'OK',
    headersList: HeadersList {
      cookies: [Array],
      [Symbol(headers map)]: [Map],
      [Symbol(headers map sorted)]: null
    },
    urlList: [ [URL], [URL] ],
    body: { stream: undefined }
  },
  [Symbol(headers)]: HeadersList {
    cookies: [
      'BAIDUID=345A346E80EBB07FD536F6894EFFF2B4:FG=1; expires=Thu, 19-Nov-26 00:18:30 GMT; max-age=31536000; path=/; domain=.baidu.com; version=1'
    ],
    [Symbol(headers map)]: Map(12) {
      'connection' => [Object],
      'content-length' => [Object],
      'content-type' => [Object],
      'date' => [Object],
      'etag' => [Object],
      'last-modified' => [Object],
      'p3p' => [Object],
      'server' => [Object],
      'set-cookie' => [Object],
      'strict-transport-security' => [Object],
      'tracecode' => [Object],
      'vary' => [Object]
    },
    [Symbol(headers map sorted)]: null
  }
}

只返回所有失败请求信息



function PromiseAllFailures(iterable) {
  return new Promise((resolve) => { // 始终 resolve,不主动 reject
    // 1. 校验输入是否为可迭代对象
    if (typeof iterable[Symbol.iterator] !== 'function') {
      resolve([new TypeError('The input is not iterable')]); // 将错误作为失败原因
      return;
    }
 
    // 2. 转换为数组处理
    const promises = Array.from(iterable);
    const len = promises.length;
 
    // 3. 空输入直接返回空数组
    if (len === 0) {
      resolve([]);
      return;
    }
 
    // 4. 收集失败原因的数组,和完成计数器
    const failures = [];
    let settledCount = 0;
 
    // 5. 遍历每个 Promise,等待其完成
    promises.forEach((item) => {
      // 用 Promise.resolve 包装,统一处理非 Promise 值(非 Promise 视为成功,不加入失败数组)
      Promise.resolve(item)
        .then(() => {
          // 成功:不做处理(不加入 failures)
        })
        .catch((reason) => {
          // 失败:将原因加入 failures 数组
          failures.push(reason);
        })
        .finally(() => {
          // 无论成功/失败,计数器加 1
          settledCount++;
          // 所有 Promise 都完成后,返回 failures 数组
          if (settledCount === len) {
            resolve(failures);
          }
        });
    });
  });
}

手写Promise.allSettled



function myPromiseAllSettled(iterable) {
  return new Promise((resolve, reject) => {
    // 1. 校验输入是否为可迭代对象(如数组、Set 等)
    if (typeof iterable[Symbol.iterator] !== 'function') {
      reject(new TypeError('The input is not iterable'));
      return;
    }
 
    // 2. 将可迭代对象转为数组(方便处理索引和长度)
    const promises = Array.from(iterable);
    const len = promises.length;
 
    // 3. 若输入为空数组,直接返回成功的空数组
    if (len === 0) {
      resolve([]);
      return;
    }
 
    // 4. 初始化结果数组(按输入顺序存储)和完成计数器
    const results = new Array(len);
    let settledCount = 0;
 
    // 5. 遍历每个元素,处理 Promise 或普通值
    promises.forEach((item, index) => {
      // 用 Promise.resolve 包装,确保 item 被视为 Promise(非 Promise 值会被转为 fulfilled 状态)
      Promise.resolve(item)
        .then((value) => {
          // 成功:存储 { status: 'fulfilled', value }
          results[index] = { status: 'fulfilled', value };
        })
        .catch((reason) => {
          // 失败:存储 { status: 'rejected', reason }
          results[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          // 无论成功/失败,计数器加 1
          settledCount++;
          // 所有 Promise 都完成后,返回结果数组
          if (settledCount === len) {
            resolve(results);
          }
        });
    });
  });
}

类和对象

new

https://juejin.cn/post/7533521571568336938?searchId=2025102300185342F74CACC0EAEDE3BCA4

创建新对象:创建一个全新的空对象{}链接原型:将这个新对象的[[Prototype]](即__proto__)指向构造函数的prototype对象绑定this:将构造函数中的this绑定到这个新对象执行构造函数:执行构造函数中的代码(通常用于初始化对象)
当构造函数显式return一个对象(数组、函数等)时,会忽略(覆盖)原本创建的对象。当return基本类型,或者无return时,则返回正常对象。 返回对象:如果构造函数没有返回对象,则自动返回创建的新对象。否则返回return语句中的对象。



function mynew(constructor,...args){
    // var obj = new Object(); 1.创建空对象
    // obj.__proto__ = constructor.prototype; 2.新对象的原型指向(继承)构造函数原型对象    
    const obj = Object.create(constructor.prototype);
    const res = constructor.apply(obj, args); // 3.利用apply将构造函数中的this绑定到新对象,并立即执行一次
    // 4.返回对象
    return typeof res === 'object' ? res || obj : obj; // res instanceof Object?res:obj
}
//示例
function Person(name,age){
    this.name=name;
    this.age=age;
}
Person.prototype.say=function(){console.log(this.name)}//不可用()=>{}
let p = mynew(Person,'lili',123)
console.log(p);
p.say();

深拷贝



function deepClone(obj, hash = new WeakMap()){
    if (typeof obj != 'object' ||  obj == null) return obj;//(0)基本类型、function类型
    if (hash.has(obj)) return hash.get(obj);// 处理循环引用
 
    let cloneObj
 
    // (1)处理日期对象
    if (obj instanceof Date) { 
        cloneObj = new Date();
        cloneObj.setTime(obj.getTime());
        hash.set(obj, cloneObj);
        return cloneObj;
    }
    // (2)处理正则对象
    if (obj instanceof RegExp) { 
        cloneObj = new RegExp(obj.source, obj.flags);
        hash.set(obj, cloneObj);
        return cloneObj;
    }
    // (3)处理数组和对象
    // 调用原型链顶端不被改写的toString方法
    cloneObj = Object.prototype.toString.call(obj) === '[object Array]' ? []:{}
    cloneObj = Array.isArray(obj)?[]:{} // 稳定的api,在实际项目中常用来判断是否是数组。
    hash.set(obj, cloneObj);  
    // 递归拷贝属性  
    Reflect.ownKeys(obj).forEach(key => {      //(3-1) 获取对象所有自有属性
        cloneObj[key] = deepClone(obj[key], hash);
    });
    for ( let key in obj ){                    //(3-2) in 会遍历原型链上的属性
        if(obj.hasOwnProperty(key)){ // hasOwnProperty会忽略从原型链上继承到的属性。
            cloneObj[key] = deepclone(obj[key],hash)
        }
    }
    return cloneObj;
}

给普通对象加上迭代器

一个对象如果要具备可被 for…of 循环调用的 Iterator 接口,就必须在其 [Symbol.iterator] 的属性上部署迭代器生成方法(或者原型链上的对象具有该方法)。迭代器对象根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性(具体参考-常见手撕-类和对象)。



let obj = {
        name:'jack',
        age:20,
        job:'web engineer',
        [Symbol.iterator](){} // 1.本质上是对象的一个特殊属性(键为 Symbol.iterator 的方法)
    }
//  2.单独添加迭代器属性
obj[Symbol.iterator] = function() {
  const self = this;
  const keys = Object.keys(self); // 获取对象的可枚举属性
  let index = 0; // 手动管理index
  return {
    next() {
      if (index < keys.length) {// 返回当前属性值 this[0] this[1]
        return { value: self[keys[index++]], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
};
// 2. 用 Generator 函数(function*)定义迭代器(结合 yield 和属性遍历)
obj[Symbol.iterator] = function* () {
  // yield关键字只能在Generator函数的直接作用域内使用,所以这里不可用forEach。
  for (const key of Object.keys(this)) {// 获取对象的可枚举属性
    yield this[key]; // 逐个返回属性值(自动管理迭代状态)
  }
};
for(const value of obj){
    console.log(value) // jack 20 web engineer
}

函数

显示绑定



Function.prototype.myCall = function (thisTmp, ...args) {
  const prop = Symbol()
  thisArg[prop] = this; //添加唯一属性名
  let res = thisTmp[prop](...args);//运行方法
  delete thisTmp[prop]// 运行完删除属性
  return res;
};
Function.prototype.myApply = function (thisTmp, args) {
  thisTmp = thisTmp || global
  args = args || []
  const prop = Symbol();
  thisTmp[prop] = this;
  let res = thisTmp[prop](...args);// 用...运算符展开传入
  delete thisTmp[prop];
  return res;
};
Function.prototype.myBind = function(thisTmp, ...args) {
  const self = this
  const res = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound
    if (isNew) {
      return new self(...finalArgs)
    } 
    return self.apply(thisTmp, finalArgs)
  }
  return res
}

闭包

防抖



function debounce(fn,delay){
    let timer = null // 用闭包保存定时器
    return function(...args) {
    if(timer)clearTimeout(timer)
        timer = setTimeout(()=>{
            fn.apply(this,args)
            timer=null
        },delay)
    }
}

节流



// 追求轻量高效用时间戳
function throttle(fn, interval) {
    let lastTime = 0
    return function(...args) {
        const now = Data.now()
        if (now - lastTime >= interval){
            fn.apply(this, args)
            lastTime = now
        }
    }
}
//追求严格间隔用setInterval
function throttle(fn, interval) {
    let timer = null; // 存储定时器ID
    let isFirstTime = true; // 标记是否首次触发
    return function(...args) {
        const context = this; // 保存当前上下文
        if (isFirstTime) { // 首次触发时立即执行一次
            fn.apply(context, args);
            isFirstTime = false;
            return;
        }
        if (timer) { // 如果定时器已存在,则不重复创建
            return;
        }
        // 启动定时器,固定间隔执行函数
        timer = setInterval(() => {
            fn.apply(context, args);
            clearInterval(timer);// 执行后清除定时器,等待下一次触发
            timer = null;
        }, interval);
    };
}

数组

数组去重

const arr = [1, 2, 2, '2', 3, NaN, NaN, undefined, undefined, null, null, { a: 1 }, { a: 1 }];



// new Set() O(n): 支持 NaN 去重
function uniqueBySet(arr){// Set 会将 NaN 视为相同值,解决了 NaN !== NaN 的判断问题。
    return [...new Set(arr)]// 或 Array.from(new Set(arr)) 
}
//map + splice(原地去重)
function uniqueBySplice1(arr) {
  if (arr.length <= 1) return arr.length;
 
  const map = new Map(); // map:记录已出现的元素
  let i = 0;
  while (i < arr.length) {
    const cur = arr[i];
    // 处理 NaN(因 NaN !== NaN,需特殊判断)
    const key = Number.isNaN(cur) ? 'NaN' : cur;
 
    if (map.has(key)) {
      arr.splice(i, 1);//下一个元素自动移到i位置
    } else {
      map.set(key, true);
      i++;
    }
  }
  return arr.length;
}
//双层循环 + splice(原地去重)
function uniqueBySplice2(arr) {
  if (arr.length <= 1) return arr.length;
 
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      const isEqual = arr[i] === arr[j] ||(Number.isNaN(arr[i]) && Number.isNaN(arr[j]));
 
      if (isEqual) {
        arr.splice(j, 1); // 删除重复元素
        j--; // 索引回退(因元素前移)
      }
    }
}
// filter+indexOf O(n^2): NaN被全部过滤
function uniqueByFilter(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}
//reduce+includes O(n^2): 支持 NaN 去重
function uniqueByReduce(arr) {
  return arr.reduce((acc, item) => {
    if (!acc.includes(item)) { // includes 可识别 NaN
      acc.push(item);
    }
    return acc;
  }, []);
}
// new Map() O(n): 支持 NaN 去重
function uniqueByMap(arr) {
  const map = new Map();
  const result = [];
  for (const item of arr) {
    if (!map.has(item)) { // Map 可识别 NaN(视为相同键)
      map.set(item, true);
      result.push(item);
    }
  }
  return result;
}
//对象键值对(利用对象属性唯一)O(n): 场景用于区分值相同类型不同的元素(以上方法也能实现)
function uniqueByObject(arr) {
  const obj = {};
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    const key = typeof item + JSON.stringify(item);
    if (!obj[key]) {
      obj[key] = true;
      result.push(item);
    }
  }
  return result;
}

对象数组去重

const
users
=
[


{
id:
1
,
name:
'李四'
},


{
id:
1
,
name:
'张三'
},


{
id:
2
,
name:
'李四'
},


[{
id:
1
,
name:
'张三'
}, {
id:
3
,
name:
'王五'
}],


{
id:
2
,
name:
'李四'
},


{
id:
4
,
name:
'赵六'
}


];

set去重拼接的key-value



/**
 * 对象数组去重函数
 * @param {Array} arr - 需要去重的数组,每个元素可能嵌套了数组
 * @param {string|Array} keyOrKeys - 去重依据的字段名或字段名数组
 * @returns {Array} 去重后的数组
 */
function deduplicateUsers(arr, keyOrKeys) {
    // 处理嵌套数组,将所有元素展平为一维数组
    const flatArray = [];
    const flatten = (item) => Array.isArray(item) ? item.forEach(flatten) : flatArray.push(item);
    arr.forEach(flatten);
 
    // 标准化key为数组形式,统一处理逻辑
    const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys];
    
    const set = new Set();// 用于记录已出现过的键组合
    const result = [];
 
    for (const item of flatArray) {
        // 对于每个对象,生成由指定字段值组成的唯一标识字符串
        let keyString = '';
        let isValid = true;
        
        for (const key of keys) {
            // 检查对象是否包含所有指定的字段
            if (!Object.prototype.hasOwnProperty.call(item, key)) {
                isValid = false;
                break;
            }
            // 将当前字段值添加到标识字符串中
            keyString += `${item[key]}|`; // 使用|作为分隔符,避免不同字段值拼接产生歧义
        }
 
        // 如果对象包含所有指定字段且未出现过,则添加到结果数组
        if (isValid && !set.has(keyString)) {
            set.add(keyString);
            result.push(item);
        }
    }
    return result;
}
 
// 按单个字段去重
const uniqueById = deduplicateUsers(users, 'id');
console.log(uniqueById); // uniqueById.length: 4
 
// 按多个字段去重
const uniqueByNameAndId = deduplicateUsers(users, ['id', 'name']);
console.log(uniqueByNameAndId); // uniqueByNameAndId.length: 5

数组转树

const data = [

{ id: 7, parentId: 6, name: “Node 2.1” },

{ id: 8, parentId: 6, name: “Node 2.2” },

{ id: 1, parentId: null, name: “Node 1” },

{ id: 2, parentId: 1, name: “Node 1.1” },

{ id: 3, parentId: 1, name: “Node 1.2” },

{ id: 4, parentId: 2, name: “Node 1.1.1” },

{ id: 5, parentId: 2, name: “Node 1.1.2” },

{ id: 6, parentId: 2, name: “Node 2” },

{ id: 9, parentId: null, name: “Node 9” },

];

Map实现



// Map实现-一次for循环
function listToTree(arr) {
  const idMap = new Map();
  const root = [];
 
  arr.forEach(item => {
    // 1. 初始化当前节点(若已存在则直接获取,避免重复创建)
    let curNode = idMap.get(item.id);
    if (!curNode) {
      curNode = { ...item, children: [] }; // 创建新节点(浅拷贝+初始化children:[])
      idMap.set(item.id, curNode);
    } else {
      curNode = { ...curNode, ...item };// 临时父节点,用实际属性更新
      idMap.set(item.id, curNode);
    }
 
    // 2. 处理父节点,建立父子关系
    if (item.parentId === null || item.parentId === undefined) { //根节点
      root.push(curNode);
    } else {//非根节点,找到其父节点,将当前节点加入父节点的children
      let parentNode = idMap.get(item.parentId);
      if (!parentNode) { //临时父节点:仅包含id和children,后续遍历到实际父节点时会更新属性
        parentNode = { id: item.parentId, children: [] };
        idMap.set(item.parentId, parentNode);
      }
      parentNode.children.push(curNode);
    }
  });
  return root;
}
// 测试
const tree = listToTree(data);
console.log(JSON.stringify(tree, null, 2));//tree or tree[0] ...tree
 
// Map实现-两次for循环(可保持新对象属性的插入顺序) 1.节点初始化并存入Map 2. 处理父节点,建立父子关系
function listToTree(arr) {
  const idmap = new Map();
  arr.forEach(item => {
    idmap.set(item.id, { ...item, children: [] });// ...展开运算符,浅拷贝一层,避免直接修改原对象
  });
 
  let root = [];// {} null or [] 数组可支持多个根节点
  arr.forEach(item => {
    const curNode = idmap.get(item.id); // 当前节点(从映射表获取)
    if (item.parentId === null) {//根节点
      root.push(curNode); // root = curNode; or root.push(curNode);
    } else {
      const parentNode = idmap.get(item.parentId); //非根节点,找到其父节点,将当前节点加入父节点的children
      parentNode && parentNode.children.push(curNode);
    }
  });
  return root; // 返回根节点对象(树形结构的顶层)
}

树转数组

const
tree
=
[{


id:
1
,
parentId:
null
,
name:
“Node 1”
,


children:
[


{


id:
2
,
parentId:
1
,
name:
“Node1.1”
,


children:
[{
id:
4
,
parentId:
2
,
name:
“Node1.1.1”
,
children:
[]},{
id:
5
,
parentId:
2
,
name:
“Node1.1.2”
,
children:
[]},],


},


{
id:
3
,
parentId:
1
,
name:
“Node 1.2”
,
children:
[] },


],


},


{
id:
9
,
parentId:
null
,
name:
“Node 9”
,
children:
[] },];

1.深度优先遍历



function treeToListDFS(tree) {
  const result = [];
  const traverse = (node) => {
    // 1.解构去除children属性(创建新对象,不改变原节点)
    const { children, ...newNode } = node;  
    result.push(newNode);
    // 2.递归处理子节点(若有children)
    if (children && children.length) {  
      children.forEach(child => traverse(child));
    }
  };
  // 遍历所有根节点
  tree.forEach(root => traverse(root));
  return result; //不含children的扁平数组
}
let arr=treeToListDFS(tree);
console.log(arr.sort((a,b)=>a.id-b.id));

2.非递归深度优先遍历



function treeToListDFSStack(tree) {
  const result = [];
  const stack = [...tree].reverse(); // 栈初始化:放入所有根节点
  while (stack.length) {
    const node = stack.pop(); // 弹出栈顶节点
 
    const { children, ...newNode } = node;  // 1.解构去除children属性(创建新对象,不改变原节点)
    result.push(newNode);    
 
    if (children && children.length) { //非递归处理子节点(逆序入栈,保障顺序)
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push(children[i]);
      }
    }
  }
  return result;
}

3.广度优先遍历



function treeToListBFS(tree) {
  const result = [];
  const queue = [...tree]; // 队列初始化:放入所有根节点
  let head = 0; // 用指针优化队列,替代shift()
  
  while (head < queue.length) {
    const node = queue[head++]; // 取出队首节点
 
    const { children, ...newNode } = node;// 1.解构去除children属性(创建新对象,不改变原节点)
    result.push(newNode);
 
    if (children && children.length) {// 处理子节点(顺序入队,按层级处理)
      queue.push(...children);
    }
  }
  return result;
}

4.reduce递归实现



function treeToListRe(tree) {
  return tree.reduce((res, node) => {
    const { children, ...newNode } = node;// 1.解构去除children属性
    return res.concat([newNode], treeToListRe(children || []));// 2. 递归处理子节点,合并结果
  }, []); // 初始累积值为[]
}

数组扁平化



const flatArr = arr.flat(Infinity);//参数为无限层
function flatten1(arr,depth = Infinity) {  
  return arr.reduce((ans, item) => {  
    const value=depth>0 && Array.isArray(item) ? flatten(item,depth-1) : item;
    return ans.concat(value);  
  }, []);
}
function flatten2(arr, depth=Infinity) {  
  let ans = [];  
  arr.forEach(item => {    
    ans=ans.concat( depth>0 && Array.isArray(item)?flatten(item):item );
  });  
  return ans;
}
// 其他方法 适用于元素为基本类型
const arr = [1, [2, [3]]];
const flatArr = arr.join(',').split(',').map(Number);
const flatArr = arr.toString().split(',').map(Number);//[1,2,3]

时间

时间求平均

const times = [“08: 30”, “09: 10”, “10: 20”]



/**
 * 计算时间字符串数组的平均时间
 * @param {Array<string>} timeArray - 时间数组,格式为["hh: mm", "hh: mm", ...](注意:mm前可能有空格,需兼容)
 * @returns {string} 平均时间,格式为"hh:mm"
 */
function averageTime(timeArray) {
  // 边界处理:空数组返回null
  if (!timeArray || timeArray.length === 0) {
    return null;
  }
 
  // 1. 将每个时间字符串转换为总分钟数
  const totalMinutesList = timeArray.map(timeStr => {
    const trimmed = timeStr.replace(/s+/g, ''); // 去除所有空格,统一格式为"hh:mm"
    const [hh, mm] = trimmed.split(':').map(Number); // 分割为小时和分钟并转数字
    return hh * 60 + mm; // 转换为总分钟数
  });
 
  // 2. 计算总分钟数的平均值(四舍五入取整)
  const total = totalMinutesList.reduce((sum, minutes) => sum + minutes, 0);
  const averageMinutes = Math.round(total / timeArray.length);
 
  // 3. 将平均分钟数转回hh:mm格式(处理超过24小时的情况)
  const totalMinutesInDay = 24 * 60; // 一天的总分钟数:1440
  const normalizedMinutes = averageMinutes % totalMinutesInDay; // 对24小时取模,确保在0-1439之间
 
  const hours = Math.floor(normalizedMinutes / 60); // 计算小时
  const minutes = normalizedMinutes % 60; // 计算分钟
 
  // 格式化:确保小时和分钟为两位数(补零)
  const formattedHours = String(hours).padStart(2, '0');
  const formattedMinutes = String(minutes).padStart(2, '0');
 
  return `${formattedHours}:${formattedMinutes}`;
}

前端常见手撕题型

数字千分位



function addThousandSeparator(num) {
  const str = String(num);
  let sign = ''; // 存储负号(若有)
  let intPart = '';
  let decPart = '';
 
  // 步骤1:判断是否为负数,提取负号
  if (str.startsWith('-')) {
    sign = '-'; // 记录负号
    const s = str.slice(1); // 截取去掉负号后的部分
    [intPart, decPart] = s.split('.'); // 分割正数字符串
  } else {
    // 非负数直接分割整数和小数部分
    [intPart, decPart] = str.split('.');
  }
 
  let res = '';
  // 步骤2:倒序处理整数部分,每3位加逗号
  for (let i = intPart.length - 1, count = 0; i >= 0; i--) {
    res = intPart[i] + res;
    count++;
    // 不是最后一位且计数满3,加逗号
    if (count % 3 === 0 && i !== 0) {
      res = ',' + res;
    }
  }
 
  // 步骤3:拼接负号、整数部分、小数部分(若有)
  return decPart?`${sign}${res}.${decPart}:`${sign}${res};
}
 
// 测试案例
console.log(addThousandSeparator(-1234.56)); // "-1,234.56"
console.log(addThousandSeparator(-9876543)); // "-9,876,543"
console.log(addThousandSeparator(123456.789)); // "123,456.789"
console.log(addThousandSeparator(-0.123)); // "-0.123"

版本号从大到小排序



/**
 * 版本号从大到小排序
 * @param {string[]} versions - 待排序的版本号数组
 * @returns {string[]} 排序后的版本号数组
 */
function sortVersionsDesc(versions) {
  return [...versions].sort((v1, v2) => { //浅拷贝一层
    // 1. 拆解版本号为数字数组(处理空字符串)
    const arr1 = v1.split('.').map(num => parseInt(num, 10) || 0);
    const arr2 = v2.split('.').map(num => parseInt(num, 10) || 0);
    
    const maxLen = Math.max(arr1.length, arr2.length); // 2. 取最长长度,逐位比较
    for (let i = 0; i < maxLen; i++) {
      const num1 = i < arr1.length ? arr1[i] : 0;// 位数不足补 0
      const num2 = i < arr2.length ? arr2[i] : 0;
      if (num1 > num2) return -1;
      if (num1 < num2) return 1;
    }
    return 0;
  });
}
 
// 示例测试
const versions = ["1.0.1", "2.1", "3", "2.0.1", "1.10", "1.2"];
const sortedVersions = sortVersionsDesc(versions);
console.log(sortedVersions); 
// 输出:["3", "2.1", "2.0.1", "1.10", "1.2", "1.0.1"]

CSS

双栏布局

可拖动双栏布局

总结要点

布局结构
使用flex布局实现双栏水平排列,容器占满视口(100vh+100vw)。三部分组成:左侧栏(left-panel)、分隔线(resizer)、右侧栏(right-panel)。 样式设计
分隔线设置cursor: col-resize提示可拖动,z-index:10确保可被点击。左右栏添加min-width限制最小宽度,避免被拖到消失;transition: width 0.1s;使宽度变化更平滑。box-sizing: border-box确保padding不影响元素实际宽度。 拖动核心逻辑
事件联动:mousedown(开始拖动)→ mousemove(更新宽度)→ mouseup(结束拖动),全局监听mousemove和mouseup避免鼠标移出分隔线后失效。状态记录:拖动开始时记录初始鼠标位置(startX)和左右栏宽度(leftWidth/rightWidth),作为计算基准。宽度计算:通过dx = 当前鼠标X – 初始X计算拖动距离,动态更新左右栏宽度(左侧增减 = 右侧减增)。 性能优化
节流控制:mousemove高频触发,用节流函数限制 10ms 执行一次,减少 DOM 更新次数,避免卡顿。边界检查:确保新宽度不小于min-width,防止布局异常。资源释放:拖动结束后移除全局事件监听,避免内存泄漏。 兼容性处理
基于 CSS 像素计算(clientX/getBoundingClientRect),自动适配浏览器缩放。初始及窗口变化时更新最小宽度(updateMinWidths),适配动态样式变化。

该实现兼顾了交互流畅性、性能和兼容性,适合作为可拖动双栏布局的通用方案(如编辑器、管理系统等场景)。



<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>可拖动双栏布局(修复缩放分隔线消失)</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
 
    body, html {
      height: 100%;
      overflow: hidden;
    }
 
    .container {
      display: flex;
      height: 100vh;
      width: 100vw;
    }
 
    .left-panel {
      min-width: 200px;
      width: 30%;
      background: #f0f0f0;
      padding: 20px;
      transition: width 0.1s;
    }
 
    .resizer {
      /* 核心优化1:最小宽度+动态适应,确保缩放时不消失 */
      min-width: 4px; /* 最小宽度,防止过细 */
      width: 6px;
      background: #ccc;
      cursor: col-resize;
      user-select: none;
      position: relative;
      z-index: 100; /* 核心优化2:提高层级,避免被覆盖 */
      /* 核心优化3:添加边框和阴影,增强视觉存在感 */
      border-left: 1px solid #999;
      border-right: 1px solid #999;
      box-shadow: 0 0 0 1px rgba(0,0,0,0.1);
    }
 
    .resizer:hover {
      background: #999;
      /* hover时增加宽度,提升可点击性 */
      width: 8px;
    }
 
    .right-panel {
      min-width: 300px;
      flex: 1;
      background: #fff;
      padding: 20px;
      transition: width 0.1s;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="left-panel">左侧栏</div>
    <div class="resizer"></div>
    <div class="right-panel">右侧栏(缩放时分隔线可见)</div>
  </div>
 
  <script>
    function throttle(fn, interval) {
      let lastTime = 0;
      return function(...args) {
        const now = Date.now();
        if (now - lastTime >= interval) {
          fn.apply(this, args);
          lastTime = now;
        }
      };
    }
 
    const resizer = document.querySelector('.resizer');
    const leftPanel = document.querySelector('.left-panel');
    const rightPanel = document.querySelector('.right-panel');
 
    let isResizing = false;
    let startX;
    let leftWidth;
    let rightWidth;
    let minLeftWidth;
    let minRightWidth;
 
    // 新增:监听浏览器缩放,动态调整resizer宽度
    function updateResizerOnZoom() {
      // 获取当前页面缩放比例(基于window.devicePixelRatio)
      const zoom = window.devicePixelRatio || 1;
      // 缩放比例越高,resizer宽度适当增加(但限制最大宽度)
      const newWidth = Math.min(6 + Math.round(zoom * 2), 12); // 6px基础 + 缩放补偿,最大12px
      resizer.style.width = `${newWidth}px`;
    }
 
    const updateMinWidths = () => {
      minLeftWidth = parseInt(getComputedStyle(leftPanel).minWidth);
      minRightWidth = parseInt(getComputedStyle(rightPanel).minWidth);
      updateResizerOnZoom(); // 同步更新resizer宽度
    };
 
    window.addEventListener('resize', updateMinWidths);// 监听窗口缩放/resize事件
    updateMinWidths();// 初始调用
 
    resizer.addEventListener('mousedown', (e) => {
      isResizing = true;
      startX = e.clientX;
      leftWidth = leftPanel.getBoundingClientRect().width;
      rightWidth = rightPanel.getBoundingClientRect().width;
 
      e.preventDefault();
      e.stopPropagation();
 
      const handleMouseMove = throttle((e) => { //鼠标移动回调函数执行-节流
        if (!isResizing) return;
        e.preventDefault();
 
        const dx = e.clientX - startX;
        const newLeftWidth = Math.round(leftWidth + dx);
        const newRightWidth = Math.round(rightWidth - dx);
 
        if (newLeftWidth >= minLeftWidth && newRightWidth >= minRightWidth) {
          leftPanel.style.width = `${newLeftWidth}px`;
          rightPanel.style.width = `${newRightWidth}px`;
          rightPanel.style.flex = 'none';
        }
      }, 10);
 
      const handleMouseUp = () => {
        isResizing = false;
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
 
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    });
  </script>
</body>
</html>

flex布局



.flex-container {       
  display: flex; /* 核心:启用flex布局 */       
  gap: 10px; /* 子元素之间的间距(可选,更美观) */       
  width: 100%; /* 父容器占满宽度,可根据需求调整 */       
  height: 300px; /* 自定义高度,也可自适应内容 */     
}     
/* 子元素:实现50%均分 */     
.flex-item {       
  flex: 1; /* 核心:子元素均分父容器剩余空间,实现50% */      
  min-width: 0; /* 解决文本溢出问题(关键兼容) */       
  background: #f0f0f0;       
  padding: 20px;       
border-radius: 8px;     }

grid布局



<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3×3布局(宽高比1:2,间隔10px)</title>
  <style>
    /* 3×3布局容器,宽高比1:2,间隔10px,容器居中显示 */
    .grid-container {
      display: grid;
      grid-template-columns: repeat(3, 1fr); 
      gap: 10px; 
      max-width: 900px; 
      margin: 0 auto; 
    }
    /* 方式一:aspect-ratio: 1 / 2; 现代浏览器支持,直接定义宽高比*/
    .grid-item {
      aspect-ratio: 1 / 2; 
      background-color: #4a90e2;
      color: white;
      font-size: 24px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 4px;
    }
 
    /* 方式二:
      padding-top: 200%; 基于宽度width计算,实现)响应式宽高比
      position: absolute; 相对已定位的父元素定位,如父元素设置relative定位 
    */
      .grid-item {
        background-color: #4a90e2;
        color: white;
        font-size: 24px;
        position: relative;
        width: 100%;
        padding-top: 50%; 
      }
      .grid-item span {
        position: absolute; 
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }
  </style>
</head>
<body>
  <div class="grid-container">
    <!-- 3×3共9个元素,内容1-9 -->
    <div class="grid-item"><span>1</span></div>
    <div class="grid-item"><span>2</span></div>
    <div class="grid-item"><span>3</span></div>
    <div class="grid-item"><span>4</span></div>
    <div class="grid-item"><span>5</span></div>
    <div class="grid-item"><span>6</span></div>
    <div class="grid-item"><span>7</span></div>
    <div class="grid-item"><span>8</span></div>
    <div class="grid-item"><span>9</span></div>
  </div>
</body>

画圆

核心属性是border-radius



/* 圆形 */
.circle {  
  width: 100px;    /* 宽高必须相等,否则是椭圆 */  
  height: 100px;  
  background: #42b983; /* 圆形颜色 */  
  border-radius: 50%;  /* 核心属性:圆角值设为50% */
}

画三角形

三角形:两边透明border+对边有颜色

直角三角形:只需保留 两个相邻的border(一个有颜色、一个透明),就能实现直角效果



/* 向上的三角形(可改方向) */
.triangle {  
  width: 0; /* 宽高设为0,靠border撑出形状 */  
  height: 0 /* 左右border透明,底部border设颜色(控制三角形方向) */ 
  border-left: 50px solid transparent;  
  border-right: 50px solid transparent; 
  border-bottom: 86.6px solid #ff4400; /* 三角形颜色+高度 */
}
.right-triangle {       
  width: 0;       
  height: 0;                      
  border-top: 50px solid #42b983; /* 上边框(有颜色) */       
  border-right: 50px solid transparent; /* 右边框(透明) */     
}

算法

三数之和

给你一个包含 n 个整数的数组 nums,判断nums中是否存在三个元素a,b,c ,使得 a + b + c = 0 ?(负数)找出所有满足条件且不重复的三元组。输入:nums= [-1,0,1,2,-1,-4] 输出:[[-1,0,1],[-1,-1,2]]



function abc0(arr){
    let res=[];
    arr.sort((a,b)=>a-b);
    let n=arr.length;
    for(let i=0;i<n;i++){
        if(arr[i]>0){
            break;
        }
        if(i>0&&arr[i]===arr[i-1]){
            continue;
        }
        let l=i+1;
        let r=n-1;
        while(l<r){
            let sum=arr[i]+arr[l]+arr[r];
            if(sum===0){
                res.push([arr[i],arr[l],arr[r]]);
                while(l<r&&arr[l]===arr[l+1]){
                    l++;
                }
                while(l<r&&arr[r]==arr[r-1]){
                    r--;
                }
                l++;
                r--;
            }else if (sum<0){
                l++;
            }else if(sum>0){
                r--;
            }
        }
    }
    return res;
}
let nums= [-1,0,1,2,-1,-4]
let ans = fn(nums);
console.log(ans);

第K大



//倒着排序后取第k位 时间O(nlogn) 空间O(n)
function findKth(nums,k,keepDuplicate=false){
  const arr = keepDuplicate ? [...nums]: [...new Set(nums)]; //去重(默认)
  return arr.sort((a, b) => b - a)[k-1];
}
//快排时,选取的基准恰好是第k位 平均时间O(n) 空间O(log n)
function findKth(nums,k){
    function quickSort(left,right){
        const pivot = nums[right];//选择最右为基准
        // 分区:将大于 pivot 的元素移到左边,小于等于的移到右边
        let i = left;
        for (let j = left; j < right; j++) {
          if (nums[j] > pivot) { // 降序分区(找第 k 大),升序用 <
            [nums[i], nums[j]] = [nums[j], nums[i]];
            i++;
          }
        }
        // 将基准元素移到最终位置(i 就是基准的索引)
        [nums[i], nums[right]] = [nums[right], nums[i]];
        // 判断基准是否是第 k 大元素
        if (i === k - 1) return nums[i];
        return i < k - 1 ? quickSort(i + 1, right) : quickSort(left, i - 1);
    }
    return quickSort(0,nums.length-1);
}
//最小堆法(适合海量数据,内存受限场景) 时间O(nlogk) 空间O(k)
// 最小堆类(用于维护前 k 大元素)
class MinHeap { 
  constructor(size) {
    this.heap = []; // 堆存储数组
    this.size = size; // 堆的最大容量(k)
  }
 
  // 插入元素:超过容量时,仅替换堆顶(最小元素)
  insert(val) {
    if (this.heap.length < this.size) {
      // 堆未满,直接插入并上浮调整
      this.heap.push(val);
      this.bubbleUp(this.heap.length - 1);
    } else if (val > this.heap[0]) {
      // 堆已满,且当前元素 > 堆顶(最小元素),替换堆顶并下沉调整
      this.heap[0] = val;
      this.bubbleDown(0);
    }
  }
 
  // 上浮:新元素插入堆尾后,向上调整到合适位置
  bubbleUp(index) {
    while (index > 0) {
      const parentIndex = Math.floor((index - 1) / 2); // 父节点索引
      // 最小堆:父节点 < 子节点,否则交换
      if (this.heap[parentIndex] > this.heap[index]) {
        [this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
        index = parentIndex;
      } else {
        break;
      }
    }
  }
 
  // 下沉:堆顶元素替换后,向下调整到合适位置
  bubbleDown(index) {
    while (true) {
      const leftChildIndex = 2 * index + 1; // 左子节点索引
      const rightChildIndex = 2 * index + 2; // 右子节点索引
      let smallestIndex = index;
 
      // 找到左、右子节点中最小的那个
      if (leftChildIndex < this.heap.length && this.heap[leftChildIndex] < this.heap[smallestIndex]) {
        smallestIndex = leftChildIndex;
      }
      if (rightChildIndex < this.heap.length && this.heap[rightChildIndex] < this.heap[smallestIndex]) {
        smallestIndex = rightChildIndex;
      }
 
      // 若最小节点不是当前节点,交换并继续下沉
      if (smallestIndex !== index) {
        [this.heap[index], this.heap[smallestIndex]] = [this.heap[smallestIndex], this.heap[index]];
        index = smallestIndex;
      } else {
        break;
      }
    }
  }
 
  // 获取堆顶元素(前 k 大元素中的最小值,即第 k 大元素)
  getTop() {
    return this.heap[0];
  }
}
 
/**
 * 最小堆法找第 k 大元素(适合海量数据,内存受限)
 * @param {number[]} arr - 输入数组
 * @param {number} k - 第 k 大(k >= 1)
 * @returns {number|undefined} 第 k 大元素,无效参数返回 undefined
 */
function heapFindKthLargest(nums, k) {
  // 边界校验
  if (!Array.isArray(nums) || nums.length === 0 || k <= 0 || k > nums.length) {
    console.warn("无效参数:数组为空或 k 超出范围");
    return undefined;
  }
 
  // 初始化最小堆(容量 k)
  const minHeap = new MinHeap(k);
  // 遍历数组,维护堆
  for (const num of nums) {
    minHeap.insert(num);
  }
 
  // 堆顶就是第 k 大元素
  return minHeap.getTop();
}
 
// 测试示例
const testArr3 = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(heapFindKthLargest(testArr3, 2)); // 输出 6(第 2 大)
console.log(heapFindKthLargest([9, 8, 7, 6, 5], 3)); // 输出 7(第 3 大)
console.log(heapFindKthLargest([1, 2, 3, 4, 5, 6], 4)); // 输出 3(第 4 大)

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...