个人面试题总结

操作系统

用户态和内核态

用户态和内核态是CPU的两种状态,分别代表着两种权限。
当CPU处于用户态时是没有权限执行特权指令的,这是出于安全的考虑。

程序和进程的区别

程序是静态的,进程是动态的,程序是存储在某种介质上的二进制代码,进程对应了程序的执行过程

进程和线程

多进程和多线程的区别

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优

线程有阻塞和唤醒吗

有,得不到cpu就会阻塞,得不到系统资源会导致进程阻塞

为什么线程切换花销小

进程切换时要切页表,而且往往伴随着页调度
统一进程下的线程共享进程里的资源,线程只需要保存线程的上下文(相关寄存器状态和栈的信息)

进程的内存模型

个人面试题总结_第1张图片

线程共享哪些内存空间

堆、动态库、全局变量、打开的文件

线程同步有哪些方法

  1. 互斥量:采用互斥对象机制,只有拥有了互斥对象的线程才有访问资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
  2. 信号量:信号量允许同一时刻多个线程访问同一个资源,但是要控制最大线程数量
  3. 事件:利用通知操作的方式,保持多线程同步,可以方便的实现多线程优先级比较

讲一下各种锁

悲观锁: 每次在拿数据的时候都会上锁(系统互斥量实现)

  • 重量级锁:获取不到锁,马上进入阻塞状态(场景:有实际竞争,且锁竞争时间长)
  • 自旋锁:获取不到锁,循环等一段固定时间,再进入阻塞状态
  • 自适应自旋锁: 根据线程最近获得锁的状态来调整等待时间

乐观锁: 不用加锁,出现冲突再想办法去解决

  • 轻量级锁:进入前用版本号设置标记正在被使用,退出后重置标记(无实际竞争,多个线程交替使用锁;允许短时间的锁竞争)
  • 偏向锁:首次进入时会记录使用的线程ID,退出不更改。
    (偏向锁适用于始终只有一个线程在执行一个方法的情况。)

乐观解决冲突的办法:版本号控制。当数据被修改时,版本号会+1,提交更新时如果自己读到的版本号与数据库中的不同,就要重新提交更新。

堆栈的区别

  1. 栈是自动分配的,速度快,堆需要手动申请回收,速度慢。
  2. 栈的最大容量是固定的,空间小,堆的大小受限于系统中有效虚拟内存,空间大
  3. 栈从高地址向低地址扩展,堆从低地址向高地址扩展
  4. 栈是连续空间,堆是不连续空间
  5. 栈是线程私有的,堆是线程公有的

在多线程环境下编程需要注意哪些情况呢

要避免产生死锁

避免死锁都有哪些方式方法

剥夺死锁的四个必要条件:互斥条件、不剥夺条件、请求和保持条件、循环等待条件
银行家算法
检测和解除

静态库和动态库的区别?

程序由源代码变成可执行文件,一般可以分解为四个步骤,分别是:

  1. 预处理(Prepressing):预处理过程主要处理源代码中以“#”开始的预编译指令
  2. 编译(Compilation):编译过程把预处理完成的文件进行词法、语法、语义等分析并产生相应的汇编代码文件
  3. 汇编(Assembly):汇编过程将汇编代码文件翻译成机器可以执行的目标文件
  4. 链接(Linking):链接过程将汇编生成的目标文件集合相连接并生成最终的可执行文件。

库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。

  1. 静态库会在编译时就与目标文件打包生成可执行文件。而动态库在运行时链接载入。
  2. 静态库移植性好,对外部没有依赖。
  3. 静态库浪费空间,多个程序调用同一个库,则只需要保留一份
  4. 静态库不方便更新

虚拟内存,为什么要有虚拟内存,虚拟内存如何映射到物理内存

每个进程创建加载的时候,会被分配一个连续的虚拟地址空间,在进程眼里这片空间是真实有效的,但是它实际使用的内存空间往往比虚拟内存小,并且是不连续的,进程暂时用不到的数据会被放进磁盘里。

好处:提高了内存利用率

虚拟内存通过页表映射到物理内存,页表是内存中的一种数据结构,用来记录虚拟内存与物理内存的映射关系。

虚拟内存页表中有什么东西

内存块号:某一页号对应的内存块号
状态位:标记页面是否在内存中
访问字段:记录最近被访问过几次,用来给页面置换算法提供参考
修改位:标记页面是否被修改过,如果被修改过则再换出的时候需要更新外存
外存地址:页面在外存中的地址

如果访问内存中的页表不命中需要进行什么操作

会产生缺页中断,由操作系统将页面从外存调入内存,如果内存不足,则通过页面置换算法挑选一个近期不用的页面调出内存

有什么页面置换算法

程序从产生到运行的全过程

程序由源代码变成可执行文件,一般可以分解为四个步骤,分别是:

  1. 预处理(Prepressing):预处理过程主要处理源代码中以“#”开始的预编译指令
  2. 编译(Compilation):编译过程把预处理完成的文件进行词法、语法、语义等分析并产生相应的汇编代码文件
  3. 汇编(Assembly):汇编过程将汇编代码文件翻译成机器可以执行的目标文件
  4. 链接(Linking):链接过程将汇编生成的目标文件集合相连接并生成最终的可执行文件。

计算机网络

怎么理解TCP/IP协议

应用层
传输层
网络层
数据链路层

TCP三次握手的流程,原因

A–>B同步包(SYN=1,seq=x)
A<–B确认+同步包(SYN=1,ACK=1,seq=y,ack=x+1)
A–>B确认包(ACK=1,seq=x+1,ack=y+1)

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。

客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

TCP四次挥手,为什么要有TIME_WAIT状态?为什么要等待2MLS?

A–>B终止(FIN)包
A<–B确认(ACK)包(此时A–>B的通道就释放了)
A<–B终止(FIN)包
A–>B确认(ACK)包(此时A<–B的通道就释放了)

  1. 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
  2. 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文

TCP和UDP有什么区别

UDP
(1)UDP不需要建立连接。
(2)UDP不保证可靠交付,也不使用拥塞控制。
(3)UDP是面向报文的,适合多媒体通信的要求。
(4)UDP支持一对一,一对多,多对一,多对多交互通信。
(5)UDP首部开销小,只有8个字节。

TCP
(1)TCP需要建立连接。(三次握手)
(2)TCP提供可靠交付,使用拥塞控制
(3)TCP面向字节流:把应用程序交付下来的数据看成一连串无结构的字节流。
(4)TCP只支持只一对一。
(5)TCP提供全双工通信。

输入网址进入网页按回车刷新网页都发生了什么

  1. 【用户】输入 URL
  2. 【浏览器】从 URL 中解析出 主机名
  3. 【浏览器】将 主机名 转换成 服务器ip地址(先查找本地DNS缓存列表,没有的话再向默认的DNS服务器发送查询请求并缓存)
  4. 【浏览器】从 URL 中解析出 端口号
  5. 【浏览器】与 目标服务器 建立 TCP连接(三次握手)
  6. 【浏览器】向 服务器 发送一条 HTTP请求报文
  7. 【服务器】向 浏览器 返回一条 HTTP响应报文
  8. 【浏览器】关闭连接(四次挥手)
  9. 【浏览器】解析文档,渲染成页面

渲染页面

  • 解析html生成DOM树
  • 解析css生成css树
  • 将两个树合成,生成渲染树(遍历每个可见节点,让后对每个可见节点找到适配的CSS样式规则并应用)
  • 渲染树布局(遍历渲染树,确认每个元素在页面上的大小和位置)
  • 渲染树绘制 (遍历渲染树,调用渲染器的paint()方法)

