📢:关于金三银四,这里有一份有趣的面试题,不容错过!
前言:相信大家为了找工作到处都在刷面试题,背八股文。笔者这里搜集一些除八股文外的面试题。发现既有趣又能拓展知识面,所以记录下来分享给大家。
1.值和引用面试题
问题:
var foo = { bar: 1 }
var arr1 = [1, 2, foo]
var arr2 = arr1.slice(1)
arr2[0]++
arr2[1].bar++
foo.bar++
arr1[2].bar++
console.log(arr1[1] === arr2[0])
console.log(arr1[2] === arr2[1])
console.log(foo.bar)
回答:
如果对堆和栈比较了解的人这道题就很简单。这道题主要考察就是值引用问题。
- 创建对象我们得分两步看,第一步创建一个新对象,在堆上给它标记一个地址A,对象里边有一个属性
bar
值为1,第二步就是把这个地址赋值给变量foo
。
变量 | 地址 |
---|---|
foo | A:{bar:1} |
2.跟第一条语句类似,也是分两步,第一步,无论是中括号还是大括号其实都是创建一个对象。在堆上给它标记一个地址B,而在这个数组第三项是变量foo
,实际就是它的地址。第二步把地址B赋值给arr1
变量 | 地址 |
---|---|
foo | A:{bar:1} |
arr1 | B:[1,2,A] |
3.第三条语句:arr1
地址是B,是一个数组,slice(1)
为截取原数组的后两项也就是2和A,给这个新数组标记一个地址C,然后赋值给arr2
变量 | 地址 |
---|---|
foo | A:{bar:1} |
arr1 | B:[1,2,A] |
arr2 | C:[2,A] |
到这里其实答案很明朗了,具体演算过程我就不详细说明了,最后经过代码运行,最后它们的值为:
变量 | 地址 |
---|---|
foo | A:{bar:4} |
arr1 | B:[1,2,A] |
arr2 | C:[3,A] |
2.将class转换为function
问题:
//将下面的代码转换为普通构造函数的写法
class Example{
constructor(name) {
this.name=name
}
func() {
console.log(this.name)
}
}
回答:
//1.严格模式
'use strict'
function Example(name) {
//2.验证this的指向,是否通过new来调用
if (!this instanceof Example) {
throw new TypeError('需要通过new来调用')
}
this.name=name
}
//3.方法成员不可枚举
Object.defineProperty(Example.prototype, "func", {
value: function () {
//4.不可通过new调用
console.log(this.name)
},
enumerable:false
})
let e = new Example()
new e.func()
'use strict'
在es6中使用类语法的话,是在严格模式下,转换为function的话需要设置为严格模式。- 虽然类本质上是一个函数,但是只能用
new
来调用,否则会报错,所以在function
里面需要验证this
的指向是否指向该实例,如果不是则抛出异常。 - 在类里边方法成员是在原型上,是不可枚举的。所以需要用到
Object.defineProperty()
来定义这个方法不可被in
操作符遍历出来。 - 类里面方法本身有一个特点。不可通过
new
来调用。但是在js里面任何函数都可以通过new
来调用。所以如果this的指向不是该对象的实例则抛出异常。

3.判断一个值是不是promiseLike
问题:
/*判断一个值是不是PromiseLike*/
function isPromiseLike(value){}
回答:
首先我们要知道什么是promiseLike
,其实很简单,就是长得像Promise
就行了。

由上图我们会发现ajax
方法跟promise
长得不像,但是它确能用await
关键字。这时候就得说到Promise A+ 规范
,我们还得知道是先有规范,再有 Promise
。 Promise A+规范 官方说明
官方文档说了很多,但是对于这道题我们只需要知道这两点就行
Promise
是一个对象或一个函数,那就是说不能为空then
必须是function。

