壹 ❀ 引
在7月21交接完所有工作后,我也进入了休年假的阶段(没用完的8天年假),看似休息内心的紧张感反而瞬间加倍,到今天为止也面了几家,好消息是工作机会特别特别多,一封简历没投,面试邀请源源不断,待下周一将三家面试走完后,后面我也要主动挑选一些感兴趣的公司进行投递,进一步确认自己在当前行业的定位。
好消息是,通过前面几轮面试积累与反馈,可以确信自己JavaScript基础还算不错,离职前的焦虑与不安少了些许。坏消息是当前环境对于vue的需求成了硬性指标,而我之前公司一直用的angular1,当下基本被淘汰(只针对框架使用率),当然angular的知识体系确实对于我学习vue有极大的帮助,这点我倒不是很担心。之前一直不够自信,JavaScript觉得学的不好,http知识得补,纠结angularjs还要不要复习,vue也得学等等,心里乱如麻,现在心里倒是清晰了不少,松了口气,以后也得加倍努力。
这篇文章原本应该是前几天发布,因为面试耽误了几天,起因是在掘金瞟到一篇考察JS基础的题目,原题网址在此。我将44题大致过了一遍,部分考点较为陈旧,也就是常说的用JS缺陷以及部分不规范写法作为考点来问你为什么,对于这种题目我一般是较为排斥的,因为较好的编程习惯,使用严格模式,ES6语法,typescript等等都能很轻易避开这些问题,技术是进步的,止步不前反复去研究缺陷我觉得是开倒车的行为。
当然题目中也存在一些平日容易忽略的点,比如哈希数组遍历,正则可能会踩的坑等等,所以整体下来还是有收获的,本篇文章将只记录我感兴趣或者觉得有复习意义的题,完整题目大家可以点击上方链接,那么本文开始。
贰 ❀ 快速过题,快速复习
第一题
["1", "2", "3"].map(parseInt)//?
出现频率较高的一题,答案我就直说了,为[1, NaN, NaN]
,以上代码等同于:
["1", "2", "3"].map((item, index) => parseInt(item, index));
本题考察点其实就是parseInt
第二参数,parseInt(string,radix)
接受2个参数,第一个参数大家熟悉,就是要转数字的字符串,而第二个参数则是转为数字的基础,比如是按照二进制还是十进制进行转换,关于第二参数大致分为如下几种情况:
所以上述等同于执行三次,分别是:
parseInt('1', 0);// 1
parseInt('2', 1);// 超出范围,为NaN
parseInt('3', 2);// 3无法用二进制规则解析,为NaN
第二题
[typeof null, null instanceof Object]//解析成什么?
首先我们知道null
的typeof
为Object
,这是因为JavaScript设计缺陷,具体原因是因为早期JavaScript的值由代表值类型和具体值两部分组成,而null被设定成了0x00,而0恰好表示的对象类型,因此才造成了typeof
检验出错的问题。
前面说了,null并不是对象,所以Object的原型并不会出现在null的原型链上,更何况null没有原型(null是原型链的顶端),第二个自然是false了。
答案是['Object' , false]
;
第三题
[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ]//解析成什么?
这里考察的其实就是reduce
的用法以及一些容易忽视的点,reduce
本质上是一个累加器,即每次遍历的结果都会累积给下次遍历使用,所以第一段代码其实等同于如下:
Math.pow(Math.pow(3,2),1);//9
需要注意的是,像foreach
这个的API遍历空数组,顶多就是不执行,但reduct
不同,它会报错。不仅如此,如果数组只有一个元素,且我们没为reduct
定义初始值同样也会报错,具体用法可以查看博主这篇文章JS reduce()方法详解,使用reduce数组去重。
因此答案其实就是报错不执行。
第四题
// 最终输出什么?
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
老生常谈的变量提升问题,上述代码其实等同于:
var name = 'World!';
(function () {
var name;
if (typeof name === 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
所以当执行到typeof
时name并没有值,因此走了前面的分支,最终输出Goodbye Jack
。
补充一点,怕大家弄混淆,下面代码输出什么?
var name = 'echo';
var name;
console.log(name)//?
有同学可能会说,是undefined
,其实还是echo
,这是因为在执行上下文创建时,会在标识符中记录变量名,对于已存在的变量不会重复声明,因此上述代码第二次var name
其实是无意义的,不要把它理解成var name = undefined
。
那题目中为什么变量提升了name
不是World
呢?这是因为这里有两个上下文啊,一个全局上下文,一个函数执行上下文,在两个执行上下文中有两个同名变量,本质并不是一个。
最简单的证明方法是:
var name = 'echo';
function fn(){
var name;
console.log(name);
};
fn();//undefined
如果你对于变量提升比较懵,建议阅读博主这篇文章【JS点滴】声明提前,变量声明提前,函数声明提前,声明提前的先后顺序;当然,变量提升本质其实就是执行上下文问题,所以我还是推荐阅读这篇文章一篇文章看懂JS执行上下文,毕竟知其然知其所以然。
第五题
var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
count++;
}
console.log(count);//?
定眼一看,这里肯定输出100啊,因为一开始就让END减去了100然后进行for循环让count
自增,但其实这里会陷入死循环。这是因为JavaScript中数字范围最大也就是2^53
,因为当执行了100次之后2^53
自增加1仍然会是2^53
,而i <= END
因此进入死循环。
第六题
var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});//得到什么?
首先看到我们声明了一个数组[0,1,2]
,随后在索引10处添加了一个10,因此此时数组ary成了稀疏数组[0, 1, 2, empty × 7, 10]
,需要注意的是empty并不会执行,因此上述代码中没有一个与undefined
相同,因此返回空数组[]
;
第七题
// 以下两段代码分别输出什么?
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'));
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'));
本题说到底其实就是在问new String()
与String()
的区别,本质上也算是JavaScript缺陷了,这个缺陷就是构造函数竟可以new调用,也可以普通调用。而new调用其实是得到一个对象,普通调用时其实就是在做单纯的类型转换,具体如下图:
所以第一段代码其实拿的就是一个对象,因此输出Do not know!
,第二段代码传递的就是一个普通字符串,因此输出Case A
。
ES6推出了class类,class只能被new调用,普通调用就会报错。
第八题
// 下方代码输出什么?
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);
考了个逻辑或||用于函数调用的情况,比如A||B,如果A为true则会跳过B判断。如果A为false则会继续判断B;由此可知上述代码其实就是将数组中每个元素分别丢到两个函数中进行判断。
唯一需要注意的几个点是,字符串求余会被转成数字,Infinity不是一个确切的数字,所以无法求余数,所以答案为[true,true,true,false,false]
;
第九题
Array.isArray( Array.prototype )//?
我印象里所有函数元素都是对象,包括了Object,String这些构造器,但是很遗憾,Array的原型还偏偏是个数组,只能说算是个冷知识吧,知道就行了。所以这里为true。
第十题
// 输出什么?
var a = [0];
if ([0]) {
console.log(a == true);
} else {
console.log("wut");
}
考的隐式转行,对于空数组,空对象,在if判断中其实会被转为true;但是a == true
这里可就不是隐式转换了,自然不相等,所以输出false。
第十一题
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1);//输出多少?
这题考了函数的按值传递,第一次调用了bar
函数,传递了3个1,但执行时c被修改为10,所以此时abc分别为1,1,10。此时又调用了sidEffecting
函数,传递的是arguments对象,说到底就是前面abc的引用,在sidEffecting
中将a改成了10,所以此时abc分别为10,1,10,所以最终返回21。
注意,调用sidEffecting
传递的其实是形参abc的引用,所以修改后直接对原本的abc造成了影响,这也是解答本题的关键所在。
第十二题
[1 < 2 < 3, 3 < 2 < 1]//解析成什么?
这题考的是运算符优先级,不知道大家在编程时有没有这样写过,结果大部分情况都没按照自己的预期执行,这是因为上述代码等同于:
1 < 2 < 3 => true < 3 => 1 < 3 => true
3 < 2 < 1 => false < 1 => 0 < 1 => true
所以这里输出了2个true也就是[true,true]
。
第十三题
3.toString()//?
3..toString()//?
3...toString()//?
第一个报错,JavaScript会解析成(3.)toString
,3.被理解成浮点数,所以没有.能用于调用了,所以报错。
第二个正常转换为字符串3,因为会被解析为(3.).toString
。
第三个报错,鬼会这么写。
所以如果我们要将一个整数转为字符串,除了上面第二种写法外,还可以这样:
(3).toString();//'3'
3+"";//'3'
第十四题
(function(){
var x = y = 1;
})();
console.log(y);//?
console.log(x);//?
上述代码其实等同于:
var y = 1
(function(){
var x = y;
})();
console.log(y);//1
console.log(x);//局部变量函数外无法访问
第十五题
var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]// ?
这题毋庸置疑考原型,我们知道只有函数才有prototype,所以a只是一个普通对象,没有prototype,第一个比较注定为false。
在看第二个比较,Object.getPrototypeOf(a)
用于获取a的原型,a的原型不就是构造器Object()
的prototype,两者指向同一个引用,所以第二个比较为true,答案为[false,true]
。
关于原型和原型链,可以阅读博主这两篇文章,保证整的明明白白
JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链
JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈
第十六题
function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b;//?
同考原型,a取的其实就是函数f的原型,也就是一个对象。b取得是啥得知道getPrototypeOf取得是啥。Object.getPrototypeOf()
方法返回指定对象的原型 ( 即, 内部[[Prototype]]属性)。所以b拿的其实是构造器Function()的原型,a和b取的不是一个东西,自然为false。
但下面这两个是相同的:
function f() {};
var b = Object.getPrototypeOf(f);
var c = f.__proto__;
b === c//true
与第十五题一样,推荐阅读原型相关文章。
第十七题
function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]
一个容易忽略的点,函数的name是只读属性不可修改,所以输出两个foo。
第十八题
"1 2 3".replace(/\d/g, parseInt)
这里不仅是考了parseInt第二参数,其实还考了replace第二参数其实可以是一个回调函数的用法,大家先看一个例子:
"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
console.log([match, $1, $2, index, input]);
//输出
["1234", "1", "4", 0, "1234 2345 3456"]
["2345", "2", "5", 5, "1234 2345 3456"]
["3456", "3", "6", 10, "1234 2345 3456"]
});
这里的参数分别表示为:
match:符合条件的匹配结果
$1:分组1的匹配内容
$2:分组2的匹配内容
index:每次匹配成功的索引
input:匹配输入
但由于题目中的正则条件并没有分组,所以$1与$2失效,再来看个例子:
"1234 2345 3456".replace(/\d\d{2}\d/g, function (match, $1, $2, index, input) {
console.log([match, $1, $2, index, input]);
//输出
["1234", 0, "1234 2345 3456", undefined, undefined]
["2345", 5, "1234 2345 3456", undefined, undefined]
["3456", 10, "1234 2345 3456", undefined, undefined]
});
题目中提供的字符串为1 2 3
,带空格,所以匹配了3次,等同于:
parseInt(1,0);//1
parseInt(2,2);//NaN
parseInt(3,4);//3
上面的0,2,4哪来的?其实就是每次匹配成功当前的下标。
如果这道题的题解看不懂,建议系统学习正则,可以查看博主正则专栏:从零开始学正则。
第十九题
var lowerCaseOnly = /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]
输出两个true,也算是正则容易忽视的坑,test在检测时会隐性将内容转为字符串,所以上述检测其实等同于:
[lowerCaseOnly.test('null'), lowerCaseOnly.test('undefined')]
第二十题
if ('http://giftwrapped.com/picture.jpg'.match('.gif')) {
'a gif file'
} else {
'not a gif file'
}
这题也是考的正则的隐式转换,match第一个参数接受一个正则表达式或者一个字符串,但如果是字符串会隐式转为正则,所以上述代码等同于:
'http://giftwrapped.com/picture.jpg'.match(/.gif/)
而在正则中.表示通配符,所以成功匹配到/gif
,匹配成功,输出a gif file
。