在IT界中,JavaScript开发人员的需求量一直居高不下。如果你的能力能够胜任这一角色,那么你有很多机会换一家公司,并提高薪水。但在你被一家公司聘用之前,你必须展示你的技能,以通过面试环节。在本文中,我将向您展示10个 JavaScript 技能典型问题以及其相关解决方案,来面试前端工程师。它们很有意思!

问题1: 作用域(Scope)

考虑以下代码:

JavaScript 代码: 
(function() {
var a = b = 5;
})();console.log(b);
控制台(console)会打印出什么?
1
2
3
4
5
6
7


答案

上述代码会打印出5。

这个问题的陷阱就是,在立即执行函数表达式(IIFE)中,有两个赋值,但是其中变量a使用关键词var来声明。这就意味着a是这个函数的局部变量。与此相反,b被分配给了全局作用域(译注:也就是全局变量)。

这个问题另一个陷阱就是,在函数中没有使用”严格模式” (‘use strict’;)。如果 严格模式开启,那么代码就会报错 ” Uncaught ReferenceError: b is not defined” 。请记住,如果这是预期的行为,严格模式要求你显式地引用全局作用域。所以,你需要像下面这么写:

JavaScript 代码:
(function() {
‘use strict’;
var a = window.b = 5;
})();

console.log(b);
问题2: 创建 “原生(native)” 方法

在 String 对象上定义一个 repeatify 函数。这个函数接受一个整数参数,来明确字符串需要重复几次。这个函数要求字符串重复指定的次数。举个例子:

JavaScript 代码:
console.log(‘hello’.repeatify(3));
应该打印出hellohellohello.
1
2
3
答案

一个可行的做法如下:

JavaScript 代码:
String.prototype.repeatify = String.prototype.repeatify || function(times) {
var str = ”;

for (var i = 0; i < times; i++) {
str += this;
}

return str;
};
1
2
3
4
5
6
7
8
9
10
这个问题测试了开发人员对 javascript 中继承及原型(prototype)属性的知识。这也验证了开发人员是否有能力扩展原生数据类型功能(虽然不应该这么做)。

在这里,另一个关键点是,看你怎样避免重写可能已经定义了的方法。这可以通过在定义自己的方法之前,检测方法是否已经存在。

JavaScript 代码:
String.prototype.repeatify = String.prototype.repeatify || function(times) {/* code here */};
当你被问起去扩展一个Javascript方法时,这个技术非常有用。

愚人码头译注:重复输出一个给定的字符串的解决方案可以看看这篇文章。也许面试官考你的是知识的广度和对新知识的掌握情况。

问题3: 变量提升(Hoisting)

执行以下代码的结果是什么?为什么?

JavaScript 代码:
function test() {
console.log(a);
console.log(foo());

var a = 1;
function foo() {
return 2;
}
}

test();
答案

这段代码的执行结果是undefined 和 2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个结果的原因是,变量和函数都被提升(hoisted) 到了函数体的顶部。因此,当打印变量a时,它虽存在于函数体(因为a已经被声明),但仍然是undefined。换言之,上面的代码等同于下面的代码:

JavaScript 代码:
function test() {
var a;
function foo() {
return 2;
}

console.log(a);
console.log(foo());

a = 1;
}

test();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
问题4: 在javascript中,this是如何工作的

以下代码的结果是什么?请解释你的答案。

JavaScript 代码:
var fullname = ‘John Doe’;
var obj = {
fullname: ‘Colin Ihrig’,
prop: {
fullname: ‘Aurelio De Rosa’,
getFullname: function() {
return this.fullname;
}
}
};

console.log(obj.prop.getFullname());

var test = obj.prop.getFullname;

console.log(test());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
答案

这段代码打印结果是:Aurelio De Rosa 和 John Doe 。原因是,JavaScript中关键字this所引用的是函数上下文,取决于函数是如何调用的,而不是怎么被定义的。

