js
实用工具集合
js
import SparkMD5 from 'spark-md5'
// 获取选中的文本
export function getSelectedText() {
// 获取Selection对象
const selection = window.getSelection()
return selection.toString()
}
// 获取文件内容
export async function readerFile(file) {
return new Promise((resolve, reject) => {
// 创建一个FileReader实例
const reader = new FileReader()
// 当文件读取完成时执行的函数
reader.onload = function (e) {
// 读取完成,e.target.result包含了文件内容
// 你可以根据文件类型(例如文本、图片等)来处理内容
resolve(e.target.result) // 在控制台显示文件内容
}
reader.onabort = function (e) {
reject('文件读取中断', e)
}
reader.onerror = function (e) {
reject('文件读取错误', e)
}
// 读取文件内容的方法,这里以读取文本文件为例
reader.readAsText(file)
})
}
// 非空检测
export function isEmpty(value) {
let status = false
// 直接检查null、undefined、空字符串""
if (value == null || value === '') status = true
// 检查空数组[]
if (Array.isArray(value) && value.length === 0) status = true
// 对于对象,我们只检查它是否不是null且不是空对象{}(注意:这不会检查嵌套对象)
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) status = true
// 如果以上都不是,则认为值不为空
return status
}
// 复制函数
export async function copyhandler(jsonValue, toJSON = false) {
function copyToClipboard(text) {
const textArea = document.createElement('textarea')
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textArea.value = text
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
const successful = document.execCommand('copy')
if (!successful) return new Error('复制失败')
} catch (err) {
return new Error(`复制失败:${err}`)
}
document.body.removeChild(textArea)
}
try {
const copyData = toJSON ? JSON.stringify(jsonValue) : jsonValue
if (navigator?.clipboard?.writeText) {
return await navigator.clipboard.writeText(copyData)
} else {
return copyToClipboard(copyData)
}
} catch (e) {
return new Error(`复制失败:${err}`)
}
}
// 控制并发
export class RequestQueue {
constructor(maxConcurrent = 3, maxRetries = 3) {
this.maxConcurrent = maxConcurrent //最大并发数
this.maxRetries = maxRetries //最大错误重试次数
this.queue = [] //请求等待队列
this.currentRequests = 0 //当前并发数
this.reqSuccessCount = 1 //成功请求的次数
this.logList = []
this.hasBreak = false
}
// 添加 请求 到 请求等待队列
enqueue(requestFunc, requestParams, requestAfterHandler, retryCount = 0) {
this.queue.push({ requestFunc, requestParams, requestAfterHandler, retryCount })
this.processQueue()
}
setLogList(logList) {
this.logList = logList
}
setHasBreak() {
this.hasBreak = true
}
onError(error) {
console.error('超过最大重试次数:', error)
this.setHasBreak()
}
setOnError(fn) {
this.onError = fn
}
setStatus(status) {
this.status = status
}
// 处理队列中的请求
async processQueue() {
while (this.currentRequests < this.maxConcurrent && this.queue.length > 0 && !this.hasBreak) {
this.currentRequests++
const { requestFunc, requestParams, requestAfterHandler, retryCount } = this.queue.shift()
try {
const res = await requestFunc(requestParams) // 假设requestFunc是一个返回Promise的函数
//扩展 请求之后处理的功能
const req = { ...requestParams, reqSuccessCount: this.reqSuccessCount }
// 请求成功,调用 requestAfterHandler 回调函数
requestAfterHandler && requestAfterHandler({ req, res })
//放在最后面,防止自定义的requestAfterHandler报错导致total计算错误
//扩展 添加进度日志-成功日志
this.reqSuccessCount += 1
const { reqSuccessCount, chunkCount } = req
if (chunkCount && reqSuccessCount && this.logList?.length) {
if (chunkCount && reqSuccessCount) {
this.logList.push({
message: `正在${this.status || '请求'}中,进度${reqSuccessCount}/${chunkCount}`
})
if (reqSuccessCount == chunkCount) {
this.logList.push({ message: `${this.status || '请求'}完毕`, isSuccess: true })
}
}
}
} catch (error) {
//扩展 添加进度日志-失败日志
if (retryCount < this.maxRetries) {
const message = `请求失败,正在进行第 (${retryCount + 1}/${this.maxRetries})次重试...`
this.logList.push({ message, isError: true, error })
// 重新将请求(带有递增的重试次数)添加到队列末尾
this.enqueue(requestFunc, requestParams, requestAfterHandler, retryCount + 1)
} else {
const message = '超过最大重试次数:'
this.logList.push({ message, isError: true, error })
// 请求失败,传递错误信息
requestAfterHandler && requestAfterHandler({ req, error })
this.onError(error)
break
}
} finally {
this.currentRequests--
// 递归调用以处理更多请求
this.processQueue()
}
}
}
}
// 控制并发 使用示例:
function RequestQueueDemo() {
// 最多同时发送2个请求,最大重试次数为3
const queue = new RequestQueue(2, 3)
// 假设fetchData是一个返回Promise的函数,用于发送网络请求
const fetchData = ({ url }) => fetch(url).then((response) => response.json())
// 添加请求到队列
queue.enqueue(fetchData, { url: 'https://api.example.com/data1' })
}
// 大文件上传
export class FileChecksumCalculator {
constructor(file, chunkSize) {
this.file = file
this.totalSize = file.size
this.chunkSize = chunkSize || 1024 * 1024 * 5 // 默认5MB的块大小
this.chunkCount = Math.ceil(this.totalSize / this.chunkSize)
this.currentIndex = 0 //当前请求成功的最后
this.chunks = []
}
//文件合并
//我们上传的每个chunk端会用特殊命名存起来,例如chunk-唯一值-hash值
//前端调用合并接口后,后端根据文件的md5值,将所有chunk合并到一块存到数据库
requstAfterHandler({ req: { reqSuccessCount }, res }) {
if (reqSuccessCount >= this.chunkCount) {
this.mergeHandler() //模拟的函数
}
}
setRequstAfterHandler(fn) {
const requstAfterHandler = this.requstAfterHandler
this.requstAfterHandler = (params) => {
requstAfterHandler.call(this, params)
fn.call(this, params)
}
}
//切片上传
//利用文件对象的slice和size方法,将文件分成一段一段的小片存在数组中
createChunks(requestFunc, requestParams = {}) {
try {
// 计算分片数
const { chunkSize, totalSize, chunkCount } = this
for (let index = 0; index < chunkCount; index++) {
const start = chunkSize * index
const end = Math.min(chunkSize * (index + 1), totalSize)
this.chunks.push(this.file.slice(start, end))
requestFunc(
{ ...requestParams, index, start, end, chunkSize, chunkCount },
this.requstAfterHandler.bind(this)
)
}
} catch (error) {
console.error('Error downloading file:', error)
}
}
//断点续传
//调用校验接口,约定接口返回 上传成功的chunk 或 未上传的chunk(这两者只上传未上传的) 或 断开的下标(从失败下标,清除后重传)
//秒传
//将整个文件md5加密,将加密后的md5串传给后端,后端校验是否存在这个md5串,存在就返回特殊状态,前端直接状态改成已上传
//由于大文件比较大,直接整个一起加密,容错率低,一般一块一块的加密,最终输出结果
//一般使用SparkMD5包,npm i -S spark-md5
getMd5() {
const chunks = this.chunks
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer()
//一片一片的读取文件内容,最终生成md5
readChunk(0)
function readChunk(i) {
if (i >= chunks.length) {
const md5 = spark.end()
resolve(md5)
return
}
const blob = chunks[i]
const reader = new FileReader()
reader.readAsArrayBuffer(blob)
reader.onload = (e) => {
const bytes = e.target.result
spark.append(bytes)
readChunk(i + 1)
}
reader.onerror = () => {
reject(new Error('Error reading file chunk'))
}
}
})
}
async upload({
requstAfterHandler,
requestFunc,
maxConcurrent = 5,
maxRetries = 3,
...requestParams
}) {
if (RequestQueue && requestFunc) {
// 最多同时发送5个请求,最大重试次数为3
const queue = new RequestQueue(maxConcurrent, maxRetries)
//请求之后要干啥的回调
if (requstAfterHandler) {
this.setRequstAfterHandler(requstAfterHandler)
}
//args接收
const args = { ...requestParams, index, start, end, chunkSize, chunkCount },
requstAfterHandler
this.createChunk((...args) => queue.enqueue(requestFunc, ...args), requestParams)
return this.getMd5()
} else {
return new Error('请先引入控制并发函数')
}
}
}
// 大文件下载
export class ChunkedDownloader {
constructor({ totalSize, chunkSize = 1024 * 1024, fileName, fileType }) {
this.totalSize = typeof totalSize == 'function' ? totalSize() : totalSize
this.chunkSize = chunkSize
this.chunkCount = Math.ceil(totalSize / chunkSize)
this.fileName = fileName
this.fileType = fileType
this.mergeChunk = []
}
setDownHandler(fn) {
this.downFile = fn
}
downFile(...args) {
downFile(...args)
}
setRequstAfterHandler(fn) {
const requstAfterHandler = this.requstAfterHandler
this.requstAfterHandler = (params) => {
requstAfterHandler.call(this, params)
fn.call(this, params)
}
}
requstAfterHandler({ req: { index, reqSuccessCount }, res }) {
if (reqSuccessCount < this.chunkCount) {
this.mergeChunk[index] = res
} else {
this.mergeChunk[index] = res
this.downFile(this.mergeChunk, this.fileType, this.fileName)
}
}
createChunks(requestFunc, requestParams = {}) {
try {
// 计算分片数
const { chunkSize, totalSize, chunkCount } = this
for (let index = 0; index < chunkCount; index++) {
const start = chunkSize * index
const end = Math.min(chunkSize * (index + 1), totalSize)
requestFunc(
{ ...requestParams, index, start, end, chunkSize, chunkCount },
this.requstAfterHandler.bind(this)
)
}
} catch (error) {
console.error('Error downloading file:', error)
}
}
beackRequest() {
this.queue?.setHasBreak()
this.queue = null
}
down({
requestAfterHandler,
requestFunc,
logList,
maxConcurrent = 5,
maxRetries = 3,
...requestParams
}) {
if (RequestQueue) {
requestAfterHandler && this.setRequstAfterHandler(requestAfterHandler)
//提供控制并发功能
const queue = new RequestQueue(maxConcurrent, maxRetries) // 最多同时发送5个请求,最大重试次数为3
queue.setStatus('下载')
logList && queue.setLogList(logList)
this.queue = queue
this.createChunks((...args) => queue.enqueue(requestFunc, ...args), requestParams)
}
}
}
// 单位转换
export function converSize(size, fromUnit, toUnit, decimalPoint = 2) {
size && (!formUnit || !toUnit)
? formatSizeUnits(size)
: convertFileSize(size, fromUnit, toUnit, (decimalPoint = 2))
}
/**
* 将文件大小格式化到仅保留两位小数的最大单位
*/
export function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB']
let unitIndex = 0
while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024
unitIndex++
}
return `${kb.toFixed(2)} ${units[unitIndex]}`
}
/**
* 将文件大小从一个单位转换为另一个单位。
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
* */
export function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit)
const toIndex = units.indexOf(toUnit)
// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units')
}
// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent)
// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit
}
获取选中的文本
js
/**
* 获取选中的文本
**/
export function getSelectedText() {
// 获取Selection对象
const selection = window.getSelection()
return selection.toString()
}
读取文件
js
/**
* 获取文件内容
**/
export async function readerFile(file) {
return new Promise((resolve, reject) => {
// 创建一个FileReader实例
const reader = new FileReader();
// 当文件读取完成时执行的函数
reader.onload = function(e) {
// 读取完成,e.target.result包含了文件内容
// 你可以根据文件类型(例如文本、图片等)来处理内容
resolve(e.target.result); // 在控制台显示文件内容
};
reader.onabort = function(e) {
reject('文件读取中断');
};
reader.onerror = function(e) {
reject('文件读取错误');
};
// 读取文件内容的方法,这里以读取文本文件为例
reader.readAsText(file);
});
}
部分属性触发 watch
js
//以下实现,如果对象中多个属性同时改变会反复触发,此时需要一个变量作为标志,避免重复调用
create(){
const exclundekeys = ['traceId', 'duration', 'servicePort', 'searchKey'];
Object.keys(this.searchData).forEach(key => {
!exclundekeys.includes(key) && this.$watch(`searchData.${key}`, () => this.log());
});
}
//or 采用
//添加一个标识 避免重复触发,
//使用nextTick使数据更新完毕才触发
/**
* 浅度监听对象改变
* @param v 要监听的对象
* @param fn 监听到改变要触发的函数
* @param excludeKeys 改变后不需要触发监听的key
* @return {Promise<void>}
*/
async function objWatch(v, fn, excludeKeys = []) {
const awaitFn = async () => {
await this.$nextTick(async () => {
await fn();
this.oldFilterData = { ...v };
});
};
if (!this.oldFilterData) return await awaitFn();
let change = false;
for (const key of Object.keys(v)) {
if (!excludeKeys.includes(key) && this.oldFilterData[key] != v[key]) {
change = true;
break;
}
}
if (change) await awaitFn();
}
watch: {
filterData: {
async handler(v) {
const exclundekeys = ['traceId', 'duration', 'servicePort', 'searchKey', 'serviceNames'];
await objWatch.call(this, v, this.log, exclundekeys);
},
deep: true,
immediate: true
},
}
监控网络状态
javascript
created () {
//监听navigator状态变化
if(navigator)navigator.connection.onchange =this.navigatorChange
},
navigatorChange(){
if(!navigator)return;
this.onLine=navigator.onLine //是否在线
this.effectiveType=navigator.connection.effectiveType //网络类型,例如4g
}
非空检测
js
function isEmpty(value) {
let status=false;
// 直接检查null、undefined、空字符串""
if (value == null || value === "") status=true;
// 检查空数组[]
if (Array.isArray(value) && value.length === 0) status=true;
// 对于对象,我们只检查它是否不是null且不是空对象{}(注意:这不会检查嵌套对象)
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) status=true;
// 如果以上都不是,则认为值不为空
return status;
}
arr to tree
javascript
let arrData = [
{ id: 1, title: "1", pid: 0, isLeaf: false, children: [] },
{ id: 11, title: "1-1", pid: 1, isLeaf: false, children: [] },
{ id: 111, title: "1-1-1", pid: 11, isLeaf: true, children: [] },
];
// #region 双for,2O复杂度
// function transferTree(list) {
// var map = {};
// var result = [];
// list.forEach((item) => {
// map[item.id] = item;
// });
// list.forEach((item) => {
// let parent = map[item.pid];
// if (parent) {
// if (!parent.children) {
// parent.children = [];
// }
// parent.children.push(item);
// } else {
// result.push(item);
// }
// });
// return result;
// }
// console.log(transferTree(arrData));
tree to arr
javascript
function transferArr(list,arr=[]) {
list.forEach((item) => {
if(item.children){
transferArr(item.children,arr);
}
arr.push(item.id);
});
return arr;
}
const arr=transferArr(你要转换的树);
数组.filter(i=>arr.includes(i.id))
复制函数
javascript
async function copyhandler(jsonValue, toJSON = false) => {
function copyToClipboard(text) {
var textArea = document.createElement('textarea');
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
if(!successful)return new Error('复制失败');
} catch (err) {
return new Error(`复制失败:${err}`);
}
document.body.removeChild(textArea);
}
try {
const copyData = toJSON ? JSON.stringify(jsonValue) : jsonValue;
if (navigator?.clipboard?.writeText) {
return await navigator.clipboard.writeText(copyData);
} else {
return copyToClipboard(copyData);
}
} catch (e) {
return new Error(`复制失败:${err}`);
}
};
过滤特殊字符
javascript
function removeInvisibleChars(str) {
// 替换空白字符(空格、制表符、换行符等)
str = str.replace(/\s/g, '');
// 替换零宽度空格等特殊字符
// 这里直接使用Unicode码点匹配
str = str.replace(/[\u200B-\u200D\uFEFF]/g, '');
// 替换ASCII控制字符(0x00-0x1F)和删除(0x7F)
str = str.replace(/[\x00-\x1F\x7F]/g, '');
return str;
}
// 示例字符串
var exampleStr = "Hello\u200B World!";
console.log(removeInvisibleChars(exampleStr)); // 打
文件预览(pdf/图片等)
javascript
//正常预览链接
//<a href="path/to/your/file.pdf" target="_blank">预览PDF文件</a>
控制请求并发
关键点在于用一个变量算现在的请求数量
javascript
export class RequestQueue {
constructor(maxConcurrent = 3, maxRetries = 3) {
this.maxConcurrent = maxConcurrent; //最大并发数
this.maxRetries = maxRetries; //最大错误重试次数
this.queue = []; //请求等待队列
this.currentRequests = 0; //当前并发数
this.reqSuccessCount=1; //成功请求的次数
this.logList=[]
this.hasBreak=false;
}
// 添加 请求 到 请求等待队列
enqueue(requestFunc,requestParams, requestAfterHandler,retryCount = 0) {
this.queue.push({ requestFunc,requestParams,requestAfterHandler, retryCount });
this.processQueue();
}
setLogList(logList){
this.logList=logList
}
setHasBreak(){
this.hasBreak=true;
}
onError(error){
console.error('超过最大重试次数:', error);
this.setHasBreak();
}
setOnError(fn){
this.onError=fn;
}
setStatus(status){
this.status=status;
}
// 处理队列中的请求
async processQueue() {
while (this.currentRequests < this.maxConcurrent && this.queue.length > 0&&!this.hasBreak){
this.currentRequests++;
const { requestFunc,requestParams,requestAfterHandler, retryCount } = this.queue.shift();
try {
const res=await requestFunc(requestParams); // 假设requestFunc是一个返回Promise的函数
//扩展 请求之后处理的功能
const req={...requestParams,reqSuccessCount:this.reqSuccessCount}
// 请求成功,调用 requestAfterHandler 回调函数
requestAfterHandler && requestAfterHandler({req,res});
//放在最后面,防止自定义的requestAfterHandler报错导致total计算错误
//扩展 添加进度日志-成功日志
this.reqSuccessCount+=1;
const { reqSuccessCount,chunkCount }=req;
if(chunkCount&&reqSuccessCount&&this.logList?.length){
if(chunkCount&&reqSuccessCount){
this.logList.push({
message:`正在${this.status||'请求'}中,进度${reqSuccessCount}/${chunkCount}`
})
if(reqSuccessCount==chunkCount) {
this.logList.push({ message: `${this.status||'请求'}完毕`, isSuccess: true })
}
}
}
} catch (error) {
//扩展 添加进度日志-失败日志
if (retryCount < this.maxRetries) {
const message=`请求失败,正在进行第 (${retryCount + 1}/${this.maxRetries})次重试...`
this.logList.push({message,isError:true,error})
// 重新将请求(带有递增的重试次数)添加到队列末尾
this.enqueue(requestFunc,requestParams,requestAfterHandler, retryCount + 1);
} else {
const message='超过最大重试次数:'
this.logList.push({message,isError:true,error})
// 请求失败,传递错误信息
requestAfterHandler && requestAfterHandler({req,error});
this.onError(error);
break;
}
} finally {
this.currentRequests--;
// 递归调用以处理更多请求
this.processQueue();
}
}
}
}
// 使用示例:
// 最多同时发送2个请求,最大重试次数为3
const queue = new RequestQueue(2, 3);
// 假设fetchData是一个返回Promise的函数,用于发送网络请求
const fetchData = (url) => fetch(url).then(response => response.json());
// 添加请求到队列
queue.enqueue(fetchData,'https://api.example.com/data1');
大文件上传
文件上传一般有两种方式,fromData和base64 base64可跨平台。但是会比之前的文件大三分之一。导致请求很慢,不适合大文件上传 formData文件流形式可以上传额外信息,当然也可以上传多个文件,适合大文件上传
javascript
// 引入SparkMD5库
import SparkMD5 from 'spark-md5';
import {RequestQueue} from '@/utils' //见文件下载,控制请求并发
class FileChecksumCalculator {
constructor(file, chunkSize) {
this.file = file;
this.totalSize = file.size;
this.chunkSize = chunkSize || 1024 * 1024 * 5; // 默认5MB的块大小
this.chunkCount = Math.ceil(this.totalSize / this.chunkSize);
this.currentIndex = 0;//当前请求成功的最后
this.chunks = [];
}
//文件合并
//我们上传的每个chunk端会用特殊命名存起来,例如chunk-唯一值-hash值
//前端调用合并接口后,后端根据文件的md5值,将所有chunk合并到一块存到数据库
requstAfterHandler({req:{reqSuccessCount},res}){
if(reqSuccessCount>=this.chunkCount){
this.mergeHandler() //模拟的函数
}
}
setRequstAfterHandler(fn){
const requstAfterHandler=this.requstAfterHandler;
this.requstAfterHandler=(params)=>{
requstAfterHandler.call(this,params);
fn.call(this,params)
}
}
//切片上传
//利用文件对象的slice和size方法,将文件分成一段一段的小片存在数组中
createChunks(requestFunc,requestParams={}) {
try {
// 计算分片数
const { chunkSize,totalSize, chunkCount } = this;
for (let index = 0; index < chunkCount; index++) {
const start = chunkSize * index;
const end = Math.min(chunkSize * (index+1), totalSize);
this.chunks.push(this.file.slice(start, end));
requestFunc(
{...requestParams,index,start,end,chunkSize,chunkCount},
this.requstAfterHandler.bind(this)
)
}
} catch (error) {
console.error('Error downloading file:', error);
}
}
//断点续传
//调用校验接口,约定接口返回 上传成功的chunk 或 未上传的chunk(这两者只上传未上传的) 或 断开的下标(从失败下标,清除后重传)
//秒传
//将整个文件md5加密,将加密后的md5串传给后端,后端校验是否存在这个md5串,存在就返回特殊状态,前端直接状态改成已上传
//由于大文件比较大,直接整个一起加密,容错率低,一般一块一块的加密,最终输出结果
//一般使用SparkMD5包,npm i -S spark-md5
getMd5() {
const chunks = this.chunks;
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
//一片一片的读取文件内容,最终生成md5
readChunk(0);
function readChunk(i) {
if (i >= chunks.length) {
const md5 = spark.end();
resolve(md5);
return;
}
const blob = chunks[i];
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = (e) => {
const bytes = e.target.result;
spark.append(bytes);
readChunk(i + 1);
};
reader.onerror = () => {
reject(new Error('Error reading file chunk'));
};
}
});
}
async upload({
requstAfterHandler,
requestFunc,
maxConcurrent = 5,
maxRetries = 3,
...requestParams
}){
if(RequestQueue&&requestFunc){
// 最多同时发送5个请求,最大重试次数为3
const queue = new RequestQueue(maxConcurrent, maxRetries);
//请求之后要干啥的回调
if(requstAfterHandler){
this.setRequstAfterHandler(requstAfterHandler);
}
//args接收
const args={...requestParams,index,start,end,chunkSize,chunkCount},requstAfterHandler;
this.createChunk((...args)=>queue.enqueue(requestFunc,...args),requestParams);
return this.getMd5()
}else{
return new Error('请先引入控制并发函数')
}
}
}
javascript
// 使用示例
<Input type="file" @change="hanleInputChange" />
hanleInputChange= async (e) => {
const file = e.target.files[0];
if (file) {
const calculator = new FileChecksumCalculator(file);
try {
//请求函数&请求之后做的事
const requestFunc = (...req)=>{}
const requstAfterHandler=function ({req:{total,chunkCount}}){}
const requestParams={requestFunc,requestAfterHandler,...一些请求参数}
//注意,虽然相同文件内容两次计算出来的md值相同,但是如果这个文件被压缩了,zip也是一种加密算法,这时候两个zip算出来的md5值会不一样
const md5 = await calculator.upload(requestParams)
console.log('File MD5:', md5);
// 发送MD5到服务器进行验证等操作...
} catch (error) {
console.error('Error calculating MD5:', error);
}
}
};
文件预览(pdf/图片等)
javascript
//正常预览链接
<a href="path/to/your/file.pdf" target="_blank">预览PDF文件</a>
文件下载
注意,需要指定接口的 responseType: 'blob'
javascript
function downFile (blobs,type,fileName) {
let blob;
if(Array.isArray(blobs)){
blob=blobs;
}else if(blobs.type=='application/json'){
this.$message.error('下载文件失败')
return JSON.parse(blobs);
}else{
blob=[blobs]
}
// 2. 使用Blob对象
const blobData = new Blob(blob, { type });
// 3. 使用URL.createObjectURL()
const downloadUrl = URL.createObjectURL(blobData);
// 4. 触发文件下载
const downloadLink = document.createElement('a');
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
// 释放URL资源
URL.revokeObjectURL(downloadUrl);
}
javascript
//只支持https
async function downFile(blobs, type, fileName) {
// 创建一个新句柄
const options = {
suggestedName: 'aaa.zip', // 使用 .bin 扩展名来表示二进制文件
types: [
{
description: "支持的文件格式",
accept: { "application/zip": [".zip"] },
},
],
}
const newHandle = await window.showSaveFilePicker(options)
// 创建一个 FileSystemWritableFileStream 用于写入
const writableStream = await newHandle.createWritable()
// 按顺序写入每个 blob
let totalWritten = 0
for (let i = 0; i < blobs.length; i++) {
const blob = blobs[i]
await writableStream.write({ type: 'write', position: totalWritten, data: blob })
totalWritten += blob.size
}
// 关闭文件并将内容写入磁盘
await writableStream.close()
}
分片下载
利用了控制请求并发,和文件上传的downFile
javascript
import {downFile,RequestQueue} from '@/utils' //见文件下载,控制请求并发
export class ChunkedDownloader {
constructor({ totalSize, chunkSize = 1024 * 1024,fileName,fileType }) {
this.totalSize = typeof totalSize=='function'?totalSize():totalSize;
this.chunkSize = chunkSize;
this.chunkCount = Math.ceil(totalSize / chunkSize);
this.fileName=fileName;
this.fileType=fileType;
this.mergeChunk = [];
}
setDownHandler(fn){
this.downFile=fn
}
downFile(...args){
downFile(...args)
}
setRequstAfterHandler(fn){
const requstAfterHandler=this.requstAfterHandler;
this.requstAfterHandler=(params)=>{
requstAfterHandler.call(this,params);
fn.call(this,params)
}
}
requstAfterHandler({req:{index,reqSuccessCount},res}){
if(reqSuccessCount<this.chunkCount){
this.mergeChunk[index]=res;
}else{
this.mergeChunk[index]=res;
this.downFile(this.mergeChunk,this.fileType,this.fileName)
}
}
createChunks(requestFunc,requestParams={}) {
try {
// 计算分片数
const { chunkSize,totalSize, chunkCount } = this;
for (let index = 0; index < chunkCount; index++) {
const start = chunkSize * index;
const end = Math.min(chunkSize * (index+1), totalSize);
requestFunc({...requestParams,index,start,end,chunkSize,chunkCount},this.requstAfterHandler.bind(this))
}
} catch (error) {
console.error('Error downloading file:', error);
}
}
beackRequest(){
this.queue?.setHasBreak()
this.queue=null
}
down ({requestAfterHandler, requestFunc,logList,maxConcurrent = 5, maxRetries = 3, ...requestParams }) {
if(RequestQueue){
requestAfterHandler&&this.setRequstAfterHandler(requestAfterHandler);
//提供控制并发功能
const queue = new RequestQueue(maxConcurrent, maxRetries) // 最多同时发送5个请求,最大重试次数为3
queue.setStatus('下载');
logList&&queue.setLogList(logList);
this.queue=queue;
this.createChunks((...args) => queue.enqueue(requestFunc, ...args), requestParams)
}
}
}
示例
javascript
import {ChunkedDownloader,RequestQueue,downFile} from '@/utils/index'
//准备参数
const fileName = `${productName}_${productVersion}.zip`
const fileType = 'application/zip'
const chunkSize = 10 * 1024 * 1024 //5M
const totalSize= 100 * 1024 * 1024 //100M
//请求函数&请求之后做的事
const requestFunc = (...req)=>{}
const requstAfterHandler=function ({req:{total,chunkCount}}){}
//分片下载
const ChunkedDownloaderParams={fileName, fileType, chunkSize, totalSize }
const chunkedDown = new ChunkedDownloader(ChunkedDownloaderParams)
//logList是自定义的日志信息
const requestParams={logList:this.logList,requestFunc,requestAfterHandler,...一些请求参数}
this.chunkedDown=chunkedDown;
chunkedDown.down(requestParams)
this.chunkedDown.brackRequest() //取消请求
权限管理
js
//router.options一般是VueRouter(options)的options
//缓存静态路由
const staticRoutes = router.options.routes;
const addRoutes=(routes=[],parentPath = '')=>{
//还原初始化的静态路由
router.options.routes = staticRoutes;
routes.forEach(item => {
if(item.children){
addRoutes(item.children,route.path + '/');
}else{
item.path=parentPath+item.path
router.addRoute(item);
}
});
}
const initRoutes=(router)=>{
const newRouter = new VueRouter();
router.matcher = newRouter.matcher;
}
//例如现在退出登录
initRoutes(router);
//例如现在登录拿到了动态路由asyncRoutes
addRoutes(asyncRoutes);
//route的来源来自于路由守卫,路由守卫中判断,在跳转登录页时,缓存from路由信息作为route
let route={};
router.beforeEach(form,to,next)=>{
if(to.path==='\login'){
route=form||{};
}
return hasPermission(router,to)?next():next('/login');
}
//登录完毕后如果需要重定向到之前的页面,需要判断是否还存在权限
const hasPermission=(router,route)=>{
const routes=router.matcher.options.routes; //3.0x
//const routes=router.getRoutes() //4.0x
const hasRoute=(item,route)=>{
//路由匹配规则
const matchRule=item.path===route.path||item.name===route.name;
//如果有子路由,则匹配子路由和当前路由,否则只匹配当前路由
return item.children?item.children.find(el=>hasRoute(el,route)||matchRule:matchRule;
}
return routes.find(el=>hasRoute(el,route));
}
hasPermission(router,route);
//可以在路由守卫中通过next跳转,也可以通过router.replace跳转
单位转换
js
function converSize(size, fromUnit, toUnit, decimalPoint = 2){
size&&(!formUnit||!toUnit)?
formatSizeUnits(size)
:convertFileSize(size, fromUnit, toUnit, decimalPoint = 2)
}
/**
* 将文件大小格式化到仅保留两位小数的最大单位
*/
function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;
while (kb >= 1024 && unitIndex < units.length - 1){
kb /= 1024;
unitIndex++;
}
return `${kb.toFixed(2)} ${units[unitIndex]}`;
}
/**
* 将文件大小从一个单位转换为另一个单位。
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
* */
function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);
// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}
// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);
// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}
防止打开控制台
javascript
(function() {
var re = /x/;
var count = 0;
var startTime = performance.now();
console.log(re); // 首次打印
// 设置断点
debugger;
re.toString = function() {
var endTime = performance.now();
// 设置一个阈值,例如100毫秒
if (endTime - startTime > 100)window.location.href = 'about:blank';
};
})();
vue组件
监听元素进入/离开容器内指定可视区
vue
<!--组件封装-->
<template>
<div id="intersection-observer-box" v-bind="$attrs">
<slot /> </div></template>
<script setup>
import { onBeforeUnmount, onMounted } from 'vue'
const props = defineProps({
// 元素是否进入视口
isIntersecting: {
type: Boolean,
default: true
},
// 进入或离开视口的比例
threshold: {
type: Number,
default: 1.0
},
// 默认进入或离开容器触发监听,
// 如果设置了rootMargin,分别对应容器上右下左收缩的比例
// 例如[0,0,0.8,0] 代表仅监听0-20%高度部分
rootMargin: {
type: Array,
default: () => [0, 0, 0, 0]
},
// 需要监听进入/离开容器的元素列表
observers: {
type: Array,
default: () => []
},
// 需要监听进入/离开容器的元素id列表
observerIds: {
type: Array,
default: () => []
}
})
/*
* mutate接受根据isIntersecting判断的进入或离开视口的元素
* getTargets接受根据observerIds获取到的元素组成的{id:target} 隐射对象
* */
const emits = defineEmits(['mutate', 'getTargets'])
let rootElement = null
let observer, resizeObserver
onMounted(() => {
rootElement = document.getElementById('intersection-observer-box')
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentBoxSize) {
// Firefox将' contentBoxSize '实现为单个内容矩形,而不是数组
const contentBoxSize = Array.isArray(entry.contentBoxSize)
? entry.contentBoxSize[0]
: entry.contentBoxSize
const { blockSize, inlineSize } = contentBoxSize
if (observer) observer.disconnect()
const { threshold, isIntersecting, observers, observerIds } = props
let rootMargin = ''
props.rootMargin.forEach((item, index) => {
rootMargin += `${index % 2 ? -inlineSize * item : -blockSize * item}px `
})
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
console.log('entry', entry)
if (entry.isIntersecting == isIntersecting) {
emits('mutate', entry)
}
})
},
{
root: rootElement,
rootMargin,
threshold: [threshold]
}
)
observers.forEach((domItem) => {
observer.observe(domItem)
})
const targets = observerIds.reduce((p, id) => {
p[id] = document.getElementById(id)
return p
}, {})
Object.values(targets).forEach((domItem) => {
observer.observe(domItem)
})
emits('getTargets', targets)
}
}
})
resizeObserver.observe(rootElement, {
box: 'content-box'
})
})
onBeforeUnmount(() => {
resizeObserver.disconnect()
observer.disconnect()
})
</script>
<style scoped lang="scss"></style>
vite插件
使用命名空间后仍使用el前缀的插件
js
-->插件
function changeHtmlClassPrefix(htmlString, oldPrefix, newPrefix) {
const regex = new RegExp(
`(class|style)\\s*:\\s*((["']((${oldPrefix}\\b)-).*["'])|((_normalizeClass|_normalizeStyle)\\(.*(${oldPrefix}\\b)-.*\\)))`,
'g'
)
return htmlString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
function changeSelectorPrefix(cssString, oldPrefix, newPrefix) {
const regex = new RegExp(`(\\.${oldPrefix}\\b|\#${oldPrefix}\\b|\--${oldPrefix}\\b)`, 'g')
return cssString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
export default function addScopedAndReplacePrefixPlugin({ prefixScoped, oldPrefix, newPrefix }) {
return {
name: 'addScopedAndReplacePrefix',
transform(code, id) {
if (!oldPrefix || !newPrefix) return
if (id.includes('node_modules')) return
const cssLangs = ['css', 'scss', 'less', 'stylus', 'styl']
let newCode = code
if (id.endsWith('.vue')) {
newCode = changeHtmlClassPrefix(newCode, oldPrefix, newPrefix)
}
// else if (id.includes('.vue') && id.includes('scoped')) {
else if (cssLangs.some((lang) => id.endsWith(`.${lang}`))) {
if (oldPrefix && newPrefix) {
newCode = changeSelectorPrefix(newCode, oldPrefix, newPrefix)
}
if (prefixScoped) {
newCode = `${newCode}${prefixScoped}{${newCode}}`
}
return newCode
}
return newCode
}
}
}
-->使用插件
import addScopedAndReplacePrefixPlugin from './plugins/addScopedAndReplacePrefix.js'
export default defineConfig((mode)=>{
plugins:[
vue(),
addScopedAndReplacePrefixPlugin({
prefixScoped: `div[data-qiankun='${QIANKUN_APP_NAME}']`,
oldPrefix: 'el',
newPrefix: '命名空间的名字,例如wl'
})
]
...,
})
将element-plus的消息框全局注入的插件
js
-->使用方式
-->1.书写插件
import addElementGlobalMacro from './plugins/addElementGlobalMacro.js'
export default defineConfig((mode)=>{
plugins:[
vue(),
addElementGlobalMacro()
]
...,
})
-->2.全局定义,以防报错
-->3.使用插件
import addElementGlobalMacro from './plugins/addElementGlobalMacro.js'
export default defineConfig((mode)=>{
plugins:[
vue(),
addElementGlobalMacro()
]
...,
})
最全的手写JS面试题
1 compose
function compose(...fn) {
return fn.reduce((pre, cur) => {
return (...x) => pre(cur(...x))
}}
}
3 发布订阅模式
class myEventEmitter {
constructor() {
this.events = {};
}
on(type, handler) {
this.events[type] ? this.events[type].push(handler) : (this.events[type] = [handler]);
}
off(type, handler) {
this.events[type] && this.events[type].filter((cur) => cur === handler);
}
emit(type, ...args) {
this.events[type] &&
this.events[type].forEach((cur) => {
cur.apply(this, args);
});
}
once(type, handler) {
function fn(...args) {
handler.apply(this, args);
this.off(type, handler);
}
this.on(type, fn);
}
}
4 数组去重
function unique(ary) {
return Array.from(new Set(ary));
}
function unique(arr) {
return arr.filter((item, index, array) => {
return array.indexOf(item) === index;
});
}
function unique(ary) {
let i = 0;
for (let j = 1; j < ary.length; j++) {
if (ary[j] !== ary[i]) {
i++;
ary[i] = ary[j];
}
}
ary.length = i + 1;
return ary;
}
5 数组扁平化
function flatten(ary, depth = 1) {
if (depth > 0) {
return ary.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur, depth - 1) : cur);
}, []);
} else {
return ary.slice();
}
}
[1, [2, [3]]];
function flatter(ary) {
while (
ary.some((cur) => {
return Array.isArray(cur);
})
) {
[].concat(...ary);
}
}
6 寄生组合继承
// 寄生组合继承
function Parent(value) {
this.val = value;
}
function Child(value) {
Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true,
},
});
// class继承
class Parent {
constructor(value) {
this.val = value;
}
}
class Child extends Parent {
constructor(value) {
super(value);
this.val = value;
}
}
// 组合继承
function Parent (name) {
this.name = name;
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
调用父级构造函数来继承原型,所以实例原型上出现了重复的父级构造函数的属性。
7 实现有并行限制的 Promise 调度器
8 new 操作符
function myNew(con, ...args) {
const obj = Object.create(con.prototype);
const res = con.apply(obj, ...args);
return res instanceof Object ? res : obj;
}
9 call apply bind
Function.prototype.call = function (thisArg, ...args) {
const fn = Symbol();
thisArg = thisArg || window;
thisArg[fn] = this;
const res = thisArg[fn](...args);
delete thisArg[fn];
return res;
};
Function.prototype.myApply = function (thisArg, ary) {
const fn = Symbol();
thisArg = thisArg || window;
thisArg[fn] = this;
let res;
if (ary) {
res = thisArg[fn](...ary);
} else {
res = thisArg[fn]();
}
delete thisArg[fn];
return res;
};
Function.prototype.myBind = function (thisArg, ...fixedArgs) {
const _this = this;
return function f(...args) {
if (this instanceof f) {
return new _this(...fixedArgs.concat(args));
} else {
return _this.apply(thisArg, args.concat(fixedArgs));
}
};
};
Function.prototype.myCall = function (context, ...args) {
const fn = Symbol();
this.fn = context;
}
10 深拷贝(考虑到复制 Symbol 类型)
function cloneDeep(obj, map = new Map()) {
if (!(obj instanceof Object)) return obj;
// 循环引用,就是指向相同的内存地址,也就是同一个obj(循环引用)
if (map.has(obj)) return map.get(obj);
const res = obj instanceof Object ? {} : [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
res[key] = deepClone(obj[key], map.set(obj, res));
}
}
return res;
}
11 instanceof
// 判断对象的原型链上是否存在构造函数的prototype
function myInstanceof(left, right) {
while (true) {
if (left === null) return false;
if (left.__proto__ === right.prototype) return true;
left = left.__proto__;
}
}
12 柯里化
19 实现 LazyMan
class _LazyMan {
constructor(name) {
this.tasks = [];
const task = () => {
console.log(`Hi! This is ${name}`);
this.next();
};
this.tasks.push(task);
// 把所有任务加进队列后执行一次next运行代码
setTimeout(() => {
this.next();
}, 0);
}
next() {
const task = this.tasks.shift(); // 取第一个任务执行
task && task();
}
sleep(time) {
this._sleepWrapper(time, false);
return this; // 链式调用
}
sleepFirst(time) {
this._sleepWrapper(time, true);
return this;
}
_sleepWrapper(time, first) {
const task = () => {
setTimeout(() => {
console.log(`Wake up after ${time}`);
this.next();
}, time * 1000);
};
if (first) {
this.tasks.unshift(task); // 放到任务队列顶部
} else {
this.tasks.push(task); // 放到任务队列尾部
}
}
eat(name) {
const task = () => {
console.log(`Eat ${name}`);
this.next();
};
this.tasks.push(task);
return this;
}
}
function LazyMan(name) {
return new _LazyMan(name);
}
20 防抖节流
js
// 防抖,连续点击只触发一次
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 1000)
);
//节流,一段时间只触发一次
function throttle(fn, delay) {
let _this, _args;
let flag = false;
return function(...args) {
_this = this;
_args = args;
if (flag) {
return;
}
flag = true;
setTimeout(() => {
fn.apply(_this, _args);
flag = false;
}, time)
}
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
23 Promise 以及相关方法的实现
Promise.prototype.resolve2 = function (value) {
return new Promise((resolve) => {
resolve(value);
});
};
Promise.prototype.reject2 = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
};
//! 等待所有都完成(或第一个失败)。
//! 返回promise;
Promise.prototype.all = function (promises) {
return new promises((resolve, reject) => {
// ==========================
// 异步或同步操作代码;
let result = [];
let resolved = 0;
if (promises.length) {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(val) => {
result[i] = val;
resolved++;
if (resolved === promises.length) {
resolve(result); //异步操作执行成功后的回调函数
}
},
(err) => {
reject(err); //异步操作执行失败后的回调函数
}
);
// ==========================
}
} else {
resolve([]);
}
});
};
Promise.all = function (ps) {
return new Promise((resolve, reject) => {
const res = [];
if (ps.length === 0) resolve(res);
for (let i = 0; i < ps.length; i++) {
Promise.resolve(ps[i]).then(response => {
res[i] = response;
if (res.length === ps.length) {
resolve(response);
}
}, error => {
reject(error);
})
}
})
}
//! 等待所有都失败(或第一个成功)。
//! 返回promise;
Promise.any = function (promises) {
return new Promise((resolve, reject) => {
// ==========================
// 异步或同步操作代码;
let result = [];
let rejected = 0;
if (promise.length === 0) {
resolve([]);
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(value) => {
resolve(value);
},
(reason) => {
result[i] = reason;
rejected++;
if (rejected === promises.length) {
reject(new AggregateError(result));
}
}
);
// ==========================
}
});
};
Promise.allSettled = function (promises) {
return new Promise((resolve) => {
let result = [];
let settled = 0;
if (promises.length === 0) {
resolve([]);
}
for (let i = 0; i < promises.length; i++) {
promises[i].then(
(val) => {
result[i] = {
value,
status: 'fulfilled',
};
settled++;
if (settled == promises.length) {
resolve(result);
}
},
(reason) => {
result[i] = {
reason,
status: 'rejected',
};
settled++;
if (settled == promises.length) {
resolve(result);
}
}
);
}
});
};
//! 最先执行完的运行resolve or reject;
Promise.prototype.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
}
for (let promise of promises) {
promise.then(resolve, reject);
}
});
};
//! onFinally成功 穿透
//! onFinally失败 失败抛出自己的reason
Promise.prototype.finally = function (onFinally) {
return this.then(
//! onFinally()
//! 穿透
(value) => Promise.resolve(onFinally()).then(() => value),
(reason) =>
Promise.resolve(onFinally()).then(() => {
throw reason;
})
);
};
24 实现一个 add 方法
function add(...args) {
var f = add.bind(null /*this不用绑定*/, ...args);
f.toString = () => {
return args.reduce((a, b) => a + b);
};
return f;
}
add(1)(2)(3) + 1;
// 来自 <
30 分片思想解决大数据量渲染问题
33 实现一个对象的 flatten 方法
const obj = {
a: {
b: 1,
c: 2,
d: {e: 5}
},
b: [1, 3, {a: 2, b: 3}],
c: 3
}
function flattenObj(obj) {
const res = {};
for (let key in obj) {
dfs(res, obj[key], `${key}`)
}
return res;
function dfs(res, obj, prefix) {
if (Array.isArray(obj)) {
obj.forEach((cur, idx) => {
dfs(res, cur, prefix + '[' + idx +']')
})
} else if (typeof obj === 'object') {
for (let [key, val] of Object.entries(obj)) {
dfs(res, val, prefix + '.' + key)
}
} else {
res[prefix] = obj;
}
}
}
let newObj = flattenObj(obj)
34 列表转成树形结构
let a = [
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
]
function listToTree(list) {
const temp = {};
const res = [];
for (let val of list) {
temp[val.id] = val;
}
for (const [key, val] of Object.entries(temp)) {
if (val.parentId !== 0) {
!temp[val.parentId].children && (temp[val.parentId].children = []);
temp[val.parentId].children.push(val);
} else {
res.push(val);
}
}
return res;
}
let b = listToTree(a);
Reduce
Object.prototype.myReduce = function (f, initial) {
let total = initial || this[0];
for (let i = initial ? 0 : 1; i < this.length; i++) {
total = f(total, this[i], i, this);
}
return total;
};
冒泡排序
冒泡排序代码实现
js
var a = [36,26,27,2,4,19,50,48];
//var temp;
for(var i= 0; i < a.length; i++)
for(var j = 0; j < a.length-i; j++){
if(a[j] > a[j+1]){
[a[j],a[j+1]] = [a[j+1],a[j]]
//temp = a[j];
//a[j] = a[j+1];
//a[j+1] = temp;
}
}
}
console.log(a);
冒泡排序原理
两相邻的数依次比较
若从小到大排列两两比较时前一个数比后一个数大互换位置
相互比较完一轮最大的数就会到最后面,并且不再参与比较
循环比较 直到比较完成
冒泡排序步骤
- 确定外层循环次数 数组的长度
- 确定内层循环的次数 每确定冒泡一个数内层循环减少一次 数组长度 减 外层循环的index
- 相邻两数相比较 前一个数比后一个数大 互换两数位置
选择排序
选择排序代码实现
js
var a = [4,12,13,4,3,42,33,4,43,44];
//var temp;
var minIndex;
for(var i = 0; i < a.length; i ++){
minIndex = i;
for(var j = i + 1; j < a.length; j++){
if(a[minIndex] > a[j]) minIndex = j;
}
[a[minIndex] , a[i]]= [a[i] , a[minIndex]]
//temp = a[minIndex];
//a[minIndex] = a[i];
//a[i] = temp;
}
console.log(a);
选择排序原理
找未排序的元素中最小的数
将最小数与起始位置互换
直到排序完成
选择排序步骤
- 确定外层循环次数 index为每一次寻找最小值的起始位置 直到数组长度
- 内层循环每次都是由起始位置 +1 直到数组长度
- 假设每一次的起始位置为最小数 碰到更小的用更小的和后面的数继续做比较
- 保存最小数的索引
- 内层循环结束后最小数与起始位置互换
快排
js
let arr = [1, 5, 3, 7, 6, 8, 12, 0];
function quick(ary) {
//1.判断传入的数组长度,如果是一个就直接返回
if (ary.length <= 1)return ary;
//2.如果长度不为1,那么就取数组的中间值
let contentIndex = Math.floor(ary.length / 2);
let contentValue = ary.splice(contentIndex, 1)[0];
//3.先定义左右两个数组,然后让数组中剩余的数与中间数进行比较,比中间数小的放到左边的数组,比中间数大的放到右边的数组。
let leftArr = [],rightArr = [];
for (let item of arr) {
item > contentValue ? rightArr.push(item) : leftArr.push(item);
}
//4.使用递归的方式让左右两边的数组持续这样处理,直至左右两边的数组都排好序,最后三者进行拼接
return quick(leftArr).concat([contentValue], quick(rightArr));
}
arr = quick(arr);
console.log(arr);
小驼峰
js
/**
* 说明:
* 写个转换函数,把一个JSON对象的key从下划线形式(Pascal)转换到小驼峰形式(Camel)
* 示例:
* converter({"a_bc_def": 1}); // 返回 {"aBcDef": 1}
*/
function converter(inputObject) {
if (typeof inputObject !== 'object' || inputObject === null) {
return inputObject;
}
const result = {};
Object.keys(inputObject).forEach((key) => {
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
result[camelKey] = converter(inputObject[key]);
});
return result;
}
const input = { "a_bc_def": 1 };
const output = converter(input);
console.log(output); // 输出 { "aBcDef": 1 }
function converter(obj) {
const getCamelKey=(key)=>key.split('_').reduce((p,c)=>p+`${c[0].toUpperCase()}${c.slice(1,c.length)}`,'');
const entriesObj=Object.entries(obj);
entriesObj.forEach(el=>el[0]=getCamelKey(el[0]))
return Object.fromEntries(entriesObj);
}
console.log(converter({"a_bc_def": 1}));
add-柯里化
js
function add(...rest) {
let res=0;
const fn=(...newRest)=>{
//没有参数,返回结果
if(!newRest.length)return res;
//有参数,返回函数
res=newRest.reduce((p,c)=>p+c,res);
return fn;
};
fn.valueOf,fn.toString=()=> res;
return fn(...rest);
}
console.log(add(1)(2)(3)());
console.log(add(1)(2)(3)+'');
console.log(add(1)(2)(3)==6);
手写flat
js
//不使用数组的 arr.flat() API,自己实现一个数组拍平函数,需要支持任意层级
function flat(list, depth=1,arr=[]) {
if(!Array.isArray(list)||!depth){
arr.push(list);
return;
}
for(let item of list){
flat(item,depth-1,arr);
}
return arr;
}
//GPT
function flattenArray(arr, depth = 1) {
if (depth === 0) {
return arr;
}
const result = [];
arr.forEach((item) => {
if (Array.isArray(item)) {
result.push(...flattenArray(item, depth - 1));
} else {
result.push(item);
}
});
return result;
}
const nestedArray = [1, [2, [3, 4, [5, 6]], 7], 8];
const flatArray = flattenArray(nestedArray, 2);
console.log(flatArray); // 输出 [1, 2, 3, 4, 5, 6, 7, 8]
循环依赖
js
//实现一个方法,检查一个 npm 包的依赖项中有没有存在循环依赖。
//不用考虑版本,只考虑包名即可
//入参 pkgs 为项目中所有的依赖(包括子依赖)
//返回 boolean
//pkg 数据结构即为 package.json 内容的数组
function hasCircularDependencies(pkgs) {
const visited = new Set();
const inProgress = new Set();
function hasCycle(pkg) {
if (inProgress.has(pkg.name)) {
return true; // 循环依赖
}
if (!visited.has(pkg.name)) {
inProgress.add(pkg.name);
if (pkg.dependencies) {
for (const depName in pkg.dependencies) {
const dep = pkgs.find((p) => p.name === depName);
if (dep && hasCycle(dep)) {
return true;
}
}
}
inProgress.delete(pkg.name);
visited.add(pkg.name);
}
return false;
}
for (const pkg of pkgs) {
if (hasCycle(pkg)) {
return true;
}
}
return false;
}
const packages = [
{
name: "a",
dependencies: {
b: "^1.0.0",
},
},
{
name: "b",
dependencies: {
c: "^1.0.0",
},
},
{
name: "c",
dependencies: {
a: "^1.0.0",
},
},
];
const hasCircular = hasCircularDependencies(packages);
console.log(hasCircular); // 输出 true(存在循环依赖)
uuid
javascript
uuid(len = 16, radix = 16) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [],
i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
},
deepClone
javascript
function deepClone(obj, Attr = null, replaceAttr = null, hash = new WeakMap()) {
// 对于非对象或数组类型,直接返回
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 如果obj是日期或正则对象等特殊对象,则直接返回新对象
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 如果hash中存在该对象,则直接返回,避免循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 创建一个新的对象或数组
let clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
// 遍历对象的属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 否则,递归复制属性值
clone[key] = deepClone(obj[key], Attr, replaceAttr, hash);
}
}
if (!clone[replaceAttr]) clone[replaceAttr] = clone[Attr]; // 替换为新的值
// 返回克隆的对象或数组
return clone;
}
aysnc/await
原理是通过 generator 和 promise 实现的
js
// 以下是generator+promise实现async/await
// yield相当于await,generator相当于async
function * generator () {
// next函数的参数会作为yield表达式的值,即下示fn函数中g.next接收的参数会作为request的参数
let num1 = yield request(1)
let num2 = yield request(num1)
let num3 = yield request(num2)
let num4 = yield request(num3)
let num5 = yield request(num4)
console.log(num1, num2, num3, num4, num5) //2,4,8,16,32
return 123;
}
/**
* @description - 封装co函数,用于接收generator函数,并自动执行generator
* @param generator
*/
function co (generator) {
if(Object.prototype.toString.call(generator)!=='GeneratorFunction'){
return new Error('不是generator函数');
}
const g = generator()
function fn (val) {
const next = g.next(val);
console.log(next.done?next.value:'')
// next.done则调用结束,next.value为generator返回值,直接返回即可
// 若!next.done,则需将promise的值在then中传递给下一个yield表达式
return next.done?next.value:Promise.resolve(next.value).then(fn);
}
fn()
}
co(generator)
三角形与矩阵生成
javascript
//目前只支持,正三角形元素齐全的时候准确排列,和矩形正常排列
fn(nodeCount) {
function calculateTrianglePositions(nodes, nodeWidth, nodeHeight, containerWidth, center) {
const positions = [];
const centerX = containerWidth / 2; // 容器中心x坐标
//偶数逻辑
if (nodes % 2 === 0) {
let cols = Math.ceil(Math.sqrt(nodes)); //每行个数
let level = 1; // 当前层数,从1开始
let xOffset = centerX - Math.floor(cols / 2) * nodeWidth; // 当前层的x坐标偏移量
let yOffset = 0; // 当前层的y坐标偏移量
let currentIndex = 0; //当前层的第几个元素
for (let i = 0; i < nodes; i++) {
// 如果当前节点数超过了当前层的节点数,则进入下一层
if (currentIndex === cols) {
// eslint-disable-next-line no-unused-vars
level++; // 层数递增
yOffset += 10;
currentIndex = 0;
}
// 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
const x = xOffset + currentIndex * nodeWidth;
// 计算当前节点的y坐标(基于层数和节点高度)
const y = yOffset; // 等边三角形排列的y坐标
currentIndex++;
positions.push({ x, y });
}
} else if (center) {
//奇数且居中逻辑
let level = 1; // 当前层数,从1开始
let xOffset = 0; // 当前层开始排列的第一个元素相对于中间的偏移量
let yOffset = 0; // 当前层的y坐标偏移量
let currentIndex = 0; //当前层的第几个元素
let leftCurrentIndex = 0; //当前层左边添加的几个元素
let rigthCurrentIndex = 0; //当前层右边添加的第几个元素
let isOdd = true; //是否是奇数行且为第一个元素
let isRight = false; //当前是否是在添加右边
for (let i = 0; i < nodes; i++) {
// 如果当前节点数超过了当前层的节点数,则进入下一层
if (currentIndex === level) {
currentIndex = 0;
level++; // 层数递增
isOdd = level % 2 === 1;
// console.log('奇数', level % 2);
xOffset = isOdd ? 10 : 5; // 更新x坐标偏移量
yOffset += 10;
leftCurrentIndex = 0;
rigthCurrentIndex = 0;
// if(overCount<level){
//
// }
}
let x;
//天衍四十九,遁去其一,这便是那遁去的一,道爷,你悟了吗
if (isOdd) {
// 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
x = centerX;
isOdd = false;
// console.log('isOdd', x);
} else {
if (isRight) {
x = centerX + xOffset + rigthCurrentIndex * nodeWidth;
rigthCurrentIndex++;
// console.log('isRight', x);
} else {
x = centerX - xOffset - leftCurrentIndex * nodeWidth;
leftCurrentIndex++;
// console.log('isLeft', x, xOffset, leftCurrentIndex);
}
isRight = !isRight;
}
currentIndex++;
// 计算当前节点的y坐标(基于层数和节点高度)
const y = yOffset; // 三角形排列的y坐标
positions.push({ x, y });
}
} else {
//奇数逻辑
let level = 1; // 当前层数,从1开始
let xOffset = 0; // 当前层的x坐标偏移量
let yOffset = 0; // 当前层的y坐标偏移量
let currentIndex = 0; //当前层的第几个元素
for (let i = 0; i < nodes; i++) {
// 如果当前节点数超过了当前层的节点数,则进入下一层
if (currentIndex === level) {
level++; // 层数递增
xOffset -= 5; // 更新x坐标偏移量
yOffset += 10;
currentIndex = 0;
// if(overCount<level){
//
// }
}
currentIndex++;
// 计算当前节点的x坐标(基于偏移量和节点在当前层的位置)
const x = centerX + xOffset + currentIndex * nodeWidth;
// 计算当前节点的y坐标(基于层数和节点高度)
const y = yOffset; // 等边三角形排列的y坐标
positions.push({ x, y });
}
}
return positions;
}
// 示例
// const nodeCount = 7; // 节点个数
const nodeWidth = 10; // 节点宽度
const nodeHeight = 10; // 节点高度
const containerWidth = 100; // 容器宽度
// 打印结果
// positions.forEach((pos, index) => {
// console.log(`Node ${index + 1}: (${pos.x}, ${pos.y})`);
// });
return calculateTrianglePositions(nodeCount, nodeWidth, nodeHeight, containerWidth, true);
},