22023面试真题之手写&代码运行篇

5,091 阅读17分钟

世上从不缺努力的人,缺的事努力到底的人,越是难的时候,越要坚持住,不要轻易的否定自己

前言

大家好,我是柒八九

今天,我们继续2023前端面试真题系列。我们来谈谈关于手写&代码运行篇的相关知识点。

这也算是我们2023前端面试真题的最后一篇了。如果,想了解该系列的文章,可以参考之前发布的文章。

本来到这里就想直入主题了,想了想还是再多啰嗦几句。 其实出这个系列的原本目的就是为了记录和总结前段时间的面试题。方便自己查漏补缺及时复习

原本就是把面试过程中遇到的面试题,一股脑的做了汇总。但是,在一边面一边记录的时候,发现东西越来越多,然后牵扯的知识点也越来越广。

所以,索性就按照知识点进行了分类处理。这才了有如下的文章。

  1. 2023前端面试真题之JS篇
  2. 2023面试真题之CSS篇
  3. 2023面试真题之浏览器篇
  4. 22023面试真题之网络篇
  5. 2023面试真题之框架篇

上面的知识点,大部分都是平时自己平时学习总结和看一些优秀文章所汇总的。不能说是面面俱到,但是在面试中遇到相同问题时,能够由浅入深的进行描述和解释。

在发布了这个系列文章中,其中有一点对我印象很深,2023年初,随着口罩的解除,本来一片欣欣向荣,但是各种毕业消息甚嚣尘上,搞的人心惶惶。虽然,我也有幸加入了被毕业行列。也有过迷茫和彷徨。但是,就像此文开头的写的。

世上从不缺努力的人,缺的事努力到底的人,越是难的时候,越要坚持住,不要轻易的否定自己

只有在我们不放弃自己,别人才会给你充足的尊重和机会。 鸡汤味过于浓重,见笑了。

在如此严峻的环境下,已经不满足,只会Ctrl + C/Ctrl + VCV战士了。要提前有忧患意识,毕竟你离60/65退休还有很长的路要走。同时,由于IT行业的35门槛,也会让你焦虑和无助。毕竟,在我们无法撼动外部环境的前提下,我们只有自我驱动。要有那种,我命由我,不由天的向死而生的觉悟。

当最坏的情况到来的时候,我们能够做到华丽转身从容应对

这同样也是我写博客和公众号的初衷,因为我不想被淘汰,也不想被毕业后让别人挑三拣四,也不想变成一个只会怨天尤人的社会边缘人员。所以,我想用我仅有的能力,去学习去进步,同时,努力把自己的文章变的有温度。(但愿,其中有一篇文章能够帮助到你,如果有请在评论区告诉我。同时,如果大家想要一些免费资料,我也可以无偿给大家,同时也会附上我自己写的思维导图)

虽然,在如此大环境下,大家都说面试机会少的可怜,但是自从我开始真正的面试找工作,其实面试机会还是很多的。以下是参与面试过的公司,其中有些公司已经谈offer,有些公司因为其他原因而分道扬镳。(排名不分先后)

  1. 百度健康
  2. 百度地图
  3. 美图外卖
  4. 白龙马
  5. 民生银行
  6. 翼欧教育
  7. 理想汽车
  8. 开源中国
  9. 外企(2-3个)
  10. 云控智行
  11. 碳阻迹
  12. 某游戏公司
  13. 字节跳动
  14. processon
  15. 很多独角兽公司等

算起来,大大小小的公司,最起码20多家,然后有大有小。(所以说,不要自怨自艾,要自信一点)

还有就是,这里给大家提一个醒,就是在面试通过后,千万不要过于相信HR的忽悠能力,在没有签署offer之前,不要把这个公司最为最优的方案和决策。别问为什么,问就是血淋淋的教训。具体情况的不在赘述了。

Last but not least,在面试过程中,不要被其中面试结果否定自己。 找工作,就像找对象,是双方的决定。感情中,哪有那么多一见钟情,更多的都是日久生情。你想让面试官在几个小时内,发现你所有的闪光点,是不可能的。你只需要把自己最真实,最擅长的一面表现出来就可以了。

大部分情况就是,你会的,可能他不兴趣;你不熟练的,他却要刨根问底。想在心里上给你一个威压。(何苦呢,duck不必)