HTTP无状态如何记录我们的登录状态

基于Session实现的会话保持
在会话开始时(客户端第一次像服务器发送http请求),服务器将会话状态保存起来(本机内存或数据库中),然后分配一个会话标识(SessionId)给客户端,这个会话标识一般保存在客户端Cookie中,以后每次浏览器发送http请求都会带上Cookie中的SessionId到服务器,服务器拿到会话标识就可以把之前存储在服务器端的状态信息与会话联系起来,实现会话保持(如果遇到浏览器禁用Cookie的情况,则可以通过url重写的方式将会话标识放在url的参数里,也可实现会话保持)

基于Cookie实现的会话保持
基于Cookie实现会话保持与上述基于Session实现会话保持的最主要区别是前者完全将会话状态信息存储在浏览器Cookie中,这样一来每次浏览器发送HTTP请求的时候都会带上状态信息,因此也就可以实现状态保持。

建立一个http连接之后如何让网络变得更快

讲一下http和https的区别

http传输的是明文,https传输密文
http端口号80,https端口号443
https协议需要到CA证书
https = http + SSL

讲一下https连接建立过程

  1. 首先客户端发起请求到服务端,服务端处理后发送一个公钥给客户端
  2. 客户端进行验证公钥,看公钥是否有效和是否过期
  3. 客户端验证通过会产生随机值key,然后用公钥进行加密回传给服务端
  4. 服务端用私钥解密后获得客户端的随机值key
  5. 利用随机值key加密数据后传输给客户端
  6. 客户端利用key值进行解密数据
  7. 客户端获取真正的数据

HTTPS抓包原理

  1. 客户端向服务器发起HTTPS请求,被中间人截获
  2. 中间人伪装成客户端向服务器发起请求
  3. 服务器将证书发给中间人
  4. 中间人获得证书,生成假证书替换掉真正书发给客户端
  5. 客户端生成随机对称密钥,用假证书中的公钥加密后发给服务器,被中间人截获
  6. 中间人用假证书的私钥解密获得对称密钥,用真正公钥加密发给服务器
  7. 服务器用私钥解密获得对称密钥,链接建立,之后发送的数据都用对称密钥加密解密。

http状态码

200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
302 - 临时重定向
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误

分类 分类描述
1** 信息,服务器收到请求
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

http 1.1和2.0的区别

  • 1.1基于文本,2.0基于二进制数据帧
  • 2.0使用了首部压缩,节省了带宽
  • 2.0多路复用的,只需一个连接即可实现并行
  • 2.0有服务端推送,客户端请求资源时,服务器会把相关的资源一起发送过去

一个TCP支持多少个HTTP?了解HTTP复用吗?

TCP支持任意数量http
http1.1之前,每次请求都需要建立连接
http1.1之后,在发送http的请求头中设置Connection: keep-alive,连接会长时间保持。
但是在同一个tcp连接中,新的请求需要等上次请求收到响应后,才能发送。

http和tcp的关系

http建立在tcp的基础上
当你打电话时,TCP协议是电话线的电信号传输协议,而HTTP协议是普通话,如果TCP连接不通,意味着你电话没打通,如果HTTP协议不遵守,就是你打通了不说话,或者说方言。

get与post请求的区别,post的安全性体现在那里,是否可以发现他的请求内容

  1. GET把参数包含在URL中,POST通过request body传递参数
  2. GET请求在URL中传送的参数是有长度限制的,而POST 么有。
  3. GET请求只能进行url编码,而POST支持多种编码方式
  4. 对参数的数据类型, GET只接受部分ASCII字符,而 POST没有限制。
  5. GET请求会被 浏览器主动缓存,而POST需要手动设置。
  6. GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  7. GET比POST更不安全,因为参数 直接暴露在URL上,所以不能用来传递敏感信息。
  8. GET在浏览器回退时是无害的,而POST会再次提交请求。
  9. GET产生的URL地址可以被做成书签,而POST不可以。
  10. GET产生一个TCP数据包;POST可能会产生两个TCP数据包。这个是由客户端决定的, 如果POST内容过多,会先发送头部,再发送参数

介绍一下DNS协议

DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。

  1. 根域名服务器: 知道所有的顶级域名服务器的域名和IP地址。
  2. 顶级域名服务器: 负责管理自己下面注册的所有二级域名服务器
  3. 权限域名服务器: 负责一个区的域名服务器
  4. 本地域名服务器: 当一个主机发出DNS请求时,这个查询请求报文就是发送给了本地域名服务器

DNS解析流程

  1. 主机先向本地域名服务器进行递归查询
  2. 本地域名服务器采用迭代查询,向一个根域名服务器进行查询
  3. 根域名服务器告诉本地域名服务器,下一次应该查询的顶级域名服务器的IP地址
  4. 本地域名服务器向顶级域名服务器进行查询
  5. 顶级域名服务器告诉本地域名服务器,下一步查询权限服务器的IP地址
  6. 本地域名服务器向权限服务器进行查询
  7. 权限服务器告诉本地域名服务器所查询的主机的IP地址
  8. 本地域名服务器最后把查询结果告诉主机

DNS劫持

解析域名的时候,DNS服务器返回了一个错误的地址

  • DDOS攻击:在知道攻击目标的IP地址后,攻击者以这个地址为源地址去向DNS服务器发送请求,然后攻击目标就会收到很多DNS返回的请求
  • 缓存污染:如果将数据放入有漏洞的服务器缓存中,当进行DNS请求的时候,就会将缓存信息返回给用户
  • 中间人攻击:通过截获主机与DNS服务器之间的通信,伪装成DNS服务器向主机返回错误的ip

TCP如何保证可靠传输

TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
ARQ协议:每发完一个分组就停止发送,等待对方确认。超过一段时间仍然没有收到确认就重传。信道利用率低
连续ARQ协议:发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。不能向发送方反映出接收方已经正确收到的所有分组的信息

流量控制(滑动窗口)

TCP 连接的每一方都有固定大小的缓冲空间,发送方和接收方各有一个窗口,窗口的大小限制了数据的传输速率。接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小,从而达到流量控制的目的。

如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离。接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。接收窗口只会对窗口内最后一个按序到达的字节进行确认

TCP协议的流量控制机制,滑窗为0时,怎么办

发送方停止发送数据

滑动窗口与拥塞窗口的区别

滑动窗口是接收方建议的窗口大小,只考虑了接受方的承载力
拥塞窗口是根据网络状况自己调整的窗口大小,考虑的是网络的承载力

拥塞控制

发送方会维护一个拥塞窗口,发生拥塞就变小,否则变大
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
慢开始与拥塞避免:
传输开始的时候,拥塞窗口会被设为1,并且会设置一个慢开始门限
每次收到确认以后,拥塞窗口就会翻倍
当拥塞窗口大于等于慢开始门限的时候,每次确认只会加1
如果出现超时的情况,慢开始门限设为拥塞窗口的一半,拥塞窗口变为1
快重传与快恢复:
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,慢开始门限设为拥塞窗口的一半,拥塞窗口减半

TCP协议切片

https://www.cyc2018.xyz/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%20-%20%E4%BC%A0%E8%BE%93%E5%B1%82.html#tcp-%E9%A6%96%E9%83%A8%E6%A0%BC%E5%BC%8F

如何实现断点续传?

http中Header 里添加两个参数来实现的。这两个参数分别是客户端请求时发送的 Range 和服务器返回信息时返回的 Content-Range

从原理层面解释下为什么我们用不了咕鸽

