引子
我们在做表单提交的事件中,经常会遇到多次提交的情况。如果后端没有对数据的唯一性进行处理,那么数据库中将会出现多条重复数据,这是不合理的。这是因为,用户操作过频(连续高频点击按钮提交),而未对操作按钮做限制导致的。之前,我都是对提交按钮添加loading,然后请求结束后,将loading去除。现在,可以尝试给该元素添加 data- 自定义属性,通过事件冒泡,然后全局监听 click事件,找到该元素,判断该元素是否有 dataset 唯一key,有则存下,无则向上查找,target.parentElement。设置默认节流控制时间。
以下完整代码:
const throttleCache = new Map<string, number>();
// 如果元素 data-throttle 属性,对该元素做全局点击事件截流控制,默认3s控制,可以通过 throttle-timeout 修改时间间隔
document.body.addEventListener('click', (e: MouseEvent) => {
const target = e.target as HTMLElement;
const currTarget = target.dataset.throttle ? target : target.parentElement;
const throttleKey = currTarget?.dataset.throttle;
const timeout = currTarget?.dataset.throttleTimeout || 3000;
if (!throttleKey) return;
if (throttleCache.has(throttleKey)) {
// eslint-disable-next-line no-alert
alert('操作过于频繁');
e.stopPropagation();
} else {
const canncel = setTimeout(() => {
throttleCache.delete(throttleKey);
}, +timeout);
throttleCache.set(throttleKey, +canncel); }});
节流
概念:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。高频事件触发,但在n秒内,只会执行一次。所以节流会稀释函数的执行频率。
当我们在设置时间内,连续多次触发提交事件,那么就会提示我们操作过频。像持续触发scroll事件。节流函数的实现,一般有两种,时间戳和定时器;结合scroll事件,下面来简单实现下节流函数。
// 立即执行 一段时间时间内只执行一次
const throttle1 = (fn,wait) => {
let preTimer = 0;
return function () {
let curTimer = +new Date();
if(curTimer - preTimer > wait){
preTimer = curTimer;
fn.apply(this,arguments);
}
}
}
// 延时执行 一段时间内只执行一次
const throttle2 = (fn,wait) => {
let timer = null;
return function () {
if(!timer){
timer = setTimeout(() => {
fn.apply(this,arguments);
timer = null;
},wait)
}
}
}
// 延时执行
const throttle3 = (fn,wait) => {
let timer = null;
let preTimer = Date.now();
return function () {
let curTimer = Date.now();
if(curTimer - preTimer >= wait){
preTimer = Date.now();
timer = setTimeout(() => {
fn.apply(this,arguments);
timer = null;
},wait)
}
}
}
const throttle4 = (fn,wait) => {
let flag = true;
return function(){
if(!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this,arguments);
flag = true;
},wait)
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
防抖:
概念:一段时间时间内,连续触发的高频事件,只在最后触发一次。触发高频事件后n秒后函数只会执行一次,如果n秒内,事件再次触发,则重新计算时间。
const debounce = (fn,wait) => {
let timer = null;
return function () {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this,arguments);
},wait);
}
}
const handleDebounce = debounce(() => console.log('防抖----'),2000);
timer = null VS clearInterval(timer)
1、clearInterval(timer) 将定时器暂停,但是timer变量本身仍然存在.。达到保留对象的目的,以便再次使用
2、两个都能达到关闭定时器的目的。但是当timer = null后,timer会被系统的垃圾回收机制回收, 无法再重新启动定时器
3、用场:关闭定时器使用clearInterval(timer); 如果需要判断定时器是否存在而进行的一些操作,在清空定时器后需要使用timer=null
call & apply & bind
均是改变this指向,稍稍不同。
call(obj, param1,param2)
apply(obj, [param1,param2])
bind() 返回一个新函数,不会自动执行,需要手动执行
Function.prototypr.zyCall = function(context,...args){
// 1、将方法挂在到传入的context;
// 2、将挂载以后的方法执行,改变this指向;
// 3、删除副作用 即 添加的属性fn删除;
context.fn = this; // 这里的this就是被执行的方法;直接绑定到context这个上下文中去
const result = context.fn(args);
delete context.fn; // 消除副作用
return result;
}
Function.prototype.zyApply = function(context,args=[]){
// return this.zyCall(context,...args);
// 或者
context.fn = this;
const result = context.fn(..args);
delete context.fn;
return result;
}
Function.prototype.zyBind = function(context,...args){
// const fn = this;
// return function(){
// return fn.zyApply(context,[...args,...arguments]);
// }
// 或者
return (...args2) => {
context.fn = this;
context.fn([...args,...args2])
delete context.fn;
}
}
接下来,咱们试一下
function show(...args){
console.log(args);
console.log(`当前对象:${this.name}`);
}
show.zyCall({name:'shiyia-myCall'},'fff','ggg');
show.zyApply({name:'shiyia-myApplay'},['fff22','ggg222']);
show.zyBind({name: 'shiyia-myBind'},'ddddd','ddd333')();
new
new 发生了什么?
1、创建一个空对象
2、将空对象的原型指向构造函数的原型
3、将空对象作为构造函数的上下文(改变this指向)
4、对构造函数有返回值的处理
function zyNew (fn,...args) {
const obj = {}; // const obj = Object.create({});
Object.setPrototypeOf(obj,fn.prototype);
let result = fn.apply(obj,args);
return result instanceof Object ? result : obj;
}
测试一下
function Fun (name,age) {
this.name = name;
this.age = age;
}
console.log(zyNew(Fun, '张三',18));
为什么要 return result instanceof Object ? result : obj 呢?
改一下 函数Fun 若被实例化函数有返回值 且返回值是引用类型 再看看
function Fun1 (name,age) {
this.name = name;
this.age = age;
return {name: '引用类型!!'}
}
这时候再打印
console.log(zyNew(Fun1,'张三',18));
控制台里输出一下 自己看看
instanceof
用于检测构造函数的propotype属性是否出现在某个实例对象的原型链上
判断左侧对象是否是右侧构造函数的实例化对象
function zyInstanceof (left,right) {
// 1、首先获取实例对象的__proto__(原型)
// 2、其次获取构造函数的prototype(原型)
// 3、循环判断实例对象的原型是否等于构造函数的原型,直到对象原型最终为null(原型链的最终为null)。
let proto = Object.getPrototype(left);
let prototype = right.prototype;
while(true){
if(!proto) return false;
if(proto === prototype) return true;
}
}
sleep 函数
// sleep 函数
function sleep (wait) {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve()
},wait)
})}
// sleep(1000).then(() => console.log('ddddd'))
某公司 1 到 12 月份的销售额存在一个对象里面
如下:{1:222, 2:123, 5:888},
请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]
var obj = {1:222, 2:123, 5:888};
function(obj){
return Array.from({length: 12}).map((_,index) => obj[index] || null).slice(1);
};
数组转树型结构
将以下数组数组转为树
let arr = [
{id: 1, bm: '一级部门1', parentId: 0},
{id: 2, bm: '一级部门2', parentId: 0},
{id: 3, bm: '二级部门1', parentId: 1},
{id: 4, bm: '三级部门1', parentId: 3},
{id: 5, bm: '二级部门2', parentId: 2},
{id: 6, bm: '三级部门2', parentId: 3},
{id: 7, bm: '四级部门1', parentId: 4}
];
方法一:
一次for循环,中间用对象存下
const toTree = (arr) => {
if(!Array.isArray(arr)) return;
const result = []; // 存放结果数组
const obj = {};
for(let i=0;i<arr.length;i++){
let item = arr[i];
obj[item.id] = item;
let parentItem = obj[item.parentId];
if(parentItem){
(parentItem.children || (parentItem.children = [])).push(item)
}else{
result.push(item);
}
}
return result;
}
// 测试一下:
console.log(toTree(arr));
方法二:
递归
const toTree2 = (arr,pid) => {
if(!Array.isArray(arr)) return;
const result = [];
for(let i=0;i<arr.length;i++){
let item = arr[i];
if(item.parentId === pid){
let childrenItem = toTree2(arr,item.id);
if(childrenItem && childrenItem.length){
item.children = childrenItem;
}
result.push(item);
}
}
return result;
}
console.log(toTree2(arr,0));
多维数组转树
[1,2,3,[4,5,[6,7]]]
转为
[
{value: 1},
{value: 2},
{value: 3},
{
children: [
{value: 4},
{value: 5},
{
children: [
{value: 6},
{value: 7}
]
}
]
}
]
方法一:
const convert1 = (arr) => {
const result = [];
for(let i=0;i<arr.length;i++){
if(typeof arr[i] === 'number' ){
result.push({
value: arr[i]
});
}else if(Array.isArray(arr[i])){
result.push({
children: convert1(arr[i])
})
}
}
return result;
}
方法二:
const convert2 = (item) => {
if(typeof item === 'number'){
return {value: item}
}else if(Array.isArray(item)){
return {
children: item.map(vm => convert2(vm))
}
}
}
promise.all & promise.race
promise.all
-
需等所有均fulfilled 返回 p1,p2,p3返回值组成的数组,传递给p的回调函数;
-
一旦有一个 rejected,p的状态就变为 rejected ,此时第一个被rejected的实例的返回值,会传递给p的回调函数;
-
promise.all([p1,p2,p3]), 如果作为参数的 promise实例,自己定义了 catch 方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法
-
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法
const p1 = new Promise((resolve,reject) => { resolve('p1-----success~~'); }).then(result => result).catch(e => e);
const p2 = new Promise((resolve,reject) => { reject('p2------error!!') }).then(result => result).catch(e => e);
const p3 = new Promise((resolve,reject) => { resolve('p3-----success~~'); }).then(result => result).catch(e => e);
Promise.all([p1,p2,p3]).then(result => { console.log(result) }).catch(e => { console.log(e,'all--error!'); })
// 将会打印 ['p1-----success~~','p2------error!!','p3-----success~~']
p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
// 手写咯,考试要考啦~~~
const zyPromiseAll = (iterator) => {
const promises = Array.from(iterator);
let len = promises.length;
let index = 0;
let data = [];
return new Promise((resolve,reject) => {
for(let i in promises){
promises[i].then((res) => {
data[i] = res;
if(++index === len){
resolve(data);
}
}).catch(err => {
reject(err);
});
}
});
}
用上面的例子测试一下
zyPromiseAll([p1,p2,p3]).then(result => {
console.log(result)
}).catch(e => {
console.log(e,'all--error!');
});
promise.race
Promise.race([p1,p2,p3]) 有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 promise实例的返回值,就传递给p的回调函数。
const zyPromiseRace = (iterator) => {
const promises = Array.from(iterator);
const result = [];
const len = promises.length;
for(let i in promises){
return new Promise((resolve,reject) => {
Promise.resolve(promises[i]).then(data => {
resolve(data);
},err => {
reject(err);
})
})
}
}
zyPromiseRace([p1,p2,p3]).then(result => {
console.log(result)
}).catch(e => {
console.log(e,'all--error!');
});
// 或者
Promise._race = promises => new Promise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve, reject)
})
})Promise._race([p1,p2,p3])
Promise.finally
Promise.prototype.finaly = function(cb){
let p = this.constructor;
return this.then(value => p.resolve(cb())
.then(() => value),reason => p.resolve(cb())
.then(() => {
throw reason;
}))
}
字符串中连续重复次数最多的字符
双指针!!
// 找出字符串中重复最多的字符
let str = 'aaaaabbbccccccaaaddddddddddd';
const filterStr = (str) => {
// 指针
let i = 0;
let j = 1;
let maxRepeatCount = 0;
let maxRepeatChar = '';
while(i<str.length){ // 当i还在范围内的时候,应该继续寻找
if(str[i] !== str[j]){
console.log(`此时${i}和${j}之间连续相同,都为${str[i]};它重复了${j-i}次!`)
if(j - i > maxRepeatCount){
// 如果当前文字重复次数(j-i)超过此时的最大值
// 就让它成为最大值
maxRepeatCount = j - 1;
maxRepeatChar = str[i];
}
i = j;
}
j++;
}
return [maxRepeatCount,maxRepeatChar];
}
console.log(filterStr2(str));
两个数组求交集
[1,2,3,4,5,6,7];
[2,3,5,8,1,9,30];
求交
const intersect = (arr1,arr2) => {
const result = [];
const map = new Map();
for(let i=0;i<arr1.length;i++){
if(map.has(arr1[i])){
let count = map.get(arr1[i]);
let nextCount = count + 1;
map.set(arr1[i],nextCount);
}else{
map.set(arr1[i],1);
}
}
for(let j=0;j<arr2.length;j++){
if(map.has(arr2[j]) && map.get(arr2[j]) > 0){
result.push(arr2[j]);
let _count = map.get(arr2[j]) - 1;
map.set(arr2[j],_count);
}
}
return result;
}
console.log(intersect([1,2,3,4,5,6,7],[2,3,5,8,1,9,30]));
旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中k 是非负数。
示例 1:
输入: [1, 2, 3, 4, 5, 6, 7] 和 k = 3 输出: [5, 6, 7, 1, 2, 3, 4]
解释: 向右旋转1 步:[7, 1, 2, 3, 4, 5, 6]
向右旋转 2 步: [6, 7, 1, 2, 3, 4, 5]
向右旋转3 步: [5, 6, 7, 1, 2, 3, 4]
示例 2:
输入: [-1, -100, 3, 99] 和 k = 2 输出: [3, 99, -1, -100]
解释: 向右旋转1 步: [99, -1, -100, 3]
向右旋转 2 步: [3, 99, -1, -100]
function rotate(arr, k) {
const len = arr.length
const step = k % len
return arr.slice(-step).concat(arr.slice(0, len - step))
}
斐波那契数列
const fib1 = (n) => { return (n===0 || n===1) ? 1 : fib1(n-1) + fib1(n-2);}
因为递归后面的值可能已经被计算过,所以可以加入缓存,优化一下这个递归。如下:
let cache = {}; // 缓存已计算过的值,下次可直接读取,不必重复计算
const fib2 = (n) => {
if(cache.hasOwnProperty(n)){
return cache[n];
}
let v = (n === 0 || n === 1) ? 1 : fib2(n-1) + fib2(n-2);
cache[n] = v; // 写入缓存
return v;
};
测试一下
for(let i=0;i<9;i++){
console.log(fib2(i));
}
数组去重
// 给定数组
// 将数组扁平化 & 去重 & 升序
const arr = [[1,2,3],[3,4,5,5],[6,7,8,9,[11,12,[12,13,[14]]]],10];
const filterArr = (arr) => {
if(!arr || !arr.length) return;
const newArr = Array.from(new Set(arr.flat(Infinity)));
return newArr.sort((x,y) => x - y);
}
不是常规的排序!
// let arr = ['A', 'C4', 'BC1', 'E0', 'D111B', 'BA10', 'CF', 'D11C12B', 'D11C12A', 'E', 'D11B', 'D8A', 'D40', 'E20', 'B9', 'E5'];
// 输入: arr = ['D12','D12A','B','BX','B1','B2','D12B','C90','C100','B0']
// 输出: ['B','B0','B1','B2','BX','C90','C100','D12','D12A','D12B']
let arr = ['D12','D12A','B','BX','B1','B2','D12B','C90','C100','B0'];
arr.sort((a, b) => a.localeCompare(b))
console.log(arr.slice(0));
let newArr = [];
let max = 0
const regA = /([A-Z]+)|([0-9])*/g;
arr.forEach((item, index) => {
let a = item.match(regA)
a.pop()
if (a.length > max) {
max = a.length
}
newArr.push(a)
})
console.log(newArr.slice(0));
for(let i = 1; i < max; i++) {
newArr.sort((a, b) => {
if (a[i - 1] !== b[i - 1] || !a[i] || !b[i]) return false;
if (i % 2 === 1) {
return a[i] - b[i]
} else {
return a[i].localeCompare(b[i]) // [[b, 12]] [[b, 3], [c, 10]]
}
})
}
newArr = newArr.map(a => a.join(''));
console.log(newArr);
两数之和
题目来源:leetcode-cn.com/problems/tw…
// 暴力枚举
function toSum(arr,target){
if(!arr || arr.length < 2) return;
const result = [];
for(let i=0;i<arr.length;i++){
for(let j=i+1;j<arr.length;j++){
if(arr[i] + arr[j] === target){
result.push(i,j);
}
}
}
return result;
}
// hashMap
function toSum (arr,target) {
let map = new Map();
for(let i=0;i<arr.length;i++){
let restItem = target - arr[i];
if(map.has(restItem)){
return [map.get(restItem),i];
}else{
map.set(arr[i],i);
}
}
return [];
}
打印 1 - 10000 之间的所有对称数
例如: 11、22、121、1221 ...
[...Array(1000).keys()].filter(x => {
return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join(''))
})
实现 add(1)(2)(1,2,3,4)() ===> 13
柯理化
function add () {
var _args = Array.prototype.slice.call(arguments,0);
var _adder = function(){
_args.push(...arguments);
return _adder;
}
_adder.toString = function(){
return _args.reduce((x,y) => x + y );
}
return _adder;
}
console.log(add(1,2,3,4)(1).toString())
深拷贝
function deepClone(obj,hash = new WeakMap()){
if(obj === null){
return null;
}
if(obj === undefined){
return undefined
}
if(obj instanceof Date){
return Date;
}
if(obj instanceof RegExp){
return RegExp;
}
if(typeof obj !== 'object'){
return obj;
}
if(hash.has(obj)){
return hash.get(obj);
}
let resultObj = Array.isArray(obj) ? [] : {};
hash.set(obj,resultObj);
Reflect.ownKeys(obj).forEach(el => {
resultObj[el] = deepClone(obj[el],hash);
});
return resultObj;
}
// WeakMap:
// 只接受对象作为key; 但是它不会持有这个对象的引用,弱引用,不会影响内存,垃圾回收等。
// WeakMap的键名所指向的对象,不计入垃圾回收机制 // Reflect.ownKeys(target)
// 用于返回对象所有属性。
// 基本等同于 Object.getOwnPropertyNames 与 Object.getOwnPropertySymbols 之和。
二分查找 & 链表反转
[1,2,3,4,5,6,7] 找到 4 用二分查找做
力扣题: https://leetcode.cn/problems/binary-search/
function search(arr,target){ let minIndex = 0;
let maxIndex = arr.length-1;
let curTarget = null;
while(minIndex <= maxIndex){
let middleIndex = parseInt((minIndex + maxIndex) / 2);
curTarget = arr[middleIndex];
if(curTarget < target){
minIndex = middleIndex + 1; // 右移一位
}else if(curTarget > target){
maxIndex = middleIndex - 1; // 左移一位
}else{
return middleIndex; // 找到啦~!
}
}
return -1; // 没有找到
}
[1,2,3,4,5,6] ---> [6,5,4,3,2,1] 链表反转
力扣题: https://leetcode.cn/problems/reverse-linked-list/
var reverseList = function(head){
if(!head) return null;
let pre = null,cur = head;
while (cur) {
// let next = cur.next;
// cur.next = pre;
// pre = cur;
// cur = next
[cur.next,pre,cur] = [pre,cur,cur.next]
}
return pre
}
字符串数组 最长公共前缀
力扣题: https://leetcode.cn/problems/longest-common-prefix/
function longestCommonPrefix (strs) {
if(strs.length === 0) return ''; // 表示没有 返回空字符串
let first = strs[0]; // 取第一个作为比对
for(let i=1;i<strs.length;i++){
let j = 0;
for(;j<first.length & j<strs[i].length;j++){
if(first !== strs[i][j]){
break;
}
}
first = first.substr(0,j);
if(first === '') return first;
}
return first;
}