3

前端面试系列 - 手写代码

 6 months ago
source link: https://wuxinhua.com/posts/Top-front-end-developer-interview-whiteboard-code-questions/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前端面试系列 - 手写代码

0xinhua 发布于 2024年02月29日

文章记录了我之前在前端面试中碰到的手写代码题,以及我的一些解题思路,大致给分了 4 类:基础题、Polyfill、业务相关题目、leetcode 题目,希望能给你在准备前端面试时提供一些解题思路。

不管是国内还是国外互联网公司前端面试流程中,基本都会有要求白板代码过程,面试官会根据岗位及候选人情况出相应难度的基础题或 leetcode 算法题,要求面试者在规定时间内使用熟悉的语言现场码代码解决问题,也被称为“手撕代码”过程。

手写代码更多的是考察你的思路和现场解决问题的能力,当然也能从一些细节中看出候选人的代码风格,例如函数命名、一致的代码风格及规范、追求更优解等,手写代码只是作为筛选候选人的门槛工具,在当下 AI 辅助编程的情况下,很多题目现在看来已经没有意义,另外我的答案、思路不一定 100% 准确,把当时的记录贴出来仅供参考,不建议你死背题目和答案代码,而是建议在不看已有答案的前提下自己思考实现一下代码。

前端面试系列文章已经更新了两篇,前端八股文系列可以查看 前端面试系列之前端八股文

实现一个 curry add 函数



1# 快手前端面试 2# 有一个 add 函数,需要实现一个 curry add 函数,将 add(a + b) 转换为 add(a)(b), 3例如 sum(1, 2) => sum(1)(2) 以及 sum(1, 2, 3) => sum(1)(2)(3) 4

函数柯里化算是我碰到频率最高的一个题,一般会出现在一面过程,如果候选人对函数柯里化(Currying)比较熟悉,面试官可能会进一步要求实现加强版,例如不定参版。

curry 的核心是把接受多个参数的函数转换成接受一个单一参数的函数,这里首先能想到的是使用闭包来实现,将函数再返回用于下一次执行,例如如下代码:



1 const add = function(x) { 2 return function(y) { 3 return x + y 4 } 5 } 6 add(1)(2) // 3

面试官可能会要求你优化一下,因为这样实现有个问题,当参数是多个时,我们需要再多加 return 逻辑,例如 3 个参数时, 那么这个时候可以考虑实现一个 curry 方法将 add 加工成 curry 化函数:



1 const add = function(x) { 2 return function(y) { 3 return function(z) { 4 return x + y + z 5 } 6 } 7 } 8 9 // or 使用箭头函数 10 const add = a => b => c => a + b + c 11 add(1)(2)(3) // 6 12 13 const curry = function (fn, ...a) { 14 // 实参数量大于等于形参数量 15 return a.length >= fn.length ? 16 // 如果大于返回执行结果 17 fn(...a) : 18 // 反之继续柯里化,递归,并将上一次的参数以及下次的参数继续传递下去 19 (...b) => curry(fn, ...a, ...b); 20 }; 21 22 const addCurry = curry(add); 23 addCurry(1, 2, 3) // 6 24 addCurry(1)(2)(3) // 6

数据结构扁平化转换

这个题目在面试大厂小厂都碰到过,出现频率很高,可提前准备及练习,主要分为两类:树形嵌套结构转化成对象结构及反过来输出扁平化后数据。



1// 需要实现一个 merge 方法 将 obj 变为 obj2 的格式 2const obj = [ 3 { id: 1, parent: null }, 4 { id: 2, parent: 1 }, 5 { id: 3, parent: 2 } 6] 7 8const obj2 = { 9 obj: { 10 id: 1, 11 parent: null, 12 child: { 13 id: 2, 14 parent: 1, 15 child: { 16 id: 3, 17 parent: 2 18 } 19 } 20 } 21}

首先想到的是使用递归调用,判断嵌套的条件 parent 值是否等于上一个节点 id 即可,代码如下:



1const merge = (arr, n) => { 2 if (n === arr.length - 1) { 3 return arr[n]; 4 } 5 arr[n].child = merge(arr, n + 1); 6 return arr[n]; 7} 8 9let obj2 = {} 10 11obj2.obj = merge(obj, 0); 12 13console.log(obj2);

类似的两道稍微有一些区别,但换汤不换药,思路也是一样的。



