你是JavaScript大师吗?试试这些面试题就知道了

你是JavaScript大师吗?试试这些面试题就知道了

原创: 无明 译 前端之巅 5月26日

作者|Toptal

译者|无明

编辑|覃云

与其他任何一项技术一样,对于 JavaScript 来说,总有人知其然,也有人知其所以然。以下的这些技术问题已经被证明可以用于找到真正的 JavaScript 语言大师。

   挑 战  

在当今的技术环境中,JavaScript 本质上已经成为客户端 Web 开发的代名词。而现在,随着 Node.js 等技术的出现,JavaScript 也正在成为一种占主导地位的服务器端技术。

因此,开发者简历上出现 JavaScript 经验的情况已经十分普遍。要找到 JavaScript 开发者非常容易,但是通过筛选来找到“少数精英”却是个有挑战的活。要找到这些“精英”,首先需要一个高效的招聘流程,然后,可以通过问他们问题——例如本文提出的这些问题——在全球范围内找到真正的 JavaScript 专家候选人。

我懂 JavaScript

与其他任何一项技术一样,对于 JavaScript 来说,总有人知其然,也有人知其所以然。在寻找真正的语言大师过程中,我们需要一个面试流程,用以准确量化候选人的 JavaScript 专业知识水平。

为了实现这一目标,本文提供了一些问题,这些问题对于评估候选人掌握 JavaScript 的广度和深度来说至关重要。不过要记住,这些示例问题可以作为一种指导,因为并非每个候选人都能正确回答所有的问题,而即使他们都能回答上来,也并不代表他们就是最好的候选人。归根结底,招聘仍然是一门艺术,一门科学。

评估语言基础

经常会遇到一些”经验丰富“的 JavaScript 开发者,他们的语言基础十分薄弱。

JavaScript 是一门基于原型的动态脚本语言。对于习惯了基于类的语言(如 Java 或 C ++)的开发者来说,JavaScript 在一开始可能会让他们感到些许的困惑,因为它是动态的,并且不提供传统的类实现。因此,经常会遇到一些”经验丰富“的 JavaScript 开发者,他们的语言基础要么很薄弱,要么对基础知识的理解十分含糊。

因此,评估开发者对 JavaScript 基础知识(包括一些细微的差别)的掌握程度是面试过程的重要组成部分。

问题:描述 JavaScript 中的继承和原型链,并举例子。

虽然 JavaScript 是一种面向对象的语言,但它是基于原型的,而且并没有提供传统的基于类的继承系统。

在 JavaScript 中,每个对象都会在内部引用一个叫作 prototype 的对象。这个原型对象也会引用自己的原型对象,并以此类推。原型链的末尾是一个以 null 为原型的对象。JavaScript 就是通过原型链机制来实现继承的——更确切地说是原型式的继承。当一个对象引用了不属于自己的属性时,将遍历原型链,直到找到引用的属性为止(或者直到找到链的末尾,这种情况说明该属性未定义)。

这里有一个简单的例子:

function Animal() { this.eatsVeggies = true; this.eatsMeat = false; }

function Herbivore() {}
Herbivore.prototype = new Animal();

function Carnivore() { this.eatsMeat = true; }
Carnivore.prototype = new Animal();

var rabbit = new Herbivore();
var bear = new Carnivore();

console.log(rabbit.eatsMeat);   // logs "false"
console.log(bear.eatsMeat);     // logs "true"

问题:比较 JavaScript 中的对象和哈希表。

这个问题有点像把戏,因为在 JavaScript 中,对象本质上就是哈希表,即键值对的集合。需要注意的是,在这些键值对中,键总是字符串,于是就有了接下来的这个问题。

问题:在下面的代码片段中,alert 将会显示什么?请解释你的答案。

var foo = new Object();
var bar = new Object();
var map = new Object();

map[foo] = "foo";
map[bar] = "bar";

alert(map[foo]);  // what will this display??

很少有候选人能够给出正确回答——“bar”。大多数人会错误地认为是“foo”,所以让我们来看看为什么“bar”才是正确答案。

正如上一个问题的答案中所提到的,JavaScript 对象本质上是键值对哈希表,其中名称(即键)总是字符串。事实上,当字符串以外的对象被用作键时,并不会发生错误,JavaScript 会隐式地将其转换为字符串,并将该值用作键。如上面的代码所示,但这可能会出现令人惊讶的结果。

