初心:俺也是23届的,从7月初到现在一直在准备秋招,整理了几十万字的面经,常规的题目网上以及有很多了,所以就把我认为各位好兄弟们可能不太熟悉、没太了解的题目总结出来,希望可以帮到大家~
一、计算机网络
1.1、ssl、ssh区别
(大部分同学都分不清)
1、ssh是什么
我们在使用github的时候,如果想上传代码,除了账号密码登录之外,是不是还有个SSH
。我们一开始需要在自己的电脑上生成一对密钥对,然后把自己的公钥放在github上面,这样就能使用SSH
去登录github了,然后就可以使用SSH
去拉取或者推送代码了。
- SSH,也就是Security Shell,是一个shell窗口
- 相当于是一个隧道,数据通过的时候保护它不被泄露和篡改,为shell提供安全的传输和运用环境。
SSH
只是一种用于主机用户登录,安全共享数据的网络应用程序。使用SSH
后会在客户端和服务端建立一个安全通道,客户端就可以在这个通道中安全使用shell命令去操作服务端。
2、ssl是什么
2.1、http存在的问题
- 明文运输
- 报文完整性
- 验证双方身份
2.2、对称加密存在的问题
A通过密钥s加密,B收到信息之后通过密钥s进行解密
存在问题:A如何安全的把密钥s发送给b,而不会泄露给其他人呢? 所以有了非对称加密
2.3、非对称加密
- 由一对
公钥
和私钥
组成 公钥
和私钥
之间可以互相加解密- 用
公钥
加密的数据只能用对应的私钥
解开,反之亦然
我们把A当作客户端
,把B当作服务端
。A和B都生成一对密钥对
,他们把各自的公钥
都发布到网上,人人都能拿到公钥
。这样,A就拿到了B的公钥B
,B拿到了A的公钥A
。当A想要发信息给B时,A拿B的公钥B
加密信息,发送加密后的密文到B那里。因为拿公钥B
加密的信息只有私钥B
能解开,所以互联网链路上其他人拿到了这个信息也无法查看和更改。B拿到信息后再用私钥B
解密,就能保证通信的安全了。
而如果是B向A发信息,反过来也一样,用A的公钥A
加密信息,A收到信息后用私钥A
解密信息。
那么问题又来了,你怎么知道公钥B
就是B的呢?如果存在一个中间人两边冒充截取信息呢?
如上图所示,有个中间人M在客户端和服务端获取对方的公钥的时候,替换成了自己的公钥M
,这样就能两边获取信息再篡改了。因为客户端和服务端无法验证这个公钥是否是来自对方的,如果被替换了也没办法认证。所以,就引入了一个数字证书的机制。
2.4、数字证书
服务端不可能像
SSH
给每个客户端都亲自发放一个公钥吧。所以又回到了最初的起点:因为客户端和服务端无法验证这个公钥是否是来自对方的,如果被替换了也没办法认证。 所以有了数字证书
这个东西。
需要找一个第三方机构来做公证证明这个公钥确实是来自于服务端
服务端去证书授权中心申请证书,服务端会把自己的网站信息
以及服务端公钥
给到证书授权中心那边,而证书授权中心会根据这些生成一张由网站信息
、证书信息
、证书授权中心的数字签名
以及服务端公钥
等组成的证书。这里需要注意的是网站信息
、证书信息
、服务端公钥
等信息在证书中都是以明文形式保存的,那么问题就来了,既然是明文那在互联网传输中肯定是不安全的,那么怎么证明这些东西没有被篡改过呢?这时候数字签名
就派上用场了。
2.5、数字签名
数字签名
是通过一些特定的hash算法计算得到的一串值。在上面我们有网站信息
、证书信息
、服务端公钥
等明文信息,那我们就可以用特定的hash算法对这些信息进行计算得到一个hash值
,这个hash值
就是我们这个证书的指纹,它是唯一的标识。
当客户端获取到这张证书后,会用同样的hash算法对那些明文信息进行计算,这样就也能得到一个hash值
,两个一对比,如果是相同的就可以证明这张证书是安全的
问题:如果中间人截获了证明,通过修改明文的hash怎么办,所以要对数字签名hash进行加密
加密方式:通过CA机构的私钥进行加密,只要拥有了ca机构的公钥都可以解密,ca机构是预装在操作系统中的
就算正式被截胡了,修改了hash,那么中间人也得不到ca的私钥进行加密,所以没有ca机构的私钥无法进行加密,就可以保证数字签名是安全的
当客户端拿到了证书后
- 首先先验证这张证书是否来自于受信任的CA机构(是否受信任是事先放在计算机Cert Store里的,用户也可以手动添加),如果不是,就会抛出异常。
- 如果是受信任的CA机构,那么则用此CA机构的公钥来解密
证书授权中心的数字签名
,获得一个hash值和对应的hash算法。 - 客户端再使用hash算法进行计算,那么也获得了一个hash值,两两比对如果相同则可以证明此证书数据完整性。就可以直接拿
服务端公钥
来搞事情了。(进行对称加密)
简而言之:HTTPS = HTTP + SSL/TLS
当服务端通过数字证书
来保证客户端确定能拿到真正的服务端公钥B
后,我们就可以在SSL
的基础上进行HTTP通信了,也就是HTTPS
。
非对称加密中,私钥和公钥是 一对多 的关系。因为公钥是公开大量发放的,而私钥是唯一的,所以拥有公钥的一方可以确定拥有 私钥 一方的身份,而拥有私钥的一方无法确定拥有 公钥 一方的身份。所以,HTTPS
就被分为单向认证和双向认证。
1.2、面试官:http和https的关系到底是什么?
http + 加密+认证+完整性保护 === https
http是直接明文进行传输的,并且可能收到中间人干扰导致数据被篡改。
https:
- 对传输数据进行加密处理
- 并且通过数字证书等方式进行CA认证
- 并且有mac来设置报文的摘要,防止报文被篡改(应用层发送数据时会附加一种叫做 MAC(MessageAuthentication Code)的报文摘要。MAC 能够查知报文是否遭到篡 改,从而保护报文的完整性。)
1、https是披着ssl外壳的http
-
HTTPS 并非是应用层的一种新协议
-
只是 HTTP 通信接口部分用SSL(Secure Socket Layer)和 TLS(Transport Layer Security)协议代替而已
-
本来http是直接和tcp进行通信的,当使用ssl的时候就变成了先和ssl通信,再由ssl和tcp通信
-
ssl是独立于http的协议
- 应用层上不光是http,其他应用层比如smtp、talnet都可以配合ssl协议使用
- ssl是当今世界上应用最为广泛的网络安全技术
1.3、链路层怎么从ip获取到MAC地址?
因为分组需要经过数据链路层,数据链路层离不开MAC,网络层首部含有源IP地址和目标地址,如果通过IP地址找到MAC地址?
使用ARP地址解析协议
-
每一套主机都在ARP高速缓存中 存放一个从IP地址到硬件地址的映射表,并且这个映射表还经常动态更新(新增或超时删除)。
-
A主机要和B主机通信,那么在链路层要找到B主机的mac地址,查询过程:
-
A现在自己的ARP表中查询B的ip对应的mac地址,找到了就皆大欢喜
-
找不到的话:找不到就进行广播,广播一个ARP请求分组,把自己ip和硬件地址以及想要知道某个ip的mac地址也发出去
- 之后本局域网上所有主机运行的ARP进程就会都接收到这个ARP请求分组】
- 只要有一个主机知道“某些线索”就会给A发一个单播,告诉A:B的mac地址是啥
-
1.4、ping原理是什么?
判断与对方网络是否畅通,使用的最多的莫过于
ping
命令
ping 是基于 ICMP
协议工作的,ICMP是网络层的,和IP协议是同层的
ICMP 全称是 Internet Control Message Protocol,也就是互联网控制报文协议。
ICMP
主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。
ICMP 报文是封装在 IP 包里面,它工作在网络层,是 IP 协议的助手。
总结:就有点像Ping是一个请求,这个请求是基于ICMP的,ICMP帮我们去看看目标ip主机是否在线,并且通过“回调”的方式告诉我们ip在线以及不在线的原因。
1.5、你知道中间人攻击吗?
《图解HTTP》:
1、攻击过程:
- 客户端发送请求到服务端,请求被中间人截获。
- 服务器向客户端发送公钥。
- 中间人截获公钥,保留在自己手上。然后自己生成一个伪造的公钥,发给客户端。
- 客户端收到伪造的公钥后,生成加密hash值发给服务器。
- 中间人获得加密hash值,用自己的私钥解密获得真秘钥。同时生成假的加密hash值,发给服务器。
- 服务器用私钥解密获得假密钥。然后加密数据传输给客户端。
2、攻击类型
- wifi欺骗:本地免费wifi
- dns欺骗
3、防止
1.6、其他热知识
1、一个TCP中可以进行几个HTTP请求
- HTTP/1.1存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求
- 在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行(多路复用)
2、post请求传输数据的格式有几种
- application/x-www-form-urlencoded
用于浏览器form表单,数据的编码方式采用 key1=val1&key2=val2 的形式
适用场景:数据量不大、数据层级不深的情况下强烈建议这种数据提交格式。
- multipart/form-data
当你需要提交文件、非 ASCII 码的数据或者是二进制流数据,则使用这种提交方式。类似下面这个请求示例:
- application/json
传输json格式
适用场景:数据结构较复杂,层级较深的情况。
二、vue
2.1、使用keepAlive之后生命周期的变化
1、vue有两种生命周期比较特别
-
activated:
- 被 keep-alive 缓存的组件激活时调用。初始化操作放在actived里面
-
deactivated:
- 被 keep-alive 缓存的组件停用时调用。在deactived里面,在里面进行一些善后操作
这两个阶段都是在keep-alive进行缓存的时候触发,这两个钩子在服务端渲染期间不会被调用
2、生命周期变化
添加keep-alive标签后会增加activated和deactivated这两个生命周期函数,初始化操作放在actived里面,一旦切换组件,因为组件没有被销毁,所以它不会执行销毁阶段的钩子函数,所以移除操作需要放在deactived里面,在里面进行一些善后操作,销毁的钩子函数一直没有执行。
-
正常生命周期
- created - 》 mounted -》 updated -》 destroyed
-
使用keepAlive之后
- 首次进入缓存页面:created -》 mounted -》 activated -》 deactivated
- 再次进入缓存页面:activated -》 deactivated
3、keep-alive原理
在这都不阐述了,可以参考:juejin.cn/post/704340…
2.2、mvc、mvp和mvvm的区别你真的懂了吗
1、MVC
- View和Model使用了观察者模式
- 数据改变的时候:Model 层发生改变的时候它会通知有关 View 层更新页面。
- view视图触发了事件的话,Conroller监听到,就通过Model层给controller层暴露的接口,来完成对Model的修改,之后Model层再去通知View层改变
这样 View 层和 Model 层耦合在一起
MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦
2、MVP
mvp的模型图如下显示
MVP的presenter相对于MVC可以实现model和view层的解耦
mvp的核心在于presenter层,该层的核心是对于dom元素的操作,以jquery实现列表页为例,presenter主要是通过循环将Model中的数据与html的标签进行组合,添加到View中去。
3、MVVM
mvvm的模型图如下显示
mvvm的核心在于Model层,该层的核心是对于数据的操作,相对于mvp模式,我们的编码重点已经由对dom的操作转化为对数据的操作。VM层是指将数据展示到view层以及view层的数据传递至Model层。vue就是viewModel的一个典型的示例
三、React
3.1、自定义Hook面试题
1、返回浏览器高宽的Hook
因为浏览器的可视化高宽是在变化的,所以通过监听浏览器的resize事件
const useWindowSize = () => {
const [width, setWith] = useState(window.innerWidth)
const [height, setHeight] = useState(window.innerHeight)
useEffect(() => {
window.addEventListener('resize', () => {
setWith(window.innerWidth)
setHeight(window.innerHeight)
})
return () => {
window.removeEventListener('resize')
}
})
return {
width,
height
}
}
2、防抖
function useDebounce(fn, delay, dep = []) {
const { current } = useRef({ fn, timer: null })
useEffect(function () {
current.fn = fn
}, [fn])
return useCallback(function f(...args) {
if (current.timer) {
clearTimeout(current.timer)
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args)
}, delay)
}, dep)
}
3、节流
function useThrottle(fn, delay, dep = []) {
const { current } = useRef({ fn, timer: null })
useEffect(function () {
current.fn = fn
}, [fn])
return useCallback(function f(...args) {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer
}, delay)
current.fn.call(this, ...args)
}
}, [dep])
}
3.2、vue2 vue3 react的diff算法区别
来源:juejin.cn/post/711417…、juejin.cn/post/691937…
下面只是进行总结,详细的看这两个大佬的掘金文章~
1、共同点
都使用了tree-diff、component-diff、element-diff的思维,即:
- 只比较同层的节点,不同层不做比较
- 新旧节点是同层节点,但不是同一个节点,不做精细化比较
- 只有同层且同key的节点才进行精细化的比较
2、diff遍历方式的不同点
Vue2
定义了四个指针:
使用旧列表的头一个节点oldStartNode
与新列表的头一个节点newStartNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的头一个节点oldStartNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的头一个节点newStartNode
对比
比较不同的节点,进行相应的指针移动操作
Vue3
先通过前置和后置处理,再使用最长递增子序列
①前置与后置预处理
类似于这样,相同的部分去掉,留下不同的
②最长递增子序列
创建一个source数组,填充满-1
再找到source数组的最长递增子序列,也就是 2 3,那么就只需要对1 -1 进行操作了,这样做是为了尽量减少操作DOM的次数
React
由于新的fiber架构是链表的形式就没有使用双向指针的方式,而是定义了一个lastIndex变量结合从左到右进行遍历(通过维护一个LastIndex来判断是否想要移动位置)
function foo(prevList, nextList) {
let lastIndex = 0
for (let i = 0; i < nextList.length; i++) {
let nextItem = nextList[i];
for (let j = 0; j < prevList.length; j++) {
let prevItem = prevList[j]
if (nextItem === prevItem) {
if (j < lastIndex) {
// 需要移动节点
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
}
}
}
}
遍历在旧列表的位置为
0 < 2 < 3
,说明A C D
这三个节点都是不需要移动的。此时lastIndex = 3
, 并进入下一次循环,发现vnode-b
在旧列表的index
为1
,1 < 3
,说明DOM-B
要移动。通过观察我们能发现,只需要把
DOM-B
移动到DOM-D
之后就可以了
因为此时的LastIndex执行了preChildren中d的位置3,但是遍历到nextChildren的时候b在preChildren的位置是1,所以1 < 3的时候就需要移动了,只需要移动到LastIndex指向的元素后面就行
diff处理方式的不同点
-
对静态节点的处理不同
vue用于因为是temp模板编译,结构清晰,所以对静态节点能够很好的进行分析
- vue2:Vue2 是判断如果是静态节点则跳过循环对比
- vue3:把整个静态节点进行提升处理,静态节点在Diff 的时候是不会进入循环的,所以 Vue3 比 Vue2 的 Diff 性能更高效
- React 因为是通过 JSX 进行编译的,是无法进行静态节点分析的(jsx没有做静态编译优化处理),所以 React 在对静态节点处理这一块是要逊色的。
-
是否可中断
- Vue2 和 Vue3 的比对和更新是同步进行的,这个跟 React15 是相同的,vue2和vue3都是不可中断的,如果数据量太大的话可能发生卡顿(但是尤大也说了用于vue的特性这种情况基本没有,反而是react16.8之前使用stack的架构的时候由于不可中断才会出现卡顿的现象,因为react的计算量比vue大,不仅要diff还要处理state啥的)
- react16起使用fiber架构,从此之后比对和更新是异步进行的,所以 React16 以后的 Diff 是可以中断,Diff 和任务调度都是在内存中进行的,所以即便中断了,用户也不会知道。
-
是否可以双端对比
-
Vue2 和 Vue3 都使用了双端对比算法
-
React 的 Fiber 由于是单向链表的结构,无法实现双端对比
-
3.3、其他热知识
1、redux为什么每次都返回一个新的state
- 因为react中的shouldComponentUpdate或者是useEffect等等hook的dep依赖比较的话都是基于Object.is的浅比较,如果每次都在原始的基础上修改属性的话,可能新旧state之间由于浅比较而认为是相同的变量,因此页面就不会重选人了
- 并且redux每次返回新的state也是有助于存储state快照,方便排查问题
四、浏览器
4.1、你知道浏览器有几个进程吗?
参考:t.zoukankan.com/jiox-p-1453…
一个浏览器主进程、一个GPU进程、一个网络进程、多个渲染进程和多个插件进程
1、浏览器主进程
- 负责浏览器页面的显示,浏览器左上角刷新(f5)、前进后退等操作
- 各个页面的管理,点击新建页面,删除销毁页面
- 获取渲染进程中绘制好的内容,绘制到用户界面
2、GPU进程
也就是我们常说的:开启物理GPU协助,主要为了渲染3d图片等更好的性能,避免cpu的阻塞
那cpu和gpu区别是什么?
3、网络进程
当我们在浏览器url输入框中输入url字符串后 并回车,浏览器主进程就通过IPC(进程通讯)通知网络进程干活了,后续就进行dns查询以及和服务器的http、tcp连接都是由网络进程完成。
也正是因为多个进程各司其职才能让我们可以异步的调用接口请求数据,而不会阻塞浏览器的渲染导致页面丢帧卡顿
4、多个渲染进程
不同的tab页面都有属于主机的渲染进程
渲染进程包含了多个线程:
-
GUI渲染线程(负责页面渲染)
-
获取到index.html之后,解析HTML、CSS、构建DOM树和RenderObject树(包含了DOM和CSS信息的树),布局(分析dom和css得到不同的元素所在的位置)和绘制(绘制像素点展示到用户界面)
-
当页面需要重绘(比如莫某个元素的背景颜色改变了)绘制回流(比如某个元素width或者height位置改变了),该线程也会指向
-
GUI线程和js线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- 这也就是为什么:不要有超过50ms的长任务,这样会导致无法在100ms以内响应用户的输入(点击、输入、滑动等),用户那边的体验就不好(100ms是因为谷歌那边发布的用户性能指标,一次好的用户体验是用户输入后响应的response时间是100ms以内)
- 如果js指向长任务,一直占着使用权(导致GUI阻塞),可能导致丢帧,因为我们一帧是16.67ms渲染完成(不同的设备FPS不同,帧率不同),导致的页面卡顿问题
-
-
js线程(单线程)
-
也称为JS内核,负责处理Javascript脚本程序
-
JS引擎线程负责解析Javascript脚本,运行代码。
- 所以浏览器就有了eventLoop,并且定义了微任务和宏任务来完成js中任务的调度,更加高效优先的去指向js脚本
-
事件触发线程
- 可以理解为是维护eventLoop的一个线程
- 并不是js的线程,而是浏览器的线程,为了配合浏览器调度的
- 浏览器有执行栈以及不同任务的任务队列(微任务、宏任务)
- 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
-
-
定时器线程
- 定时器setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果任务队列处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
- 并且node里面也是默认把setTimeout(0)设置为了setTimeout(1)
-
异步http请求线程
- 用于处理请求(XMLHttpRequest、Window.fetch等),在网络进程帮忙进行dns查询和连接后是通过浏览器新开一个线程
- 将检测到状态变更(如ajax返回结果)时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入js引擎线程的事件队列中。再由JavaScript引擎执行。
5、多个插件进程
不同的浏览器其实都支持插件,比如我们谷歌右上角的扩展程序
(稀土掘金yyds)
不同的插件就是一个个的进程
4.2、面试官:如果localStorage满了,你咋办?
1、localStorage满了会有什么反应?
报错:QuotaExceededError
2、什么情况下可能满
localStorage有5MB的内存(不同浏览器可能不同),单纯存储诸如用户信息、全局变量等数据基本很难用完,并且localStorage是有同源策略的,就是同一个源单独维护一个localStorage,大小是5mb,不同的源分别有属于主机的localStorage,除非:
- 很多公司在同一个域名下会有几百条业务线,开发测试的时候是无感知localStorage使用满了,但是上线只会可能就导致整个域名下localStorage直接爆掉了
- 开发不小心直接在localStorage存储了文件内容信息
3、方案
-
划分子域名,各域名下存储空间各业务统一规划
- 因为划分了子域名就不是同源了,那么localStorage就分开了
-
有些localStorage里面存储跨页面传数据的内容:修改成优先采用url传数据
-
用indexedDB存储文件类型的数据,localStorage存储业务数据
-
设置localStorage的过期时间(要自己封装一下localStorage,使用时间戳)
-
最后的兜底方案:清掉其他人的存储,优先使用当前急需要的,封装一个LCU,也就是先进先出(基于存储时间,存的越久就越先移除)
4.3、你知道crossorigin字段吗?
script、link、img这些的src本身都可以跨域的,但是这个crossorigin是为了干嘛呢?
1、用于获取跨域报错信息
浏览器的安全策略:HTML5中规定了,允许本地获取到跨域脚本的错误信息的,但有两个条件:
- 一是跨域脚本的服务器必须通过 Access-Control-Allow-Origin 头信息允许当前域名可以获取错误信息;
- 二是网页里的 script 标签也必须指明 src 属性指定的地址是支持跨域的地址,也就是 crossorigin 属性。
如果两个条件缺少一个的话:
如果使用window.onerror事件去捕获跨域资源的错误的话,onerror通常会上报“Script error”
原理:设置了crossorigin属性之后,script标签去请求资源的时候,请求会带上origin头,然后会要求服务器进行 cors校验。但是不设置的话,请求是没有origin的。
跨域的时候如果response header 没有 ‘Access-Control-Allow-Origin’ 是不会拿到资源的。cors验证通过后,拿到的script运行内部报错的话,,window.onerror
捕获的时候,内部的error.message
可以看到完整的错误信息。
2、获取跨域图片
在HTML5中,一些 HTML 元素提供了对 CORS 的支持, 例如 <audio>
、<img>
、<link>
、<script>
和 <video>
均有一个跨域属性 (crossOrigin property),它允许你配置元素获取数据的 CORS 请求。
这些属性是枚举的,并具有以下可能的值:
关键字 | 描述 |
---|---|
anonymous | 对此元素的 CORS 请求将不设置凭据标志。 |
use-credentials | 对此元素的CORS请求将设置凭证标志;这意味着请求将提供凭据。 |
"" | 设置一个空的值,如 crossorigin 或 crossorigin="",和设置 anonymous 的效果一样。 |
crossorigin
的属性值分为anonymous
和use-credentials
。如果设置了crossorigin
属性,但是属性值不正确的话,默认是anonymous
。anonymous
代表同域会带上cookie,跨域则不带上cookie,相当于 fecth请求的credentials: 'same-origin'
。use-credentials
跨域也会带上cookie,相当于fetch请求的credentials: 'include'
,这种情况下跨域的response header 需要设置'Access-Control-Allow-Credentials' = true
,否则cors失败。
canvas使用drawImage加载跨域图片的话,回报错
由于canvas自身的设计,加载的是本地的资源,对跨域资源默认是不加载的
- 方案一: 如果图片不大不多可以使用base64
- 方案二: 实例的image对象的设置img.crossOrigin = ' ',并且在服务器端设置Access-Control-Allow-Origin:*(或运行的域名)
html通过标签引入图片不会有跨域问题(因为html中img本身支持跨域),但是js引入图片会有跨域问题?
- img的src都是没有跨域的,所以就可以进行跨域
- js的话
都加上 crossorigin = "anonymous",并且在服务器端设置Access-Control-Allow-Origin:*(或运行的域名)
4.4、这些window属性真的很容易记混!
之前做的一个笔试题:
选A,因为host不仅返回域名还有端口
背一波:
比如链接:
4.5、如何实现LocalStorage锁?
同一个页面开了多个Tag,如何实现同时读写
如何加锁?
在这篇文章里面,大牛Lamport介绍了利用两块共享区域实现快速锁的算法
const THREAD_ID = `${Math.floor(Math.random() * 1000)} - ${Date.now()}`;
const setItem = localStorage.setItem.bind(localStorage);
const getItem = localStorage.getItem.bind(localStorage);
const removeItem = localStorage.removeItem.bind(localStorage);
const nextTick = fn => setTimeout(fn, 0);
class Mutex {
constructor (key) {
this.keyX = `mutex_key_${key}_X`;
this.keyY = `mutex_key_${key}_Y`;
}
lock() {
return new Promise((resolve, reject) => {
const fn = () => {
setItem(this.keyX, THREAD_ID);
if (!getItem(this.keyY) == null) {
nextTick(fn); //restart
}
setItem(this.keyY, THREAD_ID);
if (getItem(this.keyX) !== THREAD_ID) {
//delay
setTimeout(() => {
if (getItem(this.keyY) !== THREAD_ID) {
nextTick(fn) //restart
return;
}
//critical section
resolve();
removeItem(this.keyY);
}, 10);
} else {
resolve();
removeItem(this.keyY);
}
};
fn();
});
}
}
const KEY = 'key';
let mutex = new Mutex(KEY);
mutex.lock().then(() => {
let value = parseInt(localStorage.getItem(KEY) || 0, 10);
value += 1;
localStorage.setItem(KEY, value);
});
代码解读:就是也是利用了Localstorage全局变量的特性
-
Y可以是不同的tab,如果当前tab正在操作localStorage的话,那么就在localStroage里面操作:setItem(this.keyY, THREAD_ID),表示当前加锁了
-
如果有tab在用,那么就执行nextTick(fn) ,上面nextTick用的是setTimeout实现的,所以就是等到下一次宏任务执行的时候,刚好就是localStorage的操作时间周期了
-
每次resolve的时候就是当前拿到锁的这个tab执行操作的时候,执行完了之后就把this.keyY的值清空,也就是锁释放了
4.6、小伙:你知道为什么要区分宏任务、微任务吗?
- 微任务是线程之间的切换,速度快。不用进行上下文切换,可以快速的一次性做完所有的微任务。
- 宏任务是进程之间的切换,速度慢,且每次执行需要切换上下文。因此一个Eventloop中只执行一个宏任务。
- 而区分微任务和宏任务的根本原因是为了插队。由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用。
- 反观如果不区分微任务和宏任务,那么新注册的任务不得不等到下一个宏任务结束后,才能执行。
4.7、如果我要在浏览器关闭前发送数据怎么办?
例如我们是一个前端监控的SDK,收集到了用户当前页面阅读时长这个数据,那么肯定要在当前页面关闭的时候把数据上报到服务端中,这个时候怎么办?
1、使用XMLHttpRequest可以吗?
通过监听unload时间发起XMLHttpRequest请求
但是谷歌浏览器已经不允许页面关闭期间进行同步的 XMLHTTPRequest()
,这条规则适用于 beforeunload
、unload
、pagehide
和visibilitychange
这些 API
;
存在问题:
- 如果是同步的XMLHttpRequest的话可能导致下一个导航出现的延后,因为是同步的要等这个请求结束
- 如果是用异步的xhr(xhr.open的第三个参数是true),浏览器会直接在关闭的时候cancel掉这些请求
为了确保页面在卸载时讲数据发送到服务器,官方建议使用 sendBeacon()
或者 Fetch keep-alive
2、navigator.sendBeacon
有一个规则和特点:
- 通过 HTTP POST 将少量数据异步传输
- 这个请求不需要响应,保证在页面的 unload 状态从发起到完成之前被发送
- 不会阻塞页面卸载,也就不会影响下一导航的载入
- 支持跨域
navigator.sendBeacon(url, data);
3、使用fetch并开启keep-alive
当使用fetch() 方法时,如果把keeplive 设置为true,即便页面被终止请求也会保持连接。
window.onunload = function() {
fetch('/test', {
method: 'POST',
body: "gogocj",
keepalive: true
});
};
原理:
fetch是处理http请求的库,浏览器原生支持的,设置keepalive其实就是开启了http协议的keep-alive,连接持久化
拓展:fetch和ajax的区别
因为ajax底层是xmlhttprequest,发送一个请求的代码量太大了,很麻烦,所以社区就有了一个更方便调用的fetch方法
- ajax是利用XMLHttpRequest对象来请求数据的,而fetch是window的一个方法
- fetch比较与ajax有着更好更方便的写法
- fetch没有办法原生监测请求的进度,而XHR可以
- fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
fetch和axios的区别
Fetch API
是一个提供了fetch()
方法的用于发送网络请求的接口,它内置于现代浏览器中,因此无需安装。Axios
是一个第三方库,我们可以通过cdn或包管理器安装使用它,Axios
可以在浏览器或node.js
中运行。
-
Fetch
和axios
都是基于promise
的 -
axios支持浏览器和node
- 在浏览器中就使用 XMLHttpRequest
- 从 node.js 发出 http 请求
-
转化数据
- fetch返回的是一个未处理的方法集合,我们可以通过这些方法得到我们想要的数据类型。如果我们想要json格式,就执行response.json() ,如果我们想要字符串就response.text()
- 自动转换JSON数据
-
是否可以中断
- axios可以abortToken api中断请求
- fetch不支持abort
-
错误处理方面
- axios中如果收到404的话promise会直接返回一个reject所以就可以定义catch处理不同的报错
- fetch每次拿到响应结果都要先判断是否成功,如果没成功再手动抛出错误
-
其他
- axios基于XMLHttprequst所以也可以监听请求进度(比如我们要监听下载的进度等等)
4.8、浅浅复习一下defer和async区别
关于defer我们需要注意下面几点:
- defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
- 如果有多个声明了defer的脚本,则会按顺序下载和执行
- defer脚本会在DOMContentLoaded和load事件之前执行
- defer加载是异步的,不影响html解析
- defer在html解析完了才会再执行(不影响html的解析)
关于async
,也需要注意以下几点:
- 只适用于外联脚本,这一点和
defer
一致 - 如果有多个声明了
async
的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序(所以async引入不同脚本的时候,脚本之间关系应该是独立的,不能有包含和引用关系,因为无法控制引入的先后顺序)——async模块都独立的话才能保证可以异步并行加载这些脚本文件 async
会在load
事件之前执行,但并不能确保与DOMContentLoaded
的执行先后顺序- 异步加载不影响html解析,加载完之后立马进行执行(执行会影响html的解析)
拓展:DOMContentLoaded和load事件的不同
- DOMContentLoaded是html解析完就执行,但是不能保证img、样式表、子框架frame这些完全加载
- load:仅用于检测一个完全加载的页面,页面的html、css、js、图片等资源都已经加载完之后才会触发 load 事件。
4.9、SSO单点登录如何实现?
来源:zhuanlan.zhihu.com/p/281414244
1、虚假的单点登录
如果业务系统都在同一一级域名下,比如wenku.baidu.com
`tieba.baidu.com‘,直接给cookie设置domain就可以共享cookie了
2、真实的单点登录
一次「从 A 系统引发登录,到 B 系统不用登录」的完整流程
- 用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
- SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
- SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 A 接口
- 系统 A 校验 ticket,成功后正常处理业务请求
- 此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
- SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 B 接口
原理
- 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上(接口返回的respone设置了callback url 跳转到A系统),这个接口通常在 A 向 SSO 注册时约定
- 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
- 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
- callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
- 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
- 访问 B 系统也是一样
4.10、Token生成流程以及原理
token是对session的一个升级,解决了前后端分离的session不能共享的一个难题
1、payload组成
- 用户信息
- token过期时间
2、Header组成
- 加密算法类型
3、Token生成原理
- 将上传的信息、过期时间等(payload)和设置的header(加密算法类型)进行base64加密,形成密文payload密文,header密文
- 将形成的密文用句号链接起来,用服务端秘钥进行HS256加密(根据Header中设置的加密算法类型),生成签名.
- 将前面的两个密文后面用句号链接签名形成最终的token返回给服务端
4、Token发送给浏览器的认证过程
(1)用户请求时携带此token(分为三部分,header密文,payload密文,签名)到服务端,服务端解析第一部分(header密文),用Base64解密,可以知道用了什么算法进行签名,此处解析发现是HS256。
(2)服务端使用原来的秘钥与密文(header密文+"."+payload密文)同样进行HS256运算,然后用生成的签名与token携带的签名进行对比,若一致说明token合法,不一致说明原文被修改。
(3)判断是否过期,客户端通过用Base64解密第二部分(payload密文),可以知道荷载中授权时间,以及有效期。通过这个与当前时间对比发现token是否过期。
4.11、白屏的原因&如何检测到
1、白屏原因
- js执行过程的错误(可以通过window.onerror进行监控)
- 弱网、网络延迟(小程序的话可以通过getUserNetWork的API监听,web浏览器也有navigator.onLine这样的API检测用户的网络状况)
- 资源错误(可以通过window.addEventListener('error') 进行监控到)
2、方案
-
onerror + DOM 检测
原理就是当下主流的SPA框架下,DOM一般挂载在一个根节点上(id为root的div),发生白屏通常是根节点下所有DOM被卸载了,这个方案就是通过监听全局的onerror事件,在异常发生的时候去检测根节点下是否挂载DOM,如果没挂载就说明白屏了
-
Mutation Observer Api
其本质是监听 DOM 变化,并告诉你每次变化的 DOM 是被增加还是删除
-
饿了么-Emonitor 白屏监控方案
原理是记录页面打开 4s 前后 html 长度变化,并将数据上传到饿了么自研的时序数据库,根据html长度的异常变化来监控
-
定义一套算法,侦听页面元素的变化,每次新增的dom,都会计算这个元素的得分,给每一个dom都添加一个权重,对重要内容添加埋点,如果重要内容都没展示出来的话,那么就是白屏处理了(但是这样就入侵业务代码了)
-
图形识别:拿一个手机作为机器人每次自动进入应用,截屏,与正常图片进行比对,如果空白太多就报警(搞一个CV监控机器人)
4.12、如何设置cookie跨域?
1、子域名共享cookie
直接设置cookie的domain字段就行
2、非子域名共享cookie
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,还需指定Access-Control-Allow-Credentials字段,
并且XMLHttpRequest对象中还有一个withCredentials属性,可以设置跨域
最近更新了chrome版本至v80. . , 发现withCredentials居然没用了。跨域请求里cookie已经拿不到了。(PS:其他浏览器正常)
Google80以后的版本,cookie默认不可跨域,除非服务器在响应头里再设置same-site属性为none(捂脸),和secure属性。
4.13、面试官:你知道哪些跨页面通信方法?
来源:www.cnblogs.com/frank-link/…
背景:在浏览器中,我们可以同时打开多个
Tab
页,每个Tab
页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab
间共享。但是有时候比如我们一个tab登录了,希望另外一个tab同步登录状态的话,就需要进行跨页面(tab)通信了
1、Broadcast Channel API
Broadcast Channel API 可以实现同 源 下浏览器不同窗口,Tab 页,frame 或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面) 之间的简单通讯。
通过onmessage监听,postMessage进行发送消息
2、Service Worker
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
是一个可以长期运行在后台的 Worker
,能够实现与页面的双向通信。多页面共享间的 Service Worker
可以共享,将 Service Worker
作为消息的处理中心(中央站)即可实现广播效果
3、LocalStorage
当 LocalStorage
变化时,会触发 storage
事件。利用这个特性,我们可以在发送消息时,把消息写入到某个 LocalStorage
中;然后在各个页面内,通过监听 storage
事件即可收到通知。
4、websocket
5、非同源页面---用iframe
可以使用一个用户不可见的 iframe
作为“桥”。由于 iframe
与父页面间可以通过指定 origin 来忽略同源限制,因此可以在每个页面中嵌入一个 iframe
用于每一个页面都嵌入了同一个url的iframe。所以对iframe来说就是同源的了,就可以用上面的同源方法在这些iframe中进行操作了
4.14、如何防iframe嵌套导致的安全问题
1、window.parent、top、self区别
- window.self是对当前窗口自身的引用,和window是相同的
- window.top:是最顶层的窗口,也就是浏览器窗口(如果窗口被嵌套很多层的话)
- window.parent:返回父窗口,父窗口一般是顶层窗口,但是如果框架中还有框架的话,父窗口和顶层窗口就不一定相同了
2、iframe安全方法
-
所以可以通过window.top是否等于window(当前窗口的方式)来防止嵌套的钓鱼iframe
window.top == window.self
-
X-Frame-Options作为响应头,在服务器上对使用iframe的权限进行限制
- 又以下三个选项
-
DENY:当前页面不能被嵌套iframe里,即便是在相同域名的页面中嵌套也不允许,也不允许网页中有嵌套iframe SAMEORIGIN:iframe页面的地址只能为同源域名下的页面 ALLOW-FROM:可以在指定的origin url的iframe中加载
-
CSP响应头,也是后端的响应头,但是兼容性不好
-
sandbox是用来给指定的iframe设置一个沙盒限制iframe的更多权限
-
sandbox是h5的一个新属性
-
启用方式就是使用sandbox属性:
<iframe sandbox src="..."></iframe>
-
限制如下
1. script脚本不能执行 2. 不能发送ajax请求 3. 不能使用本地存储,即localStorage,cookie等 4. 不能创建新的弹窗和window 5. 不能发送表单 6. 不能加载额外插件比如flash等
-
4.15、前端安全问题备忘录
1、XSS
-
分类
-
反射型XSS
- 一般发生在前后端一体的应用
- 用户输入带参数的URL,后端服务器处理这个参数内容并对页面进行排版,把页面发送给客户端(没录入数据库)
-
存储型XSS
- 用户在诸如评论的input输入中写下恶意代码,存储到服务器数据库中,其他用户看到这个评论的时候可能就触发恶意代码了
-
基于DOM型XSS
- 这种攻击不需要经过服务器,网页本身的js就可以改变HTML
-
-
防御
-
输入过滤,编码
-
使用innerHTML小心点
- 如果用 Vue/React 技术栈,不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。(用v-html是不进入Vue的render阶段的直接呈现)
-
使用 W3C 提出的 CSP (Content Security Policy,内容安全策略),定义域名白名单
-
设置Cookie 的 HttpOnly 属性,禁止JavaScript读取cookie
-
验证码:防止脚本冒充用户提交危险操作。
-
4.15、其他热知识
1、cookie过期时间问题
一般cookie储存在内存里,若设置了过期时间则储存在硬盘里,浏览器页面关闭也不会失效,直到设置的过期时间后才失效。若不设置cookie的过期时间,则有效期为浏览器窗口的会话期间,关闭浏览器窗口就失效。
拓展:cookie的内容
cookie的内容主要包括:名字name,值value,过期时间expires,路径path和域domain。路径和域一起构成cookie的作用范围。
2、什么是语义化标签
指的是当前标签本身表达了它标签的字面意思
比如:
- header元素:用来展示logo 搜索框等
- main标签:主要内容
- aside标签:旁边的侧边栏
- strong元素:强调文本
- footer标签:文档底部信息
- nav标签:导航区域
- h1-h6:标题标签
- section标签:可以定义独立部分
好处:
- 结构优化
- 有利于爬虫提取分析有效信息,提高SEO
- 提高开发效率
3、容易忘记的BOM DOM的API
3.1、BOM
-
history(主要是浏览器里历史记录相关信息)
- history.forward()
- history.back()
- history.go()
-
location(浏览器地址栏)
-
navigator
-
浏览器的基本信息
- navigator.appName
- navigator.appVersion
- navigator.platform
-
-
尺寸
- window.innerHeight \ innerWidth
-
滚动事件
- window.onscroll
-
load事件
3.2、DOM
-
获取元素
-
document.getElementById
-
document.getElementsByClassName
-
document.getElementsByName
-
主要是匹配标签中属性name的值
-
一般和input配合用 :
<input name="x" type="radio" value="猫">
-
-
document.getElementsByTagName
-
document.querySelector("css 选择器")
-
document.querySelectorAll("css选择器")
-
-
元素操作
- 元素.getAttribute("属性名");
- 元素.setAttribute("属性名",值);
- 元素.removeAttribute("属性名");
-
获取元素内容
- innerText
- innerHTML
-
元素绑定事件
4、打开两个页面,其中一个页面登录了,如何让另外一个页面自动刷新登录态
使用localStorage的监听
window.addEventListener('storage', function(e){
if (e.key === 'user'){
// 更新vuex数据等操作
}
})
拓展:
- 如果要实现localStorage的跨域也可以通过iframe的方式监听localStorage事件
- 如果要设置localStorage的过期时间的话就要自己封装一下,使用时间戳
5、域名和域名对应相同的ip 也是跨域
http://www.test.com/a.js
http://192.168.4.2/a.js
也是非同源
6、处理过哪些兼容性问题
-
浏览器兼容
-
IE9以下的不支持html5 css3这些
- 比如css3的伪元素就用单冒号不用双冒号
-
IE对WebSocket有兼容性问题,upgrade字段是大写的websocket,所以就会报错,所以要进行一个polyfill一下
-
-
屏幕分辨率兼容
- 使用响应式框架:bootstrap等
-
跨平台兼容
- 比如一个应用有两个入口 一个是移动端一个是桌面端
- 所以要么就是监听用户当前设备情况是PC还是手机进行不同的跳转(把pc和移动端分为不同的应用,根据用户情况跳转到不同应用)
- 使用响应式框架(当然要移动端优先)
7、ETag解决了Last-Modified什么问题
-
ETag如何判断资源是否更改
- 通过文件hash算法
- 用Last-Modified与Content-Length表示16进制组合的字符串
-
Last-Modified不足
- If-Modified-Since只能查询到秒级别的更改,假如一秒内进行了多个更改是察觉不到的,缓存就不会失效
-
所以ETag即检测时间变化和检测内容变化
8、浏览器不同的Tab是线程还是进程?
chrome中界面内的每个标签页(Tab)都是一个进程。而其它浏览器的每个标签页都是一个线程
Tab是进程的好处:
- 安全性:因为进程之间不共享资源和空间,所以每个Tab都是相对独立的区域
- 健壮性:进程中一个线程崩了,那么就会导致其他线程崩,所以如果一个tab崩了还导致其他tab也运行不了那么不太符合用户需求,但是多进程不会有这个问题
缺点:
- 进程启动、关闭和切换的时候都要额外的开销,比线程要慢
五、Javascript
5.1、当面试官让你写了一个简单的递归算法,它肯定”另有所图“
1、递归例题
Fibonacci、跳台阶等
function jumpFloor (n , ac1 = 1 , ac2 = 2) {
if( n == 1 ) {return ac1};
if( n == 2 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
2、递归缺点
- 保存众多的调用栈
- 占用大量内存,容易导致栈溢出和内存溢出
3、解决方案
- while
- 记忆化递归
//2、记忆化递归(保存中间结果)
//对于cache数组,cache[n]就表示缓存的f(n)的结果
let cache=[,1,2];
function jumpFloor2(n){
//也就是我们在找f(n)的值的时候,如果缓存里面有,直接用缓存
if(cache[n]!==undefined) return cache[n];
return cache[n]=jumpFloor2(n-1)+jumpFloor2(n-2);
}
- 尾递归优化
这样只会存在一个调用栈,空间复杂度是O(1)
注意点:尾递归只能用在严格模式?正常模式是无效的,为什么?
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。 func.arguments:返回调用时函数的参数。 func.caller:返回调用当前函数的那个函数。 尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
那么如何在正常模式下使用尾递归优化呢?
使用while替换递归(又回到了原点。。。)
拓展:
-
你觉得递归有什么缺点?
- 调用栈溢出
- 每次递归调用都会在内存栈中分配空间,调用的层次太大了,就会超过栈容量
-
爆栈属于什么类型的错误?系统错误还是什么?
- 是JavaScript引擎抛出的错误,由浏览器帮忙抛出
-
针对你说的问题怎么改进代码? 就是上面的哪些优化
5.2、你知道js错误有哪些吗?
-
Error
- 其他错误类型都是继承该类型
- 浏览器很少会抛出Error类型的错误,该类型主要用于开发者抛出自定义错误(比如console.error 或者手动Promise.reject)
-
InternalError
InternalError
类型的错误会在底层JavaScript
引擎抛出异常时由浏览器抛出.例如,递归过多导致了栈溢出
-
EvalError
EvalError
类型错误会在使用eval()函数发生异常时抛出
-
RangeError
RangeError
会在数值越界时抛出.例如,定义数组时如果设置了不支持的长度,如-20.又或者没有给递归设置停止条件时触发.
-
ReferenceError
ReferenceError
会在找不到对象时发生.- RHS的时候
-
SyntaxError
- 为JavaScript代码中的语法错误会导致代码无法执行.
-
TypeError
- 主要发生变量不是预期类型,或者访问不存在的方法时等原因导致
-
URIError
URIError
只会在使用encodeURL()或decodeURL()但传入了格式错误的URL时发生
5.3、为什么symbol不能new,实现一个Symbol看看?
1、当new Symbol()的时候会发生什么
控制台报错,原因就是 Symbol构造函数内部 用了 this instanceof Symbol来判断this所指向的对象原型上的constuctor是否为Symbol构造函数,如果是的话,就抛出控制台打印出的错误。Bigint同理。
这样其实是为了避免创建Symbol、BigInt的原始值包装对象
在引用类型中,有三种原始值包装类型:String、Number、Boolean,原始值包装类型是用来把原始值包装成对象的引用类型
var a = Number('1') // 这个创建了一个 number 类型的数据, 非对象
typeof a // "number"
var b = new Number('1') // 这个是创建了一个Number对象,是对象
typeof b // "object"
b // Number {1}
b.valueOf() // 1
b.__proto__ === a.__proto__ // true
这种原始值包装对象除了js自身内部处理使用外,其他实际开发场景中一般都不会进行使用的,都是直接使用字面量的方式,所以新增的Symbol、BigInt没有必要向开发者提供创建对应原始值对象的API
2、实现一个Symbol
老规矩:先直接上代码
//自执行函数
(function(){
let root=this;
let createName=function(){
let id=0;
return function(desc){
id++;
return "@@__"+desc+"__"+id;
}
}()
let mySymbol=function Symbol1(desc){
if(this instanceof Symbol1){
throw new Error("Symbol1 is not constructor");
return ;
}
let myDesc= desc === undefined?undefined:String(desc);
let symbol=Object.create({
toString:function(){
return this.__name__;
},
valueOf:function(){
return this;
}
})
Object.defineProperties(symbol,{
"__name__":{
value:createName(myDesc),
writable:false,
enumerable:false,
configurable:false,
},
"__description":{
value:myDesc,
writable:false,
enumerable:false,
configurable:false,
}
})
return symbol;
}
let mapFor={};
Object.defineProperties(mySymbol,{
// 传k得symbol
"for":{
value:function(des){
let myDes=des===undefined?undefined:String(des);
return mapFor[myDes]?mapFor[myDes]:mapFor[myDes]=mySymbol(myDes);
},
writable:true,
enumerable:false,
configurable:true,
},
// 传symbol得k
"keyFor":{
value:function(obj){
for(let k in mapFor){
if(mapFor[k]===obj)return k;
}
},
writable:true,
enumerable:false,
configurable:true,
}
})
root.mySymbol=mySymbol;
})()
- createName函数是为了让每一个Symbol都是独特的,所以用了一个id进行不断的递增
let createName=function(){
let id=0;
return function(desc){
id++;
return "@@__"+desc+"__"+id;
}
}()
-
先判断this instanceof Symbol,也就是为什么new Symbol会报错的原因
-
通过Object.create给每一个Symbol都新建一个空间,并且通过Object.defineProperties的方式设置symbol的name和description数学系
-
然后实现Symbol.for和Symbol.keyFor两个api
- 每一个symbol都会放在mapFor这个映射表中,key就是用户传递的key描述,value就是生成的symbol对象
5.4、函数的length是啥
面试题:
console.log(123['toString'].length + 123) // 124
function fn1 () {}
function fn2 (name) {}
function fn3 (name, age) {}
console.log(fn1.length) // 0
console.log(fn2.length) // 1
console.log(fn3.length) // 2
如果有默认参数:就是第一个具有默认值之前的参数个数
function fn1 (name) {}
function fn2 (name = '林三心') {}
function fn3 (name, age = 22) {}
function fn4 (name, age = 22, gender) {}
function fn5(name = '林三心', age, gender) { }
console.log(fn1.length) // 1
console.log(fn2.length) // 0
console.log(fn3.length) // 1
console.log(fn4.length) // 1
console.log(fn5.length) // 0
如果有剩余参数:剩余参数不计算在内
function fn1(name, ...args) {}
console.log(fn1.length) // 1
5.5、 Promise穿透问题
先看几个输出:
then穿透(穿透需要有一个then进行捕获就不会继续穿透了)
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function(result){
console.log(result) // foo
})
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function(result){
console.log(result) // foo
})
.then(function(result){
console.log(result) // undefined
})
// example 1 值穿透
Promise.resolve(1)
.then(() => {return 2})
.then(Promise.resolve(3))
.then(console.log); // 2
// example 2 链式依次传值(被另外一个穿透所替代)
Promise.resolve(1)
.then(() => {return 2})
.then(() => {return Promise.resolve(3)})
.then(console.log); // 3
catch穿透:
new Promise((resolve, reject) => {
reject(1) //失败状态
})
.then(value => {
console.log('成功', value);
}, reason => {
console.log('失败', reason); //失败 1;无返回值、默认返回成功状态,状态值为undefined
})
.then(value => {
console.log('成功', value); //成功 undefined
}, reason => {
console.log('失败', reason);
})
第一个then接受了两个函数,第二个是失败时候的回调,由于第一个then里面没有抛出Error,则第二个then状态是resolve
new Promise((resolve, reject) => {
reject(1) //失败状态
})
.then(value => {
console.log('成功', value);
}, reason => {
console.log('失败', reason); //失败 1;无返回值、默认返回成功状态,状态值为undefined
})
.then(value => {
console.log('成功', value); //成功 undefined
}, reason => {
console.log('失败', reason);
})
.catch(reason => console.log('失败', reason)) //这里增加catch,但是不会走到这里来
new Promise((resolve, reject) => {
reject(1) //失败状态
})
.then(value => {
console.log('成功', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.then(value => {
console.log('成功', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
})
.catch(reason => console.log('失败', reason)) //这里执行了,失败 1;
使用.catch会默认为没有指定失败回调函数的.then指定失败回调函数
- .then 或者 .catch 的参数期望是函数,传入非函数就会发生值穿透;
- Promise方法链通过 return 传值,没有 return 就只是相互独立的任务而已
- 当使用.catch时,会默认为没有指定失败状态回调函数的.then添加一个失败回调函数
- .catch所谓的异常穿透并不是一次失败状态就触发catch,而是一层一层的传递下来的。
- 异常穿透的前提条件是所有的.then都没有指定失败状态的回调函数,如果.catch前的所有.then都指定了失败状态的回调函数,.catch就失去了意义。
5.6、复杂数组去重
如果要复杂的话,就要转换成字符串再进行比较了
let arr = [1,1,2,3,[4],[4],5,6,6]
let newArr=[]
arr.forEach((item)=>{
let isClude =false
newArr.forEach((item1)=>{
if(JSON.stringify(item)===JSON.stringify(item1)){
isClude=true
}
})
if(!isClude){
newArr.push(item)
}
})
console.log('newArr',newArr)//[1,2,3,[4],5,6]
5.7、不同的遍历方法
5.8、面试官:能讲一下js函数重载吗?
可能面试官想搞我一下,因为大家可能都学过一点java。是可以进行函数重载的
1、什么是函数重载
Java中的方法(函数)重载:类里面的两个或多个重名的方法,方法的参数个数、类型至少有一个不一样,构成了方法重载
但是:js其实不存在函数重载
当js函数重名函数形参个数不同时,实际上,后面的定义会覆盖前面的,相当于,所谓重名函数,实际上是一个函数被定义了好几遍,最后一次定义才是最终定义,调用时会调用最后一次定义的函数。
2、如何在js中实现类似于重载
js的重载主要是判断arguments的长度来执行不同的分支,最后实现方法的重载
5.9、这几个输出你真的会吗?
function f(x){
var x;
console.log(x)
}
f(5) // 5
function f(x){
var x = 10;
function x() {}
console.log(x)
}
f(5) // 10
function func(name) {
console.log(name) // 打印name函数
var name = 25;
console.log(name)// 25
function name () {
}
console.log(name) // 25
}
func(18)
这个就是同名变量的权重问题:
已初始化的一般变量 > 函数 > 函数参数 > 未初始化的一般变量
5.10、原型链易错题
Function.prototype.a = () => console.log(1);
Object.prototype.b = () => console.log(2);
function A() {};
var a = new A();
a.a(); // 报错:找不到a方法
a.b(); // 2
看图知结果
function Foo() {
this.a = 1;
return {
a: 4,
b: 5,
};
}
Foo.prototype.a = 6;
Foo.prototype.b = 7;
Foo.prototype.c = 8;
var o = new Foo();
console.log(o.__proto__)
console.log(o.a);
console.log(o.b);
console.log(o.c);
Foo里面返回一个新对象,直接把o的__proto指向了Object.prototype而不是Foo.prototype
5.11、你知道npm install的时候发生了什么吗?
来源:www.jianshu.com/p/dfdd236b5…
1、npm 模块安装机制:
-
发出npm install命令
-
查询node_modules目录之中是否已经存在指定模块
-
若存在,不再重新安装
-
若不存在
- npm 向 registry 查询模块压缩包的网址
- 下载压缩包,存放在根目录下的.npm目录里
- 解压压缩包到当前项目的node_modules目录
-
2、npm实现原理
-
执行项目自身的preinstall方法(就是一个钩子函数,在npm install前执行的,比如获取npm install后面的参数做一些特殊处理等等)
-
确认首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(之所以说是首层模块,是因为前端也有打包机制的,首次模块内部可能又引入了其他模块),这样就形成了一个以工程本身为整颗依赖书为根节点的树了
- 每一个首层依赖都是根节点下面的一颗子树
- npm之后会开启多线程从每个首层依赖模块开始逐步的去寻找更深层次的节点
-
获取模块(这是一个递归的过程)
-
获取模块信息:
-
version:包唯一的版本号; resolved:安装源; integrity:表明包完整性的hash值(验证包是否已失效); dev:如果为true,则此依赖关系仅是顶级模块的开发依赖关系或者是一个的传递依赖关系; requires:依赖包所需要的所有依赖项,对应依赖包package.json里dependencies中的依赖项 dependencies:依赖包node_modules中依赖的包,与顶层的dependencies一样的结构)
-
-
获取模块实体(通过上一步获取到的模块压缩包地址(resolved字段))
- npm会通过这个resolved来检查本地缓存中是否有该模块,如果没有的话就从仓库中下载
-
然后找到该模块信息后就查找该模块的依赖(引入了其他哪些模块)
-
-
模块扁平化
-
上一步获取到以工程项目本身为根节点的模块依赖树,可能有大量重复模块(不同模块引用了同一个第三方模块)——这种情况在npm3之前的话是没有优化的,所以造成了模块冗余
-
遍历树中所有的模块,逐个将模块放在根节点下面,如果发现了重复模块就丢弃
拓展:同名模块的丢弃
加入两个模块是同名,并且两个版本号之间存在兼容版本,那么就会保留兼容版本的模块,另外一个丢弃,假如不存在兼容版本,那么这两个模块都保留下来
-
-
下载模块到.npm
-
解压到node_modules中,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
-
执行工程自身生命周期 当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
5.12、你知道npm run xxx的时候发生了啥吗
下面主要进行总结,细节情况上面阳光大佬的文章
以npm run server为例:
-
启动vue项目一般使用vue-cli-service serve进行启动,但是计算机系统其实并不认识这个命令,只是认识我们全局安装的npm命令等,所以就在package..json里面定义了一个scripts对象进行脚本命令映射
{ "name": "h5", "version": "1.0.7", "private": true, "scripts": { "serve": "vue-cli-service serve" }, }
-
那么为什么酱紫执行就可以启动vue呢,因为当我们npm install的时候会给项目的node_module第三方库依赖在node_modules/.bin/目录中创建好好
vue-cli-service
为名的几个可执行文件了(在不同系统下执行不同可执行文件)。 -
当使用 npm run serve 执行 vue-cli-service serve 时,虽然没有安装 vue-cli-service的全局命令,但是 npm 会到 ./node_modules/.bin 中找到 vue-cli-service 文件作为 脚本来执行,则相当于执行了 ./node_modules/.bin/vue-cli-service serve
-
那么是怎么找到/node_modules/.bin/vue-cli-service这个路径的呢
-
npm install的时候不仅在node_module/.bin文件下生成了可执行文件,还在package-lock.json文件的bin属性中添加了映射
"bin": { 'vue-cli-service': "bin/vue-cli-service.js" }
-
运行 npm run xxx的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;
-
没有找到则从全局的 node_modules/.bin 中查找,npm i -g xxx就是安装到到全局目录;
-
如果全局目录还是没找到,那么就从 path 环境变量中查找有没有其他同名的可执行程序。
5.13、手写系列
①手写new
1、创建一个新对象。
2、让这个新的对象的原型指向该构造函数的原型对象。
3、执行构造函数,并且将构造函数指向新的对象。
4、拿到构造函数最后返回的结果,判断是否是对象或者函数,如果是的话,则直接
返回。如果不是则返回新创建的对象。
function createNew(con) {
let result = Object.create(con.prototype)
let args = [].slice.call(arguments, 1)
let ret = con.apply(result, args)
return ((typeof ret === 'object' && ret !== null) || typeof ret === 'function') ? ret : result
}
function Person(name, age, score) {
this.name = name
this.age= age
this.score = score
return {name:this.name}
}
let rest = createNew(Person, 'dmc', 21, 100)
console.log(rest)
②手写apply
Function.prototype.dyapply = function (thisArgs, argArray) {
// 1.获取到要执行的函数
var fn = this;
// 2.处理绑定的 thisArgs
thisArgs = (thisArgs !== null && thisArgs !== undefined) ? Object(thisArgs) : window;
// 3.执行函数
thisArgs.fn = fn;
argArray = argArray || [];
var result = thisArgs.fn(...argArray);
return result;
delete thisArgs.fn;
};
③手写call
// 给所有函数添加一个 dycall 的方法
Function.prototype.dycall = function (thisArgs, ...args) {
// 1.获取需要被执行的函数
var fn = this;
// 2.将thisArgs转成对象类型(防止传入的是非对象类型),非对象类型不能添加属性
thisArgs = (thisArgs !== null && thisArgs !== undefined) ? Object(thisArgs) : window;
thisArgs.fn = fn;
// 3.调用需要被改变this的函数
var result = thisArgs.fn(...args);
delete thisArgs.fn;
// 4.将调用后的结果返回
return result;
};
④手写bind
Function.prototype.dybind = function (thisArgs, ...argArray) {
// 1.获取到需要调用的函数
var fn = this;
// 2.绑定 this
thisArgs = thisArgs !== null && thisArgs !== undefined ? Object(thisArgs) : window;
function proxyFn(...args) {
// 3.将函数放到 thisArgs中进行调用
thisArgs.fn = fn;
// 特殊:对两个传入的参数进行合并
var finalArgs = [...argArray, ...args];
var result = thisArgs.fn(...finalArgs);
delete thisArgs.fn;
// 返回结果
return result;
}
// 4.返回新的函数
return proxyFn;
};
⑤使用setTimeout模拟setInterval
背景:
- 如果使用setInterval,里面的回调函数中,需要执行比较长的事件,例如setInterval一个1秒钟的时间,然后callback中需要执行3秒,下一个setInterval并不会等待上一个的setInterval的callback执行完毕才执行,这样就有可能出现同一时间触发多次setInterval的callback,然后导致页面的奇怪现象。
- 另外这样也容易造成内存溢出。而使用setTimeout代替setInterval,在setTimeout的callback中,知心完后重新新建一个setTimeout。这样就保证了每一次只会有一个定时任务执行。
function mySetInterval(fn, time = 1000) {
let isClear = false
let timer = null
function interval() {
if (isClear) {
clearTimeout(timer)
return
}
fn()
clearTimeout(timer)
timer = setTimeout(() => {
interval()
}, time);
}
timer = setTimeout(() => {
interval()
}, (time));
return () => {
isClear = true
}
}
5.14、浅浅再复习一下splice和slice
1、splice
返回删除内容,并且原数组改变
①一个参数
删除该参数所在位置以及后面的元素
var a = [1, 2, 3, 4, 5]
a.splice(0) // 或 a.splice(-5)
console.log(a) // []
var b = [1, 2, 3, 4, 5]
b.splice(-2)
console.log(b) // [1, 2, 3]
②两个参数
两个参数必须均为整数。表示从数组中索引为 i
开始删除,一共删除 j
个元素。
var c = [1, 2, 3, 4, 5]
c.splice(0, 2)
console.log(c) // [3, 4, 5]
③三个及其以上参数
splice(i, j, num1, num2, ...)
- i:整数,表示索引的起始位置
- j:整数,表示删除的个数
- num1、num2、...:删除相应元素之后要添加的元素
var d = [1, 2, 3, 4, 5]
d.splice(0, 0, 8, 9)
console.log(d) // [8, 9, 1, 2, 3, 4, 5]
var e = [1, 2, 3, 4, 5]
e.splice(0, 2, 8, 9)
console.log(e) // [8, 9, 3, 4, 5]
var f = [1, 2, 3, 4, 5]
f.splice(-2, 0, 8, 9)
console.log(f) // [1, 2, 3, 8, 9, 4, 5]
注意点:
- 若 j 为 0,则表示一个元素也不删除,则元素从 i 前一个位置开始插入
2、slice方法
arr.slice(i, j), 表示从数组/字符串的 [i, j) 前开后闭进行切片,
- 返回切片内容
- 不影响原数组
注意下面这种情况:
i表示从什么位置向右一直提取到结束,j表示什么位置是终点
var g = [1, 2, 3, 4, 5]
console.log(g.slice(-2, -2)) // []
console.log(g.slice(-1, -2)) // []
console.log(g.slice(4, 3)) // []
当i > j的时候就会返回空数组
var g = [1, 2, 3, 4, 5]
console.log(g.slice(1, -2)) // [2, 3]
console.log(g.slice(-3, -2)) // [3]
console.log(g.slice(-4, -2)) // [2, 3]
所以i到j提取的方向只会是从左到右边的
5.15、Math.floor/ceil/round区别【易混】
1、Math.floor:向下取整,舍去小数
Math.floor(12.1) = 12
Math.floor(12.8) = 12
Math.floor(12.0) = 12
Math.floor(-12.2) = -13
2、Math.ceil: 向上取整,一律向整数部分进位
Math.ceil(12.1) = 12
Math.ceil(12.8) = 12
Math.ceil(12.0) = 12
Math.floor(-12.2) = -13
3、Math.round:+0.5之后,再向下取整
Math.round(1.0) = 1
Math.round(1.4) = 1
Math.round(1.5) = 2
Math.round(1.6) = 2
Math.round(-1.0) = -1
Math.round(-1.4) = -1
Math.round(-1.5) = -1
Math.round(-1.6) = -2
5.16、创建对象有几种方法?
来源:www.cnblogs.com/zzzmj/p/114…
- 字面量创建:var obj = {}
- new操作符创建:new Object()
- Object.create创建
1、Object.create和new Object的区别?
- Object.create返回的变量本身不继承属性和方法,但是返回变量的prototype原型继承了Object.create的参数对象
- new Object 返回的变量本身继承属性和方法
new 关键字做了几件事
- 创建一个新对象
- 将新对象的原型指向构造函数的原型
- 执行构造函数,绑定this
- 返回这个对象
比如创建一个Car对象,伪代码
// new Car()
var obj = new Object()
obj._proto_ = Car.prototype
// 执行构造函数, 绑定this
Car.call(obj)
我们注意到比较关键的地方是,它调用了Car对象的构造函数,并通过call将obj的this绑定到了Car对象上
Object.create做了什么?
它同样是创建一个新对象,将新对象的原型关联到构造函数上
var f = function() { }
f.prototype = Car
return new f()
可以看出,在Object.create的内部,并没有去调用Car构造函数,而是调用了创建新对象的构造函数,因此Car上的属性不会继承到Object.create创建的实例中
let obj = {
name: '阿彬',
age: 10,
foo: function() {
console.log(this.age)
}
}
let a = Object.create(obj)
let b = new Object(obj)
a.name = '彬'
b.name = '彬'
console.log(a.__proto__,a,a.foo());
//{name: '彬', age: 10, foo: ƒ} {name: '彬'} undefined
console.log(b.name,b,b.foo());
//彬 {name: '彬', age: 10, foo: ƒ} 10
2、如何创建一个干净的空对象?
推荐使用Object.create(null),因为我们传入null当做它的参数,所以它创建的对象不会有原型,也不会有Object 原型对象的任何属性(例如toString,hasOwnProperty等),会比直接用 let newObj = {} 创建要更加干净,正是因为没有原型链和Object上面的原型函数
5.17、其他热知识
1、什么是纯函数
- 对于相同的输入,永远都是相同的输出
- 没有其他副作用
- 不依赖于外部环境的状态
2、数组有哪些方法
-
改变原数组的方法:pop 、push、reverse、shift、unshift、sort、splice
-
不改变数组方法:slice、indeOf、lastIndexOf、join、split、toString、concat
-
遍历:
- map、filter、reduce
- every、some
- forEach
- entries、keys、values
3、Object.freeze和Object.seal区别
- 先拓展一个API:Object.preventExtensions,让一个对象变的不可扩展,也就是永远不能再添加新的属性。
// Object.preventExtensions将原对象变的不可扩展,并且返回原对象.
var obj = {};
var obj2 = Object.preventExtensions(obj);
obj === obj2; // true
// 字面量方式定义的对象默认是可扩展的.
var empty = {};
Object.isExtensible(empty) //=== true
// ...但可以改变.
Object.preventExtensions(empty);
Object.isExtensible(empty) //=== false
- Object.seal:在现有对象上先调用Object.preventExtensions并把所有的属性都标记为configurable: false
- Object.freeze: 在现有对象上先调用Object.seal(),再把所有属性的标记为writable: false
4、sort底层机制
V8 引擎 sort 函数只给出了两种排序 插入排序 和 快速排序,数量小于10的数组使用 插入排序,比10大的数组则使用 快速排序
5、作用域和执行上下文的区别
我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
6、你不知道的设计模式
面向对象
原型链继承
构造函数继承
组合继承
寄生继承
工厂模式
通过传入不同短参数达到实例化不同的对象这一目的
抽象工厂模式
就是定制了实例的结构
还要靠自己继然后重载不同实例的方法
状态模式
每一个状态变化都会触发一个逻辑,我们不能总是if...else 来写
所以我们就把状态和当前对象分离开来,比如最常见都红绿灯。红灯状态下是(停下),黄灯状态是(警告),绿灯状态是(通行)。那么我们就可以把这3个状态和方法都抽离出来。提高代码复用,符合开放封闭原则
单例模式
单例模式就是不用重复去构建实例,直接取之前创建过的那个保存在内存中的实例,比如全局的Vuex
代理模式
代理模式就是我们不直接访问原对象,通过访问代理对象访问原对象。比如Object.defineProperty
观察者模式
比如响应式原理、或者是Node里面的事件监听回调
适配器模式
比如你去欧洲工作,他们的插线板你是不能直接使用的,需要在咱们的充电器外面加一个适配器然后就可以使用他们的插线板了,我们最常见的适配器模式就是当一个老接口和新接口同时使用,老接口可以 使用适配器去调用。这样我们的代码就更加容易的解耦。
外观模式
外观模式是最常见的模式了,我们也用的最多,当我们定义了一堆方法,提供统一的出口的。这样方式就是一个外观模式。我们前端和后端进行交互就是一个外观模式,服务器有统一的一个入口。