或者有一个更邪恶的想法,可能你说的那个知识点,ta可能都不会,你说的多了,ta以为你顾左右而言其他

所以说:

找工作也是很玄学的,找一个"灵魂伴侣",哪有那么容易

上面啰嗦了很多话,有些话可能词不达意,但是其中的核心思想就是:

拼搏到无能为力,坚持到感动自己

后期文章方向

如果大家翻看,我之前的文章,大部分都在科普和解读计算机相关的知识和前端基础相关(js/react等)。然后,其中有些知识大部分属于基础部分,而今年的行文方向和主要关注点,是按如下进行处理。(排名不分先后)

  1. rust 基础知识普及
  2. rust基础上,偏向前端应用的webassembly
  3. vite相关
  4. vue知识点
  5. 前端周边等

如果,有更好的行文方向,大家也可以一起讨论。希望,在这个浮躁的时代,大家能够一起进步。


好了,天不早了,干点正事哇。

下面的篇幅,我就对某些代码,不做过多的解读了,毕竟手写代码有很多实现和解释,一千个读者就有一千个哈姆雷特,我只做符合我的代码梳理和实现。如果有更好的实现和解释,欢迎大家一起讨论和研究。希望大家,不吝赐教。

你能所学到的知识点

  1. 如何找到数组中出现次数最多的字符串 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  2. var 变量运行 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  3. Promise运行 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  4. 手写curry 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  5. 手写compose 推荐阅读指数⭐️⭐️⭐️
  6. 地址数据处理 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  7. 三点是否共线 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  8. 多数之和 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  9. N进制加法 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  10. 只出现一次的数字 推荐阅读指数⭐️⭐️⭐️⭐️
  11. fibonic 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  12. ES5、ES6继承 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  13. 手写Promise 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  14. 手写apply/call/bind 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  15. 金钱格式化 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  16. 怎么判断一个对象是否为空对象 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  17. 节流和防抖的区别 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  18. 链表反转 推荐阅读指数⭐️⭐️⭐️⭐️
  19. 判断链表是否有环 推荐阅读指数⭐️⭐️⭐️⭐️
  20. 找链表中点 推荐阅读指数⭐️⭐️⭐️⭐️
  21. 链表中环的入口节点 推荐阅读指数⭐️⭐️⭐️⭐️
  22. 判断括号的正确性 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  23. 爬楼梯的最小成本 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  24. 常规算法 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  25. 手写 new 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  26. 手写发布订阅 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  27. Promise运行题 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

如何找到数组中出现次数最多的字符串

const findCountMax = arr => {
    let obj = {};
    arr.forEach(item=>{
        if(obj[item]) {
            obj[item]++
        }else {
            obj[item] =1
        }
    })
    let result = {count:0,str:''};
    for(let item in obj){
        if(obj[item]>result.count) {
            result.count = obj[item];
            result.str = item;
        }
    }
    return result.str;
}

var 变量运行

for(var i=0;i<3;i++){
  setTimeout(()=>console.log(i),1)
}
console.log(i) // 输出3

如何改正

使用let

for(let i=0;i<3;i++){
  setTimeout(()=>console.log(i),i)  // 输出0,1,2
}
console.log(i) //报错

使用IIFE

for(var i=0;i<3;i++){
  setTimeout(
      ((i)=>console.log(i))(i), // 输出0,1,2
      1
    )
}

console.log(i) // 输出3

Promise运行

Promise.resolve().then(()=>{
   console.log("Promise1");
   setTimeout(()=>{
     console.log("setTimeout2")
   },0)
})

setTimeout(()=>{
    console.log("setTimeout1")
    Promise.resolve().then(()=>{
      console.log("Promise2")
    })
},0)

打印结果为:Promise1 -> setTimeout1 -> Promise2 ->setTimeout2

解释:

  • Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
  • 立即resolve()Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时

curry

const curry = (fn,arity=fn.length,...args) => 
  arity<=args.length
  ? fn(...args)
  : curry.bind(null,fn,arity,...args)

测试函数

const add = (a,b,c) => a+b+c;
curry(add)(1,2,3) // 结果为6

compose

const compose = (...fns) => 
    fns.reduce((f,g)=>
        (...args)=>
          f(g(...args)
          )
      )

调用

const add1 = (x) => x + 1;
const mul3 = (x) => x * 3;
const div2 = (x) => x / 2;
div2(mul3(add1(5))); //=> 9