要理解上面的代码,首先必须认识到,map 对象不会将对象 foo 映射到字符串“foo”,也不会将对象 bar 映射到字符串“bar”。由于对象 foo 和 bar 不是字符串,所以当它们用作 map 的键时,JavaScript 会自动调用每个对象的 toString() 方法将它们转换为字符串。既然 foo 和 bar 都没有定义自己的 toString() 方法,那么就使用默认的实现。默认的 toString() 方法在被调用时生成字符串“[object Object]”。现在,让我们重新检查上面的代码,不过这次带有注释:

var foo = new Object();
var bar = new Object();
var map = new Object();

map[foo] = "foo";    // --> map["[Object object]"] = "foo";
map[bar] = "bar";    // --> map["[Object object]"] = "bar";
                     // NOTE: second mapping REPLACES first mapping!

alert(map[foo]);     // --> alert(map["[Object object]"]);
                     // and since map["[Object object]"] = "bar",
                     // this will alert "bar", not "foo"!!
                     //    SURPRISE! ;-)

问题:请解释 JavaScript 中的闭包。什么是闭包?它们有什么独特的特性?你如何以及为什么要使用它们?请举一个例子。

闭包是一个函数,包含在创建闭包时处于作用域内的所有变量或其他函数。在 JavaScript 中,闭包通过“内部函数”的形式来实现,也就是在另一函数的主体内定义的函数。这是一个简单的例子:

(function outerFunc(outerArg) {
  var outerVar = 3;

  (function middleFunc(middleArg) {
    var middleVar = 4;

    (function innerFunc(innerArg) {
      var innerVar = 5;
      // EXAMPLE OF SCOPE IN CLOSURE:
      // Variables from innerFunc, middleFunc, and outerFunc,
      // as well as the global namespace, are ALL in scope here.
      console.log("outerArg="+outerArg+
                  " middleArg="+middleArg+
                  " innerArg="+innerArg+"\n"+
                  " outerVar="+outerVar+
                  " middleVar="+middleVar+
                  " innerVar="+innerVar);
      // --------------- THIS WILL LOG: ---------------
      //    outerArg=123 middleArg=456 innerArg=789
      //    outerVar=3 middleVar=4 innerVar=5
    })(789);
  })(456);
})(123);

闭包的一个重要特性是,即使是在外部函数返回后,内部函数仍然可以访问外部函数的变量。这是因为,在 JavaScript 中,当函数被执行时,它们仍然使用创建函数时有效的作用域。

然而,如果内部函数在被调用时(而不是在创建时)访问外部函数变量的值,就会导致混淆。为了测试候选人对此细微差别的理解,请使用以下代码片段,它将动态创建五个按钮,并问候选人当用户单击第三个按钮时将显示什么内容:

function addButtons(numButtons) {
  for (var i = 0; i < numButtons; i++) {
    var button = document.createElement('input');
    button.type = 'button';
    button.value = 'Button ' + (i + 1);
    button.onclick = function() {
      alert('Button ' + (i + 1) + ' clicked');
    };
    document.body.appendChild(button);
    document.body.appendChild(document.createElement('br'));
  }
}

window.onload = function() { addButtons(5); };

很多人会错误地回答,当用户点击第三个按钮时,会显示“Button 3 clicked”。实际上,上面的代码包含了一个错误(基于对 closure 的误解),当用户点击五个按钮中的任何一个,都将显示“Button 6 clicked”。这是因为,在调用 onclick 方法时(对于任意一个按钮),for 循环已经完成并且变量 i 的值已经是 5。

接下来可以问候选人如何解决上述代码中的错误,以便产生预期的行为(即点击按钮 n 将显示“Button n clicked”)。如果候选人能给出正确答案,说明他们懂得如何正确使用闭包,如下所示:

function addButtons(numButtons) {
  for (var i = 0; i < numButtons; i++) {
    var button = document.createElement('input');
    button.type = 'button';
    button.value = 'Button ' + (i + 1);
    // HERE'S THE FIX:
    // Employ the Immediately-Invoked Function Expression (IIFE)
    // pattern to achieve the desired behavior:
    button.onclick = function(buttonIndex) {
      return function() {
        alert('Button ' + (buttonIndex + 1) + ' clicked');
      };
    }(i);
    document.body.appendChild(button);
    document.body.appendChild(document.createElement('br'));
  }
}

window.onload = function() { addButtons(5); };

对于许多现代 JavaScript 编程模式来说,闭包是一个特别有用的组件。一些最流行的 JavaScript 库也在广泛地使用闭包,如 jQuery 和 Node.js.

拥抱多样性

在 JavaScript 中可以采用各种各样的编程技术和设计模式,一个真正的 JavaScript 大师在选择一个方法时会知道它的重要性和影响。