在咱们国家到国外的防火墙上,设置一个类似白名单的东西,不在白名单的IP地址一律不可以把包发送到国外

C/S模型,socket的建立过程(bind/listen那一套,顺便说了accept和connect是在tcp握手的什么时间返回)

电脑接入局域网,如何分配ip

  • 主机生成 DHCP 请求报以(0.0.0.0)为源地址进行广播
  • DHCP服务器收到广播后,根据MAC地址返回IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码

数据结构与算法

快排如何优化

优化1 采用三数取中法选择基准
取收尾中间元素,取中值作为基准,避免选到最大值最小值。
优化2 序列长度小于一定值后用插入排序,如长度=10
优化3 在一次分割结束后,把与基准值相等的元素聚到一起,不参与下次分割。

哈希冲突如何解决

  1. 开放定址法
  2. 链地址法
  3. 再哈希法

红黑树基本概念

  1. 每个节点或是黑色或是红色
  2. 根节点是黑色
  3. 每个叶节点是黑色(叶节点为空节点)
  4. 如果一个节点是红色,则它的子节点必须是黑色
  5. 每个节点到该节点的所有叶节点的路径包含相同数目的黑色节点

编译原理

函数调用流程

https://zhuanlan.zhihu.com/p/25149493

每一个进程用户态对应一个调用栈结构(call stack)
程序中每一个未完成运行的函数对应一个栈帧(stack frame),栈帧中保存函数局部变量、传递给被调函数的参数等信息
栈底对应高地址,栈顶对应低地址,栈由内存高地址向低地址生长

然后cpu中有一些一些特殊的寄存器

  • ax(accumulator): 可用于存放函数返回值
  • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
  • sp(stack pointer): 用于存放执行中的函数对应的栈帧的栈顶地址
  • ip(instruction pointer): 指向当前执行指令的下一条指令

在调用一个函数时

  1. 传递被调用函数的参数:x86架构的cpu会把参数压入调用栈中,而x86_64架构的cpu会把参数存到通用寄存器中。
  2. 当前函数的下一条指令,即ip寄存器的值入栈,这个是被调函数返回后的地址
  3. 修改ip寄存器的值为被调函数的执行位置
  4. 会首先把当前函数栈帧栈底地址,也就是bp存放的值压入调用栈
  5. 然后建立新的栈帧,将被调用的函数栈帧栈底地址存到bp中,正式进入被调用函数
  6. 将传入的参数存为函数内局部变量
  7. 函数执行完之后最后结果保存在ax寄存器中
  8. 销毁当前栈帧,sp栈顶指针回退回bp栈底,将栈顶元素出栈赋值给bp,退回到原函数的栈帧内。
  9. 将栈顶元素出栈赋值给ip寄存器,指向原函数栈帧中将要执行的下一条指令地址

IDE如何判断代码哪一行报错

面向对象

面向对象三大特征

封装: 把客观的事物封装成抽象的类。类可以设置自身属性和方法的可见性,对外隐藏细节
继承: 可以在现有类的基础上扩展它的功能
多态: 允许将子类类型的指针赋值给父类类型的指针。

继承多态如何实现

https://blog.csdn.net/djl806943371/article/details/88677634

继承和多态的区别是什么

继承是静态绑定,多态是动态绑定。

设计模式

单例模式

懒汉式
volatile的作用

  1. 使变量内存可见,就是说当一个共享变量在一个线程中被修改,其他线程能够立即得到变量的最新值。为什么说不加volatile线程就得不到最新值了呢,因为java的内存模型中,线程有自己的工作内存,与主内存隔离,共享变量存在主内存中,线程使用的仅仅是共享变量的一个副本,所以说会出现更新不及时的情况。
  2. 另外一个作用是防止指令重排序。为了提高性能,编译器和处理器会在不影响单线程结果的情况下对指令做重排序,但是这样会在多线程下引发一些安全问题。
    synchronized的作用是防止多个线程同一时间调用此代码块或者方法.
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){};
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){};
    public static Singleton getInstance() {
        return instance;
    }
}

工厂模式
将对象的创建逻辑封装起来,由一个工厂对象代替创建。

public interface Shape {
    void draw();
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Rectangle");
    }
}

public class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Square");
    }
}

public class ShapeFactory {
    public shape getShape(String shapeName) {
        if (shapeName.equalsIgnoreCase("Rectangle")) {
            return new Rectangle();
        }
        else if (shapeName.equalsIgnoreCase("Square")) {
            return new Square();
        }
        else {
            return null;
        }
    }
}

设计原则

  1. 开闭原则。一个软件实体,应该对外开放扩展,对内修改关闭。开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本
  2. 里氏替换原则。在一个系统中,用子类对象替换其父类对象,系统的执行效果应该不变。子类可以扩展父类的功能,但不能改变父类原有的功能。是实现开闭原则的重要方式之一
  3. 合成复用原则。在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。维持了类的封装性,降低了类之间的耦合度,是实现开闭原则的重要方式之一
  4. 单一职责原则。一个类或者一个函数只允许有一个职责,即只有一个导致该类变更的原因。如果类与方法的职责划分的很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让bug无处藏身,也有利于bug的追踪,也就是降低了程序的维护成本。
  5. 接口分离原则。多个特定的客户端接口要好于一个通用性的总接口。客户端不应该依赖它不需要实现的接口。这样可以使接口责任划分更加明确,实现高内聚低耦合
  6. 依赖倒置原则。细节应该依赖抽象。高层模块不能依赖低层模块,二者都应该依赖抽象。尽量不要从实体类中继承,这样可以降低类之间的耦合度,提高系统的可扩展性以及可维护性
  7. 迪米特法则。一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。降低类与类之间的耦合

C++

指针和引用的区别

  1. 指针是一个存储地址实体,而引用仅是个别名
  2. 引用初始化后不能被改变,指针可以改变所指的对象
  3. 引用不能为空,指针可以为空。

const

  1. const修饰变量,使变量的值不可变,变量必须初始化

  2. const修饰指针,左定值,右定向

    int a = 8;
    const int * const  p = &a;
    
  3. const修饰参数,既可使指针不篡改,也可const &不调用复制构造

  4. const修饰返回值,不能被赋值和修改

  5. const修饰类成员函数,防止成员函数修改被调用对象的值

C++继承和多态是如何实现的

继承 通过访问限定符:public,protected,private来实现
多态 通过虚函数的重写以及父类的指针和引用指向子类的对象

C++如何判断机器是64还是32

32位指针占4字节,64位指针占8字节
sizeof(int *)
或者

int main(void)
{ 
    void *a, *b;
    cout << (char *)(&a) - (char *)(&b) << endl;

    return 0;
}

C++对象的内存分布是怎样的

  1. 非静态成员变量放在对象体内
  2. 非静态成员函数放在程序的静态数据区内
  3. 静态成员放在程序的静态数据区内

new一个对象都发生了什么?

  1. 分配内存空间
  2. 初始化成员变量
  3. 调用构造方法

C++的构造函数、析构函数、复制构造函数、符号重载

https://blog.csdn.net/weixin_50168448/article/details/113613371

string类赋值运算符是深拷贝还是浅拷贝

深拷贝

C++纯虚函数

基类中不能实现,相当于接口。

虚函数、虚表、虚指针

虚函数主要是用来实现多态的。子类重写了父类的虚函数,用父类指针接受子类对象,调用该函数,用的是子类的函数。
当一个类有虚函数时,成员中就会有一个虚指针,虚指针指向虚表,虚表中保存着虚函数的地址。继承多个类会有多个虚指针,也会有多个虚表。当子类重写父类虚函数时,重写函数的地址会覆盖虚表中原来父类虚函数的地址。
当调用对象的虚函数时,会通过查表来运行。

