每个 JavaScript 工程师都应当知道的 10 个面试题

1,133 阅读20分钟
原文链接: segmentfault.com

原文链接:10 Interview Questions Every JavaScript Developer Should Know


对大部分公司来说,招聘技术人员这种事情,管理层就应该放手交给技术团队,只有他们才能够准确地判断应聘者的技术实力。如果你恰巧是应聘者,你也是迟早都要去面试的。不管你是哪边的,都让大哥来教你几招。

大兄弟们,要收藏,也要点赞呐。

以人为本

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 如此与众不同?

下面几个问题,也许能帮你一探究竟。


1. 能说出来两种对于 JavaScript 工程师很重要的编程范式么?

JavaScript 是一门多范式(multi-paradigm)的编程语言,它既支持命令式(imperative)/面向过程(procedural)编程,也支持面向对象编程(OOP,Object-Oriented Programming),还支持函数式编程(functional programming)。JavaScript 所支持的面向对象编程包括原型继承(prototypal inheritance)。

面试加分项

  • 原型继承(即:原型,OLOO——链接到其它对象的对象);
  • 函数式编程(即:闭包(closure),一类函数(first class functions),lambda 函数:箭头函数)。

面试减分项

  • 连范式都不知道,更别提什么原型 OO(prototypal oo)或者函数式编程了。

深入了解

2. 什么是函数式编程?

函数式编程,是将数学函数组合起来,并且避免了状态共享(shared state)及可变数据(mutable data),由此而产生的编程语言。发明于 1958 年的 Lisp 就是首批支持函数式编程的语言之一,而 λ 演算(lambda calculus)则可以说是孕育了这门语言。即使在今天,Lisp 这个家族的编程语言应用范围依然很广。

函数式编程可是 JavaScript 语言中非常重要的一个概念(它可是 JavaScript 的两大支柱之一)。ES5 规范中就增加了很多常用的函数式工具。

面试加分项

  • 纯函数(pure functions)/函数的纯粹性(function purity)
  • 知道如何避免副作用(side-effects)
  • 简单函数的组合
  • 函数式编程语言:Lisp,ML,Haskell,Erlang,Clojure,Elm,F#,OCaml,等等
  • 提到了 JavaScript 语言中支持函数式编程(FP)的特性:一类函数,高阶函数(higher order functions),作为参数(arguments)/值(values)的函数

面试减分项

  • 没有提到纯函数,以及如何避免副作用
  • 没有提供函数式编程语言的例子
  • 没有说是 JavaScript 中的哪些特性使得函数式编程得以实现

深入了解

3. 类继承和原型继承有什么区别?

类继承(Class Inheritance):实例(instances)由类继承而来(类和实例的关系,可以类比为建筑图纸和实际建筑 🏠 的关系),同时还会创建父类—子类这样一种关系,也叫做类的分层分类(hierarchical class taxonomies)。通常是用 new 关键字调用类的构造函数(constructor functions)来创建实例的。不过在 ES6 中,要继承一个类,不用 class 关键字也可以。

原型继承(Prototypal Inheritance):实例/对象直接从其它对象继承而来,创建实例的话,往往用工厂函数(factory functions)或者 Object.create() 方法。实例可以从多个不同的对象组合而来,这样就能选择性地继承了。

在 JavaScript 中,原型继承比类继承更简单,也更灵活。

面试加分项

  • 类:会创建紧密的耦合,或者说层级结构(hierarchies)/分类(taxonomies)。
  • 原型:提到了衔接继承(concatenative inheritance)、原型委托( prototype delegation)、函数继承(functional inheritance),以及对象组合(object composition)。

面试减分项

  • 原型继承和组合,与类继承相比,不知道哪个更好。

深入了解

4. 函数式编程和面向对象编程,各有什么优点和不足呢?

面向对象编程的优点:关于“对象”的一些基础概念理解起来比较容易,方法调用的含义也好解释。面向对象编程通常使用命令式的编码风格,声明式(declarative style)的用得比较少。这样的代码读起来,像是一组直接的、计算机很容易就能遵循的指令。

面向对象编程的不足:面向对象编程往往需要共享状态。对象及其行为常常会添加到同一个实体上,这样一来,如果一堆函数都要访问这个实体,而且这些函数的执行顺序不确定的话,很可能就会出乱子了,比如竞争条件(race conditions)这种现象(函数 A 依赖于实体的某个属性,但是在 A 访问属性之前,属性已经被函数 B 修改了,那么函数 A 在使用属性的时候,很可能就得不到预期的结果)。

