准35岁的前端人,2024年的面试
面试流程从2024年2月份持续到5月底(杭州、厦门、福州),面试情况:交换过简历信息的有88家,约面试的20家,面试通过有4家。
所有react相关的在另外一篇文章:react在2024年的面试总结
针对一些印象比较深的公司列举,其他的小众的就不说了
1、字节头条
这次找工作,面了字节头条两个部门,第一个部门是保险业务,第二个是数字孪生业务(这个纯粹是凑HR的KPI的,招的人不是前端的,而是类似于售前技术支持角色的,也不知道啥玩意,纯粹当炮灰的,这让我对字节这家公司的面试的需求性产生严重的怀疑)
主要问的问题是:
- 2D地图的渲染、3D地图的性能优化;
- http1.1和http2的keep-alive与多路复用的区别;
- 首屏的加载优化;
- 同一个域名并发个数限制;
- 如何实现不限制次数的编辑撤销行为(自己想到双指针的方式);
- 图表组件svg和canvas的区别;
- 用户页面操作数据的上报如果采用DOM树采集快照的缺点;
- 低代码报表平台的设计;
- 还问到如果后端给了大量的数据需要渲染的话,怎么办?我一开始回答是可以使用图表下面的DataZoom的功能实现,然后另外一种就是采样了;
笔试题:
1// 实现一个promisify函数,满足下面的调用 2const promisify = (fn) => { 3 // 这里其实就是args即可的,不用去考虑多参数的可能 4 return (args) => { 5 return new Promise((resolve, reject) => { 6 // 这样才能把callback变成这个 7 fn(args, (err, data) => { 8 if (err) { 9 return reject(err) 10 } 11 resolve(data) 12 }) 13 }) 14 } 15} 16 17const test = (path, callback) => { 18 callback(null, path) 19} 20 21const rr = promisify(test); 22rr('hellp') 23.then((data) => console.log('data is',data)) 24.catch(err => console.error(err))
2、蚂蚁
蚂蚁也是面了两个部门,均挂在二面。蚂蚁的第一次面试按照流程来的,第二次面试就有点蒙圈了,都是电话面试的,笔试题也是用的阿里的笔试系统,怀疑是瞎搞的,也是没有招聘的需求,乱面试的吧。
问的技术问题当时没有马上记录,笔试题做了这些,基本全都做出来了。
第一道题是最常见的:异步控制并发个数,在22年的时候碰到了两次;
第二道题是打平数组:
1/** [ 2 ['戴尔', '苹果', '联想'], 3 ['笔记本', '平板电脑', 'PC机', '上网本'], 4 ['黑色', '银色', '白色'], 5 ] 6 输出:['戴尔-笔记本-黑色', '戴尔-笔记本-银色', '戴尔-笔记本-白色','戴尔-平板电脑-黑色', ....] 7 */ 8// 我的解法 9const flattenArray = (sourceArr) => { 10 if (sourceArr.length <= 1) { 11 return sourceArr 12 } 13 14 const result = sourceArr.slice(1).reduce((pre, current) => { 15 const res = []; 16 for (let i = 0; i < pre.length; i +=1 ) { 17 for (let j = 0; j < current.length; j += 1) { 18 res.push(`${pre[i]}-${current[j]}`) 19 } 20 } 21 return res; 22 }, sourceArr[0]) 23 return result 24}
第三道题是异步函数的封装:
1/** 2实现这个函数: function cacheAsyncFnWrap(fn) {} 3满足如下要求: 41. 如果第一次调用,那就等待调用返回 52. 如果第二次调用,那就等待第一次调用结束之后一起返回结果 63. 如果第三次调用,已经有结果了,那么直接返回结果 7**/ 8 9// 测试代码: 10function testFn() { 11 console.log('testFn called'); 12 return new Promise((resolve) => { 13 setTimeout(() => { 14 resolve('testFnResult'); }, 5000) 15}) 16} 17const wrapedTestFn = cacheAsyncFnWrap(testFn); 18 19wrapedTestFn().then(res => { console.log('1:', res); }); 20 21setTimeout(() => { 22 wrapedTestFn().then(res => { console.log('2:', res); }); 23}, 2000); 24 25setTimeout(() => { 26 wrapedTestFn().then(res => { console.log('3:', res); }); 27 }, 6000); 28 29/** 30打印结果将是: 31testFn called 32// 过了5秒之后打印: 331: testFnResult 342: testFnResult 35// 过了6秒之后打印: 363: testFnResult 37**/ 38 39// 我的解法是: 40function cacheAsyncFnWrap(fn) { 41 let isGoing = false; 42 let hasRes = false; 43 let result; 44 let cacheAsync; 45 46 return () => { 47 return new Promise((resolve) => { 48 if (hasRes) { 49 resolve(result) 50 return; 51 } 52 if (!isGoing) { 53 isGoing = true; 54 cacheAsync = fn() 55 cacheAsync.then(data => { 56 result = data; 57 resolve(data) 58 }) 59 }else { 60 cacheAsync.then(data => resolve(data)) 61 } 62 }) 63 } 64} 65 66// 后面我觉得不够简洁,问了下GPT,果然AI给的解法很简单: 67function cacheAsyncFnWrap(fn) { 68 let result; 69 let promise; 70 71 return async function () { 72 if (!promise) { 73 promise = fn().then((res) => { 74 result = res; 75 }); 76 } 77 await promise; 78 return result; 79 }; 80}
第四道题考察递归:对给定的数组进行组合,产出所有可能的数字组合
1function permute(nums) { 2 const result = []; 3 4 function backtrack(temp, remaining) { 5 if (remaining.length === 0) { 6 result.push(temp); 7 return; 8 } 9 // 从第0位开始遍历,剩余的位数作为参数往下继续递归, 10 // temp值作为一个结果一直在递归中追加 11 for (let i = 0; i < remaining.length; i++) { 12 const newTemp = [...temp, remaining[i]]; 13 // 剩余的数字进行拆分,去除remaining[i]这个数字,剩余的重新组合成数组往下递归 14 const newRemaining = [...remaining.slice(0, i), ...remaining.slice(i + 1)]; 15 backtrack(newTemp, newRemaining); 16 } 17 } 18 19 backtrack([], nums); 20 return result; 21} 22 23// 测试 24const nums = [1, 2, 3, 4]; 25const permutations = permute(nums); 26console.log(permutations);
第五道题:简单实现洋葱模型
1// 模拟Koa框架的洋葱模型实现 2class Koa { 3 constructor() { 4 this.middleware = []; 5 } 6 7 use(fn) { 8 this.middleware.push(fn); 9 } 10 11 async run(ctx, index) { 12 if (index === this.middleware.length) return; // 所有中间件都执行完毕 13 const middleware = this.middleware[index]; 14 await middleware(ctx, () => this.run(ctx, index + 1)); 15 } 16 17 async handleRequest(ctx) { 18 await this.run(ctx, 0); 19 } 20 21 listen() { 22 const ctx = { request: {} }; 23 this.handleRequest(ctx); 24 } 25} 26 27// 创建一个Koa实例 28const app = new Koa(); 29 30// 中间件1 31app.use(async (ctx, next) => { 32 console.log('Middleware 1 - before'); 33 await next(); 34 console.log('Middleware 1 - after'); 35}); 36 37// 中间件2 38app.use(async (ctx, next) => { 39 console.log('Middleware 2 - before'); 40 await next(); 41 console.log('Middleware 2 - after'); 42}); 43 44// 中间件3 45app.use(async (ctx, next) => { 46 console.log('Middleware 3'); 47}); 48 49// 启动服务 50app.listen(); 51
第六道题:设计一个组件,实现高性能的搜索组件
1import React, { useCallback, useEffect, useRef, useState } from "react"; 2 3interface Item { 4 id: number; 5 name: string; 6 birthday: number; 7} 8 9interface IProps { 10 list: Item[]; 11 // extraFilterCondition: (data: Item[]) => Item[]; 12} 13 14const defaultEmptyArr: any[] = []; 15 16const useDebounce = <T extends Function>( 17 fn: T, 18 delay: number, 19 dep: any[] = defaultEmptyArr 20) => { 21 const { current } = useRef<{ fn: T | null; timer: NodeJS.Timeout | null }>({ 22 fn: null, 23 timer: null, 24 }); 25 26 useEffect(() => { 27 current.fn = fn; 28 }, [fn]); 29 30 return useCallback(function f(this: any, ...args: any[]) { 31 if (current.timer) { 32 clearTimeout(current.timer); 33 } 34 current.timer = setTimeout(() => { 35 current.fn?.call(this, ...args); 36 }, delay); 37 }, dep); 38}; 39 40export const Search = (props: IProps) => { 41 const { list } = props; 42 const [keyword, setKeyword] = useState(""); 43 // const isFiltering = useRef(false); 44 const [actualList, setActualList] = useState<Item[]>([]); 45 const [filterList, setFilterList] = useState<Item[]>([]); 46 47 useEffect(() => { 48 const newList = list.filter( 49 (item) => item.birthday > new Date(2024, 4, 1).getTime() 50 ); 51 52 setActualList(newList); 53 }, [list]); 54 55 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { 56 setKeyword(e.target.value); 57 }; 58 59 const handleFilter = useCallback(() => { 60 if (!keyword) { 61 setFilterList([]); 62 return; 63 } 64 65 const filterList = actualList 66 .filter((item) => item.name.includes(keyword)) 67 .sort((a, b) => a.name.localeCompare(b.name)); 68 69 setFilterList(filterList); 70 }, [keyword, actualList]); 71 72 const handleFilterCb = useDebounce(handleFilter, 500); 73 74 useEffect(() => { 75 console.log(Date.now()); 76 handleFilterCb(); 77 }, [keyword, handleFilterCb]); 78 return ( 79 <div> 80 <input 81 placeholder="请输入关键词搜索" 82 type="text" 83 onChange={handleInputChange} 84 value={keyword} 85 /> 86 <div> 87 {filterList.length ? ( 88 filterList.map((item) => <div key={item.id}>{item.name}</div>) 89 ) : keyword ? ( 90 <div>您搜索的内容为空,请重新搜索</div> 91 ) : null} 92 </div> 93 </div> 94 ); 95};
3、诸云科技(厦门)
这次找工作,也想看看福建老家的机会。
非常基础常规的面试,都是八股文式的面试,刚好,我的弱点。最后做了一道笔试题:
1// 对下面的数组进行去重,主要考察了一些基础类型的比较: 2 3const data = [ 4 3, 5 1, 6 [1], 7 1, 8 [1], 9 true, 10 /123/, 11 function func() {}, 12 true, 13 {}, 14 "1", 15 new String("a"), 16 "a", 17 NaN, 18 undefined, 19 NaN, 20 undefined, 21 {}, 22 /123/, 23 null, 24 null, 25 function func() {}, 26 new String("a"), 27]; 28 29// 自定义比较函数 30function isEqual(a, b) { 31 // 判断是否为 NaN 32 if (Number.isNaN(a) && Number.isNaN(b)) { 33 return true; 34 } 35 // 判断是否为函数 36 if (typeof a === "function" && typeof b === "function") { 37 // 如果两个函数的源码相同,则认为它们相等 38 return a.toString() === b.toString(); 39 } 40 // 判断是否为 undefined 或 null 41 if ((a == null && b == null) || (a === undefined && b === undefined)) { 42 return true; 43 } 44 // 如果a和b是对象,则转换为JSON字符串进行比较 45 if (typeof a === "object" && typeof b === "object") { 46 return JSON.stringify(a) === JSON.stringify(b); 47 } 48 49 if (a instanceof RegExp && b instanceof RegExp) { 50 return a.toString() === b.toString(); 51 } 52 // 否则直接比较 53 return a === b; 54} 55 56// 使用filter和indexOf进行去重,这个是后面去问了GPT提供的写法 57// 这个用findIndex有点意思,findIndex表示找到数组中第一个满足条件的索引 58// 如果找到并且等于自身,说明是第一个加进来的,否则后面找到的都算是重复的! 59// 很有意思哦~~ 60const uniqueData = data.filter((value, index, self) => { 61 return self.findIndex((item) => isEqual(item, value)) === index; 62});
4、厦门云行+锐捷 + 永辉 + 网龙 (均是福州的)
特别提到厦门云行这家是因为他们的面试还挺有特色的,会根据候选人的回答进行深入探究,而不是一味的八股文问题,虽然最后没通过,不过还是印象蛮深刻的
后面这三家都是福州的公司,前两家的面试都是八股文的面试,尤其永辉,但是永辉的面试流程是我目前面试过来最爽快的一家(当天面完当天给结果),网龙的面试因为老大是国外回来的,整体面试也是很舒服。锐捷就是比较老牌的传统企业,所以在二面的面试中,聊到管理上的一些方式,那个技术负责人不认同我的一些管理方式,最后二面没过。
4.1、浏览器的存储方式有哪些?
这道问题来自永辉的二面,
Web SQL Database
和Cache Storage
没有答出来,因为真实业务中基本没用,也没啥印象。
-
Cookies:Cookies是最早的浏览器存储方式之一,主要用于保存用户的会话信息、用户偏好、跟踪用户行为等。Cookies有一定的大小限制(通常不超过4KB),并且会在特定时间后过期。
-
Local Storage:Local Storage是HTML5引入的一种本地存储机制,用于存储大量数据(通常为5MB左右)且数据没有过期时间限制。Local Storage是同步存储的,即数据的读取和写入是同步操作。
-
Session Storage:Session Storage与Local Storage类似,但存储的数据只在单个会话期间有效,当会话结束(即浏览器窗口或标签页关闭)时,数据就会被清除。它的存储容量与Local Storage相同(通常为5MB左右)。
-
IndexedDB:IndexedDB是一个低级API,用于客户端存储大量结构化数据。它是一个事务型的数据库系统,可以存储更多的数据,适合复杂的应用场景,如离线Web应用。IndexedDB是异步操作的,因此不会阻塞浏览器的UI线程。
-
Web SQL Database:Web SQL Database是一种基于SQL的存储方式,可以在浏览器中使用SQL语句进行数据操作。不过,Web SQL已经被废弃,新的项目不推荐使用。
-
Cache Storage:Cache Storage是Service Workers提供的一种存储方式,主要用于存储网络请求的响应数据,从而实现离线功能和提高应用性能。Cache Storage可以存储大量数据,并且可以使用Service Workers来控制缓存的更新和清除。
-
File System API:一些浏览器提供了File System API,用于直接访问用户本地文件系统,可以读取和写入文件。不过,这个API在大多数浏览器中仍处于试验阶段,并且出于安全原因,需要用户明确授权。
这道问题可以拓展:浏览器中主要有以下几种Worker,用于不同的并行任务处理需求:
-
Web Worker:
- 特点:用于在后台运行脚本,执行与用户界面无关的任务,如数据处理、复杂计算等。它可以有效地避免阻塞主线程,从而保持用户界面的响应速度。
- 使用场景:长时间运行的JavaScript任务、数据处理、文件操作等。
- 限制:不能直接操作DOM,只能通过消息传递与主线程通信。
-
Service Worker:
- 特点:用于拦截和处理网络请求、实现离线缓存、推送通知等。它运行在浏览器的后台,独立于网页生命周期,因此可以在网页关闭后仍然运行。
- 使用场景:离线应用支持、缓存策略控制、后台同步、推送通知等。
- 限制:运行在HTTPS环境中,以确保安全。
-
Shared Worker:
- 特点:允许多个浏览上下文(如多个标签页、iframe等)共享同一个Worker实例,便于在多个页面间共享数据和状态。
- 使用场景:跨页面的数据共享、协作编辑等。
- 限制:浏览器支持较少,消息传递需要处理复杂的通信逻辑。
-
Audio Worklet:
- 特点:用于处理Web Audio API中的音频节点,以实现低延迟的自定义音频处理。Audio Worklet允许开发者在音频处理管道中插入自定义音频处理代码。
- 使用场景:实时音频处理、音频效果处理、自定义音频节点等。
- 限制:需要一定的音频处理知识,且受限于实时处理的性能要求。
4.2、konvas
的性能问题
这道题来自于永辉的二面,问我地图使用konvas的绘制,那konvas如何做到一两千个元素不卡顿?
Konvas 通过提供图层机制,使得不同的图形元素可以分层绘制和管理。每个图层可以单独进行缓存和重绘,这样可以避免不必要的全局重绘操作,提高性能。通过缓存不频繁变化的图层,Konva 能够减少重绘的开销。
Konvas 设计每一个Layer会生成两个<canvas>
渲染器:一个是场景渲染器,一个是命中图渲染器(hit graph renderer)。它们的功能如下:
-
场景渲染器(Scene Renderer):
- 这是用户能够看到的部分。
- 用于绘制图形和图像,显示在网页上。
- 负责所有可视化的内容,确保图形能够正确地呈现给用户。
-
命中图渲染器(Hit Graph Renderer):
- 这是一个特殊的隐藏的
<canvas>
,用户看不到它。 - 用于高性能的事件检测,例如鼠标点击、悬停等交互事件。
- 通过在这个隐藏的
<canvas>
上绘制简单的形状和颜色,用来快速识别用户与图形的交互,而不需要对场景渲染器进行复杂的计算。
- 这是一个特殊的隐藏的
这样设计的好处在于:
- 性能优化:通过分离渲染和事件检测,可以优化性能。命中图渲染器可以用非常简单的形状和颜色来表示图形,从而使得事件检测的计算更加高效。
- 事件检测准确性:使用单独的命中图渲染器,可以精确检测用户的交互行为,而不会受到复杂场景的影响。
而对于场景渲染器 Konvas 还允许开发者对节点进行高效的增删改查。它使用内部的脏矩形(dirty rectangle)算法来最小化需要重绘的区域,而不是重新绘制整个 canvas。这样可以大大提高性能,尤其是在涉及大量元素或频繁更新的情况下。
脏矩形算法的核心思想是跟踪哪些部分的屏幕内容发生了变化(即“脏”了),然后只重新绘制这些脏区域,而不是整个屏幕。这些“脏”区域通常用矩形(rectangle)来表示,因为矩形计算简单且高效。
4.3、GIS的计算
来自于永辉的二面:如何判断两条线段相交?如何判断点在多边形区域内?
针对这块我写了一篇文章详细介绍:GIS常用的计算
4.4、重绘+重排
这道题来自于厦门云行,问了什么是重绘重排,哪些会导致重绘和重排?
4.4.1、重排
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)也叫重排。每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
会引起重排的条件:
- 页面第一次渲染(初始化)
- DOM树节点增删或移动
- 一个 DOM 元素的几何属性变化,常见的几何属性有width、height、padding、margin、left、top、border
- 浏览器窗口resize
- 获取元素的某些属性,比如offset族、scroll族和client族属性
4.4.2、重绘
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
重绘是当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变。
- 回流必定引起重绘,重绘可以单独触发。
- 背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)
4.4.3、如何避免频繁回流
-
用transform做形变和位移可以减少reflow
-
避免逐个修改节点样式,尽量一次性修改
-
使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
-
可以将需要多次修改的DOM元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)
-
避免多次读取某些属性,更多细节参考:# 介绍回流与重绘(Reflow & Repaint),以及如何进行优化?
-
通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本
4.5、setTimeout的问题
这道题来自厦门云行,因为我在回答react的时间分片调度的时候提了一嘴setTimeout不准,面试官就开始问题为啥不准,以及如何使setTimeout变准的问题?
setTimeout有人测试过的,嵌套的setTimeout因为浏览器实现机制会有个最小4ms的延迟,但是messageChannel
却不会。具体原因可以看:# 你真的了解 setTimeout 么?聊聊 setTimeout 的最小延时问题(附源码细节)
既然知道嵌套超过5层的时候会出现至少4ms的延迟,那么怎么修正呢?理论上可以借助于perfomance.now()
来修正,但其实这个对于0ms的设定估计修正不了啥,对于其他非0的超时时间可以修正,就是在设置下次定时器的时候,根据误差时间来设定不同的,比如设了个1000ms的定时器,第二次触发的时候发现延迟了5ms,那么第二次设置定时器的时候我们就将1000ms减5ms,设个995ms的定时器,从而纠偏回来。
所以这道题其实可以衍生出:如何实现精准的1s定时器?思路就和上面的一样了。
4.6、图片的鉴权实现
来自于厦门云行,就是面试官搬出他们自己遇到的一些业务问题,问问你会怎么处理,而这次他就问了:说是有个中后台页面,所有的图片是都有身份校验的,不是每个人都可以看到所有图片,那么如何设计可以把图片和鉴权联系起来?(原问题比较晦涩,我这是改编了一下)
第一种做法是使用new Image
对象,然后改写Image
的这个类,加入鉴权的接口调用
第二种做法使用service worker
,拦截image图片的请求,加入鉴权的接口调用,鉴权不通过,返回403。
4.7、字面量对象和new操作符
这道题来自于永辉的一面,问了字面量对象和new操作符新建的对象有何区别?
在 JavaScript 中,字面量对象和通过 new
操作符创建的对象之间有几个重要的区别:
-
方式:
- 字面量对象:使用花括号
{}
来定义对象,并直接在花括号中添加属性和值。 new
操作符:通过构造函数和new
操作符来创建对象。构造函数可以是内置的构造函数(如Array
、Object
)或者自定义的构造函数。
- 字面量对象:使用花括号
-
原型链:
- 字面量对象:使用字面量方式创建的对象会直接指向
Object.prototype
,其原型链为object -> Object.prototype -> null
。 new
操作符:通过new
操作符创建的对象会通过构造函数的prototype
属性指向构造函数的原型对象,形成原型链。
- 字面量对象:使用字面量方式创建的对象会直接指向
-
构造函数:
- 字面量对象:无法添加构造函数和原型方法。
new
操作符:通过构造函数创建的对象可以拥有构造函数以及构造函数原型链上的方法。
-
内存占用:
- 字面量对象:每次使用字面量方式创建对象时,都会创建一个新的对象实例。
new
操作符:通过new
操作符创建的对象会共享构造函数的原型对象,因此节省内存。
5、Zoom
一上来就问了事件循环的。
5.1、setImmediate和setTimeout的区别
setImmediate和setTimeout的区别参考这个: https://github.com/LuckyWinty/fe-weekly-questions/issues/41
里面涉及到nodejs
的poll
阶段和check
阶段。
5.2、笔试题
说出下面的异步任务的打印:
1async function async1() { 2 console.log("async1 start"); // 2 3 await async2(); 4 console.log("async1 end"); // 6 5} 6// 这里有个混淆点,只有当真正遇到await的时候才会真的放到微任务里,否则都是在 7// 主线程上执行的同步代码,跟new Promise一样的道理 8async function async2() { 9 console.log("async2"); // 3 10} 11console.log("script start"); // 1 12setTimeout(() => { 13 console.log("setTimeout"); // 8 14}, 0); 15async1(); 16new Promise((resolve) => { 17 console.log("promise1"); // 4 18 resolve(); 19}).then(() => { 20 console.log("promise2"); // 7 21}); 22console.log("script end"); // 5 23
5.3、position属性
css的position的属性有哪些?我自己忘了了还有一种叫做stick
;
粘性定位:https://developer.mozilla.org/zh-CN/docs/Web/CSS/position#%E7%B2%98%E6%80%A7%E5%AE%9A%E4%BD%8D 粘性定位可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。
5.4、如何遍历对象
用Object.keys
或者Object.entries
,然后我说还可以用 for ... in
(有个弊端是会把原型链也遍历上),又问我 for of
是干嘛的。
for ... in/of 均可以break;
for...in
循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。像 Array
和Object
使用内置构造函数所创建的对象都会继承自Object.prototype
和String.prototype
的不可枚举属性,例如 String
的 indexOf()
方法或 Object
的toString()
方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。
1var obj = {a:1, b:2, c:3}; 2 3for (let key in obj) { 4 console.log(key); 5}
for...of
语句在可迭代对象(包括Array
,Map
,Set
,String
,TypedArray
,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
1const array1 = ['a', 'b', 'c']; 2 3for (const val of array1) { 4 console.log(val); 5}
for of不可以遍历普通对象,想要遍历对象的属性,可以用for in循环, 或内建的Object.keys()方法
1Object.prototype.objCustom = function() {}; 2Array.prototype.arrCustom = function() {}; 3 4let iterable = [3, 5, 7]; 5iterable.foo = 'hello'; 6 7for (let i in iterable) { 8 console.log(i); // 0, 1, 2, "foo", "arrCustom", "objCustom" 9} 10 11for (let i in iterable) { 12 if (iterable.hasOwnProperty(i)) { 13 console.log(i); // 0, 1, 2, "foo" 14 } 15} 16 17for (let i of iterable) { 18 console.log(i); // logs 3, 5, 7 19}
5.5、最后一道笔试题
1// 现已知一个字符串是由正整数和加减乘除四个运算符(+ - * /)组成。 2// 例如存在字符串 const str = '11+2-3*4+5/2*4+10/5',现在需要将高优先级运算,用小括号包裹起来,例如结果为 '11+2-(3*4)+(5/2*4)+(10/5)'。注意可能会出现连续的乘除运算,需要包裹到一起。 3 4const add = (str) => { 5 const result = []; 6 let isAdding = false; 7 let preValueLength = 0; 8 const ifNotExitPlusOrMinus = str 9 .split("") 10 .filter((it) => it === "+" || it === "-"); 11 12 str.split("").forEach((it) => { 13 if (["*", "/"].includes(it)) { 14 if (!isAdding && ifNotExitPlusOrMinus.length) { 15 console.log("it:", it, preValueLength); 16 result.splice(result.length - preValueLength, 0, "("); 17 isAdding = true; 18 } 19 result.push(it); 20 } else if (["+", "-"].includes(it) && isAdding) { 21 result.push(")"); 22 result.push(it); 23 isAdding = false; 24 preValueLength = 0; 25 } else { 26 result.push(it); 27 preValueLength++; 28 if (["+", "-", "*", "/"].includes(it)) { 29 preValueLength = 0; 30 } 31 } 32 }); 33 34 if (isAdding) { 35 result.push(")"); 36 } 37 console.log(result.join("")); 38}; 39
6、魔云智算
前面都是一些项目以及React基础的提问,最后剩余30分钟需要手写四道笔试题。
6.1、算法题
我是没刷过算法的,看到这道题直接是硬编码出来的,后来查了下网上,才知道是动态规划最基础的题目。
1// Suppose you are climbing a staircase. It takes 𝑛 steps to reach the top. 2 3// Each time you can climb 1 or 2 steps. How many different ways can you climb to the top? 4 5// 我的解法,ways是我自己加的,方便调试知道有多少种解法,思路就是累加,算出所有的组合,刨去那些错误的,剩下就是符合条件的 6function climbStairs(n) { 7 let count = 0; 8 const ways = [] 9 10 if (n === 1) { 11 return 1 12 } 13 14 const iteration = (step, way) => { 15 way.push(step) 16 const steps = way.reduce((acc, curr) => acc + curr, 0) 17 if (steps === n) { 18 count++; 19 ways.push(way) 20 return; 21 } 22 23 if (steps > n) { 24 return; 25 } 26 27 iteration(1, [...way]); 28 iteration(2, [...way]); 29 } 30 31 iteration(1, []); 32 iteration(2, []); 33 34 return count; 35}
6.2、hook题目
1// Implement a debounce hook function using React. Like ahooks useDebounceFn 2 3// Debounce clickFn 4import React from 'react'; 5import { useRef, useCallback } from 'react'; 6 7export default function App() { 8 const clickFn = () => { 9 console.log('Q2'); 10 }; 11 12 const xx = useDebounceFn(clickFn) 13 14 return ( 15 <div className='App'> 16 <h1>Hello React TypeScript.</h1> 17 <h2>Start editing to see some magic happen!</h2> 18 <button onClick={xx}>click</button> 19 </div> 20 ); 21} 22 23// 参考蚂蚁上面的笔试题 24function useDebounceFn(fn, delay = 1000) {}
6.3、考察递归结构
1// Implement a function to convert a real DOM into a virtual DOM. 2// Give an probably TS model of a virtual node. 3 4 5interface VNode { 6 tag: string; 7 props: { [key: string]: any }; 8 children: VNode[]; 9} 10 11function domToVNode(node: HTMLElement) { 12 const props = {}; 13 for (const { name, value } of node.attributes) { 14 props[name] = value; 15 } 16 17 const children: VNode[] = []; 18 for (let i = 0; i < node.children.length; i++) { 19 children.push(domToVNode(node.children[i] as HTMLElement)); 20 } 21 22 return { 23 tag: node.tagName.toLowerCase(), 24 props, 25 children, 26 }; 27 28} 29 30const realDOMElement = document.getElementById('root'); 31 32const virtualDOM = domToVNode(realDOMElement); 33 34console.log('Q3', virtualDOM);
6.4、考察ts
1// 实现一个MyPartial类型,可以将T的所有成员转为可选的 2type MyPartial<T> = { 3 [K in keyof T]?: T[K]; 4}; 5 6// 测试 7interface User { 8 id: number; 9 name: string; 10 email: string; 11} 12 13// Partial<User> 类似于 { id?: number; name?: string; email?: string; } 14type PartialUser = MyPartial<User>; 15
6、哈希岛
唯一一家面试全程用笔试来代替的公司,第一面的题目和蚂蚁的第六道题很相似,第二面继续做题目,给了官网一个轮播图,一个小时内写出来整体的效果。最后二面没通过。
7、塔斯汀
也是很nice的一家公司,最后薪资没谈拢,没去成。
8、其他踩坑的公司
- 一人一车科技
- 紫讯
- 杭州觅他科技有限公司:面试官看着吊儿郎当的,不建议
- 彩讯股份:要求精通webGL
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 准35岁的前端人,2024年的面试 的内容有疑问,请在下面的评论系统中留言,谢谢。