前端手写题目(熟食)- 面试真题

910 阅读4分钟

接上一篇文章, 专科前端生存之路 ,最后的面试题的答案。

以下都是近期(10、11月)本人面试真题。整理出来,有多种实现方式,下面只是我的实现方法。可灵活参考。

手写Bind、Apple、Call 【美团、神策数据】

// Bind
Function.prototype.myBind = function (oThis) {
  var aArgs = [].slice.call(arguments, 1);
  var fun = this;
  return function () {
    fun.apply(oThis, aArgs.concat([].slice.call(arguments)));
  };
};
// call
Function.prototype.myCall = function(context) {
  context=context||window   
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  delete context.fn
  return result
}
// myApply
Function.prototype.myApply = function(context) {
  context = context || window
  context.fn = this
  let result
  if (arguments[1]) {
      result = context.fn(...arguments[1])
  } else {
      result = context.fn()
  }
  delete context.fn
  return result
}
 

手写debounce、throttle 【美团、头条】

function debounce(fn, ms) {
  let timer = null;
  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    let context = this;
    timer = setTimeout(() => {
      fn.apply(context, args);
      timer = false;
    }, ms);
  };
}

function throttle(fn, delay) {
  let timer = null;
  let flag = false;
  return function (args) {
    if (flag) {
      return;
    }
    let context = this;
    flag = true;
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
      flag = false;
    }, delay);
  };
}

基础算法题:冒泡、快速、插入、深度优先(DFS)、广度优先(BFS)、二分查找 【不知名的小公司】

function bubb(arr) {
  for (let i = 0; i <= arr.length; i++) {
    for (let j = 0; j <= arr.length; j++) {
      let item = arr[i];
      let item2 = arr[j];
      if (item < item2) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
    }
  }
  return arr;
}

let array = [4, 2, 5, 3, 1, 5, 9, 7, 5, 8, 6, 4];

function quick(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  let centerIndex = Math.floor(arr.length / 2);
  let center = arr.splice(centerIndex, 1)[0];
  let left = [];
  let right = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (item <= center) {
      left.push(item);
    } else {
      right.push(item);
    }
  }
  return [...quick(left), center, ...quick(right)];
}

function Insertion(arr) {
  let len = arr.length;
  let preIndex, current;
  for (let i = 0; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && current < arr[preIndex]) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = current;
  }
  return arr;
}

// 数据结构: 队列,先入先出
function bfs(node) {
  var nodes = [];
  if (node != null) {
    var queue = [];
    queue.push(node);
    while (queue.length != 0) {
      var item = queue.shift();
      // 操作当前节点 就是放到我们的目标数组中,这里你也可以做一些操作
      nodes.push(item);
      var children = item.children;
      // 将当前操作的节点的子节点 塞到队列的最后
      for (var i = 0; i < children.length; i++){
        queue.push(children[i]);
      }
    }
  }
  return nodes;
}

// 栈 先入后出
function DFS(node, nodeList) {
  if (node) {
    nodeList.push(node);
    var children = node.children;
    for (var i = 0; i < children.length; i++)
      //每次递归的时候将  需要遍历的节点  和 节点所存储的数组传下去
      DFS(children[i], nodeList);
  }
  return nodeList;
}

// 二分查找
function search(nums, target) {
  let low = 0,
      high = nums.length - 1,
      mid, 
      elem
  while(low <= high) {
      mid = Math.floor((low+high)/2)
      elem = nums[mid]
      if(elem < target) {
          low = mid + 1
      } else if(elem > target) {
          high = mid - 1
      } else {
          return mid
      }
  }
  return -1
}

console.log(search([-1,0,3,5,9,12],9));

合并区间乱序 【快手、美团】

let arr = [[1,3],[2,6],[8,10],[15,18]]

