考的不是算法,而是前端基础
最近三个月一直在频繁面试前端、java后端、客户端研发还是算法岗,为了提高面试的效率,我一直再优化自己面试的流程和技巧,而不像传统面试那样,把算法和基础割裂开来。
算法一: 实现斐波那契数列
我很惊奇地发现,前来面试的同学,不敢是前端还是后端,还有客户端的研发,居然有一半以上不了解本题,可能是因为996,大家忙于业务研发而忽略了基础吧。
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为兔子数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34。
在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)。
只要了解概念的面试者,都会很容易写出递归的实现:
function fib(n){
if(n==1||n==2){
return 1;
}
return fib(n-1)+fib(n-2);
}
即使不了解该数列的概念,稍加提示,多数也可以写出来,但作为面试,不仅仅如此,我一般会进一步提醒让其用更简洁的方式去实现,核心算法要求只能用一个语句,以考察其对三目运算符的使用:
function fib(n){
return n < 3 ? 1 : fib(n-1)+fib(n-2);
}
当然我一般在一开始就要求面试者一句话去实现:
const fib = n => n < 3 ? 1 : fib(n-1)+fib(n-2);
接着我会通过问fib(n-1)+fib(n-2) 需不要要加(),来考察他对自己代码的信心和对运算优先级的掌握程度,一般经验不足的JS研发都会犹豫,不能确认需不需要加括号,以及加好还是不加好,好在哪等。
对于这个简单的算法,答到以上程度,还不够,这时我会继续问他,还有没有问题,会通过代码健壮性和异常处理进行适当提醒,看看面试者是否有意识需要对输入值n 进行判断。显然,n必须为正整数,此时就可以通过确保n为正整数,来考察typeof判断一个JS变量类型,以及JS中浮点数的注意点等。
function fib(n){
if(typeof n === 'number' && n > 0 && parseInt(n) === n ) {
return n < 3 ? 1 : fib(n-1)+fib(n-2);
} else {
return '输入不合法,请输入正整数!';
}
}
确保输入n为正整数的方方法有很多,有些想通过indexOf('.')来判断,但是很多还是考虑不周全,单独使用它是不行的,因为会过滤掉 1.000, 等情况,我们期待1.000,也是可以。
提高浮点数后,我会接着问 0.1 + 0.2 === 0.3 的问题,一般多数前端都知道。
第一道算法题,不仅考查了JS中最基础的语法和变量,同时也为下一道算法题埋下了伏笔,聪明有潜力的面试者,一般都会举一反三,在下一题中能考虑语义化和异常的处理。
算法二: 数组两两之差最大值
本题的时间复杂度要求,一般也会被一般面试者忽略,上来就排序,这时我就会问题各种常用排序算法的时间复杂度,最后发现不满足我的要求,这种上来不分析题直接开干的面试者,工作后就特别容易一接到需求就开发,最后发现不是产品需要的。
有些聪明的面试者,很快就能把问题转化为找到一个数组中的最大值和最小值,因此只需遍历一遍数组即可,满足时间复杂度O(n)的要求。
function maxDiffInArray(arr){
let max = arr[0], min = arr[0];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < min) { min = arr[i]; }
if (arr[i] > max) { max = arr[i]; }
}
return max - min;
}
这道题,我会结合面试者的工作经验,问一下let const var的区别,以及作用域和闭包等常考问题,同时我也会进一步提示让他用更少的语句去实现。
function maxDiffInArray(arr){
let max = arr[0], min = arr[0];
arr.map(item => max = Math.max(item, max), min = Math.min(item, min));
return max - min;
}
在面试者按照我的提升实现了以后,我会进一步问遍历数组的方法有哪些,同时还会问可不可以使用遍历对象的方法遍历数组,会存在什么问题等,也会考察其对ES中箭头函数的掌握程度,由箭头函数引出argument对象,让面试者 辨析 普通对象、伪数组、数组的区别和联系,对于资深的前端,我还会考察 new 一个对象的内部原理,同时也会通过为什么ES6引入箭头函数,去考察一些this指向相关的问题,越资深,问得越深。
考察了到上面的程度,其实还不够,我会再次提醒,怎么保证代码和健壮性和异常情况怎么处理。显然,本题需要确保输入的是合法的数组。
function maxDiffInArray(arr){
if ( arr instanceof Array) {
if (arr.length) {
let max = arr[0], min = arr[0];
for (let i = 0; i < arr.length; i++) {
if ( typeof arr[i] !== 'number' ) {
return '输入不合法,数组存在非数字类型元素,请输入数字数组';
}
max = Math.max(arr[i], max), min = Math.min(arr[i], min);
}
return max - min;
} else {
return '输入不合法,数组为空,请输入非空数组';
}
} else {
return '输入不合法,请输入非空数组';
}
}
其中,我们考察面试者如何判断一个变量为数组的几种常用方法和需要注意的东西,同时也会考察面试者是否知道哪些遍历数组的方法是不能跳出遍历的,对于资深的面试者,我还会考查一下迭代器的知识点。
算法三: 实现任意项异步累加
const add = (x, y) => {
return new Promise((resolve) => {
setTimeout(() => resolve(x + y), 1000)
});
}
async function sum () {
const arr = [...arguments];
// todo: 异常处理,确保入参为数字类型
if(arr.length === 1) {
return arr[0];
}
let pArr = [], i = 0, j = arr.length - 1;
for ( let i = 0; i < j; i++, j--) {
pArr.push(add(arr[i], arr[j]));
}
let newArr = await Promise.all(pArr);
if(arr.length % 2 === 1) {
newArr.push(arr[(arr.length-1) / 2]);
}
return sum(...newArr);
}
async function sum2 () {
const arr = [...arguments];
// todo: 异常处理,确保入参为数字类型
if(arr.length === 1) {
return arr[0];
}
let newArr = [], i = 0, j = arr.length - 1;
for ( let i = 0; i < j; i++, j--) {
const res = await add(arr[i], arr[j]);
newArr.push(res);
}
if(arr.length % 2 === 1) {
newArr.push(arr[(arr.length-1) / 2]);
}
return sum2(...newArr);
}
(async function () {
let time = new Date();
const res = await sum(1, 2, 3, 4, 5, 6, 7, 8, 9);
console.log('1111', res, new Date - time)
time = new Date();
const res2 = await sum2(1, 2, 3, 4, 5, 6, 7, 8, 9);
console.log('2222', res2, new Date - time)
})()
总结
通过以上三道算法题,大概一个小时左右,我们就能相对全面地考察该面试者的JS基础,编程的逻辑性,思维的缜密度,以及编码习惯,也能判断出该同志的性格特征,甚至也能考察其英文的词汇量。
只有上面的考察回答得八九不离十,我才会进一步考察H5和CSS的东西,框架和项目经验一般都是5分钟内了解一下即可。同时也比较注重其对前端或客户端技术的掌握程度,看看其有没有博客和github,从其自我驱动的学习和自学的知识技能,来推断其潜力!
以上只是我面试前端的案例,不同人我会考不同的算法,不同的方向我会让大家选择不同的语言实现,结合不同岗位聊各自技术栈的相关知识。
另外,整个面试我也希望体现出自己的特色,不仅给面试者留下深刻的印象,而且能帮助有缘人养成编码时注重代码可用性、健壮性、可读性和简洁性的良好习惯。