常见手写题

310 阅读6分钟

引子

     我们在做表单提交的事件中,经常会遇到多次提交的情况。如果后端没有对数据的唯一性进行处理,那么数据库中将会出现多条重复数据,这是不合理的。这是因为,用户操作过频(连续高频点击按钮提交),而未对操作按钮做限制导致的。之前,我都是对提交按钮添加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

  1. 需等所有均fulfilled 返回 p1,p2,p3返回值组成的数组,传递给p的回调函数;

  2. 一旦有一个 rejected,p的状态就变为 rejected ,此时第一个被rejected的实例的返回值,会传递给p的回调函数;

  3. promise.all([p1,p2,p3]), 如果作为参数的 promise实例,自己定义了 catch 方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法

  4. 如果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;
}