let result = compose(div2, mul3, add1)(5);
console.log(result); // =>9

地址数据处理

存在如下数据,

input = [
  {id:1,city:'北京',pid:0},
  {id:2,city:'河南',pid:0},
  {id:3,city:'山西',pid:0},
  {id:4,city:'洛阳',pid:2},
  {id:5,city:'晋中',pid:3},
  {id:6,city:'榆次',pid:5},
]

转换为

result = [
  {id:1,city:'北京',pid:0},
  {id:2,city:'河南',pid:0,
    children:[{id:4,city:'洛阳',pid:2}]
  },
  {id:3,city:'山西',pid:0,
     children:[
       {id:5,city:'晋中',pid:3,
         children:[{id:6,city:'榆次',pid:5}]
      }
     ]
  },
]

代码实现

function toTree(arr){
    arr.forEach(function(it){
      delete it.children;
    })
    // 定义map/
    let map = {};
    // 这里可以重构数据类型,放回字段值
    arr.forEach(function(item){
      map[item.id]=item;
    })

    // 定义返回集合
    let val=[];
    arr.forEach(function(item){
      let parent = map[item.pid];
      if(parent){
        // 有数据说明不是顶级节点,将数据放到该 children 子节点下
        ((parent.children) || (parent.children=[]))
        .push(item);
      }else{
        // 没有数据说明是顶级节点放到val中
        val.push(item);
      }
    });
    return val;
}

三点是否共线

三点是否共线可以通过判断斜率来判断: 设有 p1,p2,q三点,判断三点是否共线:

公式:

  • k1 = (p2.y - p1.y)/(p2.x - p1.x)

  • k2 = (q.y - p1.y)/(q.x - p1.x)

如果k1 === k2就表示三点共线

function isOnLine(p1,p2,q){
  return (p2.y - p1.y)/(p2.x - p1.x) === (q.y - p1.y)/(q.x - p1.x)
}

如果,指定q是否在线段内部,还需要判断q的范围大小


多数之和

排序数组中的两个数字之和的下标

题目描述:

输入一个递增排序的数组和一个值target,在数组中找出两个和为target的数字并返回它们的下标
提示:
数组中有且只有一对符合要求
同时一个数字不能使用两次

示例:输入数组: [1,2,4,6,10],k的值为8 输出[1,3]

function twoSum4SortedArray(nums,target){
  let left=0,right= nums.length-1; // 初始化指针left,right 
  while(left<right && nums[left] + nums[right] != target){
    if(nums[left] + nums[right]<target){
      left++;
    }else{
      right--;
    }
  }
  return [left,right]
}

求数组中两数之和的值的下标

此类方法是解决,数组中的值为乱序的情况

function twoSum(nums,target){
    let map = new Map(); // 用于,存储[nums[i],i]之间的关系
    for(let i =0;i<nums.length;i++){
        let expectValue = target - nums[i];
        // 先从map中找,是否存在指定值
        if(map.has(expectValue)){
            // 如果有,直接返回与值相对于的下标
            return [map.get(expectValue),i]
        }
        // 存储[nums[i],i]之间的关系
        map.set(nums[i],i);
    }
    return null;
}

数组中和为target的3个数字

题目描述:

输入一个数组,找出数组中所有和为target的3个数字的三元组
提示:
返回值不得包含重复的三元组

示例:输入数组: [-1,0,1,2,-1,-4],target的值为0 输出[[-1,0,1],[-1,-1,2]]

代码实现

function threeSum(nums,target){
  let result = [];
  if(nums.length<3) return [];
  
  // 人工对数据进行排序处理
  nums.sort((a,b)=>a-b);
  
  let i =0;
  while(i<nums.length-2){
    twoSum(nums,i,target,result);
    let temp = nums[i];
    // 剔除,重复元祖中第一个数值
    while(i<nums.length && nums[i]==temp) i++;
  }
  return result;
}

我们把排序数组中的两个数字之和的算法,做了一个改造,因为left不在从0开始,所有需要将left的前一个位置i传入,right的逻辑不变,还是数组尾部

  • left = i + 1
  • right = nums.length - 1

