前端面试中有一个重要的环节,也是面试者最担心的一个环节。对于"手撕代码"的考察需要面试者平时的总结和积累。比如leetcode不刷个40,50道你敢去大厂面试吗?我之前一直就是温水煮青蛙,出去面试了几次才被教各种做人。
一般来说,如果代码写得好,即使理论知识答的不够好,也有大概率通过面试的,其实很多的手写背后往往就考察了你对相关理论知识的认识。
浅拷贝&深拷贝
1、浅拷贝指的是创建新的数据,这个数据有这有着原始数据属性值的一份精准拷贝,如果属性值是基本类型,拷贝的是基本类型的值,如果是引用类型,拷贝的事内存地址,浅拷贝只拷贝一层。深层次的引用类型则是共享内存地址。
function shallowClone(obj){
const newObj={};
for(let prop of obj){
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop]
}
}
return newObj;
}
2、深拷贝开辟一个新栈,两个对象属性完全相同,但对应不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
function deepClone(obj,hash=new WeakMap()){
if(obj===null) return obj;
if(obj instanceof Date) return new Date(obj);
if(typeof obj!=="object") return obj;
// 如果是对象的话,进行深拷贝
if(hash.get(obj)) return hash.get(obj)
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的constructor指向的是当前类本身
hash.set(obj,cloneObj)
for(let key of obj){
cloneObj[key]=deepClone(obj[key],hash)
}
return cloneObj
}
防抖&节流
防抖,n秒后在执行改事件,如在n秒内重复触发,则重新计算。
function debounce(fn,awit){
let timer =null;
return function(){
let context = this;
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn.apply(context,arguments)
},awit)
}
}
节流,n秒内只运行一次,若在n秒内重复触发,只有一个生效。
function throttle(fn,delay){
let preTime = Date.now();
return function(){
let context = this;
let nowTime = Data.now;
// 如果两次时间超过了指定时间,则执行该函数
if(nowTime - preTime >= delay){
return fn.apply(context,arguments)
}
}
}
实现call方法
Function.prototype.myCall = function myCall(){
let [thisArg,...args ] = Array.form(arguments);
if(!thisArg){
thisArg = typeof window==="undefined" ? global : window
}
// this的指向是当前函数func
// 为thisArg对象添加func方法,func指向myCall,所以func中this指向thisArg
thisArg.func = this;
// 执行函数
let result = thisArg.func(...args)
// thisArg 上并没有func属性,因此需要移除
delete thisArg.func;
return result;
}
实现apply方法
Function.prototype.myApply = function myApply(){
// 第一个参数为this对象,第二个参数为数组。
let [ thisArg,args] = Array.from(arguments);
if(!thisArg){
thisArg = typeof window === "undefined" ? global : window;
}
// this 的指向是当前函数,func
thisArg.func = this;
// 执行函数
let result = thisArg.func(...args);
// thisArg 上并没有func属性,因此需要移除
delete thisArg.func;
return result;
}
渲染几万条数据不卡住页面
setTimeout(()=>{
const total = 10000;
// 一次插入的数据
const once = 20;
const loopCount = Math.ceil(total/once);
let countOfRender = 0;
const ul = document.querySelector("ul");
// 添加数据的方法
function add(){
// createDocumentFragment 创建一个文档碎片,是一个虚拟节点,不是文档树,把所有的新节点附加其上,然后再将文档碎片的内容一次性添加,
// 当我们把documentFragment 节点插入文档树时,插入的不是DocumentFragment本身,而是他的所有子孙节点,这使得DocumentFragment成了一个有用的占位符,暂时存放那些一次性插入文档的节点。
/**
* 当需要向页面添加许多DOM元素时,如果一个个createElement出来,然后在一个个appendChild上去,会频繁的操作DOM,很影响性能。
*/
const fragment = document.createDocumentFragment();
for(let i=0;i<once;i++){
const li = document.createElement("li");
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li)
}
ul.appendChild(fragment);
countOfRender+=1;
loop();
}
function loop(){
if(countOfRender < loopCount){
//requestAnimationFrame 类似于一个setInterval,它不需要设置时间间隔。他的时间间隔由系统定义,一般为16.67ms
window.requestAnimationFrame(add)
}
}
},0)
手写new 操作符
function myNew(Func,...args){
// 创建一个对象
const obj = {};
// 新对象的原型指向构造函数的原型对象
obj.__proto__ = FunC.prototype;
// 将构造函数的this 指向新对象
let result = Func.apply(this,args)
// 判断返回值,如果是值类型,返回创建的类型,如果是引用对象,就返回这个引用对象
return result instanceOf Object ? result : obj
}
手写一个数组分块chunk
function chunk (arr,size){
if(arr.length===0 || size===1){
return arr;
}
let arr2=[];
for(let i=0;i<arr.length;i+=size){
// slice 从已有数组中返回选定的元素,返回一个新数组,包含从start到end,不包含end,不会改变原数组。
// end 参数没传,默认表示从start到数组结束的所有元素
arr2.push(arr.slice(i,i+size)
}
return arr2;
}
leetCode 1941. 检查是否所有字符出现的次数相同
function areOccurrencesEqual(s){
let map=new Map();
for(let i=0;i<s.length;i++){
if(!map.get(s[i]){
map.set(s[i],1)
}else{
map.set(s[i],map.get(s[i])+1)
}
return [...new Set([...map.values()])].length === 0
// return [...map.values()].every(i=>map.get[0]==i)
}
console.log(areOccurrencesEqual("abacbc")) // true
leetCode 448. 找到所有数组中消失的数字
给你一个含n个整数的数组nums,其中nums[i]在区间[1,n]内,请你找出所有在[1,n]范围内但没有出现在nums中的数字,并以数组的形式返回。
// 解法1
function findDisappearedNumber(nums){
let res=[];
for(let i=1;i<nums.length+1;i++){
if(!nums[i].includes(i)){
res.push(i)
}
}
return res;
}
console.log(maxProfit([4,2,3,7,8,2,3,1])) // [5,6]
leetCode 121. 买股票的最佳时机
给定一个数组price,他的第i个元素price[i]表示一支给定股票第i天的价格。 你只能选择某一天买入这只股票,并在未来的某天卖出。使的你获得能获取的最大的利润。
function maxProfit(price){
// minPrice 先定义第一天为最低价
let profit = 0, minPrice=price[0];
// 遍历数据
for(let i=1;i<price.length;i++){
// 如果发现比最低价还低,更新最低价
minPrice = Math.min(minPrice,price[i]);
// 如果发现利润比之前还大,更新利润
profit = Math.max(profit,price[i]-minPrice)
}
return profit
}
console.log(maxProfit([7,1,5,3,6,4])) // [5]
leetCode 349. 两个数组的交集
function intersection(nums1,nums2){
let arr=[];
for(let ch of nums1){
if(nums2.includes(ch) && !arr.includes(ch)){
return arr.push(ch)
}
}
return arr
}
console.log(intersection([4,9,5],[9,4,9,8,4])) // [4,9]
leetCode 136. 只出现一次的数字
除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
// a^a = 0
// a^0 = a
// a^b^a = (a^a)^b = 0^b=b
function singleNumber(nums){
let single = 0;
for(let ch of nums){
single = single ^ ch
}
return single
}
console.log(singleNumber(2,2,1)) // 1
leetCode 14. 最长公共前缀
查找字符串数组中最长的公共前缀,如果不存在公共前缀,返回空字符串""。
function longestCommonPrefix(strs){
if(!strs.length) return "";
let res = strs[0];
for(ch of strs){
for(let i=0;i<res.length;i++){
if(ch[i]!==res[i]){
res = res.slice(0,i)
}
}
}
return res;
}
console.log(longestCommonPrefix(["flower","flow","flight"])) // fl
leetCode 20.有效的括号
给定一个只包括"(",")","[","]","{","}"的字符串a,判断字符串是否有效。 有效字符串需满足,左括号必须用相同的右括号闭合,左括号必须以正常的顺序闭合。
function isValid(s){
let s1 = s.length;
// 字符串长度为奇数,肯定是不匹配
if(s1 % 2) return false;
let map = {
")":"(",
"]":"[",
"}":"{",
}
let stack =[];
for(let i of s){
let topIndex = stack.length-1;
// 获取栈顶的元素
let top = stack[topIndex] ? stack[topIndex]:-1;
if(map[i]==top){
stack.pop();
}else{
stack.push(i)
}
}
return !stack.length;
}
找出有序数组中与目标值最近的元素
1、使用reduce
// 如 1 2 3 6 7 8 ,目标值为4,返回 2
getClosestNumber(arr,target){
return arr.reduce((pre,next)=>{
return Math.abs(pre-target) < Math.abs( next-target) ? pre : next
},{})
}
// console.log(getClosestNumber2([1,3,4,5],2)) // 1
2、二分法
getClosestNumber2(arr,target){
let left = 0;
let right = arr.length -1;
let mid = ""
while( right - left > 1){
mid = Math.floor((right + left)/2);
if(arr[mid] > target){
right = mid;
}else{
left = mid
}
}
return Math.abs(arr[left]-target) > Math.abs(arr[right]-target) ? arr[right] : arr[left]
}
console.log(getClosestNumber3([1,2,4,6,7,4,5],4),"二分法求") // 4
大数相加
// 这里只考虑数字字符串相加
function bigNumber(str1,str2){
const arr1 = str1.split("").reverse();
const arr2 = str2.split("").reverse();
const length = Math.max(arr1.length,arr2.length);
let flag = 0; // 是否需要进位
let res = [];
for(let i=0;i<length;i++){
const num1 = Number(arr1[i] || 0);
const num2 = Number(arr2[i] || 0);
let sum = num1 + num2 + flag
if(sum > 10){
sum = sum % 10;
flag = 1;
}else{
flag = 0
}
res.push(num);
if(i===length-1 && flag){
res.push(flag)
}
}
return res.reverse().join("")
}
手动实现发布订阅模式
发布订阅模式,他其实是对象间一对多的依赖关系,一个对象状态发生改变时,所有依赖他的对象都将得到状态改变的通知
class EventEmitter{
constructor(){
this.cache = {}
}
// 发布事件
on(eventName,fn){
// 判断是否发布过事件名称 ?添加发布 :创建并添加发布
if(this.cache[eventName]){
this.cache[eventName].push(fn)
}else{
this.cache[eventName]=[fn]
}
}
// 订阅事件
emit(eventName){
if(!eventName) throw new Error("请传入事件名")
// 获取订阅的参数
const data = [...arguments].slice(1)
if(this.cache[eventName]){
this.cache[eventName].forEach((i)=>{
try {
i(...data)
} catch (error) {
console.log(e,"eventName" + eventName)
}
})
}
}
// 取消订阅
off(eventName,fn){
// 不传入参数时,全部取消订阅
if(!argument.length){
return this.cache = {}
}
// eventName 传入是一个数组时,取消多个订阅
if(Array.isArray(eventName)){
eventName.forEach((event)=>{
this.off(event,fn)
})
}
// 不传fn时取消事件名下所有的列队
if(arguments.length==1 || !fn){
this.cache[eventName] = []
}
// 取消事件名下所有的列队
this.cache[eventName] = this.cache[eventName].filter(f=>f!==fn)
}
}
移动零
给定一个数组,将所有的0都移动到数组的末尾。
function zero(arr){
let res = []
let j=0;
for(let i=0;i<array.length;i++){
if(array[i]!==0){
res.push(array[i])
}else{
j++
}
}
for(let i=j;i<array.length;i++){
res.push(0)
}
return res;
}
// console.log(zeroMove([0,1,0,0,3,12,0,4,5,2,0])) // [1, 3, 12, 4, 5,2, 0, 0, 0, 0,0, 0]
统计对象的层数
function getLeval(obj){
let result = 0;
if(obj===null) return 0;
function getObjLeval(params,level=0){
if(typeof params==="object" && params!==null){
Object.keys(params).forEach((item)=>{
if(typeof item==="object" && item!==null){
getObjLeval(item,level+1)
}else{
result = level +1 > result ? level +1 : result
}
})
}else{
result = level > result ? level : result
}
}
getObjLeval(obj)
return result
}
随机生成一个长度为10 的整数类型数组
例如:[2,10,3,35,5,11,10,11,20]将其排列成一个新数组,要求新数组的形式如下 [[2,3,5],[10,11],[20],[35]]
// 生成0-99 之间的随机数字
// 生成5-10之间的随机数,Math.random()生成0到1, 0-1 * (5 -10)
function randomNumber(min,max){
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random()*(max-min +1))
}
let initArray = Array.from({length:10},()=>randomNumber(0,99));
// 去重
initArray = new Set([...initArray]);
const map = {};
initArray.forEach((item)=>{
const key = Math.floor(item / 10);
if(map[key]){
map[key]=[]
}
map[key].push(item)
})
const result = [];
for(let key of map){
result.push(map[key])
}
无重复字符串的最长子串
给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。例如输入s="abcabcab",输出3,因为无重复最长子串是"abc",所以长度是3
function longestSubstring(s){
let arr = [];
let max = 0;
for(let i=0;i<s.length;i++){
// indexOf 判断元素是否在数组中出现过
let index = arr.indexOf(s[i]);
if(index!==-1){
// 出现过,则从数组开头到当前字符串全部截取调
arr.splice(0,index+1)
}
// 放入新的字符
arr.push(s[i])
// 更新下最大值
max = Math.max(arr.length,max);
}
return max
}
console.log(longestSubstring("pwwkew")) // 3
数组转成树形结构
原数组如下:
const data = [ { id: '01', name: '张大大', pid: '', job: '项目经理' },
{ id: '02', name: '小亮', pid: '01', job: '产品leader' },
{ id: '03', name: '小美', pid: '01', job: 'UIleader' },
{ id: '04', name: '老马', pid: '01', job: '技术leader' },
{ id: '05', name: '老王', pid: '01', job: '测试leader' },
{ id: '06', name: '老李', pid: '01', job: '运维leader' },
{ id: '07', name: '小丽', pid: '02', job: '产品经理' },
{ id: '08', name: '大光', pid: '02', job: '产品经理' },
{ id: '09', name: '小高', pid: '03', job: 'UI设计师' },
{ id: '10', name: '小刘', pid: '04', job: '前端工程师' },
{ id: '11', name: '小华', pid: '04', job: '后端工程师' },
{ id: '12', name: '小李', pid: '04', job: '后端工程师' },
{ id: '13', name: '小赵', pid: '05', job: '测试工程师' },
{ id: '14', name: '小强', pid: '05', job: '测试工程师' },
{ id: '15', name: '小涛', pid: '06', job: '运维工程师' } ]
转成树形结构:不使用递归
function arrToTree(data){
let tree = [];
if(!Array.isArray(data)){
return tree
}
// 将数组转成对象,id作为属性名,原来的数组里的对象作为属性值
let map={};
data.forEach((item)=>{
map[item.id] = item
})
// 通过对象的属性名ID,来找到父节点,将存到map里的对象取出来放到父节点里的children数组中。
data.forEach((item)=>{
let parent = map[item.pid];
item['label']=item.name;
if(parent){
(parent.children || (parent.children = [])).push(item)
}else{
tree.push(item)
}
})
return tree
}
console.log(JSON.stringify(arrToTree(list)),"arrToTree");