写点什么

美团前端一面手写面试题

  • 2023-02-19
    浙江
  • 本文字数:9214 字

    阅读完需:约 30 分钟

实现斐波那契数列

// 递归function fn (n){    if(n==0) return 0    if(n==1) return 1    return 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)
复制代码


大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisherDeveloperObserver),全程只有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 和 value      val = 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)); // 输出3console.log(getValue(array, "[0].a.b[0]", 12)); // 输出 1console.log(getValue(array, "[0].a.b[0].c", 12)); // 输出 12
复制代码


实现


/** * 测试属性是否匹配 */export function testPropTypes(value, type, dev) {  const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaN  const 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); // darkconsole.log(data.theme); // dark
复制代码


以上代码运行后,控制台会输出以下代码:


darkdark
复制代码


通过观察以上的输出结果可知,使用 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.fromArray.from(arrayLike)// 3.Array.prototype.sliceArray.prototype.slice.call(arrayLike)// 4.Array.applyArray.apply(null, arrayLike)// 5.Array.prototype.concatArray.prototype.concat.apply([], arrayLike)
复制代码

实现防抖函数(debounce)

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算



防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行


eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。


手写简化版:


// func是用户传入需要防抖的函数// wait是等待时间const debounce = (func, wait = 50) => {  // 缓存一个定时器id  let 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
复制代码


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
美团前端一面手写面试题_JavaScript_helloworld1024fd_InfoQ写作社区