铜三铁四还不抓紧手撕代码!!!

12,098 阅读13分钟

金三银四在今年彻底成了铜三铁四,不管在职的还是离职的,都在瑟瑟发抖。 看下这张图,应该会有一个很直观的感受😁

大环境你无力抵抗,能做的只能提高自己,躺平更不可能,穷逼是没资格躺平的😁。

既然无法躺平,只能做到人无我有,人有我优!

一道特别简单的数组map方法面试题

大家可以看下这段代码返回的结果是什么?为什么?

let a = [1,2,3];
a = a.map((item) => {
  item +=2;
});
console.log(a);

常见的变量输出

  1. 可以看看这里输出什么? 为什么?
(function() {
    var a = b = 3;
})()
console.log(b);
console.log(a);
  1. 这里输出什么?为什么会这样,是不是和你想的一致,如果不一致,可以想想为什么,是自己的思路哪里有问题?
function O (age) {
    this.age = age;
  }
  let o = new O(1);
  let age = 3;
  O.prototype.age = 2;

  setTimeout(function () {
    age = 4;
    O(5);
    console.log(o.age, age, window.age)
  }, 1000)
  1. 这是另外一道简单的输出题目,也可以思考下是不是和你想的输出一致?
var func1 = x => x;
var func2 = x => {x}; 
var func3 = x => ({x});


console.log(func1(1));
console.log(func2(1));
console.log(func3(1));

无限add

这是最常见的一道面试题,出现的概率是非常非常高

比如让你实现如下效果

console.log(add(1,2, 3)) // 打印6 console.log(add(1)(2,3)) // 打印6 console.log(add(1)(3)(2)) // 打印6 console.log(add(1,2)(3)) // 打印6

假如是你,可以考虑下如何实现?

我的想法是通过一个数组收集依赖,不管调用多少次,可以都把参数都push到数组中,最后求结果的时候,可以求和。

但是现在有个问题,就是什么时候求和呢?

只有使用valueOf或者toString了,如果没有想到valueOf或者toString也没啥关系,第一次遇到一般很少有人能想到。

代码如下

function add() {
      let res = [...arguments];
      function resultFn() {
        res = res.concat([...arguments]);
        return resultFn;
      }
      resultFn.valueOf = function() {
        return res.reduce((a,b) =>a+b);
      }
      return resultFn;
    }
    console.log(5+add(1,2)(3)(4))
    // 输出15 

如何缓存前面的结果

类似如下 add(1)(2)(3) 输出6, 当add(1,2)(3)的时候,不需要重新计算直接从缓存里边拿到结果?

这个可以自己思考下如何解决,任何没有经过自己思考的东西都不是你自己的😁

function add() {
  let res = [...arguments];
  if(!add.map) {
    add.map = new Map();
  }
  function resultFn() {
    res = res.concat([...arguments]);
    return resultFn;
  }
  resultFn.valueOf = function() {
    const curStr = res.join("");
    if(add.map.has(curStr)) {
      return add.map.get(curStr);
    }else {
      const result = res.reduce((a,b) =>a+b)
      add.map.set(curStr,result);
      return result;
    }
  }
  return resultFn;
}
console.log(add(1,2)(3)(4).valueOf())
console.log(add(1,2,3)(4).valueOf())

逆转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

这也是面试中经常会遇到的一道题目,题目倒不是很难,是leetcode的一道easy题目。

这里的思想很简单,就是采用递归。

递归在算法中是一个很重要的思想,很多算法都可以采用递归思想解决。如果想使用递归思想解决问题,可以记住以下两点方法,会事半功倍。

  1. 递归想清楚什么边界条件下退出。
  2. 可以写几个测试用例,从最简单的开始使用递归解决试试。
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    if(!head) {
        return null;
    }
    if(head && !head.next) {
        return head;
    }
    const pre = head;
    const cur = head.next;
    const newHead = reverseList(head.next);
    cur.next = pre;
    pre.next = null;
    return newHead;
};

洗牌函数

洗牌算法也是面试经常会遇到的一道题目,出现的频率也是蛮高的。

我有次也被面到了这道题目,但是呢,当时自己自作聪明,考虑太多了,考虑Math.random能不能做到完全随机😂

其实这道算法只要想明白了使用Math.random完全随机就可以了

function shuffle(arr=[]) {

  for(let j = arr.length-1;j>=0;j--) {
    const randomIndex = Math.floor(Math.random() * j+1);
    const temp = arr[j];
    arr[j] = arr[randomIndex];
    arr[randomIndex] = temp;
  }
  return arr;
}
console.log(shuffle([1,2,3,4,5]));

