25道核心JavaScript面试题
原文地址:https://www.toptal.com/javascript/interview-questions
在多年的程序设计生涯里,我常常面临着各种新的语言,新的工具。但是语言、工具最终也仅仅为工具,最终目的是分析问题,解决问题。虽然常年在一个领域,一种语言里摸爬滚打,可能对语言本身的理解更为深刻,即使是有意识的去系统、全面的学习了解这种语言的,也可能只是当时对语言本身有些了解,时间一长也就遗忘了,最终沉淀下来的是解决问题的方法。所以我在公司面试时,基本上不会考虑太多对语言细节的考查,在参加其它公司面试时也反感这种笔试的东西,对于这类笔试,我直接无视,丢在一边。如同我们曾经经历过的高考、考研一样,我们曾经是如此牛逼,数学、物理、化学无所不精,语文、政治、英语无所不能,到如今又记得什么呢?!别怪我唯学校论,我基本只关心哪个学校毕业的,性格是否合拍。经历过那么多年的考试,在工作多年后,再给我来个数据结构、语法,真的是件很无趣的事情。
但是世间的事真就如此无趣,如果你逃不掉这些,那你还是从了吧。在你准备跳槽之前,面试题如同你从小到大的各类考试一样,应付考试是最有效的。所以,如果你看见谁在拿着算法、数据结构、语言的的书看,那基本上是他要跳槽了,你准备找人替代他的工作吧。
面试题也并非一无是处,虽说是看了忘,跳槽再看的东西,但依然是把所涉语言、工具的特点挑出来最简单粗暴的东西,让你抓住重点。看了一下上面的25道基本面试题是JavaScript推荐度最高的,也有很多译文,但我为什么再来一次,因为我只译给自己看的,别人的与我何干,另外,拒绝装逼,尽量尊重原文,原文辛苦写了那么多,大多译文只是三两句装逼的话实在对不起原作者。
1.用typeof bar===”object”判断bar是否是一个object的潜在缺陷是什么?怎样才能避免这种缺陷?
答:尽管用typeof bar===”object”是一个判断bar是一个object的可靠方法,但是令人惊讶的是,在JavaScript中null也是一个object!
因此,以下的代码会让很多开发者惊讶,log显示true,而不是false.
var bar = null;
console.log(typeof bar === "object"); // logs true!
只要你意识到这些,这个问题就能通过判断bar是否为null轻松避免:
console.log((bar !== null) && (typeof bar === "object")); // logs false
为了使我们的答案更加完全、彻底的解决这个问题,还有两件事需要说明:
首先:上面的解决方案里,如果bar是一个function,也会返回false,通常情况下,这是我们期望的,但是在某些情况下,我们希望当bar是function时,也返回true,你就需要修改上面的解决方案如下:
console.log((bar !== null) && ((typeof bar === "object") || (typeof bar === "function")));
其次:如果bar是一个array(var bar=[]),也会返回true,大多情况下,这是我们想要的结果,因为arrays的确是objects。但是如果当bar为arrays,你想返回false,你就需要修改上面的解决方案如下:
console.log((bar !== null) && (typeof bar === "object") && (toString.call(bar) !== "[object Array]"));
或者用jQuery如下:
console.log((bar !== null) && (typeof bar === "object") && (! $.isArray(bar)));
2.下面的代码将输出什么?并说明原因
(function(){
var a = b = 3;
})();
console.log("a defined? " + (typeof a !== 'undefined'));
console.log("b defined? " + (typeof b !== 'undefined'));
答:既然a和b在一个function的代码块里定义,并且既然在同一行以var开始,很多JavaScript开发者认为typeof a和typeof b都将是undefined.然而,事实却并非如此,大多数开发者的问题是错误的理解了var a=b=3;这段代码,以为是下面代码的缩写:
Var b=3;
Var a=b;
但是事实上,var a=b=3; 实际缩写是以下代码:
b=3;
Var a=b;
如果你没有采用strict模式,输出结果如下:
a defined? false
b defined? True
但是b在function的内部,怎么在外部是defined呢? 好吧,既然var a=b=3;是b=3和var a=b;的缩写,b最终是作为全局变量的,因为b没有用var定义,因此作用域在function外部。
需要注意的是,在strict 模式(如使用use strict),var a=b=3;将会产生一个运行时error:ReferenceError: b is not defined。因此可以避免这类的错误。(因此,这也使为什么要在你的代码应采用use strict模式)。
3.以下代码的输出是什么?并说明原因。
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func: this.foo = " + this.foo);
console.log("outer func: self.foo = " + self.foo);
(function() {
console.log("inner func: this.foo = " + this.foo);
console.log("inner func: self.foo = " + self.foo);
}());
}
};
myObject.func();
答:上面的代码输出如下:
outer func: this.foo = bar
outer func: self.foo = bar
inner func: this.foo = undefined
inner func: self.foo = bar
在outer function里,this和self都指向myObject, 因此都能访问到foo,
在inner function里,this不再指向myObject,因此,this.foo在inner function里面是undefined,然而,self依然指向myObject,(在ECMA5之前,this在内部function里将指向window,然而,在ECMA5之后,内部function的this将是undefined).
4.将一个JavaScript文件封装在一个function块里的意义,原因是什么?
答:在很多流行的JavaScript库(jQuery,Node.js等)将源码文件封装在一个函数中越来越普遍。这种技术会为这个源码文件创建一个封闭的环境,可能最重要的是创建了一个私有的命名空间,因此避免了在不同的JavaScript modules和库中出现潜在的命名冲突。
这种技术的另一个特点是允许通过别名的方式很容易的引用全局变量。这个经常用到,比如,在jQuery插件中,jQurey 允许你通过jQuery.noConflict(),使得不能通过$引用jQuery命名空间。如果你这么做了,你依然可以像下面代码一样使用$采用这种闭包的技术:
(function($) { /* jQuery plugin code referencing $ */ } )(jQuery);
5.在JavaScript源码文件中以’use strict’开始的意义和好处是什么?
答:采用strict模式的主要好处如下:
a.使得debugging更容易:在一些被忽略或者潜在的错误会产生error或者exceptions,会很快的在你的代码中显示警告,引导你很快的找到对应的源码。
b.阻止出现意外的全局变量:如果不是在strict模式里,那么赋值给一个没有声明的变量时,会自动的创建一个同名的全局变量,这是JavaScript中最常见的错误之一。在strict模式里,会尝试抛出一个error.
c.强制排除this错误:在非strict模式里,this引用为null或者undefined时,会自动强制指向全局,这会导致各种错误的引用。在strict模式里,this值为null或者undefined将会抛出error.
d.不允许重名的属性名或者参数名.在strict模式里,如果定义了如:var object={foo:’bar’,foo:’baz’};或者定义一个重名参数的函数,如:function foo(val1,val2,val1){}.会产生一个error,而这个bug几乎一定会产生,但你可能浪费大量的时间才能找到。
e.使eval()更加安全。在strict模式和非strict模式里,eval()存在很多不同。在strict模式里,变量和函数在eval()中声明,但语句不在内部块创建,但是在非strict模式里,语句也会在内部块里创建,这也是常见的源码问题。
f.不正确使用delete会抛出error:delete操作(用于从object中删除一个属性)不能用于没有配置的属性,在非strict模式的代码里删除一个没有配置的属性会失败,但不会有提示,在strict模式里,则会抛出error。
6.考虑一下下面的两个函数,他们将返回同样的值吗?请说明原因
function foo1()
{
return {
bar: "hello"
};
}
function foo2()
{
return
{
bar: "hello"
};
}
答:很奇怪的是,两个函数返回的值并不一样,如果执行以下语句:
console.log("foo1 returns:");
console.log(foo1());
console.log("foo2 returns:");
console.log(foo2());
将得到下面的结果:
foo1 returns:
Object {bar: "hello"}
foo2 returns:
undefined
不仅仅只是对返回不一样的结果奇怪,也要对foo2()没有抛出error特别注意。产生这个结果的原因是分号在JavaScript中的用法(省略分号不是好的做法)。当foo2()的一行语句中只包含return时,会在return语句后面自动的加上一个分号。后面的语句也是合法的,不会抛出error,尽管它不会调用,也不做任何事(仅仅只是一个没有用到的语句块,它定义了一个等同于’hello’字符串的属性bar而已)。
这也说明了在Javascript中大括号的位置应该放在语句后面的编程风格更符合Javascript的语法要求(有些语言推荐放在新一行的开头)。
7.NaN是什么?它是什么类型?怎样能够可靠的判断一个值是否等于Nan?
答:NaN表示一个值不少number,一般指一个操作中有一个非number的值,或者操作的结果是非number的值(例如被0除)。这看起来很清楚明了,然而对于Nan,如果不了解的话,会有两个很奇怪的特性,会让人头疼。
一个是尽管NaN的意思是不是number,但NaN的类型是number,如下:
console.log(typeof NaN === "number"); // logs "true"
另外一个是NaN更任何东西比较(即使是自身),结果都为false,如下:
console.log(NaN === NaN); // logs "false"。
用isNaN()判断一个number是否等于NaN是不可靠的,更好的解决方案使用value!==value,这个只有当value等于NaN时才会为true.在ES6中可以用Number.isNaN()来判断,比使用老的全局函数isNaN()更可靠。
8.下面的代码输出结果是什么?并解释原因
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 == 0.3);
答:这个问题也可以解释为:你可能很有信心,它将打印0.3和true,然而可能是错的,在JavaScript中数字采用浮点计数,因此,常常并不是期望的值。上面的例子将打印以下结果
0.30000000000000004
False
9.讨论一下怎样写一个函数isInteger(x),判断x是一个整数。
答:这听起来并不重要,事实上,在ES6中有一个新的函数Number.isInteger()能准确的实现这个目的,然而,在ES6之前,就比较复杂,没有如Number.isInteger()的方法。
这个问题主要是因为,在ECMAScript中,整形只是概念上的,数字都是采用浮点计数。出于这个考虑,在ES6中的解决方式采用的是最简单的方式function isInteger(x) { return (x^0) === x; } 。
下面的方案也可以,尽管没有上面的优雅。
function isInteger(x) { return Math.round(x) === x; }
用Math.ceil()或者Math.floor()代替Math.round()也可以。
还可以选择:
function isInteger(x) { return (typeof x === 'number') && (x % 1 === 0); }
通常采用下面的方案是不正确的。
function isInteger(x) { return parseInt(x, 10) === x; }
这主要是因为parseInt是基于能正确转化x,一旦x太大,它会转换失败,parseInt会在转化成数字是先强制转换为字符串,因此,一旦一个数字太大,它转化的字符串是原数字的指数形式(1e+21),因此,parseInt()将会解析1e+21,但是当遇到e是会停止解析了。如下:
> String(1000000000000000000000)
'1e+21'
> parseInt(1000000000000000000000, 10)
1
> parseInt(1000000000000000000000, 10) === 1000000000000000000000
False
10.下面数字1-4的打印顺序是什么?并解释原因。
(function() {
console.log(1);
setTimeout(function(){console.log(2)}, 1000);
setTimeout(function(){console.log(3)}, 0);
console.log(4);
})();
答:上面的结果应该是:
1
4
3
2
让我们预测一下每一部分的代码:
A.1和4将在调用console.log()后,直接打印出来。
B.2在3之后打印,因为3没有时间延迟,2在3打印后延迟了1000毫秒在打印。
但是3延迟时间为0,难道不意味着立即打印吗,并且,如果这样,3应该在4之前打印,应为打印4的代码在后面执行的。这个问题的答案需要好好理解JavaScript的events和timing.
浏览器有一个事件队列,用来检查处理和等待事件。例如:当一个事件在后台触发了(例如onload),然而现在浏览器很忙(在处理onclick),这个事件会放入事件队列中,当onclick句柄完成后,会检查事件队列,执行事件(onload)。
类似,如果浏览器忙则setTimeout()会将执行的函数放入事件队列中。当setTimeout()设置时间为0,它的意思是“尽可能快”的指向相应的函数。执行的函数会放入事件队列中,在下一个时间片执行。需要指出的是,这并不是要立即执行。函数一直会等到下一个时间片。这也就是上面结果的原因。Console.log(4)会在console.log(3)之前执行。
11.写一个函数,判断一个字符串(超过80个字符)是否是回文结构(正序和逆序想同)。
答:下面的函数,如果str是回文则返回true,否则返回false.
function isPalindrome(str) {
str = str.replace(/\W/g, '').toLowerCase();
return (str == str.split('').reverse().join(''));
}
例如:
console.log(isPalindrome("level")); // logs 'true'
console.log(isPalindrome("levels")); // logs 'false'
console.log(isPalindrome("A car, a man, a maraca")); // logs 'true'
12.写一个sum函数使以下两种调用方式都正确。
console.log(sum(2,3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
答:至少有两种方式实现:
方法1
function sum(x) {
if (arguments.length == 2) {
return arguments[0] + arguments[1];
} else {
return function(y) { return x + y; };
}
}
在JavaScript中,函数可以通过arguments对象访问传入的给函数的参数。我们能通过在运行时访问length属性的值得到参数的个数。如果是两个参数,我们直接相加并返回,否则,我们假定调用的是sum(2)(3),因此我们返回一个匿名函数,通过sum()相加(参数2),并且把参数传给匿名函数(参数3)。
方法2:
function sum(x, y) {
if (y !== undefined) {
return x + y;
} else {
return function(y) { return x + y; };
}
}
在JavaScript中,一个函数被调用,是不需要输入参数完全匹配函数的参数定义的。如果输入参数超过函数定义的参数,则超过的参数会被忽略,另一方面,如果输入的参数少于函数定义的参数,则缺少的参数将会赋予undefined.因此,上面的例子中,通过判断第二个参数是否为undefined,我们可以决定采用哪种函数调用和执行方式。
13.考虑一下以下的代码片段
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode('Button ' + i));
btn.addEventListener('click', function(){ console.log(i); });
document.body.appendChild(btn);
}
(a)当用户点击“Button4”的时候会打印什么?并解释为什么?
(b)提供一个或多个正确的实现方式。
答:(a)无论点击哪个按钮,都将打印5.因为任何按钮在调用onclick方法时,for循环已经完成了,变量i的值变成了5.
(b).关键是要抓住在每一次循环for的时候要把i的值传人到最近创建的函数对象中,下面有三个可能的方式解决这个问题:
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode('Button ' + i));
btn.addEventListener('click', (function(i) {
return function() { console.log(i); };
})(i));
document.body.appendChild(btn);
}
二种:你可以将整个btn.addEventListener封装在一个新的匿名函数里。
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode('Button ' + i));
(function (i) {
btn.addEventListener('click', function() { console.log(i); });
})(i);
document.body.appendChild(btn);
}
三种:可以将for循环换成array对象的本地调用方法forEach.
['a', 'b', 'c', 'd', 'e'].forEach(function (value, i) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode('Button ' + i));
btn.addEventListener('click', function() { console.log(i); });
document.body.appendChild(btn);
});
14.下面的代码将输出什么,并解释原因?
var arr1 = "john".split('');
var arr2 = arr1.reverse();
var arr3 = "jones".split('');
arr2.push(arr3);
console.log("array 1: length=" + arr1.length + " last=" + arr1.slice(-1));
console.log("array 2: length=" + arr2.length + " last=" + arr2.slice(-1));
答:输出结果如下:
"array 1: length=5 last=j,o,n,e,s"
"array 2: length=5 last=j,o,n,e,s"
Arr1和arr2输出结果一样的原因如下:
A.调用array对象的reverse()方法不仅仅返回转置后的数组,它自身的顺序也转置了。
B.Reverse()方法返回的是指向array自身的一个引用,因此,arr2仅仅是arr1的一个引用,无论对arr2怎么操作,arr1也会受到影响,arr1,arr2都指向同一个对象。
C.通过调用array的push()方法,传人另一个array,仅仅只是把传人的array作为一个元素添加到队列的尾部,因此,arr2.push(arr3)是将arr3当作一个元素添加到arr2的队尾,而不是像concat()方法一样,合并两个array。
D.与Python类似,JavaScript通过输入-1调用slice()是一种指向array队列尾部最后一个元素的方法。
15.下面代码的输出是什么,并解释为什么?
console.log(1 + "2" + "2");
console.log(1 + +"2" + "2");
console.log(1 + -"1" + "2");
console.log(+"1" + "1" + "2");
console.log( "A" - "B" + "2");
console.log( "A" - "B" + 2);
答:上面代码的显示的结果是:
"122"
"32"
"02"
"112"
"NaN2"
NaN
主要的问题是JavaScript是一种弱类型的语言,它会在操作执行时自动转化数值类型。
例1:1+”2”+”2”输出结果为”122”.第一个操作是1+”2”,因为有个操作数是”2”,是字符串,JavaScript会在执行操作过程中转化成字符串的合并,1转化成”1”,1+”2”得到结果为“12”,然后,“12”+“2”,结果为“122”。
例2:1+ +”2”+”2”输出结果为“32”。第一个操作是+”2”,这被看成是一元操作,因此,JavaScript会把”2”的类型转为数字,再赋予+,则为正整数2,然后执行1+2得3,但是当我们执行一个数字和字符串的操作3+”2”时,又将转化成字符串连接操作,得到字符串“32”。
例3:1+ -“1” + “2”输出为“02”。这里首先定义操作优先级,一元操作-高于+,因此”1”转为1,赋予-,转为-1,加上1得结果0,然后与最后的“2”转字符串连接,结果为“02”。
例4:+“1” + “1” +“2”,输出结果为112.首先执行+一元操作得1,然后与后面的“1”,“2”执行字符串连接操作,得结果“112”。
例5:“a”-”b” +”2”输出结果为“NaN2”。因为字符串不存在-操作,因此会将A,B转化成数字,转化失败,得到结果“NaN”,再与“2”做字符串连接操作,得到结果“NaN2”.
例6:”a”-”b”+2,输出结果为NaN,因为字符串不存在-操作,因此会将A,B转化成数字,转化失败,得到结果“NaN”,再与2执行加操作,但是NaN与任何数字操作结果还是NaN。
16.下面的代码,如果队列太长会导致栈溢出,怎样解决这个问题并且依然保持循环部分。
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
nextListItem();
}
};
答:为了避免栈溢出,循环部分改为如下代码:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
setTimeout( nextListItem, 0);
}
};
栈溢出主要是因为循环事件,而不是栈。当执行nextListItem时,如果item不是null,在timeout函数中的nextListItem会推入到事件队列中。当事件空闲,则会执行nextListItem,因此,这种方法从开始到结束没有直接进行循环调用,可以不用考虑循环次数。
17.什么是闭包?并举例。
答:闭包是一个内部函数访问外部定义的变量,闭包有三种访问变量的方式。1)在自身域的变量。2)闭包函数域的变量。3)全局变量。
例子:
var globalVar = "xyz";
(function outerFunc(outerArg) {
var outerVar = 'a';
(function innerFunc(innerArg) {
var innerVar = 'b';
console.log(
"outerArg = " + outerArg + "\n" +
"innerArg = " + innerArg + "\n" +
"outerVar = " + outerVar + "\n" +
"innerVar = " + innerVar + "\n" +
"globalVar = " + globalVar);
})(456);
})(123);
上面例子中,在innerFunc中访问的变量分别在innerFunc,outFunc和全局命名空间里。输出结果如下:
outerArg = 123
innerArg = 456
outerVar = a
innerVar = b
globalVar = xyz
18.下面代码的输出结果是什么?并解释原因
for (var i = 0; i < 5; i++) {
setTimeout(function() { console.log(i); }, i * 1000 );
}
答:例子显示的结果不是预想的0,1,2,3和4.而是5,5,5,5和5.原因是每个执行的函数在循环完全执行完成后,i最后的赋值是5.闭包的方式可以解决这个问题,在每个循环中为次的循环变量创建一个唯一域,如下:
for (var i = 0; i < 5; i++) {
(function(x) {
setTimeout(function() { console.log(x); }, x * 1000 );
})(i);
}
则结果将会是0,1,2,3,4.
19.下面代码的输出结果是什么?并解释原因。
console.log("0 || 1 = "+(0 || 1));
console.log("1 || 2 = "+(1 || 2));
console.log("0 && 1 = "+(0 && 1));
console.log("1 && 2 = "+(1 && 2));
答:代码段的输出结果如下:
0 || 1 = 1
1 || 2 = 1
0 && 1 = 0
1 && 2 = 2
在JavaScript中,||和&&是逻辑操作,将从左往右进行逻辑判断。||操作符的表达式是X|Y,X先进行评估,判断一个boolean逻辑值,如果为true,则值为X。如果为false,则值为Y。&&操作恰恰相反。
20.下面代码的执行结果是什么?并解释原因
console.log(false == '0')
console.log(false === '0')
答:代码的执行结果为:true false;
在JavaScript中,有两个等于操作,其中三等号===更像传统的等于操作,如果表达是两边的类型和值都相等才为true,双等号==会在比较相等时进行强制的值比较,因此更好的编程风格是采用===,同样的应采用!==,而不是!=。
21.下面代码的输出结果是什么?并解释原因。
var a={},
b={key:'b'},
c={key:'c'};
a[b]=123;
a[c]=456;
console.log(a[b]);
答:输出的结果应该是456,而不是123.
原因是,当我们设置一个object属性时,JavaScript会隐形的将参数值字符串化。在这个例子中,b和c都是objects,他们会转化为”[object,object]”,结果a[b]和a[c]都等于a[”[object,object]”],因此,设置或者引用a[c]等同于设置和引用a[b].
22.给出以下代码的输出结果,并解释原因?
console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));
答:代码块的输出结果是10facorial值,也就是(10!或者 3628800),f()会循环调用自身,直到调用f(1),返回1,因此,以下是执行过程。
f(1): returns n, which is 1
f(2): returns 2 * f(1), which is 2
f(3): returns 3 * f(2), which is 6
f(4): returns 4 * f(3), which is 24
f(5): returns 5 * f(4), which is 120
f(6): returns 6 * f(5), which is 720
f(7): returns 7 * f(6), which is 5040
f(8): returns 8 * f(7), which is 40320
f(9): returns 9 * f(8), which is 362880
f(10): returns 10 * f(9), which is 3628800
23.下面代码块的输出是什么?并解释原因。
(function(x) {
return (function(y) {
console.log(x);
})(2)
})(1);
答:输出结果是1,尽管x值在内部函数里没有设置。
在JavaScript中,闭包是作为内部函数实现的,也就是一个函数定义在另一个函数内部,闭包的重要特性是内部函数能够访问外部函数的变量。在这个例子中,尽管x没有在内部函数定义,在外部函数里找到了定义的值为1的x变量。
24.下面代码的输出结果是什么,并解释原因?如何修改。
var hero = {
_name: 'John Doe',
getSecretIdentity: function (){
return this._name;
}
};
var stoleSecretIdentity = hero.getSecretIdentity;
console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());
答:输出结果为 undefined JohnDoe。
第一个为undefined是因为我们直接从hero对象中提取stoleSecretIdentity(),stoleSecretIdentity()是从全局上下文里调用的(window对象),不存在_name属性。可以采用下面的方式正确实现预期。
var stoleSecretIdentity = hero.getSecretIdentity.bind(hero);
25.创建一个函数,赋予page的一个dom元素,将访问自身和它所有的子元素(不仅仅是直接子元素)。因为每个元素都要访问到,需要传人一个回掉函数。函数的参数如下:一个Dom元素,一个回调函数。
答:可以采用深度优先算法。如下:
function Traverse(p_element,p_callback) {
p_callback(p_element);
var list = p_element.children;
for (var i = 0; i < list.length; i++) {
Traverse(list[i],p_callback); // recursive call
}
}