美团前端一面手写面试题
实现斐波那契数列
// 递归
function fn (n){if(n==0) return 0if(n==1) return 1return fn(n-2)+fn(n-1)
}
// 优化
function fibonacci2(n) {const arr = [1, 1, 2];const arrLen = arr.length;if (n <= arrLen) {return arr[n];}for (let i = arrLen; i < n; i++) {arr.push(arr[i - 1] + arr[ i - 2]);}return arr[arr.length - 1];
}
// 非递归
function fn(n) {let pre1 = 1;let pre2 = 1;let current = 2;if (n <= 2) {return current;}for (let i = 2; i < n; i++) {pre1 = pre2;pre2 = current;current = pre1 + pre2;}return current;
}
判断是否是电话号码
function isPhone(tel) {var regx = /^1[34578]\d{9}$/;return regx.test(tel);
}
查找数组公共前缀(美团)
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。示例 1:输入:strs = ["flower","flow","flight"]
输出:"fl"示例 2:输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
答案
const longestCommonPrefix = function (strs) {const str = strs[0];let index = 0;while (index < str.length) {const strCur = str.slice(0, index + 1);for (let i = 0; i < strs.length; i++) {if (!strs[i] || !strs[i].startsWith(strCur)) {return str.slice(0, index);}}index++;}return str;
};
请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式
<div><span><a></a></span><span><a></a><a></a></span>
</div>把上面dom结构转成下面的JSON格式{tag: 'DIV',children: [{tag: 'SPAN',children: [{ tag: 'A', children: [] }]},{tag: 'SPAN',children: [{ tag: 'A', children: [] },{ tag: 'A', children: [] }]}]
}
实现代码如下:
function dom2Json(domtree) {let obj = {};obj.name = domtree.tagName;obj.children = [];domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));return obj;
}
字符串最长的不重复子串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。示例 1:输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。示例 2:输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。示例 3:输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。示例 4:输入: s = ""
输出: 0
答案
const lengthOfLongestSubstring = function (s) {if (s.length === 0) {return 0;}let left = 0;let right = 1;let max = 0;while (right <= s.length) {let lr = s.slice(left, right);const index = lr.indexOf(s[right]);if (index > -1) {left = index + left + 1;} else {lr = s.slice(left, right + 1);max = Math.max(max, lr.length);}right++;}return max;
};
实现千位分隔符
// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {num = parseFloat(num.toFixed(3));let [integer, decimal] = String.prototype.split.call(num, '.');integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');return integer + '.' + (decimal ? decimal : '');
}
参考 前端进阶面试题详细解答
数组中的数据根据key去重
给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:
const dedup = (data, getKey = () => {} ) => {// todo
}
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现
const dedup = (data, getKey = () => { }) => {const dateMap = data.reduce((pre, cur) => {const key = getKey(cur)if (!pre[key]) {pre[key] = cur}return pre}, {})return Object.values(dateMap)
}
使用
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];
console.log(dedup(data, (item) => item.id))// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现事件总线结合Vue应用
Event Bus
(Vue、Flutter 等前端框架中有出镜)和Event Emitter
(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色—— 全局事件总线 。
全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。
如果只能选一道题,那这道题一定是
Event Bus/Event Emitter
的代码实现——我都说这么清楚了,这个知识点到底要不要掌握、需要掌握到什么程度,就看各位自己的了。
在Vue中使用Event Bus来实现组件间的通讯
Event Bus/Event Emitter
作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex
之外,我们还可以通过 Event Bus
来实现我们的需求。
创建一个 Event Bus
(本质上也是 Vue 实例)并导出:
const EventBus = new Vue()
export default EventBus
在主文件里引入EventBus
,并挂载到全局:
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus
订阅事件:
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)
发布(触发)事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的
PrdPublisher
和DeveloperObserver
),全程只有bus
这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
下面,我们就一起来实现一个Event Bus
(注意看注释里的解析):
class EventEmitter {constructor() {// handlers是一个map,用于存储事件与回调之间的对应关系this.handlers = {}}// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数on(eventName, cb) {// 先检查一下目标事件名有没有对应的监听函数队列if (!this.handlers[eventName]) {// 如果没有,那么首先初始化一个监听函数队列this.handlers[eventName] = []}// 把回调函数推入目标事件的监听函数队列里去this.handlers[eventName].push(cb)}// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数emit(eventName, ...args) {// 检查目标事件是否有监听函数队列if (this.handlers[eventName]) {// 如果有,则逐个调用队列里的回调函数this.handlers[eventName].forEach((callback) => {callback(...args)})}}// 移除某个事件回调队列里的指定回调函数off(eventName, cb) {const callbacks = this.handlers[eventName]const index = callbacks.indexOf(cb)if (index !== -1) {callbacks.splice(index, 1)}}// 为事件注册单次监听器once(eventName, cb) {// 对回调函数进行包装,使其执行完毕自动被移除const wrapper = (...args) => {cb.apply(...args)this.off(eventName, wrapper)}this.on(eventName, wrapper)}
}
在日常的开发中,大家用到
EventBus/EventEmitter
往往提供比这五个方法多的多的多的方法。但在面试过程中,如果大家能够完整地实现出这五个方法,已经非常可以说明问题了,因此楼上这个EventBus
希望大家可以熟练掌握。学有余力的同学
实现reduce方法
- 初始值不传怎么处理
- 回调函数的参数有哪些,返回值如何处理。
Array.prototype.myReduce = function(fn, initialValue) {var arr = Array.prototype.slice.call(this);var res, startIndex;res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项startIndex = initialValue ? 0 : 1;for(var i = startIndex; i < arr.length; i++) {// 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr))res = fn.call(null, res, arr[i], i, this); }return res;
}
对象数组如何去重
根据每个对象的某一个具体属性来进行去重
const responseList = [{ id: 1, a: 1 },{ id: 2, a: 2 },{ id: 3, a: 3 },{ id: 1, a: 4 },
];
const result = responseList.reduce((acc, cur) => {const ids = acc.map(item => item.id);return ids.includes(cur.id) ? acc : [...acc, cur];
}, []);
console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型city: '北京', // 中文需解码enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中let paramsObj = {};// 将 params 存到对象中paramsArr.forEach(param => {if (/=/.test(param)) { // 处理有 value 的参数let [key, val] = param.split('='); // 分割 key 和 valueval = decodeURIComponent(val); // 解码val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值paramsObj[key] = [].concat(paramsObj[key], val);} else { // 如果对象没有这个 key,创建 key 并设置值paramsObj[key] = val;}} else { // 处理没有 value 的参数paramsObj[param] = true;}})return paramsObj;
}
实现数组的乱序输出
主要的实现思路就是:
- 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
- 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
- 按照上面的规律执行,直到遍历完成
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
还有一方法就是倒序遍历:
var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,randomIndex,temp;while (length) {randomIndex = Math.floor(Math.random() * length--);temp = arr[length];arr[length] = arr[randomIndex];arr[randomIndex] = temp;}
console.log(arr)
实现 getValue/setValue 函数来获取path对应的值
// 示例
var object = { a: [{ b: { c: 3 } }] }; // path: 'a[0].b.c'
var array = [{ a: { b: [1] } }]; // path: '[0].a.b[0]'function getValue(target, valuePath, defaultValue) {}console.log(getValue(object, "a[0].b.c", 0)); // 输出3
console.log(getValue(array, "[0].a.b[0]", 12)); // 输出 1
console.log(getValue(array, "[0].a.b[0].c", 12)); // 输出 12
实现
/*** 测试属性是否匹配*/
export function testPropTypes(value, type, dev) {const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaNconst oEnums = ['Null', 'Object', 'Array', 'Date', 'RegExp', 'Error'];const nEnums = ['[object Number]','[object String]','[object Boolean]','[object Undefined]','[object Function]','[object Null]','[object Object]','[object Array]','[object Date]','[object RegExp]','[object Error]',];const reg = new RegExp('\\[object (.*?)\\]');// 完全匹配模式,type应该传递类似格式[object Window] [object HTMLDocument] ...if (reg.test(type)) {// 排除nEnums的12种if (~nEnums.indexOf(type)) {if (dev === true) {console.warn(value, 'The parameter type belongs to one of 12 types:number string boolean undefined Null Object Array Date RegExp function Error NaN');}}if (Object.prototype.toString.call(value) === type) {return true;}return false;}
}
const syncVarIterator = {getter: function (obj, key, defaultValue) {// 结果变量const defaultResult = defaultValue === undefined ? undefined : defaultValue;if (testPropTypes(obj, 'Object') === false && testPropTypes(obj, 'Array') === false) {return defaultResult;}// 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改let result = obj;// 得到知道值try {// 解析属性层次序列const keyArr = key.split('.');// 迭代obj对象属性for (let i = 0; i < keyArr.length; i++) {// 如果第 i 层属性存在对应的值则迭代该属性值if (result[keyArr[i]] !== undefined) {result = result[keyArr[i]];// 如果不存在则返回未定义} else {return defaultResult;}}} catch (e) {return defaultResult;}// 返回获取的结果return result;},setter: function (obj, key, val) {// 如果不存在obj则返回未定义if (testPropTypes(obj, 'Object') === false) {return false;}// 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改let result = obj;try {// 解析属性层次序列const keyArr = key.split('.');let i = 0;// 迭代obj对象属性for (; i < keyArr.length - 1; i++) {// 如果第 i 层属性对应的值不存在,则定义为对象if (result[keyArr[i]] === undefined) {result[keyArr[i]] = {};}// 如果第 i 层属性对应的值不是对象(Object)的一个实例,则抛出错误if (!(result[keyArr[i]] instanceof Object)) {throw new Error('obj.' + keyArr.splice(0, i + 1).join('.') + 'is not Object');}// 迭代该层属性值result = result[keyArr[i]];}// 设置属性值result[keyArr[i]] = val;return true;} catch (e) {return false;}},
};
使用promise来实现
创建 enhancedObject
函数
const enhancedObject = (target) =>new Proxy(target, {get(target, property) {if (property in target) {return target[property];} else {return searchFor(property, target); //实际使用时要对value值进行复位}},});let value = null;
function searchFor(property, target) {for (const key of Object.keys(target)) {if (typeof target[key] === "object") {searchFor(property, target[key]);} else if (typeof target[property] !== "undefined") {value = target[property];break;}}return value;
}
使用 enhancedObject
函数
const data = enhancedObject({user: {name: "test",settings: {theme: "dark",},},
});console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark
以上代码运行后,控制台会输出以下代码:
dark
dark
通过观察以上的输出结果可知,使用
enhancedObject
函数处理过的对象,我们就可以方便地访问普通对象内部的深层属性。
Promise并行限制
就是实现有并行限制的Promise调度器问题
class Scheduler {constructor() {this.queue = [];this.maxCount = 2;this.runCounts = 0;}add(promiseCreator) {this.queue.push(promiseCreator);}taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();}}request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}this.runCounts++;this.queue.shift()().then(() => {this.runCounts--;this.request();});}
}const timeout = time => new Promise(resolve => {setTimeout(resolve, time);
})const scheduler = new Scheduler();const addTask = (time,order) => {scheduler.add(() => timeout(time).then(()=>console.log(order)))
}addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4
用Promise实现图片的异步加载
let imageAsync=(url)=>{return new Promise((resolve,reject)=>{let img = new Image();img.src = url;img.οnlοad=()=>{console.log(`图片请求成功,此处进行通用操作`);resolve(image);}img.οnerrοr=(err)=>{console.log(`失败,此处进行失败的通用操作`);reject(err);}})}imageAsync("url").then(()=>{console.log("加载成功");
}).catch((error)=>{console.log("加载失败");
})
实现数组元素求和
- arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
- arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);
递归实现:
let arr = [1, 2, 3, 4, 5, 6] function add(arr) {if (arr.length == 1) return arr[0] return arr[0] + add(arr.slice(1))
}
console.log(add(arr)) // 21
类数组转化为数组的方法
const arrayLike=document.querySelectorAll('div')// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)
实现防抖函数(debounce)
防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算
防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行
,节流是将多次执行变成每隔一段时间执行
eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
手写简化版:
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {// 缓存一个定时器idlet timer = 0// 这里返回的函数是每次用户实际调用的防抖函数// 如果已经设定过定时器了就清空上一次的定时器// 开始一个新的定时器,延迟执行用户传入的方法return function(...args) {if (timer) clearTimeout(timer)timer = setTimeout(() => {func.apply(this, args)}, wait)}
}
适用场景:
- 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
实现防抖函数(debounce)
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
// 防抖函数
const debounce = (fn, delay) => {let timer = null;return (...args) => {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};
};
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
生存环境请用lodash.debounce
实现Array.isArray方法
Array.myIsArray = function(o) {return Object.prototype.toString.call(Object(o)) === '[object Array]';
};console.log(Array.myIsArray([])); // true