1 2// 将 obj 转换输出扁平化后的 result 对象 3 4const obj = { 5 a: 1, 6 b: { c: 2, d: 3 }, 7 e: { f: { g: 4 } }, 8 h: { i: { j: 5, k: 6 } } 9} 10 11const result = { 12 'a': 1, 13 'b.c': 2, 14 'b.d': 3, 15 'e.f.g': 4, 16 'h.i.j': 5, 17 'h.i.k': 6 18}; 19 20function flat(obj) { 21 // 思路 22 // input obj 23 // output obj 24 // value !== obj 25 // value === obj 26 // key 拼接 . 27 // 递归 28 let result = {}; 29 function flatArray(o, pre) { 30 for (key in o) { 31 // value === obj 32 if (o[key] instanceof Object) { 33 if (pre === null) { 34 flatArray(o[key], key) 35 } else { 36 flatArray(o[key], pre + '.' + key); 37 } 38 39 } else { 40 if (pre === null) { 41 result[key] = o[key]; 42 } else { 43 result[pre + '.' + key] = o[key]; 44 }; 45 } 46 } 47 } 48 return flatArray(obj, null); 49} 50

观察者模式 eventBus

有很多公司都会考察设计模式,最基本的这几个需要掌握一下,例如观察者模式,实现一个最简版的 eventBus。

给数字加千分号

给金额整数部分数字加上千分符合,例如 12345 变成 12,345,小数点后数字不需要处理。

我最先能想到的方法是通过转换成数组来处理,判断是否有小数点,将整数部分转化成数组,每 3 位添加一个逗号分隔符号,但注意如果有小数点,需要截取一下整数部分,另外是从后往前加,可以先将数组 reverse 一下再处理,另外投机取巧直接 toLocaleString() 也可以完成转换。



1// 转化前 1234567 2// 转化后 12345.67 3const formatNumber = (num) => { 4 const numList = (num + '').split('.') 5 let numStr = numList[0] 6 let numArr = [] 7 for (i of numStr) { 8 numArr.push(i) 9 } 10 num = numArr.reverse() 11 let formatNum = [] 12 for (let i = 0; i < num.length; i++) { 13 if (i % 3 === 0 && i !== 0) { 14 formatNum.push(',') 15 } 16 formatNum.push(num[i]) 17 } 18 numStr = formatNum.reverse().join('') 19 num = numList.length > 1 ? numStr + '.' + numList[1] : numStr; 20 return num 21} 22 23console.log(formatNumber(1234567)) // 1,234,567 24console.log(formatNumber(123456.12)) // 123,456.12 25

如何在不引入第三个变量的情况交换两个变量的值,例如 a = 1; b = 2 变成 a = 2; a = 1

这个题比较简单了,如果你对 ES6 解构赋值比较了解能一下做出来,当然加减法(适用于数值类型)也能完成交换:



1 2let a = 1 3let b = 2 4 5a = a + b 6b = a - b 7a = a - b 8 9console.log('a b', a, b)

使用数组交换两者的位置:



1[a, b] = [b, a] 2console.log('a b', a, b)

方法 polyfill

实现一个 myTypeOf,能给出数据的类型

这里主要考察对 JS 常规的数据类型的掌握,目前 JavaScript 中的 typeof 方法用于获取一个变量的类型。它可以返回以下几种类型值:

  • "undefined":表示变量未定义
  • "boolean":布尔类型
  • "number":数字类型
  • "string":字符串类型
  • "object":对象类型
  • "function":函数类型
  • "symbol": Symbol 类型

我们可以使用其它方式输出数据类型来模拟 typeof 的使用,例如 Object.prototype.toString 方法返回对象的类型字符串,