function twoSum(nums,i,target,result){
  // 初始化指针left,right 
  let left = i + 1, right = nums.length -1;
  
  while(left<right){
    // 求和
    let sum = nums[i] + nums[left] + nums[right];
    // 指针移动过程 (if/else)
    if(sum === target){
      result.push([nums[i],num[left],nums[right]]);
      
      let temp = nums[left];
      // 剔除,重复元祖第二个数值
      while(nums[left] === temp && left < right) left++;
    }else if(sum < 0) {
      left++;
    }else{
      right--;
    }
  }
}

N进制加法

function Nsum(a,b,n){
    let result = '';
    let i = a.length - 1;
    let j = b.length -1;
    let carry = 0;
    while(i>=0 || j>=0){
        let digitA = i >=0 ? a[i--] -'0':0;
        let digitB = j >=0 ? b[j--] -'0':0;
        let sum = digitA + digitB + carry;
        carry = sum >=n ? 1 :0;
        sum = sum >=n ? sum - n:sum;
        result = sum + result;
    }
    if(carry){
        result ='1' + result;
    }
    return result;
}

代码测试

  1. 二进制加法
    • Nsum('10','01',2)
    • 结果为11
  2. 十进制加法(十进制大数相加)
    • Nsum('7','8',10)
    • 结果为15

只出现一次的数字

某个元素仅出现 一次 外,其余每个元素都恰出现 N次

求仅出现一次的数字为哪个值

function singleNumber(nums,n) {
    // 构建一个用于存储数组所有数字位数之和的数组
    let bitSums = new Array(32).fill(0);
    for(let num of nums){
        for(let i=0;i<32;i++){
         // 求num在i位置的位数,并将其与指定位置的位数相加
         bitSums[i] +=(num>>(31-i)) &1;
        }
    }
    let result =0;
    for(let i=0;i<32;i++){
        //从最地位(0)位开始遍历
        result = (result<<1) + bitSums[i]%n;
    }
    return result;
};

代码测试

  1. 出现2次,找出现一次的
    • singleNumber([1,1,2,2,3],2);
    • 结果为 3
  2. 出现3次,找出现一次的
    • singleNumber([1,1,1,2,2,2,3],3)
    • 结果为 3
  3. 同理其他

某个元素仅出现 一次 外,其余每个元素都恰出现 2次的另外解法

 function singleNumber(nums) {
  let result = 0;
  for(let i of nums){
      result ^= i;
  }
  return result
};

异或运算(^)在两个二进制位不同时返回1,相同时返回0

联想消消乐


fibonic

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……

常规方式

function fibonacci(n){
  if(n<=1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2)
}

测试

fibonacci(1000) //  浏览器卡死

尾递归方式

function fibonacci(n,start = 1,total = 1){
  if(n <=1) return total;
  return fibonacci(n - 1, total,total + start)
}

测试

fibonacci(1000) //  4.346655768693743e+208

迭代方式

空间复杂度为O(n)

function fibonacci(n) {
  let result = new Array(n).fill(0);
  result[0] =1;
  result[1] =1;
  for(let i=2;i<n;i++){
    result[i] = result[i-2] + result[i-1]
  }
  return result[n-1]
}

空间复杂度为O(1)

function fibonacci(n) {
  let result = [1,1];

  for(let i=2;i<n;i++){
    result[i&1] = result[(i-2)&1] + result[(i-1)&1]
  }
  return result[(n-1)&1]
}

ES5、ES6继承

通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

ES5 继承 (6类)

原型继承

将父类实例赋值给子类的原型对象(prototype)

SubClass.prototype = new SuperClass()
// 修正因为重写子类原型导致子类的constructor属性被修改
SubClass.prototype.constructor = SubClass;

缺点

  • 父类中引用类型的属性,被子类实例公用
  • 创建父类的时候,无法向父类传递参数

构造函数继承(借助 call)

创建即继承

function SubClass(params){
  SuperClass.call(this,params)
}

缺点

  • 不能继承原型属性或者方法

组合继承

原型继承 + 构造函数继承

function SubClass(name){
    // 构造函数式继承父类name属性
   SuperClass.call(this,name)
}
// 原型链继承 子类原型继承父类实例
SubClass.prototype = new SuperClass();
// 修正因为重写子类原型导致子类的constructor属性被修改
SubClass.prototype.constructor = SubClass;

缺点

  • 父类构造函数被调用两次

原型式继承

对原型链继承的封装,过渡对象相对于原型继承的子类

