系列
- # 【3月面经】用ChatGpt来回答前端八股文🔥 - 已完成
- # 【3月面经】前端常考JS编程题🔥 - 本篇
如果你最近也在找工作or看机会,可加我进找工作群分享行情:V798595965,点击扫码加v
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
往期
- 七夕给女朋友看这个,感动哭了😭 100+ 👍🏿
- 你一个开发还搞不懂测试?速来🔥 100+ 👍🏿
- 超极速的Vue3上手指北🔥 130+ 👍🏿
- 聊一聊web图片小知识 100+ 👍🏿
- 【逃离一线】被裁后的面经与感慨 350+ 👍🏿
- 一篇搞定【web打印】知识点 90+ 👍🏿
- 一篇够用的TypeScript总结 800+ 👍🏿
- 一名 vueCoder 总结的 React 基础 240+ 👍🏿
- Vue 转 React不完全指北 900+ 👍🏿
- 跳槽人速来,面经&资源分享 1300+ 👍🏿
- 一年半前端人的求职路 350+ 👍🏿
- vue2.x高阶问题,你能答多少 500+ 👍🏿
- 聊一聊前端性能优化 1700+ 👍🏿
- Egg + Puppeteer 实现Html转PDF(已开源) 70+ 👍🏿
本文正在参加「金石计划」