精通 JavaScript 面试

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

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

以人为本

在 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)或者函数式编程了。

深入了解
  • The Two Pillars of JavaScript Part 1:JS 两大支柱之一:原型 OO
  • The Two Pillars of JavaScript Part 2:JS 两大支柱之二:函数式编程

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 中的哪些特性使得函数式编程得以实现
深入了解
  • The Two Pillars of JavaScript Part 2:JS 两大支柱之二:函数式编程
  • The Dao of Immutability
  • Composing Software
  • The Haskell School of Music

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)。
面试减分项

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

深入了解
  • The Two Pillars of JavaScript Part 1:JS 两大支柱之一:原型 OO
  • Common Misconceptions About Inheritance in JavaScript:对于 JavaScript 中继承这个概念,所普遍存在的误解

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),这样一来,想要添加诸如无限撤销/重做、倒带/回放、可后退的调试之类的功能的话,就简单多了。不管是面向对象编程还是函数式编程,这两种范式都能实现不可变性,但是要用面向对象来实现的话,共享状态对象的数量就会剧增,代码也会变得复杂很多。
面试减分项

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

深入了解

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

  • The Two Pillars of JavaScript Part 1:JS 两大支柱之一:原型 OO
  • The Two Pillars of JavaScript Part 2:JS 两大支柱之二:函数式编程

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

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

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

面试加分项
  • 尽量别用,甚至是彻底不用类继承。
  • 有时候只继承一级的话也还是 OK 的,比如从框架的基类继承,例如 React.Component
  • 相比类继承,对象组合(object composition)更好一些。
深入了解
  • The Two Pillars of JavaScript Part 1:JS 两大支柱之一:原型 OO
  • JS Objects — Inherited a Mess:JS 对象(继承)——只是继承了混乱(mess)而已

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()
深入了解

Programming JavaScript Applications:文章中的“原型”这一节

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

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

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

面试加分项
  • 避免使用类继承。
  • 避免使用问题多多的基类。
  • 避免紧耦合。
  • 避免极其不灵活的层次分类(taxonomy)(类继承所产生的 is-a 关系可能会导致很多误用的情况)
  • 避免大猩猩香蕉问题(“你只是想要一根香蕉,结果最后却整出来一只拿着香蕉的大猩猩,还有整个丛林”)。
  • 要让代码更具扩展性。
面试减分项
  • 没有提到上面任何一种问题。
  • 没有表达清楚对象组合与类继承有什么区别,也没有提到对象组合的优点。
深入了解
  • Composition over Inheritance
  • Introducing the Stamp Specification

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 则是双向数据绑定的典型。
面试减分项

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

深入了解

Introduction to React.js

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 代码的性能影响,也说不明白它俩之间的关系。
  • 在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理。本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧。结合注释,希望能让大家有所收获。
  • 1、原理
  • Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。
  • 添加网上的一张图
  • 2、实现
  • 页面结构很简单,如下:
<div id="app">
 <form>
 <input type="text"  v-model="number">
 <button type="button" v-click="increment">增加</button>
 </form>
 <h3 v-bind="number"></h3>
 </div>
  • 包含:
  • 一个input,使用v-model指令
  • 一个button,使用v-click指令
  • 一个h3,使用v-bind指令。
  • 我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释:
var app = new myVue({
      el:'#app',
      data: {
        number: 0
 },
      methods: {
        increment: function() {
 this.number ++;
 },
 }
 })
  • 首先我们需要定义一个myVue构造函数:
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函数:
  • 并改造_init函数
 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);
 }
  • 接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新:
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();
 })
 }
 }
 })
 }
 }
 }
  • 那么如何将view与model进行绑定呢?接下来我们定义一个 _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'
 ))
 }
 }
 }
  • 至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图:
  • 附上全部代码,不到150行。
<!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 算法课,或者一些解谜类的题目。

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

电话面试的注意点就这些了,在线下的面试中,我更加关注应聘者实际编写代码的能力,我会观察他如何写代码。

本文分享自微信公众号 - 李才哥(liqi13695515224)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程一生

代码荣辱观-以运用风格为荣,以随意编码为耻

对开发人员来说,需要在时间允许的条件下定期的review自己和别人的代码,加深对项目的整体理解。对自己的成长做总结。如果过了一段时间,还看到自己之前的代码,觉得...

10420
来自专栏编程坑太多

『互联网架构』软件架构-Spring boot集成日志框架(89)

在spring-boot-starter 依赖中,添加了 spring-boot-starter-logging依赖

13320
来自专栏子曰五溪

让天下没有难用的Node.js

每次阅读到这句话时总能想到我们阿里巴巴的使命“让天下没有难做的生意”,而“让天下没有难用的Node.js”我猜这应该就是阿里的前端们,对Node.js大规模应用...

10920
来自专栏一个会写诗的程序员的博客

JDK 8 中的LocalDate不符合正常逻辑的解析报错: Unable to obtain LocalDateTime from TemporalAccessor: {Year=2019, ...

I am simply trying to convert a date string into a DateTime object in Java 8. Up...

72440
来自专栏北京宏哥

jenkins手把手教你从入门到放弃02-jenkins在Windows系统安装与配置(详解)

  上一篇对jenkins有了大致了解之后,那么我们就开始来安装一下jenkins。

35760
来自专栏SpringBoot

java 画图片

15820
来自专栏老码农专栏

"轻"量级 Java Web 服务框架漫谈

时光回到 2000 年中, 老码农坐在天津河川大厦 7 楼接手平生第一个 Web 服务项目, 采用的是最新(当年)的 Java Servlet 技术, 倒腾着精...

21830
来自专栏秃头哥编程

千万不要相信程序员在加班时间写的代码!

作为一个最底层的程序员,我先记录一些只有底层程序员才会知道的事情。如果多年后,我违背自己进入这个行业的初心,走上管理岗位,也能回想起一些禁忌,避免一些错误。

15810
来自专栏秃头哥编程

哥们,你真以为你会做这道JVM面试题?

我这里捞出一道code题要各位大佬来把玩把玩,如果你一眼就看出了端倪,那么恭喜你,你可以下山了:

10210
来自专栏Java程序猿部落

面试官问你:MYSQL事务和隔离级别,该如何回答

事务是由一组SQL语句组成的逻辑处理单元,是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。事务具...

10620

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励