function inheritObject(o){
   //声明一个过渡函数对象
   function F(){}
   //过渡函数的原型继承父对象
   F.prototype = o;
   // 返回一个实例,该实例的原型继承了父对象
   return new F();
}

缺点

  • 父类中引用类型的属性,被子类实例公用

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化

寄生式继承 (过渡方式)

原型式继承的二次封装,在二次封装中对继承的对象进行拓展。

function createObject(obj){
   //通过原型式继承创建新对象
   var o =Object.create(obj);
   //拓展新对象
   o.name = `北宸南蓁`;
   //返回拓展后的对象
   return o;
}

缺点

  • 父类中引用类型的属性,被子类实例公用

寄生组合式继承

function inheritPrototype(subClass, superClass) {
  //复制一份父类的原型副本并赋值给子类原型
  subClass.prototype  =Object.create(superClass.prototype);
  // 修正因为重写子类原型导致子类的constructor属性被修改
  subClass.prototype.constructor = subClass;
}

function subClass(){
  superClass.call(this)
}

ES6 的 extends 关键字 也采用这种方式


ES6继承

原理:ES6类 + 寄生式组合继承

es5和es6的继承有什么区别

ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(superClass.call(this)).

ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this

ES5的继承时通过原型或构造函数机制来实现。

ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。


手写Promise

Promise 对象就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用

分析 Promise 的调用流程:

  1. Promise 的构造方法接收一个executor(),在new Promise()时就立刻执行这个 executor 回调
  2. executor()内部的异步任务被放入宏/微任务队列,等待执行
  3. then()被执行,收集成功/失败回调,放入成功/失败队列
  4. executor()异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行

其实熟悉设计模式,很容易就能意识到这是个观察者模式,这种

  • 收集依赖
  • 触发通知
  • 取出依赖执行

的方式,被广泛运用于观察者模式的实现,在 Promise 里,执行顺序是

  1. then收集依赖
  2. 异步触发resolve
  3. resolve执行依赖。

依此,我们可以勾勒出 Promise 的大致形状:

//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发

    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = FULFILLED              // 变更状态

      // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
      // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
      while(this._resolveQueue.length) {
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // 实现同resolve
    let _reject = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = REJECTED               // 变更状态
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }

  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}

代码测试

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('result')
  }, 1000);
})
p1.then(res =>console.log(res))
//一秒后输出result

手写apply/call/bind

apply

Function.prototype.myApply = function (context, args) {
  context = context || window;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn
  return result;
}

call

Function.prototype.myCall = function (context, ...args) {
  context = context || window;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn
  return result;
}

bind 的实现

Function.prototype.myBind = function (context,...arg1) {
    let that = this
    return function(...arg2){
      return that.apply(context, [...arg1, ...arg2])
    }
}

金钱格式化

'123456789.123'格式化为'123,456,789.12'

function formatPrice(price){
    let priceStr = String(price);
    let front = [];
    let back = []
    if(priceStr.includes('.')){
        front = priceStr.split('.')[0].split('');
        back = priceStr.split('.')[1].slice(0,2);
    }else{
        front = priceStr.split('')
    }
    
    let result = 0;
    
    result = front
            .reverse()
            .reduce(
              (prev,cur,index)=>
                  (index%3?cur:cur+',')+prev
                )

    if(back.length){
        result = result + '.' + back.toString();
    }

    return result;
}

使用JS内置 API (toLocaleString),实现金额的格式化

const options = {
  style: 'currency',
  currency: 'CNY',
};
(999999.1212).toLocaleString('zh-CN', options); // ¥999,999.12

怎么判断一个对象是否为空对象

按照是否考虑Symbol进行分类

不考虑Symbol

  1. for-in
  2. JSON.stringify(o) ==='{}'
  3. !Object.keys(o).length

考虑Symbol

  1. !Reflect.ownKeys(o).length
  2. !Object.getOwnPropertyNames(o).length && !Object.getOwnPropertySymbols(o).length

对象拍平问题 (数组/对象)

数组扁平化

const flattenArr = arr => {
    let result = [];
    (function helper(arr) {
        arr.forEach(item=>{
            if(Array.isArray(item)){
                helper(item)
            }else{
                result.push(item)
            }
        })
    })(arr)
    return result;
}

指定展开N层

const flattenArrN = (arr,depth=1) => {
    let result = [];
    (function helper(arr,depth) {
        arr.forEach(item=>{
            if(Array.isArray(item)&&depth>0){
                helper(item,depth-1)
            }else{
                result.push(item)
            }
        })
    })(arr,depth)
    return result;
}