函数式编程的优点:用函数式范式来编程,就不需要担心共享状态或者副作用了。这样就避免了几个函数在调用同一批资源时可能产生的 bug 了。拥有了“无参风格”(point-free style,也叫隐式编程)之类的特性之后,函数式编程就大大简化了,我们也可以用函数式编程的方式来把代码组合成复用性更强的代码了,面向对象编程可做不到这一点。

函数式编程更偏爱声明式、符号式(denotational style)的编码风格,这样的代码,并不是那种为了实现某种目的而需要按部就班地执行的一大堆指令,而是关注宏观上要做什么。至于具体应该怎么做,就都隐藏在函数内部了。这样一来,要是想重构代码、优化性能,那就大有可为了。(译者注:以做一道菜为例,就是由 买菜 -> 洗菜 -> 炒菜 这三步组成,每一步都是函数式编程的一个函数,不管做什么菜,这个流程都是不会变的。而想要优化这个过程,自然就是要深入每一步之中了。这样不管内部如何重构、优化,整体的流程并不会变,这就是函数式编程的好处。)甚至可以把一种算法换成另一种更高效的算法,同时还基本不需要修改代码(比如把及早求值策略(eager evaluation)替换为惰性求值策略(lazy evaluation))。

利用纯函数进行的计算,可以很方便地扩展到多处理器环境下,或者应用到分布式计算集群上,同时还不用担心线程资源冲突、竞争条件之类的问题。

函数式编程的不足:代码如果过度利用了函数式的编程特性(如无参风格、大量方法的组合),就会影响其可读性,从而简洁度有余、易读性不足。

大部分工程师还是更熟悉面向对象编程、命令式编程,对于刚接触函数式编程的人来说,即使只是这个领域的一些的简单术语,都可能让他怀疑人生。

函数式编程的学习曲线更陡峭,因为面向对象编程太普及了,学习资料太多了。相比而言,函数式编程在学术领域的应用更广泛一些,在工业界的应用稍逊一筹,自然也就不那么“平易近人”了。在探讨函数式编程时,人们往往用 λ 演算、代数、范畴学等学科的专业术语和专业符号来描述相关的概念,那么其他人想要入门函数式编程的话,就得先把这些领域的基础知识搞明白,能不让人头大么。

面试加分项

  • 共享状态的缺点、资源竞争、等等(面向对象编程)
  • 函数式编程能够极大地简化应用开发
  • 面向对象编程和函数式编程学习曲线的不同
  • 两种编程方式各自的不足之处,以及对代码后期维护带来的影响
  • 函数式风格的代码库,学习曲线会很陡峭
  • 面向对象编程风格的代码库,修改起来很难,很容易出问题(和水平相当的函数式风格的代码相比)
  • 不可变性(immutability),能够极大地提升程序历史状态(program state history)的可见性(accessible)和扩展性(malleable),这样一来,想要添加诸如无限撤销/重做、倒带/回放、可后退的调试之类的功能的话,就简单多了。不管是面向对象编程还是函数式编程,这两种范式都能实现不可变性,但是要用面向对象来实现的话,共享状态对象的数量就会剧增,代码也会变得复杂很多。

面试减分项

  • 没有讲这两种编程范式的缺点——如果熟悉至少其中一种范式的话,应该能够说出很多这种范式的缺点吧。

深入了解

总是你俩,看来你俩真是非常重要啊。

5. 什么时候该用类继承?

千万别用类继承!或者说尽量别用。如果非要用,就只用它继承一级(one level)就好了,多级的类继承简直就是反模式的。这个话题(不太明白是关于什么的……)我也参与讨论过好些年了,仅有的一些回答最终也沦为 常见的误解 之一。更多的时候,这个话题讨论着讨论着就没动静了。

如果一个特性有时候很有用
但有时候又很危险
并且还有另一种更好的特性可以用
务必要用另一种更好的特性
~ Douglas Crockford

面试加分项

  • 尽量别用,甚至是彻底不用类继承。
  • 有时候只继承一级的话也还是 OK 的,比如从框架的基类继承,例如 React.Component
  • 相比类继承,对象组合(object composition)更好一些。

深入了解

6. 什么时候该用原型继承?

