前端面试系列 - 手写代码

0xinhua 发布于

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

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

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

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

基础题

实现一个 curry add 函数

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

js
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 化函数:

js
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

数据结构扁平化转换

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

题目:

js
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 即可,代码如下:

js
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);

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

js
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() 也可以完成转换。

代码:

js
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 解构赋值比较了解能一下做出来,当然加减法(适用于数值类型)也能完成交换:

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

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

js
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 方法返回对象的类型字符串,

js
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、队列等的使用及掌握熟练情况。

题目:

js
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 链上添加新的操作。这样,即使是异步操作,也可以确保按照调用的顺序执行。

js
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 的用法等。

js
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 数组。

题目:

js
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 代码:

js
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 的判断:

js
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 取代。

示例:

js
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,我们需要做以下几步:

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

js
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

js
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 方法, 从对像中取值替换对应标记例如:

问题如下:

js
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

代码如下:

js
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 方法, 从对像中取值替换对应标记

问题如下:

js
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
js
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 亮一次;如何让三个灯不断交替重复亮灯?

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

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

js
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) 方法以对象方式输出携带的数据,结果如下:

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

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

js
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));