对象扁平化

存在如下数据类型

const obj = {
  a: 1,
  b: [1, 2, { c: true }],
  c: { e: 2, f: 3 },
  g: null,
};

经过处理变成:

{
  a: 1,
  'b[0]': 1,
  'b[1]': 2,
  'b[2].c': true,
  'c.e': 2,
  'c.f': 3,
  g: null
}

代码实现

const flattenObj = obj => {
  let result = {};
  (function helper(obj,preKey) {  
      if(!obj) return
      Object.entries(obj).forEach(([key,value])=>{
          let isArray = Array.isArray(obj);
          let keyStr = isArray 
                      ? `${preKey}[${key}]` 
                      : `${preKey}${key}`;
          if(Array.isArray(value)){
              helper(value,keyStr)
          }else if(value&&typeof value ==='object'){
              helper(value,`${keyStr}.`)
          }else{
              result[keyStr] = value;
          }
      })
  })(obj,'')

  return result;
}

通过对比数组扁平化,可以发现针对对象扁平化处理,大体的思路都是一样的,只不过在处理对象时,有些数据需要做额外的处理。这块需要做一个消化处理。


节流和防抖的区别

  • debounce(防抖):一个连续操作中的处理,只触发一次,从而实现防抖动。
  • throttle(节流):一个连续操作中的处理,按照阀值时间间隔进行触发,从而实现节流。

手写防抖

debounceTail

const debounceTail = (fn, ms = 0) => {
  let timerId;
  return function(...args) {
    clearTimeout(timeoutId);
    timerId = setTimeout(() => 
          fn.apply(this, args)
          ,ms
        );
  };
};

debounceStart

const debounceStart = (fn,ms =0) => {
    let immediate = true;
    let timerId = null;

    return function(...args){
        if(immediate) {
            fn.apply(this,args);
            immediate = false
        }

        clearTimeout(timerId);

        timerId = setTimeout(()=> 
              immediate = true 
              ,ms
            )
    }
}

throttle

function throttle(fn, ms = 0){
  let timerId;
  return function(...args){
      if(timerId) return;
      timerId = setTimeout(()=>{
          fn.apply(this, args); 
          timerId = null;
      }, ms)
  }
}

链表反转

function reverseList(head){
  // 初始化prev/cur指针
  let prev = null;
  let cur = head;
  // 开始遍历链表
  while(cur){
    // 暂存后继节点
    let next = cur.next;
    // 修改引用指向
    cur.next = prev;
    // 暂存当前节点
    prev = cur;
    // 移动指针
    cur = next;
  }
  return prev;
};

判断链表是否有环

function hasCycle(head){
  let fast = head;
  let slow = head;
  
  while(fast && fast.next){
    fast = fast.next.next;
    slow = slow.next;
    if(fast == slow) return true;
  }
  return false;
}

找链表中点

function middleNode(head){
    let slow = head;
    let fast = head;
    // 遍历链表节点
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 处理链表节点为偶数的情况
    if(fast){
      slow = slow.next;
    }
    return slow;
}

链表中环的入口节点

function detectCycle(head){
  let fast = head;
  let slow = head;
  while(fast && fast.next){
    fast = fast.next.next;
    slow = slow.next;
    if(fast ==slow){
      fast = head;
      while(fast!=slow){
        fast = fast.next;
        slow = slow.next;
      }
      return slow
    }
  }
  return null;
}

判断括号的正确性

function isValid (s) {
  let stack = new Stack();
  // 遍历 字符串
  for(let c of s){
      // 遇到左括号,将与其匹配的右括号入栈处理
      if(c==='('){
          stack.push(')')
      }else if(c==='['){
          stack.push(']')
      }else if(c==='{'){
          stack.push('}')
      // 遇到右括号
      // 1. 判断栈内是否有括号,如果没有,那说明此时匹配不了
      // 2. 满足①的情况下,判断此时字符是否和栈顶元素匹配
      }else if(stack.isEmpty() || stack.pop()!==c){
          return false;
      }
  }
  // 最后再验证一下,栈是否为空,如果不为空,说明还有未匹配的括号
  return stack.isEmpty();
};

note: 这里手动实现了一个Stack,是为了代码更加的清晰和方便记忆。当然,你也需要对手动实现一个Stack有一定的了解