function isPromiseLike(value) {
return value !== null && (typeof value === 'function' || typeof value === 'object') && (typeof value.then === 'function')
}
console.log(isPromiseLike(new Promise((resolve, reject) => { })))//true
console.log(isPromiseLike($.ajax()))//true(jquery的ajax方法)
console.log(isPromiseLike(() => { }))//false
console.log(isPromiseLike(2))//false
4.判断传入的函数是否标记了async
问题:
/**
* 判断传入的函数是否标记了async
*/
function isAsyncFunction(func) { }
isAsyncFunction(() => { })//预期:false
isAsyncFunction(async () => { })//预期:true
回答:
这里说一个常见的错误回答:直接调用func。这是错误,如果是被async
标记,那么的确会返回promise
,但是如果这个函数发送了一个请求或者有改变全局的操作。其次你调用这个函数可能需要参数,你又不知道参数是多少个,可能会报错;所以直接调用是错误的。
正确做法:我们先观察普通函数和async函数有什么区别。


我们可以在async函数的原型上找到一个知名标识符Symbol(Symbol.toStringTag)
,它的值为AsyncFunction
。而在普通函数这里没有这个知名标识符。这里不说这个知名标识符原理和作用具体是什么。在后面再说明,我们先写代码。
function isAsyncFunction(func) {
return func[Symbol.toStringTag]==='AsyncFunction'
}
console.log(isAsyncFunction(() => { }))//false
console.log(isAsyncFunction(async () => { }))//true
console.log(isAsyncFunction(()=>Promise.resolve()))//false
代码很简单,那么这样回答面试官肯定会问你Symbol.toStringTag
是啥。
这里我们就得说到常用的判断类型方法之一:Object.prototype.toString.call()
。
从上图我们发现这个判断方法不能把类型A给表达出来,过去呢这种表达方式无法更改,但是ES6开始提供一种方式,可以更改这个行为。那就是前面说到的Symbol(Symbol.toStringTag)
这个知名符号。
由上图可知,我们更改了类型。虽然Object.prototype.toString.call()
也可以用来判断async函数,但是当你使用知名标识符来讲解岂不是高级多了。
5.++[[]][+[]] + [+[]] 输出什么
console(++[[]][+[]] + [+[]])
这种题我相信很多人第一眼是懵的。你说业务中会不会有这种代码,如果有人说有,笔者直接五体投地。但是不可否认这道题很考验对于运算符的理解。吐槽的话就到了,下面直接来讲解下这道题:
- 首先我们把它拆分为两部分来运算,从中间的
+
来拆分为++[[]][+[]]
和[+[]]
。 - 我们先解决左边的
++[[]][+[]]
,++
我们先不管,后面的这么多中括号可能很多人直接就懵了。但是我们换一种说法把它拆分你就会豁然开朗。左边我们可以类似为++[0][0]
。就是说有一个数组为[]
,里面第一项的值为[]
(即[[]]
),然后我们取这个数组([[]]
)的+[]
项。
3.接下来就是换算:+[]
,数组是一个对象,使用+号时,优先用值进行运算,也就是先得到obj.value,如果没有obj.value的话,则使用toString()方法将对象转换为字符串进行相加。
这里就可以知道[]
转换为''
空字符串经过+
运算,+''
变为0,然后就是++[[]][0]
,取数组第一项值为[]
,然后就是++[]
,最后运算为1
。那么左边++[[]][+[]]
就是为数字1
4.[+[]]
就容易多了。首先里面的+[]
转换为0
,最后就是[0]
5.经过前面的换算,最后得到的是1+[0]
,这里注意[0]
经过加号运算符最后得到的是字符串0
,那么就是1+'0'='10'
。所以最后答案是字符串10
6.让 [a, b] = { a: 1, b: 2 }成立
问题:
//让下面代码成立
var [a, b] = { a: 1, b: 2 };
回答: 首先这个语句是报错的,要让它成立就是不报错,变为可运行。所以要解决报错的原因。
错误信息说明后面这个对象它不是可迭代的。那么首先要明白迭代是啥。
简单来说就是在for..of
结构中可以被遍历到。对象(或者它原型链上的某个对象)必须有一个键为 @@iterator
的属性,可通过常量 Symbol.iterator
访问该属性。

