前言
常见的 js手写大家经常会在工作面试中遇到, 这次也总结了一些,发现有很多js的文章,这也是最近面试字节问到的,这里整理给大家
数据劫持
vue2数据劫持
functiondefineReactive(data){
if(!data||Object.prototype.toString.call(data)!=='[objectObject]')
return;
for(letkeyindata){
letval=data[key];
Object.defineProperty(data,key,{
enumerable:true,//可枚举
configurable:true,//可配置
get:function(){
track(data,key);
returnval;
},
set:function(){
trigger(val,key);
},
});
if(typeofval==="object"){
defineReactive(val);
}
}
}
functiontrigger(val,key){
console.log("sueset",val,key);
}
functiontrack(val,key){
console.log("sueset",val,key);
}
constdata={
name:'better',
firends:['1','2']
}
defineReactive(data)
console.log(data.name)
console.log(data.firends[1])
console.log(data.firends[0])
console.log(Object.prototype.toString.call(data))
vue3 proxy
functionreactive(obj){
consthandler={
get(target,prop,receiver){
track(target,prop);
constvalue=Reflect.get(...arguments);
if(typeofvalue==='Object'){
reactive(value)
}else{
returnvalue
}
},
set(target,key,value,receiver){
trigger(target,key,value);
returnReflect.set(...arguments);
},
};
returnnewProxy(obj,handler)
}
functiontrack(data,key){
console.log("sueset",data,key);
}
functiontrigger(data,key,value){
console.log("sueset",key,':',value);
}
constdinner={
name:'haochi1'
}
constproxy=reactive(dinner)
proxy.name
proxy.list=[]
proxy.list.push(1)
防抖节流
依稀记得当年孤身面字节的时候
防抖
使用场景: 输入框输入搜索,拖拽( mousemove )
效果: 不是每次操作后执行函数.在频繁操作的最后一次操作结束后在设置的时间内没有触发操作时才执行回调
两种思路
- 立即执行: 在第一次触发事件的时候立即执行当前操作的回调,后面的操作在最后一次操作结束后在设置的时间内没有触发操作时才执行回调
- 无立即执行: 按最后一次操作结束后的规定时间执行
functiondebounce(fn,delay,immediate){
lettimer;//利用闭包保存同一个timer
returnfunction(){
letself=this;
letarg=arguments;
clearTimeout(timer);
if(immediate){
constcallNow=!timer;
timer=setTimeOut(()=>{
timer=null;
},delay);
if(callNow)fn.apply(self.arg);
}else{
timer=setTimeout(()=>{
fn.apply(self,arg);
},delay);
}
};
}
节流
使用场景:滚动条滚动,频繁点击请求接口
效果:预定一个函数只有在大于等于执行周期时才执行
两种思路:
- 时间戳,先会立即执行,达到时间周期再执行
functionthrottle(fn,delay){
lett;
returnfunction(){
letself=this;
letarg=arguments;
if(!t||Date.now()-t>=delay){
fn.apply(self,arg);
t=newDate();
}
};
}
- 定时器,定时一定时间周期之后去执行,但是在这时间内中不停的调用,不让他的定时器清零重新计时,不会影响当前的结果,还是那时间继续等,等到达时间周期后触发(会出现停止操作还是会触发)
functionthrottle(fn,delay){
lettimer
retrunfunction(){
letself=this
letarg=arguments
if(timer)return
timer=setTimeOut(()=>{
fn.apply(fn,arg)
timer=null
},delay)
}
}
call apply bind
三者都是用来改变 this 指向的
- call使用:
function.call(thisArg,arg1,grg2,...)
- thisArg可选参数,function 执行时内部的 this 指向thisArg
- arg1,arg2,... 可选参数,传递给 function 的参数列表
- 返回值:在指定的 this 值和所传递的参数下调用此函数的返回结果 注意:
- function 函数将会立即执行
- 在执行时,会将函数内部的 this 指向 thisArg
- 出 thisArg 外的所有剩余参数将全部传递给 function
- 返回 function 函数执行后的结果
Function.prototype.myCall=function(context,...arr){
console.log('调用mycall中的this',this);
if(context===null||context===undefined){
context=window;
}else{
context=Object(context);
}
constspecialPrototype=Symbol('特殊属性symbol');
context[specialPrototype]=this;//this指向调用者
//context[specialPrototype]执行函数调用时this指向context
letresult=context[specialPrototype](...arr);
deletecontext[specialPrototype];
returnresult;
};
//context:{
//specialPrototype:this->调用者
//}
- apply
注意:
- 使用 apply 只支持两个参数,第一个为 thisArg,第二个是包括多个参数的数组
Function.prototype.myApply=function(context,arr){
console.log(this);
if(context===null||context===undefined){
context=window;
}else{
context=Object(context);
}
constspecialPrototype=Symbol('特殊属性symbol');
context[specialPrototype]=this;
letresult=context[specialPrototype](...arr);
deletecontext[specialPrototype];
returnresult;
};
- bind使用:
function.bind(thisArg,arg1,grg2,...)
- thisArg可选参数,function 执行时会生成一个包裹着function(...)的邦迪函数,并且将function(...)的 this 指向 thisArg,如果使用 new 运算符调用这个生成的绑定函数,则忽略thisArg
- arg1,arg2,... 可选参数,传递给 function 的参数列表
- 返回值:在指定的 this 值和所传递的参数下调用此函数的返回结果 注意:
- bind 方法将创建并返回一个新的函数,新函数称为绑定函数,并且此绑定函数包裹着原始函数
- 执行时,会显示将原始函数内部的 this 指向了thisArg
- 除 thisArg 外所有剩余参数将全部传递给 function
- 执行邦定函数时,如果传递了参数,这些参数将全部传递给原始函数 function
- 如果使用 new 运算符调用生成的绑定函数,则忽略 thisArg
Function.prototype.mybind=function(){
//判断调用bind的是不是函数,抛出异常
if(typeofthis!=='function'){
thrownewError(
'function.prototype.bind-whatistryingtobeboundisnotcallable',
);
}
//将类数组的参数转换成数组然后截取第一个参数
//constargsArr=Array.prototype.slice.call(arguments)
constargsArr=[...arguments];
constargs=argsArr.shift();
constself=this;
constfToBind=function(){
console.log('返回函数的参数',arguments);
constisNew=thisinstanceoffToBind;//this是否是fToBind的实例也就是返回的fToBind是否通过new调用
constcontext=isNew?this:Object(args);//new调用就绑定到this上,否则就绑定到传入的objThis上
returnself.apply(context,argsArr.concat([...arguments]));
};
if(self.prototype){
//复制源函数的prototype给fToBind一些情况下函数没有prototype,比如箭头函数
fToBind.prototype=Object.create(self.prototype);
}
returnfToBind;
};
手写 new 关键字
作用创建一个用户定义的对象类型的实例或者具有构造函数的内置对象的实例
特点可以通过 new 一个构造函数的方式来创建一个对象实例,但是构造函数的差异导致创建的实例有一定的不同
构造函数的返回值不同
- 无返回值:生成一个实例化对象,构造函数中的 this 指向该对象
- 返回一个对象:return 之前的都被覆盖了,new 生成是 return 的对象
- 返回一个非对象:跟没有 return 是一样的结果
//new关键字可以创建对象的实例
//产生一个新的对象
functionmyNew(fn,...args){
constobj=newObject();
obj._proto_=fn.prototype;
//Object.create()方法创建一个新对象,使用现有的对象来提供新创建对象的_proto_
//constobj=Object.create(fn.prototype)
//执行fn并把fn中的this指向新创建的对象
letres=fn.apply(obj,args);
//判断构造函数的返回值是不是object,是object则使用retrun的对象,不是的话就使用生成的对象
returntypeofret==='object'?ret||obj:obj;
}
instanceof
functionmyInstanceof(A,B){
//遍历链表
letp=A;
while(p){
p=p.__proto__;
//B的prototype属性是否出现在A实例对象的原型链上
if(p===B.prototype){
returntrue;
}
}
returnfalse;
}
functionFoo(){}
varf=newFoo();
console.log(myInstanceof(f,Foo));//true
console.log(myInstanceof(f,Object));//true
console.log(myInstanceof([1,2],Array));//true
console.log(myInstanceof({a:1},Array));//false
console.log(myInstanceof(Array,Object));//true
console.log(ArrayinstanceofObject);//true
深浅克隆
functionshallowClone(source){
consttarget={};
for(letkeyinsource){
if(source.hasOwnProperty(key)){
target[key]=source[key];
}
}
returntarget;
}
//深拷贝1.0
functiondeeClone1(source){
if(typeofsource==='object'){
lettarget=Array.isArray(source)?[]:{};
for(letkeyinsource){
if(source.hasOwnProperty(key)){
//如果属性是对象类型则递归再次遍历赋值
target[key]=deeClone1(source[key]);
}
}
returntarget;
}else{
returnsource;
}
}
consttextObject={
field1:1,
field2:undefined,
field3:{
child:'child',
},
field4:[2,4,8],
};
constdeepResult=deeClone1(textObject);
constshallowResult=shallowClone(textObject);
console.log('深克隆',deepResult);
console.log('浅克隆',shallowResult);
deepResult.field4.push(1);
console.log('深克隆',deepResult,textObject);
//深拷贝2.0解决循环引用问题
consttextObject2={
field1:1,
field2:undefined,
field3:{
child:'child',
},
field4:[2,4,8],
};
textObject2.textObject2=textObject2;
//使用1.0克隆克隆循环引用的值会出现爆栈的现象
//constdeepResult2=deeClone1(textObject2);
//深拷贝2.0使用Map
//检查map中有无克隆过的对象
//有-直接返回
//没有-将当前对象作为key,克隆对象作为value进行存储
//继续克隆
functiondeeCloneMap(source,map=newMap()){
if(typeofsource==='object'){
lettarget=Array.isArray(source)?[]:{};
//检查map中有无克隆过的对象
if(map.get(source)){
//有-直接返回
returnmap.get(source);
}
//没有-将当前对象作为key,克隆对象作为value进行存储
map.set(source,target);
for(letkeyinsource){
if(source.hasOwnProperty(key)){
//如果属性是对象类型则递归再次遍历赋值
target[key]=deeCloneMap(source[key],map);
}
}
returntarget;
}else{
returnsource;
}
}
constdeepResult2=deeCloneMap(textObject2);
console.log('mapClone',deepResult2);
//深拷贝2.1使用是WeakMap弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
functiondeeCloneWeakMap(source,map=newWeakMap()){
if(typeofsource==='object'){
lettarget=Array.isArray(source)?[]:{};
//检查map中有无克隆过的对象
if(map.get(source)){
//有-直接返回
returnmap.get(source);
}
//没有-将当前对象作为key,克隆对象作为value进行存储
map.set(source,target);
for(letkeyinsource){
if(source.hasOwnProperty(key)){
//如果属性是对象类型则递归再次遍历赋值
target[key]=deeCloneMap(source[key],map);
}
}
returntarget;
}else{
returnsource;
}
}
//while循环的性能高使用while来实现一个通用的forEach遍历,iteratee是遍历的回掉函数,他可以接收每次遍历的value和index两个参数:
functionforEach(array,iteratee){
letindex=-1;
constlength=array.length;
while(++index<length){
iteratee(array[index],index);
}
returnarray;
}
//深拷贝3.0使用是WeakMap弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
functiondeeCloneWhile(source,map=newWeakMap()){
//1.判断是否为null或undefined
if(typeofsource==null)returnsource;
//2.判断是否为日期Date
if(sourceinstanceofDate)returnnewDate(osourcebj);
//3.判断是否为正则typeof/\d+/==='object'
if(sourceinstanceofRegExp)returnnewRegExp(source);
if(typeofsource==='object'){
constisArray=Array.isArray(source);
lettarget=isArray?[]:{};
//检查map中有无克隆过的对象
if(map.get(source)){
//有-直接返回
returnmap.get(source);
}
//没有-将当前对象作为key,克隆对象作为value进行存储
constkeys=isArray?undefined:Object.keys(source);
map.set(source,target);
forEach(keys||source,(value,key)=>{
if(keys){
key=value;
}
target[key]=deeCloneWhile(source[key],map);
});
//for(letkeyinsource){
//if(source.hasOwnProperty(key)){
如果属性是对象类型则递归再次遍历赋值
//target[key]=deeCloneMap(source[key],map);
//}
//}
returntarget;
}else{
returnsource;
}
}
consttextObject3={
field1:1,
field2:undefined,
field3:{
child:'child',
},
field4:[2,4,8],
f:{
f:{f:{f:{f:{f:{f:{f:{f:{f:{f:{f:{}}}}}}}}}}},
},
};
textObject3.textObject3=textObject3;
constdeepResult3=deeCloneWhile(textObject3);
console.log('deeCloneWhile',deepResult3);
promise
Promise.allSettled
特点接收一个数组作为参数,数组的每一项都是一个Promise对象,返回一个新的Promise对象,只有等到参数数组的所有的Promise对象都发生状态改变,返回的Promise对象才会发生变更
Promise.allSettled=function(arr){
letresult=[];
returnnewPromise((resolve,reject)=>{
arr.forEach((item,index)=>{
Promise.resolve(item)
.then((res)=>{
result.push({
status:'fulfilled',
value:res,
});
result.length===arr.length&&resolve(result);
})
.catch((error)=>{
result.push({
status:'rejected',
value:error,
});
result.length===arr.length&&resolve(result);
});
});
});
};
Promise.all()
Promise.all()方法用于将多个 promise 实例包装成一个新的 Promise 实例
Promise.all=function(arr){
letindex=0,
result=[];
returnnewPromise((resolve,reject)=>{
arr.forEach((item,i)=>{
Promise.resolve(item)
.then((res)=>{
index++;
result[i]=res;
if(index===arr.length)resolve(res);
})
.catch((error)=>reject(error));
});
});
};
手写 fill
最近发现有很多地方让手写 fill 函数
Array.prototype.(fill())[
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/fill] 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素, 不包括终止索引
(MDN)
语法
arr.fill(value[, start[, end]])
fill接收三个参数vlauestart和end,start和end是可选的,其默认值分别为 0 和this对象的length属性值
如果start是个负数, 则开始索引会被自动计算成length+start, 其中length是this对象的length属性值,如果 end 是个负数, 则结束索引会被自动计算成length+end
返回值修改后的数组
示例
[1,2,3].fill(4);//[4,4,4]
[1,2,3].fill(4,1);//[1,4,4]
[1,2,3].fill(4,1,2);//[1,4,3]
[1,2,3].fill(4,1,1);//[1,2,3]
[1,2,3].fill(4,3,3);//[1,2,3]
[1,2,3].fill(4,-3,-2);//[4,2,3]
[1,2,3].fill(4,NaN,NaN);//[1,2,3]
[1,2,3].fill(4,3,5);//[1,2,3]
Array(3).fill(4);//[4,4,4]
[].fill.call({length:3},4);//{0:4,1:4,2:4,length:3}
参考 MDN 中的手写代码
if(!Array.prototype.fill){
Object.defineProperty(Array.prototype,'fill',{
value:function(value){
//Steps1-2.
if(this==null){
thrownewTypeError('thisisnullornotdefined');
}
varO=Object(this);
//Steps3-5.
varlen=O.length>>>0;
//Steps6-7.
varstart=arguments[1];
varrelativeStart=start>>0;
//Step8.
vark=relativeStart<0?
Math.max(len+relativeStart,0):
Math.min(relativeStart,len);
//Steps9-10.
varend=arguments[2];
varrelativeEnd=end===undefined?
len:end>>0;
//Step11.
varfinal=relativeEnd<0?
Math.max(len+relativeEnd,0):
Math.min(relativeEnd,len);
//Step12.
while(k<final){
O[k]=value;
k++;
}
//Step13.
returnO;
}
});
}
数组扁平化
对于数组的扁平化,我已经出了一个详细的文章分析面试 20 个人居然没有一个写出数组扁平化?如何手写 flat 函数
这里也给出一个我比较喜欢的实现方式,其他的实现方式欢迎大家查看原文学习更多的实现方式
思路
通过 some 来判断数组中是否用数组,通过 while 不断循环执行判断, 如果是数组的话可以使用 拓展运算符... ... 每次只能展开最外层的数组,加上 contact 来减少嵌套层数,
functionflatten(arr){
while(arr.some(item=>Array.isArray(item))){
console.log(...arr)
arr=[].concat(...arr)
console.log(arr)
}
returnarr
}
console.log(flatten(arr));
获取 URL 的参数
前段时间自己还在某次尝试中遇到了这个题
获取浏览器参数都很熟悉,第一反应是使用正则表达式去对浏览器的参数进行切割获取,然而浏览器已经提供了一个URLSearchParams这个接口给我们去操作 URL 的查询字符串
URLSearchParams是一个构造函数,可以通过get()方法来获取
1. 使用URLSearchParams构造函数
consturlSearchParams=newURLSearchParams(window.location.search);
constparams=Object.fromEntries(urlSearchParams.entries());
console.log(params);//{id:'2',isShare:'true'}
console.log(params.id);//2
使用正则表达式获取 url
functiongetParams(url,params){
varres=newRegExp('(?:&|/?)'+params+'=([^&$]+)').exec(url);
returnres?res[1]:'';
}
//url:better.com?id=2&isShare=true
constid=getParams(window.location.search,'id');
console.log(id);//2
\
图片懒加载
概念:图片懒加载就是开始加载页面的时候把图片的 src 给替换,在图片出现在页面的可视区域内的时候再加载图片的 src
思路
- 获取所有的 img 标签,获取并替换所有 src 数据,将 src 替换成 data-src
- 判断当前图片是否进入可视区域内,进入后进行展示
getBoundingClientRect方法返回元素的大小及其相对于视口的位置
letimgList=[...document.querySelectorAll('img')]
letlength=imgList.length
constimgLazyLoad=(function(){
letcount=0
returnfunction(){
letdeleteIndexList=[]
imgList.forEach((img,index)=>{
letrect=img.getBoundingClientRect()
if(rect.top<window.innerHeight){
img.src=img.dataset.src
deleteIndexList.push(index)
count++
//优化图片全部加载完成后移除事件监听
if(count===length){
document.removeEventListener('scroll',imgLazyLoad)
}
}
})
//当img加载完图片后将他从imglist中移除
imgList=imgList.filter((img,index)=>!deleteIndexList.includes(index))
}
})()
document.addEventListener('scroll',imgLazyLoad)
函数柯里化
概念将使用多个参数的函数转换成一系列使用一个参数
比如:
functionadd(a,b,c){
retruna+b+c
}
add(1,2,3)
letcurryAdd=curry(add)
curryAdd(1)(2)(3)
思路
functioncurry(fn){
letjudge=(...args)=>{
if(args.length==fn.length)returnfn(...args)
return(...arg)=>judge(...args,...arg)
}
returnjudge
}
实现链表
//节点类
classNode{
constructor(data){
this.data=data
this.next=null
}
}
//链表类
classSinglyLinkedList{
constructor(){
this.head=newNode()//head指向头节点
}
//在链表组后添加节点
add(data){
letnode=newNode(data)
letcurrent=this.head
while(current.next){
current=current.next
}
current.next=node
}
//添加到指定位置
addAt(index,data){
letnode=newNode(data)
letcurrent=this.head
letcounter=1
while(current){
if(counter===index){
node.next=current.next
current.next=node
}
current=current.next
counter++
}
}
//删除某个位置的节点
removeAt(index){
letcurrent=this.head
letcounter=1
while(current.next){
if(counter===index){
current.next=current.next.next
}
current=current.next
counter++
}
}
//反转链表
reverse(){
letcurrent=this.head.next
letprev=this.head
while(current){
letnext=current.next
//反转:改变当前节点指针。若当前节点是第一个(即头节点后面的)节点,
//则此节点的next为null,否则next指向他的上一个节点
current.next=prev===this.head?null:prev
prev=current
current=next
}
this.head.next=prev
}
}
二叉树遍历
前序
varTraversal=function(root){
conststack=[];
while(root||stack.length){
while(root){
stack.push(root);
root=root.left;
}
root=stack.pop();
root=root.right;
}
returnres;
};
中序
//中序遍历(非递归,迭代)
constinorder2=(root)=>{
if(!root)return;
constres=[]
consttrack=[]
//letp=root
while(track.length||root){
//把左子树全入栈
while(root){
track.push(root)
root=root.left
}
root=track.pop()
res.push(root.val)
//遍历根节点左边的节点的右侧
root=root.right
console.log('root',root)
}
returnres
};
后序
varpostorderTraversal=function(root){
//初始化数据
constres=[];
conststack=[];
while(root||stack.length){
while(root){
stack.push(root);
res.unshift(root.val);
root=root.right;
}
root=stack.pop();
root=root.left;
}
returnres;
};
翻转二叉树
//给你一棵二叉树的根节点root,翻转这棵二叉树,并返回其根节点。
/**
*Definitionforabinarytreenode.
*functionTreeNode(val,left,right){
*this.val=(val===undefined?0:val)
*this.left=(left===undefined?null:left)
*this.right=(right===undefined?null:right)
*}
*/
/**
*@param{TreeNode}root
*@return{TreeNode}
*/
varinvertTree=function(root){
letnow=[root]
//广度优先遍历,利用层次遍历
//设置当前存储节点的数组,初始化是根节点
while(now.length){
constnext=[]
now.forEach(item=>{
//节点为null直接返回
if(item===null)return
//定义第三变量交换左右子节点
letn=item.left
item.left=item.right
item.right=n
//将左右子节点放进数组
next.push(item.left)
next.push(item.right)
})
now=next
}
returnroot
};
二分查找
题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
思路:
- 定义头尾两个指针 left = 0 ,right = length 中间索引 mid = left + (right - left) / 2
- 当left <= right 如果mid上的值等于target, 返回mid, 如果小于targt, left = mid + 1(砍掉左半边) 如果大于target , right = mid - 1(砍掉右半边)
- 如果while 循环结束后都没有找到target , 返回-1
constsearch=(nums,target)=>{
leti=0
letj=nums.length-1
letmidIndex=0
while(i<=j){
midIndex=Math.floor((i+j)/2)
constmidValue=nums[midIndex]
if(midValue===target){
returnmidIndex
}elseif(midValue<target){
i=midIndex+1
}else{
j=midIndex-1
}
}
return-1
}
console.log(search([-1,0,3,5,9,12],9))//4
快排
/**
*@param{number[]}nums
*@return{number[]}
*/
varsortArray=function(nums){
//constlength=nums.length
//if(length<=1){
//returnnums
//}
//constmidIndex=Math.floor(length/2)
//constmidValue=nums.splice(midIndex,1)[0]
//letleftArray=[]
//letrightArray=[]
//letindex=0
//while(index<length-1){
//constcurValue=nums[index]
//if(curValue<=midValue){
//leftArray.push(curValue)
//}else{
//rightArray.push(curValue)
//}
//index++
//}
//returnsortArray(leftArray).concat([midValue],sortArray(rightArray))
letleft=0,
right=nums.length-1;
//console.time('QuickSort');
main(nums,left,right);
//console.timeEnd('QuickSort');
returnnums;
functionmain(nums,left,right){
//递归结束的条件,直到数组只包含一个元素。
if(nums.length===1){
//由于是直接修改arr,所以不用返回值。
return;
}
//获取left指针,准备下一轮分解。
letindex=partition(nums,left,right);
if(left<index-1){
//继续分解左边数组。
main(nums,left,index-1);
}
if(index<right){
//分解右边数组。
main(nums,index,right);
}
}
//数组分解函数。
functionpartition(nums,left,right){
//选取中间项为参考点。
letpivot=nums[Math.floor((left+right)/2)];
//循环直到left>right。
while(left<=right){
//持续右移左指针直到其值不小于pivot。
while(nums[left]<pivot){
left++;
}
//持续左移右指针直到其值不大于pivot。
while(nums[right]>pivot){
right--;
}
//此时左指针的值不小于pivot,右指针的值不大于pivot。
//如果left仍然不大于right。
if(left<=right){
//交换两者的值,使得不大于pivot的值在其左侧,不小于pivot的值在其右侧。
[nums[left],nums[right]]=[nums[right],nums[left]];
//左指针右移,右指针左移准备开始下一轮,防止arr[left]和arr[right]都等于pivot然后导致死循环。
left++;
right--;
}
}
//返回左指针作为下一轮分解的依据。
returnleft;
}
};