JS常见代码题 数组去重-自定义new-节流和防抖-深拷贝-instanceof-url参数提取-千位分隔符-数组转树形结构-数组扁平化-函数柯里化

本文详细解析了JavaScript中常见的代码题目,包括数组去重、扁平化、递归实现、自定义new函数、节流与防抖、深拷贝实现、instanceof原理、setTimeout与setInterval的区别及实现、URL参数提取、千位分隔符、数组转树形结构、函数柯里化等,深入理解这些概念对于提升JavaScript编程技能至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JS常见代码题

数组

数组去重

方法1: 利用forEach()和indexOf()
说明: 本质是双重遍历, 效率差些

思路1:遍历数组,建立新数组,利用indexOf判断是否存在于新数组,不存在则push,最后返回新数组。
思路2:数组下标判断法, 遍历数组,利用indexOf判断元素的值是否与当前索引相等,如相等则加入

//1
export function unique1 (array) {
  const arr = []
  array.forEach(item => {
    if (arr.indexOf(item)===-1) {
      arr.push(item)
    }
  })
  return arr
}

方法2: 利用forEach() + 对象容器
说明: 只需一重遍历, 效率高些

思路: 遍历数组,将数组值保存成object对象的属性,判断数组值是否已经保存在object中,未保存则push到新数组并用object[arrayItem]=1的方式记录保存。

export function unique2 (array) {
  const arr = []
  const obj = {}
  array.forEach(item => {
    if (!obj.hasOwnProperty(item)) {
      obj[item] = true
      arr.push(item)
    }
  })
  return arr
}

方法3:利用ES6语法: from + new Set或者 … + new Set
说明: 编码简洁

思路:new Set 返回一个没有重复元素的可迭代对象set,利用…继续展开或者利用Array.from根据可迭代对象创建数组

export function unique3 (array) {
  // return Array.from(new Set(array))
  return [...new Set(array)]
}

使用reduce方法实现

思路:reduce设置初始值为空数组,利用reduce方法,每一次计算都可以知道上一次的值,来进行判断是否重复。最后返回累计值。

export function unique4(array);
 let arr = arr.reduce((accumulator,current)=>{
	return  accumulator.includes(current) ? accumulator : accumulator.concat(current);
 },[])

数组扁平化 flat

将多维数组转换为一维数组
核心就是有数组则继续遍历

[1,[2,3,[4,5]]].flat(Infinity)
递归

循环

function flat(arr){
	const res = [];//存放每一层的数组
	arr.forEach(item =>{
		if(Array.isArray(item))flat = res.concat(flat(item));
		else res.push(item);
	})
	return res;//每一层返回的都是数组
}

reduce写法

function flat(arr){
	return arr.reduce((accumulator,item)=>{
		return accumulator.concat(Array.isArray(item)?flat(item):item)
	},[])
}

对象

自定义new

new的过程中发生了什么?

  1. 创建一个空对象,也就是后面需要返回的实例
  2. 将实例的隐式原型__proto__指向构造函数的显式原型prototype
  3. 执行构造器函数,将构造器函数的this指向实例,为实例添加方法或属性
  4. 获取构造器函数执行的结果,如果构造函数有返回对象,那我们将其返回。如果没有,则返回我们创建的实例
function myNew(Fn,...args){
	let obj = {}; //1
	let obj.__proto__ = Fn.prototype;//2
	let result = Fn.apply(obj,args);//3
	return result instanceof Object ? result : obj;//4
}

函数

节流和防抖

作用是:控制回调函数触发的频率
参数: 控制触发频率的回调函数和时间wait
输出: 到时间后,返回callback函数

节流

节流:在函数被频繁触发时, 函数执行一次后,只有大于设定的执行周期后才会执行第二次。
语法:throttle(callback, wait)
实现思路
1.需要一个变量记录上一次执行的时间,才能判断出是否满足执行的时间间隔
2.如果满足执行的时间间隔,则执行函数
注意点
1.返回函数使用了闭包,闭包会永远在内存中保存所以这个pre都是记录的上一次的结果
2.修改this的目的是让函数的指向指向绑定事件的DOM

