2023年注定是灰暗的一年,以为度过了2022就会迎来曙光,然而有些事情就是这样不如意,不过我相信情况会越来越好,疫情后的5年,我们拭目以待。年前和年后经历了一些面试,整理了些面试题,可以让自己经常回头温习下。如果哪里有问题,希望你不吝赐教,感谢。如果对你有帮助,希望点赞收藏不迷路。
1.简单说下WebSocket
为什么会创造出websocket ?
没有websocket之前,客户端和服务端通信,只能从客户端向服务器发起请求拿到数据。如果数据的实效性比较高,那么通常的做法是客户端开启轮巡,每隔一段时间就发送一次请求,这样浪费性能和带宽。websocket的出现就是解决这个问题,websocket的连接客户端和服务端都可以主动发送数据。
Websocket的特点 ?
- 建立在TCP协议之上,服务端的实现比较容易
- 与http协议有着良好的兼容性,默认端口也是80和443;并且握手阶段也是http协议,因此握手时不容易被屏蔽,能通过各种Http代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议表示符是ws,如果加密则是wss
- 只有一次握手
Websocket请求头
- Get ws://localhost:3000/xxx Http 1.1
- Host: localhost
- Connection: Upgrade
- Upgrade: Websocket
- Origin: http://localhost:3000
- Sec-WebSocket-Key: xxx
- Sec-WebSocket-Version:xxx
Websocket 响应头
- Http1.1 101 Switching Protocals //代表切换协议成功
- Upgrade: WebSocket
- Connection: Upgrade
- Sec-WebSocket-Accept: xxx
Http2.0 可以服务端推送,那Http2.0和WebSocket有什么区别呢?
主要区别在于是否主动推送,比如A与B聊天,A将信息发送到WebSocket服务器,服务器接到信息后主动推送到B。而Http2.0的服务端推送,一般是指比如请求服务器的index.html资源,这时服务器会主动将与index.html有关的js、css、图片等资源推送给客户端,防止多次请求。并切Http2.0必须是客户端先发请求,然后我才知道要推送什么。所以就IM来说,Http2.0服务端推送和WebSocket还是有区别的。
2.介绍下Set数据结构
特点:
- Set集合通过构造函数创建
- 任何具有iterable接口的对象都可以作为Set构造函数的参数
- 存入的值不能重复,存取有序
- 内部判断元素是否相等算法类似===,区别是set内的算法认为两个NaN相等,而===认为不等
常用方法:
- add(value) 添加元素,返回Set结构本身
- delete(value) 删除元素,返回boolean类型结果,表示是否删除成功
- has(value) 返回boolean类型结果,表示set是否包含此元素
- clear() 清除所有成员,没有返回值
- keys( ) / values( ) 分别返回key和value的遍历器 如:SetIterator{2,4,5,8}, keys和* values是一样的
- entries( ) 返回键值对的遍历器,如 SetIterator {2 => 2, 5 => 5, 6 => 6, 8 => 8}
- forEach( (value,key,set)=>{ } ) 遍历集合
WeakSet
weakset与set类似,存储的值也都不能重复,但是它与set有两点区别:
- weakset只能存储引用类型的对象,不能存储基本类型,否则会报错。
- weakset中的对象都是弱引用的,即垃圾回收机制不考虑weakset对该对象的引用,也就是说,如果其他对象不再引用该对象,那么垃圾回收机制就会回收该对象占用的内存,不考虑该对象是否还存在于weakset中。
- 由于垃圾回收机制运行会导致weakset内部成员个数变动,所以ES6规定weakset不可以遍历。
3.介绍下Map数据结构
Map 特点:
* 通过构造函数 new Map( )创建,任何类似这种形式的数据都可以作为参数[['name', '张三'] , [ 'title', 'author' ] ]
* map键值重复,value值会覆盖;判断键的相等性类似===,只不过map中认为两个NaN相等
方法:
size( )
set( key, value ) 任何值都可以作为key值
get( key )
has( key )
delete( key )
clear( )
keys( )
values( )
entries( )
forEach( )
WeakMap :
- weakmap只接受对象作为键值,null不可以作为键值
- weakmap键名所指向的对象不计入垃圾回收机制
为什么需要WeakMap这样的数据结构呢?
const e1 = document.getElementById("e1");
const e2 = document.getElementById("e2");
const arr = [
[e1, '元素e1'],
[e2, '元素e2']
];
//不需要arr后必须手动删除引用
arr[0] = null;
arr[1] = null;
如上结构中,我们在arr数组中保存了两个Dom元素,这就形成了arr对e1和e2的引用;一旦不在需要arr我们必须手动删除内部的引用,否则就会造成内存泄漏。
为了解决这种情况,WeakMap应运而生,保存在其键内的对象,不会被强引用;如果没有其他对象对其引用,那么键对象随时可能会被垃圾回收器回收。
注意:WeakMap的weak是指对键对象的弱引用,而不是值;值是强引用的。
4. Object.is、==、=== 区别
- SameValue标准,接口实现是Object.is( ),满足以下条件,则判断为相等:
- 都是undefined
- 都是null
- 都是true或false
- 都是相同长度、相同字符、按相同顺序排列的字符串
- 都是相同对象,指同一个对象的引用
- 都是数字,都是+0,都是-0,都是NaN,都是同一个值非零且不是NaN
- SameValueZero标准,和SameValue的区别在于+0和-0是相等的,Array.prototype.includes( )就是用这种比较
- === 数组indexOf就是使用这种比较,和Object.is基本相似,但有2点区别:
- 一是 === 中 +0 和 -0 相等,而Object.is中不相等
- 二是 === 中NaN不相等,而Object.is中是相等的
- == 会尝试进行类型转换,== 中null只和自身和undefined是互等的。
1 == '1' //true
false == 0 //true
false == "" //true
5.CSS加载会阻塞Dom的解析和渲染吗?
Css加载会阻塞Dom解析吗?
正常情况下不会阻塞Dom的解析,Css加载解析和Dom的加载解析是并行关系,所以不会阻塞;但是Css会阻塞Js执行,所以如果Js后面有Dom标签,会阻塞其后面的Dom的渲染的;
Css加载会阻塞Dom渲染吗
会阻塞Dom的渲染,因为Dom Tree + CSSOM ==> Render Tree , 也就是说Render Tree依赖Dom和Css,所以必须Css加载完才会渲染。但这也不是绝对的,因为现代浏览器为了减少白屏时间让用户更快的看到页面,会有边解析边渲染的机制,也就是说解析和渲染是并行的,不是串行的;比如在body中某个标签用link引入外部样式,link前的dom是不会受影响的,只有link后面的dom会被阻塞。
Css加载会阻塞Js执行吗?
会的
网络相关
1. 强缓存和协商缓存
强缓存
缓存规则:
* 浏览器缓存中不存在缓存结果和标识,缓存失效,直接向服务器发起请求
* 浏览器缓存中存在缓存,缓存已经失效,走协商缓存
* 浏览器缓存中存在缓存,并且未失效,直接返回结果
缓存标识:Expires (http1.0) 和 Cache-Control (http 1.1) , Cache-Control优先级高
Cache-Control的取值:
* public 所有内容都将被缓存(客户端、代理服务器、CDN等)
* private 只有客户端缓存,默认值
* no-cache 客户端缓存内容,但是是否使用缓存内容,需要协商缓存确认
* no-store 所有环节都不缓存,即不使用强制缓存,也不使用协商缓存
* max-age: xxx 缓存将在多久后失效
协商缓存
缓存规则:
* 协商缓存生效,返回304,服务器告知浏览器资源未更新,浏览器再去浏览器缓存中取内容
* 协商缓存失效,返回200和结果
缓存标识:Last-Modified / If-Modified-Since 和 Etag / If-None-Match;Etag的组合的优先级更高;
协商缓存中为啥Etag的优先级比Last-Modified高,因为Last-Modified存在极特殊情况,而Etag都不存在这些情况。
* 比如对文件修改ABA的问题,从A状态修改到B再修改到A,这种情况从时间来看文件肯定是更新了,但是内容并没有变化。
* Last-Modified的时间是秒级别的,如果服务端返回的同时,在一秒内对文件做了多次更新,那么这个时间和上次的时间一样,会被误认为没有更新。
* 有些服务器不能精准获取文件的更新时间
2.HTTP 1.0 1.1 2.0区别
Http1.0
- 无状态:服务器不跟踪不记录请求过的状态
- 无连接:浏览器每次请求都需要建立TCP连接
Http1.1
- 支持长连接 请求头中加入Connection: keep-alive ,如果不想长连接:Connection: false
- 支持请求管道化
Http2.0
- Http2.0 采用二进制格式数据帧传输数据,而非Http1.x的文本传输,更加高效
- 多路复用,多路复用代替了http1.x中序列和阻塞机制,所有相同域名的请求都通过同一个TCP连接并发完成,针对请求并发的情况,客户端可以通过帧的标识知道属于哪个请求,通过这个技术可以解决旧版本中队头阻塞的问题,极大的提高了传输性能。
- 服务端推送,比如请求一个html页面,服务端可以主动推送这个页面相关的js和css和图片资源,减少客户端的请求次数
- 头部压缩,2.0对header进行压缩,并且通讯双方各cache一份header fields表,避免重复header传输。
3. Http报文及常用状态码
/**
* ++++++++++++++++++++
* + 报文首部 + ----> 请求报文首部包含: 请求行、请求首部字段、其他
* ++++++++++++++++++++ 响应报文首部包含: 状态行、响应首部字段、其他
* + 空行 +
* ++++++++++++++++++++
* + 报文主体 +
* ++++++++++++++++++++
*
* 请求行包括:请求方法, 请求路径path, HTTP版本 如: GET / HTTP/1.1
* 请求首部字段:
* Host: xxx
* User-Agent: xxx
* Accept: xxx
* Accept-Language: xxx
* Connection: xxx
* If-Modified-Since: xxx
* If-None-Match: xxx
* Cache-Control: max-age=0
*
* 状态行包括: HTTP版本和状态码 如: HTTP/1.1 304 Not Modified
* 响应首部字段:
* Date: xxx
* Server: Apache
* Connection: close
* Etag: xxx
*/
/**
* Http相应状态码
*
* 2xx
* 200: 服务器正常处理了客户端发来的请求
* 204: No Content, 服务器成功处理了客户端请求, 但没有可返回的实体。
* 206: Partial Content, 表示客户端发起的是范围请求。相应报文中包含由Content-Range指定范围的实体内容。
*
* 3xx
* 301: 永久性重定向, 资源路径已修改
* 302: 临时重定向
* 303: 和302一样,只不过强制要求用get请求
* 304: 客户端附带条件请求,但因请求未满足条件,直接返回304表示服务器资源未更新,不返回任何响应的主体部分。
* 307: 和302一样,规定是Post不要改成Get但是实际使用时大家不遵守。而307就遵照浏览器标准,不会从Post变Get
*
* 4xx
* 400: 请求报文中存在语法错误,当错误发生时需要修改请求的内容重新发送请求
* 401: 未认证
* 403: 无权访问
* 404: 服务器未找到请求的资源
*
* 5xx
* 500: 服务器内部错误
* 503: 服务器超负载或者正在进行停机维护,现在无法处理请求。
*
*/
问打印结果
1. 说出下面代码的输出结果
var name = "222"
var a={
name:"111",
say:function(){
console.info(this.name);
}
}
var fun = a.say;
fun(); //222
a.say(); // 111
var b = {
name:"333",
say:function(fun){
fun();
}
}
b.say(a.say); //111
b.say = a.say;
b.say(); //333
2. a.x 和 b.x分别分别等于多少?
function A(x) {
this.x = x;
}
A.prototype.x = 1;
function B(x) {
this.x = x;
}
//注意这里将B的原型指向A的实例,但是此实例没有传参数,所以实例的x属性为undefined
B.prototype = new A();
var a = new A(2);
var b = new B(3);
delete b.x;
//a.x = 2 b.x=undefined
3. 说出下面代码的输出结果
new Promise(resolve => {
console.log(3)
resolve(2)
resolve(1)
console.log(4)
}).then(console.log)
//3 4 2
new Promise(resolve => {
console.log(3)
resolve(2)
resolve(1)
console.log(4)
}).then(console.log)
console.log(5)
// 3 4 5 2
console.log(5)
try {
new Promise((_, reject) => {
console.log(3)
reject('err msg')
}).catch(err => {
console.log(2)
throw err
console.log(4)
})
} catch (err) {
console.log(8)
console.log(err)
} finally {
console.log(9)
}
console.log(10)
// 5 3 9 10 2 Uncaught error
// 解析:try catch 只能捕获同步代码块的异常,所以try中的异步代码抛出的异常都不能捕获,比如setTimeout和Promise内部抛出的
// 先执行同步代码,输出 5 3 9 10 , 然后执行异步代码 2 ,在Promise catch中执行的代码属于异步,所以抛出的异常是不能被捕获的
// 所以会有一个Uncaught error
4. 说出下面代码的输出结果
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
//输出1
//then里面的两个参数必须函数,第二个可以不传,如果不是函数,会给默认的函数值;
//所以第一个和第二个then里面都不是函数,会给默认值value=>value;
//这样1就被传下来了,所以最后打印1
5.事件循坏输出问题
async function async1 () {
console.log('1')
await async2()
console.log('AAA')
}
async function async2 () {
console.log('3')
return new Promise((resolve, reject) => {
resolve()
console.log('4')
})
}
console.log('5')
setTimeout(() => { console.log('6') }, 0);
async1()
new Promise((resolve) => {
console.log('7')
resolve() }
).then(() => {
console.log('8')
}).then(() => {
console.log('9')
}).then(() => {
console.log('10')
})
console.log('11')
// 最终结果: 5 1 3 4 7 11 8 9 AAA 10 6
// 主要是AAA的位置
算法题
1. 找到数组中每个元素后面大于自身的元素
/*
* 给定一个数字数组,找到数组每个值后面第一个大于的它自身的元素,如果没找到,设为-1。
* 最后返回一个包含所有找到的值的新数组
* 示例:
* 输入:[1, 5, 8, 7, 2, 9, 2]
* 输出:[5, 8, 9, 9, 9, -1, -1]
* 要求O(n)没有思路可以暴力解
*/
//暴力解法 O(n^2)
function func(nums) {
const res = [];
for(let i=0;i<nums.length;i++){
let find = false;
for (let j = i+1; j < nums.length; j++) {
if(nums[j]>nums[i]){
find = true;
res.push(nums[j]);
break;
}
}
if(!find) res.push(-1);
}
return res;
}
//栈 O(n)
function func(nums) {
let index = 1;
const stack = [0];
const len = nums.length;
const res = new Array(len).fill(-1);
while (index<len) {
const preIndex = stack[stack.length-1];
if (stack.length!==0 && nums[index]>nums[preIndex]) {
res[stack.pop()] = nums[index];
}else{
stack.push(index);
index++;
}
}
return res;
}
2. 实现类的延迟方法
/*
* class Person{
* eat(){}
* sleep(){}
* }
* const p = new Person();
* p.eat("aa").sleep(1000).eat("bb")
* .eat("cc").sleep(2000).sleep(3000).eat("dd");
*
* 输出aa 等1s 输出bb cc 等5s 输出dd
*/
//解决方案一,Promise.then的链式调用
class Person{
promise = Promise.resolve();
eat(something){
this.promise = this.promise.then(()=>{
return new Promise(resolve=>{
console.log(something);
resolve();
});
});
return this;
}
sleep(time){
this.promise = this.promise.then(()=>{
return new Promise(resolve=>{
setTimeOut(()=>{
console.log('sleep-'+time);
resolve();
},time);
});
});
return this;
}
}
//解决方案二
class Person{
constructor(){
this.task = [];
setTimeout(()=>{
this.next();
});
}
eat(something){
this.task.push(()=>{
console.log(something);
this.next();
});
return this;
}
sleep(time){
this.task.push(()=>{
setTimeout(()=>{
console.log("sleep-"+time);
this.next();
},time);
});
return this;
}
next(){
const func = this.task.shift();
func && func();
}
}
3.求一个Dom节点的最大深度
function maxDepth(node) {
let depth = 0;
const children = node.children;
if(!children || children.length===0) return 1;
for (let i = 0; i < children.length; i++) {
const childMaxDepth = maxDepth(children[i]);
if(childMaxDepth>depth) depth = childMaxDepth;
}
return depth + 1;
}
4.求一个无序数组的第二大元素,用O(n)的时间复杂度
function findSecondBig(nums) {
let firstBig = nums[0],secondBig = nums[1];
if(firstBig<secondBig){
const temp = firstBig;
firstBig = secondBig;
secondBig = temp;
}
for (let i = 2; i < nums.length; i++) {
if(nums[i]>firstBig){
secondBig = firstBig;
firstBig = nums[i];
}else if(nums[i]<firstBig && nums[i]>secondBig){
secondBig = nums[i];
}
}
return secondBig;
}
5.实现一个函数,完成数组转换
/*
* const arr = [
* ['a', 'b', 'c'],
* ['a', 'd'],
* ['d', 'e'],
* ['f', 'g'],
* ['h', 'g'],
* ['i']
* ]
* 实现一个函数,输入arr后输出如下结果:
* [
* ['a', 'b', 'c', 'd', 'e'],
* ['f', 'g', 'h'],
* ['i']
* ]
*/
function transferArray(arr) {
const result = [];
let tempArr = arr[0];
for (let i = 0; i < arr.length-1; i++) {
if(arr[i].some(item=>arr[i+1].includes(item))){
tempArr.push(...arr[i+1]);
}else{
result.push([...new Set(tempArr)]);
tempArr = arr[i+1];
}
}
result.push([...new Set(tempArr)]);
return result;
}
6.闭包问题
/*
* 实现一个next()
* 例如: var next = setup([1, 2,3, 4,5,6, 7])
* 依次调用 next(),分别输出1,2,3,4,5,6, 7;
*/
const arr = [1,2,3,4,5,6,7];
function setup(arr){
let i = -1;
return function(){
++i;
return arr[i];
}
}
const next = setup(arr);
console.log(next());
7.数组转树
const arr = [{
id: 2,
name: '部门B',
parentId: 0
},
{
id: 3,
name: '部门C',
parentId: 1
},
{
id: 1,
name: '部门A',
parentId: 2
},
{
id: 4,
name: '部门D',
parentId: 1
},
{
id: 5,
name: '部门E',
parentId: 2
},
{
id: 6,
name: '部门F',
parentId: 3
},
{
id: 7,
name: '部门G',
parentId: 2
},
{
id: 8,
name: '部门H',
parentId: 4
}
]
//转换成如下格式
{
id:2,
name:"部门B",
parentId:0,
children:[
{
id:1,
name:"部门A",
parentId:2,
children:[
{
id:3,
name:"部门C",
parentId:1,
children:[
{
id:6,
name:"部门F",
parentId:3
}
]
},
{
id:4,
name:"部门D",
parentId:1,
children:[
{
id:8,
name:"部门H",
parentId:4
}
]
}
]
},
{
id:5,
name:"部门E",
parentId:2
},
{
id:7,
name:"部门G",
parentId:2
}
]
}
//代码实现
function trans(arr) {
let result;
const map = new Map();
arr.forEach(item => {
map[item.id] ={...item}; //新建对象防止破坏原有数组
});
for (const item of arr) {
const parent = map[item.parentId];
if(parent){
//用map[item.id]而不是item,防止破坏原有数组
(parent.children || (parent.children=[])).push(map[item.id]);
}else{
result = map[item.id]; //用map[item.id]而不是item,防止破坏原有数组
}
}
return result;
}
8.Promise并发控制
async function asyncPool(arr,limit,createPromiseFunc){
const allPromises = [];
const pendingPromises = [];
for(const item of arr){
const p = Promise.resolve(createPromiseFunc(item));
allPromises.push(p);
if(limit<arr.length){
const thenP = p.then(()=>pendingPromises.splice(pendingPromises.indexOf(thenp),1));
pendingPromises.push(thenP);
if(pendingPromises.length>=limit){
await Promise.race(pendingPromises);
}
}
}
return Promise.all(allPromises);
}
function createPromiseFunc(prop){
console.log("开始:"+prop);
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(prop);
console.log("结束:"+prop);
},1000+Math.random()*1000);
});
}
(async ()=>{
const res = await asyncPool([1,2,,3,4,5,6,7,8,9],2,createPromiseFunc);
console.log(res);
})();
9.去掉字符串中第index个斜杠
const str = "abc/defg/hi/jkl/mn/opqrst";
function removeSlashes(str,index){
if(str.indexOf("/")==-1 || index==0) return str;
const strArray = str.split("/");
if(index>strArray.length-1) return str;
const firstStr = strArray.slice(0,index).join("/");
const secondStr = strArray.slice(index).join("/");
return firstStr + secondStr;
}
console.log(removeSlashes(str,0)); //"abc/defg/hi/jkl/mn/opqrst"
console.log(removeSlashes(str,1)); //"abcdefg/hi/jkl/mn/opqrst"
10.数组去重
//方式一
function func(arr){
return [...new Set(arr)];
}
//方式二
function func(arr){
return arr.filter((item,index)=>{
return arr.indexOf(item)===index;
});
}
//方式三
function func(arr){
for(let i=0;i<arr.length;i++){
for(let j=i+1;j<arr.length;j++){
if(arr[j]===arr[i]){
arr.splice(j,1);
j--;
}
}
}
}
//方式四
function func(arr){
arr.sort();
const result = [arr[0]];
for(let i=1;i<arr.length;i++){
if(arr[i]!==arr[i-1]){
result.push(arr[i]);
}
}
return result;
}
手写题
1. 手写instanceof
function myInstanceOf(target, origin) {
if (typeof origin !== "function") {
throw new TypeError("origin must be function");
}
if (typeof target !== "object" || target == null) return false;
let proto = Object.getPrototypeOf(target);
while (proto) {
if (proto === origin.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
2. 手写深拷贝
function deepClone(obj,map=new WeakMap()){
if(typeof obj!=='object' || obj==null) return obj;
const value = map.get(obj);
if(value) return value;
let result = {};
if(obj instanceof Array) result = [];
map.set(obj,result);
for(const key in obj){
if(Object.hasOwnProperty.call(obj,key)){
result[key] = deepClone(obj[key],map);
}
}
return result;
}
3. 手写call方法
Function.prototype.myCall = function(obj){
if(typeof this !== 'function'){
thorw new TypeError('Caller must be function');
}
obj = obj || windown;
const propName = Symbol();
obj[propName] = this;
const args = [...arguments].slice(1);
const result = obj[propName](...args);
delete obj[propName];
return result;
}
//如果面试为了兼容性,不让你用扩展运算符...怎么来实现呢,下面就是答案
Function.prototype.myCall = function(obj){
if(typeof this !== 'function'){
throw new TypeError('caller must be function');
}
const args = [];
for(let i=1;i<arguments.length;i++){
args.push('arguments['+i+']');
}
//此时args类似这样args=['arguments[0]','arguments[1]']
obj = obj || windown;
const propName = Symbol();
obj[propName] = this;
const result = eval('obj[propName]('+args+')'); //这里会自动调用args.toString()方法
delete obj[propName];
return result;
}
练习题
1. 二分法查找,时间复杂度O(logn)
function binarySearch(arr, target) {
let start = 0,end = arr.length - 1;
while (start <= end) {
let mid = Math.floor((start + end) / 2);
if (arr[mid] > target) {
end = mid - 1;
} else if (arr[mid] < target) {
start = mid + 1;
} else {
return mid;
}
}
return -1;
}
2. 冒泡排序
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let isSwap = false;
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isSwap = true;
}
}
if (!isSwap) break;
}
}