function merge(arr) {
  if (!arr || !arr.length) return [];
  arr.sort((a, b) => a[0] - b[0]); // 按照区间第一位进行排序
  let result = [arr[0]] // 排序之后第一个是最小的
  for (let i = 1; i < arr.length; i++) { // 从第二个开始比较
    let resultLast = result.length - 1
    if (result[resultLast][1] > arr[i][0]) { // 判断结尾是不是大于开始
      result[resultLast][1] = Math.max(result[resultLast][1], arr[i][1]) // 区间重复就进行合并了
    } else {
      result.push(arr[i]) // 区间没有重复
    }
  }
  return result
}

console.log(merge(arr));

连续子数组最大和【阿里】

function maxSum(arr = []) {
  let tempSum = 0;
  let maxSum = 0;
  for (let i = 0; i < arr.length; i++) {
      tempSum += arr[i];
      if (tempSum > maxSum) {
          maxSum = tempSum;
      } else if (tempSum < 0) {
          tempSum = 0;
      }
  }
  return maxSum;
}

console.log(maxSum([-2,1-3,4,-1,2,1,-5,4]));

千分位【阿里】

function format(str) {
  // 校验、小数点、负数 先切掉
  return str.split('').reduceRight((pre, next, index,origin) => {
    return (index % 3 ? next : next + ',') + pre;
  });
}

无重复字符串的最长子串


// 开始之前,我们将导致窗口开始收缩的那个值 称为 「亮」值
var lengthOfLongestSubstring = function(s) {
  // 窗口数组,可以使用对象代替,利用对象key值的唯一性更好
  const windowList = [];
  // 窗口的左右边界
  let left = 0;
  let right = 0;
  // 最终的子串最大值
  let max = 0;
  while (right < s.length) {
    if (!windowList.includes(s[right])) {
      // 扩大窗口有边界
      windowList.push(s[right]);
      right++;
    } else {
      while (left < right) {
        // 开始缩小左边界
        windowList.shift();
        left++;
        // 判断缩小的窗口内是否还包括 「亮」值
        if (!windowList.includes(s[right])) {
          break;
        }
      }
    }
    // 一直替换最大值
    max = Math.max(max, right - left);
  }
  return max
};

数组转树形对象【头条】

/**
* 数组转树  非递归求解
* 利用数组和对象相互引用  时间复杂度O(n)
* @param {Object} list
*/
function totree(list,parId) {
  let obj = {};
  let result = [];
  //将数组中数据转为键值对结构 (这里的数组和obj会相互引用)
  list.map(el => {
      obj[el.id] = el;
  })
  for(let i=0, len = list.length; i < len; i++) {
      let id = list[i].parentId;
      if(id == parId) {
          result.push(list[i]);
          continue;
      }
      if(obj[id].children) {
          obj[id].children.push(list[i]);
      } else {
          obj[id].children = [list[i]];
      }
  }
  return result;
}

数组拍平和对象拍平【快手】

function flattenArr(arr, depth = 1) {
  let res = [];
  for (let idx = 0; idx < arr.length; idx++) {
    let i = arr[idx];
    if (Array.isArray(i) && (depth > 0 || depth === Infinity)) {
      res.push(...flattenArr(i, --depth));
    } else {
      res.push(i);
    } 
  }
  return res;
}

function flatObj(data) {
  var result = {};
  function recurse(cur, prop) {
    // 如果输入进来的不是对象,就将其放在数组中,返回
    if (typeof cur !== 'object') {
      result[prop] = cur;
      // 如果输入进来的是数组,长度不为0就递归数组,得出结果
    } else if (Array.isArray(cur)) {
      for (var i = 0, len = cur.length; i < len; i++) {
        recurse(cur[i], prop + '[' + i + ']');
      }
      if (len == 0) {
        result[prop] = [];
      }
    } else {
      // 对象
      var isEmpty = true;
      for (var p in cur) {
        isEmpty = false;
        recurse(cur[p], prop ? prop + '.' + p : p);
      }
      if (isEmpty && prop) {
        result[prop] = {};
      }
    }
  }
  recurse(data, '');
  return result;
}