由图可知,我们发现数组的原型上有知名标识符Symbol.iterator
,我们将这个方法提取出来,这就是一个迭代器,可以使用next
方法来依次获取值。因为不能直接更改右边对象,我们只能在对象原型上进行操作。我们需要让这个对象变成一个可迭代对象。
Object.prototype[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator]()
}
7.高频面试题-并发请求
整体思路如下:
1.首先这是异步,因为请求本身就是异步操作。所以函数无论如何都会返回一个Promise,而且这个Promise按照题的要求一定是完成的,不能是拒绝状态。 就是说无论请求里面有几个失败或成功,对于整个函数而言都是成功,一定要返回结果数组。
2.准备好一个results数组存储结果,然后判断特殊情况(数组长度为0),直接resolve,然后退出函数。
3.定义一个变量index下标,取出index下标指向的url,取出来发送请求,发送之后下标加一指向下一项。
4.利用async/await来等待请求结果,然后不管成功还是错误的结果都加入到results数组。
(注意:这里不能用push
来加入数组,因为push
会改变results的顺序。假如最开始是A,B,C三个请求,因为是几乎同时发送三个请求,可能A没等待完成,B先完成,就会造成results数组第一项为B)。
所以要用一个新变量保存最开始这个请求的下标,然后根据这个下标来存储结果。
5.无论请求是否成功都进行下一个请求(注意:当所有请求发送完成之后会报错,因为是数组下标超过了数组的范围,所以需要判断有没有超过这数组的最大下标)
6.在每次请求完成之后判断所有的请求是否完成,让整个函数的Promise状态resolve
7.最后就是怎么调用,但是有个小问题,当你最大并发数为5,url数组长度只有3,就会有问题。所以要取最小值来循环
/**
* 实现一个并发请求
* 请求地址为一个数组,有一个最大并发数,每次发送请求数量不超过最大并发数,
* 有一个结果数组,无论成功还是失败都要返回结果,最后每一个请求所产生的结果归到一个数组里边去最终数组顺序要跟url数组顺序一样
* @param {string[]} urls 待请求的url数组
* @param {number} maxNum 最大并发数
*/
function concurRequest(urls, maxNum) {
//1
return new Promise(resolve => {
//2
if (urls.length === 0) {
resolve([])
return
}
//3
let index = 0 //下一个请求的下标
//6
let count = 0 //当前请求完成的数量
const results = []
let i = index
//发送请求
async function request() {
if (index === urls.length) {
//如果index等于数组长度说明没有请求可发
return
}
const url = urls[index]
index++
try {
const resp = await fetch(url)
//resp加入到results
results[i] =resp
} catch (error) {
//err加入到results
results[i]=error
} finally {
//判断是否所有的请求都已完成
count++
if (count === urls.length) {
console.log('over')
resolve(results)
}
//5
request()
}
}
const times = Math.min(maxNum, usrls.length)
for (let i = 0; i < times; i++){
request()
}
})
}
8.重试N次请求
问题:
/**
* 发出请求,返回Promise
* @param {string} url 请求地址
* @param {number} maxCount 最大重试次数
*/
function request(url, maxCount = 5) {
}
回答:
首先大概讲解下思路,就是类似request(url,5)
调用,当5次都成功,我们可以通过then
来拿到返回结果,如果是5都失败了,就认为不可能成功,就用catch
来获取错误。
request(url, 5).then(res => { console.log(res) }).catch(err => { console.log(err)})
接下来要明白什么时候拒绝整个函数,是失败了就拒绝吗,不是,是重试次数不够才拒绝的。也就是说maxCount<0
的时候重试机会没了,就该返回reject()
否则就重新调用,然后maxCount-1
。
最终代码:
function request(url, maxCount = 5) {
return fetch(url).catch(err=>maxCount<=0?Promise.reject(err):request(url,maxCount-1))
}
request(url, 5).then(res => { console.log(res) }).catch(err => { console.log(err)})
9.call和apply
问题:
//下面代码输出什么
console.log.call.call.call.call.call.aply((a)=>a,[1,2]);
回答:
在回答上面的前,像这么一个表达式a.b.c.d.e
最终返回的前面的a
还是后面的e
呢。这样是不一定返回后边的e
。
但是当变为a.b.c.d.e()
这样的时候,其实是相当于这个表达式的函数调用,最终返回的是最后一个函数e,它的调用结果。所以题目最终返回的是apply
的调用结果。
这里就要说到apply
是怎样执行的,最终执行这个函数函数.apply(参数1,[1,2])
,然后把这个函数的this指向参数1,将数组入参变为一般入参。apply作用 转换过来就是把参数1弄成一个对象,然后把参数1和2传进去(参数1.函数(1,2),函数里边的this就是正好指向前面的那个对象
)。
接下来看回原代码,apply
后面的参数1为(a)=>a
,参数1是一个函数,函数肯定是一个对象。再看下apply
前面的是call
,那最后其实就是执行((a)=>a).call(1,2)
这一条语句。
后面就清晰 ((a)=>a).call(1,2)
前面(a)=>a
是一个函数,函数调用call
,绑定this
为1,传入参数为2,而函数里面又没有用this
,所以说整个表达式相当于((a)=>a)(2)
。这就是一个函数传入2,返回一个2。
注意:这不是最终答案,因为最后执行的语句相当于((a)=>a)(2)
,这是没有输出语句的,所以要看到实际的值还得自己加上打印语句。