虚函数能不能inline

不能,虚函数是运行时才能确定调用哪个,而inline是在编译期对函数进行展开

右值引用是什么,移动构造函数有什么好处

C++thread里面的锁,条件变量,讲一下怎么用他们实现生产者消费者模型

static关键字

  1. 函数内static局部变量:变量在程序初始化时被分配,直到程序退出前才被释放

怎么判断大小端;

怎么在main函数之前和之后执行代码

main之前的工作

  • 配置堆栈
  • 初始化静态和全局变量
  • 为未初始化部分的全局变量赋值
  • 运行全局构造器
    所以,可以在全局变量的构造器内执行代码
    之后用atexit(func)

#define与inline的区别

#define 只进行简单的字符替换,无类型检测
typedef:定义类型别名 用于处理复杂类型
inline: 内联函数对编译器提出建议,是否进行宏替换,编译器有权拒绝

字节对齐

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2,4或8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间的接口的硬件设计

智能指针

智能指针是由类来实现的,当超出了类的实例对象的作用域时,对象的析构函数会自动调用。通过类的析构函数来自动销毁指针管理的内存。
c++11 的智能指针包括unique_ptr,shared_ptr, weak_ptr, 这三种,其中auto_ptr 已被遗弃。
unique_ptr保证同一时间内只有一个智能指针可以指向该对象
shared_ptr使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。
weak_ptr在指向一个对象的时候不会增加其引用计数

shared_ptr循环引用问题


class Person {
public:
    Person(const string& name): m_name{name} {
        cout << m_name << " created" << endl;
    }

    virtual ~Person(){
        cout << m_name << " destoryed" << endl;
    }

    friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2){
        if (!p1 || !p2){
            return false;
        }

        p1->m_partner = p2; 
        p2->m_partner = p1;

        cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
        return true;
    }

private:
    string m_name;
    std::shared_ptr<Person> m_partner;
};

int main() {
    auto p1 = std::make_shared<Person>("Lucy");
    auto p2 = std::make_shared<Person>("Ricky");
    partnerUp(p1, p2);  // 互相设为伙伴

    return 0;
}

以上的程序输出为:

Lucy created
Ricky created
Lucy is now partnered with Ricky

p1和p2都没有释放,发生了内存泄漏。
解决方案:将类内指针shared_ptr换为weak_ptr,程序输出将是

Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed

其他

git版本控制的原理

工作区、暂存区、库

git为什么要有暂存区

  1. 修改进入暂存区就进入git管理范围,修改就不会丢失了,而且方便回退
  2. 保持日志的干净,暂存区可以用来保存一些还没有完成的任务的修改,这些修改如果马上进入仓库的话会导致一个任务有多条commit记录。

JS 和 Swift 的区别

  1. Swift 为 强类型、静态类型;JavaScript 为 弱类型、动态类型

mvc,mvp,mvvm的区别

MVC
Model: 用于封装数据以及对数据的处理方法
View: 渲染页面
Controller: 连接M和V,用于控制应用程序的流程,及页面的业务逻辑

  1. View 传送指令到 Controller ;
  2. Controller 完成业务逻辑后,要求 Model 改变状态 ;
  3. Model 将新的数据发送到 View,用户得到反馈。
    个人面试题总结_第2张图片

MVP
MVP(Model-View-Presenter)是MVC的改良模式

  • M、V、P之间双向通信。
  • View 与 Model 不通信,都通过 Presenter 传递。
    安卓用的就是MVP
    个人面试题总结_第3张图片
    MVVM
    与MVP不同的是,V和VM是双向绑定的。

个人面试题总结_第4张图片

心理测试

自我评价

人生理想

职业规划

平时怎么学习

缺点优点

智力题

4位数乘以4后会得到它的反转,这个数是啥

设四位数ABCD.
ABCD × 4 = DCBA
显然,由积的个位看出,A是偶数,至少为2,又由积的千位看出,D≤9,推得A = 2
研究乘数 与 乘积的个位
D × 4 = …A = …2
推得D = 8

2BC8 × 4 = 8CB2的性质 ,列方程得:
(2008 + 100B + 10C)×4 = 8002 + 100C + 10B
化简整理得:
1+13B = 2C
结合B、C的范围和奇偶性可知,B 必 = 1,C = 7
综上解得ABCD = 2178

5位数21978
6位数219978
7位数2199978

甲乙轮流抛硬币,正面胜,先抛的人优势多大?

设甲先抛。设甲胜率为x。当且仅当甲第一次抛出反面时乙才有获胜的机会,而此时乙又变成先抛,所以乙的胜率应该是0.5x,因x + 0.5x = 1, x = 2 3 x = {2\over3} x=32

六十四匹马,八个跑道,找出最快的四匹马

详解

有 n 个灯泡,编号1…n,初始全灭,然后1的倍数的灯泡切换一次状态(亮 / 暗),2的倍数的灯泡切换一次状态,…n的倍数的灯泡切换一次状态。问最后有多少个灯泡亮着?

(1)依题意,灯泡按过的次数等于其编号的所有因数的个数;
(2)开始状态是熄的,后来是亮的,说明按过的次数是奇数;
(3)所有因数的个数为奇数的自然数只有完全平方数。
综上,编号是完全平方数的灯泡最后是亮的。

前端

css画三角形

div {
    height: 0px;
    width: 0px;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 50px solid blue;
    background-color: white;
}

css垂直居中

https://www.jianshu.com/p/7bbc4860a45c

css position属性值有哪些

  • static:默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。
  • absolute:以最近的已定位父元素为参考系,脱离文档流
  • fixed:相对于浏览器窗口进行定位,脱离文档流
  • relative:存在于文档流,占用空间不变,相对于其正常位置进行定位
  • sticky:在文档流中占据位置,但是当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置

以下代码输出

问题

var func1 = x => x
var func2 = x => { x }
var func3 = x => ({ x })
console.log(func1(1))
console.log(func2(1))
console.log(func3(1))

答案

1
undefined
{ x: 1 }

题目

Function.prototype.a = 'Function';
Object.prototype.a = 'Object';

function Person() {};
var child = new Person();
console.log(Person.a);
console.log(child.a);
console.log(child.__proto__.__proto__.constructor.constructor.constructor);

答案

Function
Object
[Function: Function]

个人面试题总结_第5张图片

if ([] == false) { console.log(1); };
if (![] == false) { console.log(2); };
if ({} == false) { console.log(3); };
if ([]) { console.log(4); };
if ([1] == [1]) { console.log(5); };
if (null == 0) { console.log(6); };

答案

1
2
4

解析
当判断中,发现有一边不是原始值类型,就会先调用valueOf方法进行转换
发现valueOf转化完后,依然不时原始值类型,那继续用toString方法转换,如果类型不相等就用Number转换

题目

const async1 = async() => {

    console.log('第一个async函数开始');

    await async2();

    console.log('第一个async函数结束');

}

const async2 = async() => {

    console.log('第二个async函数执行');

}

console.log('开始执行');

setTimeout(() => {

    console.log('setTimeout执行');

}, 0)

new Promise(resolve => {

    console.log('promise执行');

    for (var i = 0; i < 100; i++) {

        i == 99 && resolve();

    }

}).then(() => {

    console.log('执行then函数')

});

async1();

console.log('结束');

答案

开始执行
promise执行
第一个async函数开始
第二个async函数执行
结束
执行then函数
第一个async函数结束
setTimeout执行

题目