作为多范式语言,JavaScript 支持面向对象、命令式和函数式编程风格。因此,JavaScript 提供了异常广泛的编程技术和设计模式。JavaScript 大师将会清楚地知道这些模式的存在,更重要的是,他们知道在选择一种方案时将带来哪些意义和影响。以下是一些可以用于衡量候选人专业知识维度的问题。

问题:描述创建对象的不同方式及其各自的影响,并提供示例。

下面的图片列出了在 JavaScript 中用于创建对象的各种方式,以及每种方式产生的原型链差异。

问题:将函数定义为函数表达式(例如 var foo = function(){})或定义为函数语句(例如 function foo(){})是否存在实际差异?请解释你的答案。

是的,根据函数的赋值方式和时间,它们之间存在不同。

在使用函数语句(例如 function foo(){})时,函数 foo 可以在定义之前被引用(通过“hoisting”技术)。hoisting 技术带来的结果是,函数的最后一个定义将被使用,不管它什么时候被引用(如果还不明白,下面的示例代码应该有助于澄清这个问题)。

相比之下,在使用函数表达式(例如 var foo = function(){})时,函数 foo 在定义之前不能被引用,就像任何其他赋值语句一样。因此,函数的最新定义将会被使用(因此定义必须位于引用之前,否则函数就是未定义的)。

这里有一个简单的例子来证明两者之间的实际区别。考虑下面的代码片段:

function foo() { return 1; }

alert(foo());   // what will this alert?

function foo() { return 2; }

许多 JavaScript 开发者会错误地认为,代码中的 alert 将显示“1”,但实际上,他们会惊讶地发现,它实际上显示的是“2”。如上所述,这是由于 hoisting 导致的。由于使用了函数语句来定义函数,函数的最后一个定义是在调用它时被“提升”的那个(即使它在代码中位于调用语句之后)。

现在考虑下面的代码片段:

var foo = function() { return 1; }

alert(foo());   // what will this alert?

foo = function() { return 2; }

在这种情况下,答案会更直观,alert 将按预期显示“1”。由于使用函数表达式来定义函数,函数的最新定义是在调用函数时使用的那个。

隐藏在细节中的魔鬼

除了迄今为止讨论的 JavaScript 高级概念外,还有一些底层的语法细节,这些都是一个真正 JavaScript 大师应该要懂的。这里有一些例子。

问题:将 JavaScript 源文件的全部内容封装在一个函数中,这样做的重要性和原因是什么?

这是一种日益普遍的做法,被许多流行的 JavaScript 库(jQuery、Node.js 等)所采用。这将为文件的全部内容创建一个闭包,也就是创建了一个私有命名空间,有助于避免不同 JavaScript 模块和库之间潜在的命名冲突。

这种技术的另一个特点是为全局变量提供一个容易引用(可能更短)的别名。例如,jQuery 插件通常会使用这种技术。jQuery 允许通过 jQuery.noConflict() 来禁用对 jQuery 命名空间 $ 的引用。如果是这样,你的代码仍然可以通过 $ 来使用闭包,如下所示:

(function($) { /* jQuery plugin code referencing $ */ } )(jQuery);

问题:== 和 === 有什么区别?!= 和!== 又有什么区别?请举个例子。

JavaScript 中的“三重”比较运算符(=== 和!==)和双重比较运算符(== 和!=)之间的区别在于,双重比较运算符在对比之前会对操作数执行隐式类型转换,而使用三重比较运算符时不进行类型转换(即值必须相等且类型必须相同才能相等)。

一个简单的例子,表达式 123=='123'将输出 true,而 123==='123'则输出 false。

问题:在 JavaScript 源文件的开头包含“use strict”的意义是什么?

关于这个问题有很多需要说明的内容,不过为了简单起见,这里只给出最重要的答案,即使用 use strict 是一种自动在运行时对 JavaScript 代码执行严格解析和错误处理的方法。原先被忽略的代码问题在这个时候会产生错误或抛出异常。总的来说,这是一个很好的做法。

 总 结  

JavaScript 可能是当今最容易被误解和低估的编程语言之一。剥开 JavaScript 这颗洋葱越多,就越是能意识到各种可能性。因此,找到真正的 JavaScript 语言大师是一个巨大的挑战。我们希望在寻找 JavaScript 开发者中的“少数精英”时,本文所提出的问题能够帮你“将麦子从糠中分离出来”

猜你喜欢

转载自blog.csdn.net/u012207345/article/details/81433481