金三银四在今年彻底成了铜三铁四,不管在职的还是离职的,都在瑟瑟发抖。 看下这张图,应该会有一个很直观的感受😁
大环境你无力抵抗,能做的只能提高自己,躺平更不可能,穷逼是没资格躺平的😁。
既然无法躺平,只能做到人无我有,人有我优!
一道特别简单的数组map方法面试题
大家可以看下这段代码返回的结果是什么?为什么?
let a = [1,2,3];
a = a.map((item) => {
item +=2;
});
console.log(a);
常见的变量输出
- 可以看看这里输出什么? 为什么?
(function() {
var a = b = 3;
})()
console.log(b);
console.log(a);
- 这里输出什么?为什么会这样,是不是和你想的一致,如果不一致,可以想想为什么,是自己的思路哪里有问题?
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)
- 这是另外一道简单的输出题目,也可以思考下是不是和你想的输出一致?
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题目。
这里的思想很简单,就是采用递归。
递归在算法中是一个很重要的思想,很多算法都可以采用递归思想解决。如果想使用递归思想解决问题,可以记住以下两点方法,会事半功倍。
- 递归想清楚什么边界条件下退出。
- 可以写几个测试用例,从最简单的开始使用递归解决试试。
/**
* 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);
如果想要写的更完美一些,就会需要考虑一下一些特殊类型和特殊场景。
- 如果需要拷贝的对象里边有循环怎么办,类似于如下情况
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,实现效果如下,输入数组或者对象或者字符串或者数字,然后输出格式化的代码
类似如下
{
a: 1,
b: 2,
c: [
1,
{
sxx: 1,
ssd: 2
}
],
o1: {
see: 1,
sddsd: {
sdsd: 323
}
}
}
大家可以首先想下如何实现,自己实现试试,这里边的细节其实还是很多的,虽然实现思想很简单,但是里边的细节还是很多,最好是自己实现下。
这里主要难点:
- 需要处理好每行的缩进
- 需要在差不多二十分钟左右实现
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
- 首先明确async和await是generator和Promise的语法糖,其实generator发布不久后就被async和await取代了。
- 明确我们需要实现什么? 我们需要一个函数,可以接受一个generator函数,当generator函数全部执行完之后,我们返回一个Promise函数,如果generator函数全部执行过程中出现错误,那么就reject,如果没有出现错误就resolve。
- 剩下的思路就很简单了,使用递归循环,知道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));
根据树的前序遍历和中序遍历,还原一棵树
这个题目本身特别难,应该很容易就可以写出来。 这里难的地方在哪里呢?
- 首先会问你实现的时间复杂度是多少?
- 最坏的时间复杂度和最好的时间复杂度是在什么情况下发生,为什么? 这两个问题可以自己思考下😁
代码如下:
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;
};