手写call apply bind

这三个函数在面试中出现的频率也是非常非常高,所以这个基本上是如果想出去面试是必须要掌握的,而且要掌握到和喝水吃饭一样自然😁

call 实现其实不是很难,如果觉得很难的话,可以先想想哪里觉得难,找到点之后,才能对症下药。

function myCall(ctx,...args) {
  ctx = ctx || window;
  const that = this;
  ctx.fn = that;
  const res = ctx.fn(...args);
  delete ctx.fn;
  return res;
}
Function.prototype.myCall= myCall;
console.log.myCall(null,1,2);

剩下的apply和bind实现的原理基本上差不多,可以自己实现。

深拷贝

深拷贝和浅拷贝这个也是面试频率非常高的面试题,浅拷贝比较好实现,深拷贝相对来说比较难点。

写一个简单深拷贝,我相信很多人都可以写出来,而且其实在项目中使用基本上没有啥问题。但是如果想要写一个完整的深拷贝,需要考虑各种情况的处理,如果在平时可能没啥问题,但是如果面试的时候,可能就会一深入进去,就会卡住。

简单的实现

function deepClone(target) {
    if (typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = deepClone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
  };
  const a = {"ss":1, "bb":[1,2,4]};
  const b = deepClone(a);
  b.ss = 333;
  console.log(b);
  console.log(a);

如果想要写的更完美一些,就会需要考虑一下一些特殊类型和特殊场景。

  1. 如果需要拷贝的对象里边有循环怎么办,类似于如下情况
const a = {
  c: 1,
  b: [2,3],
};
a.self = a;

这种情况假如是你如何处理呢?(其实这种在真实业务场景很少会遇到)

这种情况其实就是循环,解决这种办法很直接,就是把循环给切断,不然那就是无限循环了,

或者想下有没有其它的方法,使用map缓存是不是也可以?

切断循环可以判断当前的值是不是指向自身来判断,代码如下

target[key] === target

至于还有其它一些特殊数据类型的处理,比如Symbol,函数等,可以自己去查下,其实写一个比较完整的深拷贝还是有点难度的,可以去看下loadsh如何实现的。

手写debounce和throttle函数

debounce和throttle 基本上面试都会被面到,这两个可以说是面试必问的超级高频题。

debounce和throttle的区别以前看了很多文章,总是模模糊糊的没有明确的搞明白区别,后来总算明白了,两者的区别其实就是throttle会在一段时间内肯定会触发一次,debounce是不断触发的情况下,只触发最后一次。

当然这是比较简单的版本的

function throttle(fn,delay) {
  let timeId = null;
  return function() {
    if(timeId) {
      return;
    }
    timeId = setTimeout(function() {
      fn.apply(this,arguments);
      clearTimeout(timeId);
    },delay);
  }
}
function debounce(fn,delay) {
  let timeId = null;
  return function() {
    if(timeId) {
      clearTimeout(timeId);
    }
    timeId = setTimeout(function() {
      fn.apply(this,arguments);
      clearTimeout(timeId);
    },delay);
  }
}

寻找二叉树中的最近的公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

这也是在面试中经常会被问到的问题

题目其实不是很难,但是在面试的氛围中的时候,可能就不一样了,但是这个没有啥好的办法,只能在平时多练多思考多总结,台上一分钟,台下十年功,没有其它的捷径。

二叉树的算法大多数都可以使用递归解决,我当时遇到的时候,其实也已经想到使用递归解决,但是没有想明白使用递归的基本条件---那就是需要找到何时结束递归,也就是找到递归的边界条件。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if(root === null || root.val === p.val || root.val === q.val ) {
        return root;
    }
    const leftNode = lowestCommonAncestor(root.left,p,q);
    const rightNode = lowestCommonAncestor(root.right,p,q);
    if(leftNode && rightNode) {
        return root;
    }
    if(leftNode && !rightNode) {
        return leftNode;
    }
    if(!leftNode && rightNode) {
        return rightNode;
    }
};

Promise.all 实现

面试的时候一般不会让你手写Promise,除非有点变态的公司,大多数都是让你说下如何实现原理就可以了,这个大家可以自己去搜下,很多比较好的文章。

不过Promise.all 这个在面试中出现的手写的概率是非常高的,这个必须要达到可以熟练手写的程度,不然有时候面试就是浪费自己的时间,

现在大裁员的背景下,面试官选择的多了,可能会因为任何一点就把你挂掉,自己能做的就是人无我有,人有我优,才能脱颖而出。