class Animal {
    sayName = () => {
        throw new Error('你应该自己实现这个方法');
    }
}
class Monkey extends Animal {
    sayName() {
        console.log('I love coding');
    }
}
const monkey = new Monkey();
monkey.sayName();

答案

Error: 你应该自己实现这个方法

解析
class 对于 = 号声明的方法、变量,都会将其作为实例的属性,而对于非 = 号声明的属性,则是放在原型链上。
子类使用普通函数的方式声明 sayName 的话,子类声明的 sayName 会被放在构造函数的 prototype 上。可是由于基类的 sayName 是使用箭头函数的方式,因此每一个实例都会直接有一个 sayName 变量。根据 javascript 变量的访问规则,首先会在变量本身上找,找不到后才会在原型链上找。因此,在查找 sayName 的时候,就直接找到基类声明的 sayName 函数了,就不会再在原型链上找,因此就出现了问题。
解决方法,子类使用等号赋值

题目

var length = 10;

function fn() {
    console.log(this);
    return this.length + 1;
}
var obj = {
    length: 5,
    test1: function() {
        return fn();
    }
};
obj.test2 = fn;
//下面代码输出是什么
console.log(obj.test1())
console.log(fn() === obj.test2())

答案

Window 
11
Window  
{ length: 5, test1: ƒ, test2: ƒ }
false

题目

function repeat(func, times, wait) {
    //实现这个函数
}
// 使下面调用代码能正常工作
const repeatFunc = repeat(console.log, 4, 1000);
repeatFunc("hellworld"); //会输出4次 helloworld, 每次间隔3秒

答案

function repeat(func, times, wait) {
    return (s) => {
        let cnt = 0
        let id = setInterval(() => {
            if (++cnt === times) {
                clearInterval(id)
            }
            func(s)
        }, wait)
    }
}

题目
用reduce实现map
答案

var arr = [1, 2, 3, 4, 5, 6, 7, 8]

function plusTen(num) {
    return num * 10
}

console.log(arr.map(plusTen))

Array.prototype.myMap = function(fn, thisArg) {
    return this.reduce((total, value, index, arr) => {
        return total.concat([fn.call(thisArg, value, index, arr)])
    }, [])
}

console.log(arr.myMap(plusTen))

题目

// 实现一个链式调用的串行Queue类
new Queue()
    .task(1000, () => {
        console.log(1)
    })
    .task(2000, () => {
        console.log(2)
    })
    .task(1000, () => {
        console.log(3)
    })
    .start() //调用start后才可以开始

答案
解法一

class Queue {
    constructor() {
        this.taskList = []
    }

    task(time, fn) {
        this.taskList.push(() => new Promise((res) => {
            setTimeout(() => {
                fn()
                res()
            }, time)
        }))
        return this
    }

    async start() {
        for (let v of this.taskList) {
            await v()
        }
    }
}

解法二

class Queue {
    constructor() {
        this.taskList = []
    }

    task(time, fn) {
        this.taskList.push(() => new Promise((res) => {
            setTimeout(() => {
                fn()
                res()
            }, time)
        }))
        return this
    }

    start() {
        let res = Promise.resolve()
        this.taskList.forEach((v) => {
            res = res.then(v)
        })
    }
}

解法三

class Queue {
    constructor() {
        this.taskList = []
    }

    task(time, fn) {
        this.taskList.push(() => new Promise((res) => {
            setTimeout(() => {
                fn()
                res()
            }, time)
        }))
        return this
    }

    start() {
        this.taskList.reduce((pre, cur) => {
            return pre.then(cur)
        }, Promise.resolve())
    }
}

题目

//实现一个TaskQueue类,实现以下功能
new TaskQueue().console("hello").settimeout(3000).console("world").settimeout(3000).console()

答案

class TaskQueue {
    constructor() {
        this.taskList = []
        this.startTimer = null
    }

    start() {
        clearTimeout(this.startTimer)
        this.startTimer = setTimeout(async() => {
            for (let v of this.taskList) {
                await v()
            }
        }, 0)
    }

    console(v) {
        this.taskList.push(() => {
            console.log(v)
        })

        this.start()

        return this
    }

    settimeout(delay) {
        this.taskList.push(() => {
            return new Promise(resolve => {
                setTimeout(resolve, delay)
            })
        })

        this.start()

        return this
    }
}

题目

//实现一个chain函数,eat函数打印eat,work打印work,sleep函数休息
chain().eat().sleep(5).work().eat().work().sleep(10);

答案

function chain() {
    this.taskList = [];
    this.eat = function() {
        this.taskList.push(() => {
            console.log("eat");
        });
        return this;
    };

    this.work = function() {
        this.taskList.push(() => {
            console.log("work");
        });
        return this;
    };

    this.sleep = function(time) {
        this.taskList.push(
            () =>
            new Promise(res => {
                setTimeout(res, time);
            })
        );
        return this;
    };

    //自执行函数:setTimeout里加上执行函数(还是鹅厂那三种形式)
    //这里我只写了第一种
    setTimeout(async() => { //注意:这里必须为箭头函数,不然下面的this会变就取不到正确的taskList
        for (let v of this.taskList) {
            await v();
        }
    }, 0);

    return this; //实现函数不是类,类会自动return,函数需要手动
}

chain().eat().sleep(2000).work().eat().work().sleep(1000);

题目

class Scheduler{
    constructor(){
	
    }
    add(promiseCreator){
        //code
    }
    //...
}
const timeout = time =>
  new Promise(resolve => {
    setTimeout(resolve, time);
  });

const scheduler = new Scheduler();

const addTask = (time, order) => {
  scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.start();
// output:2 3 1 4

答案

class Scheduler {
    constructor() {
        this.taskList = []
        this.maxNumber = 2
        this.runNumber = 0
    }

    add(promiseCreator) {
        this.taskList.push(promiseCreator)
    }

    start() {
        while (this.taskList.length > 0 && this.runNumber < this.maxNumber) {
            ++this.runNumber
            this.taskList.shift()().then(() => {
                --this.runNumber
                this.start()
            })
        }
    }
}
const timeout = time =>
    new Promise(resolve => {
        setTimeout(resolve, time);
    });

const scheduler = new Scheduler();

const addTask = (time, order) => {
    scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.start();
// output:2 3 1 4

题目
只修改start函数,使程序输出012345

function start(id){
    //execute(id)
}
for (let i = 0; i < 5; i++) {
    start(i);
}
function sleep() {
    const duration = Math.floor(Math.random() * 500);
    return new Promise(resolve => setTimeout(resolve, duration));
}
function execute(id) {
    return sleep().then(() => {
        console.log("id", id);
	});
}   

答案

function start(id) {
    this.p = this.p ?
        this.p.then(() => execute(id)) :
        execute(id);
}

题目
实现数组扁平化

function flatten(arr) {
    //code here
}

var arr = [1, [2, 3, [4]], 5, 6, [
    [7], 8
], 9]


console.log(flatten(arr))

答案

function flatten(arr) {
    let res = []
    for (let v of arr) {
        if (Array.isArray(v)) {
            res.push(...flatten(v))
        } else {
            res.push(v)
        }
    }

    return res
}

reduce实现

function flatten(arr) {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, [])
}

栈实现

function flatten(arr) {
    let res = []
    let stack = [].concat(arr)

    while (stack.length > 0) {
        let v = stack.pop()

        if (Array.isArray(v)) {
            stack.push(...v)
        } else {
            res.unshift(v)
        }
    }

    return res
}

原地算法

function flatten(arr) {
    let len = 0

    while (arr.length != len) {
        let v = arr.pop()

        if (Array.isArray(v)) {
            arr.push(...v)
        } else {
            arr.unshift(v)
            len++
        }
    }
}

题目

对象深拷贝

答案
方法一:

var obj1 = {a:13, b:'good', c: function(){console.log(hello)}}
var obj2 = {...obj1}
console.log(obj1 === obj2)

方法二:

var obj1 = {a:13, b:'good', c: function(){console.log(hello)}}
var obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj1 === obj2)

