最近有位同学分享给我一个网站,上面列举了 44 道 JavaScript 的送命题,想让我做一下。
对于这种要求我能不满足么,果断开搞哇。
网站地址:http://javascript-puzzlers.herokuapp.com/
这个网站的名字叫做 JavaScript Puzzlers! ,直译过来就是 JavaScript 的难题,里面大部分都是让人摸不到头脑的题目,我被活活的虐待了近 4 个小时,简直毫无人性好不好。
客观的说一句,对于不是专业前端的人来讲,有点难的过分了。
这套题目做下来的结果是不仅开始怀疑智商,而且是已经上升到怀疑人生的高度了。
里面有一部分题目作者自己也解释不清,只能通过实验得到答案。
这 44 个问题是在 ECMA 262(5.1) 环境下,浏览器中试验的,如果是 node 环境下可能不同。
所有的题目打开 Chrome 的 F12 在 console 中输入题目都能得到答案,至少实验得到的答案的途径是相当方便的。
本文内容过于硬核,并且全程无尿点,需要具备一定的 JavaScript 基础,并且最好还能自备一点速效救心丸。
1. map 和 parseInt
题目:["1", "2", "3"].map(parseInt)
的结果是什么?
知识点:
- map
- parseInt
map 的原型方法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
map 可以接受两个参数,一个是回调函数 callback 。
- callback:必须。函数,数组中的每个元素都会执行这个函数
- currentValue:必须。当前元素的值
- index:可选。当前元素的索引值
- array:可选。当前元素属于的数组对象
- thisArg:可选。对象作为该执行回调时使用,传递给函数,用作 “this” 的值。如果省略了 thisArg ,或者传入 null、undefined,那么回调函数的 this 为全局对象。
parseInt 的语法:
parseInt(string, radix)
- string:必需。要被解析的字符串。
- radix:可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
那么这道题把 map 函数去掉的话,实际上相当于下面 3 句话:
parseInt('1', 0);
parseInt('2', 1);
parseInt('3', 2);
上面这三句话只有第一句会把 radix 这个参数默认为 10 以外,其他都不满足 radix 介于 2 ~ 36 之间,所以这个题的返回值是 [1, NaN, NaN] 。
2. typeof 和 instanceof
题目:[typeof null, null instanceof Object]
的结果是什么?
知识点:
- typeof
- instanceof
typeof 的一些常见结果:
type | result |
---|---|
Undefined | “undefined” |
Null | “object” |
Boolean | “boolean” |
Number | “number” |
String | “string” |
Symbol | “symbol” |
Host object | Implementation-dependent |
Function对象 | “function” |
Object | “object” |
不要问为啥,就是这么定义的。
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
所以这道题的结果是:[object, false]
3. reduce 和 Math.pow
题目:[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ]
的结果是什么?
知识点:
- reduce
- Math.pow
arr.reduce 的原型方法如下:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
arr.reduce 方法对数组中每个元素执行一个自定义的 reducer 函数,并将结果汇总为单个值,这个常常用来累加一个数组中的数字。
- callback:必需。用于执行每个数组元素的函数。
- accumulator:它是上一次回调函数时得到的值,或者initialValue。
- currentValue:数组中正在处理的当前元素。
- index:可选,数组中正在处理的元素的索引,如果提供了 initialValue ,则起始索引号为 0 ,否则从索引 1 开始。
- initialValue:作为第一次调用 callback 时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用 reduce 将报错。
注意:如果没有提供 initialValue,reduce 会从索引 1 的地方开始执行 callback 方法,跳过第一个索引。如果提供 initialValue ,从索引 0 开始。
Math.pow(base, exponent),返回基数 base 的指数 exponent 次幂。
所以这道题里面的第一个表达式在调用 reduce 方法的时候没有提供 initialValue ,从索引 1 开始执行,第一次执行的时候 accumulator 取 arr[0] ,这里是 3 , currentValue 取第二个值,这里是 2 ,传给 Math.pow ,得到 9 。
第二个表达式是在空数组上调用 reduce ,并且没有提供 initialValue ,所以直接报错 Uncaught TypeError: Reduce of empty array with no initial value 。
所以最后整个表达式的结果还是抛出错误。
4. 三目运算符优先级
题目:下面这段代码的输出是什么?
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
这个题实际是在考察三目运算符和加号的优先级,三目运算符的优先级相当低,所以先计算加号再计算三目运算符。
这道题实际等价于 'Value is true' ? 'Somthing' : 'Nonthing'
,而不是看起来的 'Value is' + (true ? 'Something' : 'Nonthing')
。
所以结果是: Something 。
5. 变量提升
题目:下面这段代码的输出是什么?
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
在 JavaScript 中, functions 和 variables 会被提升。变量提升是 JavaScript 将声明移至作用域 scope (全局域或者当前函数作用域) 顶部的行为,所以上面这段代码其实相当于:
var name = 'World!';
(function () {
var name = 'undefined';
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
因为代码放在一个闭包里,用外层那个 name = 'World!'
是不能访问的,闭包有隔离变量的作用。
这道题的答案是:Goodbye Jack 。
6. JavaScript 最大数
题目:下面这段代码的输出是什么?
var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
count++;
}
console.log(count);
这道题看起来是在考察循环,实际上是在考察 JavaScript 能表示的最大的数。
在 JavaScript 里, Math.pow(2, 53) == 9007199254740992 是可以表示的最大值。
而最大值 +1 后还是最大值,所以循环并不会停下来,会成为无限循环。
7. 稀疏数组
题目:下面这段代码的输出是什么?
var ary = [0,1,2];
ary[10] = 10;
var result = ary.filter(function(x) { return x === undefined;});
这道题考察的是稀疏数组,在数组中未被赋值的元素是空。
这道题中,首先给 ary 数组的前三位赋值,然后又给第 11 位赋值,在中间夹杂的 7 位都是空的,并不是 undefined 。
所以在 filter 语句中,返回的是空数组 [] 。
8. 数字精度
题目:下面这段代码的输出是什么?
var two = 0.2
var one = 0.1
var eight = 0.8
var six = 0.6
[two - one == one, eight - six == two]
JavaScript 的 Number 类型为双精度 IEEE 754 64 位浮点类型。
问题产生的原因是因为计算机是通过二进制进行计算的,而二进制的实现和位数的限制导致有些数无法有限表示。
例如浮点数四舍五入,会模仿十进制进行四舍五入,由于二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这是计算机中部分浮点数运算时出现误差,丢失精度的根本原因。
遵循 IEEE 754 标准的语言都有这个问题,比如说另外几个我非常熟悉的语言:Java 和 Python。
看似有穷的数字, 在计算机的二进制表示里却是无穷的,由于存储位数限制因此存在「舍去」,精度丢失就发生了。
这就造成了 0.2 + 0.1 = 0.30000000000000004 和 0.8 - 0.6 = 0.20000000000000007 。
所以这道题的结果是 [true, false]
。
9. switch 中的 case
题目:下面这段代码的输出是什么?
function showCase(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase(new String('A'));
这道题是考察 case 语句的判断方式,在 JavaScript 中 case 是使用 ===
来判断的,这就造成了 A !== new String('A')
,所以最后的结果是 ‘Do not know!’ 。
这里都说道 ===
了那就顺便再多一句嘴,说下 ==
,==
会调用对象的 toString() 方法,这样就会产生 A == new String('A')
了。
10. String() 函数
题目:下面这段代码的输出是什么?
function showCase2(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase2(String('A'));
这道题和上面一道题很像,但是不一样, String('A')
并不会去创建一个对象,而是会返回一个字符串 'A'
,所以这里最终会打印 'Case A'
。
11. 求余数运算
题目:下面这段代码的输出是什么?
function isOdd(num) {
return num % 2 == 1;
}
function isEven(num) {
return num % 2 == 0;
}
function isSane(num) {
return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);
这道题是考察求余数,前面两个数字 7 和 4 没什么好说的,字符串 '13'
在计算的过程中会转化成整形参与计算,余数为 1 ,而 -9 参与运算会保留负号为 -1 , Infinity 参与计算会得到 NaN ,最后要注意一点是两个或 |
符号计算时,只要有一个为 true 则都为 true ,所以结果是 [true, true, true, false, false]
。
12. parseInt()
题目:下面这段代码的输出是什么?
parseInt(3, 8)
parseInt(3, 2)
parseInt(3, 0)
这道题翻译一下:
把 8 进制里的 3 转化成 10 进制,结果是 3 。
把 2 进制中的 3 转换成 10 进制, 2 进制中没有 3 ,所以结果是 NaN 。
把 10 进制中的 3 转换成 10 进制,结果当然还是 3 。
13. Array.prototype
题目:下面这段代码的输出是什么?
console.log(Array.isArray(Array.prototype))
这道题是在问 Array.property 是一个 Array 函数的原型对象?还是一个数组?
在浏览器中试一下,结果是 true 。
14. if
题目:下面这段代码的输出是什么?
var a = [0];
if ([0]) {
console.log(a == true);
} else {
console.log("wut");
}
这道题是在问 JavaScript 中的 if 条件语句在什么情况下为 true 。
MDN 中有明确的介绍:
任何一个值,只要它不是 undefined、null、 0、NaN或空字符串(""),那么无论是任何对象,即使是值为假的Boolean对象,在条件语句中都为真。
这道题接下来就是在问 [0] == true
的结果,当然结果是 false 。
15. 对象比较
题目:下面这段代码的输出是什么?
[]==[]
[]
是一个空数组,数组属于对象,而两个对象是怎么也不可能相等的,所以答案是 false 。
16. + 号
题目:下面这段代码的输出是什么?
'5' + 3
'5' - 3
加号遇到字符串的时候就是字符串连接符,而减号正好相反,它是把字符串转换成整形,所以这道题的结果是 ['53', 2]
。
17. 加减和正负号运算
题目:下面这段代码的输出是什么?
1 + - + + + - + 1
这道题我稍微翻译一下,实际上可以修改为 1 + (- + + + - + 1) ,接下来就是负负得正,正正得正,正负得负,所以最后得到的结果是 2 。
18. 稀疏数组
题目:下面这段代码的输出是什么?
var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; });
还是稀疏数组, map 函数会忽略掉其中的空元素,所以最终输出的结果是 ["1", empty × 2]
。
19. argument
题目:下面这段代码的输出是什么?
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
argument 是一个类数组对象,修改对象的属性值会影响其他使用到对象的地方,即使变量不在同一范围内。
跟着算下来结果是 21 。
20. JavaScript 最大数
题目:下面这段代码的输出是什么?
var a = 111111111111111110000,
b = 1111;
a + b;
又是一个最大数问题, JavaScript 中最大值是 Math.pow(2, 53) = 9007199254740992 。
超过这个数以后就不准确了,不准确的含义是不确定。所以这里输出的还是 111111111111111110000 ,是 a 的值。
21. Array.property.reverse
题目:下面这段代码的输出是什么?
var x = [].reverse;
x();
这道题没搞懂,原文的解释是说 reverse 方法会返回调用这个方法的数组本身(就是this),但是 x() 没有调用者,所以 this 指向了全局对象 window 。
但是我在 Chrome 浏览器中尝试的时候是得到了一个错误信息:Uncaught TypeError: Cannot convert undefined or null to object at reverse (<anonymous>)
。
22. Number.MIN_VALUE
题目:下面这段代码的输出是什么?
Number.MIN_VALUE > 0
这个是在问 Number.MIN_VALUE 的值。
实际上 Number.MIN_VALUE 表示最小正数,即最接近 0 的正数 (实际上不会变成 0)。最大的负数是 -MIN_VALUE 。
所以 Number.MIN_VALUE 是大于 0 的,这里输出是 true 。
23. boolean 强制转换
题目:下面这段代码的输出是什么?
[1 < 2 < 3, 3 < 2 < 1]
先简单转换下: [true < 3, false < 1]
。
在大于号,小于号运算中 true 会被强制转换为 1 , false 会被强制转换成 0 。
所以这里的结果是 [true, true]
。
24. 数组比较
题目:下面这段代码的输出是什么?
2 == [[[2]]]
这个题作者骂娘了,原文是 the most classic wtf 。
先公布答案,这个题是 true ,但是我没懂,
自己强行编一个理由解释一波, ==
在遇到数组的时候会将数组变成字符串然后进行比较。
25. 3. 和 .3
题目:下面这段代码的输出是什么?
3.toString()
3..toString()
3...toString()
首先搞清楚一件事情,3. 和 .3 都是合法的数字,一个是省略了后面的 0 ,一个是省略了前面的 0 ,都是可以的。
第一句中 toString() 不是数字,所以会报错:Uncaught SyntaxError: Invalid or unexpected token 。
第二句话可以这么拆开看:(3.).toString()
,这样输出一个字符串 '3'
是没有问题的。
第三句话 …toString() 不是一个合法的数字,所以一样是报错。
26. var 和闭包
题目:下面这段代码的输出是什么?
(function(){
var x = y = 1;
})();
console.log(y);
console.log(x);
因为闭包有隔离变量的作用,并且 var 不能提升变量,在闭包外部 x 是访问不到的,所以会输出 Undefined ,但是这里声明 y 是直接 y = 1 ,这样声明的 y 反而是全局的,所以外部可以直接访问到 y 。
27. 正则表达式
题目:下面这段代码的输出是什么?
var a = /123/,
b = /123/;
a == b
a === b
正则表达式是对象,这是两个对象,所以他们是不会相等的,结果是两个false 。
28. 数组比较
题目:下面这段代码的输出是什么?
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4]
a == b
a === b
a > c
a < c
这道题是数组的比较。
使用 ==
或者 ===
判断两个数组是不会相等的,所以前两个是 false 。
而大于,小于比较是按照顺序比较的,如 a[0]
和 c[0]
相等,接着比较 a[1]
和 c[1]
,然后顺次往下比较,直到比出结果。
29. 构造函数的原型
题目:下面这段代码的输出是什么?
var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]
JavaScript 中函数才有 prototype 属性,对象是没有的,对象有 proto ,指向对象的构造函数的原型,所以 a.prototype 是 undefined 。 b 是 Object 函数的原型,是一个对象,所以第一个是 false 。第二个是使用 getPrototypeOf 方法获取对象 a 的原型(即 a.proto ),这个和 Object.prototype 相等的,所以第二个表达式返回 true 。
30. 函数的原型对象
题目:下面这段代码的输出是什么?
function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b
这还是考察函数原型。
这里 f.prototype 是获取函数 f 的原型对象, Object.getPrototypeOf(f) 是获取对象构造函数的原型,注意函数也是对象,它的构造函数是 Function() ,因此f的构造函数的原型是 Function.property ,因此这这里输出 false 。
31. Function.name
题目:下面这段代码的输出是什么?
function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]
这道题是在考察函数的属性 name ,function.name 属性返回函数实例的名称,这个属性是不可写的。
所以这一题输出的是[‘foo’, ‘foo’]。
32. str.replace
题目:下面这段代码的输出是什么?
"1 2 3".replace(/\d/g, parseInt)
str.replace 方法的原型如下:
str.replace(regexp|substr, newSubStr|function)
参数解释:
- regexp (pattern):一个正则表达式对象(即RegExp对象)或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。它和substr是二选一的。
- substr (pattern):一个将被newSubStr替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅第一个匹配项会被替换。它和regexp是二选一的。
- newSubStr (replacement):用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名。参考下面的使用字符串作为参数。它和function是二选一的。
变量名 | 代表的值 |
---|---|
$$ | 插入一个 “$”。 |
$& | 插入匹配的子串。 |
$` | 插入当前匹配的子串左边的内容。 |
$’ | 插入当前匹配的子串右边的内容。 |
$n | 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始 |
- function (replacement):一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。它和newSubStr是二选一的。
变量名 | 代表的值 |
---|---|
match | 匹配的子串。(对应于上述的$&。) |
p1,p2, … | 假如replace()方法的第一个参数是一个RegExp 对象,则代表第n个括号匹配的字符串。(对应于上述的1,2等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+,p2 就是匹配的 \b+。 |
offset | 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 ‘abcd’,匹配到的子字符串是 ‘bc’,那么这个参数将会是 1) |
string | 被匹配的原字符串。 |
NamedCaptureGroup | 命名捕获组匹配的对象 |
replace 方法相当的复杂,这道题中,第一个参数是一个正则表达式,第二个参数是一个函数。
那么可以相当于执行下面的语句:
parseInt(‘1’, 0) // 10进制里的1是1
parseInt(‘2’, 2) // 2进制里没有2,所以NaN
parseInt(‘3’, 4) // 4进制中的3是3
所以最终结果是"1 NaN 3"
33. eval
题目:下面这段代码的输出是什么?
function f() {}
var parent = Object.getPrototypeOf(f);
f.name // ?
parent.name // ?
typeof eval(f.name) // ?
typeof eval(parent.name) // ?
第一句f.name就是「f」。
parent 是 Function.prototype ,这是一个对象,它没有 name 属性,所以第二句应该是啥都不输出。
eval() 函数会将传入的字符串当做 JavaScript 代码进行执行,eval(f.name)相当 于eval(‘f’) ,执行结果是输出函数 f 的内容,type of 计算返回 function 。
最后一句返回空。
34. exp.test
题目:下面这段代码的输出是什么?
var lowerCaseOnly = /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]
RegExp.prototype.test() 方法的参数是一个字符串,如果不是字符串会尝试转换成字符串。
这两句相当于 lowerCaseOnly.test(“null”),lowerCaseOnly.test(”Undefined“) ,所以返回 [true, true] 。
35. 数组
题目:下面这段代码的输出是什么?
[,,,].join(", ")
这道题其实是在考察数组, [,] 实际上是定义了一个有三个空元素的数组,输出是 [empty × 3] 。
三个空元素用逗号连接起来最后输出是两个逗号,因为都是空元素,其实最后一个逗号后面是有一个空元素的。
36. class
题目:下面这段代码的输出是什么?
var a = {class: "Animal", name: 'Fido'};
a.class
class 是关键字,尽量不要使用,我在 Chrome 的浏览器中得到的结果是 "Animal"
,虽然答案并不是这个,但是还是要注意不要使用关键字。
37. 时间转换
题目:下面这段代码的输出是什么?
var a = new Date("epoch")
new Date() 构造函数传入的必须是一个时间字符串,即可以通过 Date.parse() 解析成功。所以本题的答案是 Invalid Date 。
38. 函数形参
题目:下面这段代码的输出是什么?
var a = Function.length,
b = new Function().length
a === b
Function 构造器本身也是个 Function 。他的 length 属性值为 1,所以 a=1 。
new Function() 是一个对象,他没有形参,说以 b=0 ,本题输出 false 。
39. 时间转换
题目:下面这段代码的输出是什么?
var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c]
a 是一个时间字符串, b 是一个时间对象,表示格林威治 0 时, c 也是一个时间对象,是当前时间。
所以这道题的结果是 [false, false, false]
。
40. Math.max() 和 Math.min()
题目:下面这段代码的输出是什么?
var min = Math.min(), max = Math.max()
min < max
Max.max() 和 Max.min() 传入的参数是一个数组,如果不传参数,前者返回 +Infinity ,后者返回 -Infinity 。
Infinity 不能做比较,所以这道题的结果是 false 。
41. 正则表达式的记忆功能
题目:下面这段代码的输出是什么?
function captureOne(re, str) {
var match = re.exec(str);
return match && match[1];
}
var numRe = /num=(\d+)/ig,
wordRe = /word=(\w+)/i,
a1 = captureOne(numRe, "num=1"),
a2 = captureOne(wordRe, "word=1"),
a3 = captureOne(numRe, "NUM=2"),
a4 = captureOne(wordRe, "WORD=2");
[a1 === a2, a3 === a4]
正则表达式的 /g 表示全局匹配,找到所有匹配,而不是在第一个匹配后停止。
第一个正则有 /g ,第一次匹配成功之后会从当前位置往后查找,在执行 a3 = captureOne(numRe, “NUM=2”) 的时候,不是从字符串位置 0 开始查找的,而是从第 5 位开始,所以这里会匹配失败。
所以本题的结果是 [true, false]
。
42. Date
题目:下面这段代码的输出是什么?
var a = new Date("2014-03-19"),
b = new Date(2014, 03, 19);
[a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]
这个问题考察的是 Date 对象中的月份问题,使用第二种方式初始化时间对象的时候, Month 是从 0 开始起算的,3 表示的是 4 月份,所以这里的结果是 [false, false]
。
43. 正则表达式
题目:下面这段代码的输出是什么?
if ('http://giftwrapped.com/picture.jpg'.match('.gif')) {
'a gif file'
} else {
'not a gif file'
}
String.prototype.match 接受一个正则, 如果不是, 按照 new RegExp(obj) 转化. 所以 . 并不会转义,所以开头的 ‘http://gifwrapped…’ 中 /gif 就匹配了 /.gif/,所以输出 ‘a gif file’ 。
44. 变量提升
题目:下面这段代码的输出是什么?
function foo(a) {
var a;
return a;
}
function bar(a) {
var a = 'bye';
return a;
}
[foo('hello'), bar('hello')]
a 作为参数其实已经声明了,所以在两个方法中的 var a; var a = ‘bye’ 其实就是 a; a =‘bye’,所以这道题的结果是 ["hello", "bye"]
。
最后说一句成绩, 44 道题共计做对了 18 道,没比概率高多少,感受到了满满的套路,心态有点爆炸。
参考
https://www.cnblogs.com/lpfuture/p/5996230.html
https://www.cnblogs.com/tylerdonet/p/12742547.html