class Stack {
   constructor() {
     this.items = []; 
   }
   // 添加element到栈顶
   push(element) {
     this.items.push(element);
   }
   // 移除栈顶的元素,同时返回被移除的元素
   pop() {
     return this.items.pop();
   }
   // 如果栈里没有任何元素就返回`true`,否则返回`false`
   isEmpty() {
     return this.items.length === 0;
   }
}

上面实现了一个最简单的版本,其实还有peek(返回栈顶的元素,不对栈做任何修改)/clear(移除栈里所有的元素)/size(返回栈里的元素个数)等方法。


爬楼梯的最小成本

这是一道典型的动态规划的算法题。 解决方法有很多种。

  1. 递归方式
  2. 迭代方式

递归方式

状态转移方程其实是一个递归的表达式,可以很方便的将它转换成递归代码。

function minCost(cost){
  let len = cost.length;
  return Math.min(helper(cost,len -2),helper(cost,len -1));
}

辅助函数

function helper(cost,i){
  if(i<2){ // 基线条件
    return cost[i]
  }
  return Math.min(helper(cost,i-2),helper(cost,i-1)) + cost[i];
}

暴力递归,有重复计算的问题,一般都会利用缓存对象,对数据进行缓存处理。

迭代方式

function minCost(cost){
  let len = cost.length;
  let dp = new Array(len).fill(0);
  dp[0] = cost[0];
  dp[1] = cost[1];
  
  for(let i =2;i<len;i++){
    dp[i] = Math.min(dp[i-2],dp[i-1]) + cost[i]
  }
  return Math.min(dp[len-2],dp[len-1])
}

空间复杂度为O(1)的迭代代码

function minCost(cost){
  let len = cost.length;
  let dp = [cost[0],cost[1]];
  
  fort(let i =2;i<len;i++){
    dp[i&1] = Math.min(dp[0],dp[1])+cost[i]
  }
  return Math.min(dp[0],dp[1]);
}

常规算法

const swap = (arr,i,j) => [arr[i],arr[j]] = [arr[j],arr[i]];

交换排序 (SBQ)

BubbleSort

// 外层遍历负责次数  i<len-1
// 内层遍历处理数据对比   j=0 ,j<len-i-1
function BubbleSort(arr){
    let len = arr.length;
    if(len<2) return arr;
    for(let i=0;i<len-1;i++){
        for(let j=0;j<len -i -1;j++){
            if(arr[j]>arr[j+1]) swap(arr,j,j+1)
        }
    }
    return arr;
}

快排

// QuickSort
// helper 递归处理
// 用partition 找主元 pivot
// partition 中,找到中间位置 middle = lo + ((hi-lo)>>1)
// while(i<=j) 
const quickSort = arr =>helper(arr,0,arr.length-1);

const helper = (arr,lo,hi) => {
    if(lo>=hi) return;
    let pivot = partition(arr,lo,hi);
    helper(arr,lo,pivot-1);
    helper(arr,pivot,hi);
}

const partition = (arr,lo,hi)=>{
    const middle = lo + ((hi-lo)>>1);
    let i = lo,j =hi;
    while(i<=j){
        while(arr[i]<=arr[middle])i++;
        while(arr[j]>=arr[middle])j--;
        if(i<=j) swap(arr,i++,j--)
    }
    return i;
}

插入排序 IIS

InsertSort

// sential = arr[i] 外层
// 内存循环条件 j=i-1
const InsertSort = arr => {
    let len = arr.length;
    if(len<2) return arr;
    for(let i=1;i<len;i++){
        let sential = arr[i];
        for(let j=i-1;j>=0&&arr[j]>sential;j--){
            arr[j+1] = arr[j]
        }
        arr[j+1] = sential;
    }
    return arr;
}

ShellSort

// t = len>>1
// while
// arr[j+t] = arr[j]
const ShellSort = arr => {
    let len = arr.length;
    let i,j,sential;
    let t = len>>1;
    while(t>=1){
        for(let i=t;i<len;i++){
            sential = arr[i];
            for(let j=i-t;j>=0&&arr[j]>sential;j=j-t){
                arr[j+t] = arr[j]
            }
            arr[j+t]= sential;
        }
        t = t>>1;
    }
    return arr;
}

选择排序