//使用形式,绑定时候throttle函数就会执行,所以this是window
window.addEventListener('scroll',throttle(()=>{},500))

//自定义
function throttle(callback,wait){
	let pre=0;
	//console.log(this);window
	//节流函数/真正的事件回调函数
	return function(...args){
		const now = Date.now();
		if(now-pre>wait){
			//callback()是window调用的,所以callback函数里的this是window,这里要修改指向事件源,
			//console.log('this2',this); //DOM
			callback.apply(this,args);
			pre = now;
		}
	}
}
防抖

防抖:触发事件后不会立即执行,需要等待wait时间。如果在等待的过程中再一次触发了事件,计时器重新开始计时wait时间,直到达到wait时间后执行最后一次的回调
语法:debounce(callback, wait)

function debounce(callback, wait){
 	let timeId=null;
 	return funtion(...args){
		if(timeId){//之前已经有一个定时器了,这里再一次触发事件,重新开始即使
			clearTimeout(timer)}
		timeId = setTimeout(()=>{
			callback.apply(this,args)//执行成功之后,重置timeId,所以这里可以起作用
            timeId = null;
		},wait)
				
	}
}

深拷贝

深浅拷贝只是针对引用数据类型
复制之后的副本进行修改会不会影响到原来的

浅拷贝:修改拷贝以后的数据会影响原数据,拷贝的引用。使得原数据不安全。(只拷贝一层)
深拷贝:修改拷贝以后的数据不会影响原数据,拷贝的时候生成新数据

如何实现深拷贝: 递归 + map

参数
1.需要拷贝的对象
2.map存储

实现思路
1.判断是否是引用类型,如果是引用类型循环遍历所有元素进行复制
2.保证对象只克隆了一次,使用Map存储已经克隆之后的对象,目的是:防止循环引用时死循环,A引用B,B中又引用了A,防止套娃

function deepClone(target,map={}){
	//1.判断是否是object或者array,如果是引用类型循环遍历所有元素
	if(typeof target ==='object' && target!==null){
		//2.判断target是否已经被克隆过,已经克隆过就不用克隆了
		if(map.has(target))return map.get(target);
		//2.说明没有克隆过,进行克隆
		let isArray = Array.isArray(target);
		const res = isArray? []:{};
		map.set(target,res); //重要!! res引用类型,先将要进行深拷贝的target放入map中,后续修改res时map中res会跟着一起修改
		if(isArray){//3.数组类型,遍历取数	
			target.forEach((item,index)=>{
				res[index] = deepClone(item,map); //数组中的每一个数都需要深拷贝,递归调用
			})
		}else{//3.对象类型,Object.keys(target)获得属性值
			Object.keys(target).forEach((key)=>{
				res[key] = deepClone(target[key],map);
			})
		}
		return res;//将拷贝结果结果返回
	}
	else{ //1.基本数据类或object array外的数据类型,不需要深拷贝
		return target;
	}
	

}

instanceof

对象的隐式原型的值=对应构造函数的显式原型的值

A instanceof B 去A的隐式原型链找有没有B的显式原型,判断A是不是B的实例,主要针对与引用类型
所以instanceof的参数应该有两个一个是A一个是B,A 为对象或者函数,B为构造函数
返回值为布尔值。

function instanceof(A,B){
	if (typeof B !== 'function') return false; //B为构造函数
     if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function'))return false;//A为对象或函数
	let bp = B,prototype;//取B的显式原型
	let A = A.__proto__;//取A的隐式原型
	while(true){
		if(A === null)return false; //Object的prototype原型的隐式原型__proto__ 为null,也就是原型链的尽头。
		if(A ===bp)return true;
		A = A.__proto__;
	} 
}