在第一个console.log(),getFullname()是作为obj.prop对象的函数被调用。因此,当前的上下文指代后者,并且函数返回这个对象的fullname属性。相反,当getFullname()被赋值给test变量时,当前的上下文是全局对象window,这是因为test被隐式地作为全局对象的属性。基于这一点,函数返回window的fullname,在本例中即为第一行代码设置的。

问题5: call() 和 apply()

修复前一个问题,让最后一个console.log() 打印输出Aurelio De Rosa.

答案

这个问题可以通过运用call()或者apply()方法强制转换上下文环境。如果你不了解这两个方法及它们的区别,我建议你看看这篇文章 function.call和function.apply之间有和区别?。 下面的代码中,我用了call(),但apply()也能产生同样的结果:

JavaScript 代码:
console.log(test.call(obj.prop));
1
2
问题6: 闭包(Closures)

考虑下面的代码:

JavaScript 代码:
var nodes = document.getElementsByTagName(‘button’);
for (var i = 0; i < nodes.length; i++) {
nodes[i].addEventListener(‘click’, function() {
console.log(‘You clicked element #’ + i);
});
}
1
2
3
4
5
6
7
请问,如果用户点击第一个和第四个按钮的时候,控制台分别打印的结果是什么?为什么?

答案

上面的代码考察了一个非常重要的 JavaScript 概念:闭包(Closures)。对于每一个JavaScript开发者来说,如果你想在网页中编写5行以上的代码,那么准确理解和恰当使用闭包是非常重要的。如果你想开始学习或者只是想简单地温习一下闭包,那么我强烈建议你去阅读 Colin Ihrig 这个教程:JavaScript Closures Demystified

也就是说,代码打印两次You clicked element #NODES_LENGTH,其中NODES_LENGTH是nodes的结点个数。原因是在for循环完成后,变量i的值等于节点列表的长度。此外,因为i在代码添加处理程序的作用域中,该变量属于处理程序的闭包。你会记得,闭包中的变量的值不是静态的,因此i的值不是添加处理程序时的值(对于列表来说,第一个按钮为0,对于第二个按钮为1,依此类推)。在处理程序将被执行的时候,在控制台上将打印变量i的当前值,等于节点列表的长度。

问题7: 闭包(Closures)

修复上题的问题,使得点击第一个按钮时输出0,点击第二个按钮时输出1,依此类推。

答案

有多种办法可以解决这个问题,下面主要使用两种方法解决这个问题。

第一个解决方案使用立即执行函数表达式(IIFE)再创建一个闭包,从而得到所期望的i的值。实现此方法的代码如下:

JavaScript 代码:
var nodes = document.getElementsByTagName(‘button’);
for (var i = 0; i < nodes.length; i++) {
nodes[i].addEventListener(‘click’, (function(i) {
return function() {
console.log(‘You clicked element #’ + i);
}
})(i));
}
1
2
3
4
5
6
7
8
9
另一个解决方案不使用IIFE,而是将函数移到循环的外面。这种方法由下面的代码实现:

JavaScript 代码:
function handlerWrapper(i) {
return function() {
console.log(‘You clicked element #’ + i);
}
}

var nodes = document.getElementsByTagName(‘button’);
for (var i = 0; i < nodes.length; i++) {
nodes[i].addEventListener(‘click’, handlerWrapper(i));
}
1
2
3
4
5
6
7
8
9
10
11
问题8:数据类型

考虑如下代码:

JavaScript 代码:
console.log(typeof null);
console.log(typeof {});
console.log(typeof []);
console.log(typeof undefined);
答案
1
2
3
4
5
6
前面的问题似乎有点傻,但它考察 typeof 操作符的知识。很多JavaScript开发人员不知道typeof的一些特性。在此示例中,控制台将显示以下内容:

JavaScript 代码:
object
object
object
undefined
1
2
3
4
5
最令人惊讶的输出结果可能是第三个。大多数开发人员认为typeof []会返回Array。如果你想测试一个变量是否为数组,您可以执行以下测试:

JavaScript 代码:
var myArray = [];
if (myArray instanceof Array) {
// do something…
}
1
2
3
4
5
问题9:事件循环

下面代码运行结果是什么?请解释。

JavaScript 代码:
function printing() {
console.log(1);
setTimeout(function() { console.log(2); }, 1000);
setTimeout(function() { console.log(3); }, 0);
console.log(4);
}
printing();
答案

输出结果:

JavaScript 代码:
1
4
3
2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
想知道为什么输出顺序是这样的,你需要弄了解setTimeout()做了什么,以及浏览器的事件循环原理。浏览器有一个事件循环用于检查事件队列,处理延迟的事件。UI事件(例如,点击,滚动等),Ajax回调,以及提供给setTimeout()和setInterval()的回调都会依次被事件循环处理。因此,当调用setTimeout()函数时,即使延迟的时间被设置为0,提供的回调也会被排队。回调会呆在队列中,直到指定的时间用完后,引擎开始执行动作(如果它在当前不执行其他的动作)。因此,即使setTimeout()回调被延迟0毫秒,它仍然会被排队,并且直到函数中其他非延迟的语句被执行完了之后,才会执行。

有了这些认识,理解输出结果为“1”就容易了,因为它是函数的第一句并且没有使用setTimeout()函数来延迟。接着输出“4”,因为它是没有被延迟的数字,也没有进行排队。然后,剩下了“2”,“3”,两者都被排队,但是前者需要等待一秒,后者等待0秒(这意味着引擎完成前两个输出之后马上进行)。这就解释了为什么“3”在“2”之前。

问题10:算法

写一个isPrime()函数,当其为质数时返回true,否则返回false。

答案

我认为这是面试中最常见的问题之一。然而,尽管这个问题经常出现并且也很简单,但是从被面试人提供的答案中能很好地看出被面试人的数学和算法水平。

首先, 因为JavaScript不同于C或者Java,因此你不能信任传递来的数据类型。如果面试官没有明确地告诉你,你应该询问他是否需要做输入检查,还是不进行检查直接写函数。严格上说,应该对函数的输入进行检查。

第二点要记住:负数不是质数。同样的,1和0也不是,因此,首先测试这些数字。此外,2是质数中唯一的偶数。没有必要用一个循环来验证4,6,8。再则,如果一个数字不能被2整除,那么它不能被4,6,8等整除。因此,你的循环必须跳过这些数字。如果你测试输入偶数,你的算法将慢2倍(你测试双倍数字)。可以采取其他一些更明智的优化手段,我这里采用的是适用于大多数情况的。例如,如果一个数字不能被5整除,它也不会被5的倍数整除。所以,没有必要检测10,15,20等等。如果你深入了解这个问题的解决方案,我建议你去看相关的Wikipedia介绍。

最后一点,你不需要检查比输入数字的开方还要大的数字。我感觉人们会遗漏掉这一点,并且也不会因为此而获得消极的反馈。但是,展示出这一方面的知识会给你额外加分。

现在你具备了这个问题的背景知识,下面是总结以上所有考虑的解决方案:

JavaScript 代码:
function isPrime(number) {
// If your browser doesn’t support the method Number.isInteger of ECMAScript 6,
// you can implement your own pretty easily
if (typeof number !== ‘number’ || !Number.isInteger(number)) {
// Alternatively you can throw an error.
return false;
}
if (number < 2) {
return false;
}

if (number === 2) {
return true;
} else if (number % 2 === 0) {
return false;
}
var squareRoot = Math.sqrt(number);
for(var i = 3; i <= squareRoot; i += 2) {
if (number % i === 0) {
return false;
}
}
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结论

本文我们讨论了5个在对Javascript开发者面试中常问起的典型问题。实际中的问题会因面试的不同而不同,来自面试的真实问题可能会有所不同,但是涵盖的概念和主题通常都是十分相似的。我希望你愉悦地测试你的能力。万一你不知道所有的答案,不要担心:没有学习和经验不能解决的问题。 如果你在面试中被问到了其他有趣的问题,不要犹豫马上来和我们分享吧。这会帮助到很多的开发者。

在这篇文章中,在一些问题和练习的帮助下,我讨论了其他 JavaScript 重要概念,这些概念通常是前端开发人员角色面试的一部分。我希望你成功地回答所有这些问题,或者你学到了新的东西,以便你可以在你的下一次面试中表现更好。

原文:

​https://www.sitepoint.com/5-typical-javascript-interview-exercises/​​​
​​​https://www.sitepoint.com/5-javascript-interview-exercises/​​​
1
2
5道基础面试题

题目1

if(!(“a” in window)){
var a =1;
}

alert(a);
1
2
3
4
5
代码看起来是想说:如果window不包含属性a,就声明一个变量a,然后赋值为1。

你可能认为alert出来的结果是1,然后实际结果是“undefined”。要了解为什么,我们需要知道JavaScript里的3个概念。

首先,所有的全局变量都是window的属性,语句 var a = 1;等价于window.a = 1; 你可以用如下方式来检测全局变量是否声明:

“变量名称” in window
1
第二,所有的变量声明都在范围作用域的顶部,看一下相似的例子:

alert(“a” in window);
var a;
1
2
此时,尽管声明是在alert之后,alert弹出的依然是true,这是因为JavaScript引擎首先会扫墓所有的变量声明,然后将这些变量声明移动到顶部,最终的代码效果是这样的:

var a;
alert(“a” in window);
1
2
这样看起来就很容易解释为什么alert结果是true了。

第三,你需要理解该题目的意思是,变量声明被提前了,但变量赋值没有,因为这行代码包括了变量声明和变量赋值。

你可以将语句拆分为如下代码:

var a; //声明
a = 1; //初始化赋值
1
2
当变量声明和赋值在一起用的时候,JavaScript引擎会自动将它分为两部以便将变量声明提前,不将赋值的步骤提前是因为他有可能影响代码执行出不可预期的结果。

所以,知道了这些概念以后,重新回头看一下题目的代码,其实就等价于:

var a;
if (!(“a” in window)) {
a = 1;
}
alert(a);
1
2
3
4
5
这样,题目的意思就非常清楚了:首先声明a,然后判断a是否在存在,如果不存在就赋值为1,很明显a永远在window里存在,这个赋值语句永远不会执行,所以结果是undefined。

注:提前这个词语显得有点迷惑了,其实就是执行上下文的关系,因为执行上下文分2个阶段:进入执行上下文和执行代码,在进入执行上下文的时候,创建变量对象VO里已经有了:函数的所有形参、所有的函数声明、所有的变量声明

VO(global) = {
a: undefined
}
1
2
3
这个时候a已经有了;

然后执行代码的时候才开始走if语句。

注:相信很多人都是认为a在里面不可访问,结果才是undefined的吧,其实是已经有了,只不过初始值是undefined,而不是不可访问。

题目2

var a = 1,
b = function a(x) {
x && a(–x);
};
alert(a);
1
2
3
4
5
这个题目看起来比实际复杂,alert的结果是1;这里依然有3个重要的概念需要我们知道。

首先,在题目1里我们知道了变量声明在进入执行上下文就完成了;第二个概念就是函数声明也是提前的,所有的函数声明都在执行代码之前都已经完成了声明,和变

量声明一样。澄清一下,函数声明是如下这样的代码:

function functionName(arg1, arg2){
//函数体
}
1
2
3
如下不是函数,而是函数表达式,相当于变量赋值:

var functionName = function(arg1, arg2){
//函数体
};
1
2
3
澄清一下,函数表达式没有提前,就相当于平时的变量赋值。

第三需要知道的是,函数声明会覆盖变量声明,但不会覆盖变量赋值,为了解释这个,我们来看一个例子:

function value(){
return 1;
}
var value;
alert(typeof value); //”function”
1
2
3
4
5
尽快变量声明在下面定义,但是变量value依然是function,也就是说这种情况下,函数声明的优先级高于变量声明的优先级,但如果该变量value赋值了,那结果就完全不一样了:

function value(){
return 1;
}
var value = 1;
alert(typeof value); //”number”
1
2
3
4
5
该value赋值以后,变量赋值初始化就覆盖了函数声明。

重新回到题目,这个函数其实是一个有名函数表达式,函数表达式不像函数声明一样可以覆盖变量声明,但你可以注意到,变量b是包含了该函数表达式,而该函数表达式的名字是a;不同的浏览器对a这个名词处理有点不一样,在IE里,会将a认为函数声明,所以它被变量初始化覆盖了,就是说如果调用a(–x)的话就会出错,而其它浏览器在允许在函数内部调用a(–x),因为这时候a在函数外面依然是数字。基本上,IE里调用b(2)的时候会出错,但其它浏览器则返回undefined。

理解上述内容之后,该题目换成一个更准确和更容易理解的代码应该像这样:

var a = 1,
b = function(x) {
x && b(–x);
};
alert(a);
1
2
3
4
5
题目3

function a(x) {
return x * 2;
}
var a;
alert(a);
1
2
3
4
5
这个题目就是题目2里的加的注释了,也就是函数声明和变量声明的关系和影响,遇到同名的函数声明,VO不会重新定义,所以这时候全局的VO应该是如下这样的:

VO(global) = {
a: 引用了函数声明“a”
}
1
2
3
题目4

function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);
1
2
3
4
5
Arguments对象是活动对象的一个属性,它包括如下属性:

callee — 指向当前函数的引用
length — 真正传递的参数个数
properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。
这个共享其实不是真正的共享一个内存地址,而是2个不同的内存地址,使用JavaScript引擎来保证2个值是随时一样的,当然这也有一个前提,那就是这个索引值要小于你传入的参数个数,也就是说如果你只传入2个参数,而还继续使用arguments[2]赋值的话,就会不一致,例如:

function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2);
1
2
3
4
5
这时候因为没传递第三个参数a,所以赋值10以后,alert(a)的结果依然是undefined,而不是10,但如下代码弹出的结果依然是10,因为和a没有关系。

function b(x, y, a) {
arguments[2] = 10;
alert(arguments[2]);
}
b(1, 2);
1
2
3
4
5
题目5

function a() {
alert(this);
}
a.call(null);
1
2
3
4
这个题目可以说是最简单的,也是最诡异的,因为如果没学到它的定义的话,打死也不会知道结果的,关于这个题目,我们先来了解2个概念。

首先,就是this值是如何定义的,当一个方法在对象上调用的时候,this就指向到了该对象上,例如:

var object = {
method: function() {
alert(this === object); //true
}
}
object.method();
1
2
3
4
5
6
上面的代码,调用method()的时候this被指向到调用它的object对象上,但在全局作用域里,this是等价于window(浏览器中,非浏览器里等价于global),在如果一个function的定义不是属于一个对象属性的时候(也就是单独定义的函数),函数内部的this也是等价于window的,例如:

function method() { 
alert(this === window); //true
}
method();



了解了上述概念之后,我们再来了解一下call()是做什么的,call方法作为一个function执行代表该方法可以让另外一个对象作为调用者来调用,call方法的第一个参数是对象调用者,随后的其它参数是要传给调用method的参数(如果声明了的话),例如:

function method() { 
alert(this === window);
}
method(); //true
method.call(document); //false
1


第一个依然是true没什么好说的,第二个传入的调用对象是document,自然不会等于window,所以弹出了false。

另外,根据ECMAScript262规范规定:如果第一个参数传入的对象调用者是null或者undefined的话,call方法将把全局对象(也就是window)作为this的值。所以,不管你什么时候传入null,其this都是全局对象window,所以该题目可以理解成如下代码:

function a() { 
alert(this);
}
a.call(window);



所以弹出的结果是[object Window]就很容易理解了。