前端面试题(综合篇)
1.讲讲事件循环
在事件循环中,所有的任务可以分为同步任务和异步任务,它们的执行方式有所不同。
(1)同步任务会直接进入主线程执行,不会被阻塞。
(2)异步任务会被放入事件表(Event Table),并注册相应的回调函数到事件队列(Event Queue)。当满足触发条件时,这些异步任务的回调函数会被移入执行队列(Execution Queue)中等待执行。
主线程在执行同步任务的同时,会不断地检查执行队列,如果执行队列中有待执行的任务,主线程会将它们移入执行栈(Execution Stack)中执行。这个过程不断循环,即为事件循环。
下面结合一段代码来理解上述概念:
// 同步任务
console.log("同步任务开始");
// 异步任务1:使用setTimeout模拟一个异步任务
setTimeout(function() {
console.log("异步任务1完成");
}, 2000); // 2秒后执行回调函数
// 异步任务2:使用Promise模拟另一个异步任务
const promiseTask = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("异步任务2完成");
}, 1000); // 1秒后异步任务完成
});
promiseTask.then(function(result) {
console.log(result);
});
console.log("同步任务结束");
// 输出结果顺序为:
// 同步任务开始
// 同步任务结束
// (等待2秒)
// 异步任务2完成
// 异步任务1完成
在这段代码中,我们有一些同步任务和异步任务。
- 同步任务会立即执行,不会被阻塞。所以 “同步任务开始” 和 “同步任务结束” 会立即输出。
- 异步任务1 使用
setTimeout
模拟,会在2秒后输出 “异步任务1完成”。 - 异步任务2 使用
Promise
模拟,会在1秒后输出 “异步任务2完成”。
在执行过程中,异步任务1和异步任务2被放入事件表,并注册了相应的回调函数到事件队列。当所有同步任务执行完毕后,主线程会不断地检查事件队列,将其中的任务移入执行栈执行。所以 “异步任务2完成” 会先输出,然后是 “异步任务1完成”。
2.讲讲跨域
跨域(Cross-Origin)指的是在 Web 开发中,当一个页面的脚本尝试去访问另一个源(域、协议或端口)的资源时所发生的一种情况。由于浏览器的同源策略(Same-Origin Policy),默认情况下,页面中的 JavaScript 代码只能与同一源下的资源进行交互,而无法直接访问其他源的资源。
同源策略的存在是出于安全考虑,它可以防止恶意网站通过脚本获取用户的敏感信息,比如 Cookie、LocalStorage 等。但有时候,我们确实需要与其他源的资源进行交互,比如在前后端分离的情况下,前端页面需要获取后端 API 的数据,这就涉及到了跨域。
常见的跨域场景:
-
前后端分离开发:在前后端分离的开发模式下,前端项目通常运行在一个不同的域名或端口下,与后端 API 服务器不在同一个源(origin)下,因此会遇到跨域问题。
解决方案:在后端服务中配置 CORS(Cross-Origin Resource Sharing),允许前端应用的域名或者端口访问后端 API。或者使用反向代理服务器,在同一域名或端口下转发请求。
-
跨子域请求:例如,一个页面是从
www.example.com
加载的,而 API 接口却在api.example.com
上,这种情况也会触发跨域问题。 解决方案:在后端服务中配置 CORS,允许跨域请求来自指定的子域名。
-
跨协议请求:例如,一个页面是通过 HTTPS 加载的,但是 API 接口却是通过 HTTP 提供的,这也会触发跨域问题。
解决方案:由于浏览器对跨协议请求的限制较强,通常建议统一使用 HTTPS 协议,以保障数据传输的安全性。
-
跨端口请求:即使是在同一台服务器上,如果前端页面运行在一个端口,而后端 API 服务运行在另一个端口,也会被浏览器视为跨域请求。
解决方案:同样在后端服务中配置 CORS,允许前端应用的端口访问后端 API。
-
跨文档访问(Cross-document communication):例如,通过
<iframe>
或者在一个窗口中打开另一个窗口,如果它们的源不同,就会涉及到跨域访问。 解决方案:可以使用 postMessage API 进行跨窗口通信,或者在目标页面中设置合适的 CORS 头部来允许跨域访问。
-
第三方资源引入:例如,在页面中引入了来自其他网站的 JavaScript、CSS 或者图片等资源,如果这些资源的域名与当前页面不一致,就会产生跨域问题。
解决方案:如果资源是可以下载并重新托管的,可以将资源下载到本地再引入;如果资源是不可更改的,可以使用服务器端代理,将资源代理到当前域名下。
-
跨站点脚本攻击(Cross-Site Scripting, XSS):虽然不是正常的跨域请求,但 XSS 攻击也是一种跨域问题,它利用了浏览器不同源策略的弱点,在一个网站上执行恶意脚本来窃取用户信息等。
解决方案:避免直接执行来自用户输入的脚本,对用户输入进行合适的过滤和转义,使用 Content Security Policy(CSP)等安全机制来限制脚本的执行。
3.webpack和vite的区别
Webpack 和 Vite 是两种前端构建工具,它们在构建过程、开发体验和性能表现等方面有一些区别:
- 构建过程:
- Webpack:Webpack 是一种基于任务(task)的构建工具,它通过配置文件定义一系列任务和处理器,如编译 JavaScript、处理 CSS、压缩图片等,然后按照配置文件的定义逐个执行这些任务,最终生成构建产物。
- Vite:Vite 是一种基于原生 ES 模块的构建工具,它利用浏览器原生支持的 ES 模块特性,在开发时不需要预先构建整个应用,而是按需编译、按需加载,以更快的速度启动开发服务器和构建应用。
- 开发体验:
- Webpack:Webpack 在开发时通常需要较长的构建时间,每次修改代码都需要重新构建整个应用,因此开发体验可能不够流畅,特别是对于大型应用。
- Vite:Vite 利用了浏览器原生的 ES 模块加载特性,可以快速地启动开发服务器,且在修改代码时只需重新编译修改的部分,因此开发体验更加流畅,可以实现毫秒级的热更新。
- 性能表现:
- Webpack:Webpack 的构建速度通常较慢,尤其是在处理大型应用时,因为它需要对整个应用进行重新构建。
- Vite:Vite 的构建速度通常较快,因为它只需要编译和加载修改的部分,而不需要重新构建整个应用,因此在开发时能够实现更快的热更新和启动开发服务器。
总的来说,Webpack 更适用于构建复杂的大型应用,而 Vite 更适用于快速启动开发服务器和提供流畅的开发体验,特别是对于小型应用和中小型团队来说。
4.HTTP缓存
HTTP 缓存是一种在 Web 开发中用于提高性能和减少网络流量的技术,它通过在客户端(浏览器)和服务器之间缓存资源的副本来减少对服务器的请求。HTTP 缓存通常可以分为两种类型:浏览器缓存和代理服务器缓存。
- 浏览器缓存:
- 强缓存:浏览器在请求资源时会先检查是否存在强缓存,如果存在且未过期,浏览器直接从缓存中获取资源,不发送请求到服务器。强缓存通常通过设置响应头中的
Cache-Control
或Expires
字段来实现。 - 协商缓存:当资源的强缓存过期或不存在时,浏览器会发送一个带有条件的请求到服务器,服务器根据请求头中的
If-Modified-Since
或If-None-Match
字段验证资源是否发生了变化。如果资源未发生变化,则返回状态码 304 Not Modified,浏览器从缓存中获取资源。协商缓存通常通过设置响应头中的Last-Modified
或ETag
字段来实现。
- 强缓存:浏览器在请求资源时会先检查是否存在强缓存,如果存在且未过期,浏览器直接从缓存中获取资源,不发送请求到服务器。强缓存通常通过设置响应头中的
- 代理服务器缓存:
- 代理服务器(如 CDN)也可以缓存资源,当请求到达代理服务器时,代理服务器会根据一定的规则来判断是否缓存响应。代理服务器缓存可以减少网络流量,并且能够为全球范围内的用户提供更快的访问速度。
通过合理地配置缓存策略,可以有效地提高网站的性能和用户体验。不过需要注意的是,缓存也可能会带来一些问题,如缓存过期导致的资源不一致性,因此需要根据实际情况来选择合适的缓存策略。
5.原型链
原型链是 JavaScript 中用于实现继承和属性查找的重要机制。每个 JavaScript 对象都有一个原型(prototype),原型又是一个对象,对象之间通过原型形成了链式结构,即原型链。
- 对象和原型:
- 在 JavaScript 中,几乎所有的对象都是通过构造函数创建的,每个构造函数都有一个
prototype
属性,指向一个原型对象。 - 对象通过
__proto__
属性(或Object.getPrototypeOf()
方法)指向自己的原型对象。
- 在 JavaScript 中,几乎所有的对象都是通过构造函数创建的,每个构造函数都有一个
- 继承和属性查找:
- 当我们访问一个对象的属性时,如果该属性不存在,则 JavaScript 引擎会沿着原型链向上查找,直到找到属性或者到达原型链的顶端(即
Object.prototype
)为止。 - 这样就实现了继承的效果,子对象可以共享父对象的属性和方法。
- 当我们访问一个对象的属性时,如果该属性不存在,则 JavaScript 引擎会沿着原型链向上查找,直到找到属性或者到达原型链的顶端(即
- 原型链的终点:
- 原型链的终点是
Object.prototype
,它是 JavaScript 中所有对象的原型链的顶端。 Object.prototype
的原型是null
,表示它没有原型,是原型链的终点。
- 原型链的终点是
例如,我们创建一个对象并访问它的属性:
// 创建一个对象
var obj = {
name: 'Alice'
};
// 访问属性
console.log(obj.name); // 'Alice'
console.log(obj.age); // undefined
当我们访问 obj
的属性时,如果属性存在,则直接返回属性值;如果属性不存在,则沿着原型链向上查找,直到找到或者到达原型链的顶端。在这个例子中,obj
对象的原型是 Object.prototype
,因此当访问 age
属性时返回 undefined
。
6.节流和防抖
节流(throttling)和防抖(debouncing)是两种常用的优化性能的方法,特别是在处理频繁触发的事件时,比如滚动事件、resize 事件等。
-
节流(throttling):
- 节流的原理是在一定的时间间隔内只执行一次函数,即使事件被触发多次。
- 通常在事件持续触发时,将函数调用限制在指定的时间间隔内,避免频繁触发造成性能问题。
- 节流可以通过定时器实现,在每次触发事件时设置一个定时器,在定时器到期之前不执行函数,当定时器到期时执行函数并重新设置定时器。
-
防抖(debouncing):
- 防抖的原理是在一定的时间间隔内只执行一次函数,但是函数执行的时机是在事件停止触发之后。
- 当事件持续触发时,函数不会立即执行,而是等待一段时间(比如 100ms),如果在这段时间内事件没有再次触发,则执行函数;如果事件再次触发,则重新等待一段时间。
- 防抖可以通过定时器实现,在事件触发时设置一个定时器,在定时器到期之后执行函数,如果在定时器到期之前再次触发事件,则重新设置定时器。
代码示例
// 节流函数 function throttle(func, delay) { let timer = null; return function() { if (!timer) { timer = setTimeout(() => { func.apply(this, arguments); timer = null; }, delay); } }; } // 防抖函数 function debounce(func, delay) { let timer = null; return function() { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, arguments); }, delay); }; }
7.XSS和CSRF
XSS(Cross-Site Scripting)和 CSRF(Cross-Site Request Forgery)是两种Web应用程序中常见的安全漏洞,它们都可能导致恶意攻击者对用户数据进行窃取或篡改。
- XSS(跨站脚本攻击):
- XSS 攻击是一种通过在受信任的网站上注入恶意脚本代码,然后让用户在浏览器中执行这些恶意脚本的攻击方式。
- 攻击者可以在受害者的浏览器中执行恶意脚本,盗取用户的 Cookie、SessionID 等敏感信息,或者在用户登录状态下执行一些恶意操作,如发送恶意请求、篡改页面内容等。
- XSS 攻击分为三种类型:存储型(存储在服务器端,如评论区)、反射型(恶意脚本作为 URL 参数发送到服务端,服务端返回并执行)和 DOM 型(恶意脚本通过 URL 参数等方式直接执行在客户端)。
- CSRF(跨站请求伪造):
- CSRF 攻击是一种利用用户已登录的身份在用户不知情的情况下,以用户的名义执行非预期的操作的攻击方式。
- 攻击者通常会诱使用户点击一个恶意链接,该链接会触发一个向目标网站发送请求的操作,而用户在点击链接后,已经登录的状态下会自动携带上自己的身份凭证,导致服务器无法区分是否是合法用户的请求。
- 攻击者利用 CSRF 漏洞可以执行一系列恶意操作,比如在用户账户下进行转账、更改密码、发送消息等。
防御 XSS 和 CSRF 攻击的常用方法包括:
- XSS 防御:对用户输入和输出进行严格的过滤和转义,不信任的内容不要直接插入到页面上,使用 Content Security Policy(CSP)等安全策略来限制页面中脚本的执行。
- CSRF 防御:使用 CSRF Token 来防御 CSRF 攻击,服务器在返回页面时,向页面中插入一个随机的 Token,并要求在提交表单或者执行敏感操作时必须携带这个 Token,以确认请求的合法性。同时,使用同源策略、使用验证码、检查 Referer 头等方法也能够一定程度上减轻 CSRF 攻击的风险。