持续更新中...🥬就多练! 前端必备 or 我面试过的 欢迎指正!!!
1、new
面试
function myNew(){
// 新建一个对象
const obj = new Object()
// 获取arguments的第一个参数并返回
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
const ret = Constructor.apply(obj,arguments)
return typeof ret === 'object' ? ret : obj
}
2、图片懒加载
面试
<body>
<img src = '' class = 'image-item' data-original = "./1.svg" lazyload = 'true'/>
<img src = '' class = 'image-item' data-original = "./2.svg" lazyload = 'true'/>
<img src = '' class = 'image-item' data-original = "./3.svg" lazyload = 'true'/>
...
<script>
// 获取视口高度
var viewHeight = document.body.clientHeight || window.clientHeight || document.documentElement.innerHeight
function lazyLoad (){
var eles = document.querySelectAll('img[lazyload][data-original]')
eles.forEach((item,index) => {
if(!item.dataset.original) return
var rect = document.getBoundingClientRect() // 获取相对视口的高度
if(rect.top < viewHeight && rect.bottom >= 0){
// 在可视范围内
var img = new Image()
img.src = item.dataset.original
img.onLoad = function(){
item.src = img.src
}
item.removeAttribute('data-original')
item.removeAttribute('lazyload')
}
})
}
// 最开始执行一次
lazyLoad()
document.addEventListener('scroll',lazyLoad)
</script>
</body>
3. call、bind、apply
3.1 call
// 使用
var foo = {
value:1
}
function bar(){
console.log(this.value)
}
bar.call(foo)
Function.prototype.myCall = function(context,...args){
if(typeof context === 'undefined' || context == null){
context = window
}
// 声明一个属性
var fnSymbol = new Symbol()
// 给属性赋值为this
context[fnSymbol] = this
// 执行
var fn = context[fnSymbol](...args)
// 删除这个属性
delete context[fnSymbol]
return fn
}
3.2 apply
// 思想和call一样,入参变为数组
Function.prototype.myApply = function(context,arr){
if(typeof context === 'undefined' || context == null) context = window
const fnSymbol = new Symbol()
context[fnSymbol] = this
const fn = context[fnSymbol](...arr)
delete context[fnSymbol]
return fn
}
3.3 bind
// 用apply或call实现,返回一个函数
Function.prototype.myBind = function(context){
if(typeof context === 'undefined' || context == null) context = window
const self = this
return function(...args){
return self.apply(context, args)
}
}
4.防抖和节流
面试
4.1 防抖
// 一段时间内如果重复触发,只在最后触发之后的wait秒后执行,适用于搜索框
function debounce(func,wait){
let timeout;
return function(){
const context = this
const args = arguments
clearTimeout(timeout)
timeout = setTimeout(()=>{
func.apply(context,args)
},wait)
}
}
4.2 立即执行防抖
function debounce(func,wait,immediate){
let timeout
return function(){
const context = this
const args = arguments
if(timeout){
clearTimeout(timeout)
}
if(immediate){
const callNow = !timeout
timeout = setTimeout(()=>{
timeout = null
},wait)
if(callNow)func.apply(context,args)
}else{
timeout = setTimeout(()=>{
func.apply(context,args)
},wait)
}
}
}
4.3 hook防抖
import {useRef, useState} from 'react'
export function UseDebounce(value, delay = 500){
const [debounceValue, setDebounceValue] = useState('')
const timer = useRef()
useEffect(()=>{
if(timer.current) clearTimeout(timer)
timer.current = setTimeout(()=>{
setDebounceValue(value)
},delay)
return () => {
if(timer.current) clearTimeout(timer)
}
},[delay,value])
return debounceValue
}
4.4 节流
// 在固定的时间内,只能执行一次时间,适用于用户滚动
function throttle(func,wait){
let previous = 0
return function(){
const context = this
const args = arguments
let now = +Date.now()
if(now - previous > wait){
func.apply(context,args)
previous = now
}
}
}
5、异步任务并发控制器
面试
class PromiseQueue{
constructor(options = {}){
this.concurrency = options.concurrency || 1; // 最大并发线程
this.currentCount = 0; // 目前执行到哪个线程
this.pendingList = []; // 异步并发队列
}
add(task){
this.pendingList.push(task)
this.run()
}
run(){
if(this.pendingList.length === 0) return
if(this.currentCount === this.concurrency) return
this.currentCount++
// 优先级排序,然后取出第一个
const {fn} = this.pendingList.sort((a,b) => a.priority - b.priority).shift()
const promise = fn()
promise.then(this.completeOne.bind(this)).catch(this.completeOne().bind(this))
}
completeOne(){
this.currentCount--
this.run() // 如果线程池里还有,继续执行
}
}
// 答案2:
/**
* 并发池执行异步任务
* @param {number} poolLimit - 最大并发数
* @param {Array<() => Promise>} tasks - 返回 Promise 的工厂函数数组
* @returns {Promise<Array>} - 按任务顺序 resolve 的结果数组
*/
async function asyncPool(poolLimit, tasks) {
const results = new Array(tasks.length);
const executing = new Set(); // 当前正在跑的 Promise
const wrap = (i) =>
tasks[i]().then((res) => {
results[i] = res; // 按顺序保存结果
executing.delete(wrap(i)); // 释放池位
});
for (let i = 0; i < tasks.length; i++) {
const p = wrap(i);
executing.add(p);
if (executing.size >= poolLimit) {
await Promise.race(executing); // 任一完成再放下一个
}
}
await Promise.all(executing); // 等最后一批收尾
return results;
}
/* ---------------- 测试 ---------------- */
const tasks = Array.from({ length: 10 }, (_, i) => {
const cost = Math.random() * 1000;
return () =>
new Promise((resolve) =>
setTimeout(() => resolve(`任务${i} 完成`), cost)
);
});
(async () => {
console.time('pool');
const res = await asyncPool(3, tasks);
console.timeEnd('pool');
console.log(res); // ["任务0 完成", "任务1 完成", ..., "任务9 完成"]
})();
6、订阅发布
面试
class EventEmitter{
constructor(){
// 这里必须是一个map
this.handlers = {}
}
// 注册事件
on(eventName,cb){
if(!this.handlers[eventName]){
this.handlers[eventName] = []
}
this.handlers[eventName].push(cb)
}
// 触发事件
emit(eventName,...args){
if(!this.handlers[eventName]) return
const callbacks = this.handlers[eventName].slice()
callbacks.forEach(callback=>{
callback(...args)
})
}
// 注销事件的某个cb
off(eventName,cb){
if(!this.handlers[eventName]) return
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if(index !== -1){
callbacks.splice(index,1)
}
}
// 只执行一次
once(eventName,cb){
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName,wrapper)
}
this.on(eventName,wrapper)
}
}
7、setTimeout实现setInterval
7.1 setTimeout
// 递归
// 参数:回调函数,间隔时间
function mySetInterval(callback,interval){
let timerId;
// 递归这里
function execute(){
callback()
timerId = setTimeoout(execute,interval)
}
// 初始执行
timerId = setTimeoout(execute,interval)
// 返回clearTimeout
return {
clear:function(){
clearTimeout(timerId)
}
}
}
7.2 setTimeout 高精度版
- 还是会js单线程的限制
- 但是每次调用都会校验一次精度
function setInterval(callback,interval){
const expect = Date.now() + interval; // 用于计算精度
const timer = () => {
const diff = Date.now() - expect; // 和期望值之间的差距
callback()
setTimeout(timer,Math.max(interval - diff, 0))
}
setTimeout(timer,interval)
}
8、数组扁平化
面试
8.1 普通递归
function flatten(arr){
const result = []
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
}
8.2 reduce
function flatten(arr){
return arr.reduce((accu,val) => {
return accu.concat(Array.isArray(val) ? flatten(val):val)
},[])
}
8.3 flat
function flatten(arr,depth = 1){
return arr.flat(depth) // 默认Infinity
}
9. 树的层序遍历
面试
树结构:
A
/ | \
B C D
/ \ |
E F G
/ \
H I
9.1 简洁版
function levelOrder(root){
if(!root) return
const queue = [root]
const result = []
while(queue.length){
const node = queue.shift() // 队列队头拿一个元素
result.push(node.val)
queue.push(...node.children)
}
return result
}
// 结果: [A,B,C,D,E,F,G,H,I]
9.2 层级版
function levelOrder(root){
if(!root) return []
const result = []
const queue = [root]
while(queue.length){
const size = queue.length
const currentLevel = []
for(let i = 0; i < size; i++){
const currentNode = queue.shift() // 队头取出节点
currentLevel.push(currentNode.value)
// queue放入左右子节点
if(currentLevel.lnode){
queue.push(currentLevel.lnode))
}
if(currentLevel.rnode){
queue.push(currentLevel.rnode))
}
}
result.push(currentLevel)
}
return result
}
// 结果 [['A'],['B','C','D'],['E','F','G'],['H','I']]
10.拷贝
面试
10.1 浅拷贝
- Object.assign()
- Array.prototype.concat()
- Array.prototype.slice()
// 拷贝的值,不是地址,所以一旦复制后改变了值,原始数据也跟着改变
function shallowCopy(obj){
if(typeof obj !== 'object') return
const newObj = Array.isArray(obj) ? [] : {}
for(var key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key]
}
}
return newObj
}
10.2 深拷贝
- JSON.stringify(JSON.parse())
// 使用WeakMap,避免Map强引用类型,可以自动垃圾回收,避免内存泄露
function deepClone(obj, hash = new WeakMap()){
// null
if(obj == null) return null
// 特殊类型
if(obj instanceof Date) return new Date(obj)
if(obj instanceof RegExp) return new RegExp(obj)
// 不是对象
if(typeof obj !== 'object') return obj
// 缓存
if(hash.has(obj)) return hash.get(obj)
const newObj = Array.isArray(obj) ? [] : {}
// 存起来,用于下次缓存
hash.set(obj, newObj)
// Reflect可以遍历枚举和Symbol类型
Reflect.ownKeys(obj).forEach(key=>{
newObj[key] = deepClone(obj[key],hash)
})
return newObj
}
11.去重
11.1 new Set()
function unique(arr){
return [...new Set(arr)]
}
11.2 indexOf + filter
function unique(arr){
return arr.filter((item,index) => arr.indexOf(item) === index)
}
11.3 reduce + includes
function unique(arr){
return arr.reduce((accu,val) =>{
return arr.includes(val) ? accu: [...accu,val]
},[])
}
11.4 处理NaN
function unique(arr){
const result = []
const seen = new Set()
const flag = false
for(const ele of arr){
if(Number.isNaN(ele)){
if(!flag){
result.push(NaN)
flag = true
}
continue
}
if(!seen.has(ele)){
seen.add(ele)
result.push(ele)
}
}
return result
}
11.5 处理对象
function unique(arr){
const result = []
const seen = new WeakSet()
for(const ele of arr){
if(typeof ele === 'obj' && ele !== null){
if(!seen.has(ele)){
seen.add(ele)
result.push(ele)
}
}else{
if(!result.includes(ele)){
result.push(ele)
}
}
}
return result
}
11.6 js实现有序数组原地去重
/**
* @param {number[]} nums
* @return {number}
*/
// 方法1: 直接新开一个数组,如果和上一个不相同就放进去
// 方法2: 快慢指针,快指针先走,和慢指针做对比
// 1 2 2 3
// i = 0, j = 1
var removeDuplicates = function(nums) {
if(nums.length < 2) return nums.length
let slow = 0;
for(let fast = 0; fast< nums.length; fast++){
if(nums[slow] !== nums[fast]){
slow++
nums[slow] = nums[fast]
}
}
return slow + 1
};
12.compose
12.1 普通版
面试
// 上一个函数的返回值作为下一个函数的入参
function fn1(x){
return x + 1
}
function fn2(x){
return x + 2
}
function fn3(x){
return x + 3
}
const fn = compose(fn1,fn2,fn3)
// 用fn2调用,fn1的返回值 => next(prev)
console.log(fn(1)) // 7
// 实现
// 一定是返回一个可调用的function
function compose(){
const args = [...arguments]
return function(num){
return args.reduce((prev,next)=>{
return next(prev)
},num)
}
}
function compose(fns){
// 参数
return num => {
return fns.reduce((prev,next) => next(prev),num)
}
}
12.2 洋葱圈版
13.柯里化
面试
// 只传一部分的参数,让它的返回值去处理剩下的参数
// 返回值同样也是函数,如果传入的参数 > 函数本身的参数,函数调用结束,直接执行,否则继续调用,返回的还是函数
// fn指的传入的是参数
function curry(fn){
return function curried(...args){
// 如果参数足够,直接调用原函数
if(args.length >= fn.length){
return fn.apply(this,args)
}else{
// 如果参数不够,返回新的柯里化
return function(...nextArgs){
return curried.apply(this,args.concat(nextArgs))
}
}
}
}
// 如果实现一个currysum(1)(2)返回结果 3
const sum = (a, b) => a + b;
const currysum = curry(sum);
console.log(currysum(1)(2)); // 3
14.requestAnimationFrame
14.1 实现setTimeout
// 本质是递归
// requestAnimationFrame和页面渲染时间同步,且在后台时会暂停执行,性能优化
function setTimeoutRAF(callback,delay){
let start = performance.now()
function checkTime(currentTime){
const elapsed = currentTime - start; // 60hz执行一次,1000ms/16.67
if(elapsed >= delay){
callback()
}else{
requestAnimationFrame(checkTime)
}
}
// 初始执行
requestAnimationFrame(checkTime)
}
14.2 实现平移动画
// 递归,每16.67ms调用一次
const box = document.getElementById('box')
const container = document.getElementById('container')
const speed = 2
let pos = 0
const maxPos = container.offsetWidth - box.offsetWidth
function animate(){
// 每次调用走的距离
pos += speed;
// 边界
if(pos >= maxPos) pos = maxPos
// 更新位置
box.style.left = pos + 'px'
if(pos < maxPos) requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
15、写一个react组件
react组件A,传入src='hello world'和target = 'world'让target高亮显示。
import React from 'react'
const HighlightText = ({src,target} => {
if(!target){
return <span>{src}</span>
}
const parts = src.split(target)
return (
<>
{parts.map((part,index)=>(
<span key={index}>
{part}
{index < parts.length - 1 && <mark>{target}</mark>}
</span>
))}
</>
)
})
export default HighlightText
16、promise
面试
16.1 Promise.all
function myPromiseAll(promises){
const result = []
let count = 0
return new Promise((resolve,reject) => {
const addData = (value,index) => {
result[index] = value
count++
if(count === promises.length) resolve(result)
}
promises.forEach((promise,index) => {
if(promise instanceof Promise){
promise.then(res => {
addData(res,index)
})
.catch(err=> reject(err))
}else{
addData(promise,index)
}
})
})
}
16.2 primise.all用var和for编程
function myPromiseAll(promises){
var result = []
var count = 0
return new Promise((resolve,reject) => {
for(var i = 0; i < promises.length; i++){
(function(i){
var promise = promises[i]
promise.then(res => {
result[i] = res
count++
if(count == promises.length) resolve(result)
})
.catch(err => reject(err))
})(i)
}
})
}
16.3 Promise.race
function myPromiseRace(promises){
return new Promise((resolve,reject) => {
promises.forEach((promise,index) => {
if(promise instanceof Promise){
promise
.then(res => resolve(res),
err => reject(err))
}else{
resolve(promise)
}
})
})
}
16.4 promise完整链路
class MyPromise{
constructor(executor){
this.initValue()
this.initBind()
try{
executor(this.resolve,this.reject)
}catch(e){
this.reject(e)
}
}
initValue(){
this.result = null
this.state = 'pending'
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
}
initBind(){
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
resolve(value){
if(this.state !== 'pending') return
this.state = 'fulfulled'
this.result = value
while(this.onFulfilledCallbacks.length)
this.onFulfilledCallbacks.shift()(this.result)
}
reject(reason){
if(this.state !== 'pending') return
this.state = 'rejected'
this.result = reason
while(this.onRejectedCallbacks.length)
this.onRejectedCallbacks.shift()(this.result)
}
then(onFulfilled,onRejected){
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
var thenPromise = new MyPromise((resolve,reject) => {
const resolvePromise = cb => {
const x = cb(this.result)
if(x === thenPromise){
throw new Error('不能和返回值相同')
}
if(x instanceof MyPromise){
x.then(resolve,reject)
}else{
resolve(x)
}
}
if(this.state === 'fulfilled'){
resolvePromise(onFulfilled)
}else if(this.state === 'rejected'){
resolvePromise(onRejected)
}else{
this.onFulfilledCallbacks.push(onFulfilled.bind(this))
this.onRejectedCallbacks.push(onRejected.bind(this))
}
})
return thenPromise
}
}
17、数组增序排列
18、最长不重复字串
面试(最少三次)
- 编写方法,返回最长无重复子串的长度
- 无重复子串指:子串中每个字符都不相同
- 例如:s = 'aaabcddddefghh' ,其中,最长的无重复子串为'defgh'长度为5
// 滑动窗口原理,需要左右指针,根据滑动窗口更新最大长度
function lengthOfLongestSubstring(s){
let left = right = length = maxLength = 0
const set = new Set()
while(right < s.length){
if(!set.has(s[right])){
set.add(s[right])
length++
if(maxLength < length) maxLength = length
right++
}else{
while(set.has(s[right])){
set.delete(s[left])
left++
length--
}
set.add(s[right])
right++
length++
}
}
return maxLength
}
19、结构体转换,数组结构和树结构相互转换
- 树和数组结构
const listTree = [
{
id: 1,
name: '部门1',
pid: 0,
children: [
{
id: 2,
name: '部门1-1',
pid: 1,
children: [
{
id: 4,
name: '部门1-1-1',
pid: 2,
children: []
}
]
},
{
id: 3,
name: '部门1-2',
pid: 1,
children: [
{
id: 5,
name: '部门1-2-1',
pid: 3,
children: []
}
]
}
]
},
{
id: 6,
name: '部门2',
pid: 0,
children: [
{
id: 7,
name: '部门2-1',
pid: 6,
children: []
}
]
},
{
id: 8,
name: '部门3',
pid: 0,
children: []
}
]
期望结果:
const list = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门1-1', pid: 1},
{id: 3, name: '部门1-2', pid: 1},
{id: 4, name: '部门1-1-1', pid: 2},
{id: 5, name: '部门1-2-1', pid: 3},
{id: 6, name: '部门2', pid: 0},
{id: 7, name: '部门2-1', pid: 6},
{id: 8, name: '部门3', pid: 0},
]
19.1 树结构转换为数组结构
// 深度遍历
function treeToListDFS(tree){
const result = []
function traverse(node){
const {children,...rest} = node
result.push(rest)
if(children){
children.forEach(child=>traverse(child))
}
}
return result
}
// 广度遍历
function treeToListBFS(tree){
const result = []
const queue = [...tree]
while(queue.length){
const node = queue.shift()
const {children,...rest} = node
result.push(rest)
queue.push(...children)
}
return result
}
19.2 数组结构转换为树
// Map实现 o(n)
function arrToTree(lists){
const result = []
const map = new Map()
lists.forEach(item=>{
map.set(item.id,{...item,children:[]})
})
lists.forEach(item=>{
const node = map.get(item.id)
if(node.pid === 0){
result.push(node)
}else{
const parent = map.get(node.pid)
if(parent){
parent.children.push(node)
}
}
})
return result
}
20、实现一个中间件(koa或者express)
21、正则匹配30,str = '1 apple cost 30$'
22.拍平键路径字符串版
输入:{a:{b:{c:1}}, d:[{e:2}]} 输出:拍平键路径字符串版 {'a.b.c':1, 'd[0].e':2} 进阶:反向实现「展开」函数,把拍平对象还原成深层结构,用于 localStorage 存配置
23、虚拟dom转换为真实dom的过程
24、找第K大元素
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
const newNums = nums.sort((a,b) => a -b)
return newNums[nums.length - k]
};
25、lodash中的get方法
26、['A,'B'.['A','C',['B','D'.['D”,A”],每个子数组表示一个路径的起点和终点。判断是否有环,并输出路径
27、UseRequest
面试
27.1 简易TS版本
import { useState,useEffect,useCallback } from "react";
interface Options<TData, TParams extends any[]>{
fetchFn: (...args:TParams) => Promise<TData>;
onSuccess: (data:TData) => void;
onError: (error:Error) => void;
}
interface Result<TData, TParams extends any[]>{
data:TData | undefined;
error: Error | undefined;
loading: boolean;
run: (...args:TParams) => Promise<TData>
}
function UseRequest<TData, TParams extends any[]>(
options:Options<TData, TParams>
): Result<TData, TParams>{
const {fetchFn,onSuccess,onError} = options
const [data,setData] = useState<TData>()
const [loading,setLoading] = useState<boolean>(false)
const [error, setError] = useState<Error>()
const run = useCallback(async (...params:TParams):Promise<TData>=>{
setLoading(true)
try{
const result = await fetchFn(...params)
setData(result)
onSuccess?.(result)
return result
}catch(error){
const err = error instanceof Error ? error : new Error(String(error))
setError(err)
onError?.(err)
throw err
}finally{
setLoading(false)
}
},[fetchFn,onSuccess,onError])
useEffect(()=>{
run(...([] as unknown as TParams))
},[])
return {data,error,loading,run}
}
// 使用
async function fetchUser(userId: number){
const res = await fetchUser(`/api/user/${userId}`)
return res.json()
}
UseRequest({
fetchFn:fetchUser,
onSuccess:(data) => {
},
onError:(err) => {
}
})
28、数组排序 给有序数组nums1和nums2,长度为m + n和n,将nums2的元素插入nums1中,并排序
面试
- 输入 nums1 = [1,2,4,5,6,0] nums2 = [3] m = 5 n = 1
- 输出 nums1 = [1,2,3,4,5,6]
// 方法1:逆向双指针的归并
function merge(nums1,m,nums2,n){
let i = m - 1, j = n - 1, idx = m + n - 1;
while(i >=0 && j >= 0){
if(nums1[i] > nums2[j]) nums1[idx--] = nums1[i--]
else nums1[idx--] = nums2[j--]
}
while(j >= 0) nums1[idx--] = nums2[j--]
}
// 方法2: 插入之后排序
function merge(nums1,m,nums2,n){
nums1.splice(m, nums2.length, ...nums2)
nums1.sort((a,b) => a - b)
}
29、instanceof
30、括号匹配
// 括号匹配(含 {} [] () 三种)
// 输入:'({[()]})'
// 输出:true
// 进阶:实时监听 textarea 输入,边框变红提示非法,要求 16 ms 内反馈
31、版本号排序
输入:['2.10.1', '1.2.3', '2.1.0', '2.10.0'] 输出:升序 ['1.2.3', '2.1.0', '2.10.0', '2.10.1']
32、k个一组,翻转链表
/**
1->2->3->4->5 k = 2
a. 虚拟头节点 hair -> 1->2->3->4->5
循环while(true)
b. start = end = hair 让end找到k的一组的尾节点,用for用while都行,end不存在就跳出循环
c. 存储startNext 和 endNext,此时 start = hair,end = 2 startNext = 1,是翻转后的尾节点 endNext = 3 是下一个链的头节点
d. 前k个断链,end.next = null
e. 翻转函数抽出来,返回翻转后的头节点pre
f. start现在位于虚拟头节点,和pre连接,startNext位于翻转后的尾节点,和endNext连接,startNext变成下一个链的虚拟头节点
g. start = end = startNext,构建下一个链的虚拟头节点
h.返回hair.next
reverse实现
1->2->null
pre = null cur = 1 next = 2
while(cur)循环{
cur.next = pre 1->null
pre = cur; cur=next pre = 1 cur = 2
next = cur->next
}然后循环
2->1->null
返回pre
*/
function reverse(head){
let pre = null, cur = head;
while(cur){
const next = cur.next
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
function reverseKGroup(head, k){
// 虚拟头节点
const hair = new ListNode()
hair.next = head;
let start = end = hair;
while(true){
// 找到一个链的头尾节点
for(let i = 0; i < k && end; i++) end = end.next
if(!end) break
// startNext这个链翻转后的尾节点,endNext下一个链的头节点
let startNext = start.next, endNext = end.next;
end.next = null // 断链
// 重新连接
start.next = reverse(start)
startNext.next = endNext
start = end = startNext
}
return hair.next
}
33、翻转left和right之间的链表
// 记录一下left前的节点、left节点、right节点和right后的节点就可以了
// 示例:1->2->3->4->5 left = 2 right = 4
// 输入 1->4->3->2->5
function reverse(head){
if(head == null || head.next == null) return head
let cur = head,pre = null
while(cur){
const nex = cur.next
cur.next = pre
pre = cur
cur = nex
}
}
function reverseBetween(head,left,right){
// 虚拟头节点
const hair = new ListNode()
hair.next = head
// 找到左节点的前一个节点
let start = hair
for(let i = 0; i < left - 1; i++) start = start.next
// 找到右节点
let rightNode = start
for(let i = 0; i < right - left + 1; i++) rightNode = rightNode.next
// 找到左节点和右节点的下一个节点
let leftNode = start.next
let end = rightNode.next
// 断链
start.next = null
rightNode.next = null
// 翻转
reverse(leftNode)
// 再连上
start.next = rightNode
leftNode.next = end
return hair.next
}
34、斐波那契数列
面试
// 方法1、直接递归 o(n^2) o(n)
function Fibonacci(n){
if(n <= 1) return n
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
// 方法2、map缓存 o(n) o(n)
function Fibonacci(n, map = new Map()){
if(n <= 1) return n
if(map.has(n)) return map.get(n)
const res = Fibonacci(n-1,map) + Fibonacci(n-2,map)
map.set(n,res)
return res
}
// 方法3、动态规划,数组存储,数组下标为n
function Fibonacci(n){
if(n <= 1) return n
const arr = []
arr[0] = 0
arr[1] = 1
for(let i = 2; i <= n; i++){
arr[i] = arr[i - 1] + arr[i - 2]
}
return arr[n]
}
35、统计网页 DOM 标签频次
输出:对象 {标签名: 出现次数},按次数降序排序 进阶: 用 document.querySelectorAll('*') 会卡吗?试试 NodeIterator 优化
36、求两个日期之间的「自然月」差
输入:'2022-01-31'、'2022-05-15' 输出:4(完整自然月差)
37、颜色十六进制 → RGB 对象
输入:'#1be3'(简写 + alpha) 输出:{r:17, g:238, b:51, a:0.2} 进阶:支持省略 #、大小写混写,返回结果直接喂给 canvas.ctx.fillStyle
38、数组交集(保留重复)
输入:[1,2,2,3], [2,2,2,4] 输出:[2,2]
39、手机号脱敏
输入:'13812345678' 输出:'138****5678' 进阶:支持 11 位手机号 & 带区号 8613812345678,用同一函数处理
40、hooks手写
40.1 usePrevious普通版
import {useEffect, useRef} from 'react'
function usePrevious(value){
// 通过 ref存储
const ref = useRef()
useEffect(() => {
ref.current = value
},[])
return ref.current
}
40.2 usePrevious根据值更新版
import {useEffect, useRef, useState} from 'react'
function usePrevious(value){
// 通过 ref存储当前值,通过 useState存储上一次的值
const [previousValue, setPreviousValue] = useState()
const ref = useRef()
useEffect(() => {
if(previousValue !== value){
setPreviousValue(ref.current)
ref.current = value
}
},[value])
return previousValue
}
41.给定任意二维数组,输出所有的排列组合项
比如 [['A','B'], ['a','b'], [1, 2]],
输出 ['Aa1','Aa2','Ab1','Ab2','Ba1','Ba2','Bb1','Bb2']
function permutate(arr){
// 拿出第一组
let res = arr[0].slice() // 浅拷贝
for(let i = 1; i < arr.length; i++){
const pre = res.slice()
res = []
pre.forEach(item => {
arr[i].forEach(curr => {
res.push(item + curr)
})
})
}
return res
}
42 、场景题
场景:页面 1w 个按钮,均用 单一事件委托 监听; 输入:点击流 ['save','cancel','save','delete','save'] 输出:实时维护 Map {'save':3, 'cancel':1, 'delete':1},且支持 O(1) 撤销上一次计数(用户点了撤回)。 进阶:撤销操作来自键盘 Ctrl+Z,用栈记录上一次按钮类型即可
43、form 组件
44、找出字符串中出现次数最多的字母,并对前面的数字求和(mid)
45、合并商品标签
输入:两个商品标签数组,可能有重复
输出:去重后按字典序合并
进阶:结果用于 <select multiple>,需高亮本次新增标签。
46、回溯
[[1,2,3],[4,5],[6,7,8]]生成 [[1,4,6],[1,4,7]..]
47、短横线连接的字符串转换为驼峰形式
48、千位分隔符
输入:1234567.89 输出:'1,234,567.89' 进阶:支持负数、小数,用于表格金额列
49、找出所有消失的数字
输入:[4,3,2,7,8,2,3,1] 输出:[5,6](1~n 中缺失的) 彩蛋:数组长度 1e5,要求 O(n) 时间、O(1) 额外空间。