方法三:

function deepCopy(obj) {
    var res

    if (Array.isArray(obj)) {
        res = []
        obj.map(item => {
            res.push(deepCopy(item))
        })
    } else if (typeof obj === 'Object') {
        res = {}
        Object.keys(obj).map(item => {
            res[item] = deepCopy(obj[item])
        })
    } else {
        res = obj
    }

    return res
}

var a = ['1', 2, { good: 'we', dd: () => {} },
    [1, 2, 3]
]

console.log(a);
var b = deepCopy(a)
var c = a
console.log(b);
console.log(b === a);
console.log(c === a);

题目

实现以下功能
[['a', 'b'], ['n', 'm'], ['0', '1']] => ["an0", "an1", "am0", "am1", "bn0", "bn1", "bm0", "bm1"]

答案

const arr = [
    ['a', 'b'],
    ['n', 'm'],
    ['0', '1']
]

console.log(arr.reduce((pre, cur) => {
    let res = []
    for (let p of pre) {
        for (let c of cur) {
            res.push(p + c)
        }
    }
    return res
}, ['']))

题目

手写Array.prototype.sort()函数

答案

Array.prototype.mysort = function(cmp) {
    if (cmp === undefined) {
        cmp = (a, b) => {
            return a <= b
        }
    }

    const _sort = (l, r) => {
        if (l >= r) {
            return
        }

        let i = l
        let j = r

        while (i < j) {
            while (i < j && cmp(this[l], this[j])) {
                --j
            }
            while (i < j && cmp(this[i], this[l])) {
                ++i
            }
            let temp = this[i]
            this[i] = this[j]
            this[j] = temp
        }
        let temp = this[l]
        this[l] = this[i]
        this[i] = temp

        _sort(l, i - 1)
        _sort(i + 1, r)
    }

    _sort(0, this.length - 1)

    return this
}

let a = [3, 4, 1, 8, 3, 4, 6]
console.log(a.mysort((a, b) => {
    return a >= b
}))

题目

对版本号进行排序
const arr=[
    '1.1',
    '2.3.3',
    '4.3.5',
    '0.3.1',
    '0.302.1',
    '4.20.0',
    '4.3.5.1',
    '1.2.3.4.5'
];

答案

const arr = [
    '1.1',
    '2.3.3',
    '4.3.5',
    '0.3.1',
    '0.302.1',
    '4.20.0',
    '4.3.5.1',
    '1.2.3.4.5'
];

arr.sort((a, b) => {
    a = a.split('.')
    b = b.split('.')

    let n = Math.min(a.length, b.length)

    for (let i = 0; i < n; ++i) {
        if (a[i] != b[i]) {
            return Number(a[i]) - Number(b[i])
        }
    }

    return a.length - b.length
})

console.log(arr);

题目

实现数组的负值索引

答案

function myArray(arr) {
    return new Proxy(arr, {
        get(target, key) {
            return Reflect.get(target, Number(key) < 0 ? String(target.length + Number(key)) : key)
        },
        set(target, key, value, receiver) {
            return Reflect.set(target, Number(key) < 0 ? String(target.length + Number(key)) : key, value, receiver)
        }
    })
}

let arr = myArray([1, 2, 3, 4])
console.log(arr[-1]);
console.log(arr[-2]);
console.log(arr[-3]);
console.log(arr[-4]);
console.log(arr[1]);
arr[-4] = 9
console.log(arr[0]);

题目

将一个典型回调风格的功能函数变为promise风格的

答案

js如何判断对象为空

https://blog.csdn.net/watercatmiao/article/details/84261015

js判断对象是否循环引用

const isCycleObject = (obj, parent) => {
    const parentArr = parent || [obj];
    for (let i in obj) {
        if (typeof obj[i] === 'object') {
            for (let j in parentArr) {
                if (parentArr[j] === obj[i]) {
                    return true
                }
            }
            if (isCycleObject(obj[i], [...parentArr, obj[i]])) return true;
        }
    }
    return false;
}

var a = {
    b: null,
    c: null
}
a.b = a

console.log(isCycleObject(a))

js let和var

1.使用var声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象;
2.使用let声明的变量,其作用域为该语句所在的代码块内,不存在变量提升;
3.let不允许在相同作用域内,重复声明同一个变量。

函数及变量的声明都将被提升到函数的最顶部。
JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。

bind()和call/apply()方法的区别

https://blog.csdn.net/u010176097/article/details/80348447
这些方法的作用都是使一个对象能够以自己的的名义去调用它不拥有的方法。
call和apply 的功能完全相同,区别在于传参的方式不一样
call中的参数是以参数列表的形式传入的,apply中是以数组的形式传入的
bind 和call/apply有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是bind 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

箭头函数与普通函数的区别

  1. 箭头函数都是匿名函数
  2. 箭头函数不能用于构造函数,不能使用new
  3. 箭头函数不绑定arguments,取而代之用rest参数…解决
  4. 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向。普通函数的this指向调用它的那个对象

js数据类型

number string null undefined boolean object symbol

0.1 + 0.2 为什么不等于0.3?

因为浮点数在计算机中是二进制存储的,十进制小数转为二进制是不断乘二取整数,像0.1这种数乘2是乘不尽的,在保存的时候就会有精度损失。
解决办法就是都乘1000变成整数,再除1000

js闭包的作用

一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。
直观的说就是形成一个不销毁的栈环境

function outerFun() {
    var cnt = 0

    return () => {
        console.log(++cnt)
    }
}

const addOne = outerFun()

addOne()
addOne()
addOne()

函数科里化currying

函数科里化就是把多参数的函数分解为

function sum(a, b, c) {
    console.log(a + b + c)
}

function curry(fn, currArgs) {
    return function() {
        let args = [].slice.call(arguments);

        // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }

        // 递归调用
        if (args.length < fn.length) {
            return curry(fn, args);
        }

        // 递归出口
        return fn.apply(null, args);
    }
}

const fn = curry(sum)

fn(1, 2, 3)
fn(1, 2)(3)
fn(1)(2, 3)
fn(1)(2)(3)

函数节流和函数防抖

防抖(debounce)和节流(throttle)都是用来控制某个函数在一定时间内执行多少次的技巧,两者相似而又不同。 背后的基本思想是某些代码不可以在没有间断的情况下连续重复执行,避免产生高额的开销。

函数防抖: 如果一个事件被频繁触发多次,并且触发的时间间隔过短,则防抖函数可以使得对应的事件处理函数只执行最后触发的一次。 函数防抖可以把多个顺序的调用合并成一次。

函数节流: 如果一个事件被频繁触发多次,节流函数可以按照固定频率去执行对应的事件处理方法。 函数节流保证一个事件一定时间内只执行一次。相当于函数有冷却时间。

类型 场景
函数防抖 1. 手机号、邮箱输入检测
2. 搜索框搜索输入(只需最后一次输入完后,再放松Ajax请求)
3. 窗口大小resize(只需窗口调整完成后,计算窗口大小,防止重复渲染)
4.滚动事件scroll(只需执行触发的最后一次滚动事件的处理程序)
5. 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,(停止输入后)验证一次就好
函数节流 1. DOM元素的拖拽功能实现(mousemove)
2. 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
3. 计算鼠标移动的距离(mousemove)
4. 搜索联想(keyup)
5. 滚动事件scroll,(只要页面滚动就会间隔一段时间判断一次)