数组乱序

// 数组乱序
function shuffle(arr) {
    for (let i = arr.length; i; i--) {
        let j = Math.floor(Math.random() * i);
        [arr[i - 1], arr[j]] = [arr[j], arr[i - 1]];
    }
    return arr;
}

手写一个简易版的redux

// redux
function createStore(reducer) {
  // 默认初始状态
  let state;
  let listeners = []; // 存储所有的监听函数
  // 得到总的状态树
  function getState() {
    return state;
  }
  // 派发动作
  function dispatch(action) {
    // 得到新状态
    state = reducer(state, action);
    // 订阅
    listeners.forEach(fn => fn());
  }
  // 由于默认 state 树是没有状态的, 是一个控制,所以先派发一次action,给初始状态赋值,
  // 由于用户自己定义的reducer 默认返回初始状态,由于派发的这个type类型找不到,所以默认返回了初始值
  dispatch({ type: '@@REDUX_INIT' });
  // 添加发布订阅模式
  function subscribe(listener) {
    listeners.push(listener);
    // 返回一个取消订阅方法(就是个高阶函数)
    return function () {
      listeners = listeners.filter(fn => fn !== listener);
    };
  }
  return {
    getState,
    dispatch,
    subscribe,
  };
}

两数之和

var twoSum = function(nums, target) {
  const map = {}
  const len = nums.length
  for(let i=0;i<len; i++){
      const targetNum = target - nums[i];
      if(targetNum in map) return [map[targetNum], i]
      map[nums[i]] = i
  }  
};

合并有序数组【便利蜂、快手、美团】

var arr1 = [1, 2, 3, 6, 8];
var arr2 = [2, 6, 7];

var merge = function (nums1, m, nums2, n) {
  let count = m + n;
  while (m > 0 && n > 0) {
    nums1[--count] = nums1[m - 1] < nums2[n - 1] ? nums2[--n] : nums1[--m];
  }
  // 如果arr2开头最小
  if (n > 0) {
    nums1.splice(0, n, ...nums2.slice(0, n));
  }
};
merge(arr1, 5, arr2, 3);
console.log(arr1);

字符串全排列 【头条】

function permutate(str) {
  //保存每一轮递归的排列结果
  var result = [];
  //初始条件:长度为1
  if (str.length == 1) {
      return [str]
  } else {
      //求剩余子串的全排列,对每个排列进行遍历
      var preResult = permutate(str.slice(1));
      console.log(preResult);
      for (var j = 0; j < preResult.length; j++) {
          for (var k = 0; k < preResult[j].length + 1; k++) {
              //将首字母插入k位置 
              var temp = preResult[j].slice(0, k) + str[0] + preResult[j].slice(k);
              if(!result.includes(temp)){
                result.push(temp);
              }
          }
      }
      return result;
  }
}

console.log(permutate('aabc'));

实现一个并发控制的request【快手】

// 如果你会rxjs的话,这个一行代码就搞定
from(fetchList).pipe(mergeMap(res)=> res(),10).subscribe(cb)

// 普通版本
class Request {
  constructor(maxLimit) {
    this.maxLimit = maxLimit;
    // 等待的任务
    this.waitingQueue = [];
  }

  initRequest = reqArr => {
    this.waitingQueue.push(...reqArr)
  };
  // 执行下一个请求
  next = () => {
    if (this.waitingQueue.length > 0) {
      const next = this.waitingQueue.splice(0, 1);
      this.request({
        ...next[0],
      });
    }
  };
 // init->开始请求
 run = () => {
  // 不满足limit
  const list = this.waitingQueue.splice(0, this.maxLimit);
  for (let i = 0; i < list.length; i++) {
    this.request({
      ...list[i],
    });
  }
};
  // 主请求方法
  request = ({ method, api, params, onSuccess = () => {}, onError = () => {} }) => {
    // ...
    fetch(api, options)
      .then(data => {
        onSuccess(data);
      })
      .catch(err => {
        onError(err);
      })
      .finally(() => {
        this.next();
      });
  };
  pushRequest = (method, api, params, onSuccess, onError) => {
    this.waitingQueue.push({
      method,
      api,
      params,
      onSuccess,
      onError,
    });
  };
 