学习笔记:https://blog.youkuaiyun.com/qq_41370833/article/details/123301249

其他

用setTimeout实现setInterval

setTimeout:指定的毫秒数后调用一次函数
setInterval:每隔指定的毫秒数后调用函数

为什么推荐使用setTimeout

在定时器中指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。所以添加到队列后,什么时候执行还要看情况。
** 当使用setInterval时,仅当队列中没有该定时器代码实例时,才添加到任务队列中**。
在这里插入图片描述
setInterval的缺点:不一定按时执行

  • 缺点1:使用 setInterval 时,某些间隔会被跳过;
  • 缺点2:可能多个定时器会连续执行;
用setTimeout实现setInterval

利用setTimeout实现setInterval,就是将执行一次的函数周期性调用。
利用递归里面开定时器实现,还可以添加参数表示限制setTimeout执行的次数

//setTimeToInterval需要返回timer
function setTimeToInterval(fn,delay){
	 let timer = null;
	 let interv = function(){
 	 	 func.call(null);
   		 clearTimeout(timer); //清除上一次的setTimeout,使用了闭包,闭包就是在一个函数中能够读取其他函数内部变量

    	 timer=setTimeout(interv, wait);
  	}
  		timer = setTimeout(interv, wait);
  		 //return timer;不能直接return,因为循环执行的是interv,setTimeToInterval只会执行一次,所以timer = mySetInterval(fn, delay) 的时候 timer 被固定
  		 return {  //这里也使用了闭包,用的timer就是最新的timer
  			clear() {
        	    clearTimeout(timer)
        	}
		 }
}
//清除定时器
function myClearInterval(flagTimer) {
    flagTimer.clear()
}

//测试
function testmySetInterval() {
    console.log('testmySetInterval')
}
const timer = mySetInterval(testmySetInterval, 1000)
// 控制台直接调用 myClearInterval(timer)

URL参数提取

输入:URL参数
返回:URL参数组成的对象

  • 正则表达式去获取url
  • URLSearchParams方法
1.正则表达式匹配

案例

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
}
*/

思路

  1. 获取?后面的字符串

RegExpObject.exec(string) 分别获取匹配结果

  • 返回一个数组,其中存放匹配的结果,第一个元素是匹配的文本,第二个开始是子匹配的结果,如果没有找到匹配则返回值为null。
  • 加了()表示子匹配,匹配外部大正则的情况下同时匹配()里的内容
  • 不管是否/g开启全局,只返回匹配的第一个
 const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  1. 通过&进行取出键值对
const paramsArr = paramsStr.split('&');
  1. 将params存到对象中
    分割key和value,将val需要先解码
    如果结果集中有该key,则将值合并成数组