Promise.all 实现原理其实就是根据传入的数组b,然后

Promise.myAll = function(arr = []) {
  if(arr.length === 0) return Promise.resolve();
  return new Promise((resolve,reject) => {
    let res = [];
    let allCount = arr.length;

    for(let i =0;i<arr.length;i++) {
      arr[i].then((curRes)=> {
        res.push(curRes)
        if(res.length === allCount) {
          return resolve(res);
        }
      }).catch((err) =>{
        return reject();
      });
    }
  })
}
const a1 = new Promise((resolve,reject)=> {
  console.log(11)
  setTimeout(() => {
    resolve(1)
  },1000)
})
const b1 = new Promise((resolve,reject)=> {
  console.log(22)
  setTimeout(() => {
    resolve(3)
  },3000)
})
Promise.myAll([a1,b1]).then((res)=> {
  console.log(333,res)
}).catch((err) => {
  console.log(77,err)
})

LazyMan实现

实现一个LazyMan,可以按照以下方式调用:

LazyMan('Hank')
// 输出:
// Hi! This is Hank!

LazyMan('Hank').sleep(10).eat('dinner')
// 输出:
// Hi! This is Hank!
// 等待10秒
// Wake up after 10
// Eat dinner~


LazyMan('Hank').eat('dinner').eat('supper')
// 输出:
// Hi This is Hank!
// Eat dinner~
// Eat supper~

LazyMan('Hank').sleepFirst(5).eat('supper')
// 输出:
// 等待5秒
// Wake up after 5
// Hi This is Hank!
// Eat supper
// 以此类推。

这里可以首先想下如何实现? 不管看任何东西都要带着问题去看去思考,这样肯定会事半功倍,如果没有任何问题,其实根本下面是没有必要去看的。

这里的难点就是sleepFirst如何改变执行顺序?想想假如是你会有什么想法去改变执行顺序呢?

其实这里只要想明白执行时和未执行时两种状态就可以了。

比如我们可以通过setTimeout先延迟执行打印一个数组的长度,但是我们在setTimeout的函数还没有执行的时候,我们修改了数组的长度,不管是push进数组一个数字或者函数或者是从数组中删除一个的时候,当settimeout的函数真正执行的时候,打印出该数组的长度肯定是我们修改过的。

class LazyMan1 {
  constructor(name) {
    this.queue = [];
    this.name = name;
    this.queue.push(() => {
      console.log(this.name);
      this.next();
    });
    setTimeout(() => {
      this.next();
    });
  }
  next() {
    if (this.queue.length > 0) {
      const fn = this.queue.shift();
      fn();
    }
    return this;
  }
  eat(food) {
    this.queue.push(() => {
      console.log(`Eat ${food}`);
      this.next();
    });
    return this;
  }
  sleep(delay) {
    this.queue.push(() => {
      setTimeout(() => {
        console.log(`Wake up after ${delay}`);
        this.next();
      }, delay * 1000);
    });
    return this;
  }
  sleepFirst(delay) {
    this.queue.unshift(() => {
      setTimeout(() => {
        console.log(`Wake up after ${delay}`);
        this.next();
      }, delay * 1000);
    });
    return this;
  }
}
function LazyMan(name) {
  return new LazyMan1(name);
}
// LazyMan("Hank");

// LazyMan("Hank").sleep(10).eat("dinner");

// LazyMan("Hank").eat("dinner").eat("supper");

LazyMan("Hank").sleepFirst(5).eat("supper");

实现JSON.stringify

这里需要实现一个JSON.stringify,实现效果如下,输入数组或者对象或者字符串或者数字,然后输出格式化的代码

类似如下

{
  a1,
  b2,
  c: [
   1,
   {
    sxx1,
    ssd2
   }
  ],
  o1: {
   see1,
   sddsd: {
    sdsd323
   }
  }
}

大家可以首先想下如何实现,自己实现试试,这里边的细节其实还是很多的,虽然实现思想很简单,但是里边的细节还是很多,最好是自己实现下。

这里主要难点:

  1. 需要处理好每行的缩进
  2. 需要在差不多二十分钟左右实现