  checkStatus = response => {
    if (response.ok) {
      return response;
    } else {
      let error = new Error(response.statusText);
      error.status = response.status;
      throw error;
    }
  };

  parseJSON = response => {
    return response.json();
  };



  getSearchFromObject = param => {
    if (!param) return '';
    var str = [];
    for (var p in param)
      if (param.hasOwnProperty(p)) {
        str.push(encodeURIComponent(p) + '=' + encodeURIComponent(param[p]));
      }
    return '?' + str.join('&');
  };

}

实现一个多维数组代理器 【快手】


// proxy
let arr = [{ name: 1, age: 2 }];

function deepProxy(obj, cb) {
  if (typeof obj === 'object') {
    for (let key in obj) {
      if (typeof obj[key] === 'object') {
        obj[key] = deepProxy(obj[key], cb);
      }
    }
  }
  return new Proxy(obj, {
    set: function (target, key, value, receiver) {
      console.log(`arr name orivalue= ${target[key]}, curValue=${value}`);
      // let cbType = target[key] == undefined ? 'create' : 'modify';
      // //排除数组修改length回调
      // if (!(Array.isArray(target) && key === 'length')) {
      //   cb(cbType, { target, key, value });
      // }
      return Reflect.set(target, key, value, receiver);
    },
  });
}

let p = deepProxy(arr, (type, data) => {
  console.log(type, data);
});
p[0].name = 3;

实现一个lazyman 【阿里】

function _LazyMan(name) {
  this.tasks = [];
  let self = this;
  let fn = (name => {
    return () => {
      console.log(`hi,this is ${name}`);
      self.next();
    };
  })(name);
  this.tasks.push(fn);
  setTimeout(() => {
    self.next();
  }, 0);
}

_LazyMan.prototype.next = function () {
  let fn = this.tasks.shift();
  fn && fn();
};

_LazyMan.prototype.eat  =function (name) {
  let self = this;
  let fn = ((name)=>{
    return ()=>{
      console.log(`eat:${name}`);
      self.next();
    }
  })(name);
  this.tasks.push(fn);
  return this;
}

_LazyMan.prototype.sleep = function (time) {
  let self = this;
  let fn = ((time)=>{
    return ()=>{
      setTimeout(() => {
        console.log(`sleep:${time}s`);
        self.next();
      }, time*1000);
    }
  })(time)
  this.tasks.push(fn);
  return this;
}

function LazyMan(name) {
  return new _LazyMan(name);
}

LazyMan('愚墨').eat('午饭').sleep(3).eat('晚饭')

深克隆【不知名的小公司】


/**
 * js深拷贝(包括 循环引用 的情况)
 * 
 * @param {*} originObj
 * @param {*} [map=new WeakMap()]  使用hash表记录所有的对象的引用关系,初始化为空
 * @returns
 */
function deepClone( originObj, map = new WeakMap() ) {
  if(!originObj || typeof originObj !== 'object') return originObj;  //空或者非对象则返回本身

  //如果这个对象已经被记录则直接返回
  if( map.get(originObj) ) {
      return  map.get(originObj);
  }
  //这个对象还没有被记录,将其引用记录在map中,进行拷贝    
  let result = Array.isArray(originObj) ? [] : {};  //拷贝结果
  map.set(originObj, result); //记录引用关系
  let keys = Object.keys(originObj); //originObj的全部key集合
  //拷贝
  for(let i =0,len=keys.length; i<len; i++) {
      let key = keys[i];
      let temp = originObj[key];
      result[key] = deepClone(temp, map);
  }
  return result;
}

以上是面试的几个公司的笔试题目,下一篇文章分享各个公司的面试题目