10.无限递归一定会溢栈吗
问题:
//在浏览器控制台运行一下代码,是否会堆栈溢出?
function foo(){
setTimeout(foo,0)//是否存在堆栈溢出错误?
}
回答:
首先要知道栈溢出是什么造成的。在函数调用的时候会在栈里边加上一个执行上下文,其实就是会占用栈的一部分内存空间,而这个上下文是要到函数结束过后才会被弹出去。如果说你在这个函数里边调用自身的话,就会导致这个函数还没有运行结束的时候又在调用自己,这样又会在栈里边加上一个执行上下文,一直重复,而栈的总大小是固定的,就会把栈撑爆。这就是栈溢出。
回到最开始问题,这个函数执行是不会栈溢出,会无限执行。这个也是递归(自己调用自己),为啥不会?注意上面一句话:这个函数还没有运行结束的时候又在调用自己。我们发现,foo
在函数内部调用自己是异步操作(setTimeout)。
为什么异步操作就不会导致栈溢出呢?首先要知道执行foo
期间 有没有他有没有再次调用foo
。答案是没有的,因为它只是在执行期间做了一个计时器,告计时器到达之后再调用这个foo
。这就是涉及到事件循环。简单来说,setTimeout
是宏任务,它会被放到下一次循环执行,而事件循环的每一轮都以调用栈清空为标志,每次清空完调用栈,就代表着一轮事件循环的结束。 所以是每次foo
函数执行完才继续调用自身。
问题:
//如果修改成这样的呢?
function foo(){
setTimeout(foo(),0)
}
回答:
我们注意到函数里面变为了foo()
,这就是函数里面的知识,这等价为一个立即执行函数。这里就会变为先执行foo函数
,把这个foo
的返回结果作为计时器到达之后要执行的东西。就是这里就会变成foo
计算过程又会调用foo
。就会变成溢栈。
问题:
//如果修改成这样的呢?
function foo(){
Promise.resolve().then(foo)
}
回答: 如果你是node环境运行js代码,你可能发现不了什么,因为它不会报错,但是如果你在页面打开,你会发现页面会卡死,一直在转圈。那问题出在哪呢?
首先我们要知道Promise.resolve().then()
这个代码执行效果是什么?resolve
执行的订阅的回调也是通过微任务来调度的,会走事件循环机制,事件循环这一次循环结束标志就是清空当前栈,而微任务是在宏任务执行完后,再加入到栈里面执行上下文。而出现卡死的现象,就是微任务队列一直有任务进来,导致无法清空,主线程不会被让出。
这次分享的面试题就结束。笔者也因此从中学到很多,也希望这篇文章能够帮到你们,巩固知识,拓宽知识面。如果觉得不错,能否点个赞(* ̄︶ ̄)
转载自:https://juejin.cn/post/7202793493827059769