1 2function myTypeOf(value) { 3 4 if (value === null) { 5 return 'null' 6 } 7 8 // /ab/ RegExp 9 if (value instanceof RegExp) { 10 return 'object' 11 } 12 13 // [ab] Array 14 if (Array.isArray(value)) { 15 return 'object' 16 } 17 const str = Object.prototype.toString.call(value).slice(8, -1) 18 return str.toLocaleLowerCase() 19} 20

HardMan

实现一个 HardMan 方法,如果是在准备面微信事业群(腾讯wxg)或腾讯前端面试的同学可以思考一下这个题目,出题的概率也比较大,由于实现的方法比较多,面试官能考察候选人对 class 类、PromisesetTimeout、队列等的使用及掌握熟练情况。



1 2HardMan(“jack”) 输出: 3// I am jack 4 5HardMan(“jack”).rest(10).learn(“computer”) 输出: 6// I am jack 7//等待10秒 8// Start learning after 10 seconds 9// Learning computer 10 11HardMan(“jack”).restFirst(5).learn(“chinese”) 输出: 12// 等待5秒 13// Start learning after 5 seconds 14// I am jack 15// Learning chinese

使用 Promise 链,我们使用了一个内部的 Promise 对象来管理异步操作的顺序。每当调用 .rest()、.learn() 或 .restFirst() 方法时,我们都在内部的 Promise 链上添加新的操作。这样,即使是异步操作,也可以确保按照调用的顺序执行。



1class HardMan { 2 constructor(name) { 3 this.promise = Promise.resolve().then(() => { 4 console.log(`I am ${name}`); 5 }); 6 } 7 8 rest(time) { 9 this.promise = this.promise.then(() => new Promise(resolve => { 10 console.log(`等待${time}秒`); 11 setTimeout(() => { 12 console.log(`Start learning after ${time} seconds`); 13 resolve(); 14 }, time * 1000); 15 })); 16 return this; 17 } 18 19 learn(subject) { 20 this.promise = this.promise.then(() => { 21 console.log(`Learning ${subject}`); 22 }); 23 return this; 24 } 25 26 restFirst(time) { 27 const waitAndLog = () => new Promise(resolve => { 28 console.log(`等待${time}秒`); 29 setTimeout(() => { 30 console.log(`Start learning after ${time} seconds`); 31 resolve(); 32 }, time * 1000); 33 }); 34 35 const initialPromise = this.promise; 36 this.promise = Promise.resolve().then(() => waitAndLog()).then(() => initialPromise); 37 return this; 38 } 39} 40 41// new HardMan(“jack”) => “jack” 42// new HardMan(“jack”).rest(10).learn(“computer”) 43// I am jack 44//等待10秒 45// Start learning after 10 seconds 46// Learning computer

实现一个简单的 flat 函数,能将数组拍平

这类题目主要考察面试者对 JS 基础,例如判断元素是是否是数组,prototypethis 的用法等。



1function myFlat(arr) { 2 let res= []; 3 for (const item of arr) { 4 if (Array.isArray(item)) { 5 res= res.concat(myFlat(item)); 6 // concat 方法返回一个新数组,不会改变原数组 7 } else { 8 res.push(item); 9 } 10 } 11 return res; 12} 13 14// redude 版 15 16function myFlat(arr, depth = 1) { 17 return depth > 0 18 ? arr.reduce( 19 (pre, cur) => 20 pre.concat(Array.isArray(cur) ? myFlat(cur, depth - 1) : cur), 21 [] 22 ) 23 : arr.filter((item) => item !== undefined); 24} 25

实现一个 Promise.allSettled

如果没有使用过 allSettled 方法,可以先询问一下面试官这个方法的使用,allSettled 是指当你请求多个 Promise 方法里,当所有 Promise 执行后才返回,结果是一个数组对象分别有 statusvaluereason 三个属性,而参数是一个 Promise 数组。



1const results = allSettled([ 2 Promise.resolve(33), 3 new Promise((resolve) => setTimeout(() => resolve(66), 0)), 4 99, 5 Promise.reject(new Error("一个错误")), 6]); 7 8// [ 9// { status: 'fulfilled', value: 33 }, 10// { status: 'fulfilled', value: 66 }, 11// { status: 'fulfilled', value: 99 }, 12// { status: 'rejected', reason: Error: 一个错误 } 13// ]

allSettled Ployfill 代码:



1function allSettled(promises) { 2 3 const results = [] 4 5 for (promise of promises) { 6 let result = { status: 'fulfilled' } 7 promise 8 .then(res => result.value = res) 9 .catch(err => { 10 result.status = 'rejected' 11 result.reason = err 12 }) 13 } 14 return results 15} 16 17// VM132:8 Uncaught TypeError: promise.then is not a function

这样写有个问题,当上面参数传入的参数 99 的情况会报错,因为不是 promise 的原因,在这个基础上加上是否是 promise 的判断:



1function allSettled(promises) { 2 const results = []; 3 4 for (const promiseOrValue of promises) { 5 let result = {}; 6 7 if (typeof promiseOrValue === 'object' && typeof promiseOrValue.then === 'function' ) { // Check if it's a promise 8 result.status = 'pending'; 9 promiseOrValue 10 .then(res => { 11 result.status = 'fulfilled'; 12 result.value = res; 13 }) 14 .catch(err => { 15 result.status = 'rejected'; 16 result.reason = err; 17 }); 18 } else { 19 result.status = 'fulfilled'; 20 result.value = promiseOrValue; 21 } 22 23 results.push(result); 24 } 25 26 return results; 27}

实现 lodash 的 _.get 方法

实现 _.get(object, path (Array | String)〔筆畫〕, [defaultValue]) 方法, path 可传字符串或数组,根据 Object 对象的 path 路径获取值。 如果解析 valueundefined 会以 defaultValue 取代。



1 2const object = { 'a': [{ 'b': { 'c': 3 } }] }; 3 4_get(object, 'a[0].b.c'); 5 6_.get(object, 'a[0].b.c'); 7// => 3 8 9_.get(object, ['a', '0', 'b', 'c']); 10// => 3 11

要实现这个函数的 Polyfill,我们需要做以下几步:

  • 解析路径:路径可以是字符串或数组形式。如果是字符串,可能包含点(.)或方括号([]),需要将其解析为数组形式。
  • 遍历路径:按照路径数组逐层深入到目标对象中。
  • 处理不存在的路径:如果在路径中的任何一点发现目标值不存在,返回默认值。
  • 返回找到的值:如果成功找到值,返回该值。


1function get(object, path, defaultValue) { 2 // 将路径字符串转换为数组。考虑到路径中可能使用了点或方括号,需要适当处理。 3 const paths = Array.isArray(path) 4 ? path 5 : path.replace(/\[(\d+)\]/g, '.$1').split('.'); 6 7 // 逐步深入到目标对象中,使用 reduce 方法简化遍历过程。 8 let result = paths.reduce((acc, key) => (acc !== null && acc !== undefined) ? acc[key] : undefined, object); 9 10 // 如果最终结果是 undefined,则返回默认值;否则返回结果。 11 return result === undefined ? defaultValue : result; 12} 13

实现一个简单的 Promise, 能正常调用 then 和 catch 方法

这个可能写起来比较复制一点,主要考察你对 Promise 的掌握情况。



1// 新建一个 Promise 类 2 3const Pending = 'pending' 4const Fulfilled = 'resolved' 5const Rejected = 'rejected' 6 7class MyPromise { 8 9 constructor(executor) { 10 executor(this.resolve, this.reject); 11 } 12 13 status = Pending; 14 value = null; 15 reason = null; 16 onFulfilledCallback = []; 17 onRejectedCallback = []; 18 19 resolve = (value) => { 20 if (this.status === Pending) { 21 this.status = Fulfilled; 22 this.value = value; 23 // this.onFulfilledCallback && this.onFulfilledCallback(value); 24 while (this.onFulfilledCallback.length) { 25 this.onFulfilledCallback.shift()(value); 26 } 27 } 28 }; 29 30 reject = (reason) => { 31 if (this.status === Pending) { 32 this.status = Rejected; 33 this.reason = reason; 34 // this.onRejectedCallback && this.onRejectedCallback(reason); 35 while (this.onRejectedCallback.length) { 36 this.onRejectedCallback.shift()(reason); 37 } 38 } 39 }; 40 41 then(onFulfilled, onRejected) { 42 43 const promise2 = new MyPromise((resolve, reject) => { 44 if (this.status === Fulfilled) { 45 const x = onFulfilled(this.value); 46 resolve(x); 47 } 48 49 else if (this.status === Rejected) { 50 const x = onRejected(this.reason); 51 reject(x); 52 } 53 54 else if (this.status === Pending) { 55 // this.onFulfilledCallback = onFulfilled; 56 // this.onRejectedCallback = onRejected; 57 this.onRejectedCallback.push(onRejected); 58 this.onFulfilledCallback.push(onFulfilled); 59 } 60 }); 61 return promise2; 62 } 63} 64 65let promise = new MyPromise((resolve, reject) => { 66 setTimeout(() => { 67 resolve('success'); 68 }, 2000) 69 // resolve('success'); 70}); 71 72promise.then(value => { 73 console.log('value', value); 74}, reason => { 75 console.log('reason', reason); 76}) 77 78promise.then(value => { 79 console.log('value 2', value); 80}, reason => { 81 console.log('reason 2', reason); 82}) 83 84promise.then(value => { 85 console.log('value 3', value); 86}, reason => { 87 console.log('reason 3', reason); 88}).then(value => { 89 console.log('value 3 then', value); 90})

实现 Array 的 filter 方法

题目:实现数组 filter 方法的 polyfill, 例如 [1,2,3,4,5].myFilter(a => a !== 1) 输出 2 3 4 5



1 2Array.prototype.myFilter = (callback, context) { 3 var result = []; 4 for (var i = 0; i < this.length; i++) { 5 if (callback.call(context, this[i], i, this)) { 6 result.push(this[i]) 7 } 8 } 9 return result; 10}

业务代码题主要考察你的业务处理能力,阿里巴巴前端面试过程曾遇到类似的两道题目,当然这不是原题,思路类似:

实现 parse 方法, 从对像中取值替换对应标记例如:

问题如下:



1 2const data = { brand: 'Apple', model:'iPhone10,1', price: 1234 }; 3 4const tpl = '$model$, 应为$brand$手机,预估价格$price$'; 5 6parse(data, tpl) // iPhone10,1 应为Apple手机,预估价格1234 7

代码如下:



1 2// 思路: 3// 获取 key 和 value 4// 正则替换 5 6function parse(tpl, data) { 7 for (key in data) { 8 const reg = new RegExp("\\$"+key+"\\$") 9 tpl = tpl.replace(reg, data[key]) 10 } 11 return tpl; // iPhone10,1 应为Apple手机,预估价格1234 12} 13

实现一个 render 方法, 从对像中取值替换对应标记

问题如下:



1 2let template = '你好,我们公司是{{company}},我们属于{{group.name}}业务线,我们在招聘各种方向的人才,包括{{group.jobs[0]}}、{{group["jobs"][1]}}等。' 3 4let obj = { 5 group: { 6 name: "阿里云", 7 jobs: ["前端", "后端", "产品"] 8 }, 9 company: '阿里巴巴' 10} 11 12function render(template, obj){ 13 // 你的代码实现 14} 15// 最终返回结果为 你好,我们公司是阿里巴巴,我们属于阿里云业务线,我们在招聘各种方向的人才,包括前端、后端等。 16


1function render (template, obj) { 2// 代码实现 3 const re = /\{\{\s*(.+?)\s*\}\}/g 4 return template.replace(re, function(match, $1) { 5 console.log('match', match, '$1', $1) 6 let val = (new Function(`return this.${$1}`)).call(obj) 7 return val 8 }) 9} 10 11// 你好,我们公司是阿里巴巴,我们属于阿里云业务线,我们在招聘各种方向的人才,包括前端、后端等。 12render( 13 '你好,我们公司是{{company}},我们属于{{group.name}}业务线,我们在招聘各种方向的人才,包括{{group.jobs[0]}}、{{group["jobs"][1]}}等。', 14 { 15 group: { 16 name: "阿里云", 17 jobs: ["前端", "后端", "产品"] 18 }, 19 company: '阿里巴巴' 20 } 21) 22

实现一个红绿灯

有以下三个方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?



1function red() { 2 console.log('red'); 3} 4function green() { 5 console.log('green'); 6} 7function yellow() { 8 console.log('yellow'); 9}

思路:使用 setTimeout + Promise then 将三个方法串联起来:



1const light = () => { 2 Promise.resolve().then(res => { 3 setTimeout(() => { 4 red() 5 }, 3000); 6 }).then(() => { 7 setTimeout(() => { 8 green(); 9 }, 2000); 10 }).then(() => { 11 setTimeout(() => { 12 yellow(); 13 }, 1000); 14 }).then(() => { 15 setTimeout(() => { 16 light(); 17 }, 3000) 18 }); 19} 20// light() 21 22// async await 版本 23 24const setLight = async () => { 25 await setTimeout(() => { red(); setLight() }, 3000); 26 await setTimeout(() => green(), 1000); 27 await setTimeout(() => yellow(), 2000); 28}; 29 30// setLight(); 31

url 参数获取并转换

有这样的一个 url http://www.domain.com/?user=anonymous&id=123&id=456&id=4569&city=%E5%8C%97%E4%BA%AC&enabled 需要实现一个 parseParam(url) 方法以对象方式输出携带的数据,结果如下:



1 2{ user: 'anonymous', 3 id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型 4 city: '北京', // 中文需解码 5 enabled: true, // 未指定值得 key 约定为 true 6}

思路:使用正则匹配获取字段或者使用 split 截取后循环输出,注意中文转码和默认赋值



1const parseParam = (url) => { 2 let obj = {}; 3 const urls = url.split('?')[1]; 4 const queryList = urls.split('&'); 5 // [user=anonymous, id=123, id=45, city=%E5%8C%97%E4%BA%AC, enabled] 6 for (let i = 0; i < queryList.length; i++) { 7 if (/=/.test(queryList[i])) { 8 const item = queryList[i].split('='); 9 let [key, value] = item; 10 if (obj.hasOwnProperty(key)) { 11 obj[key] = [].concat([obj[key], value]).flat(Infinity).map(value => /^\d+$/.test(value) ? parseFloat(value) : value); 12 } else { 13 obj[key] = decodeURIComponent(value); 14 } 15 } else { 16 obj[queryList[i]] = true; 17 } 18 } 19 return obj; 20} 21 22// console.log('parseParam', parseParam(url));

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK