前端面试记录
js
1. 函数式编程
将计算过程视为一系列的函数调用,函数的输出完全由输入决定,不依赖于或改变程序的状态,使得函数式编程的代码更加可预测和易于理解。
函数式编程的三个核心概念:纯函数、高阶函数和柯里化。
高阶函数:函数可以作为参数传递给其他函数,也可以作为其他函数的返回值。
纯函数:相同的输入总是得到相同的输出,函数不改变外部状态,也不依赖外部状态,不产生副作用。
柯里化:它可以将一个接受多个参数的函数转换为一系列使用一个参数的函数。提高了函数的复用性和模块性
2. 不可变数据的优势
1、易于追踪变化: 当数据不可变时,每次变化都会生成新的数据对象,更容易跟踪和理解数据的变化过程。
2、性能优化: React可以通过比较新旧数据对象,确定何时进行渲染,从而提升性能。
3、避免副作用: 直接修改数据可能导致副作用和难以预料的错误。不可变数据可以减少这些问题。
4、时间旅行调试: 使用不可变数据,可以更轻松地实现时间旅行调试,即查看应用在不同时间点的状态。
3. 闭包
闭包是js最强大的特性之一,他允许在一个函数中嵌套另一个函数,内部函数具有外部函数的所有变量和函数的完全访问权限,但是外部函数不能访问内部函数中的变量和函数。这给内部函数的变量提供了一种封装。
由于内部函数可以访问外部函数的作用鱼,当内部函数的生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行的持续时间更长。当内部函数被外部函数之外的作用域访问时,就会创建闭包。
应用:防抖,节流,函数柯里化
特点: 局部变量无法被外部访问,变量持久化不被释放,
4. 垃圾回收机制
更详细内容: https://juejin.cn/post/6981588276356317214
引用数据类型保存在堆内存中,基础类型保存在栈内存中
js定期找到那些不再使用的内存,将其释放掉。两种常见的垃圾回收算法:引用计数法和标记清除法。
标记清除法分为标记和清除两个阶段,标记阶段从根部(全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的,做上标记,清除阶段则把没有标记的对象(非可达对象)销毁。
引用计数法主要记录对象有没有被其他对象引用,如果没有被引用,它将被垃圾回收机制回收。它的策略是跟踪记录每个变量值被使用的次数,当变量值引用次数为0时,垃圾回收机制就会把它清理掉。
为什么需要垃圾回收: 程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃
5. 对象作用域,原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
原型:被用于复制现有实例来生成新实例的函数
构造函数:用new来调用,就是为了创建一个自定义类
实例:是类在实例化之后一个一个具体的对象
6. bind, call, apply
bind() 里面传递的是对应指向的对象
call() 里面传递的是指向的对象和参数(会自动调用)
apply() 里面传递时指向的对象及参数数组(会自动调用)
相同点:call、apply和bind都是JS函数的公有的内部方法,他们都是重置函数的this,改变函数的执行环节。
不同点:bind是创建一个新的函数,而call和aplay是用来调用函数;call和apply作用一样,只不过call为函数提供的参数是逗号分割,而apply为函数提供的参数是一个数组。
7. 进程与线程
CPU是计算机的核心,承担所有的计算任务。进程是CPU资源分配的最小单位,进程包括运行中的程序和程序所使用到的内存和系统资源。
线程是CPU调度的最小单位。线程是建立在进程的基础上的一次程序运行单位,通俗点解释线程就是程序中的一个执行流,一个进程可以有多个线程。
进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线
进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多。
JS的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
8. 事件循环
更详细内容:https://juejin.cn/post/6844904050543034376
js是单线程的运行机制,先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈(又称调用栈,先进后出)。
2. 当遇到异步任务时,会将其挂起并添加到任务队列中(先进先出),宏任务放入宏任务队列,微任务放进微任务队列。
3. 当执行栈为空时,事件循环先从微任务队列中按顺序取出任务,加入到执行栈中执行。如果微任务队列清空,就从宏任务队列中取出任务加入执行栈中执行。
4. 重复上述步骤,直到任务队列为空。
首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分
同步任务会直接进入主线程依次执行
异步任务会再分为宏任务和微任务
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务
上述过程会不断重复,这就是Event Loop,比较完整的事件循环
定时器线程,异步http请求线程,事件触发线程
9. 浅拷贝,深拷贝
深拷贝和浅拷贝都是对象的拷贝,都会生成一个看起来相同的对象,他们本质的区别是拷贝出来的对象的地址是否和原对象一样,浅拷贝是地址的复制,深拷贝值的复制
10. 输入网址到页面渲染发生了什么
- DNS解析:浏览器首先会检查缓存中是否有该网址的IP地址,如果没有,则会向DNS(域名系统)服务器查询该网址对应的IP地址。DNS服务器会将域名解析成对应的IP地址,这样浏览器才能通过IP地址找到服务器。
- 建立连接:浏览器使用IP地址找到对应的服务器,并通过HTTP(超文本传输协议)或HTTPS(安全的HTTP)协议与服务器建立连接。在HTTPS连接中,还会涉及到SSL/TLS加密的过程。
- 发送请求:连接建立后,浏览器会向服务器发送一个HTTP请求报文,请求中包含了请求的方法(如GET或POST)、请求的资源的路径等信息。
- 服务器响应:服务器接收到请求后,会处理请求并生成HTTP响应报文。如果请求的是静态资源(如HTML、CSS、图片等),服务器会直接返回这些文件;如果请求的是动态资源,服务器会根据应用程序逻辑生成相应的HTML页面等数据。
- 浏览器渲染:浏览器接收到服务器响应的数据后,会开始进行渲染。这个过程包括解析HTML文档,构建DOM(文档对象模型)树,解析CSS并将其应用到DOM树上,执行JavaScript代码,以及布局和绘制最终的页面。
- 加载资源:在渲染页面的过程中,如果发现需要额外的资源(如图片、CSS文件、JavaScript文件等),浏览器会继续通过HTTP请求获取这些资源,并将它们应用到页面上。
- 交互处理:当页面加载完成后,用户就可以与页面进行交互。用户的操作可能会触发更多的HTTP请求,比如点击链接或提交表单。
11. 空间复杂度与时间复杂度
空间复杂度也就是 S(n) ,就是对一个算法或者说一段代码在运行过程中占用存储空间大小表达方式,用大O表示法来表示。存储空间是由声明的变量决定的,关于分析空间复杂度,其实我们直接从声明的变量入手就可以,看函数体内声明的变量根据传入值的变化而变化来分析。递归函数,每层递归里都会开辟一个递归栈,每次递归产生的变量等空间消耗都会存在递归栈中,这也是一个空间,不管你有没有声明变量,只要递归了递归栈它都存在,也就是说只要存在递归的情况,基本上最少的空间复杂度也是 O(n) 了,所以我们尽可能的在能使用迭代的情况下就不使用递归。
我们一般用 T(n) 简化后的估算值来表达代码执行的速度,通常我们用大些字母 O 来表示,即大 O 表示法,由于是估算,它表示的是代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。在表示时间复杂度时,常数和系数都会被忽略掉。
T(n) = 10n^4 + 100n^2 + 1000 时间按复杂度为 O(n^4)
12. 重绘与回流
更详细内容:https://juejin.cn/post/7159155955987382309
回流(Reflow)
当渲染树中的元素因为尺寸,位置,隐藏等改变而需要重新构建。这就称为回流(reflow)。在页面第一次加载的时候,会发生回流的,因为要构建render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
简单来说,回流就是计算元素在设备内的确切位置和大小并且重新绘制
回流的代价要远大于重绘。并且回流必然会造成重绘,但重绘不一定会造成回流。
重绘(Repaint)
当渲染树中的一些元素需要更新样式,但这些样式属性只是改变元素的外观,而不会影响布局的,比如background-color。则就叫称为重绘(repaint)。
简单来说,重绘就是将渲染树节点转换为屏幕上的实际像素,不涉及重新布局阶段的位置与大小计算。
优化
样式集中改变(减少重排次数)合并对DOM样式的修改,采用css class来修改
将 DOM 离线(减少重排次数): display:none
脱离文档流(减小重排范围): absolute 或 fixed
13. ts中type interface区别
相同点: 都可以描述对象和函数,都允许拓展,interface(extends)type(|)。
不同点:type可以声明基本类型别名,联合类型,元组等。
interface能够重复声明,并自动合并。type重复声明会报错。
都可以使用 typeof 获取实例的类型。
react
1. react key的作用
key可以重置state。如果没有key,在相同位置的相同组件,react更新时会认为是同一个,保留状态。可以通过动态添加key属性,赋不同值让react认为是不同的组件,重置渲染。
key可以保留state。同一层级下,如果将组件移动位置,可以通过添加key的方式,让react识别出这是同一个组件,继续复用旧的而不会重置。
2. 为什么多个 JSX 标签需要被一个父元素包裹?
JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
3. react diff算法, fiber
更详细内容: https://juejin.cn/post/7131741751152214030
react 是基于 vdom 的前端框架,组件渲染产生 vdom,渲染器把 vdom 渲染成 dom。
浏览器下使用 react-dom 的渲染器,会先把 vdom 转成 fiber,找到需要更新 dom 的部分,打上增删改的 effectTag 标记,这个过程叫做 reconcile,可以打断,由 scheducler 调度执行。reconcile 结束之后一次性根据 effectTag 更新 dom,叫做 commit。
这就是 react 的基于 fiber 的渲染流程,分成 render(reconcile + schedule)、commit 两个阶段。
当渲染完一次,产生了 fiber 之后,再次渲染的 vdom 要和之前的 fiber 对比下,再决定如何产生新的 fiber,目标是尽可能复用已有的 fiber 节点,这叫做 diff 算法。
react 的 diff 算法分为两个阶段:
第一个阶段一一对比,如果可以复用就下一个,不可以复用就结束。
第二个阶段把剩下的老 fiber 放到 map 里,遍历剩余的 vdom,一一查找 map 中是否有可复用的节点。
最后把剩下的老 fiber 删掉,剩下的新 vdom 新增。
这样就完成了更新时的 reconcile 过程。
其实 diff 算法的核心就是复用节点,通过一一对比,map 查找找到可复用的节点,移动过,然后剩下的该删删该增增。
webpack
更多详细内容:https://juejin.cn/post/7023242274876162084?searchId=20240903150902068A0B5F5C12208A6FA0#heading-11
https://juejin.cn/post/6844904094281236487?searchId=20240903150902068A0B5F5C12208A6FA0
https://juejin.cn/post/6943468761575849992?searchId=2024041114421641791B4FF2FABB1A9DC7
1. webpack 构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
开始编译: 用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
确定入口: 根据配置中的 entry 找出所有的入口文件
编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
完成模块编译: 在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。
2. webpack的主要作用如下:
模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。
编译兼容。通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less,.vue,.jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
能力扩展。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。
3. 不同环境需求
本地环境:
需要更快的构建速度
需要打印 debug 信息
需要 live reload 或 hot reload 功能
需要 sourcemap 方便定位问题
…
生产环境:
需要更小的包体积,代码压缩+tree-shaking
需要进行代码分割
需要压缩图片体积
4. loader
loader 是负责对其他类型的资源进行转译的预处理工作
style-loader #
MiniCssExtractPlugin.loader #
cache-loader # 在一些性能开销较大的 Loader 之前添加,缓存loader编译内容
css-loader
postcss-loader # 扩展 css 语法,补充前缀
less-loader
file-loader url-loader
babel-loader
ts-loader
eslint-loader # 通过 ESLint 检查 JavaScript 代码
5. plugins
Plugin 插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
mini-css-extract-plugin #分离样式文件,CSS 提取为独立文件
optimize-css-assets-webpack-plugin # 压缩 css
html-webpack-plugin
clean-webpack-plugin # 目录清理
speed-measure-webpack-plugin # 性能分析
purgecss-webpack-plugin # 会单独提取 CSS 并清除用不到的 CSS
webpack-bundle-analyzer # 可视化 Webpack 输出文件的体积
6. Webpack 的热更新原理
深度解析:https://zhuanlan.zhihu.com/p/30669007
Hot Module Replacement,HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
7. 优化
代码懒加载 预加载
css
1. rem em
rem 是基于html元素字体大小来决定的,em 是根据使用它的元素字体大小来计算的。
2. flex
更多详细内容http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
- 采用flex布局的元素称为父容器,它的子元素称为项目,通过设置主轴,交叉轴可以控制项目的排列方式。
- 父容器属性:6个
flex-direction : 决定主轴的方向 row row-reverse column column-reverse
flex-wrap : 定义如果一条轴线排不下,如何换行 nowrap wrap wrap-reverse
flex-flow: 是flex-direction属性和flex-wrap属性的简写
justify-content:定义了项目在主轴上的对齐方式 flex-start flex-end center space-around space-between
align-items: 定义项目在交叉轴上对齐方式 flex-start flex-end center stretch baseline
align-content: 定义了多根轴线的对齐方式 flex-start flex-end cend stretch space-around space-between - 项目属性 6个
order: 定义项目的排列顺序。数值越小,越靠前,默认0
flex-grow: 定义项目的放大比例。默认0,即存在剩余空间,不放大
flex-shrink: 定义项目的缩小比例。默认1,即空间不足,项目将缩小
flex-basis:定义了在分配多余空间之前,项目占据的主轴空间。它的默认值为auto,即项目的本来大小。
flex:是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto
align-self:允许单个项目有与其他项目不一样的对齐方式。可覆盖align-items属性 auto flex-start flex-end center stretch baseline
网络
1. https 是什么?
https 是超文本传输安全协议, 在 HTTP(超文本传输协议)的基础上加入了 SSL/TLS(安全套接层/传输层安全)协议,以提供加密通信和身份验证的功能。主要目的是确保数据传输的安全性和完整性。通过使用加密技术,HTTPS 可以防止数据在传输过程中被窃听或篡改。同时,HTTPS 还通过数字证书进行身份验证,确保用户正在与正确的服务器通信,而不是被伪装的恶意服务器。
2. https如何建立连接
https 建立连接的过程包括TCP三次握手和SSL/TLS握手。TCP三次握手用于建立可靠的连接,而SSL/TLS握手则用于确保通信双方的身份验证和加密通信。通过这个过程,HTTPS能够提供安全、可靠的数据传输服务。
2.1 tcp 三次握手
tcp 三次握手 需要客户端和服务器总共发送3个报文。这个过程确保了双方都能够准备好发送和接收数据,同时也为双方后续的数据传输设置了初始的序列号,以便正确地对数据进行排序和确认。
第一次握手:
客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:
服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:
客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
2.2 SSL/TLS握手
- 客户端向服务端发送消息,包含客户端支持的ssl/tls协议列表,加密套件列表,随机数等
- 服务端回复消息,包含选定的ssl/tls协议,加密套件,随机数等,并包含服务器证书,公钥
- 客户端验证服务器证书的有效性,无效则断开连接
4.通过后,客户端生成一个预主密钥,并通过证书中的公钥进行加密,发送给服务器,同时根据随机数生成会话密钥 - 服务器使用自己的私钥解密该预主密钥,同样生成会话密钥,与客户端一致
- 至此TLS连接建立完成,可以开始传输数据,并且使用同一个私钥密钥来加解密。