具体实现

//函数防抖
function debounce(fn, delay, scope) {
    let timer = null

    return function() {
        let caller = scope || this

        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(caller, ...arguments)
        }, delay)
    }
}
//函数节流
function throttle(fn, delay, scope) {
    let pre = 0

    return function() {
        let caller = scope || this
        let now = Date.now()

        if (now - pre > delay) {
            pre = now
            fn.call(caller, ...arguments)
        }
    }
}

试验具体效果


<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Documenttitle>
head>

<body>
    <div class="wrap">
        <div class="header">滚动事件:普通div>
        <div class="container">
            <div class="content">div>
        div>
    div>
    <div class="wrap">
        <div class="header">滚动事件:<strong>加了函数防抖strong>div>
        <div class="container">
            <div class="content">div>
        div>
    div>
    <div class="wrap">
        <div class="header">滚动事件:<strong>加了函数节流strong>div>
        <div class="container">
            <div class="content">div>
        div>
    div>
body>

html>

<script>
    function debounce(fn, delay, scope) {
        let timer = null

        return function() {
            let caller = scope || this

            clearTimeout(timer)
            timer = setTimeout(() => {
                fn.call(caller, ...arguments)
            }, delay)
        }
    }

    function throttle(fn, delay, scope) {
        let pre = 0

        return function() {
            let caller = scope || this
            let now = Date.now()

            if (now - pre > delay) {
                pre = now
                fn.call(caller, ...arguments)
            }
        }
    }

    let els = document.getElementsByClassName('container');
    let count1 = 0,
        count2 = 0,
        count3 = 0;
    const THRESHOLD = 200;

    els[0].addEventListener('scroll', function() {
        console.log('普通滚动事件!count1=', ++count1);
    });
    els[1].addEventListener('scroll', debounce(function() {
        console.log('执行滚动事件!(函数防抖) count2=', ++count2);
    }, THRESHOLD));
    els[2].addEventListener('scroll', throttle(function() {
        console.log(Date.now(), ', 执行滚动事件!(函数节流) count3=', ++count3);
    }, THRESHOLD));
script>

<style>
    .wrap {
        width: 200px;
        height: 330px;
        margin: 50px;
        margin-top: 200px;
        float: left;
        background-color: yellow;
    }
    
    .header {
        width: 100%;
        height: 30px;
        background-color: #a8d4f4;
        text-align: center;
        line-height: 30px;
    }
    
    .container {
        background-color: pink;
        box-sizing: content-box;
        width: 200px;
        height: 300px;
        overflow: scroll;
    }
    
    .content {
        width: 140px;
        height: 800px;
        margin: auto;
        background-color: #14ffb2;
    }
style>

js 原型和原型链

原型其实就是一个对象,每一个构造函数都有一个原型,通过构造函数创建的实例对象也可以访问这个原型。当调用一个对象的属性的时候,会先从这个对象自身去找,如果找不到就会从它的原型里找。如果在原型里没找到,因为原型也是对象,也有自己的原型,会从原型的原型里找,这就会形成一条链,这就是原型链。js的继承就是通过原型链来实现的,一个对象的原型就是它的父类对象。
个人面试题总结_第6张图片

宏任务和微任务

宏任务和微任务都是异步任务,js是一个单线程的语言,它要实现异步处理就得依赖于事件循环,然后事件循环是通过任务队列来实现的,任务分为宏任务和微任务,分别用宏任务队列和微任务队列来接收。一次事件循环会先从宏任务队列中取出一个任务执行,然后将微任务队列中的所有任务执行,最后执行页面的渲染。
宏任务就是比较粗粒度的任务,它们之间可以穿插渲染操作,而微任务粒度细,讲求效率,会在宏任务之后全执行完。

宏任务包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

微任务包括: Promises, Object.observe, MutationObserver

setTimeout(fn, 0) 的作用

https://zhuanlan.zhihu.com/p/26962590
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到主线程把同步任务和"任务队列"现有的事件都处理完,才会得到执行。

在某种程度上,我们可以利用setTimeout(fn,0)的特性,修正浏览器的任务顺序。
GUI 渲染线程:负责渲染浏览器界面 HTML 元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在 Javascript 引擎运行脚本期间, GUI 渲染线程都是处于挂起状态的,也就是说被”冻结”。即 GUI 渲染线程与 JS 引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

浏览器的内核是多线程的
javascript 引擎线程: 也可以称为 JS 内核,主要负责处理 Javascript 脚本程序,例如 V8 引擎。Javascript 引擎线程理所当然是负责解析 Javascript 脚本,运行代码。浏览器无论什么时候都只有一个 JS 线程在运行 JS 程序。

浏览器事件触发线程: 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于JS的单线程关系所有这些事件都得排队等待 JS 引擎处理。

定时触发器线程: 浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 javaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

异步 http 请求线程: XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

js为啥是单线程

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

typeof([])是个object typeof(![])就成了Boolean

alert([])是个空 alert(![]) 是个false javascript中一切空或者0在做比较的时候都会转化成boolean值false所以 答案很明显了 false equals false

执行类型转换的规则如下:
如果一个运算数是 Boolean 值,在检查相等性之前,把它转换成数字值。false 转换成 0,true 为 1。
如果一个运算数是字符串,另一个是数字,在检查相等性之前,要尝试把字符串转换成数字。
如果一个运算数是对象,另一个是字符串,在检查相等性之前,要尝试把对象转换成字符串。
如果一个运算数是对象,另一个是数字,在检查相等性之前,要尝试把对象转换成数字。
在比较时,该运算符还遵守下列规则:
值 null 和 undefined 相等。
在检查相等性时,不能把 null 和 undefined 转换成其他值。
如果某个运算数是 NaN,等号将返回 false,非等号将返回 true。
如果两个运算数都是对象,那么比较的是它们的引用值。如果两个运算数指向同一对象,那么等号返回 true,否则两个运算数不等。

如何准确判断一个变量是数组类型

var a = [1, 23]
a instanceof Array

发送ajax请求的方法

XHR
fetch: 原生, 低级浏览器不支持

js promise用法

https://juejin.cn/post/6868898917950423054

手写promise all和race

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    }, 1000)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
            // reject('failed')
    }, 500)
})

function race(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(v => {
            v.then(res => { resolve(res) }).catch(err => { reject(err) })
        });
    })
}

function all(promiseArr) {
    var resArr = new Array(promiseArr.length)
    var count = 0

    return new Promise((resolve, reject) => {
        promiseArr.forEach((v, i) => {
            v.then(res => {
                resArr[i] = res
                    ++count

                if (count === resArr.length) {
                    resolve(resArr)
                }
            }).catch(err => {
                reject(err)
            })
        })
    })
}

// race([p1, p2]).then((res) => { console.log(res); }).catch(err => { console.log(err); })
all([p1, p2]).then((res) => { console.log(res); }).catch(err => { console.log(err); })

cookie,session和localStorage,sessionStorage

http是无状态的,要记录客户端的身份信息需要借助一定的数据结构。
cookie是把信息存放在浏览器中,以后每次发送请求都会把cookie放在请求头里。cookie比较方便,但是也有很多缺点:

  1. 并不是所有的信息都需要返回,这样会浪费带宽。
  2. cookie最多只有4kb,容量太小。
  3. 隐私数据不应该放进浏览器中。
  4. cookie数据有路径(path)的概念,可以限制cookie只属于某个路径下。
  5. cookie是可以禁用的,有些浏览器甚至不支持cookie