let res = {}; //返回的结果值
paramsArr.forEach(param =>{
	if(/=/.test(param)){//有value的进行处理
		let [key,val] = param.split('='); //分割key和value
	    val = decodeURIComponent(val); //解码
	    if(res.hasOwnProperty(key)){//如果结果集中有该key,则将值合并成数组
			res[key] = [].concat(res[key],val);
		}else {
			res[key] = val;
		}
})

代码

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  let res = {}; //返回的结果值
paramsArr.forEach(param =>{
	if(/=/.test(param)){//有value的进行处理
		let [key,val] = param.split('='); //分割key和value
	    val = decodeURIComponent(val); //解码 先处理一遍
	    if(res.hasOwnProperty(key)){//如果结果集中有该key,则将值合并成数组
			res[key] = [].concat(res[key],val);
		}else {
			res[key] = val;
		}
	})
	return res;
}
URLSearchParams方法 Web API 接口

URLSearchParams接口定义一些使用的方法来处理URL的查询字符串

URLSearchParams() 构造器创建并返回一个新的URLSearchParams 对象。 开头的? 字符会被忽略。

function parseParam(url) {
 	//const paramsStr = /.+\?(.+)$/.exec(url)[1]; 
 	let url = URL.split("?")[1];
 	const urlSearchParams = new URLSearchParams(url);
 	//urlSearchParams.entries()返回的是一个迭代协议iterator
 	//Object.fromEntries()方法将把键值对列表转换为一个对象。
 	const params = Object.fromEntries(urlSearchParams .entries());
 	return params;
 } 

千位分隔符

1000 -> 1,000
1000000 -> 1,000,000
1000000000 -> 1,000,000,000

1. Number.prototype.toLocaleString()

语法:Number.prototype.toLocaleString([locales [, options])

  • locales 语言代码,表示将数字格式化成哪国语言
  • options 格式化时可选的一些配置属性

作用:返回数字在特定语言环境下表示的字符串

const num = 1276482.123;
//默认格式
num.toLocaleString() //1,276,482.123
num.toLocaleString('zh', { style: 'decimal' }) // 1,276,482.123,纯数字格式,
num.toLocaleString('zh', { style: 'percent' }) // 127,648,212%,百分数格式
num.toLocaleString('zh', { style: 'currency', currency: 'CNY' }); // ¥1,276,482.12,人民币形式
2.正则表达式 每个三个数添加一个,

将每隔三个数字替换成三个数字+“,”

1.正向断言(?=):x只有在y前面才匹配,必须写成/x(?=y)/,y作为定位元素
所以我们需要获取的是 从后往前数,3的倍数个数字左边的第一个数字
(\d)(?=((\d){3})+ $) 其中$可以表示以xxx结尾,也就是从后往前数

2.string.replace(匹配的内容,替换的内容)进行内容替换,默认只替换第一个,使用全局匹配模式g
$n正则表达式匹配的第n个子匹配的结果

vartoThousands = function(number) {
    return (number + '').replace(/(\d)(?=(\d{3})+$)/g, '$1,');
    //return String(number).replace(/(\d)(?=(\d{3})+$)/g, '$1,')
    /*
    假设number是1234567
    第一次匹配到的数是1 $1=1 修改之后是1,234567
    第二次从2开始匹配,匹配到234 修改之后是1,234,567
    */
}
3.倒序遍历,使用额外变量记录已拼接字符长度

给你一个整数 n,请你每隔三位添加点(即 “,” 符号)作为千位分隔符,并将结果以字符串格式返回。
思路
将数字转为字符串后,从字符串尾部开始遍历,使用额外变量tmpLength记录已经拼接的字符串长度,tmpLength % 3的值作为是否拼接分隔符的条件

function thousandSeparator2(n: number): string {
  // 数值转为字符串,分割,反转数组,重新拼接
  const s = String(n);

  let newS = '';
  // 记录已拼接字符长度 - 这里是刨除千位分隔符.
  let tmpLength = 0;

  // 索引从s.length - 1开始,倒序
  for (let i = s.length - 1; i >= 0; i--) {
    // 千位分割数的拼接条件:已拼接字符 % 3 === 0 
    // 同时排除第一个字符 tmpLength === 0时的情况
    if (tmpLength % 3 === 0 && tmpLength !== 0) {
      // 拼接千位分割数
      newS = s[i] + ',' + newS; //加在新字符串的前面
    } else {
      // 直接拼接
      newS = s[i] + newS; //加在新字符串的前面
    }
    
    //表示已经拼接的字符数量
    tmpLength++;
  }

  return newS;
}

数组转树形结构

let arr = [
    {id: 1, name: '部门1', pid: 0},
    {id: 2, name: '部门2', pid: 1},
    {id: 3, name: '部门3', pid: 1},
    {id: 4, name: '部门4', pid: 3},
    {id: 5, name: '部门5', pid: 4},
]
//结果
[
    {
        "id": 1,
        "name": "部门1",
        "pid": 0,
        "children": [
            {
                "id": 2,
                "name": "部门2",
                "pid": 1,
                "children": []
            },
            {
                "id": 3,
                "name": "部门3",
                "pid": 1,
                "children": [
                    // 结果 ,,,
                ]
            }
        ]
    }
]
递归实现

主要的问题是如何根据pid,将数组转换成有层级的结构

思路
通过pid寻找父节点的孩子节点,插入到children属性中

递归的参数和返回值
原数组:arr
需要插入到父节点的children属性中,所以使用一个数组来表示是父节点的children
当前的节点的父节点:pid 用于判断找的是哪个父节点的孩子节点

function getChildren(arr,res,pid){};

本层递归的逻辑
需要找到父节点是pid的节点,将该节点转成对象形式插入到父节点的children中

function getChildren(arr,res,pid){
	  for (const item of arr) { //寻找父节点是pid的节点
			if(item.pid === pid){ 
				const newItem = {...item,children:[]}; //找到了将孩子节点转化成对象形式
				res.push(newItem); //加入父节点的children属性中
				getChildren(arr,newItem.children,item.id); //寻找当前孩子节点的孩子节点
			}
	  }
}

const arrayToTree (arr){
		const result = [];
		getChildren(arr,result ,0);
		return result;
}
非递归实现 优先回答-性能好一点

map中存放key为id,value为节点元素
目的是遍历节点的时候,可以通过key来找父节点

value为节点元素,节点元素是一个对象,所以是引用类型,对引用类型的修改,在最后的结果中也可以看见

function createTree(arr){
	const map = {}
	arr.forEach(item=>{
		if(!item.children)item.children = []; //没有children 属性添加children 属性
		map[item.id] = item; //key为id,value为元素本身
	})
	const result= [];//存放结果集
	arr.forEach(item=>{ 
		const pItem = data[item.pid]; //对象的引用,去找父节点是否存在
		if(pItem ){//找到父节点了
			if(pItem.children)pItem.children.push(item); //如果已经有孩子节点了,直接push
			else pItem.children=[item]; //没有则创建children数组
		}else{ //没找到说明是根节点,根节点可能有多个,所以返回数组,直接将根节点push进去
			result.push(item);
		}
	})
	return result;
}

函数柯里化

函数柯里化:将使用多个参数的一个函数变成一系列使用一个或多个参数的函数。
柯里化是一种编程思想,函数执行产生一个闭包,把一些信息预先存储起来,目的是供下级上下文使用。这样预先存储和处理的思想,就叫做柯里化的编程思想。

函数柯里化的主要作用和特点

  • 参数复用
    在这里插入图片描述

  • 提前返回

  • 延迟执行 要接受3个参数的函数,可以先接收1个或者2个,等接收完3个后再执行

function sum(a,b,c) {
    console.log(a+b+c)
}
//方法1:利用工具函数 生成的柯里化函数
let fn = curry(sum);
fn(1,2,3); //6
fn(1)(2)(3); //6
fn(1,2)(3); //6

首先可以知道每一次函数调用的参数个数是不确定的,但是总的个数是确定的,根据柯里化函数curry接收到的函数参数可以确定。
对柯里化后的fn函数来说,当接受的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。

function curry(fn) {
//进行参数缓存
return function curryFn(...arg){
    if(arg.length<fn.length){//fn.length可以获取函数的参数
        return function (...arg2){
          return curryFn(...arg.concat(Array.from(arg2)));
        }
    }else{
      return fn(...arg);
    }
}
}
curry((a,b,c)=>{
  console.log(a+b+c);
})(2)(2)(3)
不知道参数个数,调用时没有传参那么就执行该函数
function sum() {
    console.log(...arguments)
}

function curry(fn) {
    let args = [];
    return function curryFn(...args1){ 
       if(args1.length!=0){
        args = args.concat(args1);//说明本次参数不为零,将参数缓存下来
        return (...args2)=>{
            return curryFn(...args2); //判断新参数
        }
       }else{//说明本次参数为0
        return fn(...args);
       }
    }
}

let fn = curry(sum);
fn(1,2,3)(4)(5,6)(); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值