前端面试题(基础篇)
性能优化面试题
一、在前端开发中,有哪些性能优化方面的操作?
- 减少http请求: 合并CSS、JavaScript文件,使用CSS Sprites合并图像,减少页面加载时的请求数量。
- 使用CDN: 将静态资源,如图片、CSS、JavaScript文件放在内容分发网络(CDN)上,提升资源加载速度。
- 压缩文件: 压缩CSS、JavaScript文件,减少文件大小,提高加载速度。可以使用工具如UglifyJS、CSSNano等。
- 懒加载: 对图片、视频等大文件采用懒加载技术。
- HTML原生的
loading
属性<img src="image.jpg" alt="Example Image" loading="lazy">
- 使用Intersection Observer API
- 使用第三方库, 如
lazysizes
- 缓存机制:使用浏览器缓存和服务器端缓存,让用户重复访问时不需要重新加载所有资源。
- 异步加载:将非关键的JavaScript文件设置为异步加载,不阻塞页面的渲染。
- 使用 async 和 defer 属性
<script src="script.js" async></script>
<script src="script.js" defer></script>
- async 属性告诉浏览器在下载文件的同时解析HTML,并在文件下载完成后立即执行它。适用于独立的JavaScript文件,不依赖于其他脚本。
- defer 属性告诉浏览器在下载文件的同时解析HTML,但等到HTML解析完成后再执行脚本。适用于有依赖关系的JavaScript文件。
- 优化图片:使用适当的图片格式(如WebP),并对图片进行压缩,减少文件大小。
- 减少DOM操作:尽量减少对DOM的直接操作,使用虚拟DOM(如React)来优化性能。
- 使用现代框架和工具:使用React、Vue等现代前端框架,以及Webpack、Parcel等打包工具来优化代码和提高开发效率。
- 代码分割:将应用程序代码按需加载,避免一次性加载所有代码,提升首屏加载速度。
二、什么是代码分割(Code Splitting),开发中如何利用代码分割进行性能优化?
代码分割(Code Splitting)是一种性能优化技术,它将一个大的JavaScript文件分割成多个小块(chunks),这些小块可以按需加载,从而减少初始加载时间,提高页面响应速度。代码分割常用于单页应用(SPA)和大型项目中。
为什么要使用代码分割?
减少初始加载时间:通过分割代码,可以只加载当前页面所需的代码,而不是加载整个应用的所有代码。
提高资源利用率:按需加载资源,减少不必要的网络请求和资源浪费。
提高用户体验:提高页面首次加载速度和交互响应速度。
如何利用代码分割进行性能优化?
使用代码分割有几种常见的方法,主要依赖于构建工具如Webpack、Parcel等。下面以Webpack为例,介绍如何进行代码分割。
- 入口和输出配置
在Webpack配置文件中,定义入口和输出
1
2
3
4
5
6
7
8
9
10// webpack.config.js
module.exports = {
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
}
}; - 代码分割插件
使用 SplitChunksPlugin 插件进行代码分割:
1
2
3
4
5
6
7
8
9// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
}; - 动态导入
使用动态导入 (import()) 将代码分割成更小的模块:
1
2
3
4
5
6// index.js
import(/* webpackChunkName: "example" */ './example').then(module => {
const example = module.default;
example();
});
使用React和React.lazy进行代码分割
对于React应用,可以使用 React.lazy 和 Suspense 进行代码分割
三、懒加载(Lazy loading)与预加载(Preloading)是什么?有什么作用?
懒加载
- 懒加载是指在用户需要时才加载资源,而不是在页面初始加载时就加载所有资源。这种方式可以显著减少初始页面加载时间,提升页面性能和用户体验。
作用:
减少初始加载时间:仅加载当前视口内的资源,延迟加载其他资源。
节省带宽:只在用户滚动到特定位置或需要时加载资源,减少不必要的流量消耗。
提升性能:减少初始页面的资源请求,降低服务器负载。
预加载
- 预加载是指在用户可能需要某些资源之前就提前加载这些资源。预加载可以确保资源在用户需要时已经准备就绪,避免因加载资源而产生的延迟。
作用:
减少用户等待时间:在用户需要资源之前就提前加载,确保资源可用。
提升用户体验:提前加载关键资源,确保页面切换或交互时流畅无延迟。
优化资源管理:在合适的时机加载资源,避免突然的加载高峰。
四、什么是CDN,它有什么作用?
CDN(Content Delivery Network,内容分发网络)是一组分布在不同地理位置的服务器网络,用于更高效地传递网站内容给用户。通过将内容分布到多个地理上分散的服务器上,CDN可以显著提高网站的加载速度和可靠性。
CDN的作用
- 提高加载速度:CDN会将内容缓存到离用户最近的服务器节点,从而减少延迟,提高内容加载速度。用户访问网站时,内容会从距离最近的服务器节点传送,而不是从原始服务器传送。
- 减轻服务器负载:通过将内容分发到多个服务器节点,CDN可以分担原始服务器的负载,避免因高访问量导致的服务器过载。
- 增强可用性和可靠性:CDN的分布式架构可以提供更高的可用性,即使某个服务器节点出现故障,其他节点也能继续提供服务,从而确保网站的稳定性和可靠性。
- 优化带宽使用:CDN通过缓存内容减少了原始服务器的带宽消耗,从而降低了带宽成本,并提高了网络效率。
- 提高安全性:CDN可以提供额外的安全功能,如DDoS防护、Web应用防火墙(WAF)、TLS/SSL加速等,从而提高网站的安全性。
CDN的工作原理
- 内容缓存:CDN会将静态内容(如HTML、CSS、JavaScript、图片、视频等)缓存到分布在各地的服务器节点上。
- 请求重定向:当用户请求某个资源时,CDN会根据用户的地理位置、服务器负载、网络条件等因素,将用户请求重定向到最优的服务器节点。
- 内容传输:服务器节点会将缓存的内容传输给用户。如果某个节点没有缓存所请求的内容,则会从原始服务器获取内容并缓存到该节点,以便后续请求使用。
五、浏览器缓存,HTTP缓存策略
浏览器缓存
浏览器缓存是指将网页资源(如HTML、CSS、JavaScript、图像等)存储在用户的本地计算机上,以便在后续访问时可以直接从本地读取这些资源,而不必重新从服务器获取。
HTTP缓存策略
HTTP缓存策略是通过HTTP头部字段来控制缓存行为的策略。常用的HTTP缓存头部字段包括 Cache-Control、Expires、ETag 和 Last-Modified。
- Cache-Control
Cache-Control
是一个相对现代且强大的缓存控制头部字段,它可以包含多个指令,用于定义资源的缓存策略。常见指令包括:- max-age: 指定资源在客户端缓存的最大时间(以秒为单位)。
- no-cache: 强制客户端在使用缓存之前,必须向服务器验证资源的有效性。
- no-store: 禁止缓存,所有请求都必须从服务器获取资源。
- public: 表示资源可以被任何缓存(如浏览器、CDN)存储。
- private: 表示资源只能被浏览器缓存,不能被共享缓存(如CDN)存储。
- Expires
Expires
是一个较老的缓存控制头部字段,用于指定资源的过期时间(一个具体的日期和时间)。当 Cache-Control 中包含 max-age 指令时,Expires 会被忽略。Expires: Wed, 21 Oct 2023 07:28:00 GMT
- ETag
ETag
(实体标签)是一个唯一标识符,用于标识资源的特定版本。服务器在响应中包含 ETag,客户端在后续请求中可以通过 If-None-Match 头部字段来发送 ETag。如果 ETag 匹配,服务器返回304 Not Modified状态码,客户端使用缓存资源。 - Last-Modified
Last-Modified
头部字段表示资源的最后修改时间。客户端可以通过 If-Modified-Since 头部字段来发送上次修改时间。如果资源没有更新,服务器返回304 Not Modified状态码,客户端使用缓存资源。Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
实际应用
- 长时间缓存(Cache Forever):对于不经常更改的资源(如版本化的CSS、JavaScript文件),可以设置较长的缓存时间(如一年),并使用文件名中的版本号来管理更新。
- 短时间缓存:对于经常变化的内容,可以设置较短的缓存时间(如几分钟或几小时),并结合 ETag 或 Last-Modified 进行验证。
六、开发中如何避免重绘和回流?
什么是重绘和回流?
- 重绘(Repaint):当元素的外观(如颜色、背景、边框等)发生变化时,需要重新绘制元素,但不影响布局。
- 回流(Reflow):当元素的布局(如尺寸、位置、显示/隐藏等)发生变化时,需要重新计算布局并重新渲染页面,这个过程开销较大。
怎样避免?
- 尽量避免频繁操作DOM,可以将多次DOM操作合并为一次。例如,使用文档片段(DocumentFragment)来批量添加节点
- 如果需要多次访问元素的布局信息(如宽度、高度等),应将其缓存起来,避免重复计算
- 减少对样式的频繁修改
- 避免触发同步布局
某些属性的读取会强制浏览器同步计算布局,尽量避免在修改布局后立即读取这些属性:
应该先修改所有布局,再读取:1
2
3
4const element = document.getElementById('myElement');
element.style.width = '100px';
const width = element.offsetWidth; // 触发同步布局
element.style.height = '100px';1
2
3
4const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '100px';
const width = element.offsetWidth; // 读取布局信息 - 使用CSS3硬件加速
使用CSS3的硬件加速特性(如transform、opacity等)可以将某些渲染任务交给GPU处理,减少CPU的开销
- 减少复杂的CSS选择器
- 使用虚拟DOM
七、Vue、React中的虚拟DOM是如何增强性能的?
虚拟DOM的工作原理
创建虚拟DOM:
在Vue和React中,组件的结构会被描述成一个虚拟DOM树。这个虚拟DOM树是一个JavaScript对象,表示DOM结构和属性。状态变化:
当组件状态或属性发生变化时,虚拟DOM会重新渲染,生成一个新的虚拟DOM树。差异计算(Diffing):
新旧两个虚拟DOM树之间会进行比较,找出需要更新的部分。这个过程叫做Diffing。更新补丁(Patching):
根据Diffing的结果,只将有变化的部分应用到真实DOM上。这一步称为Patching。
性能优势
减少直接DOM操作:
操作真实DOM是昂贵的,因为每次操作都可能导致浏览器重新计算布局和重新渲染。虚拟DOM通过在内存中进行操作,减少了直接操作真实DOM的次数,从而提升性能。批量更新:
虚拟DOM将多次状态变化合并成一次更新,减少了频繁的DOM操作。例如,当多个状态变化在同一个事件循环中发生时,虚拟DOM只会进行一次Diffing和Patching。优化更新策略:
虚拟DOM使用高效的算法来比较新旧DOM树,只更新必要的部分,避免不必要的重绘和回流。
总结
虚拟DOM通过以下几个方面提升性能:
- 减少直接DOM操作:通过在内存中进行操作,减少了昂贵的直接DOM操作。
- 批量更新:将多次状态变化合并成一次更新,优化性能。
- 高效Diffing:使用高效的算法找出变化部分,避免不必要的重绘和回流。
八、CSS选择器会影响浏览器的渲染性能吗?
CSS选择器确实会影响浏览器的渲染性能。
- 复杂选择器(如后代选择器、通用选择器等)会增加浏览器匹配元素的开销。浏览器需要从右到左解析选择器,并遍历DOM树来匹配每个元素。
- 越具体的选择器越优先,但也会增加计算的复杂性。具体性高的选择器在样式计算时会覆盖具体性低的选择器,这需要浏览器进行更多计算。
- 通配符选择器(如 * 和属性选择器 [attr])会影响性能,因为它们需要匹配大量的元素。
优化建议:
- 避免过度使用后代选择器
- 减少通配符选择器的使用
- 减少CSS嵌套
九、编写JavaScript代码的过程中有哪些性能优化的操作?
- 避免全局变量,因为全局变量会增加查找时间并可能导致命名冲突。
- 尽量避免在循环中进行重复计算或DOM操作,可以将结果缓存起来以提高性能。
- 对于动态添加的元素,使用事件委托可以减少事件处理器的数量,提升性能。
- 多次操作DOM会导致页面重绘和回流,应该尽量减少DOM操作的频率和数量。可以使用文档片段(DocumentFragment)或批量更新的方式来优化。
- 对于频繁触发的事件(如滚动、输入等),可以使用防抖(Debounce)和节流(Throttle)来限制事件处理器的调用频率。
- 对于耗时操作,可以使用异步操作(如Promise、async/await)来避免阻塞主线程。
- 对于不立即需要的资源(如图片、脚本等),可以使用惰性加载技术来减少初始加载时间。
- 将多个JavaScript文件合并为一个文件,并进行压缩,可以减少HTTP请求数量和文件大小,提升加载速度。
- 对于复杂的计算任务,可以使用Web Workers在后台线程中运行,避免阻塞主线程。
十、在JavaScript开发中,如何避免内存泄漏?
- 避免意外的全局变量, 使用let、const或var声明。
- 清理定时器和事件监听器
- 避免闭包中未释放的引用,闭包会引用外部作用域的变量,如果这些变量不再需要,应及时解除引用。
- 解除DOM引用,当移除DOM元素时,应解除对这些元素的引用,确保它们被正确垃圾回收。
- 使用开发工具检测内存泄漏
使用浏览器的开发者工具(如Chrome DevTools)可以帮助检测和分析内存泄漏。可以通过“性能”(Performance)和“内存”(Memory)面板查看内存使用情况,并使用快照来分析内存分配。
十一、防抖和节流在什么场景下可以提高性能?
防抖(Debounce)
防抖技术用于将多次频繁的操作合并为一次。只有在一定时间内没有新的操作时,才会执行函数。如果在指定时间内又触发了新的操作,则重新计时。适用于以下场景:
- 搜索输入:用户在搜索框中输入时,每次按键都会触发搜索请求。使用防抖可以确保只有用户停止输入后才发送请求,从而减少不必要的请求次数。
- 窗口调整:在调整窗口大小时,浏览器会频繁触发resize事件。使用防抖可以确保只有用户停止调整窗口后才执行相应的处理逻辑。
- 表单验证:在用户输入表单数据时,防抖可以防止每次输入都触发验证,减少不必要的计算。
节流(Throttle)
节流技术用于限制函数的执行频率,即在一定时间间隔内最多只执行一次函数。适用于以下场景:
- 滚动事件:在页面滚动时,scroll事件会频繁触发。使用节流可以限制滚动事件处理函数的执行频率,从而提高性能。
- 鼠标移动:在拖拽元素时,mousemove事件会频繁触发。使用节流可以确保处理函数不会过于频繁地执行。
- 窗口滚动加载:在实现无限滚动加载数据时,节流可以限制数据请求的频率,防止过多请求导致性能下降。
十二、你使用过哪些性能监控的工具?如何监控页面的性能?
- Chrome DevTools
用途:Chrome浏览器内置的开发者工具,用于分析和调试网页性能。
功能:
Performance 面板:记录和分析页面的加载和运行性能,提供详细的时间线、帧率、内存使用情况等。
Network 面板:监控网络请求,查看资源加载时间、请求大小、请求/响应头等。
Lighthouse:提供网页性能、可访问性、SEO等方面的自动化审查和改进建议。 - WebPageTest
用途:在线服务,用于测试网页的加载性能。
功能:提供不同地区、设备和浏览器的测试,生成详细的加载时间、资源请求、水瀑图等报告。
十三、如何配置DNS预解析(DNS Prefetching)以及它是如何应用的?
- 什么是DNS预解析?
DNS预解析(DNS Prefetching)是一种优化技术,它可以提前解析网页中会用到的域名,以减少用户在访问这些域名时的等待时间。通过提前解析DNS,浏览器可以在用户点击链接之前就已经完成域名解析,从而加快资源的加载速度,提高用户体验。
- 如何配置DNS预解析?
DNS预解析可以通过在HTML文档的
部分添加标签来实现。 - DNS预解析的使用场景
- 第三方资源:如果网页中使用了第三方的资源,如CDN、广告网络、分析工具等,可以通过DNS预解析提前解析这些域名。
- 导航链接:如果页面中有指向外部域名的导航链接,可以提前解析这些域名,减少用户点击链接后的等待时间。
- 图像、视频等大文件:对于需要加载的图像、视频等大文件,如果它们来自不同的域名,可以提前解析这些域名,加快加载速度。
浏览器相关面试题
一、描述浏览器的基本功能以及它是如何加载并运行网页的。
浏览器的基本功能
浏览器的主要功能是帮助用户访问和展示Web内容。以下是浏览器的基本功能:
- 用户界面:包括地址栏、返回按钮、前进按钮、书签栏、刷新按钮等,用于与用户交互。
- 浏览引擎:控制导航和展示网页内容。
- 渲染引擎:负责解析HTML、CSS,并将其转换为可视化的页面。
- 网络层:负责网络请求,如HTTP请求和响应。
- JavaScript引擎:执行JavaScript代码。
- UI后端:用于绘制基本的UI组件,如文本输入框和按钮。
- 数据存储:包括Cookie、LocalStorage、IndexedDB等,用于存储客户端数据。
浏览器加载并运行网页的过程
- 用户输入URL
用户在地址栏输入URL并按下回车键,浏览器开始处理用户请求。 - 解析URL
浏览器将输入的URL解析为协议(如HTTP或HTTPS)、域名和路径等部分。 - DNS解析
浏览器通过DNS(域名系统)将域名解析为IP地址。这一步可以通过本地缓存或DNS服务器完成。 - 发起HTTP请求
浏览器使用解析到的IP地址与服务器建立连接(通常是TCP连接),并向服务器发送HTTP请求。 - 服务器响应
服务器处理请求,并返回响应,包括状态码、响应头和响应体(通常是HTML文档)。 - 渲染过程
6.1 构建DOM树
浏览器解析HTML文档并将其转换为DOM(文档对象模型)树。DOM树是HTML文档的结构化表示。
6.2 构建CSSOM树
浏览器解析CSS文件和内联样式,生成CSSOM(CSS对象模型)树。CSSOM树用于表示CSS样式。
6.3 构建渲染树
浏览器将DOM树和CSSOM树合并,生成渲染树。渲染树包含所有可见的DOM节点及其样式信息。
6.4 布局(Reflow)
浏览器根据渲染树计算每个节点的几何信息(位置和大小),这一步称为布局或回流。
6.5 绘制(Repaint)
浏览器将渲染树的节点绘制到屏幕上,这一步称为绘制或重绘。 - 执行JavaScript
浏览器使用JavaScript引擎(如V8、SpiderMonkey)解析并执行JavaScript代码。JavaScript代码可以操作DOM树和CSSOM树,动态改变页面内容和样式。 - 处理其他资源
浏览器根据HTML文档中的引用(如img、script、link等),并行下载其他资源(如图片、脚本、样式表等)。这些资源下载后会进一步更新DOM树和CSSOM树,导致重新布局和重绘。
二、浏览器内核是什么,常见的浏览器内核有哪些?
浏览器内核(Browser Engine)是浏览器的核心组件,负责解析网页内容并将其呈现给用户。内核主要包含两个部分:渲染引擎和JavaScript引擎。
- 渲染引擎:负责解析HTML、CSS等内容,并将其转换为可视化的页面。
- JavaScript引擎:负责解析和执行JavaScript代码。
浏览器内核的性能和功能直接影响网页的渲染速度、兼容性和用户体验。
常见的浏览器内核
- Blink
简介:Blink是Google主导开发的渲染引擎,基于WebKit分支开发。
使用的浏览器:- Google Chrome
- Microsoft Edge(自从基于Chromium版本)
- Opera(从版本15起)
- Vivaldi
- Brave
- WebKit
简介:WebKit是Apple开发的渲染引擎,最早由KDE的KHTML引擎分支而来。
使用的浏览器:- Safari(包括桌面版和移动版)
- 旧版的Chrome(版本27及之前)
- 许多移动浏览器(如Android的内置浏览器)
三、解释一个网页从请求开始到最终显示给用户的完整过程
- 用户请求网页
当用户在浏览器的地址栏中输入一个URL并按下回车键时,请求过程开始。 - DNS解析
浏览器需要将输入的URL转换为IP地址。这需要通过DNS解析来完成。具体步骤如下:浏览器首先检查本地缓存中是否有该域名的IP地址记录。
如果本地缓存中没有,则浏览器会查询操作系统的缓存。
如果操作系统缓存中也没有,浏览器会向DNS服务器发送查询请求。
DNS服务器将域名转换为对应的IP地址,并将结果返回给浏览器。 - 建立连接
浏览器获得IP地址后,开始与服务器建立连接。通常这涉及以下步骤:TCP连接:浏览器与服务器建立TCP连接。这个过程包括三次握手:
客户端发送SYN(同步)包到服务器。
服务器收到SYN包后,发送SYN-ACK(同步-确认)包到客户端。
客户端收到SYN-ACK包后,发送ACK(确认)包到服务器,完成连接建立。
TLS握手(如果使用HTTPS):在建立TCP连接后,如果使用HTTPS,浏览器和服务器会进行TLS握手,以建立安全的加密连接。 - 发送HTTP请求
连接建立后,浏览器向服务器发送HTTP请求。请求包括以下内容:请求行:包括请求方法(GET、POST等)、请求的URL和HTTP版本。
请求头:包括Host、User-Agent、Accept等信息。
请求体(如果是POST请求):包含提交的数据。 - 服务器处理请求并响应
服务器接收到请求后,进行以下处理:服务器解析请求内容,确定请求的资源。
服务器处理请求(如查询数据库、执行脚本等)。
服务器生成响应内容,并将响应发送回浏览器。响应包括:
状态行:包括HTTP版本、状态码(如200、404等)和状态描述。
响应头:包括Content-Type、Content-Length、Set-Cookie等信息。
响应体:包括请求的资源内容(如HTML文档、图像等)。 - 浏览器接收并解析响应
浏览器接收到服务器的响应后,进行以下处理:解析响应头:浏览器根据响应头信息(如Content-Type)决定如何处理响应体。
解析HTML:浏览器开始解析HTML文档,构建DOM树。
解析CSS:遇到CSS文件或样式标签时,浏览器会下载并解析CSS,构建CSSOM树。
解析JavaScript:遇到JavaScript文件或脚本标签时,浏览器会下载并执行JavaScript代码。 - 构建渲染树
浏览器将DOM树和CSSOM树合并,生成渲染树。渲染树只包含可见的DOM节点及其样式信息。 - 布局(Reflow)
浏览器根据渲染树计算每个节点的几何信息(位置和大小),这一步称为布局或回流。 - 绘制(Repaint)
浏览器将渲染树的节点绘制到屏幕上,这一步称为绘制或重绘。 - 处理后续资源
在解析HTML的过程中,浏览器会发现需要加载的其他资源(如图片、CSS文件、JavaScript文件等)。这些资源会通过网络请求加载,并在加载完成后进行相应的解析和处理:CSS文件:继续构建和更新CSSOM树。
JavaScript文件:继续解析和执行,可能会更新DOM树或CSSOM树。
图片等资源:加载完成后进行绘制。 - 用户与页面交互
在整个过程中,浏览器还需要处理用户的交互操作(如点击、滚动等),并实时更新页面显示。这些交互可能触发更多的JavaScript执行、DOM更新和页面重新布局与重绘。
四、描述浏览器解析并展示HTML、CSS和JavaScript的基本过程
浏览器解析并展示HTML、CSS和JavaScript的过程包括以下关键步骤:
解析HTML并构建DOM树,解析CSS并构建CSSOM树,合并两者生成渲染树,进行布局计算和绘制,执行JavaScript代码并根据需要动态更新和重绘页面。
五、解释CSS文件是如何被浏览器解析并应用到网页上区的
- HTML解析和CSS请求:浏览器在解析HTML文档时,发现
<link>
或<style>
标签。 - 下载CSS文件:浏览器发起HTTP请求下载外部CSS文件。
- 解析CSS:
- 词法分析:将CSS文件内容分解为标记。
- 解析:将标记解析为CSSOM树。
- 构建CSSOM树:表示CSS规则及其应用目标。
- 合并DOM树和CSSOM树:生成渲染树。
- 布局(Reflow):计算每个节点的尺寸和位置。
- 绘制(Repaint):将节点绘制到屏幕上。
- 动态更新:JavaScript可能会动态修改DOM或CSS,触发重新布局和重绘。
六、详述浏览器加载和执行JavaScript文件的机制
- HTML解析和JavaScript请求
当浏览器解析HTML文档时,遇到<script>
标签时,会启动JavaScript的处理流程 - 阻塞和并行加载
- 阻塞解析:默认情况下,当浏览器遇到一个
<script>
标签时,会阻塞HTML的解析,直到该JavaScript文件被下载、解析并执行完毕。这是为了确保脚本在文档被完全解析之前可以操作DOM。 - 异步加载(async):如果在
<script>
标签中添加async属性,浏览器会并行下载JavaScript文件,但一旦下载完成,就会立即执行脚本,同时仍会阻塞HTML解析。 - 延迟加载(defer):如果在
<script>
标签中添加defer属性,浏览器会并行下载JavaScript文件,但会等到HTML文档完全解析完毕后才执行脚本,不会阻塞HTML解析。
- 阻塞解析:默认情况下,当浏览器遇到一个
- 下载JavaScript文件
浏览器根据<script>
标签中的src属性发起HTTP请求下载外部JavaScript文件。 - 解析和编译JavaScript
4.1 词法分析(Tokenization)
JavaScript引擎将下载的JavaScript文件分解为一系列的标记(tokens),如关键字、变量名、操作符等。
4.2 解析(Parsing)
JavaScript引擎将这些标记解析为抽象语法树(AST),表示程序的结构。
4.3 编译(Compilation)
现代JavaScript引擎(如V8引擎)会将AST编译为中间表示(IR),然后进一步优化和编译为机器码,以提高执行效率。 - 执行JavaScript
5.1 执行上下文(Execution Context)
每当JavaScript代码运行时,JavaScript引擎会创建一个执行上下文。执行上下文包含变量环境、词法环境和this绑定等信息。
5.2 执行栈(Call Stack)
JavaScript是单线程的,使用执行栈来管理函数调用。当一个函数被调用时,它的执行上下文被压入执行栈,函数返回后上下文被弹出。
5.3 事件循环(Event Loop)
JavaScript使用事件循环来处理异步操作。事件循环不断检查调用栈和任务队列,如果调用栈为空,事件循环会将任务队列中的第一个任务压入调用栈执行。 - DOM和CSSOM更新
在执行过程中,JavaScript代码可以操作DOM和CSSOM,动态更新页面内容和样式。这些修改可能触发重新布局(reflow)和重绘(repaint)。 - 垃圾回收(Garbage Collection)
JavaScript引擎会定期运行垃圾回收机制,回收不再使用的内存,以防止内存泄漏。现代垃圾回收算法(如标记-清除)能够高效地管理内存。
七、分析将JavaScript文件放置在HTML文档的不同位置(如头部和尾部)对加载和执行的影响
- 头部加载:会阻塞HTML解析,延迟页面渲染。适用于需要立即执行的全局脚本或初始化脚本。
- 尾部加载:不会阻塞HTML解析,提高页面初始加载速度和用户体验。适用于大部分脚本,尤其是依赖DOM结构的脚本。
- async和defer属性:提供灵活的加载选项,async适用于独立脚本,defer适用于依赖DOM结构的脚本。
八、定义回流(Reflow)在浏览器渲染过程中的含义,并解释何时会触发回流
回流(Reflow),也称为重排(Reflow),是浏览器在渲染网页时重新计算元素的几何属性(如位置和大小)并重新构建渲染树的过程。这个过程会使得部分或整个页面的布局重新计算,从而影响页面的渲染。
回流的触发条件
- 添加或删除元素
- 元素尺寸变化
- 元素位置变化
- 内容变化, 当元素的内容变化(如文本内容变化)会触发回流。
- 页面初始渲染
- 窗口大小变化
- 获取计算样式, 访问某些属性会强制浏览器刷新并返回最新的计算样式,从而触发回流。这些属性包括offsetWidth、offsetHeight、clientWidth、clientHeight等。
九、解释重绘(RePaint)以及它在浏览器渲染网页的作用
十、描述开发过程中遇到的跨域问题,并解释导致跨域问题产生的原因
跨域问题的描述
跨域问题是指在一个网页中,试图访问另一个域名下的资源(如API、脚本、图片等)时,浏览器基于安全策略限制这种操作,导致请求被阻止。
根据同源策略,只有当协议(protocol)、域名(domain)和端口号(port)完全相同时,才能共享资源。否则,就会被视为跨域请求,受到限制。
解决跨域问题的方法
- 服务器端设置:
1
2
3Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type - 前端配置代理
十一、解释正向代理和反向代理的概念,及其在网络通信中的作用
正向代理
正向代理是指在客户端和目标服务器之间建立一个代理服务器,客户端通过该代理服务器访问目标服务器。正向代理主要为客户端提供服务,隐藏客户端的真实IP地址,对外呈现代理服务器的IP。
作用:
- 隐藏客户端身份:通过代理服务器访问目标服务器,隐藏客户端的真实IP,增强匿名性和安全性。
- 访问控制:通过代理服务器,可以对客户端的访问进行控制,例如过滤某些网站,限制访问权限等。
- 缓存:代理服务器可以缓存常用资源,提高访问速度,减轻目标服务器的负载。
- 跨区域访问:代理服务器可以帮助客户端访问受区域限制的内容,例如某些国外网站。
反向代理
反向代理是指在目标服务器和客户端之间建立一个代理服务器,客户端通过该代理服务器访问目标服务器。反向代理主要为目标服务器提供服务,隐藏服务器的真实IP地址,对外呈现代理服务器的IP。
作用:
- 负载均衡:将客户端请求分配到多台后端服务器,分散负载,提高服务的可靠性和可扩展性。
- 隐藏服务器身份:通过代理服务器访问目标服务器,隐藏服务器的真实IP,增强安全性。
- SSL终止:在反向代理服务器处理SSL加密和解密,减轻后端服务器的负担。
- 缓存:代理服务器可以缓存动态内容,减少后端服务器的压力,加快响应速度。
十二、描述如何使用nginx作为解决跨域代理的一种方法,并概述其工作原理
使用Nginx作为解决跨域代理的一种方法主要是通过配置Nginx反向代理,将跨域请求转发到目标服务器,并设置适当的响应头来允许跨域访问。以下是如何使用Nginx解决跨域问题的方法及其工作原理。
- 客户端请求:
浏览器客户端发起一个跨域请求,例如http://your-domain.com/api/data。 - Nginx反向代理:
Nginx接收到客户端的请求,并根据配置将请求转发到目标服务器http://target-server.com/api/。
在转发请求时,Nginx会保留原始请求头,并添加代理头,如X-Real-IP、X-Forwarded-For等,帮助目标服务器识别请求的来源。 - 处理CORS预检请求:
对于复杂请求(如POST请求或带有自定义头部的请求),浏览器会先发起一个OPTIONS预检请求。
Nginx配置处理预检请求,添加相应的CORS响应头,如Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等。
Nginx直接返回预检请求的响应,允许实际请求的执行。 - 返回响应:
目标服务器处理请求后,将响应返回给Nginx。
Nginx将目标服务器的响应转发给客户端,同时添加CORS响应头,确保浏览器允许跨域访问。
关键配置解析
proxy_pass
:定义代理请求的目标服务器地址。proxy_set_header
:设置转发请求时的请求头,确保目标服务器能够识别和处理请求。add_header
:在响应中添加CORS相关的头部,允许跨域请求。if ($request_method = OPTIONS)
:处理预检请求,直接返回带有CORS头部的响应。
十三、解释浏览器的事件循环机制,包括它是如何处理异步操作的
tips: 可以参考之前的文章,有专门将
十四、宏任务,微任务。
tips: 同上
十五、比较Node.js和浏览器中事件循环机制,指出它们的主要差异
浏览器中的事件循环
浏览器环境中的事件循环主要由以下几个队列组成:
- 宏任务队列(Macro Task Queue):
- 包含整体脚本、setTimeout、setInterval、setImmediate、I/O操作等。
- 微任务队列(Micro Task Queue):
- 包含Promise.then、MutationObserver、queueMicrotask等。
执行步骤如下:
- 从宏任务队列中取出一个任务并执行。
- 执行所有的微任务队列中的任务,直到微任务队列为空。
- 更新UI渲染。
- 重复步骤1。
Node.js中的事件循环
Node.js事件循环的结构更复杂,主要分为以下几个阶段:
- Timers:处理setTimeout和setInterval的回调。
- Pending Callbacks:处理一些系统操作的回调,如TCP错误类型的回调。
- Idle, Prepare:仅供内部使用。
- Poll:检索新的I/O事件,执行I/O相关回调(几乎所有异步操作都在这个阶段进行)。
- Check:执行setImmediate的回调。
- Close Callbacks:执行一些关闭的回调,如socket.on(‘close’, …)。
Node.js中的事件循环执行步骤如下:
- 进入timers阶段,执行所有到期的setTimeout和setInterval回调。
- 进入pending callbacks阶段,执行延迟到下一个循环迭代的I/O回调。
- 进入idle, prepare阶段,Node.js内部使用。
- 进入poll阶段,检索新的I/O事件,执行I/O回调。这个阶段可能会阻塞,等待新I/O事件的到来。
- 进入check阶段,执行setImmediate回调。
- 进入close callbacks阶段,执行close事件回调,例如socket.on(‘close’, …)。
主要差异
- 微任务处理时机:
- 浏览器:在每个宏任务完成后,会清空所有微任务队列中的任务。
- Node.js:在各个不同阶段完成后,处理微任务队列中的任务。
- 额外阶段:
- Node.js:事件循环有多个独立阶段(如timers、poll、check等),每个阶段有特定的任务类型和处理方式。
- 浏览器:主要区分宏任务和微任务,没有独立的多个阶段。
- setImmediate与setTimeout:
- Node.js:setImmediate的回调在poll阶段之后执行,而setTimeout的回调在timers阶段执行。如果两者同时被调度,setImmediate通常会先执行。
- 浏览器:setImmediate不是标准的一部分,一般不被支持,浏览器主要使用setTimeout。
- I/O处理:
- Node.js:poll阶段专门处理I/O回调,是事件循环的重要组成部分。
- 浏览器:I/O操作通常由浏览器内部处理,开发者不直接与事件循环交互。
十六、描述process.nextTick在Node.js事件循环中的执行顺序及其与微任务的关系
process.nextTick
是 Node.js 中的一种特殊的异步操作,它在事件循环的每个阶段之间执行。它的设计初衷是为了在当前操作完成后、事件循环的下一次调度之前执行某些任务。
执行顺序
在Node.js事件循环中,process.nextTick 的回调会在当前操作结束后立即执行,即在任何I/O操作、定时器、setImmediate 回调之前。
具体顺序如下:
- 当前的同步代码执行完毕。
- 执行所有process.nextTick回调。
- 进入事件循环的下一个阶段。
process.nextTick 与微任务(Microtasks)
在Node.js中,微任务包括process.nextTick和Promise。
尽管它们都属于微任务队列,但process.nextTick回调的优先级比Promise的回调高。
十七、什么是垃圾回收机制,它是如何在现代编程语言中管理内存的?
垃圾回收机制(Garbage Collection, GC)是现代编程语言中用于自动管理内存的一种技术。
它的主要目标是释放那些不再被程序使用的内存,以防止内存泄漏和优化内存使用。
垃圾回收机制能够自动检测并回收无用对象,使程序员无需手动管理内存的分配和释放。
垃圾回收的基本概念
- 分配(Allocation):程序运行时会动态地请求内存,以存储变量、对象等数据结构。
- 使用(Utilization):分配的内存会被程序使用,直到程序不再需要这些数据。
- 回收(Reclamation):当内存中的数据不再被程序访问时,垃圾回收机制会将这些内存标记为可回收,并进行释放。
垃圾回收的常见算法
- 引用计数(Reference Counting):
- 每个对象维护一个计数器,记录指向该对象的引用数量。
- 当引用计数变为零时,对象被认为是不再使用的,可以被回收。
- 缺点:无法处理循环引用(两个对象相互引用,但不再被其他对象引用)。
- 标记-清除(Mark and Sweep):
- 分为两个阶段:标记阶段和清除阶段。
- 标记阶段:遍历所有可达对象(从根对象开始,如全局变量和栈中的引用),将它们标记为活动的。
- 清除阶段:遍历内存中的所有对象,回收未标记的对象。
- 优点:可以处理循环引用。
- 标记-压缩(Mark-Compact):
- 是标记-清除的改进版本,解决内存碎片问题。
- 在标记阶段标记活动对象后,将这些对象压缩到内存的一端,释放出连续的空闲内存。
- 分代收集(Generational Garbage Collection):
- 将内存分为几代:年轻代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation)。
- 年轻代中的对象大多数是短命的,垃圾回收频繁进行。
- 老年代中的对象存活时间较长,垃圾回收较少进行。
- 持久代用于存储类的元数据和静态变量。
- 优点:提高垃圾回收效率,减少内存碎片。
垃圾回收在现代编程语言中的应用
- Java:
- 使用分代收集算法(年轻代、老年代)。
- 主要垃圾回收器:Serial GC、Parallel GC、CMS GC、G1 GC。
- JVM自动管理内存,程序员无需显式释放内存。
- C#:
- 使用分代收集算法(0代、1代、2代)。
- CLR(Common Language Runtime)提供了垃圾回收功能。
- 程序员可以使用GC.Collect()方法手动触发垃圾回收,但一般不推荐。
- Python:
- 使用引用计数和循环垃圾回收(cycle garbage collection)。
- 引用计数:每个对象维护一个引用计数器。
- 循环垃圾回收:检测并回收循环引用的对象。
- JavaScript:
- 使用标记-清除算法。
- 浏览器和Node.js环境中,JavaScript引擎(如V8、SpiderMonkey)自动管理垃圾回收。
十八、V8引擎的垃圾回收机制具体是如何工作的
分代垃圾回收
V8将堆内存分为两个主要部分:年轻代(Young Generation)和老年代(Old Generation)。
年轻代(Young Generation):
- 包含新创建的、生命周期较短的对象。
- 使用的垃圾回收算法是 Scavenge,具体是半空间复制算法(Semi-space copying)。
- 年轻代被分为两个空间:From空间和To空间。新对象首先分配在From空间,当From空间填满时,启动一次垃圾回收,将存活对象复制到To空间,并交换From和To空间的角色。
老年代(Old Generation):
- 包含生命周期较长的对象,从年轻代晋升而来。
- 使用的垃圾回收算法是 标记-清除(Mark-Sweep) 和 标记-压缩(Mark-Compact)。
- 标记-清除:标记所有活动对象,然后清除未标记的对象。
- 标记-压缩:在标记清除的基础上,将活动对象移动到堆的一端,释放出连续的内存区域。
垃圾回收的阶段
V8垃圾回收过程主要包括以下几个阶段:
- 标记(Marking):
- 从根对象开始(如全局对象和栈中的引用),遍历所有可达对象,并标记它们为活动的。
- 未被标记的对象被认为是不可达的,可以被回收。
- 清除(Sweeping):
- 对于年轻代:Scavenge算法将存活对象复制到To空间,并清除From空间的所有对象。
- 对于老年代:标记-清除算法清除未标记的对象,释放内存。
- 压缩(Compaction):
- 在老年代,标记-压缩算法将存活对象移动到堆的一端,减少内存碎片。
增量和并发垃圾回收
为了减少垃圾回收对应用程序性能的影响,V8采用了增量和并发垃圾回收技术。
- 增量标记(Incremental Marking):
- 将标记阶段分为多个小步骤,在应用程序执行过程中逐步进行。
- 避免一次性长时间的垃圾回收暂停,提高应用程序的响应性。
- 并发清除(Concurrent Sweeping):
- 在标记阶段完成后,垃圾回收器在后台并发地执行清除操作。
- 允许应用程序在清除阶段继续执行,从而减少暂停时间。
- 并发压缩(Concurrent Compaction):
- 对老年代的压缩操作也可以并发进行,进一步减少对应用程序的影响。
示例代码解析
1 | function createObjects() { |
在这个示例中:
- 对象分配:
createObjects
函数创建了大量的对象,初始分配在年轻代的From空间。
- Scavenge回收:
- 当年轻代的From空间填满时,启动Scavenge回收,将存活对象复制到To空间。
- 对象晋升:
- 多次Scavenge回收后,仍然存活的对象将被晋升到老年代。
- 标记-清除和标记-压缩:
- 当老年代内存达到阈值时,启动标记-清除和标记-压缩回收,释放未标记的对象并压缩存活对象。
HTML-CSS面试题
一、什么是SEO(搜索引擎优化)? 为什么它对网站开发至关重要
SEO(Search Engine Optimization,搜索引擎优化)是一种提高网站在搜索引擎结果页面(SERP)中的排名,从而增加网站流量的策略和技术。
SEO涉及多个方面,包括内容优化、关键词研究、网站结构优化、外部链接建设和技术优化等。
为什么SEO对网站开发至关重要
- 提高可见性和流量
- 增强用户体验
- 提高品牌信任度和权威性
- 获取长期收益
- 竞争优势
二、SEO优化有哪些关键点?在日常开发中你采取了哪些措施来进行SEO优化?
- 确保主要关键词出现在页面的标题(
<title>
)、描述(<meta description>
)、头部标签(<h1>
)和内容的开头部分。 - 创建长尾关键词的内容以吸引特定用户群体。
- 创建一个合理的URL结构,使用有意义的、描述性的URL。
- 使用面包屑导航(Breadcrumbs),提高用户体验和搜索引擎的可见性。
- 在内容中添加内部链接,指向相关页面。
- 使用工具(如Google PageSpeed Insights、GTmetrix)检测和优化网站速度。
- 压缩和优化图片、使用浏览器缓存、减少HTTP请求数量。
- 实现响应式设计,确保网站在各种设备上的良好显示和操作体验。
- 使用CDN(内容分发网络)提高内容交付速度。
三、defer和async属性在script标签中分别有什么作用?
tips: 前面有
四、CSS3主要引入了哪些新特性
CSS3 引入了许多新特性,使得前端开发更加便捷和强大。这些新特性包括但不限于布局、动画、视觉效果、选择器和功能增强等方面。以下是一些主要的 CSS3 新特性:
- 选择器增强
- 属性选择器
[attribute^="value"], [attribute$="value"], [attribute*="value"]
。 - 伪类选择器:
:nth-child(n), :nth-of-type(n), :first-of-type, :last-of-type, :only-of-type, :not(selector)
。 - 伪元素选择器:
::before, ::after
。
- 属性选择器
- 新的颜色模式
- RGBA:
rgba(255, 0, 0, 0.5)
。 - HSLA:
hsla(120, 100%, 50%, 0.3)
。 - 透明度:
opacity
属性。
- RGBA:
- 盒模型和布局
- 盒阴影(Box Shadow):
box-shadow: 10px 10px 5px #888888
。 - 圆角(Border Radius):
border-radius: 10px
。 - 弹性盒模型(Flexbox):
display: flex
,可以轻松实现水平和垂直居中、等高布局等。 - 网格布局(Grid Layout):
display: grid
,提供了更加复杂和灵活的布局方式。
- 盒阴影(Box Shadow):
- 背景和边框
- 多重背景:
background: url(image1.png), url(image2.png)
。 - 背景尺寸:
background-size: cover, background-size: contain
。 - 边框图片:
border-image: url(border.png) 30 30 round
。
- 多重背景:
- 过渡和动画
- 过渡(Transitions):
transition: all 0.5s ease
。 - 动画(Animations):
@keyframes,animation: mymove 5s infinite
。
- 过渡(Transitions):
- 变换
- 2D 变换:
transform: rotate(45deg), transform: scale(1.5)
。 - 3D 变换:
transform: rotateX(45deg), transform: rotateY(45deg)
。
- 2D 变换:
- 多列布局
- 列计数:
column-count: 3
。 - 列间距:
column-gap: 20px
。
- 列计数:
- 媒体查询
- 媒体查询:
@media screen and (max-width: 600px) { ... }
。
- 媒体查询:
- 字体和文本
- 自定义字体(Web Fonts):
@font-face
。 - 文本阴影:
text-shadow: 2px 2px 5px #888888
。 - 字间距和行高:
letter-spacing, line-height
。
- 自定义字体(Web Fonts):
五、你怎么理解CSS Sprites,以及它在前端开发中的优势是什么?(性能优化)
CSS Sprites 是一种将多个小图标合并到一张大图中的技术,通过 CSS 控制显示大图中的特定部分来展示单个图标。这种技术主要用于减少 HTTP 请求次数,提升网页加载速度和性能。
六、能解释一下物理像素、逻辑像素、像素密度的区别,并说明为什么在移动端开发中需要@2x、@3x等图片吗?
物理像素(Physical Pixels)
物理像素是设备屏幕上实际存在的最小显示单元。每个物理像素由红、绿、蓝(RGB)三个子像素组成,这些子像素可以组合成不同的颜色。不同设备的屏幕有不同数量的物理像素。
逻辑像素(Logical Pixels)
逻辑像素是由CSS、JavaScript等开发工具定义的抽象像素,用于布局和设计。逻辑像素与物理像素的关系通常通过设备像素比(Device Pixel Ratio,DPR)来描述。一个逻辑像素可以对应多个物理像素,这取决于设备的像素密度。
像素密度(Pixel Density)
像素密度指的是单位面积(通常是每英寸)的物理像素数量,通常用 PPI(Pixels Per Inch)表示。像素密度越高,屏幕显示的图像越清晰。
设备像素比(Device Pixel Ratio,DPR)
设备像素比是指一个逻辑像素对应的物理像素数量。常见的DPR有1(普通显示器)、2(Retina显示器)、3等。
@2x、@3x 图片
在移动端开发中,不同设备有不同的像素密度,为了确保在高像素密度的设备上显示清晰的图像,需要提供多种分辨率的图片。
- @1x:标准分辨率,对应于DPR为1的设备。
- @2x:高分辨率,对应于DPR为2的设备。图片的尺寸是原图的2倍。
- @3x:超高分辨率,对应于DPR为3的设备。图片的尺寸是原图的3倍。
实际应用
在开发过程中,可以通过CSS和HTML来指定不同分辨率的图片。例如:
1 | <img src="[email protected]" srcset="[email protected] 2x, [email protected] 3x" alt="Example Image"> |
七、使用CSS,你怎样画出一条粗细为0.5px的线?
方法一:使用transform缩放
通过使用transform: scaleY(0.5);
来缩放1px的线,可以实现0.5px的效果。
方法二:使用伪元素和边框
使用伪元素创建一个0.5px的边框:
1 | <div class="half-px-line"></div> |
1 | .half-px-line { |
方法三:使用背景图
1 | .half-px-line { |
方法四:使用viewport单位
1 | .half-px-line { |
方法五:使用子像素渲染
1 | .half-px-line { |
八、什么是1px问题,并且如何在前端开发中解决它
在移动端开发中,1px问题是指在高DPI(高像素密度)屏幕设备上,1px的线条会显得过于粗,无法实现预期的细线效果。这是因为高DPI设备将多个物理像素映射到一个逻辑像素上,使得1px的线条在视觉上看起来比在低DPI设备上粗。
例如,在一个DPR(设备像素比)为2的设备上,1px的线条实际上会占用2个物理像素,导致线条变粗。这个问题尤其在需要精细设计的界面上显得很明显。
如何解决1px问题
方法一:使用transform缩放
…..和上面差不多,省略
方法二:使用媒体查询调整, 通过媒体查询,可以根据设备的像素比来调整线条的粗细。
九、如何理解块格式化上下文(BFC),并且创建BFC的方法有哪些?
块格式化上下文(Block Formatting Context,BFC)是Web页面布局中的一个概念,它决定了其子元素如何定位及与其他元素的关系。
BFC的特点
- 内部元素的垂直方向排列:BFC中的块级盒子会在垂直方向上一个接一个地排列。
- 清除浮动:BFC区域会包含内部浮动元素,不会影响外部元素的布局。
- 外边距重叠:BFC可以避免相邻块级元素之间的外边距重叠问题。
- 隔离与独立性:在同一个BFC中的元素不会与外部元素互相影响。
创建BFC的方法
- 浮动元素:设置元素的float属性为left或right。
- 绝对定位元素:设置元素的position属性为absolute或fixed。
- 设置display属性:将元素的display属性设置为inline-block、table-cell、table-caption等。
- 设置overflow属性:将元素的overflow属性设置为hidden、auto、scroll。
- 设置display: flow-root:直接将元素的display属性设置为flow-root。
十、开发中,会用BFC解决哪些问题,如何解决
清除浮动(Clearing Floats)
当父容器内的子元素浮动时,父容器的高度会塌陷,导致后续元素上升。BFC可以解决这一问题,使父容器包含浮动子元素的高度。
防止外边距重叠(Preventing Margin Collapsing)
相邻块级元素之间的外边距会发生重叠,导致布局问题。BFC可以防止相邻元素的外边距重叠。
十一、通常会采取哪些措施来确保网或应用在不同浏览器上的兼容性
- 处理浏览器特定的CSS前缀
.example {
-webkit-border-radius: 10px; /* Chrome, Safari /
-moz-border-radius: 10px; / Firefox /
border-radius: 10px; / 标准 */
} - 使用CSS中的特性查询
通过特性查询(Feature Queries)来检查浏览器是否支持某些CSS特性,并根据支持情况应用样式。1
2
3
4
5
6@supports (display: grid) {
.example {
display: grid;
}
}
JavaScript面试题
一、JavaScript代码是如何被执行的?
- 首先是加载和解析阶段,浏览器解析HTML文档,当遇到
<script>
标签时,会暂停解析HTML并开始加载和执行JavaScript代码。 - 第二步编译,JavaScript是解释执行的,但现代浏览器(如Chrome的V8引擎)会先将JavaScript代码编译为机器码,以提高执行效率。编译过程分为两个阶段:
- 解析:通过词法分析(Lexical Analysis)和语法分析(Syntax Analysis),将JavaScript代码解析成抽象语法树(AST)。
- 生成字节码:将AST转换为字节码,以便于执行。
- 第三部执行,JavaScript代码的执行是通过事件循环(Event Loop)机制进行的。具体步骤如下:
- 每当一个函数被调用时,会创建一个函数执行上下文并推入执行栈。函数执行完毕后,其上下文会从执行栈中弹出。
- 当遇到异步任务时,会将异步任务加入任务队列,等待执行。
- 当所有同步代码被执行完比,执行栈为空,此时执行任务队列里面的任务
- 任务队列分为宏任务和微任务,在执行宏任务之前,会先执行完所有的微任务。
tips: 执行上下文包括 变量对象(Variable Object)、作用域链(Scope Chain)、this指向()
二、什么是JavaScript引擎,常见的有哪些?
JavaScript引擎是用于解析和执行JavaScript代码的程序或解释器。它将JavaScript代码转换为机器码,使得浏览器或运行环境可以执行这些代码。JavaScript引擎不仅负责解释和执行代码,还包括垃圾回收和优化代码执行性能等功能。
常见的JavaScript引擎
- V8 引擎 由Google开发,主要用于Google Chrome、Node.js
- SpiderMonkey 引擎 第一个JavaScript引擎
- Chakra 引擎 Microsoft开发
- JavaScriptCore(Nitro)引擎 Apple开发,用于Safari浏览器、iOS应用
三、v8引擎是如何执行JavaScript代码的?
旧版本的浏览器在编译阶段会进行词法分析和语法分析生成AST,但一般不会将其编译为机械码,js是解释执行的。但在v8引擎等现代引擎上,会进行此操作,其他过程见问题一。
四、v8引擎的架构包括哪些部分,它们的作用是什么?
V8引擎的架构包括多个关键组件,如下:
- Parser(解析器)
- 作用:解析器读取源代码,进行词法分析和语法分析,生成表示代码结构的AST。
- Ignition(解释器)
- 作用:将AST转换为字节码,并逐行解释和执行字节码。它收集执行过程中函数的性能数据,供后续优化使用。
- TurboFan(优化编译器)
- 使用Ignition收集的性能数据,识别出执行频率高的代码路径(热点代码),并将其编译为优化后的机器码。TurboFan的优化策略包括内联、常量折叠、死代码消除等,以提高代码执行效率。
- Orinoco(垃圾回收器)
- Orinoco是V8的垃圾回收子系统,使用分代垃圾回收算法。
- 运行时系统(Runtime System)
- 提供JavaScript的内置对象和函数的实现。
- 调试器(Debugger)
- 支持代码调试功能。
五、请解释什么是变量提升(Hoisting)?为什么JavaScript语言存在变量提升这一机制
变量提升(Hoisting)是JavaScript中的一个行为,它指的是变量和函数的声明会在代码执行前被提升到其所在作用域的顶部。
使用 let 和 const 声明的变量也会被提升,但它们在声明之前是不可访问的,访问会导致引用错误(ReferenceError)。这段时间通常被称为“暂时性死区”(Temporal Dead Zone, TDZ)。
为什么存在变量提升这一机制
在JavaScript代码执行之前,会进行词法分析和语法分析阶段。在这些阶段中,变量和函数的声明会被注册到作用域内,但赋值操作和函数调用等实际执行的代码会保留在原位置。这就导致了变量提升的现象。
六、变量提升机制有哪些潜在的缺点?它可能导致哪些具体问题?
- 增加代码的复杂性和困惑
- 意外的 undefined 值
- 函数声明和变量声明的混淆
- 全局变量污染
七、如何定义’作用域‘,请举例说明不同类型的作用域
作用域是JavaScript中管理变量、函数和对象可访问性的机制。主要包括全局作用域、函数作用域和块级作用域。
全局作用域中的变量和函数可以在整个程序中访问,函数作用域中的变量和函数只能在函数内部访问,而块级作用域中的变量和函数只能在块内部访问。
八、请解释什么是作用域链,以其在JavaScript中的作用
当在一个作用域中查找变量时,JavaScript引擎会沿着作用域链逐级向上查找,直到找到变量为止。如果在最顶层的全局作用域中仍未找到,则返回 undefined 或抛出引用错误(ReferenceError)。
作用域链是在函数执行上下文创建时建立的。然而,作用域链的基础是函数定义时记录的词法环境(Lexical Environment),这个词法环境决定了在函数执行时作用域链的结构。
作用:
- 变量查找
- 嵌套函数的变量访问
- 闭包
九、闭包是什么?以及它在JavaScript中的用途
闭包(Closure)是指在一个函数内部定义的函数,即使外部函数已经执行完毕,该内部函数仍然可以访问外部函数的变量。闭包使得这些变量的作用域得以延续。
闭包可以用来创建私有变量和方法
数据隐藏和封装
回调函数和事件处理器
立即执行函数表达式(IIFE)利用闭包来创建一个新的作用域,避免全局变量污染。
十、详细说明什么是执行上下文(Execution Context)?它是如何影响JavaScript代码执行的?
执行上下文是JavaScript执行过程中用来管理代码执行环境的抽象概念,它包含了在代码执行时需要的一切信息,决定了哪些变量和函数可以被访问,以及它们是如何相互作用。
执行上下文有两种,全局执行上下文和函数执行上下文,全局执行上下文在js代码开始执行时创建,函数执行上下文在函数被调用时创建,函数执行完时,函数执行上下文被弹出执行栈并销毁。
十一、JavaScript代码的执行过程是怎样的?
从浏览器解析HTML文档遇到script标签时,开始加载和执行JavaScript代码,然后开始解析,解析通过词法分析和语法分析,生成AST,之后根据AST编译成字节码,开始执行,执行通过事件循环机制进行。
十二、JavaScript执行过程中,会产生AO、GO、VO、LE、VE、ER都是什么对象?
GO
Global Object 全局对象,浏览器是Window, Node.js中是global
对象。VO
Variable Object 变量对象,存储了上下文中所有的变量、函数声明和函数参数。全局执行上下文中,VO是GO,在函数执行上下文中,VO是AOAO
Activation Object 活动对象,函数执行上下文中的变量对象,在函数调用时创建。LE
Lexical Environment 词法环境,JavaScript在执行代码时,用于管理标识符和变量的结构。包含了环境记录(Environment Record)和外部环境的引用(outer)。词法环境有两种类型:全局环境和函数环境。VE
Variable Environment 变量环境,类似于词法环境,但专门用于存储变量声明(var
)。在现代JavaScript中,词法环境和变量环境常常被合并,特别是在处理let、const和var声明时。ER
Environment Record 环境记录,是词法环境中的一个组件,存储了标识符和变量的实际绑定。它可以是声明性环境记录(Declarative Environment Record)或对象环境记录(Object Environment Record)。声明性环境记录用于存储变量、函数和catch块中的绑定。对象环境记录用于存储全局对象的绑定。
十三、请说一下你对JavaScript中原形(prototype)和原形链的理解
每一个对象都有一个__proto__
(隐式原形),通过这个属性可以访问创建它的函数的prototype(显式原形),
在访问一个对象的属性时,会首先从对象本身的属性中找,如果找不到,则会通过__proto__
指向它的构造函数的prototype,如果一直找不到,最终会指向null,这种逐级向上查找的过程也叫原形链查找。
而查找过程中形成的__proto__链,也叫原形链。
十四、为什么JavaScript需要存在原形和原形链,它设计的目的是什么?
原型和原型链设计是为了实现对象的继承和共享,在实例化对象时,通过原型链共享方法,避免了每个实例都复制一份方法,从而节省内存空间。
JavaScript中的原型可以动态添加或修改属性和方法,从而使得对象的行为可以在运行时改变。
原型链允许对象的方法可以被重写,从而实现多态。这是面向对象编程的重要特性。
十五、原形链的终点是什么?如何在代码中打印出一个对象的原形链直至终点?
原型链的终点是null
,所有对象最终都会通过原型链追溯到Object.prototype,而Object.prototype的原型是null。
通过循环遍历Object.getPrototypeOf(currentObj) // 获取当前对象的原形
十六、JavaScript(或者其他语言中)为什么需要使用this关键字
- 引用对象实例:this指向调用它的对象实例。
- 动态上下文:this的值在函数调用时确定,取决于函数的调用方式。例如,在事件处理函数中,this指向事件发生的元素
- 构造函数:在构造函数中,this指向新创建的实例,使得构造函数可以初始化实例的属性
- 箭头函数:箭头函数不会创建自己的this,而是从外层作用域继承this,这在回调函数中非常有用
十七、JavaScript中,this的绑定规则有哪些
- 默认绑定(Default Binding):在非严格模式下,this默认绑定到全局对象(在浏览器中是window,在Node.js中是global)。在严格模式下,this绑定为undefined。
- 隐式绑定(Implicit Binding):当函数作为对象的方法调用时,this绑定到调用该方法的对象。
- 显式绑定(Explicit Binding):使用call、apply和bind方法可以显式地绑定this到指定的对象。
- new绑定(New Binding):当使用new关键字调用构造函数时,this绑定到新创建的对象。
- 箭头函数绑定(Arrow Function Binding):箭头函数不会创建自己的this,它从定义时的作用域中继承this。
十八、使用new关键字实例化一个对象具体发生了什么?
- 创建一个新的空对象:JavaScript引擎会创建一个新的空对象,并将其作为构造函数的实例。
- 设置原型:新创建的对象会继承构造函数的原型对象。这是通过将新对象的 proto 属性指向构造函数的 prototype 属性来实现的。
- 绑定 this:构造函数内部的 this 被绑定到新创建的对象上。因此,构造函数内的 this 指向这个新对象。
- 执行构造函数:执行构造函数中的代码,为新对象添加属性和方法。
- 返回新对象:如果构造函数没有显式返回对象,那么默认返回这个新创建的对象。如果构造函数显式返回一个对象,那么返回的就是这个对象,而不是新创建的对象。
十九、var、let、const分别有什么区别,从作用域、提升(hoisting)、以及是否允许重新赋值,重新声明来说
- var:函数作用域,变量提升但初始化为undefined,允许重新赋值和重新声明。
- let:块作用域,变量提升但在TDZ中不可用,允许重新赋值,不允许重新声明。
- const:块作用域,变量提升但在TDZ中不可用,不允许重新赋值,不允许重新声明。
二十、解释JavaScript中的Proxy对象是什么?以及它是如何工作的,通常用于哪些场景
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Proxy对象通过一个目标对象和一个处理程序(handler)对象来创建。处理程序对象包含一组捕获器(trap),这些捕获器是覆盖对象默认行为的函数。
1 | let target = { |
Proxy的捕获器(Traps)
get(target, property, receiver)
:捕获对目标对象属性的读取操作。set(target, property, value, receiver)
:捕获对目标对象属性的写入操作。has(target, property)
:捕获in操作符对目标对象的检查操作。deleteProperty(target, property)
:捕获对目标对象属性的删除操作。apply(target, thisArg, argumentsList)
:捕获对目标对象作为函数的调用操作。construct(target, argumentsList, newTarget)
:捕获对目标对象作为构造函数的调用操作。
典型应用场景
- 数据验证
- 自动化和日志记录
- 实现默认值
- 虚拟属性 可以创建虚拟属性,这些属性并不存在于目标对象中,但可以通过捕获器提供值。
- vue响应式
二十一、比较Proxy和Object.defineProperty之间的区别,特别是在用于对象属性拦截和修改行为时,它们在功能,性能,以及适用场景上有什么异同
功能区别
- Proxy:Proxy 是 ES6 引入的一个特性,提供了一种更强大和灵活的方式来拦截和修改对象的操作。通过 Proxy,可以拦截对象的诸多操作,包括属性访问、赋值、枚举等,可以实现更复杂的拦截逻辑,并且可以针对整个对象进行拦截,而不是单个属性。
- Object.defineProperty:Object.defineProperty 是 ES5 引入的方法,用于定义或修改对象的属性。它可以为对象添加新属性或修改现有属性的特性,如是否可枚举、是否可配置等,但只能拦截属性的读取和赋值操作,并且只能针对单个属性进行操作。
性能
- Proxy:由于 Proxy 提供了更灵活的拦截机制,它的性能通常会比 Object.defineProperty 稍低一些。每次访问或操作代理对象时,都需要经过代理的捕获器,可能会带来一些性能开销。
- Object.defineProperty:相比之下,Object.defineProperty 的性能通常更高,因为它仅仅是在属性的读取或赋值时进行拦截,且只涉及单个属性,没有额外的捕获器逻辑。
使用场景
- Proxy:适用于需要复杂拦截逻辑或对整个对象进行拦截的场景。例如,实现数据验证、数据绑定、虚拟属性等功能时,Proxy 是一个更好的选择。
- Object.defineProperty:适用于简单的属性拦截和修改场景,例如,需要定义一些特殊属性或在属性访问时执行一些逻辑的情况下,Object.defineProperty 是一个简单有效的方案。
二十二、Map和weakMap有什么不同?请解释weakMap特性,以及为什么及在什么情况下会选择使用weakMap而不是Map
- Map的键可以是任意数据类型,weakMap的键只能是引用数据类型或者非全局注册的符号
- 当Map的对象键被解除引用时,对应的值不会被垃圾回收。weakMap的则会被回收
- Map可以通过for of或者forEach进行迭代,weakMap不可迭代
- Map可以通过size属性获取键值对数量,weakMap不能直接获取所有条目,或者检查其大小
什么情况使用weakMap?
- 当你有一个键对象,且希望在该对象没有其他存活引用时自动回收相关资源时
- 如果你不希望暴露存储的信息,且这些信息与对象的生命周期相关,你可以使用 WeakMap。
示例: - DOM元素的附加信息
1
2
3
4
5
6const elementData = new WeakMap();
let element = document.getElementById('some-element');
elementData.set(element, {clickCount: 0});
// 如果'element'被移除并且没有其他引用,它关联的数据会被自动垃圾回收 - 缓存:
1
2
3
4
5
6
7
8
9const cache = new WeakMap();
function getHeavyComputationResult(obj) {
if (!cache.has(obj)) {
let result = heavyComputation(obj);
cache.set(obj, result);
}
return cache.get(obj);
}
二十三、请列举实现异步编程的几种方法,并简要说明它们各自的特点
- 回调函数,简单,但会形成回调地狱
- promise, 可读性好,支持错误处理,可以合并多个异步操作
- Async/Await, promise的语法糖
- 生成器函数,通过 yield 关键字可以暂停和恢复函数执行。可以通过 next(value) 向生成器函数传递数据,从而在暂停和恢复之间传递状态。
二十四、解释什么是回调函数,以及他在异步编程过程中的作用和存在的缺点
回调函数就是你把一个函数当作参数传入另外一个函数,在适当的时机去执行,它是一种异步编程的解决方法,但是对于复杂的逻辑,容易形成回调地狱。
二十五、如何解决所谓的回调地狱的问题
使用promise解决
二十六、什么是Promise,引入Promise的原因是什么
Promise 是一种在 JavaScript 中管理异步操作的对象。
一个 Promise 可以处于以下三种状态之一
- Pending(待定) - 初始状态,既没有被解决(resolved),也没有被拒绝(rejected)。
- Fulfilled(已解决) - 表示操作成功完成。
- Rejected(已拒绝) - 表示操作失败。
引入Promise原因
- 避免回调地狱
- 更清晰的错误处理
- 链式调用
- 增强的可控性,拥有合并异步任务的api
二十七、解释什么是生成器(Generator),以及它在异步编程中如何被利用
- 生成器(Generator)是 ES6(ECMAScript 2015)引入的一种函数类型,用于控制函数的执行过程。
- 与普通函数不同,生成器函数可以在执行过程中暂停,并在后来恢复执行。
- 生成器函数通过关键字 function* 来定义,并且返回一个迭代器对象。
- 调用生成器函数不会立即执行代码,而是返回一个可以控制其执行的生成器对象。
基本用法
1 | function* myGenerator() { |
二十八、详细描述async、await关键字的作用,它们与Promise相比有什么优势和不同?
async 关键字用于声明一个异步函数。 异步函数总是返回一个 Promise,即使函数中没有显式地返回一个 Promise,JavaScript 引擎也会在内部将其转换为一个已解析或拒绝的 Promise。
await 关键字只能在 async 函数内部使用。它用于等待一个 Promise 的解析(或拒绝)并返回其结果。使用 await 可以暂停异步函数的执行,直到被等待的 Promise 解决后再继续执行。
与Promise相比,代码可读性更好
Vue3面试题
一、描述Vue的模版编译过程,具体步骤包括哪些?
编译过程主要包括一下步骤:
- 解析模板 (Parsing)
解析器将模板字符串解析成抽象语法树(AST)结构。解析阶段的主要任务是将模板语法转换为 AST 节点。 - 优化 (Optimization)
生成 AST 树后,优化阶段的主要目的在于标记模板中所有的静态节点。 - 代码生成 (Code Generation)
在优化后的 AST 树基础上,代码生成器将其转换为 JavaScript 渲染函数。 - 执行渲染函数 (Runtime Execution)
最后一步是执行渲染函数。渲染函数在运行时接收一个上下文对象,这个对象包含 Vue 实例中的数据、方法等。根据这些数据,渲染函数会生成一个虚拟 DOM 树。 虚拟 DOM 树通过与现有的真实 DOM 树进行对比,会生成一组操作指令,然后这些指令被应用到真实的 DOM 中以更新用户界面。
二、Vue中template语法与JSX语法有何不同?它们各自的优势在哪里?
template更偏向于HTML的写法,内置了vue的指令,来进行条件判断、循环渲染。html代码,样式,脚本分离。结构更清晰。
JSX语法更倾向于JS语法,可以在jsx中写html代码,可以将组件用变量储存,支持js的条件判断,循环的方式渲染。更加灵活。
三、Vue在哪些情况下会进行依赖收集?依赖收集的机制是什么?
Vue一般在三种情况下会进行依赖收集,组件渲染时、计算属性求值时,侦听器初始化时。
依赖收集的机制依赖于观察者模式,其核心是通过 Dep 和 Watcher 两个类来实现。
- Dep 是一个依赖收集器。每个响应式对象(如数据属性)都有一个对应的 Dep 实例,用来存储所有依赖于该对象的 Watcher。
- Watcher 是一个订阅者。每个组件实例、计算属性、侦听器等都会创建一个或多个 Watcher 实例。当依赖的数据属性发生变化时,对应的 Watcher 会被通知,并触发相应的回调函数(如组件重新渲染、计算属性重新计算等)。
依赖收集的流程:
- 组件渲染或计算属性求值时,会创建一个 Watcher 实例,并将其设置为当前活动的 Watcher(通过全局的 Dep.target 进行管理)。
- 访问数据属性时,会触发该属性的 getter,在 getter 中会调用 Dep.depend() 方法。
- Dep.depend() 方法会将当前活动的 Watcher 添加到该属性的 Dep 中,并将该属性的 Dep 添加到 Watcher 的依赖列表中。
- 当数据属性发生变化时,会触发该属性的 setter,在 setter 中会调用 Dep.notify() 方法,通知所有依赖于该属性的 Watcher 进行更新。
四、Vue中使用Object.defineProperty()进行数据劫持,有哪些潜在的缺点或限制?Vue3为什么选择Proxy?
Object.defineProperty()的主要特点包括无法监听数组变化、无法代理新增属性、需要递归遍历所有属性以及对象复制的问题。
Proxy提供了更全面的功能,支持所有对象操作,性能更好,实现代码更简单。
五、Vue的双向数据绑定是如何工作的?能否简述其基本原理?
Vue 的双向数据绑定是一种通过绑定 DOM 元素和 Vue 实例的数据,实现数据的自动同步更新的机制。其基本原理是通过数据劫持和发布-订阅模式,使得数据的变化能够实时反映到视图中,视图的变化也能够实时反映到数据中。
当输入框值变化时,通过事件监听器,能将数据同步到Vue示例的数据中。
当Vue示例中的响应式数据变化时,通过set读取到值变化,然后通知更新到Dom中。
六、当Vue中的数据对象某属性值改变时,视图的更新是即时的还是异步的?请解释其更新机制
更新是异步的,为了避免多次更新,优化性能。
更新机制
当Vue数据改变时,会触发数据的setter
,并标记这个Watcher
需要更新。
Vue不会立即更新,而是会将这个Watcher
添加到一个异步队列,这个队列确保每个Watcher只会被添加一次。即使某个数据在一次循环中改变多次。
在下一个事件循环开始时,Vue 刷新队列,执行所有收集到的 Watcher,更新视图。这种方式确保了每次事件循环只会进行一次 DOM 更新,提升了性能。
七、Vue对象或者数组的属性变化监听是如何实现的?请描述其工作原理
对象是通过递归遍历,来为每个属性进行数据劫持
数组是通过重写方法,如push、pop、shift、unshift、splice、sort、reverse
等。在这些方法调用时进行拦截,并触发依赖通知。
Vue 2 创建响应式数组时,将数组的原型指向 arrayMethods,从而拦截变更方法的调用。
八、请解释Vue中keep-alive组件的作用和实现机制。它具体缓存的内容是什么?
keep-alive
是 Vue 提供的一个内置组件,用于缓存动态组件实例,使得在组件切换过程中能够保留其状态或避免重新渲染。
实现机制
- 缓存组件实例
keep-alive
组件内部维护一个缓存对象,用于存储创建的组件实例。当组件切换时,会通过key
值,从缓存中取出对应的实例进行渲染,而不是重新渲染 - 保留组件状态
当组件实例被缓存时,其内部状态(如数据,事件处理器等)也会被保留。 - 生命周期钩子
- activated:组件激活时,触发
- deactivated:组件被停用时触发
缓存内容
- 组件实例,包括组件的状态(如数据、计算属性、方法等)和 DOM 节点。
- 内部状态,组件的内部状态,如 data、props、computed、methods。
- DOM结构,组件的 DOM 结构也会被缓存,不会被移除。
九、Vue的$nextTick原理是什么?它在Vue应用中有哪些用途?
参考文章$nextTick的实现原理
十、Vuex的工作原理,你是如何理解Vuex在状态管理中的角色和重要性的?
十一、如何比较Vuex和localStorage在状态管理方面的不同用途和特性
十二、Vuex状态管理和使用全局对象进行状态管理有什么本质区别和优势?
十三、相比于Vuex,Pinia在设计上有哪些显著的优势和可能的劣势
十四、描述Pinia的核心工作原理,Pinia是如何管理和维护应用状态的
十五、Vue3较Vue2引进了哪些主要的更新和改进
十六、Vue3的响应式系统如何工作,它和Vue2有何不同
十七、Vue3在编译过程中进行了哪些优化?以提高运行时性能
十八、是什么使得Vue3在性能上比Vue2更具有优势
十九、Vue3中的watch和watchEffect有什么不同,它们各自适用于什么场景
二十、请解释Vue3中的reactive、ref的区别及其使用场景
二十一、Vue3中的Composition Api与React的Hooks在设计和用法上有什么主要的不同?
二十二、Vue3的diff算法较Vue2有什么改进?这些改进如何影响了性能
二十三、虚拟DOM是什么?为什么要使用虚拟DOM?
二十四、虚拟DOM的解析过程是怎样的?
二十五、DIFF算法的原理是什么?它是如何提高性能的?
二十六、虚拟DOM和真是DOM在性能上的对比如何
二十七、Vue中的key的作用是什么?为什么需要绑定key
二十八、为什么不建议在Vue中使用index作为key
- 标题: 前端面试题(基础篇)
- 作者: DansRoh
- 创建于 : 2024-06-06 00:00:00
- 更新于 : 2024-06-25 16:33:35
- 链接: https://blog.shinri.me/2024/06/06/21_前端面试题_基础/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。