TopTal 进阶 JavaScript 面试题
原网址:37 Essential JavaScript Interview Questions
以下为我对这37个题目的翻译和解答,其中小部分题目的解答是我认为官方解答的很合适,也无需更为深入的挖掘,会直接翻译官方的回答;大部分题目的解答都是我对题中涉及到的知识点更深入的挖掘做出的解释。
1.当你使用typeof bar === 'object'
来确定 bar
是否是 object 时,这其中存在潜在问题是什么?如何规避这些问题?
通常情况下,使用 typeof
来判断某个变量的类型,是没有什么问题的,但是如果你有一些特殊的需求,可能就会存在潜在问题了。
我们举个例子,你有个变量 a
,需要做类型校验,如果是 object
类型就 true
,程序继续往下走,此时,我们希望出现的结果是什么?是这个 a
变量不为空,且有价值数据,这个数据是一个 object
类型的对象,我们希望拿到这个对象数据来进行某些处理。
那么此时由于一些未知的原因,你没有拿到预期的数据,你拿到了一个 null
,此时你当然是希望你的判断条件不会命中它,因为一旦命中 null
的话,我们处理数据的逻辑部分很可能就会出现些不可预知的 bug 。
但是事实呢?很不幸的告诉你,它通过了你的判断条件,typeof null === 'object'
会打印出 true
这一结果。
我们在这并不想深究为什么在javascript中null
会是一个object对象,正如同我们并不想深究NaN
为什么是一个 number
类型,而 undeifined
是一个 undeifined
类型一样,我们希望可以让你知道如何去分辨和规避,而不是告诉你为什么为什么 null
是一个 object
。
言归正传,在上述示例中,还有一个特殊的情况,比如说,Array
。typeof [] === 'object'
这句代码在 JavaScript
中是成立的,如果你想真正区分 object
和 Array
,那么你可以像下面这样修改你的命中条件:
if(Object.prototype.toString.call(a) === '[object Array]'){// do something
}
或者还可以这样:
if(a != null && a.constructor === Object){// do something
}
当然,用ES5的 isArray()
也是可以的:
if(Array.isArray(a)){// do something
}
要注意,a.constructor
并不是一个通用的解决方案,比如有以下代码:
const b = null;
b.constructor === Object // Uncaught TypeError: Cannot read property 'constructor' of nullconst b = undefined;
b.constructor === Undeifined // Uncaught TypeError: Cannot read property 'constructor' of undefinedconst b = NaN;
b.constructor === Number; // logs true
2.以下的代码会输出什么?为什么呢?
(function(){var a = b = 3;
})();console.log("a defined? " + (typeof a !== 'undefined'));
console.log("b defined? " + (typeof b !== 'undefined'));
这个问题有趣的地方在于你对 javascript 的关键字声明是否熟悉。你也许认为以下的输出是对的:
"a defined? " false
"b defined? " false
但事实上很多人把 var a=b=3
当成了以下这种形式:
var a = 3;
var b = 3;
但是事实上,var a=b=3
应该是这样的:
b = 3;
var a = b;
如果你使用了严格模式(use strict
),就没有这个困扰了,因为在运行时会报以下错误:ReferenceError: b is not defined
.
所以,正确的输出应该如下:
"a defined? " false
"b defined? " true
3.以下的代码块会输出什么内容?为什么?
var myObject = {foo: "bar",func: function() {var self = this;console.log("outer func1: this.foo = " + this.foo);console.log("outer func2: self.foo = " + self.foo);(function() {console.log("inner func1: this.foo = " + this.foo);console.log("inner func2: self.foo = " + self.foo);}());}
};
myObject.func();
这是一个典型的 JavaScript
指向的问题,对此有疑惑的可以去看看我的一篇专门解释 this 指向的文章:javascript this探究
这儿我就简单解释下这其中的原理,首先正确的输出结果应该是如下:
outer func1: this.foo = 'bar',
outer func2: this.foo = 'bar'inner func1: this.foo = undefined,
inner func2: self.foo = 'bar'
要想找到 this 的指向,只要找到这个 this 的(直接调用者)上一级调用者就ok了。在上述代码中,这个this(self)出在 func 函数内,也就是说,此 this
与 func
的地位是相等的,而 func
又由 myObject
这个对象调用,所以 outer 中 this 指向的就是 myObject
对象。
而在 inner
函数中的 this
是与这个匿名函数同级的,而这个匿名函数被 func
函数调用,它的上一级调用者就是 func
函数,所以this自然就是 undefined
了,而此匿名函数又处在 func
函数的作用域范围内,所以调用 self
变量时还是能拿到 bar
值;
4.将 JavaScript 源文件的整个内容包在闭包中的意义和原因是什么?
这是一种越来越普遍的做法,被许多流行的 JavaScript
库(jQuery,Node.js等)采用。这种技术围绕文件的整个内容创建一个闭包,最重要的是,它可以创建一个私有命名空间,从而有助于避免不同 JavaScript
模块和库之间潜在的名称冲突。
该技术的另一个特征是允许使用更易于引用(可能更短)的全局变量的别名。例如,在 jQuery 插件中经常使用它。如下所示:
(function($) {/* jQuery plugin code referencing $ */
})(jQuery);
5.use strict
在 JavaScript 源文件的开头包含什么是重要的,有什么好处?
简单来说,use strict
是一种在代码运行时自动对 JavaScript
代码实施更严格的解析和错误处理的方法。在未使用 use strict
时会被忽略或会以静默方式失败的代码,错误在使用了 use strict
现在将生成错误或抛出异常,光从这个角度来说,这就是一个很好的办法。
严格模式的一些主要好处包括:
1.使调试更容易。
否则将被忽略或将以静默方式失败的代码错误现在将生成错误或抛出异常,提前警告您代码中的问题并将您更快地引导到其源代码。
防止偶然的全局变量。如果没有严格模式,则为未声明的变量赋值会自动创建具有该名称的全局变量。这是 JavaScript
中最常见的错误之一。在严格模式下,尝试这样做会引发错误。
2.消除this胁迫。
如果没有严格模式,则 this
对 null
或 undefined
值的引用会自动强制转换为全局。这可能导致许多头屑和拔出你的头发类型的错误。在严格模式下,引用 this
, null
或 undefined
值会引发错误。
3.禁止重复的参数值。 严格模式在检测到函数的重复命名参数时会抛出错误(例如,function foo(val1, val2, val1){})
,从而捕获代码中几乎可以肯定的错误,否则您可能会浪费大量时间进行跟踪。
注意:以前(在 ECMAScript 5中)严格模式将禁止重复的属性名称(例如var object = {foo: "bar", foo: "baz"};)
,但从 ECMAScript 2015 开始,情况不再如此。
4.使eval()更安全。
eval() 在严格模式和非严格模式下的行为 方式存在一些差异。最重要的是,在严格模式下,声明内部 eval() 声明的变量和函数不会在包含范围中创建(它们是在非严格模式的包含范围中创建的,这也可能是常见的问题来源)。
5.无效使用时会引发错误delete。
delete 操作者(用于从对象中删除属性)不能在对象的非配置的属性来使用。当尝试删除不可配置的属性时,非严格代码将无提示失败,而严格模式将在这种情况下抛出错误。
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
主要原因就是 return
后面的对象符 }
换行了。浏览器 JavaScript
引擎在解析时,会自动在换行的部分插入分号;,所以上述代码中的 foo2()
函数就如同下面:
function foo2()
{return;
}
那返回的自然就是一个 undefined
了。
7.什么是 NaN?如何用一个可靠的方法来判断它?
NaN
是一个全局属性,表示不是一个数字( Not a Number )。
通常来说,NaN
很少在编码中出现,它一般都是被某个方法计算错误时作为返回值给我们。
NaN
有着一些很奇怪的特性:
1.NaN 和任何值都不相等
console.log(NaN === NaN) // logs false
console.log(NaN === 1) // logs false
console.log(NaN === '1') // logs false
2.NaN 的数据类型为 number
console.log(typeof NaN) // logs number
JavaScript
提供了一个内置的函数 isNaN()
来判断 NaN
,它的作用机制是检查一个值是否能被 Number()
成功转换。如果能转换成功,就返回 false
,否则返回 true
,事实上,它并不是一个理想的判断函数:
console.log(isNaN(NaN)) // logs true 不能转换
console.log(isNaN('123')) // logs false 能转换
console.log(isNaN('abc')) // logs true 不能转换
console.log(isNaN('123.45abc')) // logs true 不能转换
显然,它不能区分 NaN
和其他不能被转换的类型。
在 ES5
之前,利用 NaN
与其自身的绝对不相等性,以下方式能够更可靠的进行 NaN
的验证:
var isNaN = function (n){return n !== n;
}
console.log(isNaN(1)) // logs false
console.log(isNaN('1.111')) // logs false
console.log(isNaN(NaN)) // logs true
ES5 之后我们可以使用 Number.isNaN()
来做判断,更安全可靠。
8.下面的代码会输出什么?为什么?
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 == 0.3);
我们先看结果:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 == 0.3); // false
这个问题本质上是在讲 JavaScript
在进行浮点数计算时精度丢失的问题。当然,不仅仅是 JavaScript
,所有遵循 IEEE 754
标准的编程语言都存在这个问题。
这是什么原因导致的呢?就是十进制数在转化成二进制数时产生的精度丢失。我稍微演示下精度丢失的过程:
准备工作:想要理解精度是怎么丢失的,你先得理解十进制数是怎么转化为二进制数的。
十进制数转化为二进制数主要分为两步:
1.整数部分按位取余,然后倒过来。比如:
//十进制 5 转化为二进制(向下取整)
5/2 = 2 --- 余1
2/2 = 1 --- 余0
1/2 = 0 --- 余1
所以 5 的二进制数据是 0101。
2.小数部分按位乘2取整,得到积后取积的小数部分,然后再把这个积的小数部分乘2,用积取整,直到积的小数部分为0。比如:
//十进制 0.625 转化为二进制(按积取整)
0.625 * 2 = 1.25 --- 取 1
0.25 * 2 = 0.5 --- 取 0
0.5 * 2 = 1 --- 取 1
所以 0.625 的二进制数据是 0.101。
OK,准备工作做完了,我们来看看 0.1 的二进制数据是怎么转化的:
0.1 * 2 = 0.2 --- 0
0.2 * 2 = 0.4 --- 0
0.4 * 2 = 0.8 --- 0
0.8 * 2 = 1.6 --- 1
0.6 * 2 = 1.2 --- 1
0.2 * 2 = 0.4 --- 0
0.4 * 2 = 0.8 --- 0
0.8 * 2 = 1.6 --- 1
0.6 * 2 = 1.2 --- 1
...
看到这想必各位看官已经有所领悟了,若是碰到这种无限循环的数据,肯定是只能通过截取部分有效位来处理,所以在截取的过程中自然就会产生精度丢失。
0.1 -> 0.000110011......0011
0.2 -> 0.00110011......0011
0.1 + 0.2 -> 0.010011001100110011001100110011001100110011001100110100
ok,我们再将这个被截取的二进制数按照二次幂的原则转化为十进制数是多少呢?
0.30000000000000004440892098500626
到此为止,我们已经知道了0.30000000000000004
这个值是怎么来的了。
那么怎么处理呢?
1.部署一个误差检查函数。
function areTheNumbersAlmostEqual(num1, num2) {return Math.abs( num1 - num2 ) < Number.EPSILON;
}
console.log(areTheNumbersAlmostEqual(0.1 + 0.2, 0.3)); // true
Number.EPSILON
是 ES6
新增的一个极小的常量,它实际上是 JavaScript
能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
2.在进行浮点数计算时,先讲浮点数转化为整型,计算后,在根据位数转化回小数。
9.ECMAscript 6
中的 Number.isInteger()
(用来确定是否是整数) 在 ES5
中可以怎么实现?
在 ECMAscript 6
中,我们可以很方便的使用Number.isInteger()
来判断一个数是否为整数。但是在 ES6
以前,这是一件挺麻烦的事。
所以,一个最简单干净的解决办法是以下这种:
var isInteger = function(x) {return (x ^ 0) === x;
}
我来简单解释下return (x ^ 0) === x
这句代码的含义。首先,你得明白 JavaScript
中按位异或的概念(以 ^ 来表示):即两个位数相等则为 0,不等则为 1。
举个例子:
// 5 ^ 1 按位异或的过程:
5 -> 0101
1 -> 00015^1 -> 0100
5^1 = 4
我们再来看这句代码:return (x ^ 0) === x
,我们可以从两个方面来解释,第一,如果这个数它本来就是整数,那么会返回什么?
5 -> 0101
0 -> 0000
5^0 -> 0101
5^0 = 5
很显然我们得到结论,任何数和0按位异或时得到的结果都是它本身。
ok,那么第二,当这个数不是整数呢?,我们看看会发生什么:
5.1 ^ 0 = 5
2.11111 ^ 0 = 2
很奇怪不是吗?按照之前的理论,应该是下面这种过程才对啊:
5.1 -> 0101.0001100110011.......0011
0 -> 0000.0000000000000.......0000
5.1^0->0101.0001100110011.......0011
5.1^0 = 5.1
这样才对呀!但是事实上并非如此,问题就出在小数部分的异或上面,我们接着看一个例子:
0.1 ^ 0 = 0;
0.2222 ^ 0 = 0;
1/3 ^ 0 = 0;0.1 ^ 1 = 1;
0.2222 ^ 1 = 1;
1/3 ^ 1 = 1;
发现没有,在 JavaScript
中,任何小数与一个数(假定它为 a)异或时,都等于这个数 a。
这是为什么呢?我们打开 ECMAScript5.1中文版
在 ***9.5 ToInt32:(32 位有符号整数)***中,有以下内容:
ToInt32 运算符将其在 -231 到 231-1 闭区间内的参数转换为 232 个整数值之一。此运算符功能如下所示:
对输入参数调用 ToNumber。
1.如果 Result(1) 是 +0 ,-0,+∞,或 -∞,返回 +0。
2.计算 sign(Result(1)) * floor(abs(Result(1)))。
3.计算 Result(3) modulo 232 ;也就是说,数值类型的有限整数值 k 为正,且小于 232 ,规模相对于 Result(3) 的数学值差异 ,232 是 k 的整数倍。
4.如果 Result(4) 是大于等于 231 的整数,返回 Result(4) - 232 ,否则返回 Result(4)。
关键就在第 2 条中的这句话:sign(Result(1)) * floor(abs(Result(1)))
中的floor(x)
函数。
这玩意是怎么运作的呢?在ECMAScript5.1中文版中的 5.2 算法约定这一节中有明确的规定:
floor(x) = x−(x modulo 1).
什么意思呢?**就是 floor(x) 等于 x 减去 x 模1。**正是这一步,将小数部分的异或去掉了。
到这里,想必大家已经对上面那句简单的return (x ^ 0) === x
有所领悟了。也正是因为这一特性,我们也可以使用下面这种方式来实现判断一个数是否为整数
var isInteger = function(x) {return Math.round(x) === x;
}
当然,你可以更奔放些,像下面这样:
var isInteger = function(x) {return (typeof x === 'number') && (x % 1 === 0);
}
但是,要注意的是,我们不可以用下面这种方式来处理:
var isInteger = function(x) {return parseInt(x, 10) === x;
}
这是因为 parseInt(string, radix)
与 Math.round(x)
不同的作用机制导致的。parseInt(string, radix)
会将它的第一个值转化为字符串类型备用,而这正是问题所在,当一个特别大的数字被传入 parseInt
函数时,它会先将这个数转化为指数形式,就像下面这样:
parseInt(1000000000000000000000, 10)
->
parseInt('1e+21', 10)
=
1
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
然后说结论:之所以会出现这个情况,4比3先执行,是因为要记住一点,定时器,都是异步执行的。JavaScript
对于异步执行的事件是有一个事件队列的,这个队列的执行优先级低于当前环境中代码的执行优先级,所以才会出现 4 比 3 先执行的情况。
11.编写一个简单的函数来判断某个字符串是否为回文结构
首先,我和大家解释下什么是回文结构。
按照维基百科的解释,回文结构就是将这个字符串的内容按相反的顺序重新排列后,所得到的字符串和原来的一样。
ok,了解了这一点之后,咱们就可以看看其实现了:
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'
当然,上面这种方式由于夹杂了切割,翻转以及插入等各种操作,效率上会比较低,所以你还可以使用以下这种方式,从字符串头部和尾部,逐步往中间检测:
function isPalindrome(str) {str = str.replace(/\W/g, '').toLowerCase();for(var i = 0,j = str.length - 1; i < j; i++,j--){if(str.charAt(i) !== str.charAt(j)){return false;}}return true;
}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
至少有两种方法可以做到这一点。
方法一:
function sum(x) {if(arguments.length == 2){return arguments[0] + arguments[1]; }else {return function(y) {return x + y;}}
}
方法二:
function sum(x, y) {if(typeof y != 'undefined'){return x + y; }else {return function(y) {return x + y;}}
}
关于 arguments,我就不做过多的解释了,如果你对这个参数还没有概念,那么你要加油了。
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) 当你点击 “Button 4”的时候会打印什么内容?为什么?
(b) 至少提供一个可按预期工作的替代方案?
很显然,这是一个 JavaScrip
t 中经典的闭包问题。当你点击 “Button 4” 时,只会打印出 5,因为在你点击的时候这个循环早已经结束了。所以才会出现你无论你点击哪一个 Button 都会显示 5。
有关闭包的解释和预期方案我不做过多解释,我专门写有一篇博客介绍 JavaScript
闭包以及怎么处理它在循环中出现的问题。这是地址 JavaScript 闭包机制的详解。
当然,你也可以去MDN上看关于闭包的解释:MDN-闭包。
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"
接下来我再讲讲为什么。首先我先给出大家可能会疑惑的点:
arr2 = ['n', 'h', 'o', 'j', ['j', 'o', 'n', 'e', 's']]
,为什么arr1.slice(-1)
输出的结果会是j,o,n,e,s
?- 为什么 arr1 会输出和 arr2 一样的结果?
ok,我们一个点一个点来讲。首先说说第一点, arr.slice()
方法返回的会是一个新数组,而且它是浅拷贝。所以如果单独看这句代码:
console.log(arr2.slice(-1)) // logs [['j', 'o', 'n', 'e', 's']]
之所以会出现 last= j,o,n,e,s 这样的结果,关键就在于 console.log
中的 " last=" + 的这个 + 号。
我们先来看一个例子:
1 + [] = "1"
1 + {} = "1[object object]"
1 + NaN = NaN
[] + {} = "[object object]"
在 JavaScript
中加法其实归根到底还是 数字+数字 以及 字符串+字符串两种模式,所以在一个加法运算中,无论是什么类型的两个数据相加,结果必然是 number 或者 string 类型中的一个。至于这加法其中的门门道道,有兴趣的伙计可以看看我这篇博客:JavaScript 中神奇的加法。
再来说说第二点, 为什么 arr1
会输出和 arr2
一样的结果?如果你认真读过 《JavaScript 高级程序设计(第三版)》
的话,你应该就知道为什么了。
翻开 JS高程 第70页,有这么一段话:
当一个变量向另一个变量复制引用类型的值时,同样也会将储存在变量对象中的复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变一个变量,就会影响到另一个变量。
那么什么是引用类型的变量呢?很简单,object
类型的都是引用类型变量,除了它,其他的形如 number, string, boolean, null, undefined
都是数值类型的变量。
现在再回到我们上面那个题目,原因就一目了然了对吧。那么如果想避免这种情况怎么办?也很好办,第一个思路是重新给变量分配堆中的内存空间;第二个思路是将原对象解构后再重组,指针自然也就不再指向原来的对象了。我们来看例子:
var arr1 = "john".split('');
var arr2 = [].concat(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=4 last=j
array 2: length=5 last=j,o,n,e,s
当然,面对一些比较复杂的数据,你也可以尝试直接遍历解构来达到解除堆引用的目的。
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);
这是一个典型的 JavaScript
加减法问题,它体现了这门语言在类型转换和校验上的特点。
我们先来说说答案:
"122"
"32"
"02"
"112"
"NaN2"
NaN
我简单解释下为什么会得到以下结果,如果你想了解其中的工作原理,建议你去看看我这篇文章,相信会给你带来一些收获::JavaScript 中神奇的加法。
首先来看 1 + "2" + "2"
,根据加法中的 从左到右原则 和 凡有一个字符串就将其他变量转换为字符串原则 这两个原则,很容易得出 "122"
这个结果。
然后我们看看第2,3,4条,你会发现他们其实是类似的转换规则。无非就是在某个字符串前面加了一个+
或者-
。但是当这个+
号只出现在单个变量的前面时,它就成一个二元运算符变成了一个一元运算符,其作用是将这个变量转换成 number
类型,其功能与 Number()
类似。
So~,我们再来看第2,3,4条,无非就是先把+"2"
转为了2
,-"1"
转成了-1
,然后再进行字符串拼接。这并不困难,伙计们可以想想下面这几个会是什么结果:
+null = ?
+undefined = ?
+true = ?
+[] = ?
+{} = ?
+function(){} = ?
最后我们看看倒数两条,首先他们两都有减法,而减法是会将两个参与算术的变量都转成 number
类型,"A"
转成 number
类型自然就是 NaN
了。
16.如果数组列表太大,以下递归代码将导致堆栈溢出。你如何在保留递归的前提下解决这个问题?
var list = readHugeList();
var nextListItem = function() {var item = list.pop();if (item) {// process the list item...nextListItem();}
};
通过修改 nextListItem 函数可以避免潜在的堆栈溢出,如下所示:
var list = readHugeList();
var nextListItem = function() {var item = list.pop();if (item) {// process the list item...setTimeout( nextListItem, 0);}
};
利用事件循环机制(Event Loop
)来处理递归而不是通过调用堆栈,因此消除了堆栈溢出。当 nextListItem
被调用时,如果 item
不为空且不为 undefined
,定时器会将(nextListItem
)加到任务队列中并结束对该函数的调用,从而留下一个干净的调用堆栈。当任务队列开始执行这个定时器里的内容时,将会处理下一次的事件并再次设置一个定时器以调用 nextListItem
。因此,在没有进行直接递归调用的情况下来处理该函数,无论递归的次数是多少,调用栈都保持干净不会发生溢出现象。
17.什么是JavaScript中的“闭包”?举个例子。
我就不举例子也不解释了,大家可以直接看官网的答案,如果觉得它解释的不清晰的,可以看看我的这篇博客:JavaScript 闭包机制的详解
18.以下代码的输出结果如何,解释你的答案。如何使用闭包有助于此?
for (var i = 0; i < 5; i++) {setTimeout(function() { console.log(i); }, i * 1000 );
}
这是一个典型的闭包,之前有好几个类似的问题,它会输以下答案:
5
5
5
5
5
原因就是循环中定时器里的匿名函数形成了5个闭包,但是这5个闭包的执行上下文缺失同一个,因为早在定时器开始执行前 for 循环就已经结束了。
解决办法一般是三种:
- 用匿名执行函数把代码块包裹起来。
- 利用工厂函数。
- 利用 ES6 let 形成的块级作用域。
下面是示例:
// 匿名执行函数
for (var i = 0; i < 5; i++) {(function(i){setTimeout(function() { console.log(i); }, i * 1000 )})(i);
}//工厂函数
function logCallBack(i){return function(){console.log(i);}
}for (var i = 0; i < 5; i++) {setTimeout(logCallBack(i), i * 1000 );
}//ES6 let
for (let i = 0; i < 5; i++) {setTimeout(function() { console.log(i); }, i * 1000 );
}
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));
这主要是考察 JavaScript
逻辑运算符的作用。先看结果:
0 || 1 = 1
1 || 2 = 1
0 && 1 = 0
1 && 2 = 2
再来讲讲这三个逻辑运算符的作用,咱们先从逻辑与 && 开始.
1.逻辑与 &&
- 两边条件都为
true
时,结果才为true
; - 如果有一个为
false
,结果就为false
; - 当第一个条件为
false
时,就不再判断后面的条件。
注意:当数值参与逻辑与运算时,结果为 true,那么会返回的会是第二个为真的值;如果结果为 false,返回的会是第一个为假的值。
2.逻辑或 ||
- 只要有一个条件为
true
时,结果就为true
; - 当两个条件都为
false
时,结果才为false
; - 当一个条件为
true
时,后面的条件不再判断。
注意:当数值参与逻辑或运算时,结果为 true,会返回第一个为真的值;如果结果为 false,会返回第二个为假的值。
3.逻辑非 !
- 当条件为
false
时,结果为true
;反之亦然。
20.执行以下代码时输出结果是什么?请解释为什么。
console.log(false == '0')
console.log(false === '0')
这个其实也是隐式转换的问题。先说结果吧:
true
false
原因就是在 JavaScript
中 ==
运算符会进行隐式转换,也就是说上面的 fasle
会被 Number()
变成 0
然后再被 toString()
变成 "0"
,所以为 true
。
但是要注意的是,运算符 ===
是不会进行隐式转换的,因为它要对比你的变量类型。 false
为boolean
,"0"
为string
,所以为false
。
21.以下代码的输出是什么?请解释你的答案。
var a={},b={key:'b'},c={key:'c'};a[b]=123;
a[c]=456;console.log(a[b]);
此代码的输出将是 456
(而不是123
)。
原因如下:在设置对象的属性时,JavaScript
将隐式字符串化 key
值。在这种情况下,因为 b 和 c 都是对象,它们将都被转换成 "[object Object]"
。所以无论是 a[b]
还是 a[c]
其 key
值都相同。所以 a[c]=456
这一句代码实际上是更新了key为[object object]
的属性值。
22.以下代码将输出上面内容到控制台,并解释你的答案。(本题无过多深究内容,答案为官网答案)
console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));
代码将输出10阶乘的值(即10!或3,628,800)。
原因如下:
命名函数以 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未在内部函数中定义,因此在外部函数的范围内搜索已定义的变量x,该变量的值为1。
如果理解不了这段内容,请自行 google 或者看一下我这篇关于闭包的博客:JavaScript 闭包机制的详解
24.以下代码将输出上面内容到控制台?这段代码有什么问题,如何解决?
var hero = {_name: 'John Doe',getSecretIdentity: function (){return this._name;}
};var stoleSecretIdentity = hero.getSecretIdentity;console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());
这个问题本质上来说,是有关 JavaScript
中 this
的指向问题,先放上答案:
undefined
John Doe
为什么呢?我们先看第一个函数 stoleSecretIdentity()
,它的执行上下文是哪?很明显是window
,而第二个函数 hero.getSecretIdentity()
呢?执行上下文是 hero
,这就是他们两的输出结果不同的原因所在。
至于有关 this 的指向问题的详细解释,你可以 google 一下,也可以看看我这篇文章::javascript this探究
25.实现一个函数,其功能是在给定页面上的DOM元素的情况下,访问元素本身及其所有后代(而不仅仅是其直接子元素)。对于访问的每个元素,函数应该将该元素传递给提供的回调函数。
函数的参数应该是: 1.一个DOM元素。 2.回调函数(以DOM元素为参数)
访问树的所有元素是经典的优先深度优先搜索算法,看如下实例:
function Traverse(p_element,p_callback) {p_callback(p_element);var list = p_element.children;// 把len缓存起来在数据量大时可以提高部分性能for (var i = 0,len = list.length;i < len; i++) {Traverse(list[i],p_callback); // recursive call}
}
26.以下代码的输出是什么?
var length = 10;
function fn() {console.log(this.length);
}var obj = {length: 5,method: function(fn) {fn();arguments[0]();}
};obj.method(fn, 1);
很明显这又是一道this
指向的问题。但是它比之前的题目更隐蔽,需要你完全理解this
在 JavaScript
中是怎么调用的。
先看答案:
10
2
fn
被当成参数传给了 method
函数,而 obj.method(fn, 1)
的执行上下文是 windows
,所以 fn
的调用者显然就是 windows
,而此时 windows
环境中有一个 var length = 10;
,所以这时输出的就是 10
;我们再来看 arguments[0]();
这句代码,这句代码等同于下面这句:
var arguments = [fn, 1];
var fn = arguments[0];
fn();
事实上 arguments
是什么我们不再多说,如果你不了解我建议你多踏实基础。所以调用方或则说此时这个 fn
的执行上下文是 arguments
,它的 length
是 2
,所以最后输出 2
。
27.请考虑以下代码,输出内容是什么,为什么?
(function () {try {throw new Error();} catch (x) {var x = 1, y = 2;console.log(x);}console.log(x);console.log(y);
})();
这道题其实考察的是 JavaScript
引擎在执行代码时关于声明提示的问题。以上代码块也可以认为是下面这种形式:
(function () {var x,y;try {throw new Error();} catch (x) {x = 1;y = 2;console.log('inner',x);}console.log('outer',x);console.log('outer',y);
})();
我简单解释下,首先呢,JavaScript
中的变量提升我就不多说了,所以var x,y;
会在闭包的最顶端。其次就是这句代码了:
catch (x) {x = 1;y = 2;console.log(x);}
这个 catch(x)
是关键,它意味着对这个块级作用域内的x进行了局部声明,而撇开了外部的全局声明,所以 inner x
输出的值应该是 1
,而 outer x
输出的值应该是 undefined
。而 y
并没有被局部重新声明,所以 y
的输出应该就是 2
。
28.下面这段代码的输出是什么?
var x = 21;
var girl = function () {console.log(x);var x = 20;
};
girl ();
先说结果,输出是 undefined
。也许有人会认为是20,或者是21,但是很明显你们都有理解上的误差。我先举个大家好理解的例子:
var x = 21;
var girl = function () {console.log(x);
};
girl(); // 21
看到问题所在了吗?关键就在于 var x = 20;
这句代码,为什么?因为它会发生变量提升,对 window
环境下的变量 a
进行重新声明,就如同下面的代码一样:
var x = 21;
var girl = function () {var x;console.log(x);x = 20;
};
girl ();
所以,打印的结果自然也就是 undefined
了。
29.在 JavaScript
中如何克隆一个对象?
首先我们要明白一个概念,那就是克隆分为浅克隆和深克隆两种类型,我们通常说的对象克隆,其实就是指的对象的深克隆。
关于浅克隆深克隆的更为详细的解释,我就不在本文介绍了,大家可以看这篇博文 JavaScript 中的浅拷贝和深拷贝,下面我就简单介绍下两种深克隆的方式:
1.序列化与反序列化
const obj = {a:1, b:'str', c:{name:'waw', age:20}};
let objCopy = JSON.parse(JSON.stringify(obj));
objCopy.c.age = 18;
console.log(obj)
console.log(objCopy)
2.for...in
深递归
function isObject(obj){return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}function deepClone(obj){let isArray = Array.isArray(obj); var cloneObj = isArray ? [] : {};for(let key in obj){cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]}return cloneObj;
}let obj = {a:1, b:2, c:{name:'waw', age:10}};
let copyObj = deepClone(obj);
copyObj.c.age = 20;
console.log('obj', obj)
console.log('copyObj', copyObj)
30.以下这段代码输出什么内容?
for (let i = 0; i < 5; i++) {setTimeout(function() { console.log(i); }, i * 1000 );
}
这是一个典型的块级作用域的问题。先说答案,将会打印出0,1,2,3,4
。
原因就在于 let
会形成一个块级作用域,使得变量 i
只在 for
循环中生效。
31.以下这段代码输出什么内容?
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);
这道题我认为实际上考察的是 JavaScript
的类型隐式转换。
第一行代码console.log(1 < 2 < 3);
,首先进行的是1 < 2
,表达式成立返回结果 true
,然后进行第二个表达式 true <3
,这时 true
被隐式转换为 1
, 即 1 < 3
,表达式成立,最终返回结果 true
。
第二行代码console.log(3 > 2 > 1);
,首先进行的是3 > 2
,表达式成立返回结果 true
,然后进行第二个表达式 true > 1
,这时 true
被隐式转换为 1
, 即 1 > 1
,表达式不成立,最终返回结果 false
。
所以最后输出结果应为 true
和 false
。
32.JavaScript 中如何在数组的头部和尾部插入元素?
对于传统的ES5来说,我们可以通过以下方式来进行插入元素:
var arr = ['bb'];
arr.push('cc'); // ['bb', 'cc']
arr.unshift('aa') // ['aa', 'bb', 'cc']
对于ES6来说,我们可以使用扩展预算符进行解构赋值:
const arr = ['aa','bb'];
const arr2 = ['dd', ...arr, 'cc']; // ['dd', 'aa', 'bb', 'cc']
33.如果你有以下代码:
var a = [1, 2, 3];a) a[10] = 99;
b) console.log(a[6]);
a)这会导致崩溃吗?
b)这个输出是什么?
先说答案:
a> 不会崩溃,JavaScript
会将中间空闲的元素位置为空插槽(<7 empty items>
),实际输出如下:
[ 1, 2, 3, <7 empty items>, 99 ]
这里需要注意一点,这些空插槽在不同的环境下表现可能有所不同:
var a = [1, 2, 3];
a[10] = 99;for(let i = 0; i< a.length;i++){a[i] = 7;
}
console.log(a) // [7, 7, 7, 7, 7, 7, 7]
显然数组 a
的元素会变成7。
我们再看一个例子:
var a = [1, 2, 3];
a[10] = 99;
a.map(e => 7);
console.log(a) // [ 1, 2, 3, <7 empty items>, 99 ]
此时的结果却又不同于 for
循环,原因是 empty items
并没有在 map
中被赋值,而是保留了下来。
b> 很明显输出会是 undefined
。
34.typeof undefined == typeof NULL
会输出什么结果?
这道题是典型的心机题,因为此题中的 NULL
并不是我们所熟知的那个 null
,原因嘛,自然是 JavaScript
中是区分大小写的。
如果是 typeof undefined == typeof null
,那么输出结果自然是 false
, 因为typeof undefined = "undefined"
, 而 typeof null = "object"
。
而当 typeof undefined == typeof NULL
时,自然输出结果是 true
了,因为此时 NULL
相当于一个未定义的变量,typeof NULL = "undefined"
。
35.下面的代码会返回什么?
console.log(typeof typeof 1);
这道题主要考察你对 MDN
熟不熟,我们看下 MDN
上对 typeof
操作符的定义:
typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
所以答案显而易见就是 string
。
36.下面的代码会返回什么?
var b = 1;
function outer(){var b = 2;function inner(){b++;var b = 3;console.log(b)}inner();
}
outer();
这是一道考察 JavaScript
作用域链的问题,先说答案:3
。
要确定为什么,只需要确定两点:
- 输出的代码处在哪个作用域。
- 从此作用域往上去找需要输出的变量,找到实例为止。
在函数 inner
中是需要输出的作用域,而 inner
中本就有变量 b
的实例,所以 b
为 3。
此外,再说说在 inner
中的变量提升,inner
中的 b
将按照以下顺序执行:
function inner(){var b;b++; // b is undefinedb++; // b is NaNb = 3; // b is 3console.log // 3
}
TopTal 进阶 JavaScript 面试题相关推荐
- 你应该知道的25道Javascript面试题
题目来自 25 Essential JavaScript Interview Questions.闲来无事,正好切一下. 一 What is a potential pitfall with usin ...
- JavaScript面试题111-120
JavaScript面试题111-120 每日坚持学10道题 111. js数组去重(9种) [问答题] 用 JavaScript 脚本为 Array 对象添加一个去除重复项的方法. 来自:百度 参考 ...
- Android中高级进阶开发面试题冲刺合集(四)
以下主要针对往期收录的面试题进行一个分类归纳整理,方便大家统一回顾和参考.本篇是第四集~ 强调一下:因篇幅问题:文中只放部分内容,全部面试开发文档需要的可在公众号<Android苦做舟>获 ...
- 一道经典的JavaScript面试题
一道经典的JavaScript面试题 转载于:https://www.cnblogs.com/suoking/p/5227430.html
- 互联网公司前端初级Javascript面试题
互联网公司前端初级Javascript面试题 1.JavaScript是一门什么样的语言,它有哪些特点?(简述javascript语言的特点) JavaScript是一种基于对象(Object)和事件 ...
- 【转】进阶 JavaScript 必知的 33 个点【进阶必备】
转自:进阶 JavaScript 必知的 33 个点[进阶必备] 进阶 JavaScript 必知的 33 个点[进阶必备] Original 前端小菜鸡之菜鸡互啄 前端开发爱好者 2022-04-1 ...
- (转载)7个去伪存真的JavaScript面试题
7个去伪存真的JavaScript面试题 上周,我发表了<C#程序员的7个面试问题>.这次我要说的是如何淘汰那些滥竽充数的JavaScript程序员. 作者:小峰来源:码农网|2015-0 ...
- 一道隐藏欺诈的JavaScript面试题
一道隐藏欺诈的JavaScript面试题 转载于:https://www.cnblogs.com/suoking/p/5227426.html
- Android中高级进阶开发面试题冲刺合集(七)
以下主要针对往期收录的面试题进行一个分类归纳整理,方便大家统一回顾和参考.本篇是第七集~ 强调一下:因篇幅问题:文中只放部分内容,全部面试开发文档需要的可在公众号<Android苦做舟>获 ...
最新文章
- 推理计算过程_转导推理—Transductive Learning
- 携程App for Apple Watch探索
- 计蒜客 三值排序 (模拟)
- 比CopyMemory还要快的函数SuperCopyMemory
- Node介绍及环境配置~超级详细哦
- codeforces346e
- 视频教程-【孙伟】网页设计(切图)视频教程-UI
- 【绝密外泄】风哥Oracle数据库DBA高级工程师培训视频教程与内部资料v0.1
- 深度学习网络训练技巧篇:神经网络初始化tricks---何凯明大神2018年新作(随机初始化网络)
- 【图像处理】SFR算法详解1
- 处方常用拉丁词缩写与中文对照表
- tiny11安装中文
- 文件系统之EXT文件系统
- 图书借阅管理用java实现_用java实现图书管理系统。 - 惊觉...
- 机器学习(0):机器学习概述及基本概念
- shell 脚本 : 获取当前路径与当前路径下的目录列表
- 国内云服务器,服务商优缺点分析
- 堆在计算机中的作用,堆(数据结构)_百度百科
- 旅游景区怎么在抖音上卖门票?
- 舵机控制(0°与90°之间反复)
热门文章
- PTA 6-6 分数 分数 10 作者 翁恺 单位 浙江大学
- 关于win10笔记本无法连接外接显示器及连接HDMI显示器后没有声音的解决方案
- 27【源码】数据可视化大屏:基于 Echarts + Python Flask 实现的32-9超宽大屏范例 - 监控指挥中心
- 济南ISO三体系认证证书办理需要准备的材料有哪些
- 笔记本充不进电 Linux,华硕笔记本电池充不进电
- 8-1 学生成绩管理系统
- Visual Studio Code 自定义Snippet配置
- AutoCAD学习笔记——基本操作1
- Harris Corner(Harris角检测)
- Paddle Graph Learning (PGL)图学习之图游走类deepwalk、node2vec模型[系列四]