对大部分公司来说,招聘技术人员这种事情,管理层就应该放手交给技术团队,只有他们才能够准确地判断应聘者的技术实力。如果你恰巧是应聘者,你也是迟早都要去面试的。不管你是哪边的,都让大哥来教你几招。
大兄弟们,要收藏,也要点赞呐。
在 How to Build a High Velocity Development Team 一文中,我提出了一些观点,我觉得这些观点很重要,所以在这里再重复一遍:
优秀的团队才是决定公司业绩的关键,一家公司要想于逆境之中仍能有所建树,最重要的就是得先培养出一只优秀的团队。
就像 Marcus Lemonis 说的,有三点(3 个 P)最重要:
员工(People),流程(Process),产品(Product)。
在创业初期,你招来的工程师必须是能够独当一面的大神队友。他最好能够帮着招聘工程师,能指导其它工程师,还能帮初级和中级工程师解决各种问题。这样优秀的队友,无论何时都多多益善。
要想知道面试应聘者时,有哪些常见的注意事项,可以读读 Why Hiring is So Hard in Tech这篇文章。
要评估一个应聘者的真实水准,最佳方式就是结对编程(pair programming)。
和应聘者结对编程,一切都听应聘者的。多观察、多聆听,看看应聘者是个怎样的人。用微博的 API 抓取消息并显示在时间线上,就是个很好的考察应聘者的面试项目。
不过结对编程再好使,也没办法让你完全了解一个应聘者。这个时候,面试也能帮上很多忙——但是千万别浪费时间去问一些语法(syntax)或者语言上的细节(language quirks)——问些高端的问题吧,大兄弟。问问项目架构(architecture),编程范式(paradigms),这个层面上的判断(the big desicions)能够在很大程度上影响一个项目的成败。
语法和语言特性(features)这种小知识,Google 一搜一大把,谁都会。而工程师在工作中所积累的软件工程方面的经验,以及个人常用的编程范式及代码风格(idioms),这些可都是很难 Google 到的宝贵财富。
JavaScript 很独特,它在各种大型项目中都起着至关重要的作用。那是什么让 JavaScript 如此与众不同?
下面几个问题,也许能帮你一探究竟。
JavaScript 是一门多范式(multi-paradigm)的编程语言,它既支持命令式(imperative)/面向过程(procedural)编程,也支持面向对象编程(OOP,Object-Oriented Programming),还支持函数式编程(functional programming)。JavaScript 所支持的面向对象编程包括原型继承(prototypal inheritance)。
连范式都不知道,更别提什么原型 OO(prototypal oo)或者函数式编程了。
函数式编程,是将数学函数组合起来,并且避免了状态共享(shared state)及可变数据(mutable data),由此而产生的编程语言。发明于 1958 年的 Lisp 就是首批支持函数式编程的语言之一,而 λ 演算(lambda calculus)则可以说是孕育了这门语言。即使在今天,Lisp 这个家族的编程语言应用范围依然很广。
函数式编程可是 JavaScript 语言中非常重要的一个概念(它可是 JavaScript 的两大支柱之一)。ES5 规范中就增加了很多常用的函数式工具。
类继承(Class Inheritance):实例(instances)由类继承而来(类和实例的关系,可以类比为建筑图纸和实际建筑 ? 的关系),同时还会创建父类—子类这样一种关系,也叫做类的分层分类(hierarchical class taxonomies)。通常是用 new
关键字调用类的构造函数(constructor functions)来创建实例的。不过在 ES6 中,要继承一个类,不用 class
关键字也可以。
原型继承(Prototypal Inheritance):实例/对象直接从其它对象继承而来,创建实例的话,往往用工厂函数(factory functions)或者 Object.create()
方法。实例可以从多个不同的对象组合而来,这样就能选择性地继承了。
在 JavaScript 中,原型继承比类继承更简单,也更灵活。
原型继承和组合,与类继承相比,不知道哪个更好。
面向对象编程的优点:关于“对象”的一些基础概念理解起来比较容易,方法调用的含义也好解释。面向对象编程通常使用命令式的编码风格,声明式(declarative style)的用得比较少。这样的代码读起来,像是一组直接的、计算机很容易就能遵循的指令。
面向对象编程的不足:面向对象编程往往需要共享状态。对象及其行为常常会添加到同一个实体上,这样一来,如果一堆函数都要访问这个实体,而且这些函数的执行顺序不确定的话,很可能就会出乱子了,比如竞争条件(race conditions)这种现象(函数 A 依赖于实体的某个属性,但是在 A 访问属性之前,属性已经被函数 B 修改了,那么函数 A 在使用属性的时候,很可能就得不到预期的结果)。
函数式编程的优点:用函数式范式来编程,就不需要担心共享状态或者副作用了。这样就避免了几个函数在调用同一批资源时可能产生的 bug 了。拥有了“无参风格”(point-free style,也叫隐式编程)之类的特性之后,函数式编程就大大简化了,我们也可以用函数式编程的方式来把代码组合成复用性更强的代码了,面向对象编程可做不到这一点。
函数式编程更偏爱声明式、符号式(denotational style)的编码风格,这样的代码,并不是那种为了实现某种目的而需要按部就班地执行的一大堆指令,而是关注宏观上要做什么。至于具体应该怎么做,就都隐藏在函数内部了。这样一来,要是想重构代码、优化性能,那就大有可为了。(译者注:以做一道菜为例,就是由 买菜
-> 洗菜
-> 炒菜
这三步组成,每一步都是函数式编程的一个函数,不管做什么菜,这个流程都是不会变的。而想要优化这个过程,自然就是要深入每一步之中了。这样不管内部如何重构、优化,整体的流程并不会变,这就是函数式编程的好处。)甚至可以把一种算法换成另一种更高效的算法,同时还基本不需要修改代码(比如把及早求值策略(eager evaluation)替换为惰性求值策略(lazy evaluation))。
利用纯函数进行的计算,可以很方便地扩展到多处理器环境下,或者应用到分布式计算集群上,同时还不用担心线程资源冲突、竞争条件之类的问题。
函数式编程的不足:代码如果过度利用了函数式的编程特性(如无参风格、大量方法的组合),就会影响其可读性,从而简洁度有余、易读性不足。
大部分工程师还是更熟悉面向对象编程、命令式编程,对于刚接触函数式编程的人来说,即使只是这个领域的一些的简单术语,都可能让他怀疑人生。
函数式编程的学习曲线更陡峭,因为面向对象编程太普及了,学习资料太多了。相比而言,函数式编程在学术领域的应用更广泛一些,在工业界的应用稍逊一筹,自然也就不那么“平易近人”了。在探讨函数式编程时,人们往往用 λ 演算、代数、范畴学等学科的专业术语和专业符号来描述相关的概念,那么其他人想要入门函数式编程的话,就得先把这些领域的基础知识搞明白,能不让人头大么。
没有讲这两种编程范式的缺点——如果熟悉至少其中一种范式的话,应该能够说出很多这种范式的缺点吧。
总是你俩,看来你俩真是非常重要啊。
千万别用类继承!或者说尽量别用。如果非要用,就只用它继承一级(one level)就好了,多级的类继承简直就是反模式的。这个话题(不太明白是关于什么的……)我也参与讨论过好些年了,仅有的一些回答最终也沦为 常见的误解 之一。更多的时候,这个话题讨论着讨论着就没动静了。
如果一个特性有时候很有用 但有时候又很危险 并且还有另一种更好的特性可以用 那务必要用另一种更好的特性~ Douglas Crockford
React.Component
。原型继承可以分为下面几类:
Object.assign()
)上面这三种原型继承都有各自的适用场景,不过它们都很有用,因为都能实现组合继承(composition),也就是建立了 A 拥有特性 B(has-a)、A 用到了特性 B(uses-a) 或者A 可以实现特性 B(can-do) 的这样一种关系。相比而言,类继承建立的是 A 就是 B 这样一种关系。
Object.assign()
。Programming JavaScript Applications:文章中的“原型”这一节
这句话引用的是《设计花纹》(Design Patterns,设计模式)这本书的内容。意思是要想实现代码重用,就应该把一堆小的功能单元组合成满足需求的各种对象,而不是通过类继承弄出来一层一层的对象。
换句话说,就是尽量编程实现 can-do、has-a 或者 uses-a 这种关系,而不是 is-a 这种关系。
双向数据绑定(two-way data binding),意味着 UI 层所呈现的内容和 Model 层的数据动态地绑定在一起了,其中一个发生了变化,就会立刻反映在另一个上。比如用户在前端页面的表单控件中输入了一个值,Model 层对应该控件的变量就会立刻更新为用户所输入的值;反之亦然,如果 Modal 层的数据有变化,变化后的数据也会立刻反映至 UI 层。
单向数据流(one-way data flow), 意味着只有 Model 层才是单一数据源(single source of truth)。UI 层的变化会触发对应的消息机制,告知 Model 层用户的目的(对应 React 的 store
)。只有 Model 层才有更改应用状态的权限,这样一来,数据永远都是单向流动的,也就更容易了解应用的状态是如何变化的。
采用单向数据流的应用,其状态的变化是很容易跟踪的,采用双向数据绑定的应用,就很难跟踪并理解状态的变化了。
不理解单向数据流/双向数据绑定的含义,也说不清楚两者之间的区别。
Introduction to React.js
采用单体架构(monolithic architecture)的应用,各组件的代码是作为一个整体存在的,组件之间互相合作,共享内存和资源。
而微服务架构(microservice architecture)则是由许许多多个互相独立的小应用组成,每个应用都有自己的内存空间,应用在扩容时也是独立于其它应用进行的。
单体架构的优势:大部分应用都有相当数量的横切关注点(cross-cutting concerns),比如日志记录,流量限制,还有审计跟踪和 DOS 防护等安全方面的需求,单体架构在这方面就很有优势。
当所有功能都运行在一个应用里的时候,就可以很方便地将组件与横切关注点相关联。
单体架构也有性能上的优势,毕竟访问共享内存还是比进程间通信(inter-process communication,IPC)要快的。
单体架构的劣势:随着单体架构应用功能的不断开发,各项服务之间的耦合程度也会不断增加,这样一来就很难把各项服务分离开来了,要做独立扩容或者代码维护也就更不方便了。
微服务的优势:微服务架构一般都有更好的组织结构,因为每项服务都有自己特定的分工,而且也不会干涉其它组件所负责的部分。服务解耦之后,想要重新组合、配置来为各个不同的应用提供服务的话,也更方便了(比如同时为 Web 客户端和公共 API 提供服务)。
如果用合理的架构来部署微服务的话,它在性能上也是很有优势的,因为这样一来,就可以很轻松地分离热门服务,对其进行扩容,同时还不会影响到应用中的其它部分。
微服务的劣势:在实际构建一个新的微服务架构的时候,会遇到很多在设计阶段没有预料到的横切关注点。如果是单体架构应用的话就很简单,新建一个中间件(shared magic helpers 不知道怎么翻译……)来解决这样的问题就行了,没什么麻烦的。
但是在微服务架构中就不一样了,要解决这个问题,要么为每个横切关注点都引入一个独立的模块,要么就把所有横切关注点的解决方案封装到一个服务层中,让所有流量都从这里走一遍就行了。
为了解决横切关注点的问题,虽然单体架构也趋向于把所有的路由流量都从一个外部服务层走一遍,但是在这种架构中,可以等到项目非常成熟之后再进行这种改造,这样就可以把还这笔技术债的时间尽量往后拖一拖。
微服务一般都是部署在虚拟机或容器上的,随着应用规模的不断增加,虚拟机抢工作(VM wrangling work)的情况也会迅速增加。任务的分配一般都是通过容器群(container fleet)管理工具来自动实现的。
在同步编程中,代码会按顺序自顶向下依次执行(条件语句和函数调用除外),如果遇到网络请求或者磁盘读/写(I/O)这类耗时的任务,就会堵塞在这样的地方。
在异步编程中,JS 运行在事件循环(event loop)中。当需要执行一个阻塞操作(blocking operation)时,主线程发起一个(异步)请求,(工作线程就会去执行这个异步操作,)同时主线程继续执行后面的代码。(工作线程执行完毕之后,)就会发起响应,触发中断(interrupt),执行事件处理程序(event handler),执行完后主线程继续往后走。这样一来,一个程序线程就可以处理大量的并发操作了。
用户界面(user interface,UI)天然就是异步的,大部分时间它都在等待用户输入,从而中断事件循环,触发事件处理程序。
Node.js 默认是异步的,采用它构建的服务端和用户界面的执行机制差不多,在事件循环中等待网络请求,然后一个接一个地处理这些请求。
异步在 JavaScript 中非常重要,因为它既适合编写 UI,在服务端也有上佳的性能表现。
Object对象的defineProperty属性,重写data的set和get函数来实现的
,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
</div>
var app = new myVue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
},
}
})
function myVue(options) {
}
_init
属性:function myVue(options) {
this._init(options);
}
myVue.prototype._init = function (options) {
this.$options = options; // options 为上面使用时传入的结构体,包括el,data,methods
this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素
this.$data = options.data; // this.$data = {number: 0}
this.$methods = options.methods; // this.$methods = {increment: function(){}}
}
接下来实现 _obverse 函数,对data进行处理,重写data的set和get函数:
myVue.prototype._obverse = function (obj) { // obj = {number: 0}
var value;
for (key in obj) { //遍历obj对象
if (obj.hasOwnProperty(key)) {
value = obj[key];
if (typeof value === 'object') { //如果值还是对象,则遍历处理
this._obverse(value);
}
Object.defineProperty(this.$data, key, { //关键
enumerable: true,
configurable: true,
get: function () {
console.log(`获取${value}`);
return value;
},
set: function (newVal) {
console.log(`更新${newVal}`);
if (value !== newVal) {
value = newVal;
}
}
})
}
}
}
myVue.prototype._init = function (options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this._obverse(this.$data);
}
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名称,例如文本节点,该值设为"text"
this.el = el; //指令对应的DOM元素
this.vm = vm; //指令所属myVue实例
this.exp = exp; //指令对应的值,本例如"number"
this.attr = attr; //绑定的属性值,本例为"innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。
}
_init
函数以及 \_obverse
函数:myVue.prototype._init = function (options) {
//...
this._binding = {}; //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
//...
}
myVue.prototype._obverse = function (obj) {
//...
if (obj.hasOwnProperty(key)) {
this._binding[key] = { // 按照前面的数据,_binding = {number: _directives: []}
_directives: []
};
//...
var binding = this._binding[key];
Object.defineProperty(this.$data, key, {
//...
set: function (newVal) {
console.log(`更新${newVal}`);
if (value !== newVal) {
value = newVal;
binding._directives.forEach(function (item) { // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新
item.update();
})
}
}
})
}
}
}
_compile
函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。 myVue.prototype._init = function (options) {
//...
this._complie(this.$el);
}
myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.children.length) { // 对所有元素进行遍历,并进行处理
this._complie(node);
}
if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click');
return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致
})();
}
if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model');
//_this._binding['number']._directives = [一个Watcher实例]
// 其中Watcher.prototype.update = function () {
// node['vaule'] = _this.$data['number']; 这就将node的值保持与number一致
// }
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function() {
_this.$data[attrVal] = nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定
}
})(i));
}
if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可
var attrVal = node.getAttribute('v-bind');
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
<!DOCTYPE html>
<head>
<title>myVue</title>
</head>
<style>
#app {
text-align: center;
}
</style>
<body>
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
</div>
</body>
<script>
function myVue(options) {
this._init(options);
}
myVue.prototype._init = function (options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this._binding = {};
this._obverse(this.$data);
this._complie(this.$el);
}
myVue.prototype._obverse = function (obj) {
var value;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
this._binding[key] = {
_directives: []
};
value = obj[key];
if (typeof value === 'object') {
this._obverse(value);
}
var binding = this._binding[key];
Object.defineProperty(this.$data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`获取${value}`);
return value;
},
set: function (newVal) {
console.log(`更新${newVal}`);
if (value !== newVal) {
value = newVal;
binding._directives.forEach(function (item) {
item.update();
})
}
}
})
}
}
}
myVue.prototype._complie = function (root) {
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute('v-click')) {
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click');
return _this.$methods[attrVal].bind(_this.$data);
})();
}
if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model');
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function() {
_this.$data[attrVal] = nodes[key].value;
}
})(i));
}
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind');
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名称,例如文本节点,该值设为"text"
this.el = el; //指令对应的DOM元素
this.vm = vm; //指令所属myVue实例
this.exp = exp; //指令对应的值,本例如"number"
this.attr = attr; //绑定的属性值,本例为"innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp];
}
window.onload = function() {
var app = new myVue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
},
}
})
}
</script>
多问问应聘者高层次的知识点,如果能讲清楚这些概念,就说明即使应聘者没怎么接触过 JavaScript,也能够在短短几个星期之内就把语言细节和语法之类的东西弄清楚。
不要因为应聘者在一些简单的知识上表现不佳就把对方 pass 掉,比如经典的 CS-101 算法课,或者一些解谜类的题目。
面试官真正应该关注的,是应聘者是否知道如何把一堆功能组织在一起,形成一个完整的应用。
电话面试的注意点就这些了,在线下的面试中,我更加关注应聘者实际编写代码的能力,我会观察他如何写代码。