function stringfiy1(
  data: object | number | string | Array<any>,
  row: number,
  isVal: boolean,
  isLast: boolean
): string {
  let res: string = "";
  if (typeof data === "number" || typeof data === "string") {
    if (isVal) {
      if (!isLast) {
        res += `\xa0${data},\n`;
      } else {
        res += `\xa0${data}\n`;
      }
    } else {
      if (!isLast) {
        res += `${data},\n`;
      } else {
        res += `${data}\n`;
      }
    }
    return res;
  }
  if (typeof data === "object" && data !== null && !Array.isArray(data)) {
    // 是不是key:val这种
    if (isVal) {
      res += "\xa0{\n";
    } else {
      res += "{\n";
    }
    const keys: Array<string | number> = Object.keys(data);
    for (let i: number = 0; i < keys.length; i++) {
      for (let j: number = 0; j <= row + 1; j++) {
        res += "\xa0";
      }
      res += `${keys[i]}:${stringfiy1(
        data[keys[i]],
        row + 1,
        true,
        i === keys.length - 1
      )}`;
    }
    if (isVal) {
      for (let i: number = 0; i <= row; i++) {
        res += "\xa0";
      }
      if (!isLast) {
        res += "},\n";
      } else {
        res += "}\n";
      }
    } else {
      if (row > 0) {
        for (let i: number = 0; i <= row; i++) {
          res += "\xa0";
        }
      }

      res += isLast ? "}\n" : "},\n";
    }
    return res;
  } else if (Array.isArray(data)) {
    // 是不是key:val这种
    if (isVal) {
      res += "\xa0[\n";
    } else {
      res += "[\n";
    }
    for (let i: number = 0; i < data.length; i++) {
      for (let j: number = 0; j <= row + 1; j++) {
        res += "\xa0";
      }
      res += `${stringfiy1(data[i], row + 1, false, i === data.length - 1)}`;
    }
    if (isVal) {
      for (let i: number = 0; i <= row; i++) {
        res += "\xa0";
      }
      if (!isLast) {
        res += "],\n";
      } else {
        res += "]\n";
      }
    } else {
      res += isLast ? "]\n" : "],\n";
    }
  }
  return res;
}
function stringify(data: object | number | string | Array<any>): string {
  return stringfiy1(data, 0, false, true);
}

console.log(
  stringify({
    a: 1,
    b: 2,
    c: [
      1,
      {
        sxx: 1,
        ssd: 2,
      },
    ],
    o1: {
      see: 1,
      sddsd: {
        sdsd: 323,
      },
    },
  })
);

实现一个简单的分页

相信只要是前端,肯定都用过分页组件,不管是自己开发的,还是使用的别人封装好的组件,那么面试中其实这个问题被问到的概率还是很大的。

我面试的时候,就会经常让候选人尝试着实现下一个分页组件😁

其实有时间也可以思考下自己怎么封装下分页组件,平时多思考总结,到了面试的时候,就会游刃有余。

面试其实就是一个搞定面试官的过程。

前几天有人提问了一个问题,某家公司的前端面试难不难?

其实有个答案回答的很好,你去面试,不要管面试难不难,面试有点类似考试,就算题目很难,你得了59分,只要其他人得了58分,那么你就通过了。

现在简化下问题,我们实现一个函数,输入总页数和当前页数,然后返回当前页的前三页和后三页,如果当前页前面没有三页,后面就多显示几页。

看下测试用例

1. 输入 7,2
返回 
{
    curr: 2,
    prev:[1],
    next:[3,4,5,6,7]
}

2. 输入 7, 4
返回
{
    curr: 4,
    prev:[1,2,3],
    next:[5,6,7]
}

3. 输入 7, 6
返回
{
    curr: 6,
    prev:[1,2,3,4,5],
    next:[7]
}

这里整体思路很简单,实现也很好实现,可以自己实现下,总体比较简单

function split(
  total: number,
  curr: number
): {
  prev: number[];
  curr: number;
  next: number[];
} {
  if (curr < 1 || curr > total || total < 1) {
    return;
  }
  const showNumber: number = 3;
  // const showLen: number = 3
  let prev: number[] = [];
  let preNumber: number = showNumber;
  if (total >= curr && total - curr < showNumber) {
    preNumber += showNumber - total + curr;
  } else if (curr <= showNumber) {
    preNumber = showNumber - curr + 1;
  }
  let curInex: number = curr;
  while (preNumber >= 1 && curInex > 1) {
    curInex--;
    preNumber--;
    prev.unshift(curInex);
  }

  let afterCurIndex: number = curr;
  let afterNumber: number = showNumber;
  if (curr <= showNumber) {
    afterNumber = showNumber - curr + 1 + showNumber;
  } else if (total - curr < showNumber) {
    afterNumber = total - curr;
  }

  let next: number[] = [];
  while (afterNumber > 0 && afterCurIndex < total && afterNumber > 0) {
    afterCurIndex++;
    afterNumber--;
    next.push(afterCurIndex);
  }

  // TODO
  return {
    prev: prev,
    curr,
    next: next,
  };
}