原型继承可以分为下面几类:

  • 委托(delegation,也就是原型链)
  • 组合(concatenative,比如混用(mixins)、Object.assign()
  • 函数式(functional,这个函数式原型继承不是函数式编程。这里的函数是用来创建一个闭包,以实现私有状态(private state)或者封装(encapsulation))

上面这三种原型继承都有各自的适用场景,不过它们都很有用,因为都能实现组合继承(composition),也就是建立了 A 拥有特性 B(has-a)、A 用到了特性 B(uses-a) 或者 A 可以实现特性 B(can-do) 的这样一种关系。相比而言,类继承建立的是 A 就是 B 这样一种关系。

面试加分项

  • 知道在什么情况下不适合用模块化(modules)或者函数式编程。
  • 知道需要组合多个不同来源的对象时,应该怎么做。
  • 知道什么时候该用继承。

面试减分项

  • 不知道什么时候应该用原型。
  • 不知道混用和 Object.assign()

深入了解

7. 为什么说“对象组合比类继承更好”?

这句话引用的是《设计花纹》(Design Patterns,设计模式)这本书的内容。意思是要想实现代码重用,就应该把一堆小的功能单元组合成满足需求的各种对象,而不是通过类继承弄出来一层一层的对象。

换句话说,就是尽量编程实现 can-dohas-a 或者 uses-a 这种关系,而不是 is-a 这种关系。

面试加分项

  • 避免使用类继承。
  • 避免使用问题多多的基类。
  • 避免紧耦合。
  • 避免极其不灵活的层次分类(taxonomy)(类继承所产生的 is-a 关系可能会导致很多误用的情况)
  • 避免大猩猩香蕉问题(“你只是想要一根香蕉,结果最后却整出来一只拿着香蕉的大猩猩,还有整个丛林”)。
  • 要让代码更具扩展性。

面试减分项

  • 没有提到上面任何一种问题。
  • 没有表达清楚对象组合与类继承有什么区别,也没有提到对象组合的优点。

深入了解

8. 双向数据绑定/单向数据流的含义和区别

双向数据绑定(two-way data binding),意味着 UI 层所呈现的内容和 Model 层的数据动态地绑定在一起了,其中一个发生了变化,就会立刻反映在另一个上。比如用户在前端页面的表单控件中输入了一个值,Model 层对应该控件的变量就会立刻更新为用户所输入的值;反之亦然,如果 Modal 层的数据有变化,变化后的数据也会立刻反映至 UI 层。

单向数据流(one-way data flow), 意味着只有 Model 层才是单一数据源(single source of truth)。UI 层的变化会触发对应的消息机制,告知 Model 层用户的目的(对应 React 的 store)。只有 Model 层才有更改应用状态的权限,这样一来,数据永远都是单向流动的,也就更容易了解应用的状态是如何变化的。

采用单向数据流的应用,其状态的变化是很容易跟踪的,采用双向数据绑定的应用,就很难跟踪并理解状态的变化了。

面试加分项

  • React 是单向数据流的典型,面试时提到这个框架的话会加分。Cycle.js 则是另一个很流行的单向数据流的库。
  • Angular 则是双向数据绑定的典型。

面试减分项

  • 不理解单向数据流/双向数据绑定的含义,也说不清楚两者之间的区别。

深入了解

9. 单体架构和微服务架构各有何优劣?

采用单体架构(monolithic architecture)的应用,各组件的代码是作为一个整体存在的,组件之间互相合作,共享内存和资源。

而微服务架构(microservice architecture)则是由许许多多个互相独立的小应用组成,每个应用都有自己的内存空间,应用在扩容时也是独立于其它应用进行的。

单体架构的优势:大部分应用都有相当数量的横切关注点(cross-cutting concerns),比如日志记录,流量限制,还有审计跟踪和 DOS 防护等安全方面的需求,单体架构在这方面就很有优势。

当所有功能都运行在一个应用里的时候,就可以很方便地将组件与横切关注点相关联。

单体架构也有性能上的优势,毕竟访问共享内存还是比进程间通信(inter-process communication,IPC)要快的。

单体架构的劣势:随着单体架构应用功能的不断开发,各项服务之间的耦合程度也会不断增加,这样一来就很难把各项服务分离开来了,要做独立扩容或者代码维护也就更不方便了。

微服务的优势:微服务架构一般都有更好的组织结构,因为每项服务都有自己特定的分工,而且也不会干涉其它组件所负责的部分。服务解耦之后,想要重新组合、配置来为各个不同的应用提供服务的话,也更方便了(比如同时为 Web 客户端和公共 API 提供服务)。

如果用合理的架构来部署微服务的话,它在性能上也是很有优势的,因为这样一来,就可以很轻松地分离热门服务,对其进行扩容,同时还不会影响到应用中的其它部分。

微服务的劣势:在实际构建一个新的微服务架构的时候,会遇到很多在设计阶段没有预料到的横切关注点。如果是单体架构应用的话就很简单,新建一个中间件(shared magic helpers 不知道怎么翻译……)来解决这样的问题就行了,没什么麻烦的。

但是在微服务架构中就不一样了,要解决这个问题,要么为每个横切关注点都引入一个独立的模块,要么就把所有横切关注点的解决方案封装到一个服务层中,让所有流量都从这里走一遍就行了。

为了解决横切关注点的问题,虽然单体架构也趋向于把所有的路由流量都从一个外部服务层走一遍,但是在这种架构中,可以等到项目非常成熟之后再进行这种改造,这样就可以把还这笔技术债的时间尽量往后拖一拖。

微服务一般都是部署在虚拟机或容器上的,随着应用规模的不断增加,虚拟机抢工作(VM wrangling work)的情况也会迅速增加。任务的分配一般都是通过容器群(container fleet)管理工具来自动实现的。

面试加分项

  • 对于微服务的积极态度,虽然初始成本会比单体架构要高一些。知道微服务的性能和扩容在长期看来表现更佳。
  • 在微服务架构和单体架构应用上都有实战经验。能够使应用中的各项服务在代码层面互相独立,但是又可以在开发初期迅速地将各项服务打包成一整个的单体架构应用。微服务化的改造可以在应用相当成熟之后,改造成本在可承受范围内的时候再进行。

面试减分项

  • 不知道单体架构和微服务架构的区别。
  • 不知道微服务架构额外的开销,或者没有实际经验。
  • 不知道微服务架构中,IPC 和网络通信所导致的额外的性能开销。
  • 过分贬低微服务。说不清楚什么时候应该把单体架构应用解耦成微服务。
  • 低估了可独立扩容的微服务的优势。

10. 异步编程是什么?又为什么在 JavaScript 中这么重要?

在同步编程中,代码会按顺序自顶向下依次执行(条件语句和函数调用除外),如果遇到网络请求或者磁盘读/写(I/O)这类耗时的任务,就会堵塞在这样的地方。

在异步编程中,JS 运行在事件循环(event loop)中。当需要执行一个阻塞操作(blocking operation)时,主线程发起一个(异步)请求,(工作线程就会去执行这个异步操作,)同时主线程继续执行后面的代码。(工作线程执行完毕之后,)就会发起响应,触发中断(interrupt),执行事件处理程序(event handler),执行完后主线程继续往后走。这样一来,一个程序线程就可以处理大量的并发操作了。

用户界面(user interface,UI)天然就是异步的,大部分时间它都在等待用户输入,从而中断事件循环,触发事件处理程序。

Node.js 默认是异步的,采用它构建的服务端和用户界面的执行机制差不多,在事件循环中等待网络请求,然后一个接一个地处理这些请求。

异步在 JavaScript 中非常重要,因为它既适合编写 UI,在服务端也有上佳的性能表现。

面试加分项

  • 理解阻塞的含义,以及对性能带来的影响。
  • 理解事件处理程序,以及它为什么对 UI 部分的代码很重要。

面试减分项

  • 不熟悉同步、异步的概念。
  • 讲不清楚异步代码和 UI 代码的性能影响,也说不明白它俩之间的关系。

总结

多问问应聘者高层次的知识点,如果能讲清楚这些概念,就说明即使应聘者没怎么接触过 JavaScript,也能够在短短几个星期之内就把语言细节和语法之类的东西弄清楚。

不要因为应聘者在一些简单的知识上表现不佳就把对方 pass 掉,比如经典的 CS-101 算法课,或者一些解谜类的题目。

面试官真正应该关注的,是应聘者是否知道如何把一堆功能组织在一起,形成一个完整的应用。

电话面试的注意点就这些了,在线下的面试中,我更加关注应聘者实际编写代码的能力,我会观察他如何写代码。在我的《精通 JavaScript 面试》这个系列文章中,会有更深入的描述。