社招前端常考手写面试题总结
- 2022-12-16 浙江
本文字数:11491 字
阅读完需:约 38 分钟
手写 Promise
const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";
function MyPromise(fn) { // 保存初始化状态 var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值 this.value = null;
// 用于保存 resolve 的回调函数 this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数 this.rejectedCallbacks = [];
// 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); }
// 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); }}
MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; };
onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; };
// 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); }
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) { onResolved(this.value); }
if (this.state === REJECTED) { onRejected(this.value); }};
手写 call 函数
call 函数的实现步骤:
判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
处理传入的参数,截取第一个参数后的所有参数。
将函数作为上下文对象的一个属性。
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性。
返回结果。
// call函数实现Function.prototype.myCall = function(context) { // 判断调用对象 if (typeof this !== "function") { console.error("type error"); } // 获取参数 let args = [...arguments].slice(1), result = null; // 判断 context 是否传入,如果未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(...args); // 将属性删除 delete context.fn; return result;};
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像 dom 的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数// wait是等待时间const throttle = (func, wait = 50) => { // 上一次执行该函数的时间 let lastTime = 0 return function(...args) { // 当前时间 let now = +new Date() // 将当前时间和上一次执行函数时间对比 // 如果差值大于设置的等待时间就执行函数 if (now - lastTime > wait) { lastTime = now func.apply(this, args) } }}
setInterval( throttle(() => { console.log(1) }, 500), 1)
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
function throttle(func, delay){ var timer = null; returnfunction(){ var context = this; var args = arguments; if(!timer){ timer = setTimeout(function(){ func.apply(context, args); timer = null; },delay); } }}
适用场景:
DOM元素的拖拽功能实现(mousemove)搜索联想(
keyup)计算鼠标移动的距离(
mousemove)Canvas模拟画板功能(mousemove)监听滚动事件判断是否到页面底部自动加载更多
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
缩放场景:监控浏览器
resize动画场景:避免短时间内多次触发动画引起性能问题
总结
函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
Array.prototype.forEach()
Array.prototype.forEach = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined'); } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; }}
Promise.race
Promise.race = function(promiseArr) { return new Promise((resolve, reject) => { promiseArr.forEach(p => { // 如果不是Promise实例需要转化为Promise实例 Promise.resolve(p).then( val => resolve(val), err => reject(err), ) }) })}
实现非负大整数相加
JavaScript 对数值有范围的限制,限制如下:
Number.MAX_VALUE // 1.7976931348623157e+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.MIN_SAFE_INTEGER // -9007199254740991
如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。
实现一个算法进行大数的相加:
function sumBigNumber(a, b) { let res = ''; let temp = 0;
a = a.split(''); b = b.split('');
while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, '');}
其主要的思路如下:
首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化
初始化 res,temp 来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
将两个数组的对应的位进行相加,两个数相加的结果可能大于 10,所以可能要仅为,对 10 进行取余操作,将结果保存在当前位
判断当前位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会自动隐式转化为 1,以便于下一次相加
重复上述操作,直至计算结束
参考 前端进阶面试题详细解答
JSONP
script 标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于 GET 请求
const jsonp = ({ url, params, callbackName }) => { const generateUrl = () => { let dataSrc = ''; for (let key in params) { if (Object.prototype.hasOwnProperty.call(params, key)) { dataSrc += `${key}=${params[key]}&`; } } dataSrc += `callback=${callbackName}`; return `${url}?${dataSrc}`; } return new Promise((resolve, reject) => { const scriptEle = document.createElement('script'); scriptEle.src = generateUrl(); document.body.appendChild(scriptEle); window[callbackName] = data => { resolve(data); document.removeChild(scriptEle); } })}
实现一个 call
call 做了什么:
将函数设为对象的属性
执行 &删除这个函数
指定 this 到函数并传入给定参数执行函数
如果不传入参数,默认指向为 window
// 模拟 call bar.mycall(null);//实现一个call方法:Function.prototype.myCall = function(context) { //此处没有考虑context非object情况 context.fn = this; let args = []; for (let i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } context.fn(...args); let result = context.fn(...args); delete context.fn; return result;};
使用 Promise 封装 AJAX 请求
// promise 封装实现:function getJSON(url) { // 创建一个 promise 对象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); // 新建一个 http 请求 xhr.open("GET", url, true); // 设置状态的监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功或失败时,改变 promise 的状态 if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; // 设置错误监听函数 xhr.onerror = function() { reject(new Error(this.statusText)); }; // 设置响应的数据类型 xhr.responseType = "json"; // 设置请求头信息 xhr.setRequestHeader("Accept", "application/json"); // 发送 http 请求 xhr.send(null); }); return promise;}
实现一个管理本地缓存过期的函数
封装一个可以设置过期时间的
localStorage存储函数
class Storage{ constructor(name){ this.name = 'storage'; } //设置缓存 setItem(params){ let obj = { name:'', // 存入数据 属性 value:'',// 属性值 expires:"", // 过期时间 startTime:new Date().getTime()//记录何时将值存入缓存,毫秒级 } let options = {}; //将obj和传进来的params合并 Object.assign(options,obj,params); if(options.expires){ //如果options.expires设置了的话 //以options.name为key,options为值放进去 localStorage.setItem(options.name,JSON.stringify(options)); }else{ //如果options.expires没有设置,就判断一下value的类型 let type = Object.prototype.toString.call(options.value); //如果value是对象或者数组对象的类型,就先用JSON.stringify转一下,再存进去 if(Object.prototype.toString.call(options.value) == '[object Object]'){ options.value = JSON.stringify(options.value); } if(Object.prototype.toString.call(options.value) == '[object Array]'){ options.value = JSON.stringify(options.value); } localStorage.setItem(options.name,options.value); } } //拿到缓存 getItem(name){ let item = localStorage.getItem(name); //先将拿到的试着进行json转为对象的形式 try{ item = JSON.parse(item); }catch(error){ //如果不行就不是json的字符串,就直接返回 item = item; } //如果有startTime的值,说明设置了失效时间 if(item.startTime){ let date = new Date().getTime(); //何时将值取出减去刚存入的时间,与item.expires比较,如果大于就是过期了,如果小于或等于就还没过期 if(date - item.startTime > item.expires){ //缓存过期,清除缓存,返回false localStorage.removeItem(name); return false; }else{ //缓存未过期,返回值 return item.value; } }else{ //如果没有设置失效时间,直接返回值 return item; } } //移出缓存 removeItem(name){ localStorage.removeItem(name); } //移出全部缓存 clear(){ localStorage.clear(); }}
用法
let storage = new Storage();storage.setItem({ name:"name", value:"ppp"})
下面我把值取出来
let value = storage.getItem('name');console.log('我是value',value);
设置 5 秒过期
let storage = new Storage();storage.setItem({ name:"name", value:"ppp", expires: 5000})
// 过期后再取出来会变为 falselet value = storage.getItem('name');console.log('我是value',value);
实现一个 JS 函数柯里化
预先处理的思想,利用闭包的机制
柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数
函数柯里化的主要作用和特点就是
参数复用、提前返回和延迟执行
柯里化把多次传入的参数合并,柯里化是一个高阶函数
每次都返回一个新函数
每次入参都是一个
当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?
有两种思路:
通过函数的
length属性,获取函数的形参个数,形参的个数就是所需的参数个数在调用柯里化工具函数时,手动指定所需的参数个数
将这两点结合一下,实现一个简单 curry 函数
通用版
// 写法1function curry(fn, args) { var length = fn.length; var args = args || []; return function(){ newArgs = args.concat(Array.prototype.slice.call(arguments)); if (newArgs.length < length) { return curry.call(this,fn,newArgs); }else{ return fn.apply(this,newArgs); } }}
// 写法2// 分批传入参数// redux 源码的compose也是用了类似柯里化的操作const curry = (fn, arr = []) => {// arr就是我们要收集每次调用时传入的参数 let len = fn.length; // 函数的长度,就是参数的个数
return function(...args) { let newArgs = [...arr, ...args] // 收集每次传入的参数
// 如果传入的参数个数等于我们指定的函数参数个数,就执行指定的真正函数 if(newArgs.length === len) { return fn(...newArgs) } else { // 递归收集参数 return curry(fn, newArgs) } }}
// 测试function multiFn(a, b, c) { return a * b * c;}
var multi = curry(multiFn);
multi(2)(3)(4);multi(2,3,4);multi(2)(3,4);multi(2,3)(4)
ES6 写法
const curry = (fn, arr = []) => (...args) => ( arg => arg.length === fn.length ? fn(...arg) : curry(fn, arg))([...arr, ...args])
// 测试let curryTest=curry((a,b,c,d)=>a+b+c+d)curryTest(1,2,3)(4) //返回10curryTest(1,2)(4)(3) //返回10curryTest(1,2)(3,4) //返回10
// 柯里化求值// 指定的函数function sum(a,b,c,d,e) { return a + b + c + d + e}
// 传入指定的函数,执行一次let newSum = curry(sum)
// 柯里化 每次入参都是一个参数newSum(1)(2)(3)(4)(5)
// 偏函数newSum(1)(2)(3,4,5)
// 柯里化简单应用// 判断类型,参数多少个,就执行多少次收集function isType(type, val) { return Object.prototype.toString.call(val) === `[object ${type}]`}
let newType = curry(isType)
// 相当于把函数参数一个个传了,把第一次先缓存起来let isString = newType('String')let isNumber = newType('Number')
isString('hello world')isNumber(999)
实现每隔一秒打印 1,2,3,4
// 使用闭包实现for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i);}// 使用 let 块级作用域for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000);}
版本号排序的方法
题目描述:有一组版本号如下 ['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']
arr.sort((a, b) => { let i = 0; const arr1 = a.split("."); const arr2 = b.split(".");
while (true) { const s1 = arr1[i]; const s2 = arr2[i]; i++; if (s1 === undefined || s2 === undefined) { return arr2.length - arr1.length; }
if (s1 === s2) continue;
return s2 - s1; }});console.log(arr);
二叉树深度遍历
// 二叉树深度遍历
class Node { constructor(element, parent) { this.parent = parent // 父节点 this.element = element // 当前存储内容 this.left = null // 左子树 this.right = null // 右子树 }}
class BST { constructor(compare) { this.root = null // 树根 this.size = 0 // 树中的节点个数
this.compare = compare || this.compare } compare(a,b) { return a - b } add(element) { if(this.root === null) { this.root = new Node(element, null) this.size++ return } // 获取根节点 用当前添加的进行判断 放左边还是放右边 let currentNode = this.root let compare let parent = null while (currentNode) { compare = this.compare(element, currentNode.element) parent = currentNode // 先将父亲保存起来 // currentNode要不停的变化 if(compare > 0) { currentNode = currentNode.right } else if(compare < 0) { currentNode = currentNode.left } else { currentNode.element = element // 相等时 先覆盖后续处理 } }
let newNode = new Node(element, parent) if(compare > 0) { parent.right = newNode } else if(compare < 0) { parent.left = newNode }
this.size++ } // 前序遍历 preorderTraversal(visitor) { const traversal = node=>{ if(node === null) return visitor.visit(node.element) traversal(node.left) traversal(node.right) } traversal(this.root) } // 中序遍历 inorderTraversal(visitor) { const traversal = node=>{ if(node === null) return traversal(node.left) visitor.visit(node.element) traversal(node.right) } traversal(this.root) } // 后序遍历 posterorderTraversal(visitor) { const traversal = node=>{ if(node === null) return traversal(node.left) traversal(node.right) visitor.visit(node.element) } traversal(this.root) } // 反转二叉树:无论先序、中序、后序、层级都可以反转 invertTree() { const traversal = node=>{ if(node === null) return let temp = node.left node.left = node.right node.right = temp traversal(node.left) traversal(node.right) } traversal(this.root) return this.root }}
先序遍历
二叉树的遍历方式
// 测试var bst = new BST((a,b)=>a.age-b.age) // 模拟sort方法
bst.add({age: 10})bst.add({age: 8})bst.add({age:19})bst.add({age:6})bst.add({age: 15})bst.add({age: 22})bst.add({age: 20})
// 先序遍历// console.log(bst.preorderTraversal(),'先序遍历')// console.log(bst.inorderTraversal(),'中序遍历')// // console.log(bst.posterorderTraversal(),'后序遍历')
// 深度遍历:先序遍历、中序遍历、后续遍历// 广度遍历:层次遍历(同层级遍历)// 都可拿到树中的节点
// 使用访问者模式class Visitor { constructor() { this.visit = function (elem) { elem.age = elem.age*2 } }}
// bst.posterorderTraversal({// visit(elem) {// elem.age = elem.age*10// }// })
// 不能通过索引操作 拿到节点去操作// bst.posterorderTraversal(new Visitor())
console.log(bst.invertTree(),'反转二叉树')
查找字符串中出现最多的字符和个数
例: abbcccddddd -> 字符最多的是 d,出现了 5 次
let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);
封装异步的 fetch,使用 async await 方式来使用
(async () => { class HttpRequestUtil { async get(url) { const res = await fetch(url); const data = await res.json(); return data; } async post(url, data) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await res.json(); return result; } async put(url, data) { const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = await res.json(); return result; } async delete(url, data) { const res = await fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = await res.json(); return result; } } const httpRequestUtil = new HttpRequestUtil(); const res = await httpRequestUtil.get('http://golderbrother.cn/'); console.log(res);})();
实现数组的乱序输出
主要的实现思路就是:
取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
按照上面的规律执行,直到遍历完成
var arr = [1,2,3,4,5,6,7,8,9,10];for (var i = 0; i < arr.length; i++) { const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i; [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];}console.log(arr)
还有一方法就是倒序遍历:
var arr = [1,2,3,4,5,6,7,8,9,10];let length = arr.length, randomIndex, temp; while (length) { randomIndex = Math.floor(Math.random() * length--); temp = arr[length]; arr[length] = arr[randomIndex]; arr[randomIndex] = temp; }console.log(arr)
验证是否是邮箱
function isEmail(email) { var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/; return regx.test(email);}
实现有并行限制的 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();
对象数组列表转成树形结构(处理菜单)
[ { id: 1, text: '节点1', parentId: 0 //这里用0表示为顶级节点 }, { id: 2, text: '节点1_1', parentId: 1 //通过这个字段来确定子父级 } ...]
转成[ { id: 1, text: '节点1', parentId: 0, children: [ { id:2, text: '节点1_1', parentId:1 } ] }]
实现代码如下:
function listToTree(data) { let temp = {}; let treeData = []; for (let i = 0; i < data.length; i++) { temp[data[i].id] = data[i]; } for (let i in temp) { if (+temp[i].parentId != 0) { if (!temp[temp[i].parentId].children) { temp[temp[i].parentId].children = []; } temp[temp[i].parentId].children.push(temp[i]); } else { treeData.push(temp[i]); } } return treeData;}
还未添加个人签名 2022-07-31 加入
还未添加个人简介








评论