// test case
console.log(split(7, 2));
// {
//   curr: 2,
//   prev:[1],
//   next:[3,4,5,6,7]
// }
console.log(split(7, 4));
// {
//   curr: 4,
//   prev:[1,2,3],
//   next:[5,6,7]
// }
console.log(split(7, 6));
// {
//   curr: 6,
//   prev:[1,2,3,4,5],
//   next:[7]
// }

手写async和await

  1. 首先明确async和await是generator和Promise的语法糖,其实generator发布不久后就被async和await取代了。
  2. 明确我们需要实现什么? 我们需要一个函数,可以接受一个generator函数,当generator函数全部执行完之后,我们返回一个Promise函数,如果generator函数全部执行过程中出现错误,那么就reject,如果没有出现错误就resolve。
  3. 剩下的思路就很简单了,使用递归循环,知道generator函数执行完毕,或者出现错误。
function asyncFake(generatorFn) {
return function (...args) {
  const gen = generatorFn.apply(this, args);
  return new Promise((resolve, reject) => {
    function next(type, val) {
      // 
      const { done, value = null } = gen[type](val);
      if (done) {
        console.log(value);
        resolve(value);
      } else {
        Promise.resolve(value)
          .then((res) => {
            next("next", res);
          })
          .catch((err) => {
            reject(err);
          });
      }
    }
    // generator函数第一传参数没用
    next("next");
  });
};
}
const getData = () =>
new Promise((resolve) => setTimeout(() => resolve("333"), 1000));
function* testG() {
const data = yield getData();
console.log("data: ", data);
const data2 = yield getData();
console.log("data2: ", data2);
return "success";
}
const gen = asyncFake(testG);
gen().then((res) => console.log(123 + res));

根据树的前序遍历和中序遍历,还原一棵树

这个题目本身特别难,应该很容易就可以写出来。 这里难的地方在哪里呢?

  1. 首先会问你实现的时间复杂度是多少?
  2. 最坏的时间复杂度和最好的时间复杂度是在什么情况下发生,为什么? 这两个问题可以自己思考下😁

代码如下:

function TreeNode(val) {
  this.val = val;
  this.left = null;
  this.right = null;
}
function restoreTree(preArr, midArr) {
  if (preArr.length === 0) {
    return null;
  }
  const firstElement = preArr[0];
  const root = new TreeNode(firstElement);
  preArr.shift();
  const sliceIndex = midArr.indexOf(firstElement);
  let preMidArr = midArr.slice(0, sliceIndex);
  let afterMidArr = midArr.slice(sliceIndex + 1);
  const len = preMidArr.length;
  const prePreArr = preArr.slice(0, len);
  const afterPreArr = preArr.slice(len);
  root.left = restoreTree(prePreArr, preMidArr);
  root.right = restoreTree(afterPreArr, afterMidArr);
  return root;
}
console.log(restoreTree([1, 2, 4, 5, 3, 6, 7], [4, 2, 5, 1, 6, 3, 7]));

3sum

经典的3sum问题 一般正常情况下,不会直接给出3sum问题,一般都是先给你个2sum问题,如果你写的不错,可能就会给你继续3sum问题。 如果你2sum问题都没有搞定,建议还是得多锻炼下。

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

例1
输入: nums = [-1,0,1,2,-1,-4]
输出: [[-1,-1,2],[-1,0,1]]

例2
输入: nums = []
输出: []

基本思路就是采用二分搜索节省时间,不然可能无法AC

/**
 * @param {number[]} nums
 * @return {number[][]}
 */

var threeSum = function(nums) {
  const triplets = [];
  if (nums.length < 3) {
    return triplets;
  }
  nums.sort((a, b) => (a - b));
  for (let i = 0; i < nums.length - 2; i++) {
    let a = nums[i];
    if (nums[i - 1] !== undefined && nums[i] === nums[i - 1]) {
      continue;
    }
    let left = i + 1;
    let right = nums.length - 1;
    while (left < right) {
      let b = nums[left];
      let c = nums[right];
      let sum = a + b + c;
      if (sum === 0) {
        triplets.push([a, b, c]);
        while (nums[left] === nums[left + 1]) {
          left++;
        }
        while (nums[right] === nums[right - 1]) {
          right--;
        }
        left++;
        right--;
      } else if (sum > 0) {
        right--;
      } else {
        left++;
      }
    }
  }
  return triplets;
};