公众号:程序员白特,可加前端技术交流群
前言
这是某211高校软件工程专业的师弟百度一面的题目和回答,全程高能,来看看你会多少~
宇宙铁律,介绍下自己
还好,之前看到过敖丙大佬面试自我介绍5句话公式 - 掘金 (juejin.cn)一文,里面有个公式:我是谁+从哪里来+我做过什么+有什么成绩+为什么能胜任。
下面为举例子:
面试官你好,我叫白特,22年毕业于XX大学物联网工程专业。之前任职于XX公司,担任前端开发工程师一职,在职期间主要负责金融活动相关项目的研发,对线上问题处理、性能调优、线程并发能问题都有自己的理解,对行业相关业务的研发设计流程也十分熟悉。
除此之外,也仔细读过多本源码研发书籍(最好列举熟悉的一两个,面试官就可以因此提问),并在公众号上写过多篇底层相关系列文章(可以举例子,为面试提问铺垫),热爱技术分享,多平台优秀作者。平时主要使用vue, 熟悉全家桶开发,数据流按理及后台管理系统等等开发。阅读过vue、react、ElementPlus等的源码,并且多次向开源组件库贡献过源码,同时也有自己的开源组件库betterUI,star有xxx+。
因贵司XX部门的研发十分吸引我,并且和我的技能树十分匹配,因此决定面试咱们公司的XX前端研发岗位,希望能获得此次机会,谢谢。
面试题
1. var a = []; a.push()、a.pop() 请说明在js中的意义
-
使用的是字面量初始化创建了一个空数组,这是最简洁、直观且推荐的写法。这种方法直接在内存中创建并初始化数组对象,执行效率高。
var a = new Array()
; 使用了Array构造函数来创建一个空数组。尽管功能上等同于前者,但性能上有差异。 -
如果传递整数参数,比如
new Array(3)
, 指定长度
比如('a', 'b', 'c')
作为元素初始化。
还有var arr = new Array(5).fill(0);
也经常用到。
通过原型链查找,Array
原型上的push
等方法
面试官又追问, var a = new A(),a 和 A 的关系,A 和 Function 的关系
a 是 A 的一个实例对象,a 的
__proto__
指向 A 的prototype
,A 的__proto__
指向Function
的prototype
。
2.promise有几种状态,可以重复改变吗?
Promise
对象有三种状态,分别是:
Pending
(未决)初始状态Fulfilled
(已履行/成功):操作成功完成时的状态Rejected
(已拒绝/失败):操作因错误或异常未能完成时的状态
Promise
状态变化的特性是:
Promise
状态的转变是不可逆且只能发生一次。也就是说,一个Promise
不能从Fulfilled
状态变回Pending
状态,也不能从Rejected
状态变为Pending
或者Fulfilled
状态。
一旦Promise
从Pending
状态变为Fulfilled(resolved)
或Rejected(rejected)
,它就永远不会再改变。
因此,Promise
的状态不能重复改变。
感觉回答的还可以,记得去字节的会长提醒面试时,切忌一个字一个字的蹦面试官。面试不只是回答问题,而是要展示自己。抓住熟悉的知识或技能点,就来个滔滔不绝,面试官不喊停就一直说。于是想到下面这个API
:
Promise.resolve()
与Promise.reject()
用于创建已确定状态的Promise
对象,方便快速返回成功的或失败的结果
面试官一听这个就感兴趣了, 继续提问Promise
还提供了哪些静态方法,平时怎么用的?开心, 面试官进包围圈了。
Promise.all(iterable)
参数是promise
对象数组。只有当所有Promise
都变为fulfilled
时,返回的Promise
才会变为fulfilled
,并且结果是一个包含所有Promise
结果的数组;只要有一个Promise
变为rejected
,则整体Promise
也会立即变为rejected
,返回第一个rejected Promise
的理由。
Promise.race(iterable)
在传入的 Promise
数组中任何一个 Promise
解决(resolve
)或拒绝(reject
)时,会立即以那个率先改变状态的 Promise
的结果为准来解决或拒绝。
这里强调下细节,其它的promise
实例仍然会继续运行,只不过其状态和结果不会归于最终的结果。
Promise.race
关注的是速度最快的 Promise
的结果,而 Promise.all
关注的是所有 Promise
是否都成功完成。
Promise.allSettled(iterable)
和Promise.all()
相似,它等待所有Promise
都达到settled
状态(即无论是fulfilled
还是rejected
)。一旦所有Promise
都决断了,返回的Promise
会变成fulfilled
,并且结果是一个数组,包含了每个输入Promise
的结果描述对象,这些对象具有status
('fulfilled'
或'rejected'
)和对应的value
或reason
属性。
Promise.all()
更关注所有 Promise
是否都成功完成,它适用于需要所有任务成功完成才能继续下一步场景。而 Promise.allSettled()
则允许你观察一组异步操作的所有结果,无论成功与否,这对于获取并处理所有任务的最终状态非常有用。
到此,在三种状态的语法回答外,还将并发任务及sellted
等偏业余的需求表演给面试官,看表情他还是挺满意的… 如果面试官不打断,我准备继续聊异步、红绿灯、手写promise
… 面试就要变被动为主动,将自己擅长的表演出来。
3.输出结果,为什么?
const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false
结果:
false,false
原因:
- == 值相等, ===严格相等,即值和类型都相等
- 类型转化,虽然
obj3
和obj4
值不一样,但面试官要听的是我们对于JS
类型转换的理解。
在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:
如果由一个操作数是布尔值,则在比较相等性之前先将其转换为数值—false转换为0,而true转换为1;
如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较;
这两个操作符在进行比较时则要遵循下列规则。
null和undefined是相等的。
要比较相等性之前,不能将null和undefined转换成其他任何值。
如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符则返回true。重要提示:即使两个操作数都是NaN,相等操作符也返回false;因为按照规则,NaN不等于NaN。
如果两个操作数都是对象,则比较他们是不是同一个对象。如果两个操作数都是指向同一个对象,则相等放回true,否则返回false。
所以这里不会转换两个对象,而是比较他们是不是指向同一个地址,是否是同一个对象。
- ===严格相等,这里比较的也是引用地址。
所以,在比较两个对象时,并不会发生类型转换以试图使它们相等。相等性判断直接基于对象的内存地址。
4. 聊聊你对语义化的理解
会长提醒过我,面试前要了解下公司的产品和技术栈什么的。百度是搜索公司,前端和搜索关联的肯定是HTML5
的语义化了,SSR
后面的题目会讲到。当然针对百度, 我也准备了一些AI
相关的内容,后面果然被问到了。
- html5 新增了语义化标签
header、nav、section、aside、footer、aside、article
一边讲标签, 一边把脑子里的这张图(掘金详情页)讲给面试官。这样说比干巴巴讲单词,好太多。
div + css
能解决布局问题,但是可读性不好,代码不好维护SEO
搜索引擎优化
更好地支持各种终端,例如无障碍阅读和有声小说等
斐波那契
哟,面试官要使出闪电五连鞭了, 出算法题了。斐波那契或爬楼梯,始于递归,终于动态规划。还好我有准备,引着面试官往我的dp
包围圈里走,大家往下看。
- 使用递归,快速解决战斗
function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};
- 有什么可以优化的地方
时间复杂度是O(n^2)
,而且递归(自顶向下)的过程中有很多重复计算,我们可以缓存,当然这里可以一步到位,用动态规划(自下向上,状态转移方程),将时间复杂度降到O(n)
,代码又有了。
function fibonacci(n) {
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契数列的结果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 计算第i个斐波那契数
}
return fib[n];
}
面试时间大概来到了25分钟左右,前面的JS
基础面试官还比较满意。但是后面还有十几位候选人要面,貌美如花的女朋友约了晚上一起吃饭,算法是杀招。不会就挂电话不浪费时间,表现好的话就在小本本上记下明天通知二面。于是,面试官来了道动态规划难点的题。
6.leetcode 原题编辑距离
给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数。你可以对一个单词进行如下三种操作:
(1) 插入一个字符;
(2) 删除一个字符;
(3) 替换一个字符;
首先,这道动态规划题有些复杂,大家需要刷一些基础的动态规划题再来搞这道。像百度等这种级别的大厂,算法题我称为三板斧。一般是1-2道简单或中等题,再是1-2道有难度的动态规划题。只要我们准备好动态规划由简到难的各种常考题型,扛住面试官的前三板斧子,基本没有问题。
word1 horse
word2 ros
1. word1 变
horse -> rorse h->r 替换
rorse -> rose 删除最2个r
rose-> ros 删除最后e
操作次数是3次 由word1 变成word2
再换个思考 由word2变成word1也是可以的
2. word2 变
ros -> rose 添加e
rose->rorse 添加r
rorse-> horse r 替换为h
其实就是上面的逆向操作
3. 都变
horse -> rorse h-> r word1 变
rorse -> rose 删除r word1 变
ros -> rose word加e word2 变
word1 和word2 都操作了, 一共操作了3次
分析demo,方便等下我们考虑最优子结构的各种情况
天啊, 太复杂了,替换、删除、添加三种操作、两个单词混合修改。别怕,最值问题求解,就用动态规划。看上去复杂的dp
问题,使用动规五部曲就能化腐朽为神奇。
动规五部曲
- 定义
dp
数组
比较两个字符串,那么我们要定义一个二维的dp[i][j]
,两个字符串的最少操作次数(无论哪个操作字符,哪种操作,都可以用二维矩阵涵盖)
以i-1
结尾的word1
,以j-1
结尾的word2
dp[i][j]
就是让两个字符相同的最少操作次数,根据动态规划的局部最优亦是全局最优,最后的dp[m][n]
就是我们要的结果
- 递推公式
我们要比较两个字符串,就要比较每个元素,那么要比较哪些元素呢?
word1[i-1]
、word2[j-1]
递推,自顶向下思考
// 如果两个字符相同
if (word1[i-1] == word2[j-1]) {
// 不需要添加、删除、替换元素
// 最少操作次数可以是不改变的,取上一次的最少操作数,因为dp局部最优也是全局最优的
dp[i][j] = dp[i-1][j-1]
} else {
// 上操作
// dp[i][j] = dp[i - 1][j - 1] + 1, // 替换操作
// dp[i][j] = dp[i][j - 1] + 1, // 插入操作
//dp[i - 1][j] + 1)// 删除操作
dp[i][j] = Math.min(
dp[i - 1][j - 1] + 1, // 替换操作
Math.min(dp[i][j - 1] + 1, // 插入操作
dp[i - 1][j] + 1) // 删除操作
);
// 总之, 根据动态规划的思想,只要有一步操作就可以到达
}
- 初始化
怎么来考虑初始化问题呢? 上图dp[i][j]
会由左上角的dp[i-1][j-1]
、左边dp[i-1][j]
、上边dp[i][j-1]
三个方向迭代而来。所以在初始化的时候,我们需要把第一行dp[]
和第一列都初始化,这样就可以递推出相应的值。
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
- 迭代
//自底向上, 迭代
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
.....
}
}
- 返回结果
最后的dp[m][n]
也就是二维矩阵的右下角。通过动态规划五步走,代码如下:
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
function minDistance(word1, word2) {
const m = word1.length;
const n = word2.length;
// 初始化一个(m+1) x (n+1)的矩阵,第一行和第一列分别表示空串到word1前i个字符、空串到word2前j个字符的距离
const dp = new Array(m + 1).fill(null).map(() => new Array(n + 1).fill(0));
// 初始化边界条件:空字符串转换成任意长度的字符串至少需要该字符串的长度次操作
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 动态规划填充矩阵
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]; // 如果两个字符相等,则不需要消耗操作次数
} else {
dp[i][j] = Math.min(
dp[i - 1][j - 1] + 1, // 替换操作
Math.min(dp[i][j - 1] + 1, // 插入操作
dp[i - 1][j] + 1) // 删除操作
);
}
}
}
return dp[m][n]; // 最终答案位于dp数组右下角
}
7. 如何用AI工具 提升开发效率
-
之前申请了
Github
的Copilot
,学生党免费嘛。对代码提速和源码学习都有挺大帮助的。也试过下通义千问的vscode
插件,挺好的。 -
使用各种
chat bot
(Chatgpt
等),从前端到后端、AI
学习、数据库等,提问式学习及解决问题,拥抱AI Native
。 -
刻意练习一些
prompt
的技巧, 生成前端页面、SQL
等
比如在做后台管理系统的时候,tailwind
的一些页面,基本都是chat
完成。 -
学习
transfromer
、openai
等AIGC
类技能,将一部分的编程任务交给Agent
来完成,发挥大模型的能力。最近在学习LangChain
, 对AI
很感兴趣。
面试官好像突然来了兴趣,哐哐问了我一堆扩散模型、pandas
,越听越像鸟语,真不会啊。这时想起来,会长的分享,面试里有不会的很正常,可以适当的请教面试官。果不其然,在我很有礼貌的说出请教后,面试官给我讲了一通,我瞬间表示学到很多,希望跟着他多学习,感觉面试官有种突然想收我的感觉。
之前有朋友说,会一点的东西不要写到简历上。但也觉得,AI如此火的今年春招,建议把自己会的都加上。面试被追问不可怕, 新的知识表现出学习兴奋和激情,再适当的拍拍面试官的马屁,可以让面试官有更深的印象。
8. 讲一讲HTTP请求三次握手
推荐大家看下关于三次握手与四次挥手面试官想考我们什么?— 不看后悔系列 - 掘金 (juejin.cn) ,我就按这个来回答的。
不出意外,面试官继续追问 为什么是三次握手,不是两次或者四次
三次握手是确定客户端和服务端接收和发送能力都正常(HTTP)的最优次数
第一次:客户端发送能力正常
第二次:服务端接收能力正常,服务端发送能力正常(接收和发送可以合并)
第三次:客户端接收能力正常
9. 说一说跨域
新手在实习的时候,git
操作和 跨域(前后端联调)是两个动手的要点。
首先,跨域问题源于浏览器的同源策略,该策略限制了从一个源加载的网页脚本如何与另一个源交互。
jsonp
(JSON with Padding
)
利用 <script>
标签不受同源策略限制,通过动态创建 src
属性指向服务端提供的接口,并带上回调函数名,服务端返回调用这个回调函数并携带数据的JS
代码。
WebSocket API
WebSocket
协议本身支持跨域,通过握手过程中服务器发送正确的Access-Control-Allow-Origin
来实现跨域通信。
POSTMessage API
主要用于窗口间通信,比如iframe
和父页面间的跨域通信,通过window.postMessage()
方法发送消息,同时监听message
事件接收消息。
Image Beacon
利用图片请求可以跨域的特点,通过创建Image
对象并向其src
属性设置跨域URL
进行GET
方式的数据传输。不适用于POST
或复杂操作。
CORS(Cross-Origin Resource Sharing)
跟其它的方案不一样的地方,它单独在服务器设置。
服务器端设置允许跨域请求头(如Access-Control-Allow-Origin
),浏览器在发起跨域请求时会检查响应头是否允许本次请求。
//具体代码
app.use((req, res, next) => {
// 设置允许任何来源进行跨域访问(生产环境请替换为具体的源)
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
// 允许浏览器预检请求(OPTIONS)通过
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 允许携带认证信息(如cookies)
res.setHeader('Access-Control-Allow-Credentials', true);
// 允许自定义请求头
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
// 对于预检请求,直接返回成功状态码
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
10. vite 为什么比webpack快?
面试来到了四十几分钟,面试官开始问工程化相关的问题了。之前会长在给我做模拟面试的时候,有问到工程化的问题。当时一脸懵逼,会长给我讲了一遍,还交给我在这种问题上延申表达,展示自己在工程化深度的一些方法。舒服,完美表演二十分钟,一小时左右的面试应该会完美收官。
越想越开心, 赶快收拾下内心的荡漾,组织语言,回答面试官。
我们先不要急于回答问题本身,会长建议我多想想面试官通过面试题想考察我们哪些点。一场前端面试无非包含前端基础(
js/css/html5/es6
)、算法(排序、动态规划)、项目(vue / node
)、计算机基础(计网/数据库/数据结构/操作系统)、工程化(webpack/vite
)等。所以像工程化这种,在拿到题目后,尽量把这块的知识,以题目为中心全盘托出给面试题。一来展示了自己对这块知识点的理解广度和深度,二来不经意延长了优质面试时长,面试官会更有兴趣随时打断我们,“参与”进来。殊不知,面试官一参与进来,就又进入到我的包围圈,他会问什么问题,怎么问,就是我们预先准备好的。 所以, 建议大家多做做模拟面试,自己当面试官,会有利于这一技巧的把握。
- 工程化、历史及现状
在开发前端项目的时候,工程化工具是标配。比如项目的初始化,让我们快速开始业务开发;模块化的支持,方便组织和复用代码;各种资源的处理和加载,如css
、图片、字体等,并将其压缩或优化后放入最后的代码包;各种loader
和plugin
,按需定制编译流程 (stylus/ts/jsx
)、压缩(MiniCssExtractPlugin
);热更新等
Webpack
和Vite
等构建工具旨在解决前端开发中的复杂性和规模问题,通过自动化处理、模块化管理、性能优化等手段极大地提高了开发效率和应用性能,现代前端开发实践中不可或缺的部分,即前端工程化。
之前,webpack
是主流,但相对复杂,有点慢。vite
非常快,更简单,有种取代webpack
地位之势。
bundle
与bundless
webpack
: 一切皆可打包,是目前使用率最高的工程化框架,帮助我们打理代码调试到打包的全过程,但是也有一些缺点:
webpack
在项目调试之前,要把所有文件的依赖关系收集完,打包后才能运行,这是它慢的主要原因,随着项目规模(强调,新手有这个视野很nice)的激增,慢的一坨屎一样(数分钟)。于是,针对webpack
的bundle
思路,社区推出了bundless
思路框架:Vite
。
从bundle
到bundless
,原因是浏览器里的JavaScript
没有很好的方式去引入其他文件。Webpack
(node
环境运行) 只能通过require
(commonJS
) 将一堆js 按依赖关系组织起来,打包后运行。但是现在主流浏览器都支持ES6
的module
功能(import/export
)。
<script type="module" src="/src/main.js"></script>
只要在script
标签上添加type="module"
标记, main.js
就可以直接使用import
语法(动态导入)去引入js
文件,所以可以不用webpack(node)
打包功能,直接使用浏览器的module
功能就可以组织我们的代码。
Vite
能够做到这么快的原因,还有一部分是因为使用了 esbuild
去解析 JavaScript
文件。esbuild
是一个用 Go
语言实现的 JavaScript
打包器,支持 JavaScript
和 TypeScript
语法,现在前端工程化领域的工具也越来越多地使用 Go
和 Rust
等更高效的语言书写,这也是性能优化的一个方向。
到这里,我们就向面试官讲清楚了 bundle
和bundless
。这才是vite
更快的关键。
- 优缺点
上面有太多东西可以说了,接下来我们再怎么扩展工程化这块的表达,主动把握优质面试时长,让面试官感到惊艳呢?这一方面是面试技巧,一方面也是检测面试准备充分程度。大家可以按这个列表扩展:
webpack.config.js
中entry
,output
,module.rules
,plugins
等关键配置项的作用及其具体用法- 代码分割、
Tree Shaking
和懒加载等技术 Vite
开发时,如何处理CSS
预处理器- 手写
Vite
- 热更新机制的理解
总结
百度的面试还是比较专业的,能学到很多东西。以下是本次面试总结的技巧:
- 找学长内推,会比自己投好很多
- 吸星大法,问下学长面试经验,社区里多刷题
- 模拟面试,找些伙伴,假设自己是面试官, 你会问伙伴哪些问题,这样来准备
- 自我介绍最好写出来,多朗诵
js
基础要准备好promise
系列、JS
语法系列、ES6
系列,面试不是被考试,而是当面展示自己。 滔滔不绝,相关联知识不停说。让面试官觉得我们基础扎实,对JS
有激情- 算法准备好动态规划由简到难,常考题一定要刷刷,状态转移方程可以计下来。
不要怕回答不上来,请教面试官,快速学习。