session是把信息存到服务器中,给浏览器一个sessionid把它存到cookie里,每次发送请求会把id带上,服务器就可以知道用户信息了。
session的缺点是占用服务器内存,如果在线人数多的话,服务器压力会比较大

localStorage和sessionStorage都是存放到浏览器里的,它们不会随着请求自动发送,而且存储空间比较大。
localStorage用来保存永久的数据,只要不主动删除就会一直存在。在所有同源窗口中都是共享的
sessionStorage的生命周期在窗口会话内,关闭窗口就会销毁。不在不同的浏览器窗口中共享,即使是同一个页面

同源策略

同源就是协议+域名+端口都相同,同源策略是浏览器的一种安全机制,目的就是防止一个站点随意使用另一个站点的资源。
为了方便,有一些标签是跨域的,比如script, link,img

跨域有哪些方法

script是支持跨域的,可以让服务器动态生成script,然后用前端用script引用,参数加载url后面。类似于get
后端可以通过加响应头Access-Control-Allow-Origin来信任前端站点。

CORS

CDN的原理

CDN(Content Delivery Network)即内容分发网络,它的作用是在用户和服务器之间增加一层缓存,来达到加速的目的。它实现的原理是接管DNS,将目标服务器的ip地址转换为为离用户最近的缓存服务器的ip地址。
CDN网络一般是由一个DNS服务器和几台缓存服务器运行起来的。

  1. 用户请求源服务器的资源
  2. 浏览器解析源服务器的域名,得到该域名的CNME域名
  3. 浏览器对CNAME域名进行解析
  4. 这次解析使用CDN的DNS服务器,计算出离用户距离最近的缓存服务器ip返回给浏览器
  5. 用户对缓存服务器进行访问,获得资源

浏览器缓存

浏览器是有缓存机制的,每次请求的时候,会先检查本地的缓存,如果找到有效的缓存就直接返回。

强缓存: 有效性只跟时间有关。
HTTP/1.0 响应头Expires 配合缓存中Last-modified
HTTP/1.1 响应头Cache-Control

协商缓存: 当强缓存失效后,就会转为协商缓存。需要与服务器交互,通过协商缓存响应头检查本地缓存与服务器资源是否一致。协商缓存生效,返回304和Not Modified, 协商缓存失效,返回200和请求结果。对于静态文件,例如:CSS、图片,服务器会自动设置协商缓存。
HTTP/1.0 响应头Last-Modified 请求头If-Modified-Since。本质上根据修改时间判断,若某资源在一秒内修改多次,则该方法会失效
HTTP/1.1 响应头ETag 请求头If-None-Match 资源的一个唯一标识,只要资源有变化,Etag就会重新生成。

什么是回流和重绘

回流: 当渲染树中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
重绘: 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流一定会触发重绘,而重绘不一定会回流

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些布局信息属性或调用某些方法

浏览器的优化
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。当你访问获取布局信息的属性或方法的时候,会强制队列刷新,这是为了保证及时性与准确性。

避免重绘与回流

  1. 把要修改的样式集中到一个 class 内统一修改
  2. 避免使用 table 布局 (尽量不要使用表格布局,如果没有定宽表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。)
  3. 尽可能在DOM树的最末端改变class,可以限制回流的范围
  4. 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位
  5. 使用display:none技术,只引发两次回流和重绘。将元素先脱离dom树进行处理再挂载。
  6. 缓存布局信息,避免多次刷新队列

JS如果执行很久阻塞页面怎么办,有哪些解决方式?

推迟加载 
如果页面初始的渲染并不依赖于js,可以最后再加载js。一种是方法把它们放在文档后面,另一种是在标签上加defer属性
异步加载
边渲染边下载。添加async属性

deferasync的区别
defer是立即下载但延迟执行
async是立即下载并执行

react/vue中key的作用,为什么不能用index作为key

key是虚拟DOM的标识,当状态中的数据发生变化时,react会根据新数据生成新虚拟DOM。然后react使用diff算法将旧虚拟DOM与新虚拟DOM进行比较:

  • 在旧虚拟DOM中找到了与新虚拟DOM中相同的key:
    • 若两者相等,直接复用之前的DOM
    • 若两者不相等,则生成新DOM挂载到页面
  • 在旧虚拟DOM中没找到与新虚拟DOM中相同的key
    • 生成新DOM挂载到页面

用index作为key可能有两个问题:
如果对列表进行逆序添加/删除等破坏顺序的操作,会导致产生没有必要的真实DOM的更新,导致性能下降
如果列表中含有输入框,输入框的内容会错位

vue

vue父组件向子组件传递数据

子组件通过props属性接收

vue中的指令

v-model双向数据绑定;
v-for循环;
v-if v-show 显示与隐藏;
v-on事件@;v-once: 只渲染一次

v-show和v-if指令的共同点和不同点?

共同点:都能控制元素的显示和隐藏;
不同点:实现本质方法不同,v-show本质就是通过控制css中的display设置为none,控制隐藏,只会编译一次;v-if是动态的向DOM树内添加或者删除DOM元素,若初始值为false,就不会编译了。而且v-if不停的销毁和创建比较消耗性能。
总结:如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大)。

如何让CSS只在当前组件中起作用?

在组件中的style前面加上scoped

请说出vue.cli项目中src目录每个文件夹和文件的用法?

assets文件夹是放静态资源;components是放组件;router是定义路由相关的配置; app.vue是一个应用主组件;main.js是入口文件。

分别简述computed和watch的使用场景

computed:
    当一个属性受多个属性影响的时候就需要用到computed
    最典型的栗子: 购物车商品结算的时候
watch:
    当一条数据影响多条数据的时候就需要用watch
    栗子:搜索数据

vue组件中data为什么必须是一个函数?

因为JavaScript的特性所导致,在component中,data必须以函数的形式存在,不可以是对象。
  组建中的data写成一个函数,数据以函数返回值的形式定义,这样每次复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱。而单纯的写成对象形式,就是所有的组件实例共用了一个data,这样改一个全都改了。

v-if和v-for的优先级

当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。所以,不推荐v-if和v-for同时使用。
如果v-if和v-for一起用的话,vue中的的会自动提示v-if应该放到外层去。

vue-router中history与hash模式的区别

1 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

2 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.abc.com/book/id 如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

事件修饰符

  • .stop阻止冒泡
  • .prevent阻止默认事件
  • .capture捕获事件
  • .once只触发一次事件
  • .self修饰符

stop的作用是让事件不再向外传播
prevent的作用是阻止默认事件,比如a标签默认的点击事件是跳转页面,
capture的作用是事件一发生当前元素的回调函数先相应,多个capture的顺序是从外向内

vue生命周期

vue每个组件都是独立的,每个组件都有一个属于它的生命周期,从一个组件创建、数据初始化、挂载、更新、销毁

  1. 创建vue实例,初始化事件和生命周期函数
  2. beforeCreate
  3. 初始化数据的观测,事件的回调
  4. created:表示实例创建完毕,可以获取数据和方法
  5. 进行模板的编译,生成dom树
  6. beforeMount:还没有进行挂载
  7. 将编译好的html内容挂载到dom树上
  8. mounted:挂载完成,页面渲染出来了,可以对dom进行操作
  9. beforeUpdate
  10. 数据更新,重新渲染
  11. updated
  12. beforeDestroy
  13. 进行销毁
  14. destroyed

你可能感兴趣的:(个人面试题总结)