系列
如果你最近也在找工作or看机会,可加我进找工作群分享行情:mv55776,点击扫码加v
大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
1、柯里化
- 柯里化作用是
拆分参数
- 实现的核心思想是
收集参数
- 递归中去判断当前收集的参数和函数的所有入参 是否相等,长度一致即可执行函数运算
面试题中,可能会存在很多变种,比如需要支持结果缓存, add(1)(2)执行之后再add(3)(4),需要用前一个函数的结果
const myCurrying = (fn, ...args) => { if (args.length >= fn.length) { return fn(...args); } else { return (...args2) => myCurrying(fn, ...args, ...args2); } } const add = (x, y, z) => { return x + y + z } const addCurry = myCurrying(add) const sum1 = addCurry(1, 2, 3) const sum2 = addCurry(1)(2)(3) console.log(sum1, 'sum1'); console.log(sum2, 'sum2'); 复制代码
2、树结构转换
很常见的一道题,真的遇到很多回了,求你好好写一下
- 先构建map结构,以各个子项id为key
- 再循环目标数组,判断上面构建的map中,是否存在当前遍历的pid
- 存在就可以进行children的插入
- 不存在就是顶级节点,直接push即可
let arr = [{ id: 1, pid: 0, name: 'body' }, { id: 2, pid: 1, name: 'title' }, { id: 3, pid: 2, name: 'div' }] function toTree (data) { let result = []; let map = {}; // 1. 先构建map结构,以各个子项id为key data.forEach((item) => { map[item.id] = item }) // 2. 再循环目标数组,判断上面构建的map中,是否存在当前遍历的pid data.forEach((item) => { let parent = map[item.pid]; if(parent) { // 3. 存在就可以进行children的插入 (parent.children || (parent.children = [])).push(item) } else { // 4. 不存在就是顶级节点,直接push即可 result.push(item) } }) return result; } console.log(toTree(arr)); 复制代码
递归的解法
function toTree(data) { const roots = []; for (const item of data) { if (item.pid === 0) { item.children = []; roots.push(item); } else { const parent = findParent(item, roots); if (parent) { item.children = []; parent.children.push(item); } } } return roots; } function findParent(item, roots) { for (const root of roots) { if (root.id === item.pid) { return root; } const parent = findParent(item, root.children); if (parent) { return parent; } } } let arr = [{ id: 1, pid: 0, name: 'body' }, { id: 2, pid: 1, name: 'title' }, { id: 3, pid: 2, name: 'div' }] console.log(toTree(arr)); 复制代码
3、树路径查找
可能会由上一题进行升级到这一题
// 查询id为10的节点,输出节点路径如[1, 3, 10] const treeData = [{ id: 1, name: 'jj1', children: [ { id: 2, name: 'jj2', children: [{ id: 4, name: 'jj4', }] }, { id: 3, name: 'jj3', children: [ { id: 8, name: 'jj8', children: [{ id: 5, name: 'jj5', }] }, { id: 9, name: 'jj9', children: [] }, { id: 10, name: 'jj10', children: [] }, ], }, ], }]; let path = findNum(10, treeData); console.log("path", path); // 实现 const findNum = (target, data) => { let result = []; const DG = (path, data) => { if (!data.length) return data.forEach(item => { path.push(item.id) if (item.id === target) { result = JSON.parse(JSON.stringify(path)); } else { const children = item.children || []; DG(path, children); path.pop(); } }) } DG([], data) return result }; 复制代码
4、发布订阅模式
不光要会写,你要搞清楚怎么用,别背着写出来了,面试让你验证下,你都不知道怎么用。一眼背的,写了和没写一样
class EventEmitter { constructor() { this.events = {}; } // 订阅 on(eventName, callback) { const callbacks = this.events[eventName] || []; callbacks.push(callback); this.events[eventName] = callbacks; } // 发布 emit(eventName, ...args) { const callbacks = this.events[eventName] || []; callbacks.forEach((cb) => cb(...args)); } // 取消订阅 off(eventName, callback) { const index = this.events[eventName].indexOf(callback); if (index !== -1) { this.events[eventName].splice(index, 1); } } // 只监听一次 once(eventName, callback) { const one = (...args) => { callback(...args); this.off(eventName, one); }; this.on(eventName, one); } } let JJ1 = new EventEmitter() let JJ2 = new EventEmitter() let handleOne = (params) => { console.log(params,'handleOne') } JJ1.on('aaa', handleOne) JJ1.emit('aaa', 'hhhh') JJ1.off('aaa', handleOne) // 取消订阅以后再发就没用了 JJ1.emit('aaa', 'ffff') JJ2.once('aaa', handleOne) JJ2.emit('aaa', 'hhhh') // 只能发一次消息,再发就无效了 JJ2.emit('aaa', 'hhhh') 复制代码
4、观察者模式
class Notifier { constructor() { this.observers = []; } add(observer) { this.observers.push(observer) } remove(observer) { this.observers = this.observers.filter(ober => ober !== observer) } notify() { this.observers.forEach((observer) => { observer.update() }) } } class Observer { constructor (name) { this.name = name; } update () { console.log(this.name, 'name'); } } const ob1 = new Observer('J1') const ob2 = new Observer('J2') const notifier = new Notifier() notifier.add(ob1) notifier.add(ob2) notifier.notify() 复制代码
5、防抖
- 某个函数在短时间内只执行最后一次。
function debounce(fn, delay = 500) { let timer = null; return function () { if(timer) { clearTimeout(timer); }; timer = setTimeout(() => { timer = null; fn.apply(this, arguments) }, delay) } } 复制代码
6、节流
- 某个函数在指定时间段内只执行第一次,直到指定时间段结束,周而复始
// 使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行 // 可以结合时间戳 实现更准确的节流 // 写法1 function throttle(fn, delay = 500) { let timer = null; return function () { if(timer) return; timer = setTimeout(() => { timer = null; fn.apply(this, arguments) }, delay) } } // 写法2 function throttle(fn, delay) { let start = +Date.now() let timer = null return function(...args) { const now = +Date.now() if (now - start >= delay) { clearTimeout(timer) timer = null fn.apply(this, args) start = now } else if (!timer){ timer = setTimeout(() => { fn.apply(this, args) }, delay) } } } 复制代码
7、手写深拷贝
- 考虑各种数据类型,存在构造函数的可以用new Xxxx()
- 考虑循环引用,用weakMap记录下
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象 if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象 //如果循环引用了就用 weakMap 来解决 if (hash.has(obj)) return hash.get(obj) let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链 hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key] } return cloneObj } // 下面是验证代码 let obj = { num: 0, str: '', boolean: true, unf: undefined, nul: null, obj: { name: '我是一个对象', id: 1 }, arr: [0, 1, 2], func: function () { console.log('我是一个函数') }, date: new Date(0), reg: new RegExp('/我是一个正则/ig'), [Symbol('1')]: 1, }; Object.defineProperty(obj, 'innumerable', { enumerable: false, value: '不可枚举属性' } ); obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop = obj // 设置loop成循环引用的属性 let cloneObj = deepClone(obj) cloneObj.arr.push(4) console.log('obj', obj) console.log('cloneObj', cloneObj) 复制代码
8、实现settimeout
- 最简单的是直接while循环,比较消耗性能,且阻塞JS运行
- 可以借助requestAnimationFrame + 递归来完成
- 也可以用setInterval来实现
const mysetTimeout = (fn, delay, ...args) => { const start = Date.now(); // 最简单的办法 // while (true) { // const now = Date.now(); // if(now - start >= delay) { // fn.apply(this, args); // return; // } // } let timer, now; const loop = () => { timer = requestAnimationFrame(loop); now = Date.now(); if(now - start >= delay) { fn.apply(this, args); cancelAnimationFrame(timer) } } requestAnimationFrame(loop); } function test() { console.log(1); } mysetTimeout(test, 1000) 复制代码
9、Promise三大API
promise实现思路,上一篇已经写了:juejin.cn/post/721621…
all
// promise all const promise1 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(1); resolve(1); }, 1000); }); }; const promise2 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(2); resolve(2); }, 2000); }); }; const promise3 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(3); resolve(3); }, 3000); }); }; const promise4 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(4); resolve(4); }, 4000); }); }; const promiseArr = [promise1, promise2, promise3, promise4]; // Promise.all(promiseArr).then(res => { // console.log(res, '1111'); // }) const promiseAll = (pList) => { return new Promise((resolve, reject) => { let count = 0; const pLength = pList.length; const result = []; for (let i = 0; i < pLength; i++) { pList[i]().then(res => { count++ result[i] = res if (pLength === count) { console.log(result, 'result'); resolve(result) } }) .catch(err => { reject(err) }) } }) } promiseAll(promiseArr) 复制代码
race
Promise.race = function(promises){ return new Promise((resolve,reject)=>{ for(let i=0;i<promises.length;i++){ promises[i].then(resolve,reject) }; }) } 复制代码
retry
function retry(fn, maxTries) { return new Promise((resolve, reject) => { function attempt(tryNumber) { console.log('重试次数',tryNumber) fn().then(resolve).catch(error => { if (tryNumber < maxTries) { attempt(tryNumber+1); } else { reject(error); } }); } attempt(1); }); } function downloadFile() { return new Promise((resolve, reject) => { // 异步下载文件 setTimeout(() => { reject('test') }, 1000) }); } retry(() => downloadFile(), 3) .then(data => console.log('文件下载成功')) .catch(err => console.error('文件下载失败:', err.message)); 复制代码
plimit
- 并发控制
- 并发的请求只能有n个,每成功一个,重新发起一个请求
- 思路:然后在一个while循环中,判断当前正在执行的Promise个数,和并发数n对比,同时要保证Promise数组有值,条件满足就进行取到执行的操作
- 先一次性拿出n个执行,在每个Promise的then中判断是否还有需要执行的,通过shift拿出任务执行,同时记录已经成功的Promise数量,当每个Promise的then中resolve的数量和原始Promise相等,即可进行整体结果的resolve
const promise1 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(1); resolve(1); }, 1000); }); }; const promise2 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(2); resolve(2); }, 1000); }); }; const promise3 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(3); resolve(3); }, 1000); }); }; const promise4 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(4); resolve(4); }, 1000); }); }; const promiseArr = [promise1, promise2, promise3, promise4]; // 控制并发 const pLimit = (pList, limit) => { return new Promise((resolve, reject) => { let runCount = 0; let resolvedCount = 0; const pListLength = pList.length; const result = []; const nextP = (p, count) => { p().then(res => { result[count] = res; resolvedCount++ if (pList.length) { const pNext = pList.shift(); nextP(pNext, runCount) runCount++; } else if (resolvedCount === pListLength) { resolve(result) } }) } while (runCount < limit && pList.length) { const p = pList.shift(); nextP(p, runCount) runCount++; } }) } pLimit(promiseArr, 3).then(res => { console.log(res, '1111') }) 复制代码
10、实现lodash的get
- 通过正则把
[]
访问形式改成.
- 针对
.
进行分割,然后对每一项进行依次的遍历,找最后一级的value
function get (source, path, defaultValue = undefined) { // a[3].b -> a.3.b const paths = path.replace(/\[(\d+)\]/g,'.$1').split('.'); let result = source; for(let p of paths) { result = result[p]; if(result === undefined) { return defaultValue; } } return result; } const obj = { a:[1,2,3,{b:1}]} const value = get(obj, 'a[3].b', 3) console.log(value, 'value'); 复制代码
11、数组扁平化
- 不用flat
- 支持自定义层级
function flat(arr, num = 1) { let result = []; arr.forEach(item => { if (Array.isArray(item) && num > 0) { result = result.concat(flat(item, num - 1)) } else { result.push(item) } }) return result } 复制代码
12、对象数组去重
- 场景简单,我遇到的面试官要求用reduce
- 不用reduce的话,map配合一次循环就可以了
let arr = [{ id: 1, name: 'JJ1', }, { id: 2, name: 'JJ2', }, { id: 1, name: 'JJ1', }, { id: 4, name: 'JJ4', }, { id: 2, name: 'JJ2', }] const unique = (arr) => { let map = new Map(); return arr.reduce((prev, cur) => { // 当前map中没有,说明可以和上一个合并 if (!map.has(cur.id)) { map.set(cur.id, true) return [...prev, cur] } else { // 已经被标记的就不用合并了 return prev } }, []) } console.log(unique(arr), 'unique'); // 不使用reduce const unique = (arr) => { let map = new Map(); let result = []; arr.forEach(item => { if (!map.has(item.id)) { map.set(item.id, true) result.push(item) } }) return result } 复制代码
13、求所有的字符串子串
// 输入:ABCD, // 返回:ABCD,ABC,BCD,AB,BC,CD,A,B,C,D const getAllStr = (str) => { let result = [] for (let i = 0; i < str.length; i++) { for (let j = i + 1; j <= str.length; j++) { // result.push(str.slice(i, j)) result.push(str.substring(i, j)) } } return result; } console.log(getAllStr('ABCD')); 复制代码
14、驼峰转换
- 主要问题在于正则
- 不会正则也可以字符串分割,比较麻烦
function converter(obj) { let newObj = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { let newKey = key.replace(/_([a-z])/g, function (match, p1) { return p1.toUpperCase(); }); newObj[newKey] = obj[key]; } } return newObj; } 复制代码
15、不常考,这次遇到的
比较有意思的两道题,可以自己先做试试
npm循环依赖检测
// 检测当前pkgs是否存在循环依赖 const pkgs = [ { name: "a", dependencies: { b: "^1.0.0", }, }, { name: "b", dependencies: { c: "^1.0.0", }, }, { name: "c", dependencies: { a: "^1.0.0", }, }, ]; 复制代码
const checkCircularDependency = (packages) => { const map = {}; const states = {}; // 存放包的状态 // 初始化图 packages.forEach((pkg) => { map[pkg.name] = pkg.dependencies || {}; states[pkg.name] = "UNVISITED"; }) // 从每个包开始进行 DFS for (const pkgName in map) { if (states[pkgName] === "UNVISITED") { if (dfs(pkgName, map, states)) { return true } } } return false }; const dfs = (pkgName, map, states) => { states[pkgName] = "VISITING"; for (const dep in map[pkgName]) { const depState = states[dep]; if (depState === "VISITING") { return true; // 存在循环依赖 } else if (depState === "UNVISITED") { if (dfs(dep, map, states)) { return true; // 存在循环依赖 } } } return false; // 不存在循环依赖 }; // 使用方法 const pkgs = [ { name: "a", dependencies: { b: "^1.0.0", }, }, { name: "b", dependencies: { c: "^1.0.0", }, }, { name: "c", dependencies: { a: "^1.0.0", }, }, ]; console.log(checkCircularDependency(pkgs)); 复制代码
查找目标对象是否是源对象的子集
function checkIsChildObject(target, obj) { // 基于层级,把 obj 所有的属性生成 map 映射存储 // 让target和obj有层级关系对比 let map = new Map(); const setMapByObj = (obj, level) => { for (let key in obj) { let current = map.get(key) || {}; // 可能存在嵌套对象Key重复,可以合并在一个key里面 map.set(key, { ...current, [level]: obj[key], }); // 把所有对象铺开 if (typeof obj[key] === 'object') { setMapByObj(obj[key], level + 1) } } } setMapByObj(obj, 0) // console.log(map, 'map'); // target子对象去 map 里面寻找有没有哪一层的对象完全匹配自身,子属性只能找下一层的对象 let level = -1; for (let key2 in target) { // 获取当前key的所有集合 let current = map.get(key2) if (current !== undefined) { if (typeof target[key2] === 'object') { // 只需要再判断一级 if (!checkIsChildObject(target[key2], current)) { return false } } else { //表示还没开始查找,需要找一下当前值在第几层 if (level === -1) { for (let key3 in current) { if (current[key3] === target[key2]) { level = key3 } } } // 查找到层数,目标值不相等 if (level !== -1 && current[level] !== target[key2]) { return false } } } else { // Key没有直接返回false return false } } return true } const obj = { a: 0, c: '', d: true, e: { f: 1, e: { e: 0, f: 2, }, }, }; console.log(checkIsChildObject({ a: 0 }, obj)); // true console.log(checkIsChildObject({ e: 0 }, obj)); // true console.log(checkIsChildObject({ a: 0, c: '' }, obj)); // true console.log(checkIsChildObject({ a: 0, e: 0 }, obj)); // false console.log(checkIsChildObject({ e: { f: 1 } }, obj)); // true console.log(checkIsChildObject({ e: { f: 2, e: 0 } }, obj)); // true console.log(checkIsChildObject({ e: { e: 0, f: 2 } }, obj)); // true console.log(checkIsChildObject({ e: { f: 2 } }, obj)); // true