前端手写综合考题
1 实现一个
// 使用 promise来实现 sleepconst sleep = (time) => {return new Promise(resolve => setTimeout(resolve, time))}sleep(1000).then(() => {// 这里写你的骚操作})
sleep 函数,比如 sleep(1000) 意味着等待1000毫秒
2 给定两个数组,写一个方法来计算它们的交集
例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。
function union(arr1, arr2) {return arr1.filter(item => {return arr2.indexOf(item) > -1;})}const a = [1, 2, 2, 1];const b = [2, 3, 2];console.log(union(a, b)); // [2, 2]
3 异步并发数限制
/* 关键点* 1. new promise 一经创建,立即执行* 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法* 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里* 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行* 5. 任务完成后,需要从 doingTasks 中移出*/
4 异步串行 | 异步并行
// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {setTimeout(function () {callback(null, a + b);}, 500);
}// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {asyncAdd(a, b, (err, res) => {if (err) {reject(err)} else {resolve(res)}})
})// 2. 串行处理
async function serialSum(...args) {return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}// 3. 并行处理
async function parallelSum(...args) {if (args.length === 1) return args[0]const tasks = []for (let i = 0; i < args.length; i += 2) {tasks.push(promiseAdd(args[i], args[i + 1] || 0))}const results = await Promise.all(tasks)return parallelSum(...results)
}// 测试
(async () => {console.log('Running...');const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)console.log(res1)const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)console.log(res2)console.log('Done');
})()
5 实现有并行限制的 Promise 调度器
题目描述:JS 实现一个带并发限制的异步调度器 Scheduler
,保证同时运行的任务最多有两个
addTask(1000,"1");addTask(500,"2");addTask(300,"3");addTask(400,"4");的输出顺序是:2 3 1 4整个的完整执行流程:一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4
class Scheduler {constructor(limit) {this.queue = [];this.maxCount = limit;this.runCounts = 0;}add(time, order) {const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(order);resolve();}, time);});};this.queue.push(promiseCreator);}taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();}}request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}this.runCounts++;this.queue.shift()().then(() => {this.runCounts--;this.request();});}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
6 图片懒加载
// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {const position = el.getBoundingClientRect()const windowHeight = document.documentElement.clientHeight// 顶部边缘可见const topVisible = position.top > 0 && position.top < windowHeight;// 底部边缘可见const bottomVisible = position.bottom < windowHeight && position.bottom > 0;return topVisible || bottomVisible;
}function imageLazyLoad() {const images = document.querySelectorAll('img')for (let img of images) {const realSrc = img.dataset.srcif (!realSrc) continueif (isVisible(img)) {img.src = realSrcimg.dataset.src = ''}}
}// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))
7 实现 getValue/setValue 函数来获取path对应的值
// 示例
var object = { a: [{ b: { c: 3 } }] }; // path: 'a[0].b.c'
var array = [{ a: { b: [1] } }]; // path: '[0].a.b[0]'function getValue(target, valuePath, defaultValue) {}console.log(getValue(object, "a[0].b.c", 0)); // 输出3
console.log(getValue(array, "[0].a.b[0]", 12)); // 输出 1
console.log(getValue(array, "[0].a.b[0].c", 12)); // 输出 12
/* 测试属性是否匹配*/
export function testPropTypes(value, type, dev) {const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaNconst oEnums = ['Null', 'Object', 'Array', 'Date', 'RegExp', 'Error'];const nEnums = ['[object Number]','[object String]','[object Boolean]','[object Undefined]','[object Function]','[object Null]','[object Object]','[object Array]','[object Date]','[object RegExp]','[object Error]',];const reg = new RegExp('\\\\[object (.*?)\\\\]');// 完全匹配模式,type应该传递类似格式[object Window] [object HTMLDocument] ...if (reg.test(type)) {// 排除nEnums的12种if (~nEnums.indexOf(type)) {if (dev === true) {console.warn(value, 'The parameter type belongs to one of 12 types:number string boolean undefined Null Object Array Date RegExp function Error NaN');}}if (Object.prototype.toString.call(value) === type) {return true;}return false;}
}
const syncVarIterator = {getter: function (obj, key, defaultValue) {// 结果变量const defaultResult = defaultValue === undefined ? undefined : defaultValue;if (testPropTypes(obj, 'Object') === false && testPropTypes(obj, 'Array') === false) {return defaultResult;}// 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改let result = obj;// 得到知道值try {// 解析属性层次序列const keyArr = key.split('.');// 迭代obj对象属性for (let i = 0; i < keyArr.length; i++) {// 如果第 i 层属性存在对应的值则迭代该属性值if (result[keyArr[i]] !== undefined) {result = result[keyArr[i]];// 如果不存在则返回未定义} else {return defaultResult;}}} catch (e) {return defaultResult;}// 返回获取的结果return result;},setter: function (obj, key, val) {// 如果不存在obj则返回未定义if (testPropTypes(obj, 'Object') === false) {return false;}// 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改let result = obj;try {// 解析属性层次序列const keyArr = key.split('.');let i = 0;// 迭代obj对象属性for (; i < keyArr.length - 1; i++) {// 如果第 i 层属性对应的值不存在,则定义为对象if (result[keyArr[i]] === undefined) {result[keyArr[i]] = {};}// 如果第 i 层属性对应的值不是对象(Object)的一个实例,则抛出错误if (!(result[keyArr[i]] instanceof Object)) {throw new Error('obj.' + keyArr.splice(0, i + 1).join('.') + 'is not Object');}// 迭代该层属性值result = result[keyArr[i]];}// 设置属性值result[keyArr[i]] = val;return true;} catch (e) {return false;}},
};
使用promise来实现
创建 enhancedObject
函数
const enhancedObject = (target) =>new Proxy(target, {get(target, property) {if (property in target) {return target[property];} else {return searchFor(property, target); //实际使用时要对value值进行复位}},});let value = null;
function searchFor(property, target) {for (const key of Object.keys(target)) {if (typeof target[key] === "object") {searchFor(property, target[key]);} else if (typeof target[property] !== "undefined") {value = target[property];break;}}return value;
}
使用 enhancedObject
函数
const data = enhancedObject({user: {name: "test",settings: {theme: "dark",},},
});console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark
8 创建10个标签,点击的时候弹出来对应的序号
var a
for(let i=0;i<10;i++){a=document.createElement('a')a.innerHTML=i+'<br>'a.addEventListener('click',function(e){console.log(this) //this为当前点击的<a>e.preventDefault() //如果调用这个方法,默认事件行为将不再触发。//例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。alert(i)})const d=document.querySelector('div')d.appendChild(a) //append向一个已存在的元素追加该元素。
}
9.版本号排序的方法
题目描述:有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
- 对版本号数组进行排序,可以使用数组的sort方法,传入一个比较函数用于比较版本号大小。比较函数接受两个参数,表示要比较的两个版本号。
- 在比较函数中,需要将版本号字符串按照.分割成数字数组,然后依次比较每一位数字的大小。如果某一位数字不同,则直接返回差值,否则比较下一位数字,直到所有数字都比较完毕。
- 注意对版本号长度不同的情况进行处理,可以在比较函数中使用数组的fill方法将版本号长度补齐为相同的长度,然后再进行比较。
const versions = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'];versions.sort((a, b) => {const arrA = a.split('.').map(Number);const arrB = b.split('.').map(Number);const len = Math.max(arrA.length, arrB.length);arrA.length = len;arrB.length = len;for (let i = 0; i < len; i++) {if (arrA[i] !== arrB[i]) {return arrB[i] - arrA[i];}}return 0;
});console.log(versions); // ['4.3.5', '4.3.4.5', '2.3.3', '0.302.1', '0.1.1']
10 请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式
<div><span><a></a></span><span><a></a><a></a></span>
</div>把上面dom结构转成下面的JSON格式{tag: 'DIV',children: [{tag: 'SPAN',children: [{ tag: 'A', children: [] }]},{tag: 'SPAN',children: [{ tag: 'A', children: [] },{ tag: 'A', children: [] }]}]
}
function dom2Json(domtree) {let obj = {};obj.tag = domtree.tagName;obj.children = [];domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));return obj;
}
11 分片思想解决大数据量渲染问题
let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total / once;
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal, curIndex) {if (curTotal <= 0) {return false;}//每页多少条let pageCount = Math.min(curTotal, once);window.requestAnimationFrame(function () {for (let i = 0; i < pageCount; i++) {let li = document.createElement("li");li.innerText = curIndex + i + " : " + ~~(Math.random() * total);ul.appendChild(li);}loop(curTotal - pageCount, curIndex + pageCount);});
}
loop(total, index);
12 实现一个add方法完成两个大数相加
// 题目
let a = "9007199254740991";
let b = "1234567899999999999";function add(a ,b){//...
}
function add(a ,b){//取两个数字的最大长度let maxLength = Math.max(a.length, b.length);//用0去补齐长度a = a.padStart(maxLength , 0);//"0009007199254740991"b = b.padStart(maxLength , 0);//"1234567899999999999"//定义加法过程中需要用到的变量let t = 0;let f = 0; //"进位"let sum = "";for(let i=maxLength-1 ; i>=0 ; i--){t = parseInt(a[i]) + parseInt(b[i]) + f;f = Math.floor(t/10);sum = t%10 + sum;}if(f!==0){sum = '' + f + sum;}return sum;
}
13 怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)
第一版 时间复杂度为 O(n^2)function getTenNum(testArray, n) {let result = [];for (let i = 0; i < n; ++i) {const random = Math.floor(Math.random() * testArray.length);const cur = testArray[random];if (result.includes(cur)) {i--;break;}result.push(cur);}return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);
第二版 标记法 / 自定义属性法 时间复杂度为 O(n)function getTenNum(testArray, n) {let hash = {};let result = [];let ranNum = n;while (ranNum > 0) {const ran = Math.floor(Math.random() * testArray.length);if (!hash[ran]) {hash[ran] = true;result.push(ran);ranNum--;}}return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);
第三版 交换法 时间复杂度为 O(n)
function getTenNum(testArray, n) {const cloneArr = [...testArray];let result = [];for (let i = 0; i < n; i++) {debugger;const ran = Math.floor(Math.random() * (cloneArr.length - i));result.push(cloneArr[ran]);cloneArr[ran] = cloneArr[cloneArr.length - i - 1];}return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 14);
14 查找数组公共前缀(美团)
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。示例 1:输入:strs = ["flower","flow","flight"]
输出:"fl"示例 2:输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
const longestCommonPrefix = function (strs) {const str = strs[0];let index = 0;while (index < str.length) {const strCur = str.slice(0, index + 1);for (let i = 0; i < strs.length; i++) {if (!strs[i] || !strs[i].startsWith(strCur)) {return str.slice(0, index);}}index++;}return str;
};
15 判断括号字符串是否有效(小米)
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。示例 1:输入:s = "()"
输出:true示例 2:输入:s = "()[]{}"
输出:true示例 3:输入:s = "(]"
输出:false
const isValid = function (s) {if (s.length % 2 === 1) {return false;}const regObj = {"{": "}","(": ")","[": "]",};let stack = [];for (let i = 0; i < s.length; i++) {if (s[i] === "{" || s[i] === "(" || s[i] === "[") {stack.push(s[i]);} else {const cur = stack.pop();if (s[i] !== regObj[cur]) {return false;}}}if (stack.length) {return false;}return true;
};
16 实现一个padStart()或padEnd()的polyfil
String.prototype.padStart
和String.prototype.padEnd
是ES8
中新增的方法,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。我们先看下使用语法:
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'// 1. 若是输入的目标长度小于字符串原本的长度则返回字符串本身
'xxx'.padStart(2, 's') // 'xxx'// 2. 第二个参数的默认值为 " ",长度是为1的
// 3. 而此参数可能是个不确定长度的字符串,若是要填充的内容达到了目标长度,则将不要的部分截取
'xxx'.padStart(5, 'sss') // ssxxx// 4. 可用来处理日期、金额格式化问题
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
String.prototype.myPadStart = function (targetLen, padString = " ") {if (!targetLen) {throw new Error('请输入需要填充到的长度');}let originStr = String(this); // 获取到调用的字符串, 因为this原本是String{},所以需要用String转为字符串let originLen = originStr.length; // 调用的字符串原本的长度if (originLen >= targetLen) return originStr; // 若是 原本 > 目标 则返回原本字符串let diffNum = targetLen - originLen; // 10 - 6 // 差值for (let i = 0; i < diffNum; i++) { // 要添加几个成员for (let j = 0; j < padString.length; j++) { // 输入的padString的长度可能不为1if (originStr.length === targetLen) break; // 判断每一次添加之后是否到了目标长度originStr = `${padString[j]}${originStr}`;}if (originStr.length === targetLen) break;}return originStr;
}
console.log('xxx'.myPadStart(16))
console.log('xxx'.padStart(16))
17 设计一个方法提取对象中所有value大于2的键值对并返回最新的对象
var obj = { a: 1, b: 3, c: 4 }
foo(obj) // { b: 3, c: 4 }
var obj = { a: 1, b: 3, c: 4 }
function foo (obj) {return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value > 2))
}
var obj2 = foo(obj) // { b: 3, c: 4 }
console.log(obj2)
// ES8中 Object.entries()的作用:
var obj = { a: 1, b: 2 }
var entries = Object.entries(obj); // [['a', 1], ['b', 2]]
// ES10中 Object.fromEntries()的作用:
Object.fromEntries(entries); // { a: 1, b: 2 }
18 实现一个拖拽
<style>html, body {margin: 0;height: 100%;}#box {width: 100px;height: 100px;background-color: red;position: absolute;top: 100px;left: 100px;}
</style>
<div id="box"></div>
window.onload = function () {var box = document.getElementById('box');box.onmousedown = function (ev) {var oEvent = ev || window.event; // 兼容火狐,火狐下没有window.eventvar distanceX = oEvent.clientX - box.offsetLeft; // 鼠标到可视区左边的距离 - box到页面左边的距离var distanceY = oEvent.clientY - box.offsetTop;document.onmousemove = function (ev) {var oEvent = ev || window.event;var left = oEvent.clientX - distanceX;var top = oEvent.clientY - distanceY;if (left <= 0) {left = 0;} else if (left >= document.documentElement.clientWidth - box.offsetWidth) {left = document.documentElement.clientWidth - box.offsetWidth;}if (top <= 0) {top = 0;} else if (top >= document.documentElement.clientHeight - box.offsetHeight) {top = document.documentElement.clientHeight - box.offsetHeight;}box.style.left = left + 'px';box.style.top = top + 'px';}box.onmouseup = function () {document.onmousemove = null;box.onmouseup = null;}}
}
基于Promise.all实现Ajax的串行和并行
- 串行:请求是异步的,需要等待上一个请求成功,才能执行下一个请求
- 并行:同时发送多个请求「
HTTP
请求可以同时进行,但是JS的操作都是一步步的来的,因为JS是单线程」,等待所有请求都成功,我们再去做什么事情?
Promise.all并发限制及async-pool的应用
并发限制指的是,每个时刻并发执行的promise数量是固定的,最终的执行结果还是保持与原来的
const delay = function delay(interval) {return new Promise((resolve, reject) => {setTimeout(() => {// if (interval === 1003) reject('xxx');resolve(interval);}, interval);});
};
let tasks = [() => {return delay(1000);
}, () => {return delay(1003);
}, () => {return delay(1005);
}, () => {return delay(1002);
}, () => {return delay(1004);
}, () => {return delay(1006);
}];/* Promise.all(tasks.map(task => task())).then(results => {console.log(results);
}); */let results = [];
asyncPool(2, tasks, (task, next) => {task().then(result => {results.push(result);next();});
}, () => {console.log(results);
});
20 修改嵌套层级很深对象的 key
// 有一个嵌套层次很深的对象,key 都是 a_b 形式 ,需要改成 ab 的形式,注意不能用递归。const a = {a_y: {a_z: {y_x: 6},b_c: 1}
}
// {
// ay: {
// az: {
// yx: 6
// },
// bc: 1
// }
// }
const regularExpress = (obj) => {try {const str = JSON.stringify(obj).replace(/_/g, "");return JSON.parse(str);} catch (error) {return obj;}
};;
const recursion = (obj) => {const keys = Object.keys(obj);keys.forEach((key) => {const newKey = key.replace(/_/g, "");obj[newKey] = recursion(obj[key]);delete obj[key];});return obj;
};