乘兴裸辞心甚爽,面试工作屡遭难。 幸得每日一题伴,点击关注莫偷懒。
又要到金九银十的跳槽季了,为了让更多的小伙伴可以在面试的时候取的更好的offer,所以自上月起我每天都会在自己的公众号【前端有的玩】里面推送一到两道面试题,俗称每日一题(每日一坑)。方便找工作的小伙伴每日都会有新的收获。本文就是小编将前期的一些比较经典的每日一题进行了梳理,欢迎大家一起来看看。本文内容首发于公众号【前端有的玩】,关注 === 学会。
一道类数组相关面试题
什么是类数组,类数组就是 拥有length属性,且其他属性(索引)为非负整数的对象,且不具备数组所用于的方法。比如我们常用的document.querySelectorAll返回的NodeLists就是一个类数组。这道题就是和类数组相关的内容.
题目
请说出以下代码输出的内容,需要区分nodejs,chrome以及chrome去掉splice之后的输出内容
1var obj = { 2 '2': 3, 3 '3': 4, 4 'length': 2, 5 'splice': Array.prototype.splice, 6 'push': Array.prototype.push 7} 8obj.push(1) 9obj.push(2) 10console.log(obj)
答案
这道题一共问了三种情况下面的输出,下面依次说明答案
-
node下面输出1{ '2': 1, 2 '3': 2, 3 length: 4, 4 splice: [Function: splice], 5 push: [Function: push] } -
chrome下面输出[empty × 2, 1, 2, splice: ƒ, push: ƒ] -
chrome去掉splice下面输出{2: 1, 3: 2, length: 4, push: ƒ}
通过上面输出的内容,可以看出相同的代码,不同情况输出的内容是有所不同的,下面进行详细分解。
题解
在解答题目之前,我们再看看这段代码
1const arr = new Array(2) 2// 输出 2 [empty * 2] 3console.log(arr.length, arr) 4arr.push(1) 5// 输出 3 [empty * 2, 1] 6console.log(arr.length, arr)
可以看到push方法会将数组的length + 1, 然后将值放在索引为length - 1的位置,比如上面的代码,因为在初始化数组的时候,已经将数组长度指定为了2, 所以在push之后length就变成了3,然后arr[3 - 1] = 1
在MDN上面对push的方法的解释是:
push方法具有通用性。该方法和call()或apply()一起使用时,可应用在类似数组的对象上。push方法根据length属性来决定从哪里开始插入给定的值。如果length不能被转成一个数值,则插入的元素索引为 0,包括length不存在时。当length不存在时,将会创建它。
根据MDN解释,push既可以使用到数组中,也可以使用到类数组中。而根据前文中对类数组的解释,可以看到题目中的obj就是一个标准的类数组,那就可以在obj上面使用数组的push方法。
再看obj.push(1), 因为obj.length = 2, 所以会将length + 1就变成了3, 这时候 索引值时obj[3 - 1] = 1 即obj[2] = 1, 同理 obj.push(2) 也一样的。因为在obj中已经有了属性(索引)2和3,所以在push的时候会覆盖掉2和3上面的默认值。
所以在nodejs中就会输出
1{ '2': 1, 2 '3': 2, 3 length: 4, 4 splice: [Function: splice], 5 push: [Function: push] }
但是在chrome控制台中输出
[empty × 2, 1, 2, splice: ƒ, push: ƒ]
很奇怪,为什么会输出这样呢?这一块有一个很特殊的陷阱,就是chrome控制台是如何判断打印的内容是数组还是其他对象呢?对于这个,chrome就是通过判断对象上面是否有splice和length这两个属性来判断的,所以如果你将splice去掉之后,就会输出以下内容
{2: 1, 3: 2, length: 4, push: ƒ}
你也可以试试下面的代码:
1console.log({splice:function(){},length:1}) 2console.log({slice:function(){},length:1})
逻辑面试题之小鼠喝毒药
小编当年毕业的时候面试就遇到过好几次逻辑类的面试题,这道题就是一道逻辑类的面试题,一起来看看。
题目
有16瓶水,其中只有一瓶水有毒,小白鼠喝一滴之后一小时会死,请问最少用多少只小白鼠,在1小时的时间一定可以找出有毒的水?
答案与题解
答案是至少需要4只小鼠,怎么理解呢?我们可以用二进制去推理一下:
假设有4只小鼠,分别是甲乙丙丁,使用二进制来表示小鼠喝药的顺序,1代表喝药,0代表不喝药
甲: 1111 1111 0000 0000
乙: 1111 0000 1111 0000
丙: 1100 1100 1100 1100
丁: 1010 1010 1010 1010
那么我们就可以这样去判断:
- 甲乙丙丁都死了,说明第一瓶有毒
- 甲乙丙死了,说明第二瓶有毒
- 甲乙丁死了,说明第三瓶有毒
- 甲乙死了,说明第四瓶有毒
- 甲丙丁死了,说明第五瓶有毒
- 。。。 依次类推
其实对于这道题,可以使用2的n次方来判断,比如有32瓶水,那么就是2的5次方,所以就需要5只小鼠。
arguments 面试题
在ES6中,我们如果一个函数参数个数不确定,我们一般会使用扩展运算符即function(...rest){},得到一个参数数组rest,但是在ES6之前,我们是不能使用扩展运算符的,这时候就需要考虑使用arguments
题目
请说出以下程序输出的内容(chrome输出内容)
1let obj = { 2 age: 18, 3 foo: function(func) { 4 func() 5 arguments[0]() 6 } 7} 8 9var age = 10 10function fn() { 11 console.log(this.age) 12} 13 14obj.foo(fn)
答案
本题的答案是:
1// 第一个输出 10 2func() 3// 第一个输出 undefined 4arguments[0]()
有点出乎意料了吗?
先来解释一下第一个,为什么不是输出18呢,虽然func()是在foo函数里面调用的,但是并没有显式指明作用域,这时候会使用默认作用域window,而对于浏览器来说,在全局通过var声明的变量会自动挂载到window上面,所以var age = 10相当于window.age = 10, 而第一个func()里面的this.age相当于window.age
第二个可能许多人有点蒙,为啥是undefined,先看一下下面的代码
1const arr = [function() {console.log(this[1])}, '我是子君'] 2// 输出 我是子君 3console.log(arr[0]())
我们通过arr[0]获取到函数,这时候函数的作用域就是这个数组,所以再调用的时候,this就是arr, 所以this[1]就是数组第二项。
这时候回过头来看arguments,这个其实是一个类数组,里面存的是函数传入的参数,第一项就是传入的函数,和上面例子一样,arguments[0]的作用域就是arguments,而arguments上面并没有age属性,所以是undefined
this指向问题
this指向问题一直是比较混乱的,在箭头函数出现之前,this的指向与代码在哪里定义并没有关系,而是取决于是被谁执行的,正因为此,所以许多开发人员经常会搞不清楚this到底是谁。下面的两道题都是和this指向相关的问题。
题目一(青铜)
请说出以下代码输出的内容
1var num = 1; 2let obj = { 3 num: 2, 4 add: function() { 5 this.num = 3; 6 (function() { 7 console.log(this.num); 8 this.num = 4; 9 })(); 10 console.log(this.num); 11 }, 12 sub: function() { 13 console.log(this.num) 14 } 15} 16obj.add(); 17console.log(obj.num); 18console.log(num); 19const sub = obj.sub; 20sub();
题目二(黄金)
请说出以下代码输出的内容
1var num = 10 2const obj = {num: 20} 3obj.fn = (function (num) { 4 this.num = num * 3 5 num++ 6 return function (n) { 7 this.num += n 8 num++ 9 console.log(num) 10 } 11})(obj.num) 12var fn = obj.fn 13fn(5) 14obj.fn(10) 15console.log(num, obj.num)
答案
题目一
输出结果: 1,3,3,4,4, 你答对了吗?下面我们来看看代码解析
1var num = 1; 2let obj = { 3 num: 2, 4 add: function() { 5 this.num = 3; 6 // 这里的立即指向函数,因为我们没有手动去指定它的this指向,所以都会指向window 7 (function() { 8 // 所有这个 this.num 就等于 window.num 9 console.log(this.num); 10 this.num = 4; 11 })(); 12 console.log(this.num); 13 }, 14 sub: function() { 15 console.log(this.num) 16 } 17} 18// 下面逐行说明打印的内容 19 20/** 21 * 在通过obj.add 调用add 函数时,函数的this指向的是obj,这时候第一个this.num=3 22 * 相当于 obj.num = 3 但是里面的立即指向函数this依然是window, 23 * 所以 立即执行函数里面console.log(this.num)输出1,同时 window.num = 4 24 *立即执行函数之后,再输出`this.num`,这时候`this`是`obj`,所以输出3 25 */ 26obj.add() // 输出 1 3 27 28// 通过上面`obj.add`的执行,obj.name 已经变成了3 29console.log(obj.num) // 输出3 30// 这个num是 window.num 31console.log(num) // 输出4 32// 如果将obj.sub 赋值给一个新的变量,那么这个函数的作用域将会变成新变量的作用域 33const sub = obj.sub 34// 作用域变成了window window.num 是 4 35sub() // 输出4
题目二
输出结果为: 22 23 65 30, 你答对了吗? 下面我们解析一下
1var num = 10 2const obj = {num: 20} 3obj.fn = (function (num) { 4 this.num = num * 3 5 num++ 6 return function (n) { 7 this.num += n 8 num++ 9 console.log(num) 10 } 11})(obj.num) 12var fn = obj.fn 13fn(5) 14obj.fn(10) 15console.log(num, obj.num)
我们把上面的代码分为以下几步进行分析
-
先看第三行代码,是一个赋值操作,我们知道赋值操作是从右向左的,而
=号右边是一个立即执行函数,所以会优先执行立即执行函数,立即执行函数没有手动指定this,这时候this = window,而立即函数的参数num是传进来的obj.num,所以num参数默认值是20 -
第四行相当于
window.num = 20 * 3 -
第五行为传入的参数加一,所以
num = 20 + 1 -
第六行
return了一个函数,而这个函数就是obj.fn的值, 但是因为return的函数引用了立即执行函数里面的num,所以形成了闭包。这时候1obj.fn = function(n) { 2 this.num += n 3 // 这个num是立即执行函数里面的num 4 num++ 5 console.log(num) 6} -
var fn = obj.fn, 将obj.fn赋值给新的变量,而这个变量的作用域是window -
在调用
fn(5)的时候, 在第二步,window.num的值已经变成了60, 然后因为这时候fn的this是window,this.num += n相当于window.num += n, 即window.num = 65 -
num++, 因为闭包的原因,第三步num是21,所以这一步num变成了22, 同时输出22 -
然后
obj.fn(10),这时候fn的this为obj,obj.num默认值是20,this.num += n相当于obj.num += 10 -
和第七步一样,
num + 1输出23 -
console.log(num, obj.num)相当于console.log(window.num, obj.num),从上面几步可知,window.num = 65,obj.num = 30。
扩展题
如果将上面两道题的 var改成 let, 又会输出什么结果呢?
数据类型转换问题
虽然在日常开发中,我们隐氏类型转换用的比较少(不一定),但是这个还是面试常问问题,掌握还是要掌握的,一起来看看这道题目吧.
题目(王炸/青铜,我也不知道)
请说出以下代码输出的内容
1console.log([] + []) 2console.log({} + []) 3console.log([] == ![]) 4console.log(true + false)
答案
一起来看看答案吧
- 第一行代码
1// 输出 "" 空字符串 2console.log([] + [])
这行代码输出的是空字符串"", 包装类型在运算的时候,会先调用valueOf方法,如果valueOf返回的还是包装类型,那么再调用toString方法
1// 还是 数组 2const val = [].valueOf() 3// 数组 toString 默认会将数组各项使用逗号 "," 隔开, 比如 [1,2,3].toSting 变成了"1,2,3",空数组 toString 就是空字符串 4const val1 = val.toString() // val1 是空字符串
所以上面的代码相当于
console.log("" + "")
-
第二行代码
1// 输出 "[object Object]" 2console.log({} + [])和第一题道理一样,对象
{}隐氏转换成了[object Object],然后与""相加 -
第三行代码
1// 输出 true 2console.log([] == ![])对于
===, 会严格比较两者的值,但是对于==就不一样了- 比如
null == undefined - 如果非
number与number比较,会将其转换为number - 如果比较的双方中由一方是
boolean,那么会先将boolean转换为number
所以对于上面的代码,看下面一步一步分析
1// 这个输出 false 2console.log(![]) 3// 套用上面第三条 将 false 转换为 数值 4// 这个输出 0 5console.log(Number(false)) 6// 包装类型与 基本类型 == 先将包装类型通过 valueOf toString 转换为基本类型 7// 输出 "" 8console.log([].toString()) 9// 套用第2条, 将空字符串转换为数值、 10// 输出 0 11console.log(Number("")) 12// 所以 13console.log(0 == 0) - 比如
-
第四行代码
1// 输出 1 2console.log(true + false)两个基本类型相加,如果其中一方是字符,则将其他的转换为字符相加,否则将类型转换为
Number,然后相加,Number(true)是1,Number(false)是0, 所以结果是1
总结
面试造火箭,工作拧螺丝。虽然我只想拧螺丝,但是我却需要通过造火箭来找到拧螺丝的工作,每日一题,每天都有新的面试题目,欢迎关注公众号【前端有的玩】,拉你进入前端技术交流群,每日一题等着你来一起答题。
结语
不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
本文转自 https://juejin.cn/post/6866920515420815374,如有侵权,请联系删除。
