前言
搜集了牛客、掘金、CSDN等网上各种前端开发实习的大厂面经题目,总结了答案,供自己复习使用,欢迎参考借鉴。答案来源:chatGPT + Google
HTML + CSS
对重排和重绘的理解,如何优化渲染,项目中怎么入手(携程一面)
当某个元素的样式(比如尺寸、位置、颜色等)发生变化时,浏览器需要重新计算元素的位置和大小(重排),然后将元素绘制在屏幕上(重绘)。重排和重绘是比较耗费资源的操作,会降低 Web 应用程序的性能和用户体验。
在项目中,可以从以下几个方面入手:
- 优化 HTML 和 CSS:尽可能减少不必要的 DOM 操作、使用 CSS3 特性代替 JavaScript 实现动画效果、使用 CSS Sprite 等方式来优化页面加载速度。
- 使用前端框架:前端框架通常具有优化渲染的功能,例如 React 中的虚拟 DOM 可以减少不必要的 DOM 操作。
- 使用懒加载技术:在某些情况下,页面中可能包含大量的图片或其他资源,这会导致页面加载速度缓慢。可以使用懒加载技术来延迟加载这些资源,只有在用户需要访问时才加载。例如,可以将图片的 src 属性设置为一个空字符串,在用户滚动到图片所在的位置时再将 src 属性设置为真实的图片地址,从而实现延迟加载。
- 使用 Web Worker:Web Worker 可以将一些计算密集型的任务交给后台线程进行处理,以避免阻塞页面的渲染。(不主动提这一点)
flex 布局的理解,容器和项目的宽高影响等(云网络一面、小米一面、快手二面、字节二面)
怎么实现响应式?为什么要有 rem ?(快手一面、小米一面)
- 媒体查询
- em/rem
- 百分比布局
- 视口单位 vh/vw
百分比布局(iPad横屏被拉的很长)->媒体查询(比较复杂,需要设计多种方案)->rem布局(主流解决方案Flexible)->vh/vw
行内元素和块元素区别,对应的盒模型怎样,为什么要有这样的区分 (快手二面)
HTML中的元素通常可以分为两种类型:行内元素和块元素。它们之间的主要区别在于:
- 盒模型不同: 块元素通常具有自己的盒模型,即它们会生成一个矩形的盒子,包含了它们所包含的所有内容,如文本、图片、其他元素等。而行内元素则不会生成自己的盒子,它们的大小和位置通常由其内容来决定。
- 定位不同: 块元素通常是“块级元素”,它们会在文档中生成一个独立的块,并占据整个可用宽度,每个块级元素通常会单独占一行。而行内元素则通常是“行内元素”,它们会在文本流中生成一个行内盒子,并且只占用所需的宽度。
- 内容格式不同: 块元素通常包含一个块状结构的内容,如段落、标题、列表、表格等,而行内元素通常包含一个行内的内容,如文本、链接、图片等。
为什么要有这样的区分呢?主要是为了更好地控制网页的布局和排版。通过将元素分为不同的类型,可以更精确地控制元素的位置、大小和排列方式,从而实现更灵活、美观的页面布局。
同时,不同的元素类型也有不同的语义和功能,它们通常用于不同的场合和目的。例如,块级元素通常用于组织页面结构和内容,如段落、标题、导航栏等,而行内元素则通常用于标记文本、链接、图片等。
常用的块级元素包括:
<div>:通常用于分组或布局,并且可以使用CSS样式来控制其大小、颜色、边框、内边距等属性。<p>:表示一个段落,通常用于显示文本内容。<h1>~<h6>:表示标题,从大到小分别表示一级标题到六级标题,用于对文本内容进行层次化组织。<ul>和<ol>:表示无序列表和有序列表,通常用于显示项目列表。
常用的行内元素包括:
<a>:表示超链接,用于跳转到另一个页面或定位到当前页面中的某个位置。<span>:通常用于对文本内容进行样式化,如文字颜色、字体大小、加粗、斜体等。<img>:表示图片,可以使用src属性指定图片的路径。<input>:表示输入框,可以用于输入文本、选择日期等。<label>:表示表单标签,通常与<input>元素结合使用。
JavaScript
null和undefined的区别/ == 类型转换规则 (百度一面、快手二面、美团一面)
- undefined 表示一个未定义的值,即该变量或属性尚未被声明或赋值。
- null 表示一个空值,即该变量或属性已经被定义了,但它的值为空。
- 这两个值用 == 比较是相同的,用===比较是不同的
-
对于 NaN来说 比较都是false 即使是NaN==NaN也一样
-
Number强制转换 {}空对象 返回NaN,因为它会先转换成字符串'[object Object]' 里面有'[ ]'括号字符串,无法被转换成数字,再转换成数字比较,会变成NaN。
-
Number强制转换 [] 空对象 返回0,因为它会先转换成空字符串'',然后再转换成数字变成0。对于空字符串'' 和空格字符串' ' 都是转换成0
-
其他类型到字符串,其实是调用toString()方法,其他类型转换到数字,先调用valueOf,如果调用valueOf没获取到基本类型,再调用toString获取基本类型。如果都获取不到就报错TypeError
-
因此 Number([])会调用toString 变成 ''空字符串进而转换成0,但是Number([1,2,3])就会先变成'1,2,3'用逗号分隔的字符串,再变成NaN
-
对于对象,先调用valueOf方法,没有就调用toString,重写出来的toString基本都会变成'[object Object]',因此都会是NaN。
说说事件循环,对宏任务和微任务的了解,为什么要分宏任务和微任务 (携程二面)
事件循环是指在浏览器或Node.js的运行环境中,用于处理异步任务的机制。当异步任务完成后,将被推入任务队列中,然后事件循环将不断从任务队列中取出任务,执行它们,并等待新任务的到来。
在事件循环中,我们通常将任务分为两种类型:宏任务(macro-task)和微任务(micro-task)。宏任务是一些较为耗时的任务,例如DOM操作、定时器、I/O操作等。而微任务是一些相对简单和快速的任务,例如Promise回调、MutationObserver回调等。
宏任务是由浏览器提供的一些任务,比如setTimeout、setInterval、requestAnimationFrame等等。这些任务会被放到宏任务队列中等待执行。
微任务是由开发者自己创建的任务,比如Promise、MutationObserver等等。这些任务会被放到微任务队列中等待执行。
宏任务和微任务的主要区别在于它们的执行顺序和优先级。事件循环会先执行所有的微任务,然后再执行宏任务。这是因为微任务的执行时间很短,通常只需要几个微秒到几毫秒,而宏任务则需要更长的时间,例如几十毫秒甚至几秒钟。如果在执行宏任务时,突然有一个微任务到来,那么它将会被立即执行,而不是等待当前宏任务执行完毕再执行。
分宏任务和微任务的目的是为了更好地控制异步任务的执行顺序和优先级。如果没有分宏任务和微任务,那么所有的异步任务都将被视为同一类型,无法准确控制它们的执行顺序和优先级。通过将任务分为宏任务和微任务,我们可以更好地控制任务的执行顺序,以及更快地响应用户的操作,从而提高应用程序的性能和响应能力。此外,通过控制微任务的执行顺序和优先级,我们还可以更好地控制Promise等异步操作的执行顺序和结果,从而避免一些常见的异步问题。
注意,微任务是在当前循环中执行的,宏任务是在每次循环开始的时候执行的。中间有一个检查是否需要渲染的过程。
每个宏任务都有一个与之关联的微任务队列。
在事件循环中,每次执行完一个宏任务后,会先清空该宏任务关联的微任务队列,然后再去执行下一个宏任务。这是因为微任务的执行优先级比宏任务高,所以需要先清空微任务队列中的任务,再去执行下一个宏任务。
例如,在执行一个定时器任务时,如果在定时器回调函数中创建了一个Promise对象并将其添加到微任务队列中,那么这个Promise任务会在执行完当前定时器任务后立即执行。如果该定时器任务还有其他任务(例如setTimeout),则这些任务会排队等待,直到微任务队列中的所有任务都被执行完毕。
因此,每个宏任务都有一个与之关联的微任务队列,以确保在执行宏任务之前,先处理完与之关联的微任务队列中的所有任务。
浏览器中的宏任务队列不止一个,通常情况下会有多个宏任务队列。常见的宏任务队列包括:
- setTimeout/setInterval队列:包含由setTimeout和setInterval创建的任务。
- I/O队列:包含所有的I/O事件,如鼠标点击、键盘输入等事件。
- requestAnimationFrame队列:包含由requestAnimationFrame创建的任务。
- UI渲染队列:包含与UI渲染相关的任务,例如DOM操作、重绘等。
这些宏任务队列之间有优先级的区别,例如UI渲染队列的优先级高于setTimeout/setInterval队列,因为UI渲染是浏览器最重要的任务之一。事件循环会根据这些队列的优先级,优先执行优先级高的任务,以提高浏览器的性能和响应速度。
需要注意的是,不同浏览器的实现可能存在一些差异,但是宏任务队列的概念和作用是相似的。
fetch API和 axios, XHR的区别
XHR(XMLHttpRequest)和fetch API都是用于在JavaScript中发起网络请求的技术,它们有以下几个区别:
- 语法和用法
XHR使用比较繁琐,需要通过创建XMLHttpRequest对象、设置请求参数、发送请求、监听请求状态等多个步骤来完成一个网络请求,如下所示:
javascriptCopy code
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var response = xhr.responseText;
console.log(response);
}
};
xhr.send();
而fetch API则更加简洁和直观,只需要使用fetch()方法发起一个请求,并使用then()方法处理响应数据,如下所示:
javascriptCopy code
fetch('http://example.com/api/data')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error(error));
- 返回值类型
XHR可以通过设置responseType属性来指定响应数据的类型,包括文本、JSON、XML、二进制等多种类型。而fetch API只能返回一个Response对象,需要使用Response对象的方法(如text()、json()、blob()等)将响应数据转换为相应的格式。
- 请求/响应处理
XHR提供了更为细致的请求/响应处理,可以通过设置请求头、请求体、监听请求状态等方式来实现。而fetch API只能通过设置请求头和请求体,不能直接获取请求状态,需要使用Response对象的属性和方法来实现。
- 兼容性
XHR在早期的浏览器中得到广泛支持,但在现代浏览器中被逐渐替代。fetch API是ES6中新增的技术,在较新版本的现代浏览器中得到广泛支持,但在一些旧版本的浏览器中可能不被支持。
综上所述,fetch API更加简洁和直观,而XHR提供了更为细致的请求/响应处理。选择使用哪一种技术取决于具体的需求和场景,需要根据实际情况进行选择。
Promise顺序执行的实现
JS 的异步遍历,你真的会写吗? 面试题-Promise顺序执行
浏览器原理
输入 url 到页面显示的过程(导航、解析、渲染)携程一面
- 浏览器解析Url 协议 域名 端口号
- DNS查询:浏览器查询解析出的IP地址,如果缓存中有该IP地址,则直接从缓存中获取,跳到第4步。如果没有浏览器就会发送DNS请求到本地DNS服务器中查询,本地查询根服务器,获取权威域名服务器的IP,再向权威域名服务器获取URL对应的IP地址
- TCP连接:浏览器通过TCP三次握手建立与服务器的TCP连接
- 发送HTTP请求 请求头 包括 请求方法 请求URL 请求头部
- 服务器处理请求后 发送HTTP响应
- 浏览器解析页面 DOM CSSOM 结合成渲染树 回流 重绘 展示给用户
- 断开TCP 四次挥手
怎么避免更新后使用到旧缓存,联系到 webpack 的哈希命名,contenthash 和 chunkhash 区别 (携程一面)
在使用缓存时,经常会遇到更新后使用到旧缓存的问题。为了解决这个问题,可以使用哈希命名来保证新的资源会生成新的缓存。Webpack 中提供了三种哈希命名方式,分别是 hash、chunkhash 和 contenthash。
- hash:根据整个应用程序的构建情况生成的唯一哈希值,即每次构建应用程序时生成的哈希值都是不同的。
- chunkhash:根据每个 chunk 内容的修改情况生成的唯一哈希值,即每个 chunk 的哈希值都是不同的。这个哈希值是基于所包含的模块的内容计算而来的。
- contenthash:根据每个模块的内容生成的唯一哈希值,即每个模块的哈希值都是不同的。这个哈希值是基于模块的内容计算而来的。
区别如下:
- hash:是针对整个应用程序的构建过程进行哈希计算的,因此所有的文件都共用同一个哈希值。这意味着,当有一个文件变化时,所有的文件的哈希值都会发生变化,从而导致浏览器重新下载所有文件。
- chunkhash:是针对每个 chunk 的内容进行哈希计算的,因此同一 chunk 中的所有文件共用同一个哈希值。这意味着,只有当同一 chunk 中的文件发生变化时,才会重新下载这个 chunk。
- contenthash:是针对每个文件的内容进行哈希计算的,因此每个文件都有一个唯一的哈希值。这意味着,只有当该文件的内容发生变化时,才会重新下载这个文件。
- 因此,建议使用 contenthash 来命名文件,以便在文件内容更改时重新生成文件名,从而避免更新后使用到旧缓存的问题。这样可以有效地利用浏览器的缓存机制,提高网站的加载速度。
跨域问题,什么是同源策略,怎么跨域,预检请求(网易一面、百度一面、钉钉一面、美团一面)
跨域问题其实就是浏览器的同源策略造成的。先讲同源策略:同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的安全机制。同源指的是:协议、端口号、域名必须一致。
跨域问题的解决方法:
- CORS 跨域资源共享
需要浏览器和服务器同时支持。CORS是分为简单请求和非简单请求
简单请求:
- HEAD,GET, POST三种请求方法之一
- 请求头不超过以下几种字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
- 不满足以上条件就是非简单请求
对于简单请求,浏览器会自动发送cors请求,在请求的头信息中增加一个origin字段,说明本次请求来自哪个源,浏览器检查之后,如果检查通过了,就会在响应头添加一个 ACAO, access-control-allow-origin
Access-Control-Allow-Origin: http://api.bob.com // 和Orign一直
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8 // 表示文档类型
非简单请求是对服务器有特殊要求的请求,比如请求方法为DELETE或者PUT等。非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。
预检请求使用的请求方法是OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法。
- Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
服务器在收到浏览器的预检请求之后,如果同意了,就会在响应的响应头添加ACAO,除此之外还有ACAM和ACAH,前者表示允许的方法,后者表示允许的请求头。
'Access-Control-Allow-Origin'
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'
减少OPTIONS请求次数:
OPTIONS请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少OPTIONS请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。
CORS中Cookie相关问题:
在CORS请求中,如果想要传递Cookie,就要满足以下三个条件:
- 在请求中设置
withCredentials
默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
复制代码
-
Access-Control-Allow-Credentials 设置为 true
-
Access-Control-Allow-Origin 设置为非
*
计算机网络
场景题
一万条数据渲染到页面上,怎么保持流畅(携程一面)
- 分页:将数据分成多个页面,每次只渲染当前页面的数据,减少渲染时间和内存消耗。
- 懒加载:当页面滚动到某个位置时,再加载该位置之后的数据,减少初始渲染时间和内存消耗。
- 虚拟列表:只渲染可视区域的数据,其他数据在需要时才进行渲染,减少内存消耗和渲染时间。
- 数据缓存:当数据不需要实时更新时,可以将数据缓存到本地,每次只需要加载部分数据,减少数据请求和渲染时间。
- 使用 Web Worker:将数据处理和渲染放在 Web Worker 中,减轻主线程的负担,提高页面渲染的流畅度。
- 虚拟 DOM:采用虚拟 DOM 技术,将数据操作和渲染分离,通过比较新旧虚拟 DOM 差异来更新页面,减少重绘和重排次数,提高页面渲染的性能。
回答的时候重点提一下前两点,后面的看情况再说。
Pagination分页/ant Design是怎么实现的
scroll事件滚动懒加载/小程序是如何实现的
getNewData (el) {
let height = el.target.scrollHeight - el.target.scrollTop - el.target.clientHeight //滚动条距离底部的距离 scrollHeight是整个可滚动的高度,scrollTop是滚动条距离顶部的高度,clientHeight是div的可视高度
// console.log('height', height)
if(height < 10){ //当滚动条距离底部距离小于10时判断要不要加载新数据
this.page++
if(this.totalPage > this.page){ //在加载完最后一页的数据时停止加载
this.sendWorker() //调数据的方法,在此省略
}
}
}
虚拟列表(携程一面)
虚拟列表(Virtual List)是一种前端优化技术,可以在处理大量数据时提高页面的渲染性能和用户体验。虚拟列表只渲染可见区域的数据,对于不可见区域的数据,只有在需要时才进行渲染,从而减少了页面的渲染时间和内存消耗。
虚拟列表的原理是:根据列表的高度、每行的高度、滚动条的位置等信息,计算出可见区域的起始行和结束行,然后只渲染可见区域的数据,其他数据在需要时再进行渲染。例如,一个包含 1000 条数据的列表,如果每页只显示 20 条数据,那么每次只需要渲染当前页的数据,而不需要渲染全部数据,可以大大减少页面的渲染时间和内存消耗。
在 JavaScript 中,可以通过以下步骤来实现虚拟列表:
- 计算可见区域的起始行和结束行:根据列表的高度、每行的高度、滚动条的位置等信息,计算出可见区域的起始行和结束行。
- 渲染可见区域的数据:只渲染可见区域的数据,其他数据暂时不进行渲染。
- 监听滚动条的位置:当滚动条的位置发生变化时,重新计算可见区域的起始行和结束行,并重新渲染可见区域的数据。
- 按需渲染其他数据:当用户滚动列表时,如果需要渲染其他数据,则按需渲染其他数据。
// 获取列表元素和数据
const list = document.querySelector('#list');
const data = getData();
// 计算每行的高度
const rowHeight = 30;
// 计算可见区域的起始行和结束行
let startRow = 0;
let endRow = Math.ceil(list.clientHeight / rowHeight);
// 渲染可见区域的数据
render(startRow, endRow);
// 监听滚动条的位置
list.addEventListener('scroll', () => {
// 计算可见区域的起始行和结束行
startRow = Math.floor(list.scrollTop / rowHeight);
endRow = Math.min(startRow + Math.ceil(list.clientHeight / rowHeight), data.length);
// 渲染可见区域的数据
render(startRow, endRow);
});
// 渲染指定范围内的数据
function render(start, end) {
const fragment = document.createDocumentFragment();
for (let i = start; i < end; i++) {
const item = document.createElement('div');
item.classList.add('item');
item.textContent = data[i];
fragment.appendChild(item);
}
list.innerHTML = '';
list.appendChild(fragment);
}
// 获取测试数据
function getData() {
const data = [];
搜索栏防抖如何处理用户输入不定性造成的无用请求问题(快手二面)
- 在React中 使用useRef
// 把获取推荐的函数单独列出来 用来给debounce装饰
const getSuggestions = (value:string)=>{
if (value) {
const results = fetchSuggestions(value)
setSuggestions(results)
} else {
setSuggestions([])
}
}
// useRef 提供一个可以修改的对象,对象的current属性指向传入的最初值。如果不手动修改,该值会在组件的整个生命周期保持不变。
let debounceSave = useRef(debounce(getSuggestions, 500)).current
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.trim()
setInputValue(value)
/*
debounceSave是 useRef.current 永远保持最初值 因此不会被多次创建
* */
debounceSave(value)
}
- 在React中使用useCallback
待更新....
React如何控制用户输入input框只能输入数字/其他类型的input拓展 (网易云音乐一面)
利用受控组件,和String.prototype.replace方法加上正则表达式来匹配非数字,把非数字置为空。
function Form() {
const [numVal, setNumVal] = useState('');
const handleChange = (val: string) => {
val = val.replace(/[^\d]/g, '');
setNumVal(val);
}
return (
<input type="text" value={numVal} onChange={e => handleChange(e.target.value)} />
);
}
富文本编辑器的简单实现
组件库是如何实现按需加载的
手写/算法
写个数组乱序,命令式和声明式的写法区别 (携程一面)
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
在这个函数中,我们使用了一个for循环来迭代数组中的每个元素,并生成一个随机的索引j,将当前元素和索引为j的元素交换位置。这个过程不断重复,直到所有元素都被打乱。
另一方面,数组乱序的声明式写法则是使用数组方法来创建一个新的打乱后的数组,而不是在原始数组上进行修改。例如,可以使用Array.prototype.sort()方法,将数组元素随机排序,示例代码如下:
function shuffleArray(array) {
return array.sort(() => Math.random() - 0.5);
}
命令式编程和声明式编程是两种不同的编程范式,它们在编写代码时的思考方式和实现方法都有所不同。
命令式编程是一种基于指令序列的编程范式。程序员需要指定每个步骤的具体实现方法,从而达到预期的结果。在命令式编程中,程序员需要明确控制程序的执行流程,通常使用循环、条件语句、变量等语言特性来实现。
声明式编程更加注重描述程序的行为,而不是实现细节。在声明式编程中,程序员将程序的行为描述成一系列函数调用或表达式,并使用一些组合器和高阶函数来操作数据和转换数据类型。声明式编程更强调解决问题的本质,而非实现细节。
总的来说,命令式的写法更加直观和可控,但声明式的写法更加简洁和函数式。具体选择哪种方法,可以根据实际需求和个人偏好来决定。
Vue相关面试题
React相关面试题
React 函数组件什么时候渲染
React 函数组件内定义的变量和常量,包括 useState、useRef、useEffect、useCallback 等钩子函数返回的值,都是在组件函数内执行的。
在函数组件每次执行时,这些变量和常量都会被重新定义和初始化。这就是为什么 React 函数组件中的变量不会跨越渲染之间保持其值的原因。
React 函数组件会在以下情况下重新执行:
- 组件首次渲染时;
- 组件的
props或state发生改变时; - 使用
forceUpdate方法强制重新渲染组件时。
因此,在组件重新渲染时,所有函数内的代码都会被重新执行一遍,包括变量和常量的初始化。这也是为什么 React 函数组件中的变量会多次执行的原因。
不过,需要注意的是,React 的渲染机制会尽可能地减少不必要的重复渲染,以提高性能。因此,并不是每次渲染都会执行函数组件中的所有代码,只有与渲染相关的代码才会被执行。