// 同向比较
const SelectSort = arr => {
    let len = arr.length;
    let i,j,minIndex;
    for(let i=0;i<len;i++){
        minIndex = i;
        for(let j=i+1;j<len;j++){
            if(arr[j]<arr[minIndex]) minIndex = j
        }
        swap(arr,i,minIndex)
    }
    return arr;
}

new

使用 new 调用类的构造函数会执行如下操作(三步)

  1. 在内存中创建一个新对象
    • 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性
    • context = Object.create(constructor.prototype);
  2. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
    • 执行构造函数内部的代码(给新对象添加属性)
    • result = constructor.apply(context, params);
  3. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
    • return (typeof result === 'object' && result != null) ? result : context;
 function _new(
     /* 构造函数 */ constructor, 
     /* 构造函数参数 */ ...params
     ) {
    // 创建一个空对象,继承构造函数的 prototype 属性
    let context = Object.create(constructor.prototype);
    // 执行构造函数
    let result = constructor.apply(context, params);
    // 如果返回结果是对象,就直接返回,否则返回 context 对象
    return (typeof result === 'object' && result != null) 
          ? result 
          : context;
}

发布订阅

发布订阅核心点

  • on:订阅
    • this.caches[eventName].push(fn);
  • emit:发布
    • this.caches[eventName].forEach(fn => fn(data));
  • off:取消订阅
    • this.caches[eventName]filter出与fn不同的函数
    • const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : [];
    • this.caches[eventName] = newCaches;
  • 内部需要一个单独事件中心caches进行存储
class Observer {
  caches = {}; // 事件中心
  
  // eventName事件名-独一无二, fn订阅后执行的自定义行为
  on (eventName, fn){ 
    this.caches[eventName] = this.caches[eventName] || [];
    this.caches[eventName].push(fn);
  }
  
  // 发布 => 将订阅的事件进行统一执行
  emit (eventName, data) { 
    if (this.caches[eventName]) {
      this.caches[eventName]
      .forEach(fn => fn(data));
    }
  }
  // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
  off (eventName, fn) { 
    if (this.caches[eventName]) {
      const newCaches = fn 
        ? this.caches[eventName].filter(e => e !== fn) 
        : [];
      this.caches[eventName] = newCaches;
    }
  }

}

测试用例

ob = new Observer();

l1 = (data) => console.log(`l1_${data}`)
l2 = (data) => console.log(`l2_${data}`)

ob.on('event1',l1)
ob.on('event1',l2)

//发布订阅
ob.emit('event1',789) 
// l1_789
// l2_789

// 取消,订阅l1
ob.off('event1',l1)

ob.emit('event1',567)
//l2_567

Promise+setTimeout

const testPromise = new Promise((resolve) => {
    setTimeout(() => {
        console.log('准备第一次resolve');
        resolve(1000);
        setTimeout(() => {
            console.log('准备第二次resolve');
            resolve(2000);
        }, 1000);
    }, 1000);
});

testPromise.then(val => {
    console.log('first', val);
})

testPromise.then(val => {
    console.log('second', val);
})

setTimeout(() => {
    testPromise.then(val => {
        console.log('third', val);
    })
}, 5000);

输出结果为

  1. 准备第一次resolve
  2. first(1000)
  3. second(1000)
  4. 准备第二次resolve 5. third(1000)

PromiseQueue

存在如下的场景: 针对一批异步任务,由于某些特殊的原因,只能在某一时间内,并发执行concurrentCount个异步任务。只有在前面的N个任务处于落定状态,剩余的任务才会被执行。

换句话说,手动实现一个能够控制异步触发的数据结构。(如果大家对网络有了解的h话,这个结构是不是很像滑动窗口

class PromiseQueue{
    constructor(tasks,concurrentCount=1){
        this.totals = tasks.length;
        this.todo =tasks;
        this.count = concurrentCount;
        this.running =[];
        this.complete =[];
        
    }

    runNext(){
        return (
            this.running.length < this.count
            && this.todo.length
        )
    }

    run(){
        while(this.runNext()){
            let promise = this.todo.shift();
            promise.then(()=>{
                this.complete.push(this.running.shift());
                this.run();
            })

            this.running.push(promise)
        }
    }
}

测试用例

// 接收一个promise数组,定义窗口大小为3
const taskQueue = new PromiseQueue(tasks, 3); 
taskQueue.run();

后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。