前端面试官如何从笔试中选拔人才
我没有参加过一线IT公司的笔试,但是我听说大公司都喜欢笔试,于是从网上搜了一些面试方面的题来试手。无论是笔试还是被笔试,有备无惧嘛。我个人倾向于javascript编程方向的开发,所以面试题自然也是选择这方面相关的。网上各类面试题的质量参差不齐,其中有一篇叫《如何面试前端工程师》的博文里边给的题型是我认为最好的。题量很少,但是考察的方向都基本上覆盖了。作者说他是个行业派(言下之意他也是写代码的),注重前端编程的能力。下面我就拿它的题来分析一下:
第一部分:Object Prototypes (对象原型)
题目是:定义一个方法,传入一个string类型的参数,然后将string的每个字符间加个空格返回,例如:
spacify('hello world') // => 'h e l l o w o r l d'
这题我认为是比较简单的,但是考虑到好的考题都是容易的在前,稍难的在中间,最难的在后面。这样布局是很合乎常理的。让面试者有一个热身过程,做到良好发挥。
因为做为面试官,不是要把面试者都难倒,证明你个人有多牛X,而是要选出合适的人才,为企业创造价值。好像说的有点远了,继续分析试题。
我一下子就想出好几个方法,但是保险起见,我用了这个:
function spacify(str) { return str.split('').join(' '); }
接下来,难度加大,如何把这个方法放入String对象上面,例如:
'hello world'.spacify();
显然这个问题是要考察侯选人是否对function prototypes(方法原型)有一个基本的理解。我把上面的方法简单改了一下就出来了:
String.prototype.spacify = function(){ //能否理解这里的this 是本题的关键 return this.split('').join(' '); };
我认为这题即考察了原型对象,又考察了this的经典用法,如果不是有多年的编程经验,是很难领会这一行代码的。当然,如果说你是背出来的,另当别论了。
第二部分:apply和call的用法
定义一个log函数,它可以代理console.log的方法。这个简单,再基础不过了:
function log(msg) { console.log(msg); }
我发现,越是简单的题,我越小心,总要多看几遍,身怕里边有什么坑在等着我跳,仔细看了三遍,确实是没有地雷.
接着题目难度加大,要求可以打印多个参数,参数个数不确定,温馨提示可以使用apply。
既然提示都这么明显了,用apply是没错的:
function log(){ console.log.apply(console, arguments); };
这里要注意的是,用apply的时候,第一个参数是log执行环境的this指向。第二个参数必须是数组,或者是arguments这样的类数组对象。对于这个题目来说,如果第一个参数用
this的话,指的是在window中去找log这个方法, 那不成了自己找自己啊。
接下来要给每一个log消息添加一个"(app)"的前辍,比如:
log('hello world'); //'(app) hello world'
现在可能有点麻烦了。好的侯选人知道arugments是一个伪数组,然后会将他转化成为标准数组。通常方法是使用Array.prototype.slice,像这样:
function log(){ var args = Array.prototype.slice.call(arguments); args.unshift('(app)'); console.log.apply(console, args); };
当然你也可以不用call或apply之类的方法实现,但是按照出题者的意思走,可以收到事半功倍的效果. 如果是技术官,他会认为你懂他!如果不是,他会认为你的答案很贴合“标准答案”。
第三部分:上下文
关于this的身份,一直以来都是各大面试题喜欢八卦的命题。这次也不例外,题目是一段代码:
var User = { count: 1, getCount: function() { return this.count; } };
下面几行,log输出的会是什么?
console.log(User.getCount()); var func = User.getCount; console.log(func());
对于一个javascript 编程的老手来说,这种情况下,正确的答案是1和undefined,一点都不会意外的,但是对于以前一直是从事套站写网页模版的候选人来说,就未必了。
当然,我在此并没有要黑这些资深网页制作者的意思。
那么问题来了,怎么让这个答案输出都是1呢?
正确的答案是使用Function.prototype.bind,例如:
var func = User.getCount.bind(User); console.log(func());
用bind?这些从ie5时代走过来的前辈们会觉得难以接受。这个方法对老版本的浏览器不起作用,好吧,那就顺道问下,这个怎么兼容这个bind好了。
聊到兼容,理论派或者说思路派就要开始滔滔不绝了,balabala...讲一大堆。他们甚至不屑于写这样的代码,认为太低估了他们的实力。
为了这篇文章的严谨和真实性,我特意拿到我所在的前端开发群里考察了一翻,注意,这个群里边有许多“XX神之类的人物”哦,当然也有我自己。
一起来看看它们的答案吧:
杭州的朋友给的答案:
这个暂且不说他写的对与错,这逻辑就有点让人眼晕,直接放弃。事后我采访他,原来是从网上copy的,这种不经过自己吸收的拿来主义,吃再多也是长不胖滴。
再看一位广州的朋友给的答案:
Function.prototype.binds=function(obj){ var fc=this; return function(){ fc.call(obj); } }
再看一位烟台的朋友:
有些人说起思路来涛涛不绝,但是写出来的代码却是漏洞百出。看起来很简单,却不一定做的很容易。同意的点个赞!
下面说下我的理由:
虽然广州的朋友修改过多次仍然没有全对,但是都是粗心或是考虑不全面,假以时间,还是可以胜任工作。
而烟台的那位虽然写的很简洁,也非常注意命名规范,思路也明确,但是Function.apply这一行,暴露了他基础的不足,需要学习的时间成本明显要多一些。当然如果悟性很好的话,另当别论。
以上纯属娱乐,请看到的朋友不要当真。
下面看下我的参考答案:
Function.prototype.bind = function(context){ var self = this; var arg = [].slice.call(arguments,1); return function(){ return self.apply(context,arg) } }
检测一下:
setTimeout(function(msg){ console.log(this.name,this.age,msg) }.bind({ name : 'frog', age : 18 },'blogs')) var User = { count: 1, getCount: function() { return this.count; } }; var func = User.getCount.bind(User); console.log(func());
上面的结果和使用bind的结是一致的,但是既然是做兼容,这样写,相当于忽略了那些本来就支持bind方法的浏览器了。得修改一下:
Function.prototype.bind = Function.prototype.bind || function(context){ var self = this; var arg = [].slice.call(arguments,1); return function(){ return self.apply(context, arg); }; }
这个地方,可以顺便提问关于闭包的问题。
函数式声明提升及没有块级作用域
这个是我自己加的,因为我在工作中,经常会遇到这样或那样的问题,多少都与这些东东粘点边。下面是一个例子:
function say(){ function name(){ console.log('frog') } return name(); function name(){ console.log('hello') } } var name = say();
输出的是hello;我承认,不会有人傻X到写这样无聊的代码。这个题的原型我不记得了,反正大致就是说明这样一个问题。当然关于这个例子你肯定还有更好的代码。例如:
function say(){ ok(); //这里会报错 var ok = function(){ console.log('ok') } } //---------------- function say(){ ok(); //这样就正常了 function ok(){ console.log('ok') } }
变量声明则不会被提升,函数式声明则会提升。再看一个例子:
var name = 'frog' function hello(){ alert(name); // undefined var name = 'bbc'; }
很多初学者都在这里翻船的。原因在于他们只记得没有块级作用域,但是没理解什么叫块级作用域。
在javascript中,函数是可以形成一个独立作用域的,变量的查找,首先是就近原则,先看自己有没有,自己没有,就会自动跑到外层去找,这一点和其它语言可能不一样,它会自动跑外边去找。在整个hello作用域内,只要定义了name这个变量,就不会去window中找,不过呢,在hello自己的作用域内,还有一个规则,申明之前调用,都是undefined,申明且赋值之后调用才会有值。alert(name)发生在申明之前,所以会弹出undefined就是这么个道理。
下面我再来说说这个没有块级作用域:
for(var i=0;i<10;i++){ //... } alert(i)//10
这个块,指的就是两个大括号之间的区域, 在javascript中,原本是不存在这个问题的,出现这个疑问,是由那些搞过c语言之类的人转来搞javascript带来的。他们以前的知识
中,循环之后,i自动销毁了,但是javascript中不是这样的。只要记得javascript中,函数才是划分作用域的就可以了。
类似的情况还有判断:
function fn(){if(true){ var i = 10 } return i; }
其他方面
当然这些问题只能覆盖前端一点点的知识的,还有很多其他的方面你有可能会问到,像性能,HTML5 API, AMD和CommonJS模块模型,构造函数(constructors),类型和盒子模型(box model)。以及热门的移动前端开发框架angula.js,spa应用,栅格布局, 异步编程问题,甚至可以问问算法,数据库什么的。只要给的起价,怎么折腾都行。