Skip to content

代码实现

🕒 Published at:

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);
}

fileStream

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);

冒泡排序原理

  • 两相邻的数依次比较

  • 若从小到大排列两两比较时前一个数比后一个数大互换位置

  • 相互比较完一轮最大的数就会到最后面,并且不再参与比较

  • 循环比较 直到比较完成

冒泡排序步骤

  1. 确定外层循环次数 数组的长度
  2. 确定内层循环的次数 每确定冒泡一个数内层循环减少一次 数组长度 减 外层循环的index
  3. 相邻两数相比较 前一个数比后一个数大 互换两数位置

选择排序

选择排序代码实现

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);

选择排序原理

  • 找未排序的元素中最小的数

  • 将最小数与起始位置互换

  • 直到排序完成

选择排序步骤

  1. 确定外层循环次数 index为每一次寻找最小值的起始位置 直到数组长度
  2. 内层循环每次都是由起始位置 +1 直到数组长度
  3. 假设每一次的起始位置为最小数 碰到更小的用更小的和后面的数继续做比较
  4. 保存最小数的索引
  5. 内层循环结束后最小